Files
im_server/docs/IM-原生Cluster运维部署说明.md
T

13 KiB
Raw Blame History

IM 原生 Cluster 运维部署说明

1. 目标

本文给运维说明 IM Server 如何部署才能真正启用 Tinode 原生 Cluster。

目标效果:

  • 多个 IM Pod 组成同一个 Tinode Cluster,不再是 standalone。
  • 每个 Pod 拥有稳定、唯一的节点名。
  • 节点之间通过 cluster_config.nodes.addr 建立内部 TCP RPC 通信。
  • Topic 请求可以路由到唯一 Topic master,由 master 处理 Topic 内状态机、顺序和实时下发。

如果部署后日志仍出现:

Cluster: running as a standalone server.

说明原生 Cluster 没有启用,跨 Pod Topic 状态机不会生效。

2. 原生 Cluster 启用条件

必须同时满足以下条件:

  1. cluster_self 非空。
  2. cluster_self 的值必须等于 cluster_config.nodes 中某个节点的 name。
  3. cluster_config.nodes 必须包含所有 IM 节点,包括当前节点。
  4. 每个节点的 addr 必须是其他 Pod 可访问的内部地址。
  5. 每个节点必须监听自己的 addr 端口。
  6. 节点之间网络策略、安全组、Service 必须放通 cluster 内部端口。
  7. 至少 2 个节点才算有效 Cluster,生产建议 3 个节点起。

当前代码启动参数由 docker/tinode/entrypoint.sh 传入:

./tinode --config=${CONFIG} --static_data=$STATIC_DIR --cluster_self=$CLUSTER_SELF --pprof_url=$PPROF_URL

所以运维侧必须确保每个 Pod 的环境变量 CLUSTER_SELF 有值,并且值稳定。

3. 推荐部署形态

推荐使用 Kubernetes StatefulSet,不要用普通 Deployment 直接滚动多个副本。

原因:

  • 原生 Cluster 依赖稳定节点名,例如 im-server-0、im-server-1、im-server-2。
  • 原生 Cluster 依赖稳定 DNS,例如 im-server-0.im-server-cluster.default.svc.cluster.local。
  • 普通 Deployment 的 Pod 名会变化,不适合作为 cluster_self。
  • StatefulSet 配合 Headless Service 可以给每个 Pod 提供固定网络身份。

推荐结构:

Headless Service: im-server-cluster
StatefulSet:      im-server
Pod DNS:
  im-server-0.im-server-cluster.<namespace>.svc.cluster.local
  im-server-1.im-server-cluster.<namespace>.svc.cluster.local
  im-server-2.im-server-cluster.<namespace>.svc.cluster.local

4. 端口规划

至少需要区分三类端口:

端口 用途 是否对公网暴露 说明
6060 HTTP/WebSocket/API 通过 Ingress/SLB 暴露 客户端连接入口。
16060 Tinode gRPC 客户端端口 通常不对公网暴露 管理或内部 gRPC 客户端使用。
12000 原生 Cluster 内部 RPC 不对公网暴露,只在 Pod 间放通 cluster_config.nodes.addr 使用。

注意:cluster_config.nodes.addr 使用的是原生 Cluster 内部 TCP RPC 地址,不是 grpc_listen 的 16060。

建议统一所有 Pod 的 cluster 监听端口为 12000,通过 Pod DNS 区分节点:

{
  "name": "im-server-0",
  "addr": "im-server-0.im-server-cluster.default.svc.cluster.local:12000"
}

不要沿用 docker compose 示例里的 tinode-0:12000、tinode-1:12001、tinode-2:12002,除非是在同一主机或 docker compose 网络里部署。

5. 配置文件要求

server/tinode.conf 默认配置是:

"cluster_config": {
  "self": "",
  "nodes": [],
  "failover": {
    "enabled": false
  }
}

这个配置不会启用 Cluster。

生产或测试环境需要提供完整配置。建议 3 节点配置如下:

"cluster_config": {
  "self": "",
  "nodes": [
    {
      "name": "im-server-0",
      "addr": "im-server-0.im-server-cluster.default.svc.cluster.local:12000"
    },
    {
      "name": "im-server-1",
      "addr": "im-server-1.im-server-cluster.default.svc.cluster.local:12000"
    },
    {
      "name": "im-server-2",
      "addr": "im-server-2.im-server-cluster.default.svc.cluster.local:12000"
    }
  ],
  "failover": {
    "enabled": true,
    "heartbeat": 100,
    "vote_after": 8,
    "node_fail_after": 16
  }
}

说明:

  • self 可以保持为空,由启动参数 --cluster_self=$CLUSTER_SELF 覆盖。
  • CLUSTER_SELF 必须分别为 im-server-0、im-server-1、im-server-2。
  • nodes 必须所有 Pod 完全一致。
  • addr 必须能被其他 Pod 访问。
  • 3 节点建议开启 failover;2 节点不建议开启 failover。

6. Kubernetes 示例

以下示例只展示 Cluster 相关关键配置,实际部署时需要合并现有镜像、环境变量、Secret、资源限制、探针、Ingress 和业务配置。

6.1 Headless Service

apiVersion: v1
kind: Service
metadata:
  name: im-server-cluster
  namespace: default
spec:
  clusterIP: None
  selector:
    app: im-server
  ports:
    - name: cluster
      port: 12000
      targetPort: 12000
    - name: http
      port: 6060
      targetPort: 6060
    - name: grpc
      port: 16060
      targetPort: 16060

6.2 客户端入口 Service

客户端入口可以继续使用普通 Service 或 Ingress,负载到所有 Pod:

apiVersion: v1
kind: Service
metadata:
  name: im-server
  namespace: default
spec:
  sessionAffinity: None
  selector:
    app: im-server
  ports:
    - name: http
      port: 6060
      targetPort: 6060

6.3 StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: im-server
  namespace: default
spec:
  serviceName: im-server-cluster
  replicas: 3
  selector:
    matchLabels:
      app: im-server
  template:
    metadata:
      labels:
        app: im-server
    spec:
      containers:
        - name: im-server
          image: <im-server-image>
          ports:
            - name: http
              containerPort: 6060
            - name: grpc
              containerPort: 16060
            - name: cluster
              containerPort: 12000
          env:
            - name: CLUSTER_SELF
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: EXT_CONFIG
              value: /etc/tinode/tinode.conf
            - name: CLUSTER_PROXY_TO_MASTER_BUFFER
              value: "4096"
            - name: CLUSTER_PROXY_TO_MASTER_WORKERS
              value: "16"
            - name: CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS
              value: "300"
            - name: CLUSTER_ROUTE_RETRIES
              value: "3"
            - name: CLUSTER_ROUTE_RETRY_BACKOFF_MS
              value: "25"
          resources:
            requests:
              cpu: "1"
              memory: 512Mi
            limits:
              cpu: "2"
              memory: 1Gi
          readinessProbe:
            httpGet:
              path: /healthz
              port: 6060
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /livez
              port: 6060
            timeoutSeconds: 3
          volumeMounts:
            - name: tinode-config
              mountPath: /etc/tinode
              readOnly: true
      volumes:
        - name: tinode-config
          configMap:
            name: im-server-tinode-conf

重点:

  • serviceName 必须指向 Headless Service。
  • CLUSTER_SELF 直接取 Pod 名,StatefulSet 下会稳定为 im-server-0、im-server-1、im-server-2。
  • ConfigMap 中的 cluster_config.nodes.name 必须和 Pod 名一致。
  • ConfigMap 中的 cluster_config.nodes.addr 建议使用 StatefulSet Pod DNS。

7. NetworkPolicy / 安全组要求

如果集群启用了 NetworkPolicy,需要允许 IM Pod 之间访问 cluster 端口:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: im-server-cluster-allow
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: im-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: im-server
      ports:
        - protocol: TCP
          port: 12000
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: im-server
      ports:
        - protocol: TCP
          port: 12000

如果没有 NetworkPolicy,可以跳过此项,但仍要确认云厂商安全组、服务网格策略没有拦截 Pod 间 TCP 12000。

8. 启动顺序

建议:

  1. 先部署 ConfigMap、Headless Service、普通入口 Service。
  2. 再部署 StatefulSet。
  3. 第一个 Pod 可以执行数据库初始化和升级。
  4. 其他 Pod 设置 NO_DB_INIT=true,避免多 Pod 同时初始化数据库。
  5. 等所有 Pod Ready 后再接入客户端流量。

如果当前镜像沿用 docker/tinode/entrypoint.sh 的逻辑:

  • 第一个 Pod 可以保留 NO_DB_INIT=false。
  • 其他 Pod 建议设置 NO_DB_INIT=true。
  • 如果所有 Pod 同时启动,要确认数据库初始化脚本具备幂等能力,否则建议改成 init Job 单独执行。

9. 发布步骤建议

9.1 灰度前检查

执行:

kubectl -n default get pod -l app=im-server -o wide
kubectl -n default get svc im-server-cluster
kubectl -n default exec im-server-0 -- sh -c 'echo $CLUSTER_SELF'
kubectl -n default exec im-server-1 -- sh -c 'echo $CLUSTER_SELF'
kubectl -n default exec im-server-2 -- sh -c 'echo $CLUSTER_SELF'

期望:

im-server-0
im-server-1
im-server-2

验证节点 DNS:

kubectl -n default exec im-server-0 -- sh -c 'nc -vz im-server-1.im-server-cluster.default.svc.cluster.local 12000'
kubectl -n default exec im-server-0 -- sh -c 'nc -vz im-server-2.im-server-cluster.default.svc.cluster.local 12000'

9.2 启动后日志检查

查看日志:

kubectl -n default logs im-server-0 | grep -i cluster
kubectl -n default logs im-server-1 | grep -i cluster
kubectl -n default logs im-server-2 | grep -i cluster

不能出现:

Cluster: running as a standalone server.

如果出现该日志,优先检查:

  • CLUSTER_SELF 是否为空。
  • CLUSTER_SELF 是否不在 cluster_config.nodes.name 中。
  • 配置文件是否确实被容器读取。
  • EXT_CONFIG 是否指向了正确文件。
  • cluster_config.nodes 是否为空。

如果出现节点连接失败,优先检查:

  • cluster_config.nodes.addr DNS 是否能解析。
  • 端口 12000 是否监听。
  • Pod 间 NetworkPolicy 是否放通。
  • Service targetPort 是否正确。

9.3 功能验收

至少验证:

  1. A、B 两个账号分别连到不同 Pod,P2P 消息实时互达。
  2. A、B 分别连到不同 Pod,语音通话 ringing、accept、hang-up 状态实时互达。
  3. 同一个账号多端连接不同 Pod,recv/read 回执正常。
  4. 群聊成员分布在不同 Pod,群消息实时下发正常。
  5. 重启任意一个 Pod 后,客户端能重连,Topic 可重新订阅。
  6. 日志无持续 cluster unreachable、proxy timeout、dial timeout。

10. 与 Redis Fanout 的关系

启用原生 Cluster 后,Redis 仍然保留,但职责要收敛:

  • 保留:全局 presence、session control、踢端、同平台挤旧端、大群在线缓存、热点缓存、限流、幂等、补偿。
  • 收敛或关闭:Topic data/info/pres 的 Redis topic fanout。
  • 收敛或关闭:通话 call_event/topic 的 Redis fanout。

否则会出现同一个 Topic 事件既由 Cluster 下发、又由 Redis Fanout 下发,导致重复 data/info/pres/call event。

对应职责边界参考:

  • docs/IM-原生Cluster与RedisFanout职责边界.md

11. 常见问题

11.1 日志仍然是 standalone

原因通常是 cluster_self 为空或配置文件里的 nodes 为空。

处理:

  1. 确认 Pod 环境变量 CLUSTER_SELF。
  2. 确认启动参数包含 --cluster_self=$CLUSTER_SELF。
  3. 确认容器读取的是带完整 cluster_config.nodes 的配置。

11.2 Pod 能启动,但跨 Pod 消息还是不通

原因通常是 cluster 内部端口不通,或节点地址写错。

处理:

  1. 从任意 Pod nc -vz <peer-pod-dns> 12000。
  2. 检查 Headless Service selector 是否匹配 Pod label。
  3. 检查 NetworkPolicy 是否放通 egress/ingress。
  4. 检查 cluster_config.nodes.addr 是否写成了只在 docker compose 里可用的短域名。

11.3 扩缩容后部分 Topic 异常

原生 Cluster 的 Topic master 基于节点集合计算。扩缩容会改变节点集合,可能触发 Topic 重新路由。

建议:

  • 生产先固定 3 节点。
  • 不要频繁自动扩缩容 IM Server。
  • 扩缩容必须配合完整压测和重连验证。
  • 如果要从 3 节点扩到 5 节点,先更新所有 Pod 的 cluster_config.nodes,再按发布计划滚动。

11.4 2 节点能不能用

2 节点可以启用基础 Cluster,但不建议开启 failover,也不建议生产使用。

生产推荐 3 节点起,便于 leader 心跳和故障判断。

12. 运维上线检查清单

  • 使用 StatefulSet,不使用普通 Deployment 承载 Cluster 节点身份。
  • 创建 Headless Service。
  • 每个 Pod 的 CLUSTER_SELF 等于 Pod 名。
  • cluster_config.nodes 包含所有 Pod。
  • 所有 Pod 使用同一份 cluster_config.nodes。
  • 每个 nodes.addr 可从其他 Pod 访问。
  • 放通 cluster 内部 TCP 端口 12000。
  • 普通 HTTP/WebSocket 入口仍通过 Service/Ingress 负载到所有 Pod。
  • 只有一个入口负责数据库初始化,或数据库初始化已改为独立 Job。
  • 启动日志不再出现 Cluster: running as a standalone server.。
  • 跨 Pod P2P、群聊、read/recv、通话状态已验收。
  • 原生 Cluster 启用后,Topic 相关 Redis Fanout 已按职责边界关闭或降级。