EMQX生产环境部署脚本:持久化与systemd自启动实战 先说一件我自己吃过亏的事。两年前第一次在生产环境部署 EMQX我图省事用docker run一条命令把 emqx 拉起来Dashboard 能开、客户端能连当时觉得部署这事就算翻篇了。直到某天机房断电重启容器被 Docker 自动拉起打开 Dashboard 一看之前建的账号、认证规则、保留消息全没了连节点名都变了。查了一圈文档才明白EMQX 的元数据落在容器可写层里容器一删、/var/lib/docker一清理数据就彻底蒸发。从那时候起我给自己定了一条规矩任何环境部署 EMQX必须用 shell 脚本把安装、自启动、持久化三件事一起固化下来绝不裸跑。这篇就记录我是怎么写的以及为什么这样写。适合刚上手 EMQX 的运维、准备上生产环境的开发者还有想把手动部署变成一条命令的同学。你不需要是 shell 高手我会把脚本里每一条命令的意图都摊开讲清楚你拿去改改路径就能用。1. 为什么要把部署写成脚本一次重启引发的“丢数据”事故1.1 手动部署最大的问题不是慢是记不住很多人第一次装 EMQX 都是照着官网文档敲下载、解压、./bin/emqx start完事。放在测试机没问题但生产环境一旦多起来问题就暴露了每次部署要重新查一遍文档版本还不一定对得上有人用docker run裸跑有人用 tar 包解压有人用apt install环境之间差异巨大装完忘记systemctl enable服务器一重启服务就再也不起来了数据目录用默认的/var/lib/emqx某天做磁盘清理直接给删了。这些坑我几乎都踩过一遍。手动步骤越多越容易漏而漏掉的往往是最要命的那一步持久化和自启动。写成 shell 脚本之后整个部署过程变成一条命令而且脚本是幂等的——跑一次是装好跑十次也是装好不会重复创建目录、重复写入配置。这个特性在批量初始化环境时特别值钱。1.2 脚本到底要解决哪三件事这篇标题里其实已经点明了核心我就按这三个目标拆部署从零装好 EMQX锁定版本而不是永远“最新版”自启动注册成 systemd 服务开机自动拉起崩溃自动重启持久化把数据目录挪到独立磁盘或指定路径配置文件、账号、保留消息、会话元数据在重启后都还在。三者缺一不可。只做自启动不做持久化重启后服务是起来了但数据像失忆一样清零只做持久化不做自启动宕机后还要人工登录服务器敲systemctl start那自动化就白搭了。脚本的价值在于把这三件事放进同一个流程里保证每次部署的结果一致。2. EMQX 的数据到底存在哪持久化前先摸清目录结构2.1 三个默认目录各管什么用官方 deb 包安装完 EMQX 5.x 之后系统里会有三个关键路径路径内容重启/重装后/etc/emqx静态配置文件比如emqx.conf保留但容易被人改动/var/lib/emqx运行时数据Mnesia 数据库、生成的配置、账号、ACL、规则、保留消息等默认保留但容器场景下极易丢失/var/log/emqx运行日志可丢失但不影响功能大多数人知道/etc/emqx是配置但不一定清楚/var/lib/emqx里那个 Mnesia 才是 EMQX 真正的心脏。Mnesia 是 Erlang 自带的分布式数据库EMQX 用它存集群元数据、客户端会话信息、路由表、认证数据、保留消息这类需要跨重启保留的东西。你可以把它理解成一个嵌入式的迷你数据库而它所有数据文件都在node.data_dir指定的目录下面。2.2 哪些数据会丢哪些本来就不该指望靠磁盘这里必须说实话EMQX 的普通消息默认是内存态客户端发完就投递不落盘。QoS 1/2 的离线消息、会话续传取决于会话持久化配置不是默认全量往磁盘写的。所以不要指望装个 EMQX 就自动拥有“消息不丢”的数据库级保证。真正会丢的是这些控制台创建的账号、密码认证与授权ACL规则Dashboard 配置、规则引擎里的规则、数据桥接配置保留消息retained message集群节点名、cookie、集群状态元数据。上面这些都落在 Mnesia 里跟着/var/lib/emqx走。容器场景为什么最容易丢因为容器可写层是临时的容器删除、镜像重建、docker system prune都会让这个目录消失。即使容器还在如果升级镜像时内部路径变了老数据也可能读不回来。2.3 所以持久化的本质是什么持久化的本质就是把 EMQX 依赖的少量但关键的数据目录放到一个不会被误删、不会随容器生命周期变化、最好在独立磁盘上的固定位置。目录结构上node.data_dir下面会有mnesia、configs、certs、patches这些子目录。configs/cluster.hocon这个文件尤其重要它是 EMQX 启动时根据主配置生成的运行时集群配置里面包含了节点名、集群发现方式等内容。迁移数据目录时整个data_dir最好整体搬走别只复制一个mnesia子目录否则容易出现配置和数据库对不上号的问题。注意我见过有人在脚本里只把mnesia目录挪走结果configs还在老地方启动后节点名冲突、集群状态错乱排查了半宿。要搬就整个目录搬别搞“部分迁移”。3. systemd 自启动的底层逻辑从 enable 到开机拉起3.1 为什么不用 rc.local、不用 crontab reboot“自启动”最粗暴的实现是往/etc/rc.local里塞一行启动命令或者用 crontab 的reboot伪装开机任务。这两种方式不是不能用但有一堆隐性毛病rc.local在很多新系统上默认没有执行权限得自己折腾crontabreboot在 SSH 级、用户级环境里的加载时机非常迷环境变量不完整经常莫名其妙失败两者都没有“进程崩溃后自动拉起”的能力没有统一的日志、状态管理排错只能靠 ps 看进程。systemd 是当前所有主流 Linux 发行版的标准 init 系统EMQX 官方 deb/rpm 包安装完就会带一个/lib/systemd/system/emqx.service单元文件这就是我们做自启动的基础。3.2 emqx.service 单元文件里有什么单元文件的大致结构是这样的[Unit] DescriptionEMQX Afternetwork.target Wantsnetwork.target [Service] Typesimple ExecStart/usr/bin/emqx start ExecStop/usr/bin/emqx stop Restarton-failure LimitNOFILE1048576 [Install] WantedBymulti-user.targetAfternetwork.target表示等网络就绪后再启动避免启动时因为网卡还没配好导致监听地址绑定失败。Typesimple表示/usr/bin/emqx start这个进程直接就是服务主进程。Restarton-failure是关键进程异常退出时 systemd 会自动帮你拉起来这比任何自启动脚本都稳。WantedBymulti-user.target则决定了开机时机系统进入多用户模式也就是正常开机后的完整系统状态时会读取这个声明把服务拉起来。你可以理解为系统开机流程里登记了一个“到点叫我”的名单。3.3 systemctl enable 到底做了什么很多人以为systemctl enable是“设置开机启动”这个抽象动作其实它的物理操作非常简单在/etc/systemd/system/multi-user.target.wants/目录下创建一个指向/lib/systemd/system/emqx.service的符号链接。开机时 systemd 扫描这个目录发现链接就启动对应服务。因为机制这么直白所以脚本里可以放心多次执行systemctl enable符号链接已存在时它会静默跳过不会报错。这一点保证了我们部署脚本的幂等性。提示systemctl enable --now emqx等于enable加start一步到位。但在脚本里我倾向于分开写因为先enable再daemon-reload再start每一步失败都能定位得更清楚。4. deploy_emqx.sh 逐段拆解安装、迁移、自启动一次完成4.1 完整脚本先放这儿下面是完整脚本基于 Ubuntu/Debian 系、EMQX 5.x我默认版本 5.8.3。你直接存成deploy_emqx.shsudo bash deploy_emqx.sh就能跑。#!/usr/bin/env bash # # deploy_emqx.sh # 功能安装 EMQX 5.x、配置数据持久化目录、注册 systemd 自启动 # 用法sudo bash deploy_emqx.sh [版本号] # 示例sudo bash deploy_emqx.sh 5.8.3 # 适用Ubuntu / Debianamd64 / arm64 # set -euo pipefail EMQX_VERSION${1:-5.8.3} OS_CODENAME$(. /etc/os-release echo ${VERSION_CODENAME:-unknown}) ARCH$(dpkg --print-architecture 2/dev/null || echo amd64) DATA_DIR/data/emqx LOG_DIR/data/emqx/log log_info() { printf \033[32m[INFO]\033[0m %s\n $*; } log_error() { printf \033[31m[ERROR]\033[0m %s\n $* 2; } # ---------- 0. 前置检查 ---------- if [ $(id -u) -ne 0 ]; then log_error 请使用 root 或 sudo 执行本脚本 exit 1 fi # ---------- 1. 安装 EMQX ---------- if command -v emqx /dev/null 21; then log_info 已检测到 EMQX$(emqx version | head -n 1) else DEB_NAMEemqx-${EMQX_VERSION}-${OS_CODENAME}-${ARCH}.deb DEB_URLhttps://package.emqx.net/${DEB_NAME} log_info 开始下载 ${DEB_NAME} ... curl -fL --retry 3 --connect-timeout 10 -o /tmp/${DEB_NAME} ${DEB_URL} || { log_error 下载失败。请确认版本号与系统代号 ${OS_CODENAME} 是否在官方支持列表新版号可能要去官网下载页拿直链。 exit 1 } dpkg -i /tmp/${DEB_NAME} || apt-get -f install -y rm -f /tmp/${DEB_NAME} log_info EMQX ${EMQX_VERSION} 安装完成 fi # ---------- 2. 停服并准备持久化目录 ---------- systemctl stop emqx 2/dev/null || true if [ ! -d ${DATA_DIR} ]; then log_info 创建持久化目录 ${DATA_DIR} mkdir -p ${DATA_DIR} ${LOG_DIR} # 如果 /var/lib/emqx 里已有历史数据先整体搬过去 if [ -d /var/lib/emqx ] [ -n $(ls -A /var/lib/emqx 2/dev/null) ]; then log_info 发现 /var/lib/emqx 下有历史数据开始迁移 ... if command -v rsync /dev/null 21; then rsync -a /var/lib/emqx/ ${DATA_DIR}/ else cp -a /var/lib/emqx/. ${DATA_DIR}/ fi fi fi chown -R emqx:emqx ${DATA_DIR} ${LOG_DIR} # ---------- 3. 把自定义目录写入配置 ---------- # node.data_dir 决定所有运行时数据mnesia/configs 等的位置 if ! grep -q node *{ /etc/emqx/emqx.conf; then printf \nnode {\n data_dir %s\n}\n ${DATA_DIR} /etc/emqx/emqx.conf else sed -i s|^\([[:space:]]*data_dir[[:space:]]*\).*|\1 \${DATA_DIR}\| /etc/emqx/emqx.conf fi # 日志目录也改到持久化磁盘 if ! grep -q file_handler *{ /etc/emqx/emqx.conf 2/dev/null; then printf \nlog {\n file_handler {\n enable true\n dir %s\n }\n}\n ${LOG_DIR} /etc/emqx/emqx.conf else sed -i s|^\([[:space:]]*dir[[:space:]]*\).*|\1 \${LOG_DIR}\| /etc/emqx/emqx.conf fi # ---------- 4. 注册自启动并启动 ---------- systemctl daemon-reload systemctl enable emqx /dev/null 21 systemctl start emqx # ---------- 5. 健康检查 ---------- log_info 等待 EMQX 启动 ... for i in $(seq 1 30); do if curl -fsS -m 2 http://127.0.0.1:18083/status /dev/null 21; then log_info EMQX 启动成功Dashboard 地址 http://服务器IP:18083 log_info 持久化目录${DATA_DIR}自启动已启用 exit 0 fi sleep 1 done log_error 等待超时请执行 journalctl -u emqx -n 50 查看日志 exit 14.2 从set -euo pipefail开始脚本的保命符第一行set -euo pipefail是 Bash 脚本里最重要的防御性设置我解释一下每个字母的意思-e任何一条命令返回非零退出码脚本立刻终止。避免“前面装失败了后面还继续跑最后告诉你成功”这种假象-u使用未定义的变量直接报错。能拦住大量因为拼错变量名导致的诡异问题-o pipefail管道命令的退出码以最后一个失败命令为准。比如curl ... | tar ...curl 挂了但 tar 成功没有pipefail脚本会误判成功。代价是脚本必须对“可能失败但没关系”的命令做保护。脚本里systemctl stop emqx 2/dev/null || true就是这个原因——第一次部署时 emqx 服务根本不存在stop返回非零但这是预期中的失败加|| true吞掉它。4.3 安装部分为什么用 .deb 直装而不是配 apt 源安装我用了下载.deb包 dpkg -i的方式而不是往sources.list里加 EMQX 官方 apt 源。原因是版本锁定更清晰你想装 5.8.3脚本里写的就是 5.8.3不会某天apt update后悄悄升级到你不一定想要的新版本。.deb文件名里带系统代号和 CPU 架构${OS_CODENAME}直接用/etc/os-release里读出来的值拼出来。这里有两个容易踩的坑Ubuntu 每个大版本代号不同比如 20.04 是focal、22.04 是jammy、24.04 是noble。EMQX 官方是否发布了对应代号包以官网下载页为准。如果你用的系统很新官方还没出对应包就换官方提供的 tar.gz 方式安装或者先确认有没有兼容包。dpkg -i可能会因为缺依赖失败所以脚本里加了|| apt-get -f install -y兜底让 apt 把缺失的依赖补齐。4.4 数据迁移先停服再搬绝不热迁移脚本里迁移的顺序是有讲究的先systemctl stop emqx再检查/var/lib/emqx里有没有老数据有就整体搬到DATA_DIR最后chown。为什么必须先停服因为 Mnesia 是有状态的数据库进程运行时会持续写文件、留 WAL 日志。你趁它活着的时候复制目录得到的文件可能处于不一致状态轻则启动报错重则数据库直接打不开。停服之后复制等于做了一次冷备份一致性有保证。迁移工具我优先用rsync因为它能保留权限、属主、软链接而且传输时不会因为个别文件读取失败就中断整个流程。没有 rsync 的极简环境里cp -a也能起到同样的“归档模式复制”效果。chown -R emqx:emqx这步绝对不能省。EMQX 的 deb 包会创建emqx用户服务以这个用户运行。数据目录如果是 root 所有服务启动时连mnesia目录都进不去日志里全是Permission denied。4.5 配置写入grep 探测、sed 替换、printf 追加脚本修改/etc/emqx/emqx.conf用的是三件套grep -q探测结构是否存在存在就用sed -i替换data_dir那一行不存在就printf把整个 HOCON 块追加到文件末尾。这段体现了 shell 脚本里最常见的“先查后改”模式。grep -q node *{的-q是静默模式只关心有没有匹配不输出任何内容配合if用很干净。sed -i s|...|\1 \...\|里我刻意用|当分隔符因为路径里全是/再用/当分隔符就要写一堆转义看着都疼。这个小技巧在处理路径类字符串时特别有用。EMQX 5.x 的 HOCON 配置语法要求data_dir写在node { }块里日志目录写在log { file_handler { } }里。如果文件里已经存在node块、但没有data_dir字段上面这段严谨来说还应该判断得更细一点日常够用但你要是拿这脚本去管理几百台机器建议再补一层字段级检查。提示EMQX 还支持用环境变量覆盖配置比如EMQX_NODE__DATA_DIR、EMQX_LOG__FILE_HANDLER__DIR这在 Docker 场景下比改配置文件更常规。如果你用 docker compose优先用 environment 或 bind mount 搞定持久化别进容器里改文件。4.6 自启动注册与健康检查最后两公里systemctl daemon-reload是保险动作因为脚本可能改了 unit 文件虽然这个脚本没直接改但包升级时 unit 可能更新过reload 一下让 systemd 重新加载定义。接着systemctl enable把开机自启的符号链接挂上systemctl start立即拉起服务。健康检查我用了 18083 端口的/status接口而不是sleep 5硬等。for i in $(seq 1 30)循环每秒钟探测一次最多等 30 秒。EMQX 第一次启动要初始化 Mnesia、加载插件慢机器上 30 秒未必够你可以把数字改成 60。用 HTTP 探测而不是检查端口监听是因为 Erlang 虚拟机进程起来后HTTP 服务可能还要再等一两秒才对外响应探测/status才是真正的“业务就绪”。5. 验收与排错怎么证明重启后数据还在、服务真能自启5.1 持久化验收从写入到重启到再读脚本跑完不等于万事大吉。我第一次写完类似脚本时自信满满重启服务器结果回来看数据目录是空的排查半天发现是chown在迁移之前执行了新目录的属主被后续写入覆盖掉了。所以验收一定要做而且要用数据来验收。三个步骤打开 Dashboardhttp://服务器IP:18083新建一个测试用户再发布一条带 retain 标志的消息。没有现成 MQTT 客户端的话Dashboard 的 WebSocket 工具就能发消息执行systemctl restart emqx等进程起来重新打开 Dashboard看测试用户还在不在用任意客户端订阅那个 topic看 retain 消息能不能立即收到。如果用户没了、retain 消息也收不到多半是node.data_dir没生效或者/var/lib/emqx的老数据没有被正确搬过去。5.2 自启动验收别省掉重启这一步systemctl is-enabled emqx输出enabled只说明“开机启动项”注册了不能证明开机流程真能拉起来。最靠谱的验收方式是直接reboot等机器起来后执行systemctl status emqx --no-pager看是不是active (running)。如果服务没起来systemctl status会显示failed这时重点排查journalctl -u emqx -n 50看启动日志尤其是Permission denied和端口冲突ps -ef | grep beam | grep emqx确认 Erlang 虚拟机进程是否存在ss -lntp | grep -E 1883|18083确认端口有没有监听。5.3 排错优先级日志、权限、路径三件套我在生产环境排 EMQX 启动问题基本按这个顺序现象可能原因排查命令服务 active 但 18083 打不开EMQX 还在初始化或插件没加载journalctl -u emqx -f观察日志启动即失败Permission denied数据目录属主不对ls -ld /data/emqx启动报data_dir相关错误emqx.conf没改对grep -A2 node /etc/emqx/emqx.conf端口被占老实例没停干净ss -lntp迁移后节点名变了configs/cluster.hocon没随数据目录迁移检查/data/emqx/configs/cluster.hocon这里还想提醒一句不要看到服务起来了就急着宣布部署成功。EMQX 的进程起来和“集群状态正常”是两码事journalctl里如果有持续的Mnesia报错即使端口在监听数据读写也可能是坏的。用emqx ctl status看一眼集群状态输出里有is_running才叫真健康。6. 留个后手备份脚本、磁盘规划与多节点的持久化边界6.1 定时备份持久化的最后防线持久化目录移到了独立磁盘只解决了“误删”和“容器生命周期”问题解决不了磁盘损坏、人为rm -rf、勒索软件加密这类灾难。所以我在部署脚本之外还配了一个极简备份脚本#!/usr/bin/env bash set -euo pipefail BACKUP_ROOT/data/backup/emqx STAMP$(date %Y%m%d_%H%M%S) systemctl stop emqx mkdir -p ${BACKUP_ROOT} tar -czf ${BACKUP_ROOT}/emqx_data_${STAMP}.tar.gz -C /data emqx systemctl start emqx # 保留最近 7 份备份 ls -1t ${BACKUP_ROOT}/emqx_data_*.tar.gz | tail -n 8 | xargs -r rm -f$(date %Y%m%d_%H%M%S)是 shell 脚本里生成时间戳的惯用写法把当前日期时间碾成一个纯数字串保证备份文件名不重样。备份前停服、备份后拉起逻辑和部署脚本里的迁移一致都是为了冷一致性。注意在线备份 Mnesia 目录不是不可以但不同版本、不同负载下的一致性表现不一样。宁可每天停机几秒钟做个冷备也别在某次恢复时发现备份文件缺了个 WAL 导致整个库起不来。6.2 磁盘规划数据目录千万别放在 /tmp脚本里默认DATA_DIR/data/emqx这个/data是我习惯上单独挂载的数据盘。生产环境建议遵守几条原则数据目录所在磁盘不要和系统盘混在一起系统崩溃、重装系统时数据盘还能保住Mnesia 对小文件的随机读写比较敏感机械盘上节点多了会有明显卡顿有条件上 SSD不要把数据目录放 NFS 这类网络文件系统上Mnesia 的锁机制在网络存储上容易出幺蛾子报错还特别隐晦日志目录和数据目录分开日志增长快满了不能反过来拖垮数据库目录。如果你用的是群晖 NAS 这类设备脚本里的 deb 安装方式不适用要走 Docker 或套件方式。但核心思路一样把/opt/emqx/data这类目录挂载到 NAS 的持久化存储上容器删除重建后数据依然在。6.3 多节点集群持久化的边界在“全节点”最后说一句多节点。EMQX 集群模式下Mnesia 是分布式的每个节点都有数据副本。单机脚本的持久化思路依然成立但要注意三点每个节点的node.name和cookie必须保持稳定否则节点重启后加入集群会失败备份不是只备一台所有节点都要覆盖否则集群恢复时缺一角的副本数据会很麻烦集群规模大了之后emqx.conf里cluster段的配置要统一管理别再靠脚本一台台改了上配置管理工具是正道。单机部署是起点但脚本思维是通用的把容易忘、容易错、容易丢的步骤固化下来让机器去重复执行而不是让运维靠记忆力。我现在每接一个新环境第一个动作永远是先把部署脚本丢上去跑一遍确认能复现再做业务接入。最后再分享一个小技巧脚本里所有路径我都写成变量放在文件头部换机器、换磁盘时只改顶部那几行不用在几十行代码里翻找。这个习惯让我在维护几十套环境时省了大量时间也推荐你从第一个脚本开始就这么干。