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