笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 接入与通信

    • 游戏基础架构与工具
    • 接入网关(长连接接入层)
    • 消息总线(共享内存 IPC)
    • 帧同步(Lockstep)
    • 全栈极限低延迟(物理层→应用层)
    • KCP 与 QUIC
  • 网络与服务网格

    • CNI 与 K8s 网络插件
    • 容器运行时:Docker 与 containerd
    • Istio 与 Cilium 服务网格
    • 服务网格:中心化 vs 去中心化
    • eBPF 原理与在网络/可观测/安全的落地
    • 自研 Mesh 服务网格 × K8s 部署
    • 自研 Mesh × K8s · 数据面深水区
    • Tco 协程框架 · 运行时深水区
    • K8s 异构 & 网络插件
  • 有状态服务

    • 分布式游戏存储(分布式 KV)
    • 有状态服务的数据迁移
    • 有状态服务的数据恢复与容灾
    • 有状态服务的 K8s 编排层治理(防漂移 + 节点崩溃恢复)
    • 内存配置热刷新与更新机制
  • 算法与协程

    • 一致性哈希四算法实现(Ring Hash / Ketama / Maglev / Jump Hash)
    • 蓄水池抽样(Reservoir Sampling)与加权扩展
    • C++ 协程:有栈 vs 无栈 vs C++20 原生(函数染色与 libco)
    • Raft 与 Gossip:CP 强一致共识 vs AP 最终一致传播
  • 流量与承载

    • 游戏秒杀场景承载
    • 令牌桶与漏桶
    • 限流与熔断
    • 限流:互联网 vs 游戏
    • 灰度发布 · Canary / BlueGreen / A-B / Shadow
    • 服务器日常系统开发与维护 SOP
    • 极限发压工具设计(多长连接 + 发压任务)
    • 性能分析与优化方法论(六条排查线)
  • 编译与排查

    • 编译优化与 LLVM/Clang/GCC
    • Sanitizer 工具链与内存泄漏定位

自研 Mesh 服务网格 × K8s 部署

自研服务网格 · 从 Consul 到全连接单跳广播 · 万级节点

一句话结论

用 O(N) 全连接换 O(1) 单跳收敛,靠数字 ID + hostNetwork 撑起万级有状态直连。

场景问题

⚠️ 校正清单(面试必带)

  1. 是 gossip 的退化特例,不是教科书 SWIM:代码是"全连接网格 + 单跳全量广播",fanout 拉满、无感染轮次、无随机邻居、收方不转发。准确说法是"gossip 家族的退化变体",别简单说"未落地/不是 gossip"。
  2. 节点发现:自研 Mesh 的 C 代码只读 host.txt;K8s API / 服务发现是运维面板(Go) 的外部链路,负责把 IP 写进 host.txt。
  3. 本机通信:本机 mesh 客户端(msc) 接业务是 消息总线共享内存 channel,不是 UDS。
  4. 跨 DC "选 2 个中转":代码只见"直连优先 (host_cache)",独立实现未见,存疑。

打个比方:教科书 gossip 像传八卦——你只随机挑几个人说,他们再随机传给几个人,几轮下来全村都知道(O(log N) 轮,省流量但要多跳、有延迟)。tmesh 的"全连接单跳广播"则像班级群里 @所有人:有事直接一条消息群发给全体成员,一跳到位、零转发延迟。代价是每个人都得维护和所有人的连接、群消息流量是 O(N)。所以它是 gossip 家族里"把 fanout 拉满到全体、砍掉多跳感染"的退化特例,而不是"没做 gossip"。类比失效边界:群发一跳到位的前提是"人数可控"——万级节点还扛得住 O(N) 的连接数和广播流量;一旦冲到十万、百万级,全连接的连接数与每次广播的流量都会爆炸,那时就必须退回真正的多跳 gossip,用"多跳延迟"去换"规模"。(tmesh 的具体实现代码见 Raft 与 Gossip,本篇不重复展开。)

一台机器一个 mesh,所有 Pod 共享

        NODE(实体机 / 云主机 / K8s 节点)
   ┌──────────────────────────────────┐
   │  Pod-1   Pod-2   ...   Pod-N      │
   │   └───────┴────┬─────────┘        │
   │        本机 自研 Mesh ──► 跨机直连 │
   └──────────────────────────────────┘
跨机调用:业务→msc→本机自研 Mesh→远端自研 Mesh→远端msc→远端业务

收益:连接数大幅收敛 · 内存/CPU 省 · 用主机网络跳出 K8s Overlay(公司内网即可跨集群组网,规避 K8s 网络组件频繁异常)。

三种部署模型

模型说明用在哪
DaemonSet(主用)一机一 mesh,所有 Pod 共享通用业务
Service业务连内网 CLB,不在每节点部署 mesh战斗服务集群 (DS 链路单一)
Sidecar(已弃用)每 Pod 一个 mesh sidecar放弃:K8s 网络组件频繁异常;多 Pod 互联单实例近 1GB 内存,10 Pod 节点要 10GB

Gossip 的真相:概念 vs 代码

代码里没有任何 gossip/infect 标识符(grep 命中 0),关键在于收方不转发、组包不装别人的实例:

  • 组包 make_mesh_heartbeat_package:只装本机 local_bus 实例,不装从别人学到的实例
  • 广播 mesh_heartbeat:遍历 hash_cache(所有连接)逐个发;内部计数就叫 broadcasts++
  • 收包 _mesh_heartbeat:对每条实例只 svr_heartbeat 更新本地路由表,循环结束就 return,无任何 re-broadcast

口径统一(两层要分清):从算法家族看,它属于 gossip 的一种退化特例——把 fanout 拉满成"全连接"、砍掉 incarnation 与间接探测;从教科书定义看,它不是完整 SWIM(无多跳感染、无反熵、收方不转发)。所以准确表述是"gossip 家族的退化变体,而非教科书 SWIM",不要简单说成"不是 gossip"。取舍方向:自研 Mesh 用 O(N) 连接换 O(1) 收敛跳数,教科书 gossip 用 O(W) 连接换 O(log N) 跳数——方向相反。tmesh 相对 SWIM 砍了哪些组件、代价如何,逐行代码详见 Raft & Gossip · tmesh 实战。

实现方案

心跳机制

链路周期备注
mesh⇄mesh5s定时器只置 isHeartBeat=TRUE,主循环检测才真发包
msc⇄mesh3s客户端侧心跳
业务→meshTCP 直连标 islocal=TRUE

三大加速手段:

  • 事件驱动立即广播:状态变更(disable/normal/迁移)直接置 isHeartBeat=TRUE,下轮主循环立即广播不等 5s
  • 合包 StampCache:发送不直接 write,取缓冲块挂到连接待发链表,主循环批量刷出——多个逻辑包合并成少量 syscall
  • 单线程主循环:连接检查/定时器/收发/心跳全在一个无锁线程跑完

路由能力(6 类 + 备份 + 就近 + 跨DC)

路由类型实现场景
RANDOM_mesh_random先就近再 rand,失败走备份重试
MOD / MOD_BACKUP_mesh_mod取模分片(合并 backup)
MOD_MS_mesh_mod_ms取模 + 主备双发
MASTER_SLAVE_mesh_master_slave中心化服务
CO_HASH_mesh_co_hash一致性哈希(玩家/房间粘性)

就近路由:解决云主机跨机性能

get_nearest_instance_index:只有本地连接(islocal)的包才尝试就近,命中本机同 zone/route 的健康实例则直接走本地(按 backup_cnt 概率)。

云主机跨节点网络因虚拟化下降,对高流量请求多部署几个本地 DB 代理,就近路由让本地优先承接——本地转发基本不受 IO 虚拟化影响。

备份路由 + 跨 DC

  • 两级灾备:SBusRoute 有 backup_route_id(route 全挂切别的 route)和 backup_zone_id(zone 全异常切别的 zone),回包失败时改写 header 重路由
  • 跨 DC:"直连优先"已落地——实例记录来源连接 host_cache,转发直接用这条已知连接发
  • 跨外网(比赛现场):引入外网 CLB,链路开加解密

K8s 部署:DaemonSet + hostNetwork

kind: DaemonSet                         # 一机一节点
spec.template.spec:
  hostNetwork: true                     # 跳出 Overlay,用宿主机网络
  hostPID: true                         # preStop killall daemon 需要
  terminationGracePeriodSeconds: 7200   # 优雅退出窗口 2 小时
  volumes: [hostPath /data/home/user00,
            hostPath /data/corefile,
            emptyDir(Memory) /dev/shm]
  containers[mesh]:
    ports: 8000/TCP(mesh 主服务), 9912/UDP(tlog), 9906/TCP
    lifecycle.preStop: exec [killall, daemon]
    resources: requests{2Gi,1core} limits{4Gi,2core}

Service 模型是 headless Service(clusterIP: None)。

业务 Pod 怎么连本机 mesh — hostIP 注入,不是 UDS

业务 Pod → 本机 mesh 靠 hostNetwork + hostIP 注入:

  1. 业务进程启动参数统一带 --mesh=$host-ip$:8000
  2. $host-ip$ 来自 Downward API:所有工作负载注入 HOST_IP <- status.hostIP
  3. 自研 Mesh DaemonSet hostNetwork: true,监听宿主机 0.0.0.0:8000,业务用 HOST_IP:8000 直接命中本机这台 mesh,流量不走 Overlay

多集群 / 跨 DC 拓扑

  • 集群清单:按地域分文件(如 regionA.yaml/regionB.yaml),各一套 apiserver 证书
  • CLB 接入:某云厂商托管 K8s 的自定义 Ingress(非标准 nginx);按运营商拆 CLB:-dx(电信)/-yd(移动)/-wt(联通)/hongkong-bgp/-inner;isDirectConnect: true CLB 直连 Pod
  • DS 战斗集群:ds/ chart 三件套 idc-dispatcher/ds-center/ds-agent;ds-agent 单实例 16Gi/4core 起、limit 可到 128Gi/64core,nodeSelector: node_type=ds + podAntiAffinity 强制一机一 ds-agent,PortPool 申请固定 UDP 端口给玩家直连

运维面板:运维 + 节点发现的真正落点

入口是内网运维面板(Go 进程 + Vue 前端)。它才是"K8s API 拉节点 / 服务发现 / host.txt 对账"的实现处。

节点发现两条来源 + 交叉对账:

  • host.txt:扫 host.*.txt,构建 IP→文件映射;同 IP 出现在多个 host.txt 会告警
  • K8s API(client-go):为每个集群建 clientset,Nodes().List() 拉节点、Pods().List() 拼总线地址
  • 对账:在 host.txt 却未组网 / 已组网却不在 host.txt / DS 节点未跑 ds-agent,分别告警

生产保护:所有写操作受 enable_k8s_op 总闸控制,dev/test=1,体验/线上=0(线上禁止面板直接操作 Pod)。

服务发现:Polaris + 集群化 CLB 入口

运维面板负责的是节点级发现(哪台机跑着 tmesh、host.txt 与 K8s API 是否对账);对业务侧,tmesh 又叠了一层注册中心 + 统一入口,把"业务感知面"降到最低。

服务发现走 Polaris(北极星):tmesh 启动后向 Polaris 注册 实例名 → 节点 IP:Port;业务 SDK 从 Polaris 拉服务实例列表,不需要感知具体 Pod 或节点 IP。这一层的价值在上下线自动化——机器扩缩容、故障剔除、灰度上线,全靠 Polaris 的健康检查驱动,tmesh 与业务都不用感知。

对外只暴露一个内网 CLB IP:Port:整套 tmesh 集群化以后,业务连接的唯一入口是内网 CLB 的 VIP;CLB 后挂所有 tmesh DaemonSet 实例,业务侧的服务发现降到"配一个 VIP"的成本。

这一层怎么和"运维面板 + host.txt 对账"共存:

层谁在用干什么
运维面板 + host.txt / K8s API运维 + tmesh 自己节点级对账、跨集群拼总线地址、防脑裂
Polaris 服务发现业务 SDK拉 tmesh 实例列表、健康检查、自动上下线
CLB VIP业务 SDK 简化路径统一入口,业务只配 VIP 不需要感知实例列表

后端 DS 集群(有状态服务)也可以复用这套:接弹性伸缩 + Polaris,运维只处理 dsa(ds-agent)相关,其他自动化。这也是"业务感知面最小化"的完整闭环。

状态机与一致性冲突应对

NORMAL ──disable signal──► DISABLE
   │                          │
heartbeat timeout       (组包时跳过,不再扩散)
   ▼
TIMEOUT

冲突应对组合拳:

  • 缩短不一致窗口(立即广播 + 快速收敛)
  • 业务用一致性 HASH(扩缩容时少迁移)
  • 关键包最多 3 次重试(避免雪崩)
  • 客户端兜底

新旧两套 K8s 部署(Helm → 自研生成器)

  • 旧(声明式 Helm):按 system / game / ds 三 chart,每服务一份手写 templates/*.yaml + 一堆按"环境-地域-集群"命名的 values。目录名 old/ 本身就是"已归档"信号
  • 新(程序化生成器):读精简的 service.*.yaml/system.*.yaml + 3 个基础模板,内存里组装 K8s 资源并下发

为什么从 Helm 转程序化生成:手写模板"每个 yaml 复制粘贴改名"太脆——旧 Helm 里 limits 把 {{ 误写成 { { 渲染不出,同款笔误还误引用了别的服务的 memory_limit。程序化生成把差异收敛到几行配置。

为什么这么做

为什么要自研:早期方案的死法

中心网关 + PROXY + 消息总线时代(容器化后直接崩):

  • 消息总线走共享内存:容器化后容器间内存隔离,共享内存通道直接失效
  • 服务发现纯人工:K8s 按资源调度,无法预知部署到哪台
  • PROXY 中心化:路由要跨 3 跳(业务→网关→PROXY→网关→业务)
  • PROXY 带宽吃紧:越来越多模块走无状态化,PROXY 处理跟不上

calc_connect:单向连接算法(最扎实的亮点)

全互联的代价是连接数——但 TCP 是全双工,两节点之间只需 1 条链路,关键是"谁主动连"。

  • 朴素方案"大 IP 连小 IP":主动/被动连接数极度不均,CPU 倾斜
  • 自研 Mesh 方案:基于 IP 末位 bit 异或的"公认随机值"——两个数相加奇偶各 50%,异或值双方算出必然一致且 0/1 均匀
int calc_connect(SMeshNode *a, SMeshNode *b) {
    if (a->outside_ip == b->outside_ip) return 0;
    unsigned int bit = (a->outside_ip ^ b->outside_ip) & 0x01000000;
    return !bit == (a->outside_ip > b->outside_ip) ? 1 : -1;
}

实测均衡度:5000 节点下最大连接差仅 0.37%,单次比较开销可忽略、额外内存 0。

Reservoir Sampling 推荐 32 个节点

msc 启动要快速感知附近 mesh。下行心跳 make_msc_heartbeat_package 用水库抽样从全网 mesh 等概率抽 32 个健康节点:

先顺序填满 32;之后 r = rand() % (idx+1);r < 32 则替换。

不论集群多大,下行包恒定 32 个、分布均匀。

Jump Consistent Hash(取代 Ketama)

_jump_consistent_hash:Google Jump Hash,64 位 LCG,O(log n)、0 内存,分布更均匀。实测"100 桶≈10M/s,1000 桶≈7.2M/s"。

成立前提:实例 ID 用数字标识(沿用数字实例 ID)。字符串 ID 得建环 + 字符串 hash,性能差——这是自研 Mesh 整体的性能基石。

为什么别的选择不行

业界方案对比

维度Istio某公司内部 Mesh 组件自研 Mesh
有状态服务不支持路由不完美,需再叠一层 PROXY原生支持
点对点不支持跨多节点跳转直连单跳
性能单核单线程 ~6k QPS,tp50=5ms / tp90=9ms,3 倍 socket 开销单节点 ~6w QPS,跨节点骤降单核 20w~28w QPS;云主机 130% CPU 跑满万兆
跨集群依赖 Overlay需开发 CROSS 模块主机网络直连多 K8s 集群

Istio 的性能账单细项(tp50=5ms、tp90=9ms 是怎么来的):主要基于流量拦截(iptables REDIRECT),一次 A→B 调用附带 3 次 socket 开销 + 3 次内核/用户空间切换 + 3 次 TCP/IP 协议栈遍历——iptables 拦截细节见 Istio 与 Cilium 服务网格 · Netfilter 五链视图。

为什么不用某内部游戏总线 tbuspp(选型窗口期的选择):

  • 优点:针对游戏业务专门做过工程化,可用性 ok。
  • 不选的三条原因:① 选型窗口期 tbuspp 只支持随机 / 轮询 / 直接转发 3 种路由,无法满足有状态服务的一致性 HASH / 主备 / 就近路由等需求——已接入 tbuspp 的业务实际上是在 tbuspp 之上又叠了一层自己的路由;② 无法同时中和"内存占用 × 业务混部 × 异构部署"三者的关系(内存要控、业务要混部、跨 K8s/非 K8s 又要通);③ 与我们"数字 ID + Jump Consistent Hash + 万级节点全连接"的取舍方向不契合。

自研 tmesh 的实测能力(压测口径)

能力数字场景
单实例最大连接数压测 15w,无上限单核单线程
单实例转发能力20w/s(峰值 28w/s)包大小 512B / 3w 连接 / 每连接 2M SO_RCVBUF-SNDBUF
整机吞吐130% CPU 跑满万兆网卡多进程/多 tmesh 实例并发(不是单核 1.3 核)
横向扩展无中心节点,直接加机全连接单跳广播,加机即接入

后期主要 CPU 消耗:内核锁(数据包碎片化 + 系统调用频繁)——这也是"单核 20w QPS 是 socket/内核层天花板"的推理落点,进一步优化要走 io_uring / eBPF 或 batching。

第一版基于 Consul 的水土不服(用了一年多)

  • 实例超 300 个就数据重复
  • 100+ 服务要同时 100 个 Watch
  • 单实例 4.5KB 冗余,5000 实例每次变更解析要 5~8 秒
  • 节点上限 5000
  • 2021 年 Consul 官方宣布不再对中国区支持

第二版根本性需求:去中心化(不依赖配置中心)· 万级节点 · 自动注册/剔除 · 云主机转发性能挖掘 · 异构混部互通 · 可定制路由。

云主机网络虚拟化 40~45% 瓶颈

云主机(虚拟化机型)网络 IO 仅为实体机 40~45%,专业团队结论"所有 virtio 方案都有此问题"。单核单线程到顶,转多核多通道:

  • 方案一(否)自研 Mesh 本地多通道转发:业务无感,但新增 2 次额外中转
  • 方案二(选)SDK 直连多通道:业务 SDK 直连多个通道无多余中转;自研 Mesh 更新时 SDK 自适应动态屏蔽链路;多通道用一致性 HASH 选链路保证玩家数据有序、迁移最少

效果:云主机总 CPU 130% 跑满万兆网卡。

深水区 →

数据面细节(流量流转 8 段路径 / socket 三档旋钮 / C 高性能 5 条推理链 / tbus 点分位三层查表 + 5 类策略)走独立成篇的 self-mesh-k8s-deepdive.md,附 闪卡。

沉淀结论

工程细节里的经验教训

  • msc ↔ mesh 主连接切换太频繁:"9 秒才更新一次防止切换太频繁被 mesh 误判不稳定"——细节到"9 秒"这种拍出来的量。
  • 实例迁移防抖反复:演进痕迹里"超时实例是否组包"曾被去掉、又因网页显示异常恢复,典型反复。
  • CPU 热点抓取到锅:总线 pair 统计因比较/查找函数占 13%/10% CPU 被注释掉,"优化后 CPU 54%→33%"。

最终结论

  • 自研 Mesh 的本质是用 O(N) 全连接换 O(1) 单跳收敛,与 gossip 的取舍方向相反;这套取舍在万级节点、有状态、点对点直连的游戏后台场景下成立。
  • 性能基石是数字 ID:让 Jump Consistent Hash、calc_connect、水库抽样都能 O(1)/O(log n)、0 额外内存跑起来。
  • 部署上用 DaemonSet + hostNetwork 跳出 K8s Overlay,规避网络组件频繁异常;节点发现落在 运维面板 而非 mesh C 代码本身,靠 host.txt 与 K8s API 交叉对账 保证一致性。
  • 服务化面:业务侧靠 Polaris 注册中心 + 内网 CLB 统一入口,业务只配一个 VIP、不感知实例列表;节点级发现和服务级发现职责分离。
  • 实测能力:单核 20w~28w QPS / 单实例 15w 连接压测无上限 / 整机 130% CPU 跑满万兆——Istio 单核只有 6k QPS,tp90=9ms,主要成本是 iptables REDIRECT 引入的 3 倍 socket + 3 次内核/用户切换。
  • 面试记住四条校正:是 gossip 退化特例(非教科书 SWIM)、节点发现在面板、本机通信走消息总线共享内存、跨 DC 只见直连优先。

记忆口诀

拓扑:全连接 / 单跳广播 / O(N)换O(1)
部署:DaemonSet / hostNetwork / 跳出Overlay
性能基石:数字ID / Jump Hash / calc_connect / 水库抽样32
一致性:立即广播 / 一致性HASH / 3次重试 / 面板对账
服务化:Polaris 注册中心 / 内网 CLB VIP 统一入口 / 业务只配 VIP
实测:单核 20w~28w QPS / 15w 连接压测 / 130% CPU 跑满万兆

内容来源

综合整理自自研服务网格的架构演进与工程实践(连接算法、水库抽样、Jump Consistent Hash、DaemonSet+hostNetwork 部署、节点发现对账、Polaris 服务注册、内网 CLB 集群化入口、单核 20w+ QPS 压测口径等),配合 Consul、Istio、Cilium 等公开方案的对比。数据面深水区(流量路径 / socket 优化 / C 高性能 / tbus 分发)见 self-mesh-k8s-deepdive.md;Istio 流量劫持的 netfilter 五链细节见 mesh-istio-cilium.md。

自测:合上资料能说清楚吗?

自研 Mesh 的心跳广播和 Gossip 到底是不是一回事?它们的取舍方向有什么本质区别?

参考答案

是 gossip 家族的退化特例,但不是教科书 SWIM。代码是全连接网格 + 单跳全量广播:收包只更新本地路由表、不再转发,组包只装本机实例,且砍掉了 incarnation/间接探测。Gossip 用 O(W) 连接换 O(log N) 跳数;自研 Mesh 把 fanout 拉满,用 O(N) 连接换 O(1) 收敛跳数,取舍方向恰好相反。(逐行代码与砍掉的 SWIM 组件见 Raft & Gossip · tmesh 实战)

为什么全互联下连接数还能均衡?calc_connect 怎么决定"谁主动连"?

参考答案

TCP 全双工,两节点只需 1 条链路。朴素"大 IP 连小 IP"会让连接数极度倾斜。calc_connect 用 IP 末位 bit 异或的公认随机值:异或值双方算出必然一致且 0/1 各 50% 均匀。实测 5000 节点最大连接差仅 0.37%。

业务 Pod 是怎么连到本机 mesh 的?为什么不用 UDS?

参考答案

靠 hostNetwork + hostIP 注入:mesh DaemonSet 监听宿主机 0.0.0.0:8000,业务经 Downward API 拿到 HOST_IP,用 HOST_IP:8000 直接命中本机 mesh,流量不走 Overlay。本机 msc 接业务走的是消息总线共享内存 channel,不是 UDS。

对比 Sidecar 与 DaemonSet 两种部署模型,为什么放弃 Sidecar?

参考答案

Sidecar:每 Pod 一个 mesh,多 Pod 互联单实例近 1GB 内存,10 Pod 节点要 10GB,且 K8s 网络组件频繁异常。DaemonSet:一机一 mesh 所有 Pod 共享,连接数大幅收敛、内存/CPU 省、用主机网络跳出 Overlay。故弃 Sidecar 用 DaemonSet。

第一版基于 Consul 为什么撑不住?第二版提出了哪些根本性需求?

参考答案

Consul 痛点:超 300 实例数据重复、100+ 服务同时 Watch、5000 实例每次变更解析 5~8 秒、节点上限 5000、2021 官方停中国区支持。第二版需求:去中心化 · 万级节点 · 自动注册剔除 · 云主机转发性能 · 异构混部 · 可定制路由。

最近更新: 2026/9/10 11:38
Prev
eBPF 原理与在网络/可观测/安全的落地
Next
自研 Mesh × K8s · 数据面深水区