ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker Compose部署EMQX性能优化与排坑指南

Docker Compose部署EMQX性能优化与排坑指南 用 Docker Compose 跑 EMQX 的教程一搜一大把但很多人只写到容器起来就结束镜像一拉、端口一映射完事。结果线上设备一多连接数冲到几千就上不去消息吞吐明显下降最后全甩锅给 EMQX。我这次在项目里重构 IoT 接入层同样用 Docker Compose 部署了 EMQX并专门花时间把系统内核、容器资源限制、EMQX 的监听器和消息队列配置整体理了一遍。这篇内容就是这次部署和性能优化的完整记录重点讲清楚哪些参数真正值得动、哪些默认值不用改以及遇到连接数上不去、内存暴涨、Compose 命令不识别这些坑时该怎么处理。如果你是正在做 IoT 平台、用 EMQX 做设备消息接入或者已经用 Docker Compose 把所有服务编排好、但不知道该往 EMQX 里塞哪些性能参数这篇内容应该能帮你省下不少试错时间。1. 为什么选 Docker Compose 作为 EMQX 的部署方式1.1 先把基础设施边界画清楚EMQX 是一个用 Erlang/OTP 写的 MQTT 消息代理设备接入、主题订阅、消息路由这些核心工作都在它身上。它跟普通 Web 服务不太一样连接是长连接单节点经常要扛几万甚至几十万条 TCP 连接而且消息是持续流动的不是请求一下响应一下就完事。所以部署 EMQX 时不能只想着“把容器跑起来”。容器只是把你的应用进程包起来网络、文件描述符、内核 TCP 参数、磁盘 IO这些仍然依赖宿主机。换句话说Docker 负责的是可移植性和可重复性性能优化还得从宿主机和容器两层入手。我选择 Docker Compose核心原因是它刚好卡在“裸机部署”和“Kubernetes 重武器”之间。单机或少量几台机器时Compose 能把镜像版本、端口、环境变量、数据卷都写成一个文件和代码一起提交、一起回滚省掉了手动装依赖的麻烦。而且团队其他人拉下来就能起一套一样的环境。1.2 裸机、Compose、K8s 三选一我在这次项目里没有直接上 Kubernetes原因很简单业务量还没到需要自动扩缩容的程度K8s 的运维成本反而会吃掉产品迭代的精力。下面这个对比是我自己权衡时画的供你参考。部署方式优势需要解决的问题裸机手动部署参数调整最直接没有容器层干扰环境一致性差升级回滚都靠手工难以复现Docker Compose 单机配置版本化、启动快、资源限制清晰需要处理 ulimit、网络模式、内核参数集群能力弱Kubernetes 集群弹性伸缩、故障恢复、服务发现完善运维复杂EMQX 集群还要处理节点间网络和持久化问题我的结论是第一版先用 Compose 把单节点跑稳定把性能压明白等连接数到了单节点承载极限再往集群方向演进。这个路径对大多数中小团队来说是最务实的。2. 性能优化的起点宿主机和容器的资源底座2.1 连接数为什么上不去文件描述符和内核参数这是很多 Docker Compose 部署 EMQX 翻车的第一大原因。Linux 下每一条 TCP 连接都是一个文件描述符默认的ulimit -n经常只有 1024这意味着你最多只能打开 1024 个文件随便压个两三百个 MQTT 连接就到顶了。我实际操作时会先把下面这些内核参数写进/etc/sysctl.conf然后执行sysctl -pfs.file-max 2000000 fs.nr_open 2000000 net.core.somaxconn 65535 net.core.netdev_max_backlog 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65000 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 15fs.file-max是系统级文件描述符上限fs.nr_open是单个进程能打开的最大文件数上限。somaxconn和tcp_max_syn_backlog影响半连接队列和全连接队列长度EMQX 这种高并发接入服务如果队列太短突发连接会直接被内核丢掉。接着还要改/etc/security/limits.conf* soft nofile 1048576 * hard nofile 1048576 root soft nofile 1048576 root hard nofile 1048576改完建议重启 Docker 让守护进程继承新限制否则你在宿主机改了ulimitDocker 起来时没带上等于白改。2.2 Ubuntu 上安装 Docker Compose 的正确姿势如果服务器还没有 Docker ComposeUbuntu 上的安装其实很简单。我自己一般用官方源或者系统源二选一sudo apt update sudo apt install docker.io docker-compose-v2 docker compose version如果你装的是 Docker 官方仓库那对应的包是docker-compose-pluginsudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker compose version安装完一定要先用docker compose version验证。很多老一点的服务器上只有旧版 Python 写的docker-compose输入docker compose时就会报docker: unknown command: docker compose。这个坑很典型我放在后面的排查章节详细说。2.3 磁盘、文件系统和数据卷选型EMQX 的高频操作是消息路由和会话内存管理但数据目录里的持久化文件、日志写入和规则引擎落库仍然会产生磁盘 IO。Compose 里我坚持用命名卷不要图省事直接 bind mount 到 NFS、SMB 或者虚拟机的共享目录上。原因很直接NFS 这类网络存储延迟高高并发下写日志和写数据容易把 EMQX 的消息收发拖垮。如果你是在群晖 NAS 里跑 EMQX也需要格外注意磁盘 IONAS 的机械盘加网络卷组合并不适合高吞吐场景。另外 Docker 默认的json-file日志驱动会把容器日志无限写下去我习惯在/etc/docker/daemon.json里把日志切割做掉顺便给所有容器一个合理的默认nofile{ default-ulimits: { nofile: { Name: nofile, Hard: 1048576, Soft: 1048576 } }, log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }改完systemctl restart docker。注意default-ulimits会影响所有容器如果同一个宿主机上还跑着其他服务建议在 Compose 文件里单独给 EMQX 配ulimits而不是全局都拉高。3. Docker Compose 编排文件与部署细节3.1 一份可用的 Compose 配置下面这份配置是我这次项目实际用的EMQX 版本用的 5.8.2没有直接写latest避免镜像更新后行为变化services: emqx: image: emqx/emqx:5.8.2 container_name: emqx restart: unless-stopped ulimits: nofile: soft: 1048576 hard: 1048576 environment: EMQX_NAME: emqx127.0.0.1 EMQX_NODE__PROCESS_LIMIT: 2097152 EMQX_NODE__MAX_PORTS: 1048576 EMQX_LISTENERS__TCP__DEFAULT__BIND: 0.0.0.0:1883 EMQX_LISTENERS__TCP__DEFAULT__MAX_CONNECTIONS: 1024000 EMQX_MQTT__MAX_MQUEUE_LEN: 10000 EMQX_LOG__FILE__LEVEL: warning EMQX_DASHBOARD__DEFAULT_USERNAME: admin EMQX_DASHBOARD__DEFAULT_PASSWORD: ChangeMe123 ports: - 1883:1883 - 8083:8083 - 8084:8084 - 8883:8883 - 18083:18083 volumes: - emqx-data:/opt/emqx/data - emqx-log:/opt/emqx/log healthcheck: test: [CMD, emqx, ctl, status] interval: 15s timeout: 10s retries: 5 volumes: emqx-data: driver: local emqx-log: driver: local说一下几个关键点。ulimits.nofile对应容器内进程能打开的文件描述符数量EMQX 官方建议高并发场景至少给到 1048576。EMQX_NODE__PROCESS_LIMIT和EMQX_NODE__MAX_PORTS是 Erlang VM 层面的限制很多人只调了 Linux 的ulimit忘了 Erlang 自己还有一层限制连接数照样上不去。端口映射这块1883 是 MQTT 普通 TCP8883 是 TLS8083 是 WebSocket8084 是 WebSocket over TLS18083 是 Dashboard。如果暂时用不到 TLS 和 WS可以不放出来少一个监听器就少一份资源消耗。3.2 端口映射与网络模式怎么选Compose 默认用的是 bridge 网络通过ports做端口映射。这种方式的好处是隔离性好一台机器上可以跑很多个容器互不干扰坏处是流量会经 iptables 和 NAT 转发高连接数下有一定开销。如果 EMQX 独占一台机器可以考虑network_mode: host让容器直接复用宿主机网络栈。这样少一层转发抓包也方便压测时连接数上限更容易顶满。不过 host 模式下就不能再用ports端口冲突也得自己管理Compose 不会帮你检查。我这次没直接上 host是因为同一台机器还要跑 Nginx 和监控服务bridge 的隔离粒度更适合我。另外一个容易踩的细节是端口映射时别写成127.0.0.1:1883:1883那只会让本机访问外部设备连不上。我总是先写1883:1883确认部署完能通再按安全要求收紧。3.3 数据卷、日志和健康检查数据卷分开挂载一个给/opt/emqx/data一个给/opt/emqx/log这样做方便备份和日志清理。升级镜像前把数据卷打个快照就不会出现升级完会话数据丢光的情况。健康检查我用的是emqx ctl status这个命令会在 EMQX 正常时返回Node is running。Docker 默认的HEALTHCHECK可以在 Compose 里通过healthcheck写清楚之后再用docker compose ps看容器状态时就能看到healthy而不是一直显示running。4. EMQX 性能优化参数逐项拆解4.1 Erlang 节点层参数决定了连接规模的天花板EMQX 底层是 Erlang/OTP所有连接都被抽象成 Erlang 进程和 Erlang 端口。理解这一点后再看官方文档里的那些参数会通透很多。node.process_limit对应环境变量EMQX_NODE__PROCESS_LIMIT。每个 MQTT 连接、每个会话、每条正在处理的 QoS 消息都可能占据 Erlang 进程连接数一旦超过 process_limitEMQX 就会拒绝新连接甚至直接出错。高并发场景下我会把它设成预期连接数的 2 倍左右比如预期 10 万连接就设 2097152。node.max_ports对应环境变量EMQX_NODE__MAX_PORTS。注意这里的端口不是 TCP 端口而是 Erlang VM 里的“端口对象”TCP socket 也会占用。默认值不足时现象是连接到一个数量级后无论如何都上不去而且 Dashboard 上不会显示明确错误。我建议直接把EMQX_NODE__MAX_PORTS设成 1048576 以上避免这种隐性瓶颈。还要提醒一个老教程陷阱EMQX 4.x 的环境变量是单下划线风格比如EMQX_NODE_PROCESS_LIMIT5.x 改成了双下划线风格EMQX_NODE__PROCESS_LIMIT。网上很多资料还在用旧格式照抄到 5.x 上完全不生效这是低版本到高版本迁移时最常见的配置问题。4.2 监听器和 TCP 层参数监听器配置对应EMQX_LISTENERS__TCP__DEFAULT__*这一组环境变量。最核心的三个是参数作用我的建议BIND监听器绑定的 IP 和端口高可用场景绑0.0.0.0:1883单机隔离场景可以绑内网 IPMAX_CONNECTIONS单个监听器最大连接数根据业务预期设别默认无上限否则被刷连接时很难受BACKLOGTCP 监听队列长度建议和内核somaxconn一起调大突发连接不容易丢MAX_CONNECTIONS我建议显式设置哪怕设一个比较大的数。原因是你需要知道自己的容量边界在哪里等它到顶时告警能看到一个明确指标而不是进程莫名拒绝连接。如果 EMQX 前面还有负载均衡器或反向代理TCP 层面的proxy_protocol也要考虑。开启后EMQX 能从 proxy protocol 头里拿到真实客户端 IP避免所有流量都显示成代理 IP导致基于 IP 的认证或限流全部失效。这个配置不在性能优化范围里但它影响生产环境的功能正确性值得顺手检查。4.3 MQTT 会话和消息队列怎么调MQTT 层的性能和业务模型强相关不要盲目调大所有参数。EMQX_MQTT__MAX_MQUEUE_LEN控制的是每个会话在服务端的消息队列长度对应 QoS 1/2 消息没有得到确认时积压的最大条数。默认值通常够用但如果设备端经常不在线、消息又不断投递队列会被打满超出部分会丢弃。调大的代价非常明确内存上涨。我通常根据单条消息大小和设备离线时长估算撑峰值的内存再决定开多少比如单条 1KB 消息、离线 10 分钟、每秒来 1000 条那队列至少要能装 60 万条内存预算是 600MB 上下。session_expiry_interval也要看业务。如果大量设备连上后很快就断又不清理会话EMQX 内存会被僵尸会话吃光。我会把过期时间设置成一个合理业务周期比如 2 小时避免无限制保留。QoS 的max_inflight控制一个客户端最多同时有多少未确认的 QoS 1/2 消息。这个值不是越大越好很多客户端库默认值就很合适盲目调高只是把压力往后端推。4.4 规则引擎、认证和日志的取舍EMQX 自带规则引擎和数据桥接功能很强但不是每个部署都需要。用不上就关掉省下 CPU 和内存用上了也要注意规则引擎里如果接外部数据库或 HTTP 服务下游慢会拖慢消息处理链路因为存在背压机制。日志是性能杀手的一个隐藏点。EMQX 默认日志级别可能是 info消息量大的时候会把大量收发事件写进日志文件直接拉高磁盘 IO。我把EMQX_LOG__FILE__LEVEL设成warning只保留故障信息需要排查时再临时调到 debug。下面是我整理的一个速查表可以对照检查优化项配置示例解决什么问题注意风险容器文件描述符ulimits.nofile1048576连接数到了一定程度就连不上无Erlang 进程限制EMQX_NODE__PROCESS_LIMIT2097152新连接被拒、进程耗尽太高会更快耗尽内存建议 2 倍连接数Erlang 端口限制EMQX_NODE__MAX_PORTS1048576socket 达到上限后连环报错与连接数匹配即可监听器连接数MAX_CONNECTIONS1024000被异常流量打满设一个可告警的上限消息队列长度EMQX_MQTT__MAX_MQUEUE_LEN10000离线设备消息被丢弃过多内存按队列长度线性增长日志级别EMQX_LOG__FILE__LEVELwarning日志 IO 挤占业务资源排查问题时需要临时降级到 debug这些参数之间不是孤立的文件描述符再高Erlang 端口不够照样连接失败Erlang 内存再大容器日志无限写磁盘 IO 也会拖垮吞吐。调优时一定要把系统层和应用层放在一条链路里看。5. 压测过程与优化前后的对比5.1 压测工具和场景怎么定压测工具我用了一个非常简单的 Python 脚本基于paho-mqtt方便复现。没有直接依赖重型压测平台因为我的场景很明确先测连接建立能力再测简单发布订阅吞吐。连接建立场景的核心代码如下import paho.mqtt.client as mqtt import threading import time HOST 127.0.0.1 PORT 1883 TOTAL 20000 conn_count 0 lock threading.Lock() clients [] def on_connect(client, userdata, flags, rc): global conn_count if rc 0: with lock: conn_count 1 start time.time() for i in range(TOTAL): c mqtt.Client(client_idfbench-{i}, protocolmqtt.MQTTv311) c.on_connect on_connect c.connect(HOST, PORT, keepalive60) c.loop_start() clients.append(c) if (i 1) % 1000 0: print(f已发起 {i 1} 个连接) while conn_count TOTAL: time.sleep(0.5) print(f{TOTAL} 个连接建立耗时: {time.time() - start:.1f}s) time.sleep(10) for c in clients: c.disconnect() c.loop_stop()消息吞吐测试我用了另一套思路订阅端开 200 个客户端订阅同一个主题发布端用一个客户端连续发 2 万条 QoS 0 消息统计订阅端收到的总数和时间。这样能很直观地看出 broker 的路由转发能力。5.2 需要先排掉的压测自身坑压测机最好不要和 EMQX 在同一台机器。我一开始图方便直接在宿主机上跑脚本结果 EMQX 还没到瓶颈压测机的 CPU 先被 Python 的几千个线程打满了指标全是失真的。还有一个非常隐蔽但几乎必踩的坑EADDRNOTAVAIL。当压测机是一台普通服务器时它本地的临时端口范围默认只有几千个几万个客户端同时从这个 IP 建连源端口很快耗尽。压测机上也要调大net.ipv4.ip_local_port_range否则你会误判成 EMQX 的问题。5.3 优化前后的实测记录我用的测试机是 4 核 8GUbuntu 系统Docker 24EMQX 5.8.2。第一轮直接用默认配置启动容器第二轮应用了系统内核参数、容器ulimits、Erlang 节点参数和前面说的 MQTT 参数。数据记录在下面压测项默认配置调优后说明2 万连接建立耗时约 52 秒约 34 秒中途默认配置下出现过连接失败连接后emqx ctl status响应卡顿数秒秒级返回说明 Erlang VM 负载明显下降2 万条消息端到端接收约 13 秒约 6 秒QoS 01KB payload简单订阅场景Broker 内存峰值接近 2GB约 1.4GB去掉不必要日志和消息积压后下降明显不要把这些数字当成绝对值硬件不同、消息 payload 不同结果都会变。我要强调的是趋势默认配置下先撞到的是 Erlang 进程和文件描述符瓶颈调优后瓶颈转移到了压测机本地端口和 CPU 上这本身就说明 EMQX 的容量被释放出来了。5.4 数据说明什么跑完压测我最大的感受是EMQX 本身的性能底子很厚大多数“连不上、吞吐低”的问题出在部署环境没有配合。比如连接数到 2 万就报错你先查的不是 EMQX 版本而是docker exec emqx bash -c ulimit -n和docker exec emqx emqx ctl status的状态。调优后的单节点在我的测试机上是能稳定撑住 10 万长连接的但前提是压测机不再是瓶颈。生产环境如果设备数量不到这个量级参数不需要全部拉满留 20% 余量反而是健康的状态。6. 常见问题与排查技巧实录6.1docker: unknown command: docker compose这个报错几乎是新服务器部署必遇。原因是 Docker 客户端里没有 Compose 插件或者 Docker 版本太旧。先确认docker --version docker compose version如果docker compose version报 unknown command就直接安装插件包。Ubuntu 系统安装docker-compose-v2或者 Docker 官方仓库的docker-compose-plugin后重新打开终端验证即可。我遇到过一个特殊场景插件装了但用户用的是 root 账号加自定义 shell 环境PATH里没有 Docker 插件目录导致docker compose找不到。这种情况直接用绝对路径调用或修一下PATH就能解决。6.2 容器内文件描述符没生效宿主机ulimit已经改到 1048576但容器内还是 1024。这种问题常见于 Docker 守护进程没有重启或者 Compose 文件里没有写ulimits。一定要在容器内确认docker exec emqx bash -c ulimit -n docker exec emqx bash -c cat /proc/1/limits看到Max open files是 1048576 才算真正生效。我习惯在部署脚本里把这个检查写进去避免手动部署时漏掉。6.3 连接数高时EADDRNOTAVAIL如果你已经是压测场景先看压测机而不是 EMQX。EADDRNOTAVAIL通常意味着本地端口耗尽调大压测机的net.ipv4.ip_local_port_range或者把压测脚本分散到多台机器执行。生产环境设备都在不同 IP一般不会遇到这个问题但压测环境一定要考虑到。6.4 连接不稳定、频繁掉线先检查客户端的keepalive设置和 EMQX 的 keepalive 检查机制是否匹配。有的设备把 keepalive 设成 60 秒但网络链路做 NAT 超时会先断掉连接设备又没有按 MQTT 规范重连表现就是“看起来经常掉线”。另一个原因是 listener 的 backlog 太小。大量设备同一时间重连时TCP 连接会在内核队列里堆积客户端表现为建连超时。对应解法是把内核somaxconn和 EMQX listener 的backlog同时调大。6.5 内存上涨和 OOMKilled内存上涨最常见的原因有三个消息队列积压、会话没清理、日志无限制增长。先看 Dashboard 里当前会话数和队列长度再结合docker logs判断日志量。如果容器被 OOMKilled大多数时候不是 EMQX 内存泄漏而是你没给它留足内存预算。我给容器设内存上限时一直很谨慎。EMQX 是 Erlang VM内存管理有自己的伸缩机制限制太低会直接触发 OOM。建议要么不设硬限制只做监控告警要么设一个明显高于常规峰值的内存 limit给 VM 留出余量。6.6emqx ctl status卡住这通常说明 broker 已经很忙或者节点在做大型 GC。排查时先看docker stats的 CPU 和内存曲线如果 CPU 持续 100%说明消息路由或规则引擎是热点如果内存持续上涨优先检查消息队列。再不行就抓docker logs看有没有 Erlang 进程崩溃或out of memory相关日志。7. 维护、监控与后续扩展思路7.1 日常监控看哪几个指标监控不要贪多我先盯三个核心指标当前连接数、消息收发速率、内存使用率。EMQX Dashboard 自带这些指标部署完先看它确认基线。等规模再大一点再上 Prometheus 和 Grafana。EMQX 提供 Prometheus 指标接口把指标拉取写到监控里配合告警规则比每天登录 Dashboard 看一遍高效得多。告警阈值我的建议是连接数达到上限 80% 时先预警内存超过总内存 70% 时开始查消息队列CPU 持续超过 80% 优先看规则引擎。7.2 升级和备份用 Compose 升级很方便但 EMQX 升级前一定要先备份数据卷。我的操作顺序是docker compose down docker run --rm -v emqx-data:/data -v /backup:/backup alpine tar czf /backup/emqx-data.tar.gz -C /data . docker compose pull emqx docker compose up -d升级后马上看健康状态观察 10 分钟连接数和消息速率确认没有回归再结束。镜像 tag 不要一直用latest锁死一个版本升级时主动变更 tag日志里才看得出改动。7.3 集群扩展的下一步单节点撑不住后EMQX 可以走集群扩展。集群模式下要解决两件事节点发现机制和负载均衡入口。EMQX 5.x 支持静态节点发现Compose 里把多个 EMQX 服务通过同一个EMQX_NODE__COOKIE连接起来然后在前面加负载均衡器或者 DNS 轮询设备连接会分散到多个节点上。不过集群不是把事情变简单而是重新定义复杂度。数据备份、节点间消息路由、客户端重连策略都要重新验证。我会在单节点性能稳定后再专门做一轮集群压测。7.4 我最后想说的经验这次 Docker Compose 部署 EMQX 的性能优化做下来我的体会是大多数参数不该照搬社区配置而应该从连接数、消息量、消息大小、设备在线率这些业务数据倒推。我实际调过的参数里有一半最后又调回了接近默认值的状态因为业务根本没到那个量级。先搞清楚你的设备连接模型是什么再决定开多高的上限。上线前留出 20% 的资源余量压测时把压测机的瓶颈排除掉遇到问题时按“系统文件描述符、Erlang VM、监听器连接数、消息队列”这个顺序排查基本能覆盖 90% 的 EMQX 容器化性能问题。
返回列表