459 lines
13 KiB
Markdown
459 lines
13 KiB
Markdown
# IM 原生 Cluster 运维部署说明
|
||
|
||
## 1. 目标
|
||
|
||
本文给运维说明 IM Server 如何部署才能真正启用 Tinode 原生 Cluster。
|
||
|
||
目标效果:
|
||
|
||
- 多个 IM Pod 组成同一个 Tinode Cluster,不再是 standalone。
|
||
- 每个 Pod 拥有稳定、唯一的节点名。
|
||
- 节点之间通过 `cluster_config.nodes.addr` 建立内部 TCP RPC 通信。
|
||
- Topic 请求可以路由到唯一 Topic master,由 master 处理 Topic 内状态机、顺序和实时下发。
|
||
|
||
如果部署后日志仍出现:
|
||
|
||
```text
|
||
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` 传入:
|
||
|
||
```bash
|
||
./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 提供固定网络身份。
|
||
|
||
推荐结构:
|
||
|
||
```text
|
||
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 区分节点:
|
||
|
||
```json
|
||
{
|
||
"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` 默认配置是:
|
||
|
||
```json
|
||
"cluster_config": {
|
||
"self": "",
|
||
"nodes": [],
|
||
"failover": {
|
||
"enabled": false
|
||
}
|
||
}
|
||
```
|
||
|
||
这个配置不会启用 Cluster。
|
||
|
||
生产或测试环境需要提供完整配置。建议 3 节点配置如下:
|
||
|
||
```json
|
||
"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
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```yaml
|
||
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
|
||
|
||
```yaml
|
||
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 端口:
|
||
|
||
```yaml
|
||
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 灰度前检查
|
||
|
||
执行:
|
||
|
||
```bash
|
||
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'
|
||
```
|
||
|
||
期望:
|
||
|
||
```text
|
||
im-server-0
|
||
im-server-1
|
||
im-server-2
|
||
```
|
||
|
||
验证节点 DNS:
|
||
|
||
```bash
|
||
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 启动后日志检查
|
||
|
||
查看日志:
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
不能出现:
|
||
|
||
```text
|
||
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 已按职责边界关闭或降级。
|