Server 多实例高可用设计
状态:已实现 — 成员表(复用 RegistryStore 自注册 + 租约)、实例互联与 owner 转发(含 fencing epoch / 一跳防环)、
cluster.enabled接线与集群拓扑页均已落地;测试见internal/cluster/。部署形态(L4 LB / 多宿主 / K8s)参见负载均衡指南。调用路径补全(2026-09-11):读路径聚合(
/functions/instances系列接共享归属表)与写/调用路径的 owner 转发均已落地——同步 invoke、异步任务(start_task)、任务取消(cancel_task)、广播(broadcast)三类共用同一转发帧(§5.4kind);dispatcher 候选集本地为空或 failover 耗尽本地候选时,经共享归属表 +agent_sessions快照表同步兜底(RemoteAgentSource,1s 预算,出错降级)补远端候选再转发(§5.5);跨实例任务取消从共享task_runs解析 agent。refreshRemoteSnapshots30s 回灌降级为性能优化层。双活切换步骤与剩余已知边界(mesh 明文、哈希落点等)见负载均衡指南「部署模式约束」。
本文档定义 Croupier Server 控制面从单实例演进为多实例高可用(HA)部署的目标设计,覆盖问题分析、方案选型、共享目录、实例互联、转发协议、故障语义与实施拆解。
1. 背景与问题
当前 Server 控制面是单点:
- Agent 会话、函数路由表存放在
internal/platform/registry/store.go的进程内存 map(RWMutex 保护) - Agent 与 Server 之间是一条物理绑定的 TCP session 长连接
dispatch层的NewDispatcherWithHA只解决"选哪个 Agent"的高可用,不解决 Server 自身的高可用
由此产生三个后果:
- 宕机 = 全平台停摆:Server 崩溃或升级重启期间,注册表丢失、所有 Agent 断连,GM 功能整体不可用
- 无法水平扩容:部署两台 Server 时,请求被负载均衡分到未持有目标 Agent 的实例上,会因内存注册表查无此 Agent 而调用失败(脑裂式错误)
- 无滚动升级能力:任何发版都伴随全平台中断窗口
2. 目标与非目标
目标
- Server 宕机影响面从"全平台瘫痪"收敛为"该局实例名下 Agent 秒级中断 + 自动故障转移"
- HTTP 读路径(权限、审计、数据浏览)在任意存活实例上全程可用
- 支持滚动重启/升级,全程业务无感
- 单实例部署体验不变:不强制引入任何新的外部组件
非目标
- 不追求"连接永不中断"——持有连接的进程死亡时 TCP 连接必断,HA 保证的是局部影响 + 自动快速恢复 + 状态不丢(与 Kafka/etcd/Sentinel 的 HA 语义同构)
- 不做 SaaS 多租户扩容,实例规模预期 2~5 台
- 不改变 Agent 出站连接模型(游戏网络"只出不进"的安全约束不变)
3. 方案选型
3.1 共享状态机制选型
多实例协同必须有一种"实例间共享状态/达成共识"的机制,候选路线:
| 路线 | 新增依赖 | 评价 |
|---|---|---|
| A. 复用现有 DB(MySQL/PG)存注册表 | 无 | 及格线方案;心跳写入需降频/批量 |
| B. Redis 存注册表 | Redis | 业界标准做法,天然 TTL/pub-sub;仓库 cache/MQ 已有 Redis 实现 |
| C. 嵌入共识(Raft/gossip) | 无 | 自行开发分布式逻辑成本过高,对本项目规模过度设计 |
| D. L4 粘滞路由 + 重连重注册 | 无 | 改动最小但负载不均、故障切换有空窗,仅作过渡 |
结论:RegistryStore 抽象 + 多实现,复用仓库既有的可插拔存储模式(cache 的 local/redis/null、MQ 的 redis/kafka/noop、RateLimitStore)。
3.1.1 业界模式对比:反向隧道 Agent + 多副本控制面
本问题在分布式系统中是经典难题:"反向隧道型 Agent + 多副本控制面,请求如何找到持有连接的实例"。业界有五种成熟模式,均有开源项目背书:
模式 1:消息总线做路由层(连接管理层独立化)
代表:Pitaya(topfreegames,Go 游戏服务器框架)。
GameServer ──► Edge(连接层,只持有连接)◄──► NATS(消息总线)◄──► Logic Server(业务层)
▲ ▲
etcd(服务发现)─────────────┘前端服务器只持有连接,业务服务器经 NATS 发 RPC,总线把消息路由到订阅对应 subject 的连接持有者;服务发现用 etcd。无显式转发代码。
- 独特收益:业务层重启/升级时 Agent 连接完全不掉(连接在 Edge 层,业务层无状态)
- 代价:强制引入 NATS(自身需集群化,成为新的关键路径)+ etcd;所有调用流量过一遍总线(带宽放大、延迟加一跳);排障多一个黑盒
模式 2:Agent 连所有副本
代表:Teleport ≤ v10。Agent 对每台 Proxy 维持一条反向隧道,任何 Proxy 都有直达通道,零转发。
- 优点:路由逻辑为零,延迟最低
- 缺点:N×M 连接、Agent 变重、单台 Proxy 重启引发全量 Agent 惊群重连
- 关键证据:Teleport 在 v11 亲手放弃了该模式,转向模式 3(大规模集群下连接风暴不可承受)
模式 3:共享目录 + 实例间转发
代表:Teleport v11+(Proxy Peering)。Agent 只连一台,Proxy 之间互相转发。即本文档方案(见第 4、5 章)。
模式 4:调用方侧路由
代表:Tailscale DERP、Kubernetes Konnectivity。不转发,由控制面目录告诉调用方"目标连接在哪台实例",调用方自己连到该实例投递(Konnectivity:apiserver 与所有 konnectivity-server 副本保持连接,按 agentID 哈希选目标副本,选错则重解析重试)。
- 对应到 Croupier:入口网关按
gameId/agentId一致性哈希路由 HTTP 请求到 owner 实例 - 优点:无转发代码、无跳跃延迟
- 缺点:复杂度转移到网关/LB 层;Agent 重连换实例后哈希映射需跟随;网关自身成为关键组件
模式 5:单点隧道 + 外部兜底
代表:frp、rathole。隧道服务器单点,靠 keepalived/多入口 + 客户端重连兜底。Croupier 现状本质即此类。
五模式对比:
| 维度 | ① Pitaya 总线 | ② Agent 连所有 | ③ 目录+转发 | ④ 调用方路由 | ⑤ 单点兜底 |
|---|---|---|---|---|---|
| 新增基础设施 | NATS + etcd | 无 | 无(复用 DB/Redis) | 智能网关 | 无 |
| Agent 改动 | 无 | 大(多连接) | 无 | 无 | 无 |
| Server 改动 | 拆 edge/logic 两层 | 小 | 中(转发协议) | 小 | 无 |
| 业务重启掉 Agent 连接 | 不掉 | 部分掉 | 只掉该实例名下 | 只掉该实例名下 | 全掉 |
| 调用延迟 | +1 跳(总线) | 直达 | 可能 +1 跳 | 直达 | 直达 |
| 故障爆炸半径 | 总线故障 = 全局 | 1/M | 1/M | 1/M | 全局 |
| 规模天花板 | 高 | 低(连接风暴) | 中高 | 中 | — |
| 业界验证 | 游戏后端大规模实战 | Teleport 已弃用 | Teleport 现役 | Tailscale/K8s 现役 | 小团队 |
选型结论:
- 现阶段选模式 ③:无新组件、Agent 零改动、Teleport 演进路径(②→③)提供了大规模实战背书
- 模式 ① 作为终态演进选项保留:当实例规模上升、发版频率高到"秒级中断"不可接受时,升级为 edge/logic 两层 + 总线路由。届时模式 ③ 的共享目录与 fencing 设计不会浪费,总线模式下依然需要
- 模式 ② 已被 Teleport 否决;模式 ④ 意味着自行开发智能网关,投入产出不划算
3.2 关键设计决策:显式配置,不与数据库驱动联动
存储实现不得根据 database.driver 隐式切换(sqlite→memory、mysql→redis 之类的猜测),原因:
- 数据库类型 ≠ 部署形态:MySQL 单实例部署很常见,不应被强制要求 Redis
- 隐式切换产生"惊喜行为":同一配置换驱动即改变运行时行为,排障困难
- 正确做法:显式配置 + 保守默认值 + 启动时 fail-fast 校验
registry:
store: memory # memory | db | redis,默认 memory
redis:
addr: localhost:6379
keyPrefix: "croupier:registry:"校验规则(接入既有 config validate 入口):
store: memory:启动打警告"单实例注册表,多副本部署将导致函数路由错误"store: redis未配addr:启动报错store: db:复用database.dataSource,零新依赖
4. 总体架构
两类入口两层 LB(易混淆点):Agent 的 TCP 长连接走 L4 (haproxy/nginx stream,四层透传、按活跃会话打散);Dashboard HTTP API 走 L7(nginx 反代 + split_clients)。二者职责不同不能合并—— 运维中心大屏/操作经 L7 到达任意实例(无状态路由),Agent 会话经 L4 打散后由 owner 转发语义兜底。详见部署文档的「负载均衡方案选型」。
### 4.1 状态分层:什么能共享,什么必须留在本地
注册表中的状态分两类,必须拆开:
| 内容 | 位置 |
| -------------------------------------------------- | ------------------------ |
| Agent 元数据(ID、game/env、健康分、心跳时间) | 共享目录 |
| 函数路由表(functionId → agentId → ownerInstance) | 共享目录 |
| **TCP 连接句柄(net.Conn)** | **本进程内存,不可共享** |
共享的组件准确说是"**目录服务**"(谁在线、有什么能力、连在哪台实例上);连接表永远留在本地。
### 4.2 为什么需要实例间转发(而非 Server 直连 Agent)
Agent 部署在游戏 VPC/内网,安全策略为"只出不进",隧道方向是 Agent → Server。目录能记录 Agent 归属,但**变不出从其他实例直达 Agent 的网络通路**。因此非 owner 实例收到调用请求时,必须通过互联通道请 owner 代发——这是 reverse tunnel 架构(frp、konnectivity、Azure Relay)的标准解法。
被否决的替代方案:
- **Server 直连 Agent(Server → Agent 拨号)**:要求游戏网络开入站防火墙,推翻安全模型,Agent 还需处理多 Server 并发连接,改动量比转发大一个数量级
- **Agent 全连接网(连所有 Server)**:N×M 连接风暴、注册风暴,且任务取消/幂等/事件序号的多 session 一致性协调复杂度远超转发
- **纯粘滞路由**:仅作过渡,扩容再均衡和故障切换质量差
## 5. 实例互联设计
### 5.0 Interconnect 抽象(实现基座)
互联层抽象为 `internal/cluster.Interconnect` 接口——转发逻辑(owner 发现、
建连/复用、一跳限制、fencing)全部封装在实现内,dispatch 层只依赖接口:
```go
type Interconnect interface {
// Forward 把调用请求转给持有目标 Agent 连接的实例。
Forward(ctx context.Context, agentID string, req *ForwardedInvoke) (*ForwardedResult, error)
// Peers 返回当前已知存活对端(诊断/测试)。
Peers() []PeerInfo
}这不是引入真 broker 组件(NATS 类总线是模式①的终态演进);转发本身就是 "掮客",接口化的价值:调用方零感知、实现可替换(模式③→①升级不动调用方)、 测试可用替身验证转发语义。
5.1 实例身份与发现:共享目录,无 seed、无静态 peers
结论:不引入 seed 节点,不配置静态 peers 列表。
理由:seed/gossip 是"没有外部共享存储时集群自组织"的方案(Cassandra 没有 成员表存放处才用 gossip);本设计多实例模式强制共享目录(RegistryStore 的 DB/Redis),成员表 instances 直接放那里——单一事实来源,零新增依赖:
启动 → 写 instances 记录(advertiseAddr + 租约 + epoch)→ 轮询表发现对端 → 懒建连- 扩容第 5 台实例:自注册即被其他实例发现,不改任何配置
- 每实例唯一必填配置是自己的对外地址;共享存储地址在
registry.store(本来就存在),不是新增"种子" - 不维护第二份静态成员表(会与 instances 漂移)
cluster:
enabled: true
instanceId: "" # 空则自动生成 UUID
advertiseAddr: "10.0.1.11:8444" # 唯一必填:告诉对端怎么连我
heartbeatInterval: 5s
leaseTtl: 15s # 3 个心跳周期每台 Server 启动时写入 instances 记录并周期续租:
{
"instanceId": "server-a-7f3d",
"advertiseAddr": "10.0.1.11:8444",
"startedAt": "...",
"leaseExpireAt": "...",
"epoch": 42
}instanceId:可配置,空则启动时生成 UUIDepoch:每次启动递增的任期号,充当 fencing token- 租约 TTL 过期(建议 3 个心跳周期)即判定实例死亡
边界情况:
- 共享存储抖动:本地缓存 last-known peers 继续转发;降级只影响新成员发现
- 存储 vs 现实不一致:以租约 TTL + epoch 裁决
- advertiseAddr 误配(如 127.0.0.1):注册后自检(从对端视角 dial 验证可达), 不可达启动告警
5.2 拓扑与传输
- 懒连接全互联(lazy mesh):首次需要转发时才建立到对端的连接,之后连接池复用;实例规模 2~5 台,mesh 足够
- 复用自行开发 session 传输基座(
internal/transport/tcp),新增第三种握手角色:
握手消息: { role: "server", instanceId: "server-b", epoch: 17 }- 不引入 RPC 框架(与 传输层决策 一致)
- 双向多路复用天然支持转发的任务事件流(progress/log 事件需穿过转发层回传给 SSE 调用方)
- 独立监听端口(如 8444),与 Agent-facing 端口分离,防火墙只放行集群内网段
5.3 互联安全
设计目标(mTLS)与当前实现的差距:
- 设计:复用既有 mTLS CA(devcert/tlsutil/证书监控),互联端口只接受
role = server的对端证书,Agent 证书连接直接拒绝 - 现状:互联是内网明文 TCP(
Insecure: true,ClusterConfig 尚无证书配置面),握手仅校验 hello 的 role 字符串,无对端认证。信任边界完全依赖网络隔离——互联端口(interconnectAddr)只允许集群内网可达,接 mTLS 前不得暴露公网 - owner 不重查 policy/approval(实现决策,偏离早期设计):转发请求携带原始调用者上下文(username/roles/adminId/traceId),但 owner 只落审计、不重新执行权限校验。理由:caller 已走完完整鉴权链(policy 命中 + 审批通过后审批续跑的二次调用经转发到达 owner),owner 重查会因审批上下文不在本实例而卡死审批续跑。该决策的前提同样是「互联端口仅集群内网可达」——内网实例被视作可信方
- owner 侧审计:每次转发投递(成功/失败)落一条
function.invoke审计(actor 与 caller 侧审计同键,details 携forwarded: true/kind/agent_id)。审计哈希链的 sequence 分配在多实例并发下会撞唯一约束(各实例 memCache 视角独立)——AuditService.Log检测到链冲突即重查共享库真实链尾重算 sequence/prev/hash 后重试(≤3 次),唯一约束兜底保证只有与链尾正确衔接的行能落库,链完整性不因多实例分叉;重试耗尽留 Warn 日志
5.4 转发协议与两条铁律
新增内部消息 MsgForwardInvokeReq (0x060103)。实现偏差:帧 body 是 ForwardedInvoke 的 JSON(非 protobuf 信封;内部协议,字段名 lowerCamelCase);forwarded/callerEpoch 由 Interconnect.Forward 统一盖戳、ServeForwardHandler 统一校验,调用方不填。
{
"agentId": "…",
"functionId": "…",
"payload": "<proto 字节,base64>",
"metadata": { "gameId": "…", "env": "…", "taskId": "…" },
"idempotencyKey": "…",
"kind": "",
"taskId": "…",
"forwarded": false,
"callerEpoch": 42,
"caller": {
"adminId": 1,
"username": "…",
"roles": ["…"],
"gameId": "…",
"env": "…",
"traceId": "…"
},
"timeoutMs": 0
}kind区分三类转发调用:""(同步 invoke,首个使用者,向后兼容)/start_task(异步任务投递)/cancel_task(任务取消)。防环/fencing/盖戳三类共用taskId仅cancel_task使用(目标任务);start_task的服务端任务 ID 在metadata.taskId里随 InvokeRequest 语义透传(agent 侧同解析路径)timeoutMs字段保留但未启用(沿用 caller ctx deadline)
铁律一:最多一跳。 forwarded: true 的请求若 owner 发现自己也不是 owner(目录过期),不得再次转发,返回 not_owner 错误,由调用方重新解析目录重试。杜绝转发环路。
铁律二:fencing 校验。 owner 执行前对比目录中的 epoch 与本地 epoch;若目录显示更新的实例已接管该 Agent,说明自身是网络分区恢复后的"僵尸 owner",必须拒绝执行。防脑裂双写。
5.5 完整调用路径与职责拆分
1. 运营人员请求落到任意 Server B
2. B 查共享目录: function X → agent-1 → owner = A, epoch = 42
3. B 懒建立/复用到 A 的互联连接,发 ForwardInvoke(携带 caller 上下文)
4. A 校验: 我是 owner 吗?epoch 匹配吗?
5. A 走本地既有 dispatch 路径,通过隧道发给 agent-1
6. 结果经 A → 互联连接 → B 回运营人员;异步任务事件经 A 落共享库
(task_events),B 的 HTTP 轮询/SSE 从共享库读取——事件不走 mesh对调用方完全透明。
三类调用路径的 caller/owner 职责拆分:
| 职责 | caller 实例(HTTP 入口) | owner 实例(持有 agent 连接) |
|---|---|---|
| 候选集 | 本地 registry,空则查归属表兜底 | —(转发请求已指定 agent) |
| policy / 审批 | 完整鉴权链(policy 命中 + 审批创建) | 不重查,只审计 |
| 同步 invoke | 失败可换候选重试(failover) | InvokeRequestOnAgent 定向投递 |
| async start_task | 生成服务端 task ID + 写 task_runs 行 + | StartTaskOnAgent 只投递(metadata.taskId |
| registerTask + 成功后回 task ID;与 | 沿用 caller 的 ID,不建行不注册路由) | |
| invoke 同款 failover(失败尝试行标 | ||
| failed,耗尽映射 503) | ||
| broadcast | 候选集 local ∪ remote(去重本地优先), | 逐 agent 同 invoke 转发投递 |
| 半失败落 Failures(HTTP 200) | ||
| cancel_task | task routing miss 时从共享 task_runs | CancelTaskOnAgent 只投递;取消成功后的 |
| 解析 agent;成功后 unregisterTask | 路由清理在 caller | |
| 任务事件 | HTTP 轮询/SSE 读共享库 | agent 事件落共享库(task_events) |
候选集兜底(RemoteAgentSource):本地候选为空(或 failover 耗尽本地候选)时,同步查共享归属表 + agent_sessions 快照表(1s 预算,出错降级不放大故障)补远端候选,选中的远端候选经转发执行。refreshRemoteSnapshots 的 30s 周期回灌因此从正确性依赖降级为性能优化层。
registry 内存会话的生命周期与归属表对齐(无行即清):本实例连接断开即删(RemoveAgentIfStale 的 notAfter 校验挡住断连瞬间重连注册的竞态)、重启恢复按归属表活跃全集过滤快照行、30s 对账周期清理「归属表无行且 LastSeen 超 5min 宽限(> ownerTTL 3min,防 Touch 抖动误删)」的孤儿副本——归属表无行 = 无任何实例持有连接,内存条目随之消亡。DB agent_sessions 快照行不删不改、自然过期,避免与重连注册的 Upsert 写竞态。
6. 故障语义
6.1 连接分布
多副本部署时 Agent 连接经 L4 LB 打散到各实例,单实例故障只直接影响其名下 1/M 的 Agent。
6.2 故障转移时间线
t=0s Server A 宕机,A 名下 Agent 的 TCP 断开
t≤1 周期 Agent 心跳循环发现断连(Connected()=false)→ 立即重新拨号;
网络半开(无 RST)时需连续 2 次心跳失败 ≈2 个周期才判死
t+拨号 经 L4 LB 重连,分发到存活实例 B(落在哪台由 LB 决定)
t+ε 全量重注册(函数 descriptor + providers 全量重发,非增量):
B 写内存会话 + ClaimOwner 覆盖归属行
秒级 归属表指向 B,后续调用直达 B 或经转发到 B,平台恢复心跳周期按 Agent 配置(configs/agent.yaml 示例为 30s,未配置时代码回退 3s,internal/app/agent/upstream.go);ownerTTL 默认 3 分钟 = 30s × 6 容忍。
期间:
- HTTP 读路径全程无感(状态在 DB/共享目录)
- B/C 名下 Agent 的调用路径全程无感
- 仅 A 名下 Agent 有秒级调用中断;在途任务按既有状态机标记
timed_out/ 失败
6.2.1 Agent 重连切换的端到端机制
§6.2 时间线背后是一组明确的触发、裁决与防竞态规则(均为实现事实,代码入口 internal/app/agent/upstream.go、internal/server/control_handler.go、internal/server/tcp_listener.go、internal/cluster/)。
断连检测的三个入口(Agent 侧):
| 入口 | 触发条件 | 延迟 |
|---|---|---|
| 心跳循环发现连接断开 | Connected()=false(RST/FIN 即断) | ≤1 个心跳周期 |
| 心跳连续失败 2 次 | 网络半开、server 无响应 | ≈2 个心跳周期 |
| 启动时连接失败的后台重连循环 | 进程启动时 server 不可达 | 每 5s 重试 |
保险丝:心跳从失败中恢复后主动重新注册一次,确保会话信息最新(不依赖断连事件本身)。
归属裁决:last-claim-wins,无实例间协商。 Agent 只配置单一 serverAddr(双活下即 L4 LB 的 VIP),重连落到哪台由 LB 决定;新 owner 在 Register 成功时覆盖写归属行(ClaimOwner:instance_id 指向自己、owner_epoch 取本实例任期)。归属表永远以最后一次成功注册为准,两台 server 之间不需要任何协商协议。Agent 从 Register 响应得知当前 owner 实例(reportedOwnerInstance)并随每次心跳上报,服务端存入会话 labels["reportedOwner"]——agent 视角 / 归属表 / 实际连接三方对账,供集群拓扑页做归属漂移探测。
切换瞬间的四层防竞态(新旧两条路径并发收尾):
| 竞态 | 防线 | 实现 |
|---|---|---|
| 旧实例 A 断连清理误删 B 的新归属行 | Release 带 instance_id = self 条件删除 | 行已被 B 覆盖时匹配 0 行,no-op |
| Agent 重连回同一实例:旧连接清理删新会话 | 断连清理按 (agentID, sessionID) 精确删除 | RemoveSession |
| 断连清理删掉刚被重连注册覆盖的内存条目 | notAfter 时间戳校验(新注册的 LastSeen 必晚于断连时刻) | RemoveAgentIfStale |
| 分区恢复的僵尸 owner 继续接受转发 | fencing:帧内 callerEpoch ≠ owner 本地 epoch → not_owner,调用方重解析重试 | ServeForwardHandler |
自愈兜底:
- 心跳自愈:owner 收到心跳但本地 registry 会话丢失(过期清理/替换竞态,TCP 仍活)→ 从归属表回读 scope,仅当归属行确属本实例时重建最小会话并重新 Claim;不是自己的 claim 则拒绝(不得抢他人归属)
- 候选集恢复:其他实例的本地函数候选由 RemoteAgentSource 同步兜底(本地候选为空时查共享归属表 +
agent_sessions快照,1s 预算,§5.5)+refreshRemoteSnapshots30s 回灌(性能优化层)
切换窗口内的调用方语义:
| 场景 | 表现 | 恢复时点 |
|---|---|---|
| 归属行仍指 A、A 已死 | ResolveOwner 与成员租约交叉验证失败 → no live agent | B Claim 后立即 |
| 归属行仍指 A、A 活着但连接已断 | 转发到 A → A 本地无 session → agent unreachable | B Claim 后立即 |
| B 已 Claim、其他实例候选集未刷新 | 本地候选缺失(RemoteAgentSource 同步兜底可即时补) | 同步兜底即时 / 回灌 ≤30s |
6.3 必须做对的工程细节
- 重注册幂等:Agent 重连重注册不得产生重复/过期目录条目(现有注册去重可复用)
- 僵尸条目清理:归属解析双重判活(owner 记录 ownerTTL + 实例成员租约交叉验证,
ResolveOwner),请求不会被转发到死实例;本地内存副本按「归属表无行 + LastSeen 超 5min 宽限」由 30s 对账周期清理(§5.5) - 在途任务对账:实例死亡时其名下 running 任务需启动 reconciliation,标记失败/超时
- 防脑裂:fencing token(epoch)保证分区恢复的实例不再下发调用
6.4 与其他功能的协同
| 场景 | 单实例 | 多实例(已实现) |
|---|---|---|
| Server 宕机 | 100% 功能不可用,人工重启 | 1/M Agent 秒级中断,自动恢复 |
| 升级发版 | 必然全平台中断 | 逐台滚动重启,全程可用 |
| 可用性量级 | ~99.9%(月停机约 43 分钟) | 99.99%+(月停机约 4 分钟) |
7. 实施拆解
| 组件 | 工作量 | 说明 |
|---|---|---|
instances 表 + 租约心跳 | 小 | 模式照搬 agent session 的 TTL 管理 |
RegistryStore 抽象 + memory/db/redis 实现 | 中 | 内存 map 操作语义平移到接口 |
互联端口 + server 握手角色 | 小 | 复用 session 基座 |
ForwardInvoke + 一跳限制 + fencing | 中 | 协议简单,难在测试 |
| 在途任务对账 | 中 | 复用任务状态机 timed_out |
| 多实例混沌/集成测试(模拟分区、宕机) | 大 | 分布式 bug 只能靠故障注入暴露 |
8. 前置条件与排期
本设计依赖以下能力先达到稳定状态(当前均在完善中):
- 任务系统补齐重试/死信后,在途任务对账语义才能定稿
- 共享目录的 DB 实现依赖 database-per-game 路由稳定
- 实施前应先补 K8s/Helm 部署形态,否则多实例缺少标准编排载体
9. 开放问题
- 转发调用是否参与限流/熔断计数(建议:按原始 caller 计入,owner 侧不再重复计数)
- 事件流回传的背压语义跨实例传递细则
store: db实现下心跳写入的降频/批量策略阈值
