Appearance
Agent-Server TCP Session 重构设计
状态
- 状态: Accepted Target Design
- 适用范围:
Agent <-> Server - 不在范围内:
SDK <-> Agent
设计结论
本设计用于收敛 Agent <-> Server 主链路的传输模型,核心结论如下:
Agent <-> Server默认传输从当前历史 REQ/REP迁移为独立的tcpsession,并按需启用tls。Agent只保留到Server的主动出站长连接,不再要求Server反向直连Agent暴露的rpc_addr。Server -> Agent的Invoke、StartTask、CancelTask、Ops请求,统一复用已有Agent-Serversession 下发。Agent本地监听地址只服务GameServer / SDK / 第三方应用,不再承担Server -> Agent回拨职责。- 不再把
历史消息模式作为主链路抽象中心,统一收敛为“单连接、双向、多路复用、可重连、可治理”的轻量 session 协议。
shared session runtime 与 subprotocol
这里同样需要明确两个术语:
shared session runtime- 指两条主链路共用的会话传输基座
- 包括
tcp/tls + framing + mux + reconnect + heartbeat + drain + backpressure
subprotocol- 指在该基座上运行的应用层子协议
- 对
Agent <-> Server而言,就是agent-server subprotocol
agent-server subprotocol 的关键特征是:
- 首帧必须是
RegisterRequest或其后续等价 connect 消息 - 默认启用
tls - 面向 agent session,而不是
rpc_addr回拨
背景
当前仓库里,Agent <-> Server 实际上混合了两条不同语义的链路:
- 控制面:
Agent通过历史 REQ/REP主动连接Server,完成Register/Heartbeat - 调用面:
Server/Dispatcher再根据rpc_addr主动拨回Agent,执行Invoke/Task/Ops
这带来了几个问题:
- 传输模型分裂: 控制面一条连接,调用面另一条连接
rpc_addr语义混乱: 看起来像“Agent 的注册信息”,本质却是“供 Server 回拨的入口”- 集群语义不清晰:
Agent实际连到了某个Server节点,但调用面又试图绕开该 session 直接拨Agent 历史 REQ/REP本身不适合“在已有连接上由 Server 主动发新请求”
这不是因为 旧传输 没有长连接能力,而是因为当前使用的 REQ/REP pattern 与目标会话模型不一致。
我们真正需要的东西
当前场景真正需要的不是“某个消息中间件 pattern”,而是一个轻量的应用层 session:
- 底层是一条可靠长连接
- 连接建立后先做身份与能力协商
- 双方都能在同一条连接上主动发起新请求
- 同一连接上允许多个并发 in-flight 请求
- 断线后旧 session 立即失效,重连后重建新 session
- 支持心跳、背压、drain、超时、取消、优雅摘流
换句话说,我们要的不是 REQ/REP,而是“单连接双向会话协议”。
为什么当前场景更适合 TCP Session
业务边界特点
当前 Agent <-> Server 场景有几个非常明确的约束:
- 主要部署在内网、同机房、同集群网络中
- 不需要
libp2p那类打洞、对等发现、多节点 mesh 能力 - 不需要
RSocket那种覆盖大量 transport 和交互模型的通用协议栈 - 不需要默认引入
Aeron这种偏高性能消息基础设施 - 需要的是稳定、简单、可控、可跨版本演进的传输层
因此默认方案应优先满足:
- 简单
- 易于排错
- 便于跨服务统一
- 便于后续与
SDK <-> Agent复用同一套 session 心智模型
为什么不是继续用 历史消息模式
旧传输 本身并不是错误的,问题在于它提供的是“消息模式抽象”,而我们需要的是“会话协议抽象”。
当前主链路继续依赖 旧传输 的问题主要有:
REQ/REP不适合双向主动发请求- 即使用
PAIR等其他模式,session、mux、重连、背压语义仍然要自己补 - 最终复杂度落在我们自己实现的协议层,而不是
旧传输帮我们解决 - 既然协议层仍然要自己做,继续绑定
旧传输只会让模型更绕
结论不是“旧传输 不能长连接”,而是“历史消息模式 不是我们当前主链路最合适的抽象”。
为什么不是 RSocket / libp2p / Aeron / HTTP2
这几个方向都能提供启发,但都不适合作为当前默认基础设施:
RSocket- 优点是 session、双向、多路复用都很完整
- 问题是协议层过重,header 和交互模型都超出当前项目需要
libp2p- 优点是协议协商和 stream multiplexing 很成熟
- 问题是它主要服务点对点网络与多节点覆盖网络,不是当前诉求
Aeron- 优点是同机和低延迟场景很强,后续可作为本地优化参考
- 问题是它更像高性能消息基础设施,不适合作为当前默认控制面
HTTP/2- 优点是 stream/mux 心智模型值得参考
- 问题是完整 HTTP 语义和头部体系并不是我们要的
因此最合适的落点是:
- 参考
HTTP/2的“单连接多路复用”思想 - 保持比
RSocket更轻 - 保持比
Aeron更通用、更易接入 - 直接基于
TCP自己实现最小 session 协议
目标架构
目标拓扑
text
+----------------------+ +----------------------+
| Agent | | Server Node |
| - Session client | | - Session manager |
| - Local dispatch | TCP(+TLS) | - Registry |
| - SDK/Game bridge +-----------> | - Invoke/Task/Ops |
| | | |
+----------------------+ +----------------------+关键变化:
Agent主动连接ServerServer持有Agent sessionServer -> Agent的请求全部走这条现有 sessionAgent本地监听只留给本地接入方,不再暴露给Server
会话语义
连接建立后,Agent 必须在该连接上完成会话注册:
Agent建立tcp连接,按配置可附加tlsAgent发送AgentConnectRequest或等价注册消息Server返回session_id、能力协商结果和告警信息- 双方开始 heartbeat
Server后续在同一连接上向Agent下发Invoke/Task/Ops- 连接断开后 session 立即失效
Agent重连后必须重新注册,拿到新的session_id
传输层设计
默认 transport
- 默认 transport:
tcp - 默认安全模式:
tls.enabled tls是tcp之上的安全配置,不单独定义为新 transport
framing
沿用最小 framing:
text
+--------------+------------------+-----------+
| FrameLength | Croupier Header | Body |
| 4 bytes | 8 bytes | N bytes |
+--------------+------------------+-----------+约束:
FrameLength表示后续Header + Body总长度Header继续复用现有Version + MsgID + RequestIDBody为 protobuf 编码- 单帧大小受双方配置的最大帧长限制
双向多路复用模型
基本原则
同一条 Agent-Server 连接允许双方主动发起请求:
Agent -> Server- Connect/Register
- Heartbeat
- Capabilities update
- Metrics / 状态上报
Server -> Agent- Invoke
- StartTask
- CancelTask
- Ops / control
- Drain / shutdown
请求响应规则
- 请求使用奇数
MsgID - 响应使用偶数
MsgID - 双方各自维护本端
RequestID递增计数器 - 响应必须回填原请求的
RequestID - 同一连接上允许多个并发 in-flight 请求
这意味着 Agent <-> Server 不再是“单向 register/heartbeat + 另一条连接回拨”,而是一个真正双向复用的 session。
集群与路由
这是 Agent <-> Server 改造里最关键的差异点。
现状问题
当前 Dispatcher 可以绕过 Agent -> Server 的控制面 session,直接根据 rpc_addr 去拨 Agent。
一旦改为 session 模型,这种做法必须废弃。
新模型
应引入“session 所属节点”概念:
- 每个
Agent session归属于某个Server node - registry 必须记录:
agent_idsession_idserver_node_idexpire_at- 功能与 provider 摘要
Dispatcher选中Agent后,不再直拨Agent- 而是把请求路由到持有该
session的Server node - 由该节点通过现有 session 下发给
Agent
路由后果
这样做之后:
rpc_addr不再是Server -> Agent的运行时依赖Agent可以只有一条到某个Server节点的出站连接- 集群内其他
Server节点只需要知道“这个 session 在哪台节点上”
Agent 本地监听的边界
重构完成后,Agent 本地监听地址只服务本地接入方:
GameServer -> AgentSDK -> Agent- 第三方本地应用 ->
Agent
明确不再用于:
Server -> Agent回拨
因此以下概念应进入废弃状态:
rpc_addr作为Server -> Agent调用入口Dispatcher直接拨Agent- 依赖
Agent暴露控制面回调端口的模型
重连、背压与摘流
重连
建议默认:
enabled = trueinitial_delay_ms = 1000max_delay_ms = 30000backoff_multiplier = 2.0jitter_factor = 0.2- 上限后固定在廉价周期持续重试
背压
Server 应至少维护:
- 每个 agent session 的
max_inflight_requests - 每个 agent session 的待处理队列上限
- 最大帧大小
- session 当前
draining状态
drain
Server 应支持对单个 Agent session 发出 drain:
- 停止向其分配新请求
- 允许已发请求在超时窗口内完成
- 超时后关闭连接并使 session 失效
这里的 drain 是会话级优雅摘流,不是立即断连:
Dispatcher不再把新的Invoke / Task / Ops路由给该 sessionAgent继续处理已在途请求- heartbeat 继续保持
- 排空完成后再关闭连接,或在宽限期后强制关闭
协议与消息演进建议
当前 agent/v1/register.proto 仍带有明显的旧模型痕迹,例如:
rpc_addr- 将
RegisterRequest仅视为控制面注册,而非 session 建立
后续建议:
- 保留现有消息做过渡兼容
- 明确其语义切换为“session connect/register”
- 逐步引入更准确的命名,例如:
AgentConnectRequestAgentConnectResponseAgentHeartbeatRequestAgentDrainRequest
- 在过渡完成后废弃
rpc_addr
实施顺序
建议按以下顺序推进:
- 冻结本文档
- 在
Server侧引入tcp session listener - 在
Agent侧引入tcp session client - 让
Register/Heartbeat跑在新 session 上 - 将
Invoke/StartTask/CancelTask/Ops改为走现有 session 下发 - 在 registry 中增加
server_node_id + session_id路由信息 - 改造
Dispatcher,不再直拨Agent - 清理
rpc_addr直连依赖 - 将
旧传输从默认主链路中移除
最终判断
对于当前项目,Agent <-> Server 的核心诉求不是:
- 多种 transport 大而全
- P2P 发现与打洞
- 极致本机低延迟消息总线
而是:
- 单连接
- 双向主动请求
- 多路复用
- 简单可控
- 易于排错
- 易于与
SDK <-> Agent统一模型
因此 Agent <-> Server 更适合收敛为:
- 轻量 TCP session
- 按需 TLS
- Server 持有 Agent session
- Invoke/Task/Ops 全部复用既有 session
- Agent 本地监听只服务本地接入方
这比继续围绕 历史消息模式 修补当前主链路,更符合当前系统真实需求。
