JVM实战—OOM的生产案例
1.每秒仅上百请求的系统为何会OOM(RPC超时时间设置过长导致QPS翻几倍)
(1)案例背景
在这个案例中,一个每秒仅仅只有100+请求的系统却因频繁OOM而崩溃。这个OOM问题会涉及:Tomcat底层工作原理、Tomcat内核参数的设置、服务请求超时时间。
(2)系统发生OOM的生产现场
某一天突然收到线上系统的一个报警,说系统发生了异常。一看异常信息让人大惊失色,居然是OOM问题。这个问题不得了,因为一旦JVM发生OOM,将会导致系统直接崩溃。OOM代表的是JVM本身已经没办法继续运行了。因此马上登录到系统的线上机器去查看对应的日志。
需要注意:一旦收到系统OOM报警,或者有人突然反馈说线上系统崩溃了。第一件事情一定是登录到线上机器去看日志,而不是做别的事情。当时在机器的日志文件中看到类似下面的一句话:
Exception in thread "http-nio-8080-exec-1089" java.lang.OutOfMemoryError: Java heap space
根据Tomcat的底层工作原理可知:这里的http-nio-8080-exec-1089指的是Tomcat的工作线程,而java.lang.OutOfMemoryError: Java heap space指的是堆内存溢出。
所以连起来看,这段日志的意思就是:Tomcat的工作线程在处理请求时需要在堆内存里分配对象。但发现堆内存满了,且没办法回收任何一点多余的对象,所以OOM。
(3)Tomcat的基本工作原理
一.项目部署的发展
很久以前我们写的系统是部署在Tomcat中的,我们会在IDEA等开发工具里写一堆的Servlet。然后打包放入Tomcat,之后再启动Tomcat。接着我们会访问Tomcat监听的一个端口号比如8080,然后系统的功能就可以运行起来了。
后来随着技术发展,不再写Servlet这么原始的东西了,有一些类似Spring MVC的框架把Servlet封装起来,于是就基于Spring MVC框架去开发。
再后来技术越来越先进,出现了Spring Boot,可以把Tomcat之类的Web容器都内嵌在系统里,再后来甚至是基于Spring Cloud去开发分布式的系统。
二.项目部署在Tomcat的处理请求流程
如下图示,这是一个最基本的Web系统部署在Tomcat中运行的架构图:
上面这个图表示:基于Spring Cloud、Spring Boot、Spring MVC等技术,写好一套系统。然后打包后放入线上服务器中部署的Tomcat目录下,接着启动Tomcat。Tomcat会监听一个默认8080的端口号,之后我们就可以通过浏览器对这个机器上的Tomcat发起HTTP请求了。
接着Tomcat会监听8080端口收到请求,通常Tomcat会把请求交给Spring Web MVC等框架去处理。这类框架一般都会在底层封装Servlet/Filter之类的组件去处理请求,如下图示:
然后类似Spring MVC框架的Servlet组件,就会根据请求的路径去找到处理这个请求的Controller组件。
三.Tomcat究竟是什么
如果我们把写好的系统放入Tomcat目录中,然后启动Tomcat。那么启动的Tomcat本身就是一个JVM进程,因为Tomcat是由Java写的。
如下图示,Tomcat自己就是一个JVM进程,我们写好的系统只不过是一些类而已。这些类被Tomcat加载到内存里,然后由Tomcat来执行。
四.Tomcat如何监听8080端口上收到的请求
很简单,因为Tomcat有自己的工作线程。Tomcat有很多自己的工作线程,少则一两百个,多则三四百个。然后从8080端口上收到的请求会被均匀分配给这些工作线程去处理。这些工作线程收到请求后,就负责调用Spring MVC框架的代码。Spring MVC框架又负责调用我们写好的代码,比如一些Controller类,所以最终的运行流程如下图示:
(4)异常日志
接着查看当时线上系统的日志,发现如下异常:
Exception in thread "http-nio-8080-exec-1089" java.lang.OutOfMemoryError: Java heap space
http-nio-8080-exec-1089其实就是上图中的Tomcat工作线程,它会负责调用Spring MVC及Controller、Service、DAO等代码。所以它发现运行时堆内存不够,就会抛出堆内存溢出的异常。
(5)不要忘了一个关键的JVM参数
一旦发现线上系统发生了OOM内存溢出异常:
第一步是查看日志确定OOM类型和线程
首先需要确定具体的溢出类型,看看到底是堆内存溢出、还是栈内存溢出、或者是Metaspace内存溢出。然后查找哪个线程运行代码时内存溢出,因为Tomcat运行时不仅有自己的工作线程,也有我们创建出的一些线程。
注意:在部署系统上线的时候务必要设置HeapDumpOnOutOfMemoryError参数,这个参数会在系统OOM内存溢出时导出一份内存快照到指定的位置。
第二步是排查和定位OOM内存溢出问题
主要就是根据系统OOM内存溢出时自动导出的内存快照,通过MAT进行分析。
(6)对内存快照进行分析
MAT分析内存快照的技巧:通常在内存溢出时分析内存快照也并没那么复杂,主要就是通过MAT来找到占据内存最大的对象。
一.首先发现占据内存最大的是大量的byte[]数组
大量的byte[]数组就占据了大约8G左右的内存空间,而当时线上机器给Tomcat的JVM堆内存分配的也就8G的内存而已。因此可以直接得出第一个结论:Tomcat工作线程在处理请求时会创建大量byte[]数组,大概有8G。大量的byte[]数组直接把JVM堆内存占满了,这就导致继续处理新请求时,没法继续在堆中分配新对象,从而OOM。如下图示:
二.接下来需要分析,到底是哪些byte[]数组
继续通过MAT深入查看,发现大概是类似下面的一大堆byte[]数组:
byte[10008192] @ 0x7aa800000 GET /order/v2 HTTP/1.0-forward...
byte[10008192] @ 0x7aa800000 GET /order/v2 HTTP/1.0-forward...
byte[10008192] @ 0x7aa800000 GET /order/v2 HTTP/1.0-forward...
byte[10008192] @ 0x7aa800000 GET /order/v2 HTTP/1.0-forward...
上面只是写出来几个示例而已,其实当时看到了很多类似这样的数组,而且数组大小都是一致的10M。清点了一下,类似上面的数组大概就有800个,也就对应了8G的空间。
三.接着进一步思考,这种数组应该是谁创建的
可以在MAT上可以继续查看一下这个数组是被谁引用的,发现是由Tomcat的类引用的,具体来说是类似下面的这个类:
org.apache.tomcat.util.threads.TaskThread
这个类一看就是Tomcat自己的线程类,因此可以认为是Tomcat的线程创建了大量的byte[]数组,然后占据了8G内存。
此时我们发现Tomcat的工作线程大致有400个左右,即每个Tomcat工作线程会创建2个byte[]数组,每个byte[]数组是10M。也就是400个Tomcat工作线程在同时处理请求,创建了8G的byte[]数组,从而导致内存溢出。如下图示:
(7)系统每秒QPS才100
此时结合上图可知:一秒内来了400个请求,导致Tomcat的400个工作线程全部处理请求。每个工作线程在处理一个请求时,会创建2个数组,每个数组是10M,结果导致瞬间让8G的内存空间被占满了。
看起来根据MAT信息,分析出来的生产系统的情况就是如此。但是检查了一下系统的监控,发现每秒请求并不是400,而是100。
问题一:为何Tomcat的400个线程都处于工作状态
明明每秒才100个请求,为什么Tomcat的400个线程都处于工作状态?出现这种情况只有一种可能,那就是每个请求处理需要耗费4秒时间。如果每秒来100个请求,但是每个请求处理完毕需要4秒的时间。那么这会导致,在4秒内的某个瞬间就会有400个请求需要同时处理。从而也就会导致Tomcat的400个工作线程都在工作,出现上述情况。
问题二:为何Tomcat工作线程在处理一个请求时会创建2个10M的数组
这里可以到Tomcat的配置文件里搜索一下,发现了如下的一个配置:
max-http-header-size: 10000000
这个配置会导致Tomcat工作线程在处理请求时创建2个数组,每个数组的大小就是上面配置的10M。
因此可以总结一下系统运行时的场景了:每秒100个请求,每个请求处理需要4秒。导致4秒内的某个瞬间,400个请求会同时被400个线程处理。每个线程根据配置创建2个数组,每个数组10M,于是占满了8G的内存。如下图示:
(8)为什么处理一个请求需要4秒
这个问题绝对是偶发性的,不是每次处理请求都如此。因为这个系统的负责人说,平时处理一个请求也就几百毫秒而已。既然如此,那么唯一办法只能在日志里去找了,查看事故发生时的日志。发现日志中除了OOM外,还有大量的服务请求调用超时的异常,如下:
Timeout Exception...
也就是说,这个系统在通过RPC调用其他系统时出现大量的请求超时。马上翻看一下系统的RPC调用超时配置,惊讶地发现:这个系统的负责人将服务RPC调用超时时间设置了刚好是4秒。
也就是说,在这个时间里远程服务自己故障了,然后导致我们的系统RPC调用时访问不通。于是就会在配置好的4秒超时时间后抛出异常,在这4秒内,工作线程会直接卡死在无效的网络访问上。如下图示:
上图已经表述很清楚了:之所以每个请求要处理4秒,是因为下游服务故障了,网络请求失败了。此时会按设置好的4秒超时时间一直卡住4秒后才会抛出Timeout异常。然后请求才会处理结束,这就是一个请求处理需要4秒的根本原因。处理一个请求4秒会导致4秒内积压400请求同时在处理(每秒100请求),最后导致400个工作线程创建800个10M的数组,耗尽8G内存而OOM。
(9)对系统进行优化
上述问题原因就是超时时间设置得太长了,因此将超时时间改为1秒。这样每秒100个请求,只有200个数组占据2G内存,不会占满堆内存。然后1秒内这100个请求会全部超时,请求就处理结束。下一秒再来100个请求又是新一轮处理,不会出现每秒积压100个请求,4秒内出现400个请求同时处理的情况。
另外,对Tomcat的参数,max-http-header-size可以适当调节小一些。这样Tomcat工作线程自身为请求创建的数组,不会占据太大的内存空间。
2.Jetty服务器的NIO机制如何导致堆外内存溢出(S区太小 + 禁NIO的显式GC)
(1)案例背景
使用Jetty作为Web服务器时在某个罕见场景下发生的一次堆外内存溢出,这种场景其实并不多见,但可以学习这种场景的排查方法。
(2)案例发生现场
有一天突然收到线上的一个报警:某台机器部署的一个服务突然间就不可以访问了。此时第一反应当然是马上登录机器去看一下日志,因为服务挂掉,很可能是OOM导致的,当然也可能是其他原因导致的。这个时候在机器的日志中发现了如下的一些核心信息:
nio handle failed java.lang.OutOfMemoryError: Direct buffer memory
at org.eclipse.jetty.io.nio.xxxx
at org.eclipse.jetty.io.nio.xxxx
at org.eclipse.jetty.io.nio.xxxx
上述日志告诉我们有OOM异常,但是是哪个区域导致的呢?居然是Direct buffer memory区域,且后面还有Jetty相关的方法调用栈。到此为止,根据这些日志基本就可以分析出这次OOM发生原因了。
(3)初步分析事故发生的原因
Direct buffer memory其实就是堆外内存,堆外内存是JVM堆内存外的一块内存空间,这块内存不是JVM管理的。Java代码在JVM堆之外使用的内存空间,就叫Direct buffer memory。
如果按英文字面理解,它的意思就是直接内存。所以从字面就可以看出,这块内存并不是JVM管理的,而是OS管理的。
Jetty可以理解为是一个和Tomcat一样的Web容器,它也是Java写的。写好的一个系统,可以打包后放入Jetty,然后启动Jetty即可。Jetty启动后本身就是一个JVM进程,它会监听一个端口号,比如9090。接着就可以向Jetty监听的9090端口发送请求,Jetty会把请求转交给我们用的Spring MVC之类的框架,而Spring MVC之类的框架再去调用我们写好的Controller之类的代码。
Jetty作为一个JVM进程运行我们写好的系统的流程,如下图示:
首先可以明确的是:这次OOM是Jetty这个Web服务器在使用堆外内存时导致的。所以基本可以推测出,Jetty服务器可能在不停地使用堆外内存。然后堆外内存空间不足,没法使用更多的堆外内存,此时就抛出OOM。如下图示:
(4)解决OOM问题的技术储备建议
Jetty是如何去JVM堆外申请一块堆外内存来使用的?然后这个堆外内存又是如何释放的?
这就涉及到Java的NIO底层技术细节,如果对NIO不了解,突然看到这个异常,估计是没法继续分析下去了。因此JVM性能优化相对还是较为容易,而且按照整个套路没太大难度。但如果要解决OOM问题,那么除了一些特别简单的模拟内存溢出,其他很多时候生产环境下的OOM问题,都是有技术难度的。
1000个工程师可能会遇到1000种不同的OOM问题,可能排查的思路是类似的,或者解决问题的思路是类似的。但是如果要解决各种OOM问题,那么需要对各种技术都有一定的了解。换句话说,需要有较为扎实的技术内功修为。
比如上面那个案例,就需要了解Tomcat的工作原理,才能分析清楚。同样这里的案例,也要求了解Java NIO技术的工作原理才能分析清楚。
所以需要对其他的核心主流技术做一些深层次的甚至源码级别的研究,这些底层技术积累会在线上系统出现问题时,帮助我们分析和解决问题。
(5)堆外内存是如何申请的和如何释放的
一.堆外内存是如何分配的
如果在Java代码申请使用一块堆外内存空间,要使用DirectByteBuffer。可通过这个类构建DirectByteBuffer对象,这个对象会在JVM堆内存里。构建该对象同时,会在堆外内存中划出一块内存空间与该对象关联起来。这就是分配堆外内存时的大致思路,如下图示:
二.堆外内存是如何释放的
当DirectByteBuffer对象没被引用成了垃圾对象后,自然会在某一次YGC或者FGC时把该DirectByteBuffer对象回收掉。只要回收一个DirectByteBuffer对象,就会释放掉其关联的堆外内存,如下图示:
(6)为什么会出现堆外内存溢出的情况
一般什么情况下会出现堆外内存的溢出?如果创建了很多DirectByteBuffer对象,那么就会占用大量的堆外内存。如果DirectByteBuffer对象没被GC线程回收,则堆外内存就不会释放。
久而久之,当堆外内存已经被大量的DirectByteBuffer对象关联使用后。如果要再创建DirectByteBuffer对象使用更多堆外内存,那么就会OOM。
什么情况会出现大量DirectByteBuffer对象存活,使堆外内存无法释放?有一种可能就是系统正在承载超高并发,负载压力很高。瞬时大量请求过来,创建过多的DirectByteBuffer占用了大量堆外内存。此时再继续想要使用堆外内存,就会内存溢出。但是这个系统是这种情况吗?明显不是。因为这个系统的负载其实没有那么高,也没有大量的瞬时请求过来。
(7)真正的堆外内存溢出原因分析
这时就得思路活跃起来了,我们完全可以去用jstat等工具观察一下线上系统的实际运行情况,同时根据日志看看一些请求的处理耗时,综合性地进行分析。当时我们通过jstat工具分析JVM运行情况,同时分析过往的GC日志。另外还看了系统各个接口的调用耗时后,分析出了如下结论。
一.首先看了接口的调用耗时
这个系统并发量不高,但是它每个请求处理较为耗时,平均每个请求需要一秒多的时间去处理。
二.然后通过jstat发现
随着系统不停地运行,会一直创建各种对象,包括Jetty本身会不停地创建DirectByteBuffer对象,直到新生代的Eden区满了,就会触发YGC。
但往往在进行YGC垃圾回收的一瞬间,可能有的请求还没处理完毕。此时就会有不少DirectByteBuffer对象处于存活状态,不能被回收掉。当然之前不少DirectByteBuffer对象对应的请求可能处理完毕了,它们就可以被回收。所以肯定会有一些DirectByteBuffer对象是处于存活状态的,那么这些存活状态下的对象就需要转入Survivor区域中。
但这个系统当时在上线时,内存分配不合理。年轻代只分配一两百M的空间,老年代反而给七八百M的空间,进而导致新生代中的Survivor区域只有10M左右的空间。而在YGC过后,一些存活下来的对象(包括DirectByteBuffer)会超过10M,于是这些存活的对象就没法放入Survivor中,只能直接进入老年代。
上述过程反复执行,慢慢就会导致一些DirectByteBuffer对象进老年代。老年代的DirectByteBuffer对象越来越多,而且都关联很多堆外内存。这些老年代里的DirectByteBuffer其实很多都是可以回收的了。但是因为老年代一直没塞满,所以没有触发FGC,自然也就不会回收老年代里的这些DirectByteBuffer对象。而老年代里这些没有被回收的DirectByteBuffer对象,却又一直与占据了大量的堆外内存空间关联着。
直到最后,当系统要继续使用堆外内存时:发现所有堆外内存都被老年代里大量的DirectByteBuffer对象给占用了。虽然它们可以被回收,但是无奈因为始终没有触发老年代的FGC。所以堆外内存也始终无法被回收掉,最后就导致堆外内存溢出问题。
(8)难道Java NIO就没考虑过这个问题吗
先不说如何解决这个问题,难道Java NIO就没考虑过会有上述问题吗?当然不是了,Java NIO是考虑到的。它知道可能会有很多DirectByteBuffer对象也许没被引用了,但是因为没有触发GC就导致它们一直占据着堆外内存。
所以在Java NIO的源码中会做如下处理:每次分配新的堆外内存发现不足时,都会调用System.gc(),以此提醒JVM执行一下GC去回收那些没被引用的DirectByteBuffer对象,从而释放堆外内存空间。
只要能触发垃圾回收去回收一些没被引用的DirectByteBuffer对象,那么就会释放一些堆外内存,自然就可分配更多对象到堆外内存去了。
可惜在JVM中设置了如下参数,该参数会导致System.gc()不生效,因此才导致上述情况。
-XX:+DisableExplicitGC
(9)最终对问题的优化
其实项目问题有两个:
一是内存设置不合理导致DirectByteBuffer对象进入老年代,导致堆外内存一直释放不掉。
二是设置了参数-XX:+DisableExplicitGC禁用了System.gc(),导致Java NIO没法显式触发去回收一些垃圾DIrectByteBuffer对象,导致堆外内存一直释放不掉。
因此需要做的事情就是:
一是合理分配内存,给年轻代更多内存,让Survivor区域有更大的空间。
二是放开参数-XX:+DisableExplicitGC这个限制,让System.gc()生效。
做完优化后,DirectByteBuffer对象一般就不会不断进入老年代了。只要它停留在年轻代,YGC后就会被正常回收,然后释放堆外内存。另外,只要放开对-XX:+DisableExplicitGC的限制。那么Java NIO发现堆外内存不足,就能通过System.gc()显式进行FGC。回收掉进入了老年代的一些DirectByteBuffer对象来释放一些堆外内存。
3.一次微服务架构下的RPC调用引发的OOM故障排查实践(MAT案例)
(1)解决系统OOM的核心能力
解决系统OOM故障的核心能力:必须对自己的线上系统,所使用的各种技术源码最好都要有深入理解,从服务框架到第三方框架、到Tomcat/Jetty等Web服务器、再到各种底层中间件系统。
因为一般线上系统OOM都不是简单地由我们写的代码导致的,往往都是因为系统使用了某个开源技术,该技术的内核代码有一定的故障和缺陷而导致的。这时要解决OOM问题,就必须深入到源码中去分析。
所以这里只能介绍各种真实的生产案例以及分析思路,真正到了具体的OOM故障时,由于每个系统的故障都是不同的。需要我们分析对应的技术底层,然后找到故障原因,最后解决故障。
(2)系统架构介绍
这个系统是比较早的一个系统,在进行服务间的RPC通信时,采用的是基于Thrift框架封装的RPC框架。然后公司的系统,就是基于这个RPC框架去进行通信。下图是一个最基本的服务间RPC调用的示例:
(3)故障发生现场
平时都是服务A通过RPC框架去调用服务B,但有一天服务A更新代码上线后发现没什么问题,但服务B却宕机了。明明修改代码和重新部署的是服务A,为什么造成服务B挂掉呢?如下图示:
登录到服务B的机器去查看服务B的日志,结果在里面发现了OOM异常,直接显示出堆内存溢出了;
java.lang.OutOfMemoryError Java heap space
可服务B为什么会OOM呢?难道是服务B的问题?那不如重启服务B?于是我们尝试重启了服务B。结果发现服务B重启过后很快又宕机了,而且原因还是OOM。这就奇怪了,因为服务A修改代码部署前,服务B从来没出现过这种情况。现在却是服务A修改了代码部署后才导致服务B出现了这种情况。
(4)初步查找内存溢出的故障发生点
一般内存溢出时,务必要先确认一下故障的发生点。这个通过查看日志就可以确认,因为在日志中都会有详细的异常栈信息。通过查看日志发现,引发OOM异常的,居然就是那个自研的RPC框架,当时的异常信息大概如下所示:
java.lang.OutOfMemoryError: Java heap space
xx.xx.xx.rpc.xx.XXXClass.read()
xx.xx.xx.rpc.xx.XXXClass.xxMethod()
xx.xx.xx.rpc.xx.XXXClass.xxMethod()
由此可以初步确定,是自研的RPC框架在接收请求时引发了OOM,如下图示:
(5)分析内存快照找到占用内存最大的对象
既然已经定位到OOM故障发生的位置了,也就是谁引发的OOM。接下来用MAT分析一下发生OOM时,对内存占用最大的是哪个对象了。
打开OOM时的内存快照,发现占用内存最大的是一个超大的byte[]数组。当时一台机器给的堆内存也不过就是4G而已,而在内存快照中发现一个超大的byte[]数组,就已占据了差不多4G空间。
这个byte[]数组是哪儿来的?通过分析该byte[]数组的引用,会发现就是由RPC框架内部的类引用的。MAT里右击一个对象会出现一个菜单,里面有个选项就可以查看引用者。如下图示:
(6)通过分析源代码找出原因
一.整个分析流程步骤
第一步先定位到是谁导致的OOM
这时往往可能是由于某种技术,比如Tomcat、Jetty、或者是RPC框架。
第二步一般使用MAT等工具分析内存快照
找到当时占用内存最大的对象是谁,可以找找都是谁在引用这个对象,当然一般在第一步通过看日志的异常栈就大概知道导致OOM的类是谁。
第三步需要得对那个技术的源代码进行分析
比如对Tomcat、RPC框架源代码进行追踪分析。
一般分析到这一步,答案几乎快要揭晓了,通过代码分析找到故障发生原因。如果可能的话,最好在本地搭建类似的环境,把线上问题给复现出来,接下来就结合日志里的异常栈分析这个RPC框架的源代码。
二.这个RPC框架在接收请求时的流程
首先在服务A发送请求时,会对传输过来的对象进行序列化。这个序列化,就是把类似Request的对象变成一个byte[]数组。
然后对服务B而言,它首先会根据自定义的序列化协议,对发送过来的数据进行反序列化。接着把请求数据读取到一个byte[]缓存,然后调用业务代码处理请求。最后请求处理完毕,清理byte[]缓存。
以上便是这个RPC框架运行的原理了,接着需要在源码中要寻找一下:为什么用来缓冲请求的byte[]数组会达到几个G那么大?正常情况下,这个用来缓冲请求的byte[]数组应该最多不超过1M的。
(7)一个关键点——RPC框架的类定义
通过源码的分析,我们最终总算搞清楚了,原来当时有这么一个特殊的情况:因为RPC框架要进行对象传输,就必须让服务A和服务B都知道有该对象。举个例子,比如服务A要把一个Request对象传输给服务B。那么首先需要使用一种特定的语法定义一个对象文件,大概类似如下:
syntax = "proto3";
message Request {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}
然后会通过上面特殊语法写的文件可以反向生成一个对应的Java类,此时会生成一个Java语法的如下Request类:
public class Request {//这里是一堆的自动生成的代码
}
接着这个Request类需要在服务A和服务B的工程里都要引入,这样服务A和服务B就知道:把Request交给服务A,它会自己进行序列化成字节流。然后到服务B时,它会把字节流反序列化成一个Request对象。如下图示:
服务A和服务B都必须知道有Request类的存在,才能把Request对象序列化成字节流,才能从字节流反序列化出一个Request类对象。
(8)RPC框架的一个Bug——过大的默认值
在上图中,服务B在接收到请求后,会先反序列化,接着把请求读出来放入一个byte[]数组。
但是这里RPC框架有一个Bug,就是一旦发现发送过来的字节流反序列化失败,就会开辟一个byte[]数组。这个byte[]数组默认4G,会把发送方的字节流原封不动地放进去。
而出现对字节流反序列化失败的原因是服务A修改了Request类,但服务B不知这次修改,Request是旧版本。比如服务A的Request类有15个字段,序列化成字节流发送给服务B了。但服务B的Request类只有10个字段,那么反序列化的时候就会失败。
所以最终的问题就出在这里了:当时服务A修改了很多Request类的字段,结果没告诉服务B。所以服务A上线后,服务B没能反序列化传过来的序列化的Request对象。此时服务B就会开辟一个默认4G的byte[]数组存放数据,然后导致OOM。如下图示:
(9)最终的解决方案
当时为什么要把异常情况下的数组默认大小设置为几个G那么大?这个其实也没办法,因为当时写这段代码的刚好是新手工程师。考虑反序列化失败,就原封不动封装字节流到数组,由对方自行处理。由于不知道字节流里有多少数据,所以就开辟一个大数组,保证能放下。且一般测试时又不会测到这种异常情况,所以之前一直没发现这个问题。
其实解决这个问题的办法很简单,把RPC框架中那个数组的默认值从4G调整为4M即可。一般请求都不会超过4M,不需要开辟那么大的数组,另外就是让服务A和服务B的Request类定义保持一致即可。
4.一次没有WHERE条件的SQL语句引发的OOM问题排查实践(使用MAT技巧)
(1)案例背景引入
在使用Mybatis写SQL语句时,在某些情况下不加where条件也可执行,结果导致一下子查询出上百万条数据引发系统OOM。这个案例本身比较简单,不涉及太多其他的技术问题。下面通过这个案例来介绍使用MAT对线上内存快照进行分析的技巧。
(2)关于MAT工具对OOM故障的实践意义
如果系统触发OOM是由于Tomcat、Jetty、RPC框架之类的底层技术,那么MAT对这种系统来说用处并不是那么大。
因为最多就是用MAT找一找占用内存过多的对象,然后结合异常日志调用栈和MAT中的对象引用情况,初步定位是底层技术中的哪部分代码导致的内存溢出。
如果要真正解决这种内存溢出问题,还得去仔细研究Tomcat、Jetty、RPC框架之类技术的底层源码,然后结合线上系统的负载情况、访问压力、GC情况,以及底层技术的源码细节,才能分析清楚。
如果OOM是由于自己系统代码问题导致的,那么就容易解决得多了。只要根据MAT层层分析,就可以定位到代码的问题所在。毕竟自己写的代码自己是最熟悉的,所以很快就可以解决问题。
(3)故障发生现场
某天突然收到反馈说线上一个系统崩溃不可用了,此时立即登录到线上机器去查看日志。在日志中果然发现了OOM的异常,堆内存溢出了:
java.lang.OutOfMemoryError,java heap space
那么下一步就是把自动导出的内存快照拷贝到电脑上,用MAT去分析。下面介绍如何巧妙的用MAT工具迅速定位问题代码。
(4)第一步检查内存中到底什么对象太多了
这时可以用MAT中的Histogram功能去检查占用内存最多的对象有哪些。当然MAT的那个内存泄漏报告的使用,那个功能也是没问题也很好用。但这里介绍另外一套分析思路,其实原理都是类似的。
分析内存快照,就是要找到占用内存最大的对象。然后找到谁在引用这个对象,是哪个线程在引用。接着找到创建这些对象的相关代码和方法,这样就可以到对应的源码里去分析问题了。
在如下图中,红圈处那个按钮就是Histogram按钮,点击过后就会进入另外一个界面。
接着进入Histogram界面,如下图示:
在这个界面中,就可以看到是谁占用过多内存了。比如这里明显是Demo1$Data这个内部类占用了过多内存,而且这里显示Demo1$Data对象有10000个,此时我们就会想看看这些Demo1$Data对象都是什么?
(5)第二步深入查看占用内存过多的对象
为了查看这些Demo1$Data对象都会是什么?此时进入第二步,即查看占用内存过多的对象是被谁引用、哪个线程引用、里面都是什么。也就是按照下图,点击红圈处的按钮:
点击上图那个红圈处的按钮之后,就会进入一个dominator_tree的界面,dominator_tree界面会展示出当前JVM中所有的线程。如下图示:
在dominator_tree界面可以清晰看到哪些线程创建了过多的对象,比如排第一的线程是:java.lang.Thread @ 0x5c0020838 main Thread。这就表明了是一个main线程创建了过多的对象,那接下来就可以直接展开这个线程,看看它到底创建了哪些对象。如下图示:
当展开main Thread后,发现有一个java.util.ArrayList @ 0x5c00206a8,说明main线程创建了一个巨大的ArrayList。于是继续展开这个ArrayList,发现里面是一个java.lang.Object[]数组。于是继续展开,就会看到大量的Demo1$Data对象了。至此真相大白。
其实在dominator_tree界面就能很快找到是哪个线程创建了过多的对象,而且通过层层展开,可以看到这个线程创建了哪些对象太多了,这样就和之前的Histogram界面中占用内存最多的Demo1$Data对上了,而且这里可以轻易看到每个Demo1$Data对象的详细的情况。
(6)生产案例的追踪
当时对于那个线上生产案例而言,追踪到dominator_tree界面这里,发现某个Tomcat的工作线程创建了一大堆的java.lang.HashMap。那么这些java.lang.HashMap中是什么呢?
我们发现全都是各种从数据库里查出来的字段,因为只要展开那个HashMap就能看到查出来放入内存的所有数据。
所以看到这一步基本就很明确了:就是工作线程处理一个请求时,发起了一个SQL查询,查出大量的数据。每条数据是一个HashMap,就是这大量的数据导致了系统的OOM。
(7)第三步查找哪行代码创建了这么多对象
找到占用内存最大的对象后,最后一步就是要定位是哪一行代码或者是哪个方法创建了那么多对象。这时又需要另外一个视图了,如下图的红圈处所示:
点击上图红圈的按钮会进入一个thread_overview界面,下图展示出JVM中所有的线程以及每个线程当时的方法调用栈,以及每个方法创建了哪些对象。
从上图可直接看到一个main Thread,它先执行了一个Demo1.java类中的第12行处的一个Demo1.main()方法,接着main方法又执行了一个java.lang.Thread类的sleep()方法。任何一个线程在此时此刻执行和调用了哪些方法,都会在这里显示出来。
我们接着展开上图中的Demo1.main()方法,就可以看到线程调用每个方法的时候创建和引用了哪些对象。如下图示:
在上图中,我们发现Demo1.main方法执行的时候创建了一个ArrayList,展开发现是一个java.lang.Object[]数组,再次展开发现就是一大堆的Demo1$Data对象。到此真相大白,一清二楚。
通过上述步骤,我们就可以快速的定位出来占用内存过多的对象,以及是哪个线程创建了这些对象,线程执行哪个方法时创建了这些对象,每个对象的细节我们都可以看得清清楚楚。
(8)继续对生产案例进行追踪
采用上述方法,对生产案例进行追踪,定位到系统中的一个业务方法。该方法在执行查询操作时,因没带WHERE条件,查询出上百万数据,最后导致内存溢出。因此对那个方法对应的SQL语句进行修改即可。
MAT使用技巧总结:
一.首先通过Histogram界面找占用大的对象
二.然后进入dominator_tree界面找对应的线程
三.接着进入thread_overview界面找线程对应的方法调用栈
5.每天10亿数据的日志分析系统的OOM问题排查(堆内存太小 + 递归要谨慎)
(1)案例背景
这个案例背景是一个每天10亿数据量的日志清洗系统,这个系统做的事情非常简单,主要是从Kafka中不停消费各种日志数据,然后对日志的格式进行清洗。比如对一些涉及到用户敏感信息的字段(姓名、手机号)进行脱敏处理,然后把清洗后的数据交付给其他的系统去使用,比如推荐系统、广告系统、分析系统都会去使用这些清洗好的数据。如下图示:
(2)事故发生现场
某天突然收到线上的报警,发现日志清洗系统发生了OOM异常,登陆到线上机器查看日志后,发现还是那么经典的堆内存溢出问题。
java.lang.OutOfMemoryError: java heap space
于是便来分析一下问题到底出在哪里了。首先看异常日志,定位一下到底是谁导致的这个问题,当时我们在日志里大致看到了类似如下的一些信息:
java.lang.OutOfMemoryError: java heap space
xx.xx.xx.log.clean.XXClass.process()
xx.xx.xx.log.clean.XXClass.xx()
xx.xx.xx.log.clean.XXClass.xx()
xx.xx.xx.log.clean.XXClass.process()
xx.xx.xx.log.clean.XXClass.xx()
xx.xx.xx.log.clean.XXClass.xx()
xx.xx.xx.log.clean.XXClass.process()
xx.xx.xx.log.clean.XXClass.xx()
xx.xx.xx.log.clean.XXClass.xx()
从日志中可以很明显发现:同样的一个方法(XXClass.process())反复出现了多次,最终导致OOM。
这时通过日志,有经验的可能已经可以发现一个问题了。那就是在某一处代码出现了大量的递归操作,正是大量的递归操作,反复调用一个方法后,导致了堆内存溢出问题。
初步定位是XXClass.process()方法出现了递归操作导致OOM,接下来用MAT分析一下内存快照。
(3)初步分析内存快照
接着开始分析生产现场的内存快照,在分析MAT时发现了一个问题:因为有大量的XXClass.process()方法递归执行,每个XXClass.process()方法中都创建了大量的char数组,最后因为XXClass.process()方法又多次递归调用,也就导致了大量的char[]数组耗尽了内存。如下图示,每次调用都创建大量char[]数组导致内存溢出。
(4)问题在JVM参数上
基本定位出问题所在了,但当时还发现了另一个比较大的问题。虽然XXClass.process()方法递归调用了多次,但实际上在MAT中发现递归调用的次数也并不多,大概就几十次递归,而且所有递归调用加起来创建的char[]数组对象总和也就最多1G而已。
如果是这样的话,其实我们应该先注意一个问题,那就是可能这次OOM的发生不一定是代码写得有多差。可能就是JVM的内存参数设置的不对,给堆内存分配的空间太小了。
(5)分析一下JVM的GC日志
为了确认堆内存到底是不是设置太小了,就得分析发生GC的具体过程。现在系统已经宕机,我们唯一可以看到JVM启动参数的,就是GC日志。从GC日志中,可以看到JVM启动时的完整参数设置的,核心的内容如下:
-Xmx1024m -Xms1024m -XX:+PrintGCDetails -XX:+PrintGC() -XX:+HeapDumpOnOutOfMemoryError -Xloggc:/opt/logs/gc.log -XX:HeapDumpPath=/opt/logs/dump
这里的参数主要是把GC日志详细记录在了一个日志文件里。另外指定了内存溢出时要导出内存快照,还有就是堆内存给1G,但是这台机器可是4核8G的。
接着我们看一下当时记录下来的gc.log日志:
[Full GC (Allocation Failure) 866M->654M(1024M)]
[Full GC (Allocation Failure) 843M->633M(1024M)]
[Full GC (Allocation Failure) 855M->621M(1024M)]
[Full GC (Allocation Failure) 878M->612M(1024M)]
上面把GC日志中大量无关的信息省略掉了,因为和我们分析关系不大。从上面的GC日志可以发现:Allocation Failure触发的FGC很多,每次FGC都只能回收一点点对象。另外日志里显示的是每秒都会执行一次FGC,这就很可怕了。
所以基本上可以明确GC的具体过程如下:
一.XXClass.process()方法递归创建大量char[]数组,导致堆内存满了。
二.然后导致连续一段时间,每秒触发一次FGC。因为内存都满了,特别是老年代几乎都满了。所以可能是每秒执行YGC前,发现老年代可用空间不足,提前触发FGC。也可能是YGC存活对象太多无法放入S区,老年代又放不下,只能FGC。
三.每次FGC只能回收少量对象,直到某一次FGC回收不到任何对象。然后新的对象无法放入堆内存,此时就会触发内存溢出的异常,如下图示:
因此堆内存肯定是偏小了,从而导致频繁的FGC。
(6)分析一下JVM运行时内存使用模型
接着我们用jstat分析一下当时JVM运行时的内存模型。当时重启了系统,每秒打印一次jstat的统计信息,就看到了下面的情况:
S0 S1 E O YGC FGC
0 100 57 69 36 0
0 100 57 69 36 0
0 100 65 69 37 0
0 100 0 99 37 0
0 100 0 87 37 1
一.刚开始都是新生代的Eden区在涨。
二.接着YGC从36到37,就发生了一次YGC。
三.然后Old区从占比69%到99%,说明YGC后存活对象太多,Survivor区放不下,直接进老年代了。
四.接着老年代都占了99%了,直接就触发了一次FGC。但这次FGC仅让老年代从占比99%降到87%而已,回收了少量的对象。
五.上面的那个过程反复循环几次,新生代的对象反复进入老年代,不停触发FGC,但回收不了多少对象。几次循环过后,老年代满了,可能FGC没回收多少对象。新的对象又放不下,就触发OOM了。
(7)优化第一步:增加堆内存大小
所以这个OOM的问题,就是内存分配不足的问题。因此第一步,给堆内存加大空间,给堆内存5G的空间(4核8G机器)。接着运行系统,通过jstat观察,就可以发现:每次YGC过后存活对象都落入S区了,不会随便进入老年代,而且因为堆内存很大,基本上运行一段时间不会发生OOM问题了。
(8)优化第二步:改写代码
另外就是改写代码,让它不要占用过多的内存。代码之所以递归,就是因为在一条日志中,可能会出现很多用户的信息。一条日志也许会合并包含了十几个到几十个用户的信息,这时代码中就会递归十几次到几十次去处理这个日志。每次递归都会产生大量的char[]数组,这些数组是来自切割了的日志,用来后续处理的。
其实这个代码写的完全没有必要,因为对每一条日志,如果发现包含了多个用户的信息。其实就对这一条日志切割出来进行处理即可,完全没有必要递归调用。每次调用都切割一次日志,生成大量的char[]数组。这一步代码优化后,便发现线上系统的内存使用情况降低了10倍以上。
所以JVM有风险,递归要谨慎。
6.类加载器过多引发OOM问题排查(服务假死自己恢复+ top判断 + OS杀进程)
(1)案例背景
公司里有一个非常正常的线上的服务,采用的是Web系统部署在Tomcat的方式来启动的。但是有一段时间,经常会收到反馈说这个服务非常不稳定,访问这个服务的接口会出现假死问题。也就是一段时间内无法访问这个服务的接口,但过一会又可以访问了。
(2)使用top命令检查机器资源使用
因为当时的情况是服务假死,接口无法调用,并不是OOM之类的异常。所以此时也很难直接去看它的线上日志,根据日志马上进行问题定位。
因此针对服务假死这个问题:首先可以用top命令去检查一下机器的资源使用量,通过top命令查看机器上运行的各进程对CPU和内存两种资源的使用量。
一.接口假死的两者情况
为什么要先用top看机器资源?因为如果服务出现无法调用接口假死的情况,首先要考虑的是两种原因。
第一种原因:这个服务可能使用了大量的内存,内存始终无法释放,导致频繁FGC。也许每秒都执行一次FGC,结果每次都回收不了,最终导致频繁FGC。频繁FGC又会频繁Stop the World,所以接口调用时就会出现频繁假死。
第二种原因:可能是这台机器的CPU负载太高了,也许是某个进程耗尽了CPU资源。CPU资源耗尽会导致这个服务的线程始终无法得到CPU资源去执行。没有CPU资源去执行也就无法响应接口调用请求,从而频繁假死。
因此针对服务假死的问题,先通过top命令查看,就知道关键点了。
二.根据JVM进程对CPU消耗和内存的消耗判断是否发生频繁GC
针对线上这台机器使用top命令检查之后,就发现了一个问题:这个服务的进程对CPU耗费很少,仅仅耗费了1%的CPU资源。但是这个进程虽然耗费1%CPU,却耗费了50%以上的内存资源,这个就引起了我们的注意。
因为这台机器是4核8G的标准线上虚拟机,针对这种机器通常会给部署的服务的JVM分配5~6G总内存。除掉Metaspace区,堆内存大概会给到4G~5G,毕竟还得给创建大量的线程留下一部分内存。
由于JVM使用的内存主要是三类:栈内存、堆内存和Metaspace区域,现在给Metaspace区域512M以上的空间、堆内存可能有4G。每个线程的栈内存给1M,JVM进程有几百上千线程,也要将近1G内存。
此时JVM服务进程对内存耗费已超过50%,说明它几乎快把分配给它的内存消耗尽。而且最关键的是,它长期保持对内存资源的消耗在50%以上甚至更高,这说明JVM在GC时并没有把内存回收掉。
(3)当内存使用过高时会发生什么
一.JVM进程对内存使用率过高的可能原因
既然这个服务的进程对内存使用率这么高,可能发生的问题也就三种:
第一种是内存使用率居高不下,导致频繁FGC
第二种是内存使用过多,导致OOM内存溢出
第三种是内存使用率过高,导致JVM进程被OS杀掉
二.分析这是属于哪种原因导致内存使用率过高
首先分析第一种情况,于是使用jstat分析一下JVM运行的情况。发现内存使用率确实很高,也确实经常发生GC。但实际上GC耗时每次也就几百毫秒,并没有耗费过多的时间。也就是说虽然GC频率高,但是其实是个常规现象。而且这个服务虽然经常频繁GC,但也没听见上游服务反馈说服务假死。因此第一种情况可以直接排除掉了。
接着分析第二种情况,难道是JVM自己有时候发生OOM挂掉了?挂掉时必然导致服务无法访问,上游服务肯定会反馈说我们服务死掉的。但是检查应用服务自身日志,并没有看到任何日志输出OOM异常。
接着分析第三种情况,也就是JVM运行时要申请的内存过多。结果内存不足,有时OS会直接杀掉这个进程。可能在进程被杀掉的过程中,就出现了上游服务无法访问的情况。而该机器上的JVM进程是有监控脚本的:一旦JVM进程被杀掉,会有脚本自动把JVM进程重新启动拉起来。所以也许其他服务过一会就会发现,服务又可以访问了。
(4)到底是谁占用了过多的内存
如果要解决这个问题,就必须要找出到底是什么对象占用内存过多,进而申请过多的内存,最后导致进程被杀掉了。很简单,直接从线上导出一份内存快照即可。我们在线上系统运行一段时间后,用top命令和jstat命令观察一段时间,发现当JVM已耗费超50%内存时,迅速导出一份内存快照进行分析。
此时用MAT进行内存快照分析时发现,存在一大堆的ClassLoader也就是类加载器,大概有几千个。而且这些类加载器加载的东西,都是大量的byte[]数组,这些一共占用了超过50%的内存。
那么这些ClassLoader是哪儿来的?为什么会加载那么多的byte[]数组?其实除了可以用类加载器加载类,还可以用类加载器加载一些其他资源。比如可以用类加载器加载一些外部配置文件。
当时写这个系统代码的工程师做了自定义类加载器,而且在代码里没限制地创建大量自定义类加载器,重复加载大量数据。结果经常一下子就把内存耗尽了,进程就被杀掉了。
因此解决这个问题非常的简单,直接就是修改代码,避免重复创建几千个自定义类加载器,避免重复加载大量数据到内存。
7.数据同步系统频繁OOM内存溢出的排查(时不时OOM+内存泄露+MAT排查)
(1)案例背景
首先说一下案例背景:线上有一个数据同步系统,是专门负责去同步另外一个系统的数据,另外一个系统会不停地发布自己的数据到Kafka中,然后该数据同步系统从Kafka里消费数据,接着保存到自己的数据库中。大概的流程如下图示:
结果就这么一个简单的系统,居然时不时就报一个内存溢出的错误。然后就得重启系统,过了一段时间又会再次内存溢出一下。而且当这个系统处理的数据量越来越大,它内存溢出的频率越来越高。到这种情况,就必须要处理一下了。
(2)堆内存OOM的三种原因
既然每次重启后过一段时间会出现内存溢出的问题,那么就说明每次重启后,内存都会不断上涨。
JVM出现内存溢出,通常有三种原因:
原因一是并发太高,大量并发创建过多的对象,导致系统直接崩溃了
原因二是内存泄漏,有很多对象都在内存里,无论如何GC都回收不掉
原因三是代码问题,导致某种情况下加载了大量数据,创建大量对象
那么这个场景是怎么回事呢?这个系统的负载并不高,虽然数据量不少,但并不是瞬时高并发场景。这么看来,很可能是随着时间推移,某种对象越来越多,都在内存里了。然后不断地触发GC,结果每次GC都回收不掉这些对象。一直到最后,内存实在不足了,就会内存溢出。如下图示:
(3)通过jstat来确认我们的推断
接着直接在一次重启系统后,用jstat观察JVM运行的情况。我们发现,老年代的对象一直在增长,不停在增长。每次YGC过后,老年代的对象就会增长不少。而且当老年代的使用率达到100%后,会正常触发FGC。但是FGC根本回收不掉任何对象,导致老年代使用率还是100%。然后老年代使用率维持100%一段时间过后,就会报内存溢出的问题。因为此时再有新的对象进入老年代,实在没有空间存放它了。
所以这就基本确认了我们的判断:每次系统启动,不知道什么对象会一直进入堆内存。而且随着YGC执行,对象会一直进入老年代。最后触发FGC都无法回收老年代的对象,最终就是内存溢出。
(4)通过MAT找到占用内存最大的对象
在内存快照中,发现有一个队列数据结构,直接引用了大量的数据,正是这个队列数据结构占满了内存。
那这个队列是干什么用的?从Kafka取出的数据会先写入这个队列,接着再从这个队列写入数据库。由于要额外做一些中间的数据处理和转换,所以才在中间加一个队列。如下图示:
那么问题就出在这个队列是怎么用的,由于从Kafka消费数据是可以一下子消费一批的(比如几百条数据)。所以当时直接把每次消费的一批数据做成一个List,然后放入队列中。最后就造成了如下这种情况,这个队列有大量元素,每个元素都是一个List,每个List有几百条数据。
所以这种做法一定会导致内存中的队列积压大量数据,最终就会OOM。而且只要数据还停留在队列中,就没有办法被回收。如下图示:
这就是一个典型的对生产和消费的速率没控制好的案例:从Kafka里消费出数据放入队列的速度很快。但从队列里消费数据进行处理,然后写入存储的速度较慢。最终导致内存队列快速积压数据,导致内存溢出,而且这种让队列里的每个元素都是一个List的做法,也会导致内存队列能容纳的数据量大幅膨胀。
最终的解决方法很简单:把上述内存队列的使用修改一下,做成定长的阻塞队列即可。然后每次从Kafka消费出数据,直接将数据写入队列,而不是List。
比如队列中最多1024个元素,那么内存中最多就是1024个数据,如下图示:
一旦内存队列满了,Kafka消费线程就会停止工作,因为被队列阻塞住了,就不会让内存队列中的数据过多。
8.线上系统的JVM参数优化、GC问题定位排查、OOM分析解决总结
(1)JVM运行系统时的工作原理
一.内存各个部分的划分
二.代码执行过程中各内存区域如何配合工作
三.对象是如何分配的
四.GC如何触发
五.GC执行的原理是什么
六.常见的用于控制JVM的一些核心参数
(2)JVM参数在上线前如何预设、测试时如何调整、上线后如何优化
对于一个开发好的系统:首先应该如何通过预估的方法给它设置一些相对合理的JVM参数,然后在测试时如何合理的优化调整JVM参数,接着在线上运行时如何对JVM进行监控,如何对线上出现GC性能问题时进行合理优化。
(3)如何分析定位解决线上OOM问题
出现OOM的三种原因,三种OOM的定位和解决方法,OOM的一些生产案例。
文章转载自:东阳马生架构
原文链接:JVM实战—13.OOM的生产案例 - 东阳马生架构 - 博客园
体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构
相关文章:
JVM实战—OOM的生产案例
1.每秒仅上百请求的系统为何会OOM(RPC超时时间设置过长导致QPS翻几倍) (1)案例背景 在这个案例中,一个每秒仅仅只有100请求的系统却因频繁OOM而崩溃。这个OOM问题会涉及:Tomcat底层工作原理、Tomcat内核参数的设置、服务请求超时时间。 (2)系统发生OOM的…...
oracle闪回恢复数据:(闪回查询,闪回表,闪回库,回收站恢复)
oracle的闪回查询,可以查询提交在表空间的闪回数据,并可以还原所查询的数据,用于恢复短时间内的delele 或者 update 误操作,非常方便,缺点是只能恢复大概几小时内的数据。 文章目录 概要闪回查询恢复数据的主要方法包括…...
STM32 单片机 练习项目 LED灯闪烁LED流水灯蜂鸣器 未完待续
个人学习笔记 文件路径:程序源码\STM32Project-DAP&DAPmini\1-1 接线图 3-1LED闪烁图片 新建项目 新建项目文件 选择F103C8芯片 关闭弹出窗口 拷贝资料 在项目内新建3个文件夹 Start、Library、User Start文件拷贝 从资料中拷贝文件 文件路径:固…...
计算机网络之---数据传输与比特流
数据传输的概念 数据传输是指将数据从一个设备传输到另一个设备的过程。传输过程涉及将高层协议中的数据(如包、帧等)转化为比特流,在物理介质上传输。 比特流的概念 比特流是数据传输中最基本的单位,它是由0和1组成的连续比特…...
es 单个节点cpu过高
背景: es 单个节点cpu一直持续很高,其它节点cpu不高。 观察这个节点的jvm使用率比较高,怀疑是jvm内存没释放导致内存寻址效率低,引起cpu过高。 解决方法:手动执行fullgc, 在线执行对业务无影响。 jcmd pi…...
设计模式 结构型 组合模式(Composite Pattern)与 常见技术框架应用 解析
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次结构。通过这种模式,客户端可以一致地处理单个对象和对象组合。 在软件开发中,我们经常会遇到处理对象的层…...
pcie学习记录(1):基于xdma的工程搭建
写在最前面 Vivado的版本!!!(后面验证是错误的思路,可以不用看) XDMA PCIE开发期间两个版本问题的解决https://blog.csdn.net/kris_paul/article/details/128131527 我说实话在这里卡了很久ÿ…...
HTML5 动画效果:淡入淡出(Fade In/Out)详解
HTML5 动画效果:淡入淡出(Fade In/Out)详解 淡入淡出(Fade In/Out)是一种常见的动画效果,使元素逐渐显现或消失,增强用户体验。以下是淡入淡出的详细介绍及实现示例。 1. 淡入淡出的特点 平滑…...
AI赋能R-Meta分析核心技术:从热点挖掘到高级模型、助力高效科研与论文发表
Meta分析是针对某一科研问题,根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法,对来源不同的研究成果进行收集、合并及定量统计分析的方法,现已广泛应用于农林生态,资源环境等方面,成为Science、Nature论文的…...
maven如何从外部导包
1.找到你项目的文件位置,将外部要导入的包复制粘贴进你当前要导入的项目下。 2.从你的项目目录下选中要导入的包的pom文件即可导包成功 注意一定是选中对应的pom文件 导入成功之后对应的pom.xml文件就会被点亮...
使用python发送gmail邮件
首先要有一个gmail邮箱:https://accounts.google.com/lifecycle/steps/signup/name?ddm1&dshS237022023:1736341599452877&flowEntrySignUp&flowNameGlifWebSignIn&TLAE–Llw01wbAW0P9gYEloi_C5wBurZd0MeIi4B1j8daH-72CNcc-1qwapbIRA_UZ&conti…...
Android 绘制学习总结
1、刷新率介绍 我们先来理一下基本的概念: 1、60 fps 的意思是说,画面每秒更新 60 次 2、这 60 次更新,是要均匀更新的,不是说一会快,一会慢,那样视觉上也会觉得不流畅 3、每秒 60 次,也就是 1…...
推动多语言语音科技迈向新高度:INTERSPEECH 2025 ML-SUPERB 2.0 挑战赛
随着语音技术在各领域应用的迅速扩展,全球语言与口音的多样性成为技术进一步突破的重大挑战。为了应对这一难题,来自卡内基梅隆大学(CMU)、斯坦福大学(Stanford University)、乔治梅森大学(George Mason Un…...
『SQLite』子查询可以这样用
摘要:本节主要讲子查询的使用,可以在查询、更新、修改、删除等操作中使用。 什么是子查询? 子查询是一种在 SELECT-SQL 语言中嵌套查询下层的程序模块。当一个查询是另一个查询的条件时,称之为子查询(Sub Query&#…...
thingsboard通过mqtt设备连接及数据交互---记录一次问题--1883端口没开,到服务器控制面板中打开安全组1883端口
1,链接不上:原因是1883端口没开,到服务器控制面板中打开安全组1883端口 2,参考链接: https://blog.csdn.net/bujingyun8/article/details/120024788...
Pod状态为“被驱逐(evicted)”的解决思路
在Kubernetes集群中,Pod状态为“被驱逐(evicted)”表示Pod无法在当前节点上继续运行,已被集群从节点上移除。针对Pod被驱逐的问题,以下是一些常见的解决方法: 一、识别被驱逐的原因 资源不足:…...
【代码随想录-数组篇02】:双指针(快慢指针)法相关力扣练习题
提示1:本篇共包含5道题,全部用python语言进行实践,看会不如行动会,请大家多多实践~ 提示2:强烈推荐 代码随想录 提示3:博主最近在跟着【代码随想录】进行刷题,有小伙伴有想法的可以私…...
Docker基础
Docker基础 命令介绍 其中,比较常见的命令有: 命令说明文档地址docker pull拉取镜像docker pulldocker push推送镜像到DockerRegistrydocker pushdocker images查看本地镜像docker imagesdocker rmi删除本地镜像docker rmidocker run创建并运行容器&am…...
计算机网络之---传输介质
传输介质的定义 传输介质是计算机网络中用于传递信号的物理媒介。它将数据从发送方传输到接收方,可以是有线的(如铜线、电缆、光纤等)或者无线的(如无线电波、微波等)。传输介质的作用是承载和传播物理信号(…...
数据结构:LinkedList与链表—面试题(三)
目录 1、移除链表元素 2、反转链表 3、链表的中间结点 4、返回倒数第k个结点 5、合并两个有序链表 1、移除链表元素 习题链接https://leetcode.cn/problems/remove-linked-list-elements/description/ 描述:给你一个链表的头节点 head 和一个整数 val ÿ…...
P10424 [蓝桥杯 2024 省 B] 好数
题目描述 一个整数如果按从低位到高位的顺序,奇数位(个位、百位、万位……)上的数字是奇数,偶数位(十位、千位、十万位……)上的数字是偶数,我们就称之为“好数”。 给定一个正整数 N…...
MySQL - 子查询和相关子查询详解
在SQL中,子查询(Subquery)和相关子查询(Correlated Subquery)是非常强大且灵活的工具,可以用于执行复杂的数据检索和操作。它们允许我们在一个查询中嵌套另一个查询,从而实现更复杂的逻辑和条件…...
MySQL入门学习笔记
第一章 数据库系统概述 数据库的4个基本概念 数据、数据库、数据库管理系统、数据库系统是与数据库技术密切相关的4个基本概念 数据 数据是数据库中存储的基本对象,描述事物的符号记录称为数据,数据的表现形式还不能完全表达其内容,需要…...
使用vue-pdf预览pdf和解决pdf电子签章显示问题
使用vue-pdf预览pdf和解决pdf电子签章显示问题 第一步:npm install vue-pdf 第二步页面使用vue-pdf <template><div class"pdf1"><Pdf v-for"i in numPages" :key"i" :src"src" :page"i" />…...
MySQL Windows 11 的 MySQL 配置文件 (my.ini) 路径查找指南
✅ Windows 11 的 MySQL 配置文件 (my.ini) 路径查找指南 在 Windows 11 上,MySQL 的 ini 配置文件(通常是 my.ini 或 my.cnf)的位置取决于 MySQL 的安装方式。下面是一些常见的路径和方法来找到这个配置文件。 🔍 方法 1&#…...
记一次k8s下容器启动失败,容器无日志问题排查
问题 背景 本地开发时,某应用增加logback-spring.xml配置文件,加入必要的依赖: <dependency><groupId>net.logstash.logback</groupId><artifactId>logstash-logback-encoder</artifactId><version>8…...
Delaunay三角刨分算法理解及c#过程实现
Delaunay三角刨分算法理解及c#过程实现 0 引言1 关于三角剖分2 Delaunay三角剖分算法实现及对比3 结语0 引言 💻💻AI一下💻💻 三角剖分是什么? 三角剖分是一种将平面或曲面划分成三角形集合的方法。在二维平面中,给定一个平面区域(可以是多边形等),通过连接区域…...
【单片机】实现一个简单的ADC滤波器
实现一个 ADC的滤波器,PT1 滤波器(也称为一阶低通滤波器),用于对输入信号进行滤波处理。 typedef struct PT1FilterSettings PT1FilterSettings; struct PT1FilterSettings {//! last Filter output valueuint32_t filtValOld;//…...
在 Ubuntu 22.04 上从 Wayland 切换到 X11的详细步骤
在 Ubuntu 22.04 上从 Wayland 切换到 X11,步骤其实很简单,主要是在登录界面进行选择。以下是详细的步骤: 步骤 1:退出当前会话 首先,点击屏幕右上角的用户菜单,选择 注销 或 退出,以退出当前…...
NOVA:AutoRegressive Video Generation Without Vector Quantization——自回归视频生成无需向量量化
这篇文章介绍了一种名为NOVA的新型自回归模型,用于高效的文本到图像和文本到视频生成。以下是文章的主要内容总结: 1. 研究背景与问题 自回归大语言模型(LLMs)在自然语言处理(NLP)中表现出色,但…...
79 Openssl3.0 RSA公钥加密数据
1 引言 最近不小心用到了openssl3.0,项目中需要使用rsa非对称加解密算法,所以把openssl3.0使用公钥加密数据的函数调用摸了一遍。 之所以记录此篇文章,是因为网络上大多数是openssl3.0以前的版本的函数接口,而openssl3.0之后已经丢…...
基于单片机的数字电子秒表设计
此文章谨为课设记录 一、实验要求 题目六 数字电子时钟 基本要求: (1) 设计一个单片机电子时钟,设计的电子时钟通过数码管显示; (2) 具有能通过按键实现设置时间的功能; (3) 显示格式为小时十位、小时个位,分…...
rust学习——环境搭建
rust安装:https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html 1、vscode装插件: toml语法支持 依赖管理 rust语法支持 2、创建demo 3、查看目录 4、执行文件的几种方式: rust安装:https://www.rust-lang.org/z…...
解读若依框架中的 @Xss 注解
文章目录 1. 背景与问题定义什么是 XSS 攻击?XSS 的常见类型传统解决方案的局限性 2. Xss 注解详解Xss 注解源码解析注解核心要素 XssValidator 实现解析核心逻辑 3. 应用场景场景一:表单输入校验示例代码 场景二:API 接口参数校验示例代码 4…...
ADO.NET知识总结4---SqlParameter参数
目录 5.1 SqlParameter介绍 5.2 SqlParameter创建参数方式 5.3 添加命令参数 5.4 参数使用 5.1 SqlParameter介绍 SqlParameter: 表示SqlCommand对象的参数,或与DataSet中列的映射。 常用属性 DbType 参数的SqlDbType(即 数据库的类型而言&#x…...
分布式主键ID生成方式-snowflake雪花算法
这里写自定义目录标题 一、业务场景二、技术选型1、UUID方案2、Leaf方案-美团(基于数据库自增id)3、Snowflake雪花算法方案 总结 一、业务场景 大量的业务数据需要保存到数据库中,原来的单库单表的方式扛不住大数据量、高并发,需…...
【顶刊TPAMI 2025】多头编码(MHE)之极限分类 Part 4:MHE表示能力
目录 1 MHE的表示能力2 基于Frobenius-范数的低秩逼近3 基于CE的低秩近似 论文:Multi-Head Encoding for Extreme Label Classification 作者:Daojun Liang, Haixia Zhang, Dongfeng Yuan and Minggao Zhang 单位:山东大学 代码:h…...
Spring Boot 项目启动报 NoClassDefFoundError 异常的原因分析与解决方案 - jackson 版本不一致
目录 报错: 问题分析: 解决方案: 方案 1:对 Jackson 版本进行统一 方案 2:升级 Springfox 版本 方案 3:替换 Springfox 为 springdoc-openapi(推荐) 方案 4:排除冲突的 Jack…...
Ubuntu网络连接问题(笔记本更换wifi后,虚拟机连不上网络)
1、笔记本更换wifi后,虚拟机的IP地址变了,然后就连不上网络了(主机笔记本连接wifi正常上网) 2、修改子网地址(按照ubutun的ip设置子网掩码) 3、Ubuntu已经显示网络连接正常了,但是就是无法上网&…...
vue3中el-table实现多表头并表格合并行或列
1、el-table中添加事件 :span-method"genderSpanCity" <el-table :span-method"genderSpanCity":data"data.tableData":fit"true" table-layout"fixed" header-align"center" stripestyle"width:100%;he…...
Selenium 八大元素定位方法及场景扩展
Selenium 提供了八种常见的元素定位方法,用于在网页中准确找到需要操作的元素。以下是详细介绍以及一些特殊场景的解决方案。 1. ID 定位 用法: 通过元素的唯一 id 属性进行定位。 element driver.find_element(By.ID, "element_id")使用场…...
计算机网络之---网络拓扑
什么是网络拓扑 为什么需要网络拓扑 网络拓扑有哪些 什么是网络拓扑 网络拓扑 是指网络中各设备(如计算机、路由器、交换机等)如何连接以及它们之间数据流动的结构和布局。它是网络设计的一个关键方面,决定了网络的性能、可扩展性、可靠性…...
YOLO11新王登基!实战改进版,杀穿顶会!
众所周知,YOLO一直是发论文的大热门!而YOLO11做为其的最新版本,必然会在2025成为各大顶会的“宠儿”! 主要在于:一方面,YOLO其他系列的改进早已卷成“红海”,而YOLO11提出才几个月,…...
【《游戏编程模式》实战04】状态模式实现敌人AI
目录 1、状态模式 2、使用工具 3、状态模式适用范围 4、实现内容 5、代码及思路 Enemy.cs EnemyState.cs 6、unity里的设置 7、运行效果展示 1、状态模式 “允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像是在修改自身类。” 就是一个对象能随着自己…...
Linux 环境(Ubuntu)部署 Hadoop 环境
前置准备 准备三台机器 cat /etc/hosts 192.168.1.7 hadoop-master 192.168.1.11 hadoop-slave01 192.168.1.12 hadoop-slave02Linux 环境 cat /etc/os-release PRETTY_NAME"Ubuntu 24.10" NAME"Ubuntu" VERSION_ID"24.10" VERSION"24.…...
裸机器搭建k8s部署 1.28.10版本
问了搭建k8s集群踩了很多坑,问题主要出现在网络插件处,因此主要是master节点操作问题。重新走一下流程整理一下笔记。 目录 虚拟机准备 虚拟机 系统版本信息 修改镜像地址 配置静态ip 关闭防火前和交换分区 转发 IPv4 并让 iptables 看到桥接流量…...
np.ndarray 是 NumPy 库中的核心数据结构
np.ndarray 是 NumPy 库中的核心数据结构,代表一个多维数组(或称为矩阵)。它是一个高效的容器,用于存储和操作数据,可以包含任意类型的数据(整数、浮点数、布尔值等)。 主要特点: …...
全新免押租赁系统打造便捷安全的租赁体验
内容概要 全新免押租赁系统的推出,标志着租赁行业的一次重大变革。这个系统的最大特点就是“免押金”,大大减轻了用户在租赁过程中的经济负担。从此,不再需要为一部手机或其他商品支付高昂的押金,用户只需通过简单的信用评估&…...
C语言——结构体,位段,枚举和联合
目录 前言 结构体 1含义 2语法 3匿名结构体 4结构体自引用 5结构体的定义与初始化 6内存对齐 7修改对齐数 8结构体传参 位段 1含义 2位段的内存分配 编辑3位段的问题 4位段的应用 枚举 1含义 2定义 3枚举优点 4枚举使用 联合 1含义 2定义 3特点 4计…...
现场展示deepseek VS openAI o1模型大对比
DeepSeek-V3 模型的发布在 AI 领域引起了广泛关注。作为一款拥有 6850 亿参数的混合专家(MoE)语言模型,DeepSeek-V3 在多个基准测试中表现出色,甚至超越了一些闭源模型。其在 Aider 代码能力排行榜上的正确率达到 48.4%࿰…...