Mooncake TENT 高性能 TCP 传输(hp_tcp)全解析:架构、静态多轨路由与调优实战

原创2026-10-03 14:19:011,405 阅读
文章标签:大模型人工智能模型推理服务缓存KV存储高性能计算通信分布式数据库

Mooncake TENT 高性能 TCP 传输(hp_tcp)全解析:架构、静态多轨路由与调优实战

hp_tcp 是 Mooncake 项目 TENT(Transfer Engine)体系中的独立传输通道,面向数据中心 TCP 网络上的 CPU DRAM 数据搬运场景,以"每对端持久化 lane + 静态多轨路由 + 版本化协议"取代基于 RPC 的兼容路径。本文基于仓库设计文档 docs/source/design/tent/hp-tcp.md,并结合 mooncake-transfer-engine/tent 的源码、配置模板与测试用例,完整讲解其架构模型、配置参数、运行验证与并发调优方法。读完本文,你将能够为单机双轨乃至四轨场景正确配置 hp_tcp,理解 lane/rail/worker 三者的定量关系,并用 tebench、ss、ip route 等手段验证多轨数据是否真正走通。

一、定位:为什么要单独做一个高性能 TCP 传输

在 TENT 中,标准 tcp 传输保留了基于 RPC 的兼容路径,而 hp_tcp 则是一个独立的、为 CPU DRAM 传输量身定制的高性能 TCP 传输。二者不能同时启用:控制面通知(notification)的所有权是单一的,这一点在配置解析器中以硬约束体现——hp_tcp_transport_config.cpp 会直接校验"tcp 与 hp_tcp 不能同时 enable",否则返回 InvalidArgument。

首版 hp_tcp 刻意划定了能力边界,以下特性不在其内:GPU 内存、TLS、WRITE 条带化(striping)、rail 故障切换、歧义 WRITE 后的透明重放、以及动态 lane 调度。理解这一边界很重要——它是一个"少而精"的传输,复杂度被刻意压到可验证的水平。

在配置模板 transfer-engine.json 中,hp_tcp 默认处于关闭状态("enable": false),并预留了完整参数骨架;启用时需显式把 transports.tcp.enable 置为 false。

二、架构:worker 亲和、持久 lane 与进程级准入

2.1 Worker 与 lane 模型

hp_tcp 的线程模型极其克制:每个 worker 拥有一个 asio::io_context 和一条线程,对应的实现是 hp_tcp_workers.h 中的 HighPerformanceTcpWorkers,每个 WorkerContext 内含 asio::io_context、executor_work_guard 与 std::thread。

每个对端(peer)有一组配置数量的持久 lane。请求分配到哪条 lane 由两层规则决定:

  1. 每个 peer 独立的 lane 序列轮转:next_lane_sequence_(见 hp_tcp_transport.h)为每个 peer 维护独立的序列号,操作在 lanes 之间轮转。这样,发往其他 peer 的交错流量不会把某个 peer 钉死在单条 lane 上。
  2. 序列从该 peer 的第一个请求 ID 起算,以保持其初始 lane 选择;而请求 ID 本身全局唯一(next_request_id_{1} 原子递增)。

lane 的 owner 由 peer 与 lane 的稳定哈希决定(affinityOwner(peer, lane)),socket 状态永不跨 worker 迁移,同一条 lane 上的操作严格 FIFO。这意味着:从 admission 到 socket 读写,一条请求的生命周期始终绑定在一个线程上,既消除了锁竞争,也让超时取消等逻辑可确定性地归位。

ASIO 充当事件队列;进程级任务数与字节数准入上限约束所有被接受的 work——包括排队在事件队列中的回调,即 hp_tcp_workers.h 中 HighPerformanceTcpAdmissionController 的 tryReserve/release 计数。任务在 no-throw 的批量提交段内才真正占用 reservation(见 hp_tcp_task.h 的注释)。

2.2 Nagle 与写合并

两端都会关闭 Nagle 算法:头部与 payload 是分开的两次 write,在持久 socket 上,延迟 ACK(delayed ACK)不能卡住短的 payload。这是吞吐与延迟取舍中的关键细节——分写 + 禁 Nagle 保证小请求的头部能立即发出。

2.3 服务端会话管理

服务端复用同一个 worker 池:accept 到的 socket 按轮转(next_worker_ 原子计数)分配给 worker,存进 worker 专属的 session 集合(hp_tcp_server.h 的 sessions_ 向量,每个集合只被对应 worker 线程触碰)。全局连接数上限(max_connections,默认 4096)约束存活会话;会话关闭即从集合中移除,不会为历史连接保留线程。这避免了经典的"一连接一线程"内存与调度开销。

数据路径可用下面这条链概括:

TENT request -> bounded admission -> owner worker -> persistent lane
             -> versioned TCP protocol -> registered remote buffer

三、静态多轨路由(rail_addresses)

3.1 路由规则

rail_addresses 可选地将持久 lane 分配到一组有序的本地与远端 TCP 地址上,每个条目必须是分配给本地主机的数字地址,当前实现仅接受 IPv4。

  • 当列表非空时,它取代 advertise_address 成为发布端点;
  • lane i 使用 rail i % rail_count,socket 绑定到对应的本地地址,同时保留原有的 peer-and-lane worker 归属;
  • 两端必须配置相同的非零 rail 数;
  • 单一 rail 时,bind_address 必须与唯一的 rail 地址一致;多个本地 rail 时,监听地址必须是通配符(空或 0.0.0.0)。

3.2 切片的触发条件

路由是刻意静态的:

  • 小传输与所有 WRITE 都停留在单条持久 lane 上;
  • 只有当 READ 大到足以每条 rail 至少贡献一个内部 I/O 步时,才按 rail 切成连续切片:当前内部步长为 1 MiB,即双 rail 下 ≥2 MiB 的 READ 才会被切片;更小的 READ 与单 rail READ 不切;
  • 切片复用持久 lane,整体作为一个 TENT 任务完成;
  • 余数字节分配给最前面的切片,因此长度不均的切片仍能精确覆盖整个请求;
  • 多条独立 WRITE 可以用不同 rail,但每条 WRITE 只走一条 lane,等待自己的远端完成 ACK。

hp_tcp 不会在 rail 之间做负载再平衡或故障切换。

3.3 connections_per_peer 的语义陷阱

connections_per_peer 是每对端的 lane 总预算,不是 per-rail 倍数。例如 4 条 lane + 2 个 rail = 每个 rail 2 条 lane。worker 数与 rail 数相互独立:多条 lane 可以共享同一个 owner worker。需要特别注意的是:单 rail 下增加 lane 数不会把单个 READ 拆成多流。

这一语义在源码中对应 HighPerformanceTcpClient::Config::connections_per_peer(hp_tcp_client.h),lane 的归属通过 LaneKey{peer_id, incarnation, host, port, lane_id} 键管理。

四、协议与内存安全

4.1 线协议帧

协议常量定义在 hp_tcp_protocol.h:

  • Magic:0x4d435450(即 ASCII "MCTP");
  • 版本:kHighPerformanceTcpVersion = 1;
  • 请求帧固定 48 字节,含 opcode(kRead = 1 / kWrite = 2)、request ID、registration ID、远端地址与长度;
  • 响应帧固定 32 字节,含状态、request ID 与 committed 字节数。

远端状态码包括 kOk、kBadVersion、kBadOpcode、kBadLength、kRangeRejected、kPermissionDenied、kStaleRegistration、kShuttingDown、kInternalError。

完成语义:WRITE 只有在目标端拷贝完完整 payload 并返回 ACK 后才算完成;READ 在完整响应 payload 到达后完成。

4.2 注册 ID、lease 与重启防护

每个注册缓冲区 ID 由每注册器随机命名空间 + 单调序列构成,并附远端权限(HighPerformanceTcpBufferAttr{registration_id, permission})。随机命名空间的引入,防止上一次服务端化身(incarnation)遗留的陈旧 ID 在重启后复活。目标端在访问前校验 ID、范围与权限。

  • 操作持有 lease 直到其最终 I/O 回调退役;unregister 把该范围对新增 work 隐藏,并等待现有 lease 结束(对应 hp_tcp_task.h 中任务持有的 local_lease_);
  • 陈旧的注册元数据触发一次有界的元数据刷新与重试(仍在同一传输上);
  • 权限与范围失败是终态,不做重试。

4.3 歧义 WRITE 的"不可重放"纪律

若一个 WRITE 请求可能已到达对端、但未收到有效 ACK,远端结果是未知的。该失败被判定为终态,不通过其他传输重放——否则一个已提交的 WRITE 可能因 ACK 丢失被执行两次。这是保证 at-most-once 语义的关键设计取舍。

五、超时与关闭:有界且 fail-closed

5.1 两级超时

  • resolve/connect 使用 connect_timeout_ms;
  • 头部、payload 与响应的进度使用 progress_timeout_ms,客户端与服务端都适用:
    • 新接受的连接必须在期限内发出第一个头部字节;
    • 每个部分头部或 payload 必须持续取得进展;
    • 合法请求完成后,持久连接上的纯空闲不算停滞 I/O——下个头部一开始,deadline 重新起算;
    • 超时取消 resolver 或 socket,终态完成只在其对应回调退役后发布。

5.2 失败传播

  • 某条 READ 切片失败会取消其兄弟切片;逻辑任务在所有切片回调退役后才结算;
  • 陈旧注册结果可触发有界元数据刷新/重试,但不会迁移到另一条 rail;
  • 一个 peer 在等待其进度超时期间,其他 peer 可继续工作;
  • 同一 peer 的 lane 间 FIFO 共享仍会让小请求排在大请求后面——切片不是优先级或抢占机制。

5.3 客户端空闲连接回收

客户端在 idle_connection_timeout_ms(默认 60000 ms)内、该 lane 无活动或排队 work 时,单独关闭池化 socket:

  • 到期前的新 work 会取消该定时器并复用 socket;
  • 到期后的 work 触发重连;
  • 这样可释放更新版客户端持有的接收端连接槽位;
  • 服务端不主动驱逐已建立的空闲 socket——它无法知道客户端是否刚发起另一个 WRITE;
  • 无限期保持 socket 打开的老客户端仍需要自己的池清理;
  • 接收端连接数占满时尝试的任务仍可能失败:空闲清理不是任务背压,也不是自动重试策略。

5.4 关闭顺序与故障封锁

关闭流程为:关闭准入与 listener → 排空已排队的 dispatch 回调 → 在每个 owner 上取消所有客户端 lane 与服务端 session → 等待操作与 lease → 停止并 join worker 线程。这保证即使对端只发来半个请求,关闭也是有界的。

  • 这是生命周期不变量,不是析构的附带细节:客户端与服务端在它们使用的 worker 上下文之前被销毁;debug 构建下,正常 teardown 会断言 admission、客户端操作与服务端会话都在 owner 销毁前排空;
  • 异常逃出 ASIO handler 会标记运行时失败并封锁后续准入;owner 事件循环仅继续退役已提交的 work、以相同亲和性处理 teardown 取消;资源排空后 join worker 并上报失败;
  • admission-release 下溢是 fail-closed 的:计数器保持不变、新 work 被拒绝、drain 返回错误,而不是把存活 work 当作已完成。

六、配置指南:参数、默认值与双轨示例

6.1 参数总表

hp_tcp 在 transports.hp_tcp 下配置。下表为文档参数与源码默认值(hp_tcp_transport_config.h)的合并视图:

Field Meaning 默认值(源码)
enable 启用 hp_tcp;须同时把 transports.tcp.enable 置为 false,两者不能共存(控制面通知所有权单一) false
bind_address / advertise_address / port 监听端点与发布端点 "" / "" / 0
rail_addresses 静态 lane→rail 路由的有序 IPv4 源地址列表;须唯一、长度不超过 connections_per_peer、两端等长;非通配 bind_address 必须等于唯一 rail 地址,多 rail 要求其为空或 0.0.0.0 []
worker_count ASIO 事件循环线程数 16
connections_per_peer 每对端持久 lane 数 4
max_outstanding_tasks / max_outstanding_bytes 全局准入(任务/字节)上限 4096 / 1 << 32(4 GiB)
max_transfer_bytes 单请求最大尺寸;HP TCP 启用时,HP TCP/UNSPEC 请求的合并同时尊重本地与发布的远端上限,单独超限的请求仍被拒绝 1 << 30(1 GiB)
connect_timeout_ms / progress_timeout_ms 连接与 I/O 进度截止时间 2000 / 30000
idle_connection_timeout_ms 客户端空闲池保留时长(正数),默认 60000 ms;有活动或排队请求的 lane 永不被此定时器驱逐;保留越短越早释放接收端槽位,但间歇流量需要更多重连 60000

源码解析器 hp_tcp_transport_config.cpp 额外给出两类校验:启用状态下所有上限与超时均为严格正数(任一为零即报"zero or inconsistent limits"),且与 transports/tcp/enable 互斥。它刻意不用 Config::get() 的静默类型默认替换(注释明确说明),而是对每个字段做严格类型/范围解析。

6.2 单 rail 配置示例

以下 JSON 写入服务端 MC_TENT_CONF 文件(示例地址须替换为本机实际分配地址):

{
  "transports": {
    "tcp": {"enable": false},
    "rdma": {"enable": false},
    "shm": {"enable": false},
    "hp_tcp": {
      "enable": true,
      "bind_address": "",
      "rail_addresses": ["10.0.0.2"],
      "worker_count": 4,
      "connections_per_peer": 4
    }
  }
}

客户端使用相同配置,仅把本地地址换成 10.0.0.1。注意:MC_TENT_CONF 加载的是完整配置,因此即使使用 tebench 的 --xport_type=hp_tcp,也必须显式包含各传输的 enable 标志。

6.3 双 rail 配置

双 rail 只需改两个列表,条目按索引配对:

Host rail_addresses
Client ["10.0.0.1", "10.1.0.1"]
Server ["10.0.0.2", "10.1.0.2"]

两端需要为这些地址对准备可用的源地址路由;复用的 socket 保持该映射,失败的 socket 在后续请求重连前被关闭。测试代码 hp_tcp_e2e_test.cpp 展示了同构的最小化配置:单 rail 绑定 127.0.0.1,多 rail 把 bind_address 置空、rail_addresses 设为 ["127.0.0.1", "127.0.0.2"],且同时置 rdma/shm/tcp 为禁用。

七、用 tebench 验证传输正确性

先启动目标的 target,再用 initiator 以发布段名连接:

MC_TENT_CONF=client.json tebench --backend=tent --xport_type=hp_tcp \
  --tent_transport_hint=hp_tcp --target_seg_name=SERVER_SEGMENT \
  --seg_type=DRAM --op_type=mix --check_consistency=true \
  --start_block_size=67108864 --max_block_size=67108864 --duration=3
  • 上述命令使用 64 MiB 块走切片路径;把两个块大小标志都改为 4096 可覆盖未切片路径;
  • 在 --xport_type=hp_tcp --check_consistency=true 下,CPU 检查器使用种子可复现、非常量的数据并做全字节比对,用于检出切片乱序(reordered slices);
  • 吞吐测试应单独运行以避开检查开销;
  • 普通 mix、其他 backend 与 write_seed/read_verify 仍沿用各自既有数据模式。

八、检查 rail 是否真正被使用

8.1 观测手段

  • 测试本地的 socket 中继(relay)会记录对端/本地地址与完成的切片区间;
  • 全引擎测试校验 payload、guard、切片阈值与连接复用;双进程 E2E 还覆盖两端传输大小上限不一致的情况(见 hp_tcp_e2e_test.cpp);
  • 在两台机器上:
    • ss -tnp 检查连接;
    • ip route get REMOTE from LOCAL 检查源地址路由;
    • 传输前后记录每接口字节计数器:READ payload 从服务端 TX 流向客户端 RX,对比两条 rail 的增量与成功应用层字节数,并扣除协议开销与无关流量;
    • 注意:两条打开的连接本身不能证明 payload 走了多轨。

8.2 实验设计注意事项

  • loopback 只能证明路由逻辑,不能证明物理 NIC 使用——需检查 PCI 设备与共享的主机/结构上限;
  • 对比三组:单 rail/单 lane、单 rail/多 lane、双 rail/同总 lane 数——后两者不仅在 rail 放置上不同,在单 READ 是否切片上也不同;
  • 保持 CPU/内存绑定固定,记录各 NIC 的 NUMA 节点;
  • 使用每线程 CPU 测量,因为 Store 与 tebench 的调用方也在轮询完成时消耗 CPU。

九、并发 READ 调优:流数上界与实测数据

9.1 理论边界

对单个 peer,若有 C 个并发大 READ(大到足以跨所有 rail 切片)和 R 条 rail,最多可有 min(connections_per_peer, C * R) 条活跃 payload 流:

  • 4 个并发 READ + 4 条总 lane,在单/双 rail 下都是同样的四流上限——单加 rail 不提高上界;
  • 4 rail + 4 lane = 每 rail 1 连接;8 lane = 每 rail 2 连接;
  • 每条 lane 同时只处理一个操作;当 4 条 lane 已有不同 owner 时,只加 worker 无法增加活跃 lane owner 数;
  • 应分开比较 lane 数与 worker 数,两端都记录数量、吞吐、尾延迟与 CPU;共享同一批 worker 的多余 lane 未必提升吞吐。

9.2 Xeon 8457C VM 上的对照结果

两台 Xeon 8457C 虚拟机上,4 worker + 4 总 lane,三次交错运行(1 秒 warmup、3 秒测量)的中位数:

READ workload Metric One rail Two rails
64 MiB, one concurrent task Throughput (GB/s) 3.22 6.46
64 MiB, four concurrent tasks Throughput (GB/s) 11.13 10.96
4 KiB, one concurrent task Mean latency (microseconds) 72 78

两个接口都承载了 payload 方向流量,但其底层资源独立性并不保证。在该 4 lane/4 worker 配置下,4 个并发 READ 没有额外收益;同一池中 4 KiB/64 MiB 的闭环混合仍让小任务排在大任务后面:静态切片既无延迟隔离,也无普适的带宽扩展。

9.3 H20 主机上的 Store get_into 对比

另一个 Store get_into 对比实验:两台 H20 主机,未改动的 202ad9c89 Release 构建,8 MiB 主机内存对象,4 个闭环调用方(各 1 个在途 READ),CPU 0-89、新分配绑定 NUMA node 0;双 rail 用 node 0 的 eth1/2,四 rail 再加 node 1 的 eth3/4;每组 30 秒 × 3 次(2 秒 warmup),配置对交错、三对顺序执行:

Rails Workers / total lanes Median GB/s [min, max] Client CPU (core equivalents)
2 4 / 4 10.371 [9.664, 10.747] 6.84
2 8 / 8 8.022 [7.904, 8.159] 5.40
4 4 / 4 7.599 [7.440, 7.877] 6.86
4 4 / 8 8.537 [8.139, 8.624] 6.93
4 8 / 4 7.339 [7.260, 7.790] 6.83
4 8 / 8 11.487 [11.343, 11.551] 7.76

关键读数:

  • 四 rail 下同时提高两个计数,吞吐较 4/4 提升 51.2%,客户端 CPU 从 6.86 升至 7.76 core;
  • 只加 worker 无效;只加 lane 效果有限;
  • 四 rail 8/8 比测得的最佳双 rail(4/4)快 10.8%,代价是 13.5% 更多的客户端 CPU;服务端进程 CPU 采样也从约 2.24 升至 3.35 core;
  • 双 rail 在 8/8 下反而回退——这些结果不能作为上调默认值的依据。

18 次计时运行前,全部 32 个对象通过逐字节校验;每次运行都用满配置的 4 或 8 条连接,per-rail payload 字节总数与静态切分一致。实验同时改变两端 worker 数,不能隔离客户端/服务端成本,也无法消除 NUMA 效应;其覆盖范围是 Store 主机内存 READ,不适用于 GPU 传输或模型级性能。

十、源码验证路径速览

若想深入验证上述结论,建议按以下路径阅读:

综上,hp_tcp 的价值在于用"持久 lane + 静态多 rail 切片 + 有界准入 + 严格关闭纪律"在纯 TCP 网络上换取确定性与可验证性。它的调优空间并非无限:流数上界由 min(connections_per_peer, C × R) 决定,worker 数只影响并发 owner 的覆盖,切片只解决带宽叠加、不解决延迟隔离。部署时请以实测数据为准,逐项对照"单 rail/多 lane/多 rail 同总 lane"三组配置,而不是盲目上调默认值。

登录后查看全文
Mooncake