乐于分享
好东西不私藏

【技术实战】从源码到 K8s:QMQ 消息队列 ARM64 镜像构建与部署全攻略

【技术实战】从源码到 K8s:QMQ 消息队列 ARM64 镜像构建与部署全攻略

注:本篇是源于实际项目需求,基于 qunarcorp/qmq 开源项目,完成源码编译、Docker 镜像构建、K8s 编排部署的端到端实践,附踩坑记录与验证方案。文章较长,阅读需要约 15 分钟。

📋 全文目录

  1. 背景:为什么需要自建 QMQ 镜像
  2. QMQ 架构与组件解析
  3. 环境准备与源码获取
  4. 源码编译:Docker 内 Maven 构建
  5. 镜像构建:Dockerfile 设计思路
  6. 镜像导出与导入
  7. K8s 部署:从 YAML 到全组件 Running
  8. 踩坑记录:三个关键问题的排查与修复
  9. 功能验证
  10. 国内 Docker 镜像源推荐
  11. 总结与展望

一、背景:为什么需要自建 QMQ 镜像

QMQ 是去哪儿网开源的分布式消息队列(github.com/qunarcorp/qmq),在消息可靠性、顺序消费、延迟消息等场景有着成熟的生产实践。但在实际项目中,QMQ的使用范围并不如kafka、rabbitMq等其他项目广泛。并且官方仅提供 x86_64 架构的镜像,随着 ARM64 信创服务器的普及,我们面临一个现实问题:

官方 QMQ 镜像仅支持 linux/amd64,在 ARM64(鲲鹏/飞腾/M1)服务器上无法运行。K8s 集群已全面切换到 ARM64 节点,亟需自建兼容镜像。

本文完整记录了从源码编译到 K8s 部署的全过程,涵盖以下关键环节:

二、QMQ 架构与组件解析

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

组件
Main Class
职责
MetaServerqunar.tc.qmq.meta.startup.Bootstrap
服务发现、Broker 注册、路由管理
Brokerqunar.tc.qmq.container.Bootstrap
消息存储与转发,实时消息
Delay Brokerqunar.tc.qmq.delay.container.Bootstrap
延迟消息处理
Watchdogqunar.tc.qmq.task.Bootstrap
延迟调度、Leader 选举

💡 关键机制

Broker 启动前必须通过 HTTP 接口 /management(AddBrokerAction)向 MetaServer 注册,注册成功后才能通过 Netty 协议 acquire_meta 获取路由信息。这一机制是后续踩坑的根源之一。

三、环境准备与源码获取

3.1 环境要求

项目
版本
说明
操作系统
macOS / Linux (ARM64)
M1/M2 或鲲鹏/飞腾
Docker
20.10+
支持 --platform linux/arm64
Minikube
v1.32+
本地 K8s 测试
kubectl
v1.28+
集群管理
JDK
8
QMQ 源码编译要求

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-8
    AWS 维护的 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
JRE 8 而非 JDK,镜像体积从 ~400MB 降到 ~180MB
静态 curl 二进制
apt-get install curl
 在公司代理下 GPG 验证失败,预编译的静态二进制完美替代
符号链接兼容
K8s 原部署的 command: /app/start_broker.sh 无需修改
配置注入目录 /config/
Secret 挂载到子目录,由 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 的部署结构相似,核心差异在于端口和注册信息:

组件
GROUP_NAME
ROLE
Serve Port
Sync Port
Broker
k8sgroup
0 (MASTER)
20881
20882
Delay Broker
k8sgroup-delay
5 (DELAY_BACKUP)
20801
20802

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          2m

9.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.iodocker pull docker.m.daocloud.io/library/mariadb:10.6
✅ 延迟 0.23s,秒级拉取
1ms.run
docker.1ms.rundocker pull docker.1ms.run/library/mariadb:10.6
✅ 可用
轩辕镜像(免费)
docker.xuanyuan.medocker pull docker.xuanyuan.me/library/mariadb:10.6
✅ 延迟 0.9s
AtomHub(开放原子)
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/qmq
  • Eclipse Temurin JRE 8:adoptium.net
  • DaoCloud 镜像源:docker.m.daocloud.io
  • Minikube:minikube.sigs.k8s.io

本文所有部署产物(Dockerfile、K8s YAML、启动脚本、一键构建脚本)均已开源,可直接复用。https://gitcode.com/liuhuoxingkong/qmq-arm-images