网络背压与故障注入
BigWorld 的网络层不仅是 epoll + UDP。真正影响游戏服务器稳定性的,是接收预算、发送队列满、人工丢包/延迟、Channel overflow、可靠重发、rate limit 和 tick 公平性如何一起工作。这章专门分析背压和故障注入。
先给结论
BigWorld 网络层已经有多处“保护主循环”的机制:
PacketReceiver一次 socket 通知处理多个包,但可用maxSocketProcessingTime限制本轮处理时长。BWMessageForwarder默认可把最大 socket 处理时间设为1 / gameUpdateHertz。PacketSender支持人工丢包和人工延迟,用于测试可靠 UDP。- 发送遇到
EAGAIN/ENOBUFS时,会用select()等待 10ms 再重试。 - 网络接口暴露 artificial loss、latency 和 max socket processing time 到 Watcher。
- 网络单测大量使用
dropNextSend()验证可靠层。
关键源码:
- 接收预算在 packet_receiver.cpp。
maxSocketProcessingTime()watcher 在 packet_receiver.cpp。BWMessageForwarder配置 socket 处理预算在 bw_message_forwarder.cpp。- 人工丢包/延迟在 packet_sender.cpp。
- 发送队列满等待在 packet_sender.cpp。
- 逐包
sendto()在 packet_sender.cpp。 - 网络测试中使用
dropNextSend(),例如 test_reliable.cpp。
接收侧背压
概述: PacketReceiver::handleInputNotification() 在收到 socket 可读通知后循环调用 processSocket(),但有时间限制。这不是内核级背压,而是主线程公平性保护:防止一次 UDP burst 把 Timer、FrequentTask、脚本和其他消息饿死。
源码入口: packet_receiver.cpp:83
// packet_receiver.cpp:83 - 接收预算
void PacketReceiver::handleInputNotification()
{
// 1. 记录开始时间
uint64 startTime = timestamp();
// 2. 循环处理 socket 中的包
while (true)
{
// 3. 处理一个包
if (!this->processSocket())
{
break;
}
// 4. 计算已用时间
uint64 elapsed = timestamp() - startTime;
// 5. 检查是否超时
if (elapsed > maxSocketProcessingTimeStamps_)
{
WARNING_MSG( "PacketReceiver::handleInputNotification: "
"Spent too long processing socket (%.2fms), "
"source=%s, processed=%d packets, "
"remaining in queue=%d\n",
float(elapsed) / 1000.0f,
lastSourceAddr_.ipAsString(),
numPacketsProcessed_,
receiveQueueSize() );
break;
}
}
}流程图:
flowchart TD A[EPOLLIN 事件] --> B[handleInputNotification] B --> C[记录开始时间] C --> D{socket 有数据?} D -- 否 --> E[返回 dispatcher] D -- 是 --> F[processSocket] F --> G[计算已用时间] G --> H{超过 maxSocketProcessingTime?} H -- 否 --> D H -- 是 --> I[打印警告] I --> E 详细讲解:
时间限制:
maxSocketProcessingTimeStamps_限制单次 socket 处理的最大时间。默认值由gameUpdateHertz推导。循环处理:在时间限制内,循环调用
processSocket()处理包。每处理一个包检查一次时间。超时处理:超时后打印警告,包含:
- 处理耗时
- 来源地址
- 已处理包数
- 剩余队列大小
公平性保护:允许 receive queue 留到下一轮处理,确保 Timer、FrequentTask、脚本和其他消息有机会执行。
与 Tick 的关系:
maxSocketProcessingTime默认为1 / gameUpdateHertz,即一个 tick 的长度。如果gameUpdateHertz = 10,一个 tick 是 100ms。
BWMessageForwarder 配置
源码入口: bw_message_forwarder.cpp:47
// bw_message_forwarder.cpp:47 - socket 处理预算配置
void BWMessageForwarder::init( NetworkInterface & networkInterface )
{
// 1. 读取配置
float maxInternalSocketProcessingTime =
Config::maxInternalSocketProcessingTime();
float gameUpdateHertz = Config::gameUpdateHertz();
// 2. 如果配置值小于 0,使用默认值
if (maxInternalSocketProcessingTime < 0)
{
maxInternalSocketProcessingTime = 1.f / gameUpdateHertz;
}
// 3. 设置到 NetworkInterface
networkInterface.maxSocketProcessingTime(
maxInternalSocketProcessingTime );
}关键细节:
- 配置值可以覆盖默认值,提供灵活性
- 默认值与游戏 tick 频率绑定,确保公平性
- 可以通过 Watcher 动态调整,便于调试
与 Tick 的关系
BWMessageForwarder 读取:
maxInternalSocketProcessingTimegameUpdateHertz
如果配置值小于 0,则设置为 1.f / gameUpdateHertz,然后调用 networkInterface.maxSocketProcessingTime()。源码见 bw_message_forwarder.cpp。
这说明接收预算不是孤立参数,它和游戏 tick 频率绑定。
设计含义:
- 如果
gameUpdateHertz = 10,一个 tick 是 100ms。 - 内部 socket 单轮处理预算可按 tick 长度推导。
- 高峰网络包不会无限吃掉主循环。
源码观测应把这个指标拆成:
- 每 tick 网络处理耗时。
- 每次 socket notification 处理包数。
- 超预算次数。
- receive queue size。
- handler 分发耗时。
发送侧背压
发送路径使用 sendto():
int len = socket_.sendto( pPacket->data(), pPacket->totalSize(), addr.port, addr.ip );源码见 packet_sender.cpp。
错误映射:
ECONNREFUSED->REASON_NO_SUCH_PORTEAGAIN->REASON_RESOURCE_UNAVAILABLEENOBUFS->REASON_TRANSMIT_QUEUE_FULLEMSGSIZE->REASON_MESSAGE_TOO_LONG
源码见 packet_sender.cpp。
遇到 REASON_RESOURCE_UNAVAILABLE 或 REASON_TRANSMIT_QUEUE_FULL 时,basicSendWithRetries() 会:
- 打印
Transmit queue fullwarning。 - 用
select()等待 socket 可写 10ms。 - 然后继续重试。
源码见 packet_sender.cpp。
这是发送侧背压的源码取舍:
- 简单稳妥。
- 能处理短暂 kernel transmit queue 满。
- 但会在发送路径引入最多 10ms 等待,可能拖慢主线程。
应优先统计这条路径出现频率和等待耗时,再判断发送侧背压是否是主线程抖动来源。
人工丢包与延迟
PacketSender::rescheduleSend() 支持两类故障注入:
dropNextSend()或按dropPerMillion()概率人工丢包。- 按
minLatencyMillion()/maxLatencyMillion()人工延迟发送。
源码见 packet_sender.cpp。
如果延迟大于等于 2ms,会创建 RescheduledSender 稍后发送。见 packet_sender.cpp。
设计价值:
- 可测试 Mercury 可靠 UDP 重传。
- 可模拟公网延迟和丢包。
- 可验证 ACK、piggyback、fragment、receive window。
- 可在不依赖外部 netem 的情况下做单元测试。
网络单测中可以看到大量 dropNextSend() 用法,例如可靠层测试见 test_reliable.cpp。
故障注入与测试的边界
BigWorld 的人工故障注入偏网络层:
- 丢下一包。
- 按概率丢包。
- 延迟包。
- flood/mangle/fragment 等单测覆盖。
但它不是完整 chaos framework:
- 不能直接模拟 DBApp 卡死。
- 不能完整模拟 clock jump。
- 不能自动验证 CellApp death 后实体恢复。
- 不能跨多进程注入部分网络分区。
- 缺少统一 scenario 描述和自动报告。
所以它很适合可靠 UDP 单元测试,但不等于现代混沌测试平台。
背压链路
flowchart LR A[Kernel RX Queue] --> B[PacketReceiver budget] B --> C[Mercury decode] C --> D[Channel reliable state] D --> E[Message handler] E --> F[Python/Entity logic] F --> G[Bundle output] G --> H[PacketSender] H --> I{sendto result} I -- success --> J[Kernel TX Queue] I -- EAGAIN/ENOBUFS --> K[select wait 10ms retry] I -- EMSGSIZE --> L[MTU/message too long] H --> M[artificial drop/latency] 这条链说明:真正的背压不只是 socket。
可能瓶颈包括:
- Kernel receive queue。
- 单轮 socket 处理预算。
- Mercury 包解析。
- 可靠层重发和 ACK。
- handler 执行。
- Python 脚本。
- Entity/AOI 更新。
- Bundle 聚合。
- Kernel transmit queue。
必须先观测每一段,否则容易把所有问题都归因于 epoll 或 io_uring。
为什么没有 io_uring 背压模型
BigWorld 设计年代没有 io_uring。更关键的是,当前模型是 readiness + 同步 handler:
- epoll 通知可读。
- 主线程同步处理包。
- handler 立即执行。
- Timer 和 network 在同一 dispatcher 中调度。
io_uring 是 completion 模型,引入:
- submit queue。
- completion queue。
- buffer 生命周期。
- 多 outstanding I/O。
- 完成事件与 handler 调度解耦。
这会改变执行模型,不只是替换 recvfrom()。
源码取舍
背压和故障注入的源码取舍是把网络压力显式暴露到主循环:
PacketReceiver::maxSocketProcessingTime限制一次可读事件处理时间,保护 Tick 公平性。PacketSender::basicSendWithRetries()在发送失败时有重试和短等待路径,但仍可能占用主线程。- artificial loss/latency、
dropNextSend()等测试钩子让可靠层可复现丢包和乱序。 - Channel overflow 配置把可靠窗口压力转成可观测/可处理状态。
源码代价:
- 发送队列满路径仍可能让主线程等待。
- 故障注入主要在引擎测试钩子内,不能覆盖所有真实网络抖动。
- 接收预算只能限制单次 socket 处理,不限制 handler 内部业务耗时。
- I/O 后端变化会牵动 buffer 生命周期和 handler 调度,不只是替换系统调用。
源码验证重点
背压和故障注入测试应覆盖网络压力传播:
PacketReceiver::handleInputNotification()应记录处理包数、耗时和超预算退出。PacketSender::basicSendWithRetries()应覆盖 EAGAIN、ENOBUFS、select 等待和最终失败。dropNextSend()、artificial loss/latency 应能复现 ACK 丢失、数据包丢失、回复丢失和乱序。- Channel overflow 达到阈值后应按配置丢弃、阻塞或报错,不能静默损坏可靠状态。
- 登录 flood、畸形包、超大 Bundle 和非法 message id 应在进入重业务逻辑前被拦截。
- 故障注入场景应同时覆盖 external client channel 和 internal server channel。
本章边界
本章分析网络背压和故障注入。它与 网络 I/O 模型选择 互补:前者解释选择 epoll/recvfrom 的模型,本文解释运行时如何保护 Tick 和验证可靠层。
相关文档
- 网络 I/O 模型,详见 网络 I/O 模型选择。
- Mercury 可靠 UDP,详见 Mercury 可靠 UDP。
- 通信抽象与 RPC,详见 通信抽象与 RPC。
- 主循环与事件分发,详见 主循环、Tick 与事件分发。
- 测试与故障注入,详见 测试体系、故障注入与覆盖边界。
