ConstStar
发布于 2026-03-25 / 5 阅读 / 0 评论 / 0 点赞

在 Kubernetes 上手动部署 EMQX 集群

1. 前提条件

  • Kubernetes 集群 1.19+(kubeadm、k3s、云厂商均可)
  • kubectl 已配置,能正常访问集群
  • 至少 3 个工作节点(推荐,每个节点部署一个 EMQX 实例)
  • 节点间网络互通,Pod 网络已部署(如 Flannel、Calico)
  • 安装 StorageClass 启用动态存储
  • 防火墙/安全组需放行:
    • Pod 网络通信端口(例如 Flannel 使用 UDP 8472,Calico 可能使用 BGP 或 IPIP)
    • EMQX 集群内部通信端口 TCP 4370(用于 Erlang 分布)
    • 后续对外暴露的端口(NodePort 范围 30000-32767,或 externalIPs 指定的端口)
  • (可选)如果从 Docker Hub 拉取镜像慢,提前配置镜像加速器
  • (可选)提前在各个节点拉取需要的docker容器

2. 创建命名空间(可选)

kubectl create namespace emqx

3. 创建 Headless Service(用于集群内部通信)

Headless Service(clusterIP: None)不会分配虚拟 IP,而是直接返回后端 Pod 的 IP 列表。EMQX 通过 DNS 查询该 Service 的 SRV 记录来发现兄弟节点。

# emqx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: emqx-service
  namespace: emqx
spec:
  clusterIP: None          # 关键:Headless Service
  selector:
    app: emqx
  ports:
  - name: ekka             # EMQX 集群内部通信端口
    port: 4370
    targetPort: 4370
  - name: mqtt-tcp
    port: 1883
    targetPort: 1883
  - name: mqtt-ws
    port: 8083
    targetPort: 8083
  - name: dashboard
    port: 58083
    targetPort: 58083
kubectl apply -f emqx-service.yaml

所有节点必须使用相同的 Erlang cookie,并通过 DNS 策略发现彼此。

# emqx-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: emqx-config
  namespace: emqx
data:
  EMQX_DASHBOARD__LISTENERS__HTTP__BIND: "58083"
  EMQX_CLUSTER__DISCOVERY_STRATEGY: "dns"
  EMQX_CLUSTER__DNS__RECORD_TYPE: "srv"
  EMQX_CLUSTER__DNS__NAME: "emqx-service.emqx.svc.cluster.local"
  EMQX_NODE__COOKIE: "mysecretcookie"   # !!必须修改!!
kubectl apply -f emqx-config.yaml

5. 创建 StatefulSet(部署 3 节点 EMQX)

关键点

  • 使用 $(POD_NAME) 语法(圆括号)构造完整的节点名,确保每个 Pod 有稳定的域名(如 emqx-0.emqx-headless.emqx.svc.cluster.local)。
  • 切勿使用 ${POD_NAME},否则节点名会保留字面量,导致集群无法通信。
  • 从 ConfigMap 引入集群配置。
  • 添加就绪探针(readinessProbe)和存活探针(livenessProbe),使用 EMQX 的 /status 接口。
# emqx-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: emqx
  namespace: emqx
spec:
  serviceName: emqx-service
  replicas: 3
  selector:
    matchLabels:
      app: emqx
  template:
    metadata:
      labels:
        app: emqx
    spec:
      securityContext:
        fsGroup: 1000
      # volumes:
      # - name: tls-certs
      #   secret:
      #     secretName: mqtt-tls-secret   # 使用同步后的 Secret
      containers:
      - name: emqx
        image: emqx/emqx:5.8.9   # 开源免费集群版
        volumeMounts:
        # - name: tls-certs
        #   mountPath: /opt/emqx/certs   # EMQX 默认证书路径
        #   readOnly: true
        - name: data
          mountPath: /opt/emqx/data   # EMQX 数据目录
        - name: log
          mountPath: /opt/emqx/log   # EMQX 日志目录
        ports:
        - containerPort: 1883
          name: mqtt-tcp
        - containerPort: 5883
          name: mqtt-ssl
        - containerPort: 8083
          name: mqtt-ws
        - containerPort: 8084
          name: mqtt-wss
        - containerPort: 58083
          name: dashboard
        - containerPort: 4370
          name: ekka
        envFrom:
        - configMapRef:
            name: emqx-config
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: EMQX_NODE__NAME
          value: "emqx@$(POD_NAME).emqx-service.emqx.svc.cluster.local"
        - name: EMQX_NODE__MAX_CONNECTIONS
          value: "2000000"
        livenessProbe:
          httpGet:
            path: /status
            port: 58083
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /status
            port: 58083
          initialDelaySeconds: 10
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi
  - metadata:
      name: log
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 5Gi
kubectl apply -f emqx-statefulset.yaml

等待所有 Pod 进入 Running 状态:

kubectl get pods -n emqx -w

如果卡主:

kubectl describe pod emqx-0 -n emqx

6. 创建对外 Service(暴露 MQTT 和 Dashboard)

根据你的网络环境,从以下三种方式中选择一种。注意:如果使用云环境,推荐方式 C(LoadBalancer);如果使用自建集群且需要标准端口(如 1883),推荐方式 B(externalIPs);如果只是测试或内网使用,方式 A(NodePort)最简单。

方式 A:NodePort(适合内网测试或配合外部端口转发)

NodePort 会在每个节点上开放一个高位端口(默认范围 30000-32767),外部可通过 节点IP:NodePort 访问。

# emqx-external.yaml
apiVersion: v1
kind: Service
metadata:
  name: emqx-external
  namespace: emqx
spec:
  selector:
    app: emqx
  ports:
  - name: mqtt-tcp
    port: 1883
    targetPort: 1883
    nodePort: 31769   # 可选,不指定则由集群自动分配(必须在 30000-32767 内)
  - name: mqtt-ssl
    port: 5883
    targetPort: 5883
  - name: mqtt-ws
    port: 8083
    targetPort: 8083
  - name: mqtt-wss
    port: 8084
    targetPort: 8084
  - name: dashboard
    port: 58083
    targetPort: 58083
    nodePort: 31992
  type: NodePort
kubectl apply -f emqx-external.yaml

重要nodePort 的值必须在集群配置的端口范围内(默认 30000-32767)。不能设为 1883 或 58083,否则创建 Service 时会报错 is invalid。如果客户端强制要求使用 1883 端口,请使用方式 B(externalIPs)。

方式 B:externalIPs + 内网 IP(适合自建集群,需配合路由器端口映射)

如果你的节点只有内网 IP(如 192.168.0.x),但可以通过路由器将公网 IP 的端口映射到节点内网 IP 的相同端口,则使用此方式。这样客户端可以使用标准端口(1883/58083)访问公网 IP,流量经路由器转发到节点内网 IP,再由 kube-proxy 转发到 Pod。

原理externalIPs 中的 IP 必须实际存在于节点的网络接口上。kube-proxy 会监听这些 IP 的指定端口,并将流量转发到 Service 的后端 Pod。因为节点拥有这些内网 IP,所以可以正常工作。

# emqx-external.yaml
apiVersion: v1
kind: Service
metadata:
  name: emqx-external
  namespace: emqx
spec:
  selector:
    app: emqx
  externalIPs:
  - 192.168.0.100   # 节点1内网IP
  - 192.168.0.251   # 节点2内网IP
  - 192.168.0.229   # 节点3内网IP
  ports:
  - name: mqtt-tcp
    port: 1883
    targetPort: 1883
  - name: mqtt-ssl
    port: 5883
    targetPort: 5883
  - name: mqtt-ws
    port: 8083
    targetPort: 8083
  - name: mqtt-wss
    port: 8084
    targetPort: 8084
  - name: dashboard
    port: 58083
    targetPort: 58083
  type: ClusterIP      # 或保留 ClusterIP,不需要 NodePort
kubectl apply -f emqx-external.yaml

注意

  • externalIPs 中填的是节点的内网 IP,不是公网 IP。如果填公网 IP 但节点没有该 IP,流量将无法到达。
  • 需要在路由器上配置 DNAT(端口转发):将公网 IP 的 1883/58083 端口映射到任意一个节点的内网 IP 的相同端口。建议映射到所有节点(多出口),或使用负载均衡器分发。

方式 C:LoadBalancer(云环境推荐)

如果使用云厂商(阿里云、AWS、GCP 等),直接使用 LoadBalancer 类型会自动创建公网负载均衡器,并分配公网 IP。云厂商通常支持将公网 IP 的端口直接映射到后端 Pod 的端口(通过 NodePort 或直连 Pod)。

# emqx-external.yaml
apiVersion: v1
kind: Service
metadata:
  name: emqx-external
  namespace: emqx
spec:
  selector:
    app: emqx
  ports:
  - name: mqtt-tcp
    port: 1883
    targetPort: 1883
  - name: mqtt-ssl
    port: 5883
    targetPort: 5883
  - name: mqtt-ws
    port: 8083
    targetPort: 8083
  - name: mqtt-wss
    port: 8084
    targetPort: 8084
  - name: dashboard
    port: 58083
    targetPort: 58083
  type: LoadBalancer
kubectl apply -f emqx-external.yaml

注意:云厂商的 LoadBalancer 默认会通过 NodePort 转发,因此仍需确保节点防火墙放行对应的 NodePort 端口(但外部访问的是 LB 的公网 IP 和标准端口)。


7. 保留客户端源 IP 的配置

默认情况下,通过 NodePort 或 LoadBalancer 访问时,kube-proxy 会做 SNAT,导致 Pod 中看到的客户端 IP 是节点 IP。如果你需要 EMQX 记录客户端的真实 IP,有两种方法:

方法一:使用 externalTrafficPolicy: Local

在 Service 中设置:

externalTrafficPolicy: Local

这样流量只会被转发到本地运行了 Pod 的节点,且不执行 SNAT,从而保留源 IP。

缺点

  • 负载可能不均:只有本地有 Pod 的节点才会转发流量。如果你的 Pod 分布在所有节点上(例如通过反亲和性调度),则可以正常工作,但若某个节点没有 Pod,其 NodePort 会拒绝连接。
  • 如果使用云 LoadBalancer,需确保 LB 的健康检查只向后端有 Pod 的节点转发。

方法二:使用 PROXY 协议

在流量入口处(如云 LB、Nginx、HAProxy)启用 PROXY 协议,并在 EMQX 中开启对应监听器的 PROXY 协议支持。这种方法可以保留源 IP,同时不限制负载均衡策略。

EMQX 端配置(环境变量):

- name: EMQX_LISTENERS__TCP__DEFAULT__PROXY_PROTOCOL
  value: "true"

如果使用多个监听器,可分别设置。

前置代理配置示例(Nginx TCP 代理):

stream {
    server {
        listen 1883 proxy_protocol;
        proxy_pass emqx-backend:1883;
        ....
    }
}

8. 系统调优

9. 验证部署

9.1 检查 Pod 状态

kubectl get pods -n emqx -o wide

9.2 查看集群状态

进入任意一个 Pod(例如 emqx-0)执行命令:

kubectl exec -it emqx-0 -n emqx -- emqx_ctl cluster status

如果 kubectl exec 不通,可以登录到 Pod 所在节点,使用 docker execcrictl exec

# 找到容器 ID
docker ps | grep emqx-0
docker exec <container-id> emqx_ctl cluster status

期望输出三个节点互相通信。

9.3 压力测试

# 1. 连接测试(模拟 1 万个客户端连接)
docker run --rm -it --init emqx/emqtt-bench:latest conn -h 192.168.0.100 -p 1883 -c 10000

# 2. 订阅测试(50 个订阅者,订阅主题 test/topic)
docker run --rm -it --init emqx/emqtt-bench:latest sub -h 192.168.0.100 -p 1883 -c 50 -t "test/topic"

# 3. 发布测试(200 个发布者,每个每秒发送 100 条消息到 test/topic)
docker run --rm -it --init emqx/emqtt-bench:latest pub -h 192.168.0.100 -p 1883 -c 200 -t "test/topic" -I 100

10. 常见问题与排查

问题现象 可能原因 解决方法
Pod 一直 ContainerCreating 镜像拉取慢/失败 手动在节点拉取镜像,或配置镜像加速器(如阿里云加速器)
DNS 解析不到 Pod 域名(如 emqx-1.emqx-headless... Pod 未 Ready,或 Headless Service 未发布 检查 Pod 状态:kubectl describe pod emqx-1 -n emqx 查看 readiness 探针;临时可设置 Headless Service 的 publishNotReadyAddresses: true 测试
emqx_ctl cluster status 只看到自身 节点名配置错误,或网络不通 检查 EMQX_NODE__NAME 环境变量是否正确(必须为 emqx@emqx-0.emqx-headless...);检查防火墙是否放行 4370 端口;测试节点间 Pod IP 连通性
节点名包含 ${POD_NAME} 字面量 在 StatefulSet 中使用了 ${POD_NAME} 而不是 $(POD_NAME) 修改 StatefulSet,将 ${POD_NAME} 替换为 $(POD_NAME),重建 Pod
NodePort 设置报错 is invalid nodePort 不在默认范围 30000-32767 内 改用自动分配,或使用 externalIPs 方式(见 6.B)
externalIPs 配置后无法访问 填写的 IP 不在节点的网络接口上 必须填写节点实际拥有的 IP(通常是内网 IP);公网 IP 需通过路由器 NAT 映射,不能直接填在 externalIPs 中
使用 externalTrafficPolicy: Local 后某些节点无法访问 访问的节点上没有运行 Pod 确保每个节点都有 Pod(通过反亲和性调度),或改用 Cluster 模式(但会丢失源 IP)
跨节点 Pod 网络不通 防火墙/安全组拦截,或 CNI 插件异常 检查 iptables 规则:iptables -L FORWARD -n -v;查看 Flannel/Calico 日志;临时关闭防火墙测试

11. 总结

通过以上步骤,你可以在 Kubernetes 上成功运行一个免费的 EMQX 集群。关键点回顾:

  • Headless Service + DNS 发现:使用 SRV 记录让节点自动发现彼此。
  • 节点名构造:必须用 $(POD_NAME) 语法,避免字面量。
  • 防火墙:放行 4370 端口及 Pod 网络。
  • 对外暴露:根据环境选择 NodePort、externalIPs 或 LoadBalancer;注意端口范围限制。
  • 源 IP 保留:可使用 externalTrafficPolicy: Local 或 PROXY 协议。
  • 持久化:通过 PVC 挂载数据目录。

评论