絡(luò)負(fù)載:一條路能跑多少車,堵了會(huì)怎樣)
一句話網(wǎng)絡(luò)負(fù)載就是網(wǎng)絡(luò)上正在跑的數(shù)據(jù)量占了多少能力。負(fù)載低數(shù)據(jù)暢通無阻負(fù)載接近上限數(shù)據(jù)開始排隊(duì)于是延遲升高、抖動(dòng)變大最后直接丟包。前幾篇講了網(wǎng)關(guān)數(shù)據(jù)從哪出去、子網(wǎng)誰和誰是鄰居、抖動(dòng)緩沖怎么扛波動(dòng)、CDN怎么把大文件分散出去。這一篇講它們背后共同的問題網(wǎng)絡(luò)的能力是有限的用多了會(huì)怎樣以及怎么少用。一、生活里的例子高速公路一條三車道高速公路路況車流體驗(yàn)凌晨車很少一路 120 碼平時(shí)車不少但能跑偶爾要減速節(jié)假日接近路的極限走走停停時(shí)快時(shí)慢出了事故超過極限堵死后面的車根本上不了高速高速公路網(wǎng)絡(luò)車道數(shù)帶寬最多能跑多少數(shù)據(jù)實(shí)際車流吞吐量實(shí)際跑了多少車流 ÷ 能力負(fù)載率 / 利用率收費(fèi)站排隊(duì)路由器、網(wǎng)卡里的隊(duì)列走走停停延遲升高、抖動(dòng)變大匝道封閉車上不去丟包關(guān)鍵路堵不堵不只看車多不多要看車流 ÷ 路的能力。同樣 1000 輛車在單車道是災(zāi)難在八車道很輕松。二、幾個(gè)必須分清的概念概念意思單位帶寬Bandwidth鏈路的最大能力Mbps、Gbps吞吐量Throughput實(shí)際傳了多少M(fèi)bps利用率Utilization吞吐量 ÷ 帶寬%包速率PPS每秒處理多少個(gè)包包/秒延遲 / 抖動(dòng) / 丟包負(fù)載高的后果ms、ms、%單位換算最常見的誤會(huì)運(yùn)營商說的 100M 寬帶 100 Mbps兆比特每秒 下載軟件顯示的速度 MB/s兆字節(jié)每秒 1 字節(jié) 8 比特 → 100 Mbps ÷ 8 ≈ 12.5 MB/s所以辦了 100M 寬帶下載只有 12MB/s是正常的不是被騙。帶寬和 PPS 是兩種不同的上限情況先撞到哪個(gè)上限下載大文件每個(gè)包都接近 1500 字節(jié)帶寬游戲同步每個(gè)包只有幾十到幾百字節(jié)但非常多PPS游戲服務(wù)器經(jīng)常是帶寬還很空PPS 已經(jīng)滿了。每個(gè)包無論大小都要經(jīng)過一次中斷、一次系統(tǒng)調(diào)用前面講過的recvfrom/sendto、一次路由查表 ——處理開銷是按個(gè)算的。很多云服務(wù)器也會(huì)對(duì)每臺(tái)機(jī)器的 PPS 設(shè)上限。三、負(fù)載高了為什么會(huì)變慢排隊(duì)網(wǎng)絡(luò)設(shè)備里每個(gè)出口都有一個(gè)隊(duì)列。包到得比發(fā)得快就要排隊(duì)。一個(gè)反直覺的事實(shí)利用率不是越接近 100% 越好用最簡單的排隊(duì)模型估算平均等待時(shí)間大約是平均耗時(shí) ≈ 處理一個(gè)包的時(shí)間 ÷ (1 ? 利用率)利用率相對(duì)于空閑時(shí)的耗時(shí)10%1.1 倍50%2 倍80%5 倍90%10 倍95%20 倍99%100 倍延遲 │ ╱ │ ╱ │ ╱ │ ╱ │ __╱ │____________________───── └──────────────────────────────────── 利用率 0% 50% 80% 100% ↑ 過了這里延遲開始飆升這就是為什么運(yùn)維常說鏈路利用率長期超過 70%80% 就該擴(kuò)容了。不是因?yàn)闈M了而是因?yàn)檫€沒滿體驗(yàn)就已經(jīng)崩了。負(fù)載升高的完整過程負(fù)載低 → 包幾乎不用排隊(duì) 延遲穩(wěn)定 負(fù)載升高 → 隊(duì)列時(shí)長時(shí)短 **抖動(dòng)變大**抖動(dòng)緩沖區(qū)要變大 接近上限 → 隊(duì)列一直很長 **延遲持續(xù)升高** 超過上限 → 隊(duì)列滿了新包被直接扔掉 **丟包**上一篇提到的**緩沖膨脹Bufferbloat**就發(fā)生在第三步路由器的隊(duì)列設(shè)得太大寧可讓包排幾百毫秒也不丟結(jié)果能用但巨卡。四、負(fù)載從哪來家里、路上、服務(wù)器一個(gè)玩家打游戲數(shù)據(jù)要經(jīng)過好幾段路任何一段滿了都會(huì)卡玩家電腦 ─ Wi-Fi ─ 家用網(wǎng)關(guān) ─ 運(yùn)營商接入 ─ 骨干網(wǎng) ─ 機(jī)房出口 ─ 服務(wù)器網(wǎng)卡 ─ 游戲進(jìn)程 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧位置典型的負(fù)載問題① 玩家電腦后臺(tái)在下載、同步網(wǎng)盤、更新系統(tǒng)② Wi-Fi信號(hào)差、鄰居信道干擾、幾十臺(tái)設(shè)備搶一個(gè)路由器③ 家用網(wǎng)關(guān)上行帶寬小很多寬帶下行 300M上行只有 30M有人在直播、上傳視頻④⑤ 運(yùn)營商 / 骨干晚高峰擁塞、跨運(yùn)營商互聯(lián)口擁塞⑥ 機(jī)房出口開服、版本更新、遭遇 DDoS⑦ 服務(wù)器網(wǎng)卡帶寬或 PPS 上限⑧ 游戲進(jìn)程處理不過來接收緩沖區(qū)堆滿內(nèi)核開始丟包第 ⑧ 點(diǎn)容易被忽略網(wǎng)絡(luò)沒滿是程序處理不過來。表現(xiàn)和網(wǎng)絡(luò)擁塞一樣延遲高、丟包但要優(yōu)化的是代碼不是帶寬。五、動(dòng)手查看網(wǎng)絡(luò)負(fù)載客戶端 / 個(gè)人電腦系統(tǒng)工具看什么Windows任務(wù)管理器 → 性能 → Wi-Fi/以太網(wǎng)實(shí)時(shí)收發(fā)速度Windows資源監(jiān)視器 → 網(wǎng)絡(luò)哪個(gè)進(jìn)程在占帶寬macOS活動(dòng)監(jiān)視器 → 網(wǎng)絡(luò)每個(gè)進(jìn)程的流量通用ping -t 服務(wù)器邊下載邊 ping負(fù)載升高時(shí)延遲有沒有飆升一個(gè)簡單的緩沖膨脹自測1. 不開任何下載ping 游戲服務(wù)器 → 比如 30ms 2. 開一個(gè)大文件下載同時(shí)繼續(xù) ping → 如果漲到 200ms就是排隊(duì)嚴(yán)重Linux 服務(wù)器nload# 實(shí)時(shí)看網(wǎng)卡收發(fā)帶寬iftop# 看哪些連接在占流量sar-nDEV1# 每秒的收發(fā)字節(jié)數(shù)、包數(shù)PPSss-s# 連接數(shù)統(tǒng)計(jì)netstat-su# UDP 統(tǒng)計(jì)留意 receive buffer errors程序來不及收iperf3-c服務(wù)器IP# 測兩臺(tái)機(jī)器之間的實(shí)際帶寬netstat -su里的receive buffer errors一直在漲說明內(nèi)核替你丟包了網(wǎng)絡(luò)把包送到了你的進(jìn)程沒來得及讀。這時(shí)調(diào)大接收緩沖區(qū)SO_RCVBUF或優(yōu)化處理邏輯比加帶寬有用。Unity 里工具用途Profiler → Network Messages / Network Operations看每幀收發(fā)多少消息、多少字節(jié)Netcode for GameObjects 的Network Profiler / Runtime Network Stats Monitor按對(duì)象、按消息類型統(tǒng)計(jì)流量Wireshark抓包看每個(gè)包到底有多大、多少個(gè)六、算一筆賬射擊游戲到底要多少帶寬假設(shè)一局64 人的射擊游戲服務(wù)器30Hz每秒 30 次快照。每個(gè)玩家的狀態(tài)樸素寫法字段類型字節(jié)玩家 IDint4位置 xyz3 × float12朝向4 × float四元數(shù)16速度 xyz3 × float12血量、狀態(tài)int int8合計(jì)52 字節(jié)計(jì)算每個(gè)快照 63 個(gè)其他玩家 × 52 字節(jié) ≈ 3276 字節(jié) IP/UDP 頭部 28 字節(jié) ≈ 3.3 KB ← 已經(jīng)超過一個(gè)包大約 12001400 字節(jié)的安全上限要拆成 3 個(gè)包 每個(gè)玩家下行3.3 KB × 30 次/秒 ≈ 99 KB/s ≈ 790 Kbps 一局服務(wù)器出口× 64 人 ≈ 6.3 MB/s ≈ 50 Mbps 一臺(tái)機(jī)器跑 50 局≈ 2.5 Gbps ← 普通千兆網(wǎng)卡直接打滿 PPS64 人 × 30 次 × 3 個(gè)包 × 50 局 ≈ 29 萬包/秒這還沒算子彈、技能、特效事件、語音。所以帶寬優(yōu)化不是錦上添花而是決定一臺(tái)服務(wù)器能開多少局、服務(wù)器賬單是多少。優(yōu)化之后第七節(jié)的技巧組合手段每個(gè)玩家的數(shù)據(jù)樸素寫法52 字節(jié)量化 位壓縮≈ 15 字節(jié)只發(fā)變化的字段增量平均 ≈ 6 字節(jié)只發(fā)附近 / 視野內(nèi)的 20 人63 人 → 20 人優(yōu)化后每個(gè)快照20 × 6 28 ≈ 150 字節(jié) → 一個(gè)包就夠 每個(gè)玩家下行150 × 30 ≈ 4.5 KB/s ≈ 36 Kbps從 790 Kbps 降到 36 Kbps約二十分之一。同一臺(tái)服務(wù)器可以多開很多局玩家在差網(wǎng)絡(luò)下也更穩(wěn)。七、降低負(fù)載的六個(gè)核心技巧1. 量化別用 float 存不需要那么精確的東西地圖范圍 ±1024 米精度 1 厘米就夠可能的取值2048 ÷ 0.01 204,800 種 2^18 262,144 ≥ 204,800 → 每個(gè)軸 18 位就夠 位置3 × 32 位 96 位 → 3 × 18 位 54 位數(shù)據(jù)樸素量化后說明位置96 位54 位1cm 精度只有水平朝向yaw32 位10 位約 0.35° 精度完整旋轉(zhuǎn)128 位約 30 位最小三分量壓縮四元數(shù)血量 010032 位7 位布爾狀態(tài)蹲下、開火…各 832 位各 1 位publicstaticclassQuantize{// 把 [min, max] 范圍的 float 壓到 bits 位整數(shù)publicstaticuintEncode(floatv,floatmin,floatmax,intbits){uintmaxValue(1ubits)-1;floattUnityEngine.Mathf.InverseLerp(min,max,v);// 自動(dòng)夾到 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. 位壓縮按位寫不按字節(jié)寫量化之后是 18 位、7 位、1 位這種零碎的長度用字節(jié)存會(huì)浪費(fèi)。寫一個(gè)簡單的位寫入器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;// 向上取整到字節(jié)publicbyte[]Bufferbuffer;}使用varwnewBitWriter(1200);w.Write(Quantize.Encode(pos.x,-1024,1024,18),18);w.Write(Quantize.Encode(pos.y,-64,192,15),15);// 高度范圍小位數(shù)更少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);// 合計(jì) 69 位 ≈ 9 字節(jié)原來是 52 字節(jié)這里的移位和按位與和上一篇子網(wǎng)掩碼的計(jì)算是同一套東西 3是除以 8 7是對(duì) 8 取余。3. 增量壓縮只發(fā)變了的站著不動(dòng)的玩家位置每幀都一樣沒必要重發(fā)。每個(gè)字段前加 1 位變了嗎 0 → 沒變后面不帶數(shù)據(jù) 1 新值 → 變了 更進(jìn)一步以客戶端已確認(rèn)收到的那一幀為基準(zhǔn)做差情況位置字段站著不動(dòng)1 位在走路1 位 54 位基準(zhǔn)必須是對(duì)方確認(rèn)收到過的幀否則丟一個(gè)包后面的增量全都對(duì)不上。4. 興趣管理AOI只發(fā)你需要知道的地圖另一頭的敵人你看不見、聽不見、打不著就不用每幀告訴你。距離 / 狀態(tài)發(fā)送頻率視野內(nèi)、50 米內(nèi)每個(gè)快照都發(fā)30Hz50200 米每 3 個(gè)快照發(fā)一次10Hz200 米外每秒 2 次只發(fā)位置墻后面、完全看不見不發(fā)順便防透視外掛第一篇講遮擋剔除時(shí)說看不見的不畫這里是看不見的不發(fā)。思路完全一樣甚至可以復(fù)用同樣的空間劃分和可見性數(shù)據(jù)。5. 帶寬預(yù)算 優(yōu)先級(jí)總量固定重要的先發(fā)給每個(gè)客戶端每個(gè)快照一個(gè)固定預(yù)算比如 1200 字節(jié)按優(yōu)先級(jí)裝包裝不下的留到下一次 —— 但它的優(yōu)先級(jí)會(huì)一直累積不會(huì)被永遠(yuǎn)餓死。usingSystem.Collections.Generic;publicclassPrioritySender{classEntry{publicintentityId;publicfloataccumulated;}readonlyListEntryentriesnewListEntry();publicvoidAdd(intid)entries.Add(newEntry{entityIdid});// basePriority根據(jù)距離、是否在視野內(nèi)、是否在開火等計(jì)算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;// 這次放不下下次優(yōu)先級(jí)更高usedsize;picked.Add(e.entityId);e.accumulated0;// 發(fā)了就清零}returnpicked;}}實(shí)體基礎(chǔ)優(yōu)先級(jí)正在朝你開火的敵人很高附近的隊(duì)友高遠(yuǎn)處走動(dòng)的敵人中地上掉落的彈藥箱低6. 合并小包、控制發(fā)送頻率做法原因一個(gè)快照的所有內(nèi)容合進(jìn)一個(gè)包每個(gè)包都有 28 字節(jié)頭部還占一次 PPS包大小控制在約 1200 字節(jié)以內(nèi)超過路徑 MTU常見 1500各種隧道下更小會(huì)被分片丟一片整個(gè)包作廢客戶端輸入可以每幀帶上最近幾幀的輸入多幾個(gè)字節(jié)換來丟一個(gè)包也不丟操作冗余用少量帶寬換可靠性tick 率按需選擇競技射擊 3064Hz休閑游戲 1020Hz 往往足夠八、網(wǎng)絡(luò)已經(jīng)擁塞時(shí)怎么辦擁塞控制TCP 自帶剎車TCP 發(fā)現(xiàn)丟包或延遲升高會(huì)自動(dòng)降低發(fā)送速度等路況好了再慢慢提速。所以下載大文件時(shí)網(wǎng)絡(luò)不會(huì)被一個(gè)連接徹底堵死。UDP 沒有剎車游戲要自己做射擊游戲大多用 UDP。如果路已經(jīng)堵了還照常狂發(fā)只會(huì)讓隊(duì)列更長、丟包更多自己和別人都更卡。自適應(yīng)發(fā)送的簡單思路觀察到的信號(hào)反應(yīng)RTT 持續(xù)升高、抖動(dòng)變大降低非關(guān)鍵數(shù)據(jù)的頻率遠(yuǎn)處實(shí)體、裝飾性事件丟包率超過閾值如 5%降低快照頻率比如 30Hz → 20Hz一段時(shí)間都很穩(wěn)定慢慢恢復(fù)極差提示網(wǎng)絡(luò)不佳只保留最核心的數(shù)據(jù)這和抖動(dòng)緩沖區(qū)里緩沖區(qū)根據(jù)抖動(dòng)自動(dòng)調(diào)整是一個(gè)思路持續(xù)測量動(dòng)態(tài)適應(yīng)變化要平滑。客戶端能做的措施效果游戲中自動(dòng)暫停后臺(tái)的資源下載不和自己的戰(zhàn)斗流量搶上行熱更新下載限速邊玩邊下時(shí)不影響對(duì)局語音降低碼率網(wǎng)絡(luò)差時(shí)優(yōu)先保證操作同步九、服務(wù)器扛不住時(shí)把負(fù)載分出去單臺(tái)機(jī)器的帶寬、PPS、CPU 都有上限。超過之后就要把負(fù)載分?jǐn)偟蕉嗯_(tái)機(jī)器。手段分?jǐn)偟氖鞘裁辞懊嬷v過的關(guān)聯(lián)CDN安裝包、補(bǔ)丁、熱更資源上一篇靜態(tài)大流量完全不經(jīng)過游戲服務(wù)器負(fù)載均衡器登錄、商城、匹配等 HTTP / TCP 請(qǐng)求把請(qǐng)求輪流或按負(fù)載分給多臺(tái)后端按對(duì)局分配服務(wù)器實(shí)時(shí)戰(zhàn)斗一局只在一臺(tái)機(jī)器上新開的局放到最空閑的機(jī)器多地區(qū)部署跨地區(qū)的長距離流量就近接入減少骨干網(wǎng)負(fù)載和延遲抗 DDoS / 清洗惡意流量攻擊的本質(zhì)就是人為制造網(wǎng)絡(luò)負(fù)載┌─ 靜態(tài)資源 ─→ CDN 玩家 ─────────┼─ 登錄/商城 ─→ 負(fù)載均衡器 ─→ 多臺(tái) Web 服務(wù)器 └─ 實(shí)時(shí)戰(zhàn)斗 ─→ 匹配服務(wù)分配 ─→ 某臺(tái)戰(zhàn)斗服務(wù)器某一局這又回到架構(gòu)原則不同類型的負(fù)載走不同的路各自獨(dú)立擴(kuò)容一邊出問題不拖垮另一邊。十、和前面幾篇的聯(lián)系前面講過的關(guān)系網(wǎng)關(guān)家用網(wǎng)關(guān)的上行帶寬往往是瓶頸開啟 QoS / SQM 可以讓游戲包優(yōu)先、減少排隊(duì)子網(wǎng)掩碼網(wǎng)段越大廣播越多局域網(wǎng)本身的負(fù)載就越高 —— 劃分子網(wǎng)能減少廣播負(fù)載抖動(dòng)緩沖區(qū)負(fù)載升高 → 排隊(duì)變化 → 抖動(dòng)變大 → 緩沖區(qū)被迫變大 → 延遲升高CDN把最大的那部分流量下載從源站卸掉遮擋剔除看不見的不畫與興趣管理看不見的不發(fā)同一個(gè)思路系統(tǒng)調(diào)用每個(gè)包一次sendto/recvfromPPS 高時(shí)可以用批量收發(fā)如 Linux 的sendmmsg/recvmmsg減少系統(tǒng)調(diào)用次數(shù)對(duì)象池每個(gè)包都new byte[]會(huì)產(chǎn)生大量 GC收發(fā)緩沖區(qū)應(yīng)復(fù)用十一、常見的坑坑現(xiàn)象解決把 Mbps 當(dāng) MB/s帶寬規(guī)劃差了 8 倍記住除以 8只看帶寬不看 PPS帶寬很空服務(wù)器卻在丟包同時(shí)監(jiān)控 PPS合并小包利用率跑到 90% 才擴(kuò)容還沒滿延遲已經(jīng)飆升70%80% 就開始規(guī)劃擴(kuò)容位置、旋轉(zhuǎn)全用 float 原樣發(fā)帶寬是需要的好幾倍量化 位壓縮全圖所有實(shí)體發(fā)給所有人人一多帶寬平方級(jí)增長n 人 × n 人興趣管理 優(yōu)先級(jí)增量壓縮的基準(zhǔn)沒被確認(rèn)丟包后客戶端狀態(tài)全亂以客戶端 ACK 過的幀為基準(zhǔn)包太大被分片丟包率莫名其妙偏高控制在約 1200 字節(jié)以內(nèi)UDP 擁塞時(shí)照??癜l(fā)越卡越發(fā)越發(fā)越卡根據(jù)丟包和 RTT 自適應(yīng)降頻進(jìn)程處理慢誤以為網(wǎng)絡(luò)問題加了帶寬依然丟包看netstat -su的 buffer errors優(yōu)化接收線程對(duì)局中后臺(tái)下載熱更新玩著玩著突然延遲飆升對(duì)局中暫?;蛳匏傧螺d只在局域網(wǎng)測試本地流暢上線就卡用網(wǎng)絡(luò)模擬工具限制帶寬、加丟包測試沒算服務(wù)器出口賬單上線后流量費(fèi)超預(yù)算開發(fā)階段就按每人每秒多少字節(jié)做預(yù)算一句話總結(jié)網(wǎng)絡(luò)負(fù)載 實(shí)際流量 ÷ 網(wǎng)絡(luò)能力。負(fù)載升高時(shí)數(shù)據(jù)先排隊(duì)抖動(dòng)變大、延遲升高再丟包而且延遲不是線性增長利用率過了 70%80% 就會(huì)急劇變差。衡量負(fù)載要同時(shí)看帶寬和