self-mesh-k8s-deepdive — 闪卡
8 段路径 · socket 三档旋钮 · C 单线程无锁 · tbus 点分位分发(细节 → self-mesh-k8s-deepdive.md)
记忆口诀
8 段路径:2 段 tbus SHM / 2 段跨机 TCP / 2 段本机 TCP / 2 段 tmesh 内部
socket 三档:2MB buf / TCP_NODELAY / stamp 攒批合包 / host_cache 免二次查
C 高性能:单线程无锁 / 内存池 / 位运算 / Jump Hash / 数字 ID 是前提
tbus 分发:点分位三层查表 O(1) / 5 类策略 / TTL=4 防意外反射
压测疤痕:CPU 54%→33% / 迁移周期反复 / turbulance 事故 / 9s 防抖 / CVM 方案二 / tbus 判空重试 / 60s 延迟销毁
压测疤痕:CPU 54%→33% / 迁移周期反复 / turbulance 事故 / 9s 防抖 / CVM 方案二 / tbus 判空重试 / 60s 延迟销毁
Card 1 · 流量路径 8 段解剖
Q:一条业务包从 A 机业务进程到 B 机业务进程一共走了几段?哪些段是共享内存、哪些段是 TCP?系统调用密度分别是多少?
A:8 段——① 业务A→msc-A tbus SHM(0 syscall),② msc-A→tmesh-A TCP(本机 hostIP),③ tmesh-A 内部路由分派(0 syscall,就地改 header),④ tmesh-A→tmesh-B 跨机 TCP(epoll_wait 在 nss_run;发送侧 stamp 攒批 N 包合并成 1 次 write),⑤ tmesh-B 收包(读 tip->host_cache 免二次 hash_find),⑥ tmesh-B→msc-B TCP(对称),⑦ msc-B→业务B tbus SHM(msc_tbus_send),⑧ 回包对称 8 段。就近路由 get_nearest_instance_index 命中时把跨机的 ③④⑤⑥ 短路成本机闭环(本机内 4 段)。
代码坐标:msc_tbus.cpp → msc_tbus_send + msc_tbus_run · msc_net.cpp → cn_write + msc_send_data · tmesh_main.c → tmesh_send · tmesh_net.c → _tmesh_send_msg + _tmesh_random + _tmesh_return_tgw。
Card 2 · socket 三档旋钮
Q:socket 层优化按"离系统调用远近"分三档,各自砍了什么开销?收益最大的一档是哪档?
A:
- 档 1 · 内核旋钮:
SO_RCVBUF/SO_SNDBUF=2MB(一次 write 不EAGAIN)+TCP_NODELAY=1(免 Nagle 40ms)+SO_KEEPALIVE(半死连接探测)。为什么关 Nagle 后还要应用层攒批:Nagle 是"tcp 时序合并",应用层攒批是"业务时序合并"(tick 内),两者互不冲突、组合使用才能把 syscall/包比压到接近 0。 - 档 2 · 应用层缓冲:
stamp_create(32K×16K)内存池 +stamp_get拿块 +slist_insert_tail挂到连接 →ncn_add_temp就绪队列(发送不立即 write)→ 主循环末尾tmesh_send遍历nss_get_temp批量刷ncn_stamp_write—— N 个逻辑包合并成 1 次 write。 - 档 3 · 免查缓存 · 最大收益:
svr_heartbeat里tip->host_cache = mn->cache记录来源连接;_tmesh_send_msg转发时直接cache_dest = tip->host_cache跳过hash_find。作者留了实证注释:cmpTBusPair 13.22% + hash_find 10.39% CPU,优化后 CPU 54%→33%。
保护:slist_nums(&cache_dest->list_data) >= 32000 整段丢弃并 gsm_alert,慢连接不拖爆内存。hostNetwork ≠ UDS:本机 msc↔tmesh 是 TCP + hostIP 环回,不是 UDS——因为 msc 要在多个 tmesh 之间动态切主备。
Card 3 · C 高性能 5 条推理链
Q:为什么用 C 而不是 Go/Rust 重写?"C 更快"这句话展开来是哪 5 条推理?
A:5 条推理链的叠加——
- 单线程无锁主循环 vs Go goroutine + mutex 的调度开销:
tmesh_start串行跑完"检查 → 定时器 → 发送 → epoll → 再发送",核心数据结构全部单线程独占,无 mutex / atomic / lock-free 复杂性。 - 手写
stamp内存池 vs GC 抖动:热路径 0 分配、0 释放,稳定 P99(Go GC 5~20ms STW 是不可接受的)。 - 位运算路由 vs Go int/interface 拆包:
busid & 0xff、(busid >> 8) & 0xff、(a ^ b) & 0x01000000、htonl(busid & 0xffff0000)全是单周期指令。 - Jump Consistent Hash · O(log n) · 0 内存:64bit LCG 常量
2862933555777941757ULL(还有 5 个可选素数),j = (b+1)*(1<<31)/((*key>>33)+1)算下一跳变点。实测 100 桶 10M/s、1000 桶 7.2M/s。对照 Ketama 建环 + 虚节点 + 字符串 hash,同 100 桶只能几十万 op/s。 - 数字
TBUSID32bit 是整套算法的前提——字符串 ID 会让前 4 条全部作废。
CVM 130% CPU 打满万兆网卡口径:整机总 CPU(多核合计),依赖 SDK 直连多通道 + 多进程 tmesh 并发,不是"1.3 核吃满万兆"。单核单线程无锁是"每个 tmesh 实例内部"的写法。
Card 4 · tbus 点分位分发
Q:tbus 的 32bit TBUSID 如何映射到具体实例?三层查表各是什么复杂度?5 类路由策略分别用什么算法?
A:点分位 → 三层查表 → 5 类策略:
- 点分位编码:
TBUSID32bit ≡zone.route.svrgroup.instance(例0.1.0.0)。zoneid = busid & 0xff、route_id = (busid >> 8) & 0xff、idx = htonl(busid & 0xffff0000)。 - 三层查表:
route_table[zoneid] → route[dest_routeid] → uinMod[idx] → dl_next(list) → STBusIP——全部数组下标 + 位运算 O(1),无 hash 无锁。 - 5 类策略在 route 内选实例:
MOD/MOD_BACKUP→_tmesh_mod:id = qqnum % route->total,同桶自动 backup。CO_HASH→_tmesh_co_hash:Jump Hash + active_list 二次 rehash。关键坑:二次 rehash 不能把 key 重置为qqnum(会让故障 mod 上的 key 又落回原桶,剩余机器雪崩)——必须用 LCG 已 mutate 的 key。RANDOM→_tmesh_random:就近优先 →rand() % route->total。MASTER_SLAVE/MOD_MS→ 首个成功打ECONN_MASTER,其余ECONN_SLAVE。
共享内存底座:tbus_init(iBusKey, 20480000) 20MB SHM 环形队列,生产者/消费者各原子写 head/tail 无锁。TBUS_ERR_CHANNEL_FULL 时若 dwHead == dwTail == 0 判空重试一次(msc_tbus_send)——共享内存无锁队列的经典坑。
消费循环:msc_tbus_run(sc, 300) 一次 tick 上限 300 条;peek + process + delete 三段式让处理失败可以不出队。
TTL 防环:header->ttl 0→4 初始化、1 丢弃、否则递减。这不是多跳追踪——全连接单跳广播的语义里包根本不该跳超过一次,TTL 只是"防意外反射"的最后防线。
Card 5 · 压测与演进疤痕(用 C 到底比其他语言多拿到了什么)
Q:用 C 写和其他语言写的差距到底是什么?压测里发现了什么、修了什么、留下了哪些疤痕?
A:不是"C 更快",是"C 允许把每个决策都拍在代码里"。
三层地板:① syscall/包 的地板(Envoy 3× socket 开销 vs ncn_add_temp 批量刷 syscall/包 ≈0)② P99 抖动的地板(Go GC write barrier +10~20% CPU vs stamp 预分配 0 分配)③ 单周期指令 vs 拆箱开销(C busid & 0xff 单周期 vs Go interface{} 拆箱 20~50 周期)——Rust 能打平前两层,Go/Java 天生打不平。
7 大疤痕:
- CPU 54%→33%:
_tmesh_send_msg里 perf 抓到cmpTBusPair13.22% +hash_find10.39% = 23% CPU 集中在 tbus pair 统计。决策:整段注释掉统计 + 引入tip->host_cache免二次hash_find。保留原代码为注释而非 git rm——决策日志留在源码。 - 迁移周期条件三次反复:14s(防迁移太快)→ 7s(缩短玩家延迟感知)→ 2023.11.23 去掉(怀疑影响正确切换)→ 2023.12.07 恢复(panxie 反馈网页监控页显示"幽灵实例")。产品面观察窗口 vs 数据面正确性的拉锯。
- turbulance 双层启发式:起因是"全军"(赛事场景)时起了 2 个同 BusId 实例导致来回踢;修复"短时间 IP 端口切了 2 次" + 二次修补
ipChangeFromDisable白名单(正常重启不误判)。 - 9 秒防抖:
update_master_connection里 msc 侧切换间隔 = mesh 侧HeartBeatTimeOut = 9000ms——不是防抖,是多方超时对齐。 - CVM 多通道选方案二:virtio 打折 40~45%,方案一(tmesh 本地多通道)新增 2 次中转;方案二(SDK 直连多通道)无多余中转,不破坏单线程无锁架构,Jump Hash 靠数字 ID 直接可用。
- tbus 共享内存判空重试:
TBUS_ERR_CHANNEL_FULL时双检查dwHead == dwTail == 0——共享内存无锁队列的经典瞬态处理(msc_tbus_send)。Go/Rust 用 channel 自动保证内存序,代价是有 atomic 开销。 - 60s 延迟销毁:
tmesh_destroy只标记时间戳、__tmr_tmesh_clean60s 后才真删——重连能继承缓冲区,配合iQueueSize=32000discard 保护避免堆积。
结论:Go/Rust 项目里这些决策要么被抽象进接口/配置管理层丢失上下文,要么被 GC/runtime 遮蔽在语言运行时里根本看不到。代码本身就是决策日志——这条价值在 GC 语言里几乎拿不到。
(细节 → 见 self-mesh-k8s-deepdive.md#主干五-压测与演进疤痕-用-c-到底比其他语言多拿到了什么)