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
4. 创建 ConfigMap(配置集群发现策略与 Erlang cookie)
所有节点必须使用相同的 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. 系统调优
-
调整
/etc/docker/daemon.json{ "default-ulimits": { "nofile": { "Name": "nofile", "Hard": 1048576, "Soft": 1048576 } } }
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 exec 或 crictl 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 挂载数据目录。