Skip to content

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.4 kind);dispatcher 候选集本地为空或 failover 耗尽本地候选时,经共享归属表 + agent_sessions 快照表同步兜底(RemoteAgentSource,1s 预算,出错降级)补远端候选再转发(§5.5);跨实例任务取消从共享 task_runs 解析 agent。refreshRemoteSnapshots 30s 回灌降级为性能优化层。双活切换步骤与剩余已知边界(mesh 明文、哈希落点等)见负载均衡指南「部署模式约束」。

本文档定义 Croupier Server 控制面从单实例演进为多实例高可用(HA)部署的目标设计,覆盖问题分析、方案选型、共享目录、实例互联、转发协议、故障语义与实施拆解。

1. 背景与问题 ​

当前 Server 控制面是单点:

  • Agent 会话、函数路由表存放在 internal/platform/registry/store.go 的进程内存 map(RWMutex 保护)
  • Agent 与 Server 之间是一条物理绑定的 TCP session 长连接
  • dispatch 层的 NewDispatcherWithHA 只解决"选哪个 Agent"的高可用,不解决 Server 自身的高可用

由此产生三个后果:

  1. 宕机 = 全平台停摆:Server 崩溃或升级重启期间,注册表丢失、所有 Agent 断连,GM 功能整体不可用
  2. 无法水平扩容:部署两台 Server 时,请求被负载均衡分到未持有目标 Agent 的实例上,会因内存注册表查无此 Agent 而调用失败(脑裂式错误)
  3. 无滚动升级能力:任何发版都伴随全平台中断窗口

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 游戏服务器框架)。

text
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/M1/M1/M全局
规模天花板高低(连接风暴)中高中—
业界验证游戏后端大规模实战Teleport 已弃用Teleport 现役Tailscale/K8s 现役小团队

选型结论:

  • 现阶段选模式 ③:无新组件、Agent 零改动、Teleport 演进路径(②→③)提供了大规模实战背书
  • 模式 ① 作为终态演进选项保留:当实例规模上升、发版频率高到"秒级中断"不可接受时,升级为 edge/logic 两层 + 总线路由。届时模式 ③ 的共享目录与 fencing 设计不会浪费,总线模式下依然需要
  • 模式 ② 已被 Teleport 否决;模式 ④ 意味着自行开发智能网关,投入产出不划算

3.2 关键设计决策:显式配置,不与数据库驱动联动 ​

存储实现不得根据 database.driver 隐式切换(sqlite→memory、mysql→redis 之类的猜测),原因:

  • 数据库类型 ≠ 部署形态:MySQL 单实例部署很常见,不应被强制要求 Redis
  • 隐式切换产生"惊喜行为":同一配置换驱动即改变运行时行为,排障困难
  • 正确做法:显式配置 + 保守默认值 + 启动时 fail-fast 校验
yaml
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 漂移)
yaml
cluster:
  enabled: true
  instanceId: "" # 空则自动生成 UUID
  advertiseAddr: "10.0.1.11:8444" # 唯一必填:告诉对端怎么连我
  heartbeatInterval: 5s
  leaseTtl: 15s # 3 个心跳周期

每台 Server 启动时写入 instances 记录并周期续租:

json
{
  "instanceId": "server-a-7f3d",
  "advertiseAddr": "10.0.1.11:8444",
  "startedAt": "...",
  "leaseExpireAt": "...",
  "epoch": 42
}
  • instanceId:可配置,空则启动时生成 UUID
  • epoch:每次启动递增的任期号,充当 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),新增第三种握手角色:
text
握手消息: { 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 统一校验,调用方不填。

json
{
  "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 完整调用路径与职责拆分 ​

text
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_tasktask routing miss 时从共享 task_runsCancelTaskOnAgent 只投递;取消成功后的
解析 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 故障转移时间线 ​

text
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)+ refreshRemoteSnapshots 30s 回灌(性能优化层)

切换窗口内的调用方语义:

场景表现恢复时点
归属行仍指 A、A 已死ResolveOwner 与成员租约交叉验证失败 → no live agentB Claim 后立即
归属行仍指 A、A 活着但连接已断转发到 A → A 本地无 session → agent unreachableB Claim 后立即
B 已 Claim、其他实例候选集未刷新本地候选缺失(RemoteAgentSource 同步兜底可即时补)同步兜底即时 / 回灌 ≤30s

6.3 必须做对的工程细节 ​

  1. 重注册幂等:Agent 重连重注册不得产生重复/过期目录条目(现有注册去重可复用)
  2. 僵尸条目清理:归属解析双重判活(owner 记录 ownerTTL + 实例成员租约交叉验证,ResolveOwner),请求不会被转发到死实例;本地内存副本按「归属表无行 + LastSeen 超 5min 宽限」由 30s 对账周期清理(§5.5)
  3. 在途任务对账:实例死亡时其名下 running 任务需启动 reconciliation,标记失败/超时
  4. 防脑裂: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 实现下心跳写入的降频/批量策略阈值