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