
一句话网络负载就是网络上正在跑的数据量占了多少能力。负载低数据畅通无阻负载接近上限数据开始排队于是延迟升高、抖动变大最后直接丢包。前几篇讲了网关数据从哪出去、子网谁和谁是邻居、抖动缓冲怎么扛波动、CDN怎么把大文件分散出去。这一篇讲它们背后共同的问题网络的能力是有限的用多了会怎样以及怎么少用。一、生活里的例子高速公路一条三车道高速公路路况车流体验凌晨车很少一路 120 码平时车不少但能跑偶尔要减速节假日接近路的极限走走停停时快时慢出了事故超过极限堵死后面的车根本上不了高速高速公路网络车道数带宽最多能跑多少数据实际车流吞吐量实际跑了多少车流 ÷ 能力负载率 / 利用率收费站排队路由器、网卡里的队列走走停停延迟升高、抖动变大匝道封闭车上不去丢包关键路堵不堵不只看车多不多要看车流 ÷ 路的能力。同样 1000 辆车在单车道是灾难在八车道很轻松。二、几个必须分清的概念概念意思单位带宽Bandwidth链路的最大能力Mbps、Gbps吞吐量Throughput实际传了多少Mbps利用率Utilization吞吐量 ÷ 带宽%包速率PPS每秒处理多少个包包/秒延迟 / 抖动 / 丢包负载高的后果ms、ms、%单位换算最常见的误会运营商说的 100M 宽带 100 Mbps兆比特每秒 下载软件显示的速度 MB/s兆字节每秒 1 字节 8 比特 → 100 Mbps ÷ 8 ≈ 12.5 MB/s所以办了 100M 宽带下载只有 12MB/s是正常的不是被骗。带宽和 PPS 是两种不同的上限情况先撞到哪个上限下载大文件每个包都接近 1500 字节带宽游戏同步每个包只有几十到几百字节但非常多PPS游戏服务器经常是带宽还很空PPS 已经满了。每个包无论大小都要经过一次中断、一次系统调用前面讲过的recvfrom/sendto、一次路由查表 ——处理开销是按个算的。很多云服务器也会对每台机器的 PPS 设上限。三、负载高了为什么会变慢排队网络设备里每个出口都有一个队列。包到得比发得快就要排队。一个反直觉的事实利用率不是越接近 100% 越好用最简单的排队模型估算平均等待时间大约是平均耗时 ≈ 处理一个包的时间 ÷ (1 − 利用率)利用率相对于空闲时的耗时10%1.1 倍50%2 倍80%5 倍90%10 倍95%20 倍99%100 倍延迟 │ ╱ │ ╱ │ ╱ │ ╱ │ __╱ │____________________───── └──────────────────────────────────── 利用率 0% 50% 80% 100% ↑ 过了这里延迟开始飙升这就是为什么运维常说链路利用率长期超过 70%80% 就该扩容了。不是因为满了而是因为还没满体验就已经崩了。负载升高的完整过程负载低 → 包几乎不用排队 延迟稳定 负载升高 → 队列时长时短 **抖动变大**抖动缓冲区要变大 接近上限 → 队列一直很长 **延迟持续升高** 超过上限 → 队列满了新包被直接扔掉 **丢包**上一篇提到的**缓冲膨胀Bufferbloat**就发生在第三步路由器的队列设得太大宁可让包排几百毫秒也不丢结果能用但巨卡。四、负载从哪来家里、路上、服务器一个玩家打游戏数据要经过好几段路任何一段满了都会卡玩家电脑 ─ Wi-Fi ─ 家用网关 ─ 运营商接入 ─ 骨干网 ─ 机房出口 ─ 服务器网卡 ─ 游戏进程 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧位置典型的负载问题① 玩家电脑后台在下载、同步网盘、更新系统② Wi-Fi信号差、邻居信道干扰、几十台设备抢一个路由器③ 家用网关上行带宽小很多宽带下行 300M上行只有 30M有人在直播、上传视频④⑤ 运营商 / 骨干晚高峰拥塞、跨运营商互联口拥塞⑥ 机房出口开服、版本更新、遭遇 DDoS⑦ 服务器网卡带宽或 PPS 上限⑧ 游戏进程处理不过来接收缓冲区堆满内核开始丢包第 ⑧ 点容易被忽略网络没满是程序处理不过来。表现和网络拥塞一样延迟高、丢包但要优化的是代码不是带宽。五、动手查看网络负载客户端 / 个人电脑系统工具看什么Windows任务管理器 → 性能 → Wi-Fi/以太网实时收发速度Windows资源监视器 → 网络哪个进程在占带宽macOS活动监视器 → 网络每个进程的流量通用ping -t 服务器边下载边 ping负载升高时延迟有没有飙升一个简单的缓冲膨胀自测1. 不开任何下载ping 游戏服务器 → 比如 30ms 2. 开一个大文件下载同时继续 ping → 如果涨到 200ms就是排队严重Linux 服务器nload# 实时看网卡收发带宽iftop# 看哪些连接在占流量sar-nDEV1# 每秒的收发字节数、包数PPSss-s# 连接数统计netstat-su# UDP 统计留意 receive buffer errors程序来不及收iperf3-c服务器IP# 测两台机器之间的实际带宽netstat -su里的receive buffer errors一直在涨说明内核替你丢包了网络把包送到了你的进程没来得及读。这时调大接收缓冲区SO_RCVBUF或优化处理逻辑比加带宽有用。Unity 里工具用途Profiler → Network Messages / Network Operations看每帧收发多少消息、多少字节Netcode for GameObjects 的Network Profiler / Runtime Network Stats Monitor按对象、按消息类型统计流量Wireshark抓包看每个包到底有多大、多少个六、算一笔账射击游戏到底要多少带宽假设一局64 人的射击游戏服务器30Hz每秒 30 次快照。每个玩家的状态朴素写法字段类型字节玩家 IDint4位置 xyz3 × float12朝向4 × float四元数16速度 xyz3 × float12血量、状态int int8合计52 字节计算每个快照 63 个其他玩家 × 52 字节 ≈ 3276 字节 IP/UDP 头部 28 字节 ≈ 3.3 KB ← 已经超过一个包大约 12001400 字节的安全上限要拆成 3 个包 每个玩家下行3.3 KB × 30 次/秒 ≈ 99 KB/s ≈ 790 Kbps 一局服务器出口× 64 人 ≈ 6.3 MB/s ≈ 50 Mbps 一台机器跑 50 局≈ 2.5 Gbps ← 普通千兆网卡直接打满 PPS64 人 × 30 次 × 3 个包 × 50 局 ≈ 29 万包/秒这还没算子弹、技能、特效事件、语音。所以带宽优化不是锦上添花而是决定一台服务器能开多少局、服务器账单是多少。优化之后第七节的技巧组合手段每个玩家的数据朴素写法52 字节量化 位压缩≈ 15 字节只发变化的字段增量平均 ≈ 6 字节只发附近 / 视野内的 20 人63 人 → 20 人优化后每个快照20 × 6 28 ≈ 150 字节 → 一个包就够 每个玩家下行150 × 30 ≈ 4.5 KB/s ≈ 36 Kbps从 790 Kbps 降到 36 Kbps约二十分之一。同一台服务器可以多开很多局玩家在差网络下也更稳。七、降低负载的六个核心技巧1. 量化别用 float 存不需要那么精确的东西地图范围 ±1024 米精度 1 厘米就够可能的取值2048 ÷ 0.01 204,800 种 2^18 262,144 ≥ 204,800 → 每个轴 18 位就够 位置3 × 32 位 96 位 → 3 × 18 位 54 位数据朴素量化后说明位置96 位54 位1cm 精度只有水平朝向yaw32 位10 位约 0.35° 精度完整旋转128 位约 30 位最小三分量压缩四元数血量 010032 位7 位布尔状态蹲下、开火…各 832 位各 1 位publicstaticclassQuantize{// 把 [min, max] 范围的 float 压到 bits 位整数publicstaticuintEncode(floatv,floatmin,floatmax,intbits){uintmaxValue(1ubits)-1;floattUnityEngine.Mathf.InverseLerp(min,max,v);// 自动夹到 0~1return(uint)UnityEngine.Mathf.RoundToInt(t*maxValue);}publicstaticfloatDecode(uintq,floatmin,floatmax,intbits){uintmaxValue(1ubits)-1;returnUnityEngine.Mathf.Lerp(min,max,q/(float)maxValue);}}2. 位压缩按位写不按字节写量化之后是 18 位、7 位、1 位这种零碎的长度用字节存会浪费。写一个简单的位写入器publicclassBitWriter{readonlybyte[]buffer;intbitPos;publicBitWriter(intcapacity)buffernewbyte[capacity];publicvoidWrite(uintvalue,intbits){for(inti0;ibits;i){if(((valuei)1)!0)buffer[bitPos3]|(byte)(1(bitPos7));bitPos;}}publicvoidWriteBool(boolb)Write(b?1u:0u,1);publicintByteCount(bitPos7)3;// 向上取整到字节publicbyte[]Bufferbuffer;}使用varwnewBitWriter(1200);w.Write(Quantize.Encode(pos.x,-1024,1024,18),18);w.Write(Quantize.Encode(pos.y,-64,192,15),15);// 高度范围小位数更少w.Write(Quantize.Encode(pos.z,-1024,1024,18),18);w.Write(Quantize.Encode(yaw,0,360,10),10);w.Write((uint)health,7);w.WriteBool(isCrouching);// 合计 69 位 ≈ 9 字节原来是 52 字节这里的移位和按位与和上一篇子网掩码的计算是同一套东西 3是除以 8 7是对 8 取余。3. 增量压缩只发变了的站着不动的玩家位置每帧都一样没必要重发。每个字段前加 1 位变了吗 0 → 没变后面不带数据 1 新值 → 变了 更进一步以客户端已确认收到的那一帧为基准做差情况位置字段站着不动1 位在走路1 位 54 位基准必须是对方确认收到过的帧否则丢一个包后面的增量全都对不上。4. 兴趣管理AOI只发你需要知道的地图另一头的敌人你看不见、听不见、打不着就不用每帧告诉你。距离 / 状态发送频率视野内、50 米内每个快照都发30Hz50200 米每 3 个快照发一次10Hz200 米外每秒 2 次只发位置墙后面、完全看不见不发顺便防透视外挂第一篇讲遮挡剔除时说看不见的不画这里是看不见的不发。思路完全一样甚至可以复用同样的空间划分和可见性数据。5. 带宽预算 优先级总量固定重要的先发给每个客户端每个快照一个固定预算比如 1200 字节按优先级装包装不下的留到下一次 —— 但它的优先级会一直累积不会被永远饿死。usingSystem.Collections.Generic;publicclassPrioritySender{classEntry{publicintentityId;publicfloataccumulated;}readonlyListEntryentriesnewListEntry();publicvoidAdd(intid)entries.Add(newEntry{entityIdid});// basePriority根据距离、是否在视野内、是否在开火等计算publicListintPick(System.Funcint,floatbasePriority,System.Funcint,intsizeOf,intbudgetBytes){foreach(vareinentries)e.accumulatedbasePriority(e.entityId);entries.Sort((a,b)b.accumulated.CompareTo(a.accumulated));varpickednewListint();intused0;foreach(vareinentries){intsizesizeOf(e.entityId);if(usedsizebudgetBytes)continue;// 这次放不下下次优先级更高usedsize;picked.Add(e.entityId);e.accumulated0;// 发了就清零}returnpicked;}}实体基础优先级正在朝你开火的敌人很高附近的队友高远处走动的敌人中地上掉落的弹药箱低6. 合并小包、控制发送频率做法原因一个快照的所有内容合进一个包每个包都有 28 字节头部还占一次 PPS包大小控制在约 1200 字节以内超过路径 MTU常见 1500各种隧道下更小会被分片丢一片整个包作废客户端输入可以每帧带上最近几帧的输入多几个字节换来丢一个包也不丢操作冗余用少量带宽换可靠性tick 率按需选择竞技射击 3064Hz休闲游戏 1020Hz 往往足够八、网络已经拥塞时怎么办拥塞控制TCP 自带刹车TCP 发现丢包或延迟升高会自动降低发送速度等路况好了再慢慢提速。所以下载大文件时网络不会被一个连接彻底堵死。UDP 没有刹车游戏要自己做射击游戏大多用 UDP。如果路已经堵了还照常狂发只会让队列更长、丢包更多自己和别人都更卡。自适应发送的简单思路观察到的信号反应RTT 持续升高、抖动变大降低非关键数据的频率远处实体、装饰性事件丢包率超过阈值如 5%降低快照频率比如 30Hz → 20Hz一段时间都很稳定慢慢恢复极差提示网络不佳只保留最核心的数据这和抖动缓冲区里缓冲区根据抖动自动调整是一个思路持续测量动态适应变化要平滑。客户端能做的措施效果游戏中自动暂停后台的资源下载不和自己的战斗流量抢上行热更新下载限速边玩边下时不影响对局语音降低码率网络差时优先保证操作同步九、服务器扛不住时把负载分出去单台机器的带宽、PPS、CPU 都有上限。超过之后就要把负载分摊到多台机器。手段分摊的是什么前面讲过的关联CDN安装包、补丁、热更资源上一篇静态大流量完全不经过游戏服务器负载均衡器登录、商城、匹配等 HTTP / TCP 请求把请求轮流或按负载分给多台后端按对局分配服务器实时战斗一局只在一台机器上新开的局放到最空闲的机器多地区部署跨地区的长距离流量就近接入减少骨干网负载和延迟抗 DDoS / 清洗恶意流量攻击的本质就是人为制造网络负载┌─ 静态资源 ─→ CDN 玩家 ─────────┼─ 登录/商城 ─→ 负载均衡器 ─→ 多台 Web 服务器 └─ 实时战斗 ─→ 匹配服务分配 ─→ 某台战斗服务器某一局这又回到架构原则不同类型的负载走不同的路各自独立扩容一边出问题不拖垮另一边。十、和前面几篇的联系前面讲过的关系网关家用网关的上行带宽往往是瓶颈开启 QoS / SQM 可以让游戏包优先、减少排队子网掩码网段越大广播越多局域网本身的负载就越高 —— 划分子网能减少广播负载抖动缓冲区负载升高 → 排队变化 → 抖动变大 → 缓冲区被迫变大 → 延迟升高CDN把最大的那部分流量下载从源站卸掉遮挡剔除看不见的不画与兴趣管理看不见的不发同一个思路系统调用每个包一次sendto/recvfromPPS 高时可以用批量收发如 Linux 的sendmmsg/recvmmsg减少系统调用次数对象池每个包都new byte[]会产生大量 GC收发缓冲区应复用十一、常见的坑坑现象解决把 Mbps 当 MB/s带宽规划差了 8 倍记住除以 8只看带宽不看 PPS带宽很空服务器却在丢包同时监控 PPS合并小包利用率跑到 90% 才扩容还没满延迟已经飙升70%80% 就开始规划扩容位置、旋转全用 float 原样发带宽是需要的好几倍量化 位压缩全图所有实体发给所有人人一多带宽平方级增长n 人 × n 人兴趣管理 优先级增量压缩的基准没被确认丢包后客户端状态全乱以客户端 ACK 过的帧为基准包太大被分片丢包率莫名其妙偏高控制在约 1200 字节以内UDP 拥塞时照常狂发越卡越发越发越卡根据丢包和 RTT 自适应降频进程处理慢误以为网络问题加了带宽依然丢包看netstat -su的 buffer errors优化接收线程对局中后台下载热更新玩着玩着突然延迟飙升对局中暂停或限速下载只在局域网测试本地流畅上线就卡用网络模拟工具限制带宽、加丢包测试没算服务器出口账单上线后流量费超预算开发阶段就按每人每秒多少字节做预算一句话总结网络负载 实际流量 ÷ 网络能力。负载升高时数据先排队抖动变大、延迟升高再丢包而且延迟不是线性增长利用率过了 70%80% 就会急剧变差。衡量负载要同时看带宽和