Kubernetes 部署(Helm)
Chart 位于 deploy/helm/imcore。它部署 websocket 主服务,外加可选的 persist_consumer / ai_worker / sync_worker 部署,内置滚动升级、优雅摘流、PodDisruptionBudget 和 HPA。Kubernetes 是三种受支持部署形态之一——compose(deploy/compose/)与 systemd(deploy/systemd/ + imctl)同样是一等公民;每套环境选一种。
前置条件
以下多实例规则 chart 无法替你强制:
- 所有副本必须共享同一个 Redis 与同一个 broker(RabbitMQ、NATS 或 Redis Streams)——跨实例 fan-out、在线状态、会话状态都依赖它们。Redis/broker/DB 在本 chart 之外运行(operator、云服务或你自己的清单)。
- AI 子系统(
ai_worker、RAG、人机切换、通话分析)RabbitMQ、NATS 或 Redis Streams 均可用。 Redis Streams 复用必需的 Redis 连接;生产必须开启 AOF,让redis.stream.ai_claim_min_idle_ms大于最慢任务耗时,并按恢复窗口内的最大积压配置maxlen。Helm chart 内置的可选 broker 仍是 RabbitMQ 而非 NATS;选择 Redis Streams 时,同一个 Redis 可同时承担状态/缓存与 broker。 - 迁移在首次启动前手工应用——镜像自带
/app/bin/migrate(见下)。 - 高吞吐多实例:优先 NATS cluster,或 RabbitMQ + quorum queues(纯部署配置,无代码改动)。
安装
- 创建配置 Secret。从
config/app.yaml.example出发;app.yaml 含密钥,必须放 Secret:
kubectl create namespace imcore
kubectl -n imcore create secret generic imcore-config --from-file=app.yaml
要在集群内访问 admin console,需在 app.yaml 里设 admin.console.listen_addr: "0.0.0.0:7080"(默认只绑回环),并配合 service.exposeAdmin。
- 跑迁移(一次性 Pod;重复执行安全——已应用的文件会跳过):
kubectl -n imcore run imcore-migrate --rm -i --restart=Never \
--image=balalaim/imcore-basic:latest \
--overrides='{"spec":{"containers":[{"name":"imcore-migrate","image":"balalaim/imcore-basic:latest","env":[{"name":"APP_BIN","value":"migrate"}],"volumeMounts":[{"name":"config","mountPath":"/app/config/app.yaml","subPath":"app.yaml"}]}],"volumes":[{"name":"config","secret":{"secretName":"imcore-config"}}]}}'
- 安装 chart:
helm install imcore deploy/helm/imcore -n imcore \
--set config.existingSecret=imcore-config \
--set persistConsumer.enabled=true
系统里必须有 persistConsumer 才能持久化房间历史(NATS 模式设 binary: jetstream_consumer)。验证:kubectl -n imcore rollout status deploy/imcore。
全栈一体安装(内置依赖)——仅限 PoC / 离线 / 信创
对于自包含集群(开发环境、离线环境、信创单机式私有化),chart 可以把 Postgres(pgvector)、Redis、RabbitMQ、MinIO 作为子 chart 一并内置,并自动跑迁移,一条命令拉起整套栈:
# 1. 从内置示例创建配置 Secret(按需修改):
cp deploy/helm/imcore/examples/app.bundled.yaml app.yaml
kubectl create secret generic imcore-config --from-file=app.yaml
# 2. 用内置 profile 安装:
helm install imcore deploy/helm/imcore \
-f deploy/helm/imcore/values-bundled.yaml \
--set config.existingSecret=imcore-config
内置依赖是单实例、不具备高可用,不做备份,只适用于 PoC / 开发 / 离线 / 信创单机式私有化。生产环境请保持 postgresql.enabled / redis.enabled / rabbitmq.enabled / minio.enabled 默认的 false,并让 app.yaml 指向外部托管服务(或下方的 operator)。内置的 broker 是 RabbitMQ 而非 NATS,这是打包选择——本 chart 没有内置 NATS 子 chart,不是因为 AI 子系统需要它(AI 子系统现在支持三种 broker,见上文前置条件)。内置数据的备份/恢复不在本 chart 范围内——见 docs/RUNBOOK_BACKUP_RESTORE_ZH.md。
迁移:values-bundled.yaml 设了 migration.enabled=true,因此应用 Pod 启动前会有一个 <release>-migrate Job 先应用 schema,每个应用 Pod 还带一个 wait-for-migrations initContainer,阻塞到该 Job 完成为止。也可以只对外部数据库开启这个行为,不启用任何内置依赖:--set migration.enabled=true。
生产依赖 operator(参考)
生产环境的高可用,请在本 chart 之外用成熟的 operator 运行这些有状态依赖,而不是用内置子 chart:CloudNativePG(Postgres)、RabbitMQ Cluster Operator、某个 Redis operator(例如 Spotahome Redis Operator),以及 MinIO Operator。保持 postgresql/redis/rabbitmq/minio.enabled=false,让 app.yaml 指向 operator 提供的地址。
关键 values
| Value | 默认 | 说明 |
|---|---|---|
replicaCount |
2 | autoscaling.enabled 时忽略 |
image.repository / tag |
balalaim/imcore-basic / appVersion |
大陆镜像见 values.yaml 注释(ACR) |
config.existingSecret / config.inline |
— | 二选一必填,否则渲染即失败。inline 变更自动滚 Pod(checksum 注解);existingSecret 修改后需手动 rollout restart |
ports.http/grpc/admin |
8080/9001/7080 | 必须与 app.httpPort / app.rpcPort / admin.console.listen_addr 一致 |
service.exposeAdmin |
false | 把 7080 加进 Service;务必配 NetworkPolicy,绝不公网暴露 |
updateStrategy |
surge 1 / unavailable 0 | 无损滚动(见下) |
preStopSleepSeconds / terminationGracePeriodSeconds |
5 / 30 | 摘流预算(见下) |
podDisruptionBudget.* |
开启,maxUnavailable 1 | 仅副本 >1(或开 HPA)时渲染 |
autoscaling.* |
关;CPU 70% | customMetrics 追加原生 HPA 指标(见下) |
ingress.* |
关 | 已带 ingress-nginx 的 WebSocket 注解 |
serviceMonitor.* |
关 | /metrics 门控注意事项见下 |
persistConsumer.* |
关 | 公共镜像自带该二进制;2+ 副本即 HA |
aiWorker.* / syncWorker.* |
关 | 需要自建镜像(aiagent / imultimate build tags)——不填 image.repository 渲染即失败 |
env |
[] | 例如从 Secret 注入 IMCORE_GOVERNANCE_PSEUDONYM_SALT |
滚动升级与优雅摘流
一次滚动如何做到无损(与 cmd/websocket/main.go 的停机路径对应):
maxSurge: 1, maxUnavailable: 0——新 Pod 必须先 Ready(/healthz),旧 Pod 才会被要求停止。- Pod 从 Service endpoints 摘除;
preStop睡preStopSleepSeconds(5s),让 LB/kube-proxy 在它仍在服务时完成摘流。 - SIGTERM:
DrainClients(3s)通知已连接的 WebSocket 客户端(它们重连到存活 Pod 并 catch-up),随后 HTTP/gRPC 关停(5s)、审计日志落盘、persister 停止、broker 拆除。 terminationGracePeriodSeconds: 30宽裕覆盖 2+3;超时才会 SIGKILL。
滚动期间的重连压力以 Pod 为界——每一步只有一个 Pod 的客户端重连,不是全量。websocket.max_connections_per_instance(app.yaml)用 503 甩掉超额握手,让 LB 把流量摆到刚 surge 出来的新 Pod。
helm rollback 按同样的编排反向执行。
升级现有 NATS 部署到 AI-on-NATS
一次性人工步骤,仅 NATS 需要:gowebsocket_persist 从 push consumer 改成了
pull consumer(这样多个 jetstream_consumer 副本才能真正竞争消费——此前第二个
副本会在启动时 panic)。JetStream 不允许把已存在的 push consumer 原地改成
pull,所以必须先删掉旧的再起新版本:
- 把
jetstream_consumer缩到 0(以及所有nats.embedded_persist_inproc: true的 websocket 实例)。 nats consumer rm gowebsocket gowebsocket_persist- 发布新镜像——消费者自动重建,
DeliverAll从 stream 里现存的消息接着处理, 核心 stream 5 分钟MaxAge内的房间历史不会丢。
内嵌 NATS(nats.embedded: true)走不了上面这个顺序——NATS 服务端就是那个
websocket pod,停掉之后第 2 步已经没有对象可连,而 nats.embedded_store_dir 里
旧的 push durable 还在。二进制会对 cannot pull subscribe to push based consumer
直接 panic,于是 pod 崩溃重启循环。两条出路:先用
nats.embedded_persist_inproc: false 起一次、对着 nats.embedded_listen 执行第 2 步、
再改回去;或直接删掉 nats.embedded_store_dir(最多丢 5 分钟未投递的广播,不丢任何
持久业务状态)。完整步骤见 docs/RUNBOOK_BACKUP_RESTORE_ZH.md §6.3。
两个新的 AI stream(<stream_name>_ai_work / _ai_push)由 natsjets.New()
在首次启动时全新创建——不需要对它们做任何步骤,从 RabbitMQ 切到 NATS 也不需要
任何数据迁移(AI 主题不承载持久业务状态,业务状态全在数据库里)。
PodDisruptionBudget
maxUnavailable: 1 让节点排水、集群升级一次只动一个 Pod,而不是同时驱逐多个 WebSocket Pod(每次驱逐 = 该 Pod 的全部客户端重连)。模板只在有真实冗余时渲染(副本 >1 或开 HPA)——单副本上的 PDB 会把排水卡死。
自动扩容
CPU 70% 是安全默认。WebSocket 服务器更好的信号是每 Pod 连接数——把内置 /metrics 的 imcore_current_connections 通过 prometheus-adapter 暴露出来:
# prometheus-adapter values.yaml(rules.custom)
rules:
custom:
- seriesQuery: 'imcore_current_connections{namespace!="",pod!=""}'
resources: { overrides: { namespace: {resource: namespace}, pod: {resource: pod} } }
name: { matches: "imcore_current_connections" }
metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
# imcore values.yaml
autoscaling:
enabled: true
customMetrics:
- type: Pods
pods:
metric: { name: imcore_current_connections }
target: { type: AverageValue, averageValue: "8000" } # 来自你的容量基线
目标值来自实测容量基线(docs/CHAOS_TESTING_ZH.md),不要拍脑袋。HPA 的 scaleDown 刻意放慢(5 分钟窗口后每 2 分钟缩 1 个 Pod),避免长连接在收缩的 Pod 间来回迁移。
指标抓取
GET /metrics 与 /stat 同一道 ops 门控:来源须为回环或在 admin.allowed_cidrs 内,且 admin.require_token(默认 true)时还要 admin token。集群内 Prometheus 二选一:
- 把集群 Pod CIDR 加进
admin.allowed_cidrs并保留 token → 配serviceMonitor.bearerTokenSecret: {name, key}; - 若你的威胁模型接受仅 CIDR 门控,也可再设
admin.require_token: false。
告警规则见 deploy/prometheus/imcore-alerts.yml。
与 compose / systemd 的关系
chart 只部署 imcore 本体。compose 栈为单机捆绑 Postgres/Redis/RabbitMQ/MinIO;systemd + imctl 覆盖裸机机群。Kubernetes 形态假定依赖由托管服务/operator 提供。三种形态的 app.yaml 完全一致——只有注入方式不同。