你的API latency 曲線平得像機場跑道,突然某個整點飆出200ms尖刺。監(jiān)控面板里,GC(垃圾回收)的CPU占用把業(yè)務(wù)邏輯按在地上摩擦——這種劇本,.NET 團隊里至少一半人見過。
問題不是GC壞了,是你把它當(dāng)全自動掃地機器人,往地上狂扔垃圾。
多數(shù)開發(fā)者對GC的認知停留在"它負責(zé)清內(nèi)存"。但寫高并發(fā)API時,這種黑盒思維直接變成性能殺手。GC在.NET里是分代(generational)架構(gòu):對象按存活時間進三代——Gen 0像便利店,Gen 1像社區(qū)超市,Gen 2像區(qū)域倉庫。越往后,掃描成本指數(shù)級上漲。
Gen 0:短命對象的屠宰場
臨時字符串、LINQ中間結(jié)果、async/await的狀態(tài)機——這些Gen 0常客本來該被快速清理。但當(dāng)你的代碼在循環(huán)里不斷new對象,GC的觸發(fā)頻率會從"偶爾打掃"變成"追著屁股收垃圾"。
一個典型陷阱:日志字符串拼接。string.Format或插值字符串$"user={id}"每次執(zhí)行都造新對象。高QPS接口里,這相當(dāng)于往Gen 0傾瀉小型垃圾山。更隱蔽的是ORM的跟蹤查詢(tracking queries),EF Core默認會把實體塞進上下文緩存,短生命周期的對象被意外續(xù)命到Gen 1甚至Gen 2。
Gen 2:大對象的沼澤地
超過85KB的對象直接進LOH(大對象堆),繞過Gen 0/1。這里不壓縮、只清理,碎片化后內(nèi)存占用虛高。JSON序列化大響應(yīng)、文件流讀取、Bitmap操作——這些場景如果反復(fù)分配,LOH會像漏氣的輪胎,看起來滿了,實際全是空洞。
某電商平臺的訂單導(dǎo)出接口曾因此暴雷:每次生成Excel都new一個MemoryStream,LOH碎片化導(dǎo)致Full GC(全代回收)每30秒觸發(fā)一次,P99延遲從50ms崩到800ms。修復(fù)方案簡單到尷尬——池化MemoryStream,復(fù)用而非重建。
和GC談判的技巧
不是讓你手寫內(nèi)存管理,而是減少無意義的分配壓力。ArrayPool租還數(shù)組代替new,StringBuilder池化重用,Span和Memory在棧上切片——這些API設(shè)計初衷就是讓你"少麻煩GC"。
監(jiān)控層面,dotnet-counters看GC Heap Size和Gen 2 GC Count,dotnet-trace抓分配熱點。指標(biāo)不會撒謊:如果Gen 2回收頻率超過每分鐘一次,你的代碼大概率在批量生產(chǎn)"長壽垃圾"。
那個200ms的延遲尖刺,現(xiàn)在你知道該查什么了——但你的監(jiān)控告警規(guī)則里,有GC相關(guān)的閾值嗎?
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.