首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【技术实战】从源码到 K8s:QMQ 消息队列 ARM64 镜像构建与部署全攻略

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

作者头像
程序员架构进阶
发布2026-06-29 16:44:28
发布2026-06-29 16:44:28
650
举报
文章被收录于专栏:架构进阶架构进阶

注:本篇是源于实际项目需求,基于 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

职责

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 获取路由信息。这一机制是后续踩坑的根源之一。

三、环境准备与源码获取

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 镜像加速:

代码语言:javascript
复制
# 方式一:官方仓库
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 容器编译,一次命令搞定:

代码语言:javascript
复制
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,解压后包含:

代码语言:javascript
复制
qmq-dist-1.1.44-SNAPSHOT-bin/
├── bin/          # 启停脚本
├── conf/         # 配置模板
├── lib/          # JAR 依赖
└── sql/          # 数据库初始化脚本
    ├── init.sql
    └── init_client.sql

五、镜像构建:Dockerfile 设计思路

5.1 单阶段构建方案

由于编译我们已在 Docker 容器中完成,Dockerfile 只需打包运行时即可,完整Dockerfile如下:

代码语言:javascript
复制
# 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"]

5.2 设计要点

代码语言:javascript
复制
JRE 8 而非 JDK,镜像体积从 ~400MB 降到 ~180MB

5.3 一键构建脚本

将编译+构建整合为 build.sh

代码语言:javascript
复制
#!/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:

代码语言:javascript
复制
# 导出
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
代码语言:javascript
复制
apiVersion: v1
kind: Namespace
metadata:
  name: goo
② MariaDB
代码语言:javascript
复制
apiVersion: 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 初始化数据库表结构:

代码语言:javascript
复制
# 将 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"
④ Secrets 配置

两个关键 Secret:qmq-datasource(数据库连接)和 qmq-token(API 认证):

代码语言:javascript
复制
# 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
代码语言:javascript
复制
# 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!
⑤ MetaServer
代码语言:javascript
复制
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 + 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 的关键环境变量配置:

代码语言:javascript
复制
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 一键部署命令

代码语言:javascript
复制
# 按顺序执行
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

八、踩坑记录:三个关键问题的排查与修复

部署过程中,我遇到了三个棘手问题,每一个都值得记录:

问题 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 的验证逻辑是:

代码语言:javascript
复制
// 源码简化
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 中没有 appCodemeta.server.endpoint 两个配置项。虽然 start_watchdog.sh 脚本有注入逻辑,但 K8s lifecycle.postStart 钩子会异步执行 copy_config_files.sh,可能在注入之后覆盖了配置文件,导致注入丢失。

修复方法:在 K8s YAML 的 command 中直接执行注入逻辑,避免 postStart 的竞态条件:

代码语言:javascript
复制
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 组件状态检查

代码语言:javascript
复制
$ 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

9.2 服务发现验证

代码语言:javascript
复制
# 从 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 端口,说明服务发现正常。

9.3 Broker 注册验证

代码语言:javascript
复制
# 查询已注册的 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 端口转发本地测试

代码语言:javascript
复制
# 转发 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

✅ 延迟 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 容器内 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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员架构进阶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 📋 全文目录
  • 一、背景:为什么需要自建 QMQ 镜像
  • 二、QMQ 架构与组件解析
  • 三、环境准备与源码获取
    • 3.1 环境要求
    • 3.2 源码获取
  • 四、源码编译:Docker 内 Maven 构建
  • 五、镜像构建:Dockerfile 设计思路
    • 5.1 单阶段构建方案
    • 5.2 设计要点
    • 5.3 一键构建脚本
  • 六、镜像导出与导入
  • 七、K8s 部署:从 YAML 文件到全组件 Running
    • 7.1 部署顺序
    • 7.2 逐个部署过程
      • ① 创建Namespace
      • ② MariaDB
      • ③ 数据库初始化
      • ④ Secrets 配置
      • ⑤ MetaServer
      • ⑥ Broker + ⑦ Delay Broker + Watchdog
    • 7.3 一键部署命令
  • 八、踩坑记录:三个关键问题的排查与修复
    • 问题 1:datasource.properties 的 key 名称不对
    • 问题 2:API Token 格式错误
    • 🔍 问题 3:Watchdog 缺少 appCode 和 meta.server.endpoint
  • 九、功能验证
    • 9.1 组件状态检查
    • 9.2 服务发现验证
    • 9.3 Broker 注册验证
    • 9.4 端口转发本地测试
  • 十、国内 Docker 镜像源推荐
  • 十一、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档