1. 程式人生 > >.net 服務因為GC時遇到的問題和解決辦法

.net 服務因為GC時遇到的問題和解決辦法

on() .config attr 監控 ctp -c 編程習慣 應用程序 app

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 Model32-bit64-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還會為每個處理器分配一個單獨的堆。每個處理器堆裏,包含一個小對象堆和大對象堆。從你的應用程序角度上看,你的代碼不知道引用的對象是屬於哪個堆上面的(他們都有相同的虛擬地址空間)。

使用多個堆有下一些優點

  1. 垃圾回收可以並行處理。每個GC線程處理一個對應的堆。這是的服務器模式的GC比工作站模式要快的原因。
  2. 某些情況下,分配速度會更快,尤其是將大對象相對分配在同一個堆上快。還有一些其他內部差異,比如內存段的大小,越大的段在做垃圾回收時時間也會越長。

你可以在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回收直到股市重新開市。

如果要開啟低延遲模式,至少要符合以下標準:

  1. 在正常執行期間,完整的垃圾回收操作是不可接受的
  2. 應用程序消耗的內存要遠小於可分配內存
  3. 應用程序在開啟低延遲模式後,要有足夠的內存撐到下一次手動執行完整回收或者重啟。

這是一個很少用的配置,如果你要使用請三思而後行,因為開啟之後會出現一些意想不到的後果。如果你認為還是有必要使用,請確保你的應用經過了充分測試。在開啟後,系統會產生更頻繁的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時遇到的問題和解決辦法