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

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

组件 | Main Class | 职责 |
|---|---|---|
MetaServer | qunar.tc.qmq.meta.startup.Bootstrap | 服务发现、Broker 注册、路由管理 |
Broker | qunar.tc.qmq.container.Bootstrap | 消息存储与转发,实时消息 |
Delay Broker | qunar.tc.qmq.delay.container.Bootstrap | 延迟消息处理 |
Watchdog | qunar.tc.qmq.task.Bootstrap | 延迟调度、Leader 选举 |
💡 关键机制
Broker 启动前必须通过 HTTP 接口 /management(AddBrokerAction)向 MetaServer 注册,注册成功后才能通过 Netty 协议 acquire_meta 获取路由信息。这一机制是后续踩坑的根源之一。
项目 | 版本 | 说明 |
|---|---|---|
操作系统 | macOS / Linux (ARM64) | M1/M2 或鲲鹏/飞腾 |
Docker | 20.10+ | 支持 --platform linux/arm64 |
Minikube | v1.32+ | 本地 K8s 测试 |
kubectl | v1.28+ | 集群管理 |
JDK | 8 | QMQ 源码编译要求 |
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 包)。
直接在宿主机编译需要安装 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由于编译我们已在 Docker 容器中完成,Dockerfile 只需打包运行时即可,完整Dockerfile如下:
# syntax=docker/dockerfile:1
ARG REGISTRY=docker.1ms.run/library
ARG BASE_IMAGE=${REGISTRY}/eclipse-temurin:8-jre-jammy
FROM ${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/curl
RUN 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 || true
RUN mkdir -p /app/logs /app/pid /data /config/db /config/token /config/extra
ENV QMQ_HOME=/app JAVA_HOME=/opt/java/openjdk
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD /app/scripts/healthcheck.sh || exit 1
CMD ["/app/scripts/start_metaserver.sh"]JRE 8 而非 JDK,镜像体积从 ~400MB 降到 ~180MB将编译+构建整合为 build.sh:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_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 build
docker 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。

apiVersion: v1
kind: Namespace
metadata:
name: gooapiVersion: apps/v1
kind: StatefulSet
metadata:
name: qmq-mariadb
namespace: goo
spec:
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 Pod
kubectl cp qmq/qmq-dist/sql/init.sql goo/qmq-mariadb-0:/tmp/init.sql
kubectl 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"两个关键 Secret:qmq-datasource(数据库连接)和 qmq-token(API 认证):
# datasource.properties(注意 key 是 jdbc.url 而非 jdbcUrl)
apiVersion: v1
kind: Secret
metadata:
name: qmq-datasource
namespace: goo
type: Opaque
data:
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: v1
kind: Secret
metadata:
name: qmq-token
namespace: goo
type: Opaque
data:
valid-api-tokens.properties: <base64 encoded>
# 内容: admin=admin
# 注意:不能写成 tokens=admin!apiVersion: apps/v1
kind: StatefulSet
metadata:
name: qmq-metaservers
namespace: goo
spec:
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 的部署结构相似,核心差异在于端口和注册信息:
组件 | 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"# 按顺序执行
kubectl apply -f 00-namespace.yaml
kubectl apply -f 05-mariadb.yaml
kubectl -n goo rollout status statefulset/qmq-mariadb --timeout=120s
# 初始化数据库(执行 QMQ 官方 SQL)
kubectl cp qmq-dist/sql/init.sql goo/qmq-mariadb-0:/tmp/init.sql
kubectl 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.sql
kubectl exec -n goo qmq-mariadb-0 -- bash -c \
"mysql -uroot -pqmq_root_pass < /tmp/init_client.sql"
# 部署 QMQ 组件
kubectl apply -f 07-secrets.yaml
kubectl apply -f 10-metaserver.yaml
kubectl -n goo rollout status statefulset/qmq-metaservers --timeout=120s
kubectl apply -f 20-broker.yaml
kubectl apply -f 30-delay-broker.yaml
kubectl apply -f 40-watchdog.yaml
# 等待全部就绪
kubectl -n goo get pods部署过程中,我遇到了三个棘手问题,每一个都值得记录:
现象: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,而非手动建表。
现象: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。
现象: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$ kubectl -n goo get pods
NAME READY STATUS RESTARTS AGE
qmq-mariadb-0 1/1 Running 0 5m
qmq-metaservers-0 1/1 Running 0 4m
broker-0 1/1 Running 0 3m
delay-broker-0 1/1 Running 0 3m
watchdog-5594b9b597-xxxxx 1/1 Running 0 2m# 从 Broker Pod 内访问 MetaServer 服务发现接口
$ kubectl -n goo exec broker-0 -- \
curl -sS http://qmq-metaserver-service.goo.svc.cluster.local:8080/meta/address
10.244.0.23:20880返回 MetaServer 的集群内 IP 和 Netty 端口,说明服务发现正常。
# 查询已注册的 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"# 转发 MetaServer 端口到本地
kubectl -n goo port-forward svc/qmq-metaserver-service 8080:8080
# 转发 Broker 端口到本地(用于消息收发测试)
kubectl -n goo port-forward svc/broker-svc 20881:20881在国内拉取 Docker Hub 镜像经常遇到超时或被代理阻断的问题,以下是实测可用的国内镜像源:
渠道 | 地址 | 使用方式 | 实测状态 |
|---|---|---|---|
DaoCloud(首选) | docker.m.daocloud.io | docker pull docker.m.daocloud.io/library/mariadb:10.6 | ✅ 延迟 0.23s,秒级拉取 |
1ms.run | docker.1ms.run | docker pull docker.1ms.run/library/mariadb:10.6 | ✅ 可用 |
轩辕镜像(免费) | docker.xuanyuan.me | docker 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 buildx),同时输出 amd64 + arm64 镜像📌 参考资料
github.com/qunarcorp/qmqadoptium.netdocker.m.daocloud.iominikube.sigs.k8s.io本文所有部署产物(Dockerfile、K8s YAML、启动脚本、一键构建脚本)均已开源,可直接复用。https://gitcode.com/liuhuoxingkong/qmq-arm-images