
消息中间件搬到容器平台这件事真正让人头疼的从来不是把进程拉起来而是让它以集群形态跑起来之后内网的节点能互相认出对方、外网的业务系统又能稳稳地连进来。Kubernetes 部署 ActiveMQ Artemis 集群并打通外部通信涉及的东西比想象中多StatefulSet 的稳定标识、Headless Service 的 DNS 解析、broker.xml里静态连接器与集群连接的配合、NodePort/LoadBalancer 暴露之后客户端拿到内部拓扑导致的重连失败……每一个环节单拎出来都不难凑在一起就很容易翻车。这篇内容面向已经在写 YAML、但还没把 Artemis 集群跑顺的运维和后端同学也适合那些刚接手一套能收消息但不知道为啥老断的中间件服务的同学。我会按真实的搭建顺序走一遍把每一步为什么这么做、参数为什么这么给、踩过哪些坑讲透最后给出可直接复制的清单文件和排查速查表。有基础的同学可以直接跳到部署章节抄作业小白建议从头看前面两章的概念对齐决定了后面少调三天。1. 需求拆解与部署形态选型1.1 集群要解决的三个真实问题很多人一提集群就想到高可用其实这两件事得拆开看。Artemis 集群解决的是三个层次不同的问题第一是消息分发的负载均衡多个 broker 节点通过集群连接互相转发消息把生产者的压力摊到多个节点上消费端无论连到哪个节点都能拿到消息第二是容量横向扩展单个 broker 的 journal 写入、内存分页、连接数都有天花板加节点是最直接的扩容手段第三才是故障转移也就是某个节点挂掉之后它上面的消息能被其他节点接管。前两个靠纯集群cluster connection就能实现第三个必须是集群 HA 策略主备复制或共享存储组合才能做到。这个区分非常关键因为它直接决定了存储怎么配、副本数给多少。我见过不少团队一开始就按主备设计每个 primary 配一个 backup结果副本数翻倍、资源翻倍实际业务的消息重要性根本用不上纯属浪费。所以动手之前先问自己一句这套集群里一条消息丢了业务能不能接受不能接受就必须上 HA复制模式或共享存储模式能接受纯集群就够了节点数按吞吐量和连接数来定别为了看起来高可用硬凑偶数。这个判断做完后面的存储选型、副本数规划、升级流程才有依据。1.2 为什么是 StatefulSet 而不是 DeploymentArtemis 是有状态服务它的集群依赖一个东西每个节点有稳定、可预测的网络标识。因为集群连接需要预先知道其他节点的地址而基于 DNS 的服务发现又要求主机名在 Pod 重建后保持不变。Deployment 的 Pod 名是随机后缀重建一次名字就变一次集群连接列表根本没法写死StatefulSet 的 Pod 名是artemis-0、artemis-1这种带序号的配合 Headless Service 会自动生成artemis-0.artemis-hl.mq.svc.cluster.local这样稳定可解析的域名这才是能写进配置里的东西。存储也是同样的逻辑。StatefulSet 的volumeClaimTemplates会让每个 Pod 绑定一块专属的 PVCartemis-0永远挂>kubectl create namespace mq kubectl -n mq create secret generic artemis-secret \ --from-literaladmin-useradmin \ --from-literaladmin-passwordChangeMe_2024然后准备broker.xml模板。这里用了一个__POD_NAME__占位符后面由 initContainer 替换成实际的 Pod 名目的是让每个节点的 broker name 唯一日志和监控里能一眼看出是哪个节点。broker name 唯一不是集群的硬性要求集群靠随机生成的 nodeID 识别节点但对排障帮助极大强烈建议做。apiVersion: v1 kind: ConfigMap metadata: name: artemis-broker-tpl namespace: mq data: broker.xml: | ?xml version1.0 encodingUTF-8? configuration xmlnsurn:activemq xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationurn:activemq /schema/artemis-configuration.xsd core xmlnsurn:activemq:core name__POD_NAME__/name persistence-enabledtrue/persistence-enabled journal-typeNIO/journal-type journal-directorydata/journal/journal-directory paging-directorydata/paging/paging-directory large-messages-directorydata/large-messages/large-messages-directory journal-file-size10M/journal-file-size journal-min-files2/journal-min-files journal-pool-files10/journal-pool-files journal-buffer-timeout33333/journal-buffer-timeout paging-enabledtrue/paging-enabled max-disk-usage90/max-disk-usage disk-scan-period5000/disk-scan-period security-enabledtrue/security-enabled security-settings security-setting match# permission typecreateNonDurableQueue rolesadmin/ permission typedeleteNonDurableQueue rolesadmin/ permission typecreateDurableQueue rolesadmin/ permission typedeleteDurableQueue rolesadmin/ permission typecreateAddress rolesadmin/ permission typedeleteAddress rolesadmin/ permission typeconsume rolesadmin/ permission typebrowse rolesadmin/ permission typesend rolesadmin/ permission typemanage rolesadmin/ /security-setting /security-settings address-settings address-setting match# dead-letter-addressDLQ/dead-letter-address expiry-addressExpiryQueue/expiry-address redelivery-delay0/redelivery-delay max-size-bytes104857600/max-size-bytes max-size-messages100000/max-size-messages address-full-policyPAGE/address-full-policy message-counter-history-day-limit10/message-counter-history-day-limit auto-create-queuestrue/auto-create-queues auto-create-addressestrue/auto-create-addresses auto-delete-queuesfalse/auto-delete-queues auto-delete-addressesfalse/auto-delete-addresses /address-setting /address-settings acceptors acceptor nameartemis tcp://0.0.0.0:61616?protocolsCORE,AMQP,STOMP,MQTT,OPENWIRE; tcpSendBufferSize1048576;tcpReceiveBufferSize1048576; amqpMinLargeMessageSize102400;useNIOtrue; connectionTtl60000;connectionSetupTimeout30000 /acceptor /acceptors connectors connector nameartemis-0 tcp://artemis-0.artemis-hl.mq.svc.cluster.local:61616 /connector connector nameartemis-1 tcp://artemis-1.artemis-hl.mq.svc.cluster.local:61616 /connector connector nameself-connector tcp://__POD_NAME__.artemis-hl.mq.svc.cluster.local:61616 /connector /connectors cluster-connections cluster-connection nameartemis-cluster address#/address connector-refself-connector/connector-ref retry-interval1000/retry-interval retry-interval-multiplier1.5/retry-interval-multiplier max-retry-interval10000/max-retry-interval initial-connect-attempts-1/initial-connect-attempts reconnect-attempts-1/reconnect-attempts use-duplicate-detectiontrue/use-duplicate-detection message-load-balancingON_DEMAND/message-load-balancing max-hops1/max-hops static-connectors connector-refartemis-0/connector-ref connector-refartemis-1/connector-ref /static-connectors /cluster-connection /cluster-connections graceful-shutdown-enabledtrue/graceful-shutdown-enabled graceful-shutdown-timeout15000/graceful-shutdown-timeout /core /configuration这里有几个参数值得单独说清楚。journal-type我用了NIO而不是ASYNCIO原因是异步 IO 依赖 Linux 的 libaio 和足够大的aio-max-nr内核参数容器里这两个条件经常不满足一旦达不到就会在启动时报错或者悄悄退化成同步写性能反而不如直接用 NIO。除非你确认宿主机内核参数调过、镜像里有 libaio否则别碰 ASYNCIO。message-load-balancing设成ON_DEMAND是生产环境的常规选择只有当某个节点的消费者已经准备好接收但队列空了时才会从其他节点把消息转发过来。设成STRICT的话消息会严格轮流分发到每个节点即使那个节点根本没有消费者消息会堆在那儿直到有人来取既浪费空间又增加延迟。address配成#表示这个集群连接对所有地址生效。老版本的配置示例里经常写成jms那是历史遗留写法只对 JMS 前缀的地址生效用通配符更省心。3.2 StatefulSet 关键字段逐个说明apiVersion: apps/v1 kind: StatefulSet metadata: name: artemis namespace: mq spec: serviceName: artemis-hl replicas: 2 podManagementPolicy: Parallel selector: matchLabels: app: artemis template: metadata: labels: app: artemis spec: terminationGracePeriodSeconds: 60 securityContext: fsGroup: 1001 runAsUser: 1001 runAsGroup: 1001 initContainers: - name: render-config image: apache/activemq-artemis:2.33.0 command: [/bin/sh, -c] args: - | set -e mkdir -p /data/etc-override sed s/__POD_NAME__/${POD_NAME}/g /conf-tpl/broker.xml /data/etc-override/broker.xml echo rendered broker.xml for ${POD_NAME} env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name volumeMounts: - name: data mountPath: /data - name: conf-tpl mountPath: /conf-tpl containers: - name: artemis image: apache/activemq-artemis:2.33.0 imagePullPolicy: IfNotPresent ports: - name: core containerPort: 61616 - name: amqp containerPort: 5672 - name: web containerPort: 8161 env: - name: ARTEMIS_USER valueFrom: secretKeyRef: name: artemis-secret key: admin-user - name: ARTEMIS_PASSWORD valueFrom: secretKeyRef: name: artemis-secret key: admin-password - name: ANONYMOUS_LOGIN value: false - name: JAVA_ARGS value: -XX:UseG1GC -XX:MaxRAMPercentage70 -XX:InitialRAMPercentage70 -Xms1g -Xmx2g resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 3Gi startupProbe: tcpSocket: port: 61616 initialDelaySeconds: 15 periodSeconds: 10 failureThreshold: 30 readinessProbe: tcpSocket: port: 61616 periodSeconds: 10 timeoutSeconds: 3 livenessProbe: tcpSocket: port: 61616 initialDelaySeconds: 60 periodSeconds: 20 failureThreshold: 5 volumeMounts: - name: data mountPath: /var/lib/artemis-instance volumes: - name: conf-tpl configMap: name: artemis-broker-tpl volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: standard resources: requests: storage: 20Gi配置渲染的方式值得解释一下。官方镜像约定/var/lib/artemis-instance/etc-override/目录下的文件会在启动时覆盖etc/下的同名配置这给了我们一个干净的自定义入口——不用改镜像不用自定义 entrypoint把渲染好的broker.xml放进去就生效。所以 initContainer 干的事很单纯拿 ConfigMap 里的模板把 Pod 名替换进去写到那个目录。initContainer 和主容器挂同一块 PVC配置就落在 PVC 上了。有人会问为什么不直接把 ConfigMap 挂到etc-override目录。因为__POD_NAME__每个 Pod 不一样ConfigMap 挂载的内容对所有 Pod 是相同的没法做差异化渲染。用 initContainer 是最省事且不污染镜像的做法。关于fsGroup: 1001这个设置官方镜像里的 artemis 用户 uid 是 1001PVC 挂上去之后默认属主是 root如果 Pod 以 1001 运行又没有fsGroup写入 journal 会直接报权限拒绝启动就挂。这个错误日志通常只写一句java.io.IOException: Permission denied不仔细看很容易误判成存储插件的问题。3.3 Service 的两种形态与各自的用途集群内部需要的是Headless Service它不分配 ClusterIP直接为每个 Pod 生成一条 DNS A 记录这正是broker.xml里那些完整域名能解析的前提。apiVersion: v1 kind: Service metadata: name: artemis-hl namespace: mq spec: clusterIP: None publishNotReadyAddresses: true selector: app: artemis ports: - name: core port: 61616 targetPort: 61616publishNotReadyAddresses: true这行很容易被忽略但很关键。默认情况下Pod 没通过 readiness 探针就不会出现在 Endpoints 里DNS 也解析不到。而 Artemis 集群的问题在于节点启动时就要去连其他节点如果其他节点还没 Ready 就解析不到initial-connect-attempts虽然设成了 -1 会一直重试但拓扑建立会被拖慢很多滚动升级期间甚至可能出现新旧节点互不认的情况。打开这个开关只要 Pod 被调度起来DNS 就能解析集群连接可以尽早开始重试。对外则用一个普通 Service类型按环境选apiVersion: v1 kind: Service metadata: name: artemis-external namespace: mq spec: type: NodePort selector: app: artemis ports: - name: core port: 61616 targetPort: 61616 nodePort: 30616 - name: amqp port: 5672 targetPort: 5672 nodePort: 30572这里要清楚一个事实这个 Service 背后是两个 Pod客户端每次新建连接会被随机分到其中一个。对于普通的生产/消费场景这没问题消息会自动转发到有消费者的节点。但对于使用临时队列temporary queue或者非持久订阅的场景一旦连接断开重连到另一个节点原来的临时资源就找不到了客户端会收到InvalidDestinationException之类的错误。这是设计使然不是 bug。3.4 起服务并验证集群真的连上了部署完之后别急着接业务先用三组命令确认三件事。第一组确认 Pod 和 PVC 都正常kubectl -n mq get pods -o wide kubectl -n mq get pvc kubectl -n mq logs artemis-0 | grep -iE cluster|bridge|AMQ日志里应该能看到AMQ221024: Backup server ... started之类的内容更关键的是这类信息AMQ221000: successfully connected to ...或者Bridge is active。如果只看到自己启动、没有任何跨节点的连接日志说明集群连接根本没建起来。第二组在 Pod 里用 Artemis 自带的命令行工具查集群拓扑kubectl -n mq exec -it artemis-0 -- /opt/artemis/bin/artemis queue stat \ --user admin --password ChangeMe_2024 --url tcp://localhost:61616如果工具版本支持还可以用artemis check node --up做一次连通性检查。更直接的办法是进 Web 控制台看把artemis-0的 8161 端口临时转发出来kubectl -n mq port-forward artemis-0 8161:8161浏览器打开http://localhost:8161/console登录后在 Clustered 或者 Topology 相关页面里能看到两个节点互相连接的关系这是最直观的验证方式。第三组发一条消息看是否真的能在集群里流转。这里有个小技巧往节点 0 发从节点 1 收如果收不到说明集群转发没生效如果收得到说明ON_DEMAND分发和 bridge 都是通的。# 在 artemis-0 上生产 kubectl -n mq exec -it artemis-0 -- /opt/artemis/bin/artemis producer \ --user admin --password ChangeMe_2024 \ --url tcp://localhost:61616 \ --destination queue://demo.queue \ --message-count 5 \ --message hello-cluster # 在 artemis-1 上消费 kubectl -n mq exec -it artemis-1 -- /opt/artemis/bin/artemis consumer \ --user admin --password ChangeMe_2024 \ --url tcp://localhost:61616 \ --destination queue://demo.queue \ --message-count 5注意消费者的 destination 用的是queue://前缀这样即使队列还不存在也会自动创建前提是auto-create-queues打开。如果消费命令挂在那儿一直等先检查生产者那边是不是真的发成功了再检查address-settings里的auto-create-*配置。4. 外部通信专项让集群外的系统连进来4.1 一个端口对外为什么会有连上又断的现象把 61616 通过 NodePort 暴露出去之后往往会遇到一个奇怪的现象外部客户端第一次连接成功、能发消息但过一会儿就报连接失败重连又好了过一会儿又断。日志里一大堆AMQ214013: Failed to decode packet或者Connection reset。根本原因是CORE 协议的拓扑发现机制。CORE 客户端连上一个节点后服务端会把整个集群的拓扑也就是所有节点的地址下发给客户端客户端默认会用这份拓扑做负载均衡和故障转移于是它会主动去连拓扑里的其他节点地址。而这些地址是 artemis-0.artemis-hl.mq