注:本篇是源于实际项目需求,基于 qunarcorp/qmq 开源项目,完成源码编译、Docker 镜像构建、K8s 编排部署的端到端实践,附踩坑记录与验证方案。文章较长,阅读需要约 15 分钟。
📋 全文目录
背景:为什么需要自建 QMQ 镜像 QMQ 架构与组件解析 环境准备与源码获取 源码编译:Docker 内 Maven 构建 镜像构建:Dockerfile 设计思路 镜像导出与导入 K8s 部署:从 YAML 到全组件 Running 踩坑记录:三个关键问题的排查与修复 功能验证 国内 Docker 镜像源推荐 总结与展望
一、背景:为什么需要自建 QMQ 镜像
QMQ 是去哪儿网开源的分布式消息队列(github.com/qunarcorp/qmq),在消息可靠性、顺序消费、延迟消息等场景有着成熟的生产实践。但在实际项目中,QMQ的使用范围并不如kafka、rabbitMq等其他项目广泛。并且官方仅提供 x86_64 架构的镜像,随着 ARM64 信创服务器的普及,我们面临一个现实问题:
官方 QMQ 镜像仅支持 linux/amd64,在 ARM64(鲲鹏/飞腾/M1)服务器上无法运行。K8s 集群已全面切换到 ARM64 节点,亟需自建兼容镜像。
本文完整记录了从源码编译到 K8s 部署的全过程,涵盖以下关键环节:

二、QMQ 架构与组件解析
QMQ 由 4 个核心组件构成,理解其架构是后续部署的基础:

| MetaServer | qunar.tc.qmq.meta.startup.Bootstrap | |
| Broker | qunar.tc.qmq.container.Bootstrap | |
| Delay Broker | qunar.tc.qmq.delay.container.Bootstrap | |
| Watchdog | qunar.tc.qmq.task.Bootstrap |
💡 关键机制
Broker 启动前必须通过 HTTP 接口 /management(AddBrokerAction)向 MetaServer 注册,注册成功后才能通过 Netty 协议 acquire_meta 获取路由信息。这一机制是后续踩坑的根源之一。
三、环境准备与源码获取
3.1 环境要求
--platform linux/arm64 | ||
3.2 源码获取
QMQ 官方仓库在国内访问可能较慢,推荐使用 GitHub 镜像加速:
# 方式一:官方仓库git clone https://github.com/qunarcorp/qmq.git# 方式二:国内镜像加速(推荐)git clone https://ghfast.top/https://github.com/qunarcorp/qmq.git⚠️ 注意
QMQ 源码要求 JDK 8 编译,不要使用 JDK 11+,否则会出现 javax.annotation.PreDestroy 找不到等编译错误(JDK 9+ 移除了 java.annotation 包)。
四、源码编译:Docker 内 Maven 构建
直接在宿主机编译需要安装 JDK 8 和 Maven,但不同版本的依赖冲突容易翻车。推荐方案是使用 Docker 容器编译,一次命令搞定:
docker run --rm \ --platform linux/arm64 \ -v $(pwd)/qmq:/build \ -v ~/.m2:/root/.m2 \ -e http_proxy=http://10.228.0.116:8080 \ -e https_proxy=http://10.228.0.116:8080 \ -w /build \ maven:3.9-amazoncorretto-8 \ mvn -B -U clean package -Pdist \ -Dmaven.test.skip=true -DskipTests \ -am -pl qmq-dist关键参数说明:
--platform linux/arm64确保在 ARM64 环境下编译 -v ~/.m2:/root/.m2挂载本地 Maven 缓存,避免重复下载依赖 -Pdist激活 dist profile,生成完整的分发包 -pl qmq-dist仅编译 dist 模块及其依赖 amazoncorretto-8AWS 维护的 JDK 8 发行版,Maven 镜像集成
编译产物位于 qmq-dist/target/qmq-dist-1.1.44-SNAPSHOT-bin.tar.gz,解压后包含:
qmq-dist-1.1.44-SNAPSHOT-bin/├── bin/ # 启停脚本├── conf/ # 配置模板├── lib/ # JAR 依赖└── sql/ # 数据库初始化脚本 ├── init.sql └── init_client.sql五、镜像构建:Dockerfile 设计思路
5.1 单阶段构建方案
由于编译我们已在 Docker 容器中完成,Dockerfile 只需打包运行时即可,完整Dockerfile如下:
# syntax=docker/dockerfile:1ARG REGISTRY=docker.1ms.run/libraryARG BASE_IMAGE=${REGISTRY}/eclipse-temurin:8-jre-jammyFROM ${BASE_IMAGE}LABEL org.opencontainers.image.title="qmq"LABEL org.opencontainers.image.version="1.1.44-SNAPSHOT"# 静态 curl 二进制(避免 apt-get 受公司代理干扰)COPY docker/scripts/curl /usr/local/bin/curlRUN chmod +x /usr/local/bin/curl# qmq-dist 产物: bin/ conf/ lib/ sql/COPY docker/qmq-dist/ /app/# 启动脚本 + 符号链接兼容COPY docker/scripts/ /app/scripts/RUN ln -sf /app/scripts/start_metaserver.sh /app/start_metaserver.sh && \ ln -sf /app/scripts/start_broker.sh /app/start_broker.sh && \ ln -sf /app/scripts/start_delay_broker.sh /app/start_delay_broker.sh && \ ln -sf /app/scripts/start_watchdog.sh /app/start_watchdog.sh && \ ln -sf /app/scripts/copy_config_files.sh /app/copy_config_files.sh && \ chmod +x /app/scripts/*.sh /app/bin/*.sh 2>/dev/null || trueRUN mkdir -p /app/logs /app/pid /data /config/db /config/token /config/extraENV QMQ_HOME=/app JAVA_HOME=/opt/java/openjdkHEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \ CMD /app/scripts/healthcheck.sh || exit 1CMD ["/app/scripts/start_metaserver.sh"]5.2 设计要点
eclipse-temurin:8-jre | |
apt-get install curl | |
command: /app/start_broker.sh 无需修改 | |
/config/ | copy_config_files.sh 统一拷贝到 /app/conf/ |
5.3 一键构建脚本
将编译+构建整合为 build.sh:
#!/usr/bin/env bashset -euo pipefailSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"QMQ_SRC="${QMQ_SRC:-${SCRIPT_DIR}/../qmq}"REGISTRY="${REGISTRY:-docker.1ms.run/library}"# Step 1: docker run maven 编译docker run --rm --platform linux/arm64 \ -v "${QMQ_SRC}":/build \ -v ~/.m2:/root/.m2 \ -w /build \ ${REGISTRY}/maven:3.9-amazoncorretto-8 \ mvn -B -U clean package -Pdist \ -Dmaven.test.skip=true -DskipTests -am -pl qmq-dist# Step 2: 解压产物DIST_TAR=$(find "${QMQ_SRC}/qmq-dist/target" -name "qmq-dist-*-bin.tar.gz" | head -1)rm -rf "${SCRIPT_DIR}/docker/qmq-dist"mkdir -p "${SCRIPT_DIR}/docker/qmq-dist"tar -xzf "${DIST_TAR}" -C "${SCRIPT_DIR}/docker/qmq-dist" --strip-components=1# Step 3: docker builddocker build --platform linux/arm64 \ -t qmq:1.1.44-arm64 \ -f "${SCRIPT_DIR}/docker/Dockerfile" \ "${SCRIPT_DIR}"💡 构建加速
REGISTRY 变量控制基础镜像源,默认使用 docker.1ms.run 镜像加速,也可替换为 docker.m.daocloud.io/library 等其他源。构建完成后镜像约 318MB。
六、镜像导出与导入
信创环境通常无法直连 Docker Hub,需要离线导入。QMQ 镜像导出约 314MB:
# 导出docker save qmq:1.1.44-arm64 -o qmq_1.1.44-arm64.tar# 在目标机器上导入docker load -i qmq_1.1.44-arm64.tar# Minikube 环境:直接加载到集群minikube image load qmq:1.1.44-arm64⚠️ Minikube 镜像更新
minikube image load 对同名 tag 不会自动覆盖旧镜像。如需更新,先在 minikube 内删除旧镜像:minikube ssh "docker rmi qmq:1.1.44-arm64"然后重新执行命令: minikube image load。
七、K8s 部署:从 YAML 文件到全组件 Running
7.1 部署顺序

7.2 逐个部署过程
① 创建Namespace
apiVersion: v1kind: Namespacemetadata: name: goo② MariaDB
apiVersion: apps/v1kind: StatefulSetmetadata: name: qmq-mariadb namespace: goospec: serviceName: qmq-mariadb replicas: 1 template: spec: containers: - name: mariadb image: mariadb:10.6 imagePullPolicy: IfNotPresent env: - name: MYSQL_ROOT_PASSWORD value: "qmq_root_pass" - name: MYSQL_DATABASE value: "qmq_meta" readinessProbe: exec: command: [mariadb-admin, ping, -h, localhost, -u, root, -pqmq_root_pass] initialDelaySeconds: 15③ 数据库初始化
使用 QMQ 官方 SQL 初始化数据库表结构:
# 将 SQL 文件拷贝到 MariaDB Podkubectl cp qmq/qmq-dist/sql/init.sql goo/qmq-mariadb-0:/tmp/init.sqlkubectl cp qmq/qmq-dist/sql/init_client.sql goo/qmq-mariadb-0:/tmp/init_client.sql# 执行初始化kubectl exec -n goo qmq-mariadb-0 -- bash -c \ "mysql -uroot -pqmq_root_pass -e 'CREATE DATABASE IF NOT EXISTS qmq_meta DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS qmq_client DEFAULT CHARACTER SET utf8mb4;' \ && mysql -uroot -pqmq_root_pass qmq_meta < /tmp/init.sql \ && mysql -uroot -pqmq_root_pass < /tmp/init_client.sql"④ Secrets 配置
两个关键 Secret:qmq-datasource(数据库连接)和 qmq-token(API 认证):
# datasource.properties(注意 key 是 jdbc.url 而非 jdbcUrl)apiVersion: v1kind: Secretmetadata: name: qmq-datasource namespace: gootype: Opaquedata: datasource.properties: <base64 encoded> # 内容: # jdbc.url=jdbc:mysql://qmq-mariadb.goo.svc.cluster.local:3306/qmq_meta?... # jdbc.username=root # jdbc.password=qmq_root_pass # jdbc.driverClassName=com.mysql.jdbc.Driver# valid-api-tokens.properties(注意 key 格式)apiVersion: v1kind: Secretmetadata: name: qmq-token namespace: gootype: Opaquedata: valid-api-tokens.properties: <base64 encoded> # 内容: admin=admin # 注意:不能写成 tokens=admin!⑤ MetaServer
apiVersion: apps/v1kind: StatefulSetmetadata: name: qmq-metaservers namespace: goospec: replicas: 1 template: spec: initContainers: - name: wait-mysql image: mariadb:10.6 command: [bash, -c, "until mysqladmin ping -h qmq-mariadb ... --silent; do sleep 3; done"] containers: - name: qmq-meta image: qmq:1.1.44-arm64 command: [/app/start_metaserver.sh, start] ports: - {containerPort: 8080, name: http} - {containerPort: 20880, name: meta} volumeMounts: - {name: datasource, mountPath: /config/db/} - {name: api-token, mountPath: /config/token/}⑥ Broker + ⑦ Delay Broker + Watchdog
Broker 和 Delay Broker 的部署结构相似,核心差异在于端口和注册信息:
Broker 的关键环境变量配置:
env: - name: GROUP_NAME value: k8sgroup - name: TOKEN value: admin - name: ROLE value: "0" - name: METASERVER value: qmq-metaserver-service.goo.svc.cluster.local:8080 - name: BROKER_PORT value: "20881" - name: SYNC_PORT value: "20882"7.3 一键部署命令
# 按顺序执行kubectl apply -f 00-namespace.yamlkubectl apply -f 05-mariadb.yamlkubectl -n goo rollout status statefulset/qmq-mariadb --timeout=120s# 初始化数据库(执行 QMQ 官方 SQL)kubectl cp qmq-dist/sql/init.sql goo/qmq-mariadb-0:/tmp/init.sqlkubectl exec -n goo qmq-mariadb-0 -- bash -c \ "mysql -uroot -pqmq_root_pass qmq_meta < /tmp/init.sql"kubectl cp qmq-dist/sql/init_client.sql goo/qmq-mariadb-0:/tmp/init_client.sqlkubectl exec -n goo qmq-mariadb-0 -- bash -c \ "mysql -uroot -pqmq_root_pass < /tmp/init_client.sql"# 部署 QMQ 组件kubectl apply -f 07-secrets.yamlkubectl apply -f 10-metaserver.yamlkubectl -n goo rollout status statefulset/qmq-metaservers --timeout=120skubectl apply -f 20-broker.yamlkubectl apply -f 30-delay-broker.yamlkubectl apply -f 40-watchdog.yaml# 等待全部就绪kubectl -n goo get pods八、踩坑记录:三个关键问题的排查与修复
部署过程中,我遇到了三个棘手问题,每一个都值得记录:
问题 1:datasource.properties 的 key 名称不对
现象:MetaServer 启动报错:配置项: jdbc.url 值为空
原因分析:Secret 中写的是 jdbcUrl=jdbc:mysql://...(驼峰命名),但 QMQ 源码 DefaultDataSourceFactory.java 中使用 config.get("jdbc.url")(点号分隔)读取配置。
修复方法:将 Secret 中的 jdbcUrl 改为 jdbc.url,同时把主机名从 qmq-mysql 改为 qmq-mariadb。
同时,数据库表也不对:我们手动创建的表名是 broker_group,而 QMQ 官方 SQL 中是 broker 表。解决方法是直接使用 QMQ 源码中的 qmq-dist/sql/init.sql,而非手动建表。
问题 2:API Token 格式错误
现象:Broker 注册时 MetaServer 返回:{"status":-1,"message":"没有提供合法的 Api Token"}
原因分析:Secret 中 valid-api-tokens.properties 内容为 tokens=admin。而源码 TokenVerificationAction.java 的验证逻辑是:
// 源码简化Map<String, String> validApiTokens = config.asMap();// validApiTokens = {"tokens": "admin"}String token = request.getHeader("X-Api-Token"); // token = "admin"return validApiTokens.containsKey(token); // containsKey("admin") = false!Map 的 key 是 tokens,而查找的 key 是 admin(Token 值本身),自然找不到。
修复方法:将 valid-api-tokens.properties 内容改为 admin=admin,使 Map 的 key 为 admin。
🔍 问题 3:Watchdog 缺少 appCode 和 meta.server.endpoint
现象:Watchdog 启动报错:配置项: appCode 值为空,修复后又报 配置项: meta.server.endpoint 值为空
原因分析:Watchdog 的配置文件 watchdog.properties 中没有 appCode 和 meta.server.endpoint 两个配置项。虽然 start_watchdog.sh 脚本有注入逻辑,但 K8s lifecycle.postStart 钩子会异步执行 copy_config_files.sh,可能在注入之后覆盖了配置文件,导致注入丢失。
修复方法:在 K8s YAML 的 command 中直接执行注入逻辑,避免 postStart 的竞态条件:
command: - bash - -c - | QMQ_HOME="${QMQ_HOME:-/app}" . "${QMQ_HOME}/scripts/common.sh" "${QMQ_HOME}/scripts/copy_config_files.sh" || true inject_property "${QMQ_HOME}/conf/watchdog.properties" \ "meta.server.endpoint" "http://${METASERVER}/meta/address" inject_property "${QMQ_HOME}/conf/watchdog.properties" \ "appCode" "${APPCODE:-qmq_watchdog}" exec /app/start_watchdog.sh九、功能验证
9.1 组件状态检查
$ kubectl -n goo get podsNAME READY STATUS RESTARTS AGEqmq-mariadb-0 1/1 Running 0 5mqmq-metaservers-0 1/1 Running 0 4mbroker-0 1/1 Running 0 3mdelay-broker-0 1/1 Running 0 3mwatchdog-5594b9b597-xxxxx 1/1 Running 0 2m9.2 服务发现验证
# 从 Broker Pod 内访问 MetaServer 服务发现接口$ kubectl -n goo exec broker-0 -- \ curl -sS http://qmq-metaserver-service.goo.svc.cluster.local:8080/meta/address10.244.0.23:20880返回 MetaServer 的集群内 IP 和 Netty 端口,说明服务发现正常。
9.3 Broker 注册验证
# 查询已注册的 Broker 列表$ kubectl -n goo exec qmq-metaservers-0 -- \ curl -sS "http://127.0.0.1:8080/management" \ -X POST -H "X-Api-Token: admin" \ -d "action=ListBroker"9.4 端口转发本地测试
# 转发 MetaServer 端口到本地kubectl -n goo port-forward svc/qmq-metaserver-service 8080:8080# 转发 Broker 端口到本地(用于消息收发测试)kubectl -n goo port-forward svc/broker-svc 20881:20881十、国内 Docker 镜像源推荐
在国内拉取 Docker Hub 镜像经常遇到超时或被代理阻断的问题,以下是实测可用的国内镜像源:
| DaoCloud(首选) | docker.m.daocloud.io | docker pull docker.m.daocloud.io/library/mariadb:10.6 | |
docker.1ms.run | docker pull docker.1ms.run/library/mariadb:10.6 | ||
docker.xuanyuan.me | docker pull docker.xuanyuan.me/library/mariadb:10.6 | ||
atomhub.openatom.cn |
💡 使用技巧
拉取 Docker Hub 官方镜像时,只需在镜像名前加 docker.m.daocloud.io/library/ 前缀即可,拉完后再 docker tag 为标准名称。例:docker pull docker.m.daocloud.io/library/mariadb:10.6 && docker tag docker.m.daocloud.io/library/mariadb:10.6 mariadb:10.6
十一、总结与展望
本文完整实践了 QMQ 消息队列从源码到 K8s 的端到端部署流程:
✅ 源码编译:使用 Docker 容器内 Maven 构建,避免 JDK 版本冲突 ✅ 镜像构建:JRE 8 运行时 + 静态 curl + 配置注入脚本,镜像仅 318MB ✅ 离线导入:镜像导出为 tar,支持信创环境离线部署 ✅ K8s 部署:5 个组件全部 Running,服务发现和 Broker 注册正常 ✅ 踩坑修复:jdbc.url key 格式、API Token Map 结构、postStart 竞态条件
后续可进一步优化:
引入 Prometheus + Grafana 监控 QMQ 各组件的关键指标 编写 QMQ Java 客户端 Producer/Consumer,端到端验证消息收发 基于 Helm Chart 统一编排,支持生产环境多副本部署 适配多架构构建( docker buildx),同时输出 amd64 + arm64 镜像
📌 参考资料
QMQ 官方仓库: github.com/qunarcorp/qmqEclipse Temurin JRE 8: adoptium.netDaoCloud 镜像源: docker.m.daocloud.ioMinikube: minikube.sigs.k8s.io
本文所有部署产物(Dockerfile、K8s YAML、启动脚本、一键构建脚本)均已开源,可直接复用。https://gitcode.com/liuhuoxingkong/qmq-arm-images
夜雨聆风