.net 服務因為GC時遇到的問題和解決辦法
1.問題:
.net單一服務中,大量的請求訪問後臺服務,多線程處理請求,但每個線程都可能出現超時的現象。記錄超時日誌顯示,超時可能在序列化時,Socket異步發送AsyncSend數據時,普通業務處理時超時,
插入數據庫時超時,而且超時時間都比較固定,內存大時可達到5s,內存小時2~3s。
2.分析問題:
超時問題,針對具體的超時現象,具體分析:
(1)插入數據庫時超時,sqlserver 表被鎖住,所以插入不了;
(2)序列化時超時,對象序列化成字節數組時,由於對象太大,會出現耗時的情況,因此線程被卡住,超時。
(3)Socket異步發送超時,Socket應答客戶端消息,由於beginSend是異步方法,服務把字節流寫入發送緩沖區,如果發送緩沖區滿了,發送緩沖區沒有及時發送數據,
導致字節流寫不進去發送緩沖區,導致線程被掛起。
(4)處理業務時超時,業務邏輯有問題,導致超時
經過分析,以上這四種超時沒辦法定位到問題,測試後發現均不是以上所說的問題。
考慮到,每次請求超時,幾乎所有線程都會掛起暫停,應該是有其他原因導致服務中的線程暫停了。
首先考慮原因,GC垃圾回收時,服務內存太大,垃圾自動回收,2代托管堆清理時,工作GC會把服務中其他線程都掛起。
3. .net GC回收機制
3.1 GC是如何工作的
GC的工作流程主要分為如下幾個步驟:
標記(Mark) → 計劃(Plan) → 清理(Sweep) → 引用更新(Relocate) → 壓縮(Compact)
3.2 GC的根節點
本文反復出現的GC的根節點也即GC Root是個什麽東西呢?
每個應用程序都包含一組根(root)。每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要麽引用托管堆中的一個對象,要麽為null。
在應用程序中,只要某對象變得不可達,也就是沒有根(root)引用該對象,這個對象就會成為垃圾回收器的目標。
用一句簡潔的英文描述就是:GC roots are not objects in themselves but are instead references to objects.而且,Any object referenced by a GC root will automatically survive the next garbage collection.
.NET中可以當作GC Root的對象有如下幾種:
1、全局變量
2、靜態變量
3、棧上的所有局部變量(JIT)
4、棧上傳入的參數變量
5、寄存器中的變量
註意,只有引用類型的變量才被認為是根,值類型的變量永遠不被認為是根。因為值類型存儲在堆棧中,而引用類型存儲在托管堆上。
3.3 什麽時候發生GC
1、當應用程序分配新的對象,GC的代的預算大小已經達到閾值,比如GC的第0代已滿;
2、代碼主動顯式調用System.GC.Collect();
3、其他特殊情況,比如,windows報告內存不足、CLR卸載AppDomain、CLR關閉,甚至某些極端情況下系統參數設置改變也可能導致GC回收。
3.4 GC中的代
代(Generation)引入的原因主要是為了提高性能(Performance),以避免收集整個堆(Heap)。一個基於代的垃圾回收器做出了如下幾點假設:
1、對象越新,生存期越短;
2、對象越老,生存期越長;
3、回收堆的一部分,速度快於回收整個堆。
.NET的垃圾收集器將對象分為三代(Generation0,Generation1,Generation2)。不同的代裏面的內容如下:
1、G0 小對象(Size<85000Byte):新分配的小於85000字節的對象。
2、G1:在GC中幸存下來的G0對象
3、G2:大對象(Size>=85000Byte);在GC中幸存下來的G1對象
object o = new Byte[85000]; //large object Console.WriteLine(GC.GetGeneration(o)); //output is 2,not 0
3.5、當GC遇到多線程
前面討論的垃圾回收算法有一個很大的前提就是:只在一個線程運行。而在現實開發中,經常會出現多個線程同時訪問托管堆的情況,或至少會有多個線程同時操作堆中的對象。一個線程引發垃圾回收時,其它線程絕對不能訪問任何線程,因為垃圾回收器可能移動這些對象,更改它們的內存位置。CLR想要進行垃圾回收時,會立即掛起執行托管代碼中的所有線程,正在執行非托管代碼的線程不會掛起。然後,CLR檢查每個線程的指令指針,判斷線程指向到哪裏。接著,指令指針與JIT生成的表進行比較,判斷線程正在執行什麽代碼。
如果線程的指令指針恰好在一個表中標記好的偏移位置,就說明該線程抵達了一個安全點。線程可在安全點安全地掛起,直至垃圾回收結束。如果線程指令指針不在表中標記的偏移位置,則表明該線程不在安全點,CLR也就不會開始垃圾回收。在這種情況下,CLR就會劫持該線程。也就是說,CLR會修改該線程棧,使該線程指向一個CLR內部的一個特殊函數。然後,線程恢復執行。當前的方法執行完後,他就會執行這個特殊函數,這個特殊函數會將該線程安全地掛起。然而,線程有時長時間執行當前所在方法。所以,當線程恢復執行後,大約有250毫秒的時間嘗試劫持線程。過了這個時間,CLR會再次掛起線程,並檢查該線程的指令指針。如果線程已抵達一個安全點,垃圾回收就可以開始了。但是,如果線程還沒有抵達一個安全點,CLR就檢查是否調用了另一個方法。如果是,CLR再一次修改線程棧,以便從最近執行的一個方法返回之後劫持線程。然後,CLR恢復線程,進行下一次劫持嘗試。所有線程都抵達安全點或被劫持之後,垃圾回收才能使用。垃圾回收完之後,所有線程都會恢復,應用程序繼續運行,被劫持的線程返回最初調用它們的方法。
實際應用中,CLR大多數時候都是通過劫持線程來掛起線程,而不是根據JIT生成的表來判斷線程是否到達了一個安全點。之所以如此,原因是JIT生成表需要大量內存,會增大工作集,進而嚴重影響性能。
這裏再說一個真實案例。某web應用程序中大量使用Task,後在生產環境發生莫名其妙的現象,程序時靈時不靈,根據數據庫日誌(其實還可以根據Windows事件跟蹤(ETW)、IIS日誌以及dump文件),發現了Task執行過程中有不規律的未處理的異常,分析後懷疑是CLR垃圾回收導致,當然這種情況也只有在高並發條件下才會暴露出來。
3.6、開發中的一些建議和意見
由於GC的代價很大,平時開發中註意一些良好的編程習慣有可能對GC有積極正面的影響,否則有可能產生不良效果。
1、盡量不要new很大的object,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內存壓縮整理,因為在堆中下移85000字節或更大的內存塊會浪費太多CPU時間;
2、不要頻繁的new生命周期很短object,這樣頻繁垃圾回收頻繁壓縮有可能會導致很多內存碎片,可以使用設計良好穩定運行的對象池(ObjectPool)技術來規避這種問題
3、使用更好的編程技巧,比如更好的算法、更優的數據結構、更佳的解決策略等等
update:.NET4.5.1及其以上版本已經支持壓縮大對象堆,可通過System.Runtime.GCSettings.LargeObjectHeapCompactionMode進行控制實現需要壓縮LOH。
4. GC模式和配置
服務器模式:
(1)給每個業務邏輯進程創建一個專用的線程,最高優先級
(2)主要應用於多處理器系統,並且作為ASP.NET Core宿主的默認配置。它會為每個處理器都創建一個GC Heap,並且會並行執行回收操作。
(3)該模式的GC可以最大化吞吐量和較好的收縮性。這種模式的特點是初始分配的內存較大,並且盡可能不回收內存,進行回收用時會很耗時,並進行內存碎片整理工作。
工作站模式:
(1)主要應用於單處理器系統,Workstation GC盡可能地通過減少垃圾回收過程中程序的暫停次數來提高性能。
(2)低負載且不常在後臺(如服務)執行任務的應用程序,可以在禁用並發垃圾回收的情況下使用工作站垃圾回收。特點是會頻繁回收,來阻止一次較長時間的回收。
GC Model | 32-bit | 64-bit |
---|---|---|
Workstation GC | 16 MB | 256 MB |
Server GC | 64 MB | 4 GB |
Server GC with > 4 logical CPUs | 32 MB | 2 GB |
Server GC with > 8 logical CPUs | 16 MB | 1 GB |
此外,CLR還會為每個處理器分配一個單獨的堆。每個處理器堆裏,包含一個小對象堆和大對象堆。從你的應用程序角度上看,你的代碼不知道引用的對象是屬於哪個堆上面的(他們都有相同的虛擬地址空間)。
使用多個堆有下一些優點
- 垃圾回收可以並行處理。每個GC線程處理一個對應的堆。這是的服務器模式的GC比工作站模式要快的原因。
- 某些情況下,分配速度會更快,尤其是將大對象相對分配在同一個堆上快。還有一些其他內部差異,比如內存段的大小,越大的段在做垃圾回收時時間也會越長。
你可以在App.config 文件裏的節點裏配置 服務器模式
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
你要如何選擇工作站或者服務器模式嗎?
後臺GC
(1)修改後臺GC配置會更改2代對象的回收策略。相對於0代和1代的回收的前臺GC,它不會中斷當前應用裏其他的線程執行。
(2)後臺GC在會而外創建一個線程用來處理2代對象的回收。這意味著,如果你同時開啟後臺GC和服務器GC,你將為每個處理器創建2個線程來處理GC。但這沒啥大不了的,雖然進程裏多了很多個線程,但這些線程在大部分時間裏還是不工作的。
在你的應用執行的時候GC也可以同時進行,但在某些情況下,還是會發生阻塞。在這時,後臺GC還是會將應用程序裏的其它線程給掛起。
如果使用工作站模式,則始終開啟後臺GC模式,從.NET4.5開始,默認情況下服務器GC模式下也會開啟,當然你也可以關閉它。
以下是關閉後臺GC的配置
<configuration>
<runtime>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>
實際上,我們很少有理由去禁用後臺GC。如果你想通過禁用後臺GC的線程來提高你的應用程序在CPU的占用率,但這個想法是不現實的。但如果是減少GC的延遲或者頻率可以考慮關閉它。
低延遲模式
如果你的應用希望在一段特定時間裏高速執行,不希望被GC的2代回收打擾。你可以通過改變 GCSettings.LatencyMode 的設置來實現。
LowLatency—只能在工作站模式運行,它可以暫停2代回收。
SustainedLowLatency—可以在工作站和服務器模式下執行。它可以暫停完整的2代回收,但如果你開啟裏後臺GC模式,你還是可以在後臺GC線程裏對2代對象做回收。
這兩種模式都將大大的增加內存的消耗,因為它沒對內存做壓縮。如果你的應用需要消耗大量的內存,則最好避免開啟這兩個模式。
當你要準備進入低延遲模式前,最好手動執行一次完整的GC(GC.Collect(2, GCCollectionMode.Forced)。等離開低延遲模式後,也手動觸發一次完成GC。
默認情況下,是不需要開啟這個模式。只有你的程序執行時間不要被GC打擾才需要開啟,不用在全過程都開啟。舉個栗子:如果你有一個股票交易的高頻應用,在交易時間段裏不希望發生GC回收暫停應用執行。但在股市交易結束後,你可以關閉這個模式進行完整的GC回收直到股市重新開市。
如果要開啟低延遲模式,至少要符合以下標準:
- 在正常執行期間,完整的垃圾回收操作是不可接受的
- 應用程序消耗的內存要遠小於可分配內存
- 應用程序在開啟低延遲模式後,要有足夠的內存撐到下一次手動執行完整回收或者重啟。
這是一個很少用的配置,如果你要使用請三思而後行,因為開啟之後會出現一些意想不到的後果。如果你認為還是有必要使用,請確保你的應用經過了充分測試。在開啟後,系統會產生更頻繁的0代和1代的回收操作,用來減少完整的回收,這可能會導致一些其他性能問題。這可能會導致解決了一個又另外產生了一個問題。
最後,請註意,低延遲模式不是一個保證。如果GC在做回收的時候仍然拋出了OutOfMemoryException異常,仍然有可能會不管你的配置選項,進行一次完整的GC回收。
5. .net memory profiler 工具跟蹤
具體就不介紹了,主要是監控內存的變化和泄露,對監控GC不好
6. perfmon 性能計數器
開啟性能監控器
監控指標有如下
"(PDH-CSV 4.0) 時間
(","\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\# Bytes in all Heaps", 托管堆的大小 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\# Gen 0 Collections", 0代回收數量 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\# Gen 1 Collections", 1代回收數量 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\# Gen 2 Collections", 2代回收數量 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\Promoted Memory from Gen 0", 0代--1代保留的字節大小 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\Promoted Memory from Gen 1" 1代--2代保留的字節大小 .net CLR Memory
"\\FLYBUS02\.NET CLR Memory(Wind.IBroker.Server)\Private Bytes" 服務內存大小 Process
7 解決辦法
修改GC的配置模式:
目前,由於服務器為多核16G內存的服務器,我們服務大概需要4G的內存,所有可以配置
1.服務器模式+後臺GC模式(默認是conCurrent 是 true,不必配置)
在App.Config中配置
<configuration>
<runtime>
<gcConcurrent enabled="true"/>
</runtime>
</configuration>
2. 晚上請求少,創建對象不多,可以定時調用GC.Collection()對垃圾進行回收。
3.修改業務代碼邏輯,減少創建對象。
8. 引用文章
https://www.cnblogs.com/huchaoheng/p/6295688.html
https://blog.csdn.net/sD7O95O/article/details/78549892
https://www.cnblogs.com/yahle/p/6915751.html
.net 服務因為GC時遇到的問題和解決辦法