1.前言在线排查相对于coding来说是一个低频的工作,很多人不会经常遇到。一旦需要排除故障,往往又重要又紧急,因此排除故障的效率就显得尤为重要。有些线上问题比较直观,比如磁盘占用率高、网络流量高,借助合适的工具可以快速定位原因;但对于一些复杂的问题,比如系统负载高、RSS使用率高、内存溢出等,需要结合各种数据来定位原因。这时候就需要有正确的解题思路,辅以合适的工具,才能高效的解决问题。目前业界还是有很多优秀的排查问题的工具,比如国内阿里巴巴开源的Arthas,PerfMa开源的解决性能问题的xPocket,官方JavaJMC(JDKMissionControl),EclipseMAT(MemoryAnalyzer)Tooling),还有Async-Profiler,我一直推崇的神器。以上只是一些流行的开源工具。jProfiler、YourKit等商业工具也建立了稳定的用户群。这些工具的功能各不相同。当然这不是本文的重点,就不赘述了。当横向比较这些工具时,我们发现它们的目标都是为了解决一些特定的问题。如果我们有一个清晰的排查思路,结合这些工具,就能很快解决问题。而对于一些复杂的场景,尤其是一些不为人知的复杂问题,如果没有头绪,纵使拥有各种法宝,也无能为力。在线排除故障就像开车一样。老司机驾轻就熟,新手却手忙脚乱。当然,如果新手有老司机指点,或许很快就能解决问题。但问题是这种老司机不常见,也不可能一直帮你。我们可以去网上查查别人总结的排错套路,然后结合自己的场景去尝试解决问题,我也经常这样做。但是,这种方法的效率仍然不高,原因有三:信息检索成本:我们需要花时间翻阅资料来匹配自己的场景,判断是否适合自己;试错成本:有些资料不适合我们的场景,如果根据资料去试,可能会被引入沟里,浪费时间;故障排除需要借助一些第三方工具,而这些工具需要在生产环境中进行安装、配置和使用,需要花费更多的时间和成本。根据在线问题排查的特点和现状,是否可以构建一个系统,形成一个知识(套路)库,用于排查各种在线问题。对于每一个问题,都有相应的套路和自动化工具帮助我们定位问题。本文将结合一个具有代表性的线上问题排查流程,探讨该方法的可行性。2.日常排查本章以RSS使用率过高为例,说明日常排查。RSS使用率高是很多人都遇到过的问题。这个问题涉及的因素很多,很有代表性。当然,在启用Swap的运行环境下,Swap高也是RSS高的标志,同一个目的是通过不同的路由来实现的。RSS是ResidentSetSize(常驻内存大小)的缩写,用来表示进程使用了??多少内存(RAM中的物理内存)。如果遇到进程RSS接近服务器的物理内存,说明需要关注应用的健康状况,这意味着应用后期可能会出现OOM问题,比如进程被OOM杀死杀手,或者容器被重新启动。或者通过使用Swap减慢速度。关于RSS高的问题,首先我们要知道Java进程消耗的内存不仅仅是你设置的Xmx或者堆内存的多少那么简单。Java进程占用的内存主要分为堆上(In-heapmemory)和堆外(out-of-heapmemory)两部分。堆外内存包括JVM自身消耗的内存和JVM之外的内存。所以后续的考察思路也是按照堆内存、JVM内存、JVM内存三个方向依次进行的。1、堆内存是否过大首先需要确认Java应用的堆内存是否过大,因为JVM本身也会消耗一些内存,所以至少要为JVM预留一部分内存使用。如果应用涉及到大量的网络通信,你需要预留一些内存供堆外使用,所以一般来说你的堆内存最多为服务器物理内存的75%(经验值,需要根据需要调整应用的特性),比如4G内存的服务器,最大堆内存为3G。有很多方法可以检查堆内存使用情况。相信各个公司的基础架构团队都提供了可视化的监控方式。当然也可以通过原生命令jcmdGC.heap_info查看,如图1所示:图1如果Java进程的堆内存使用率接近或超过物理内存的75%,则可以基本可以确定是堆内存占用过大。这时候可以降低Xmx来控制堆内存的使用。如果Xmx无法降低,可以使用dumpheapmemory+MAT或者JFR(JavaFlightRecorder)+JMC(JDKMissionControl)分析内存占用/分配,通过程序调优降低堆内存占用。如果此时RSS占用呈现稳定趋势,则可以告一段落,否则继续进行后续步骤。2.是否有大量的ARENA区域。如果堆内存不大,继续查看非堆内存。先来看ARENA区域。在高并发应用中,ARENA区往往会占用较多的内存。为什么要先看ARENA区的内存使用情况呢?因为这一步不需要重启JVM进程就可以完成。接下来,我们直接进入故障排除部分。执行如下命令:sudo-upmap-x|sort-gr-k2|less如果有大量内存区大小在65536或60000左右,很可能是ARENA区占用内存过多,如图图2中:对于图2的情况,最简单粗暴的方式就是在JVM启动参数中添加配置:exportMALLOC_ARENA_MAX=1。需要注意的是,上面的值只能是1,其他大于1的值已经被实践证明无法控制ARENA的数量。的。3、非堆内存的开销是不是太大了?如果前两步都没有发现问题,还有很多内存不知道消耗到哪里了,那我们就开始第三步:启用NativeMemoryTracking。如前所述,Java应用程序的执行需要JVM本身消耗一些内存。通过启用NativeMemoryTracking,我们可以知道JVM本身消耗了多少内存。回到正题,通过修改JVM参数并重启Java进程开启NativeMemoryTracking:-XX:NativeMemoryTracking=detail进程重启后,可以通过NMT的一些子命令(summary/detail/baseline)查看NativeMemory的占用情况/diff):sudo-ujcmdVM.native_memorydetail图3是使用baseline建立基线时用detail.diff看到的各个内存区域的变化:图3通过上图可以看到各个区域使用的内存大小JVM的,主要包含JavaHeap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol等。各部分的作用如下:Class:加载的类和方法信息,其实就是metaspace,其中包括两部分:一是元数据,即-XX:MaxMetaspaceSize限制最大大小,二是类空间,由-XX:CompressedClassSpaceSize限制;Thread:线程和线程栈占用内存,每个线程栈的大小受-Xss限制,但总大小不限。在x64JVM中,Xss默认为1024K,所以如果你的应用开启了1000个线程,那么Thread区会占用1024M,所以一般我们会把Xss设置为256K来满足要求;代码:JIT即时编译(C1C2编译器优化)代码占用内存,受-XX:ReservedCodeCacheSize限制;GC:垃圾回收占用内存,比如垃圾回收需要的CardTable,标记个数,区域划分记录,标记GCRoot等,都需要内存。这个不限,一般不会很大,但也有例外。图3显示了27G的堆内存。使用G1垃圾收集器,可以看到GC区实际占用了3.8G内存;Compiler:C1C2compileritself代码和标记占用的内存是无限的,一般不会很大;internal:命令行解析,JVMTI使用的内存没有限制,一般不会很大;Symbol:常量池占用的大小,字符字符串常量池受-XX:StringTableSize个数限制,总内存大小不限;我们需要注意Class、Thread和GC区域的大小。图4显示了各种JVM垃圾收集器消耗的内存比例。注意这部分内存在堆内存之外:图4已经证明G1的内存开销甚至可以占到堆内存的20%。当然,这不是本文要讨论的内容,有兴趣的读者可以阅读本书《JVM G1源码分析和调优》查看相关内容。将上面每个区域的大小相加,看是否接近RSS的大小。如果是这样,恭喜你,你可以到此为止了。后面需要做的就是对内存占用比较大的JVM区域进行优化,这里就不赘述了。如果没有,很遗憾你进入了最难的环节,继续往下看。注意:开启NativeMemoryTracking会导致5%的性能下降。记得修改JVM参数并重启才能永久关闭,也可以使用以下命令暂时关闭:jcmdvm.native_memorystop。4.堆外内存是否过多?堆外内存也是一个容易被忽视的区域,尤其是对于网络通信非常频繁的应用程序。这种应用往往会大量使用JavaNIO,而NIO往往会申请大量的堆外内存。确认这块区域的使用量是否过大,最直接的方法就是先查看DirectByteBuffer或MappedByteBuffer是否使用了较多的堆外内存。如果你的服务器开启了远程JMX,可以通过ops提供的jmx查询工具,或者通过jdk自带的工具(如jconsole,jvisualvm)进行查询,如图5:图5如果远程JMX是没有启用JMX,可以使用jmxterm(https://docs.cyclopsgroup.org/jmxterm)工具在本地模式下查询以下两项来确认使用:java.nio:name=direct,type=BufferPooljava.nio:name=mapped,type=BufferPool如果确认上述堆外内存使用过多,可以通过在jvm参数中设置-XX:MaxDirectMemorySize参数来控制,因为堆外内存是通过分配的DirectByteBuffer默认不会控制这块区域的内存使用。如果上面的内存占用量不大,那我们就需要求助于终极杀手jemalloc做进一步的分析了。这里涉及到的内容很多,限于文章篇幅,这里就不一一赘述了。通过jemalloc收集的数据,我们基本可以定位到堆外内存问题的原因。5、总结以上4步,基本可以解决大部分RSS使用率高的问题。当然,没有绝对的,没有一种药可以包治百病。我们追求的基本目标是通过日常故障排除,帮助工程师理清思路,少走弯路,提高故障排除效率。3.故障排除工具回顾上述故障排除过程。我们使用了很多命令和第三方工具。整个过程仍然是工程师驱动的命令行和工具。如果你对命令参数不熟悉,或者本地没有安装相应的工具,那么这种套路教程只能在一定程度上提高效率。在这个套路的基础上,是否可以转变思路,聚焦工具,辅助工程师提高排错效率?让我们试试。1.流程梳理首先梳理一下工具的执行流程,以及每个步骤需要做的事情,与第二节保持一致。此部分也分为4个子步骤。1)确认堆内存是否过大。第一步要做的事情很多,如下:Java进程pid:使用jps-v列出java进程列表,让用户选择具体的进程;获取运行环境的物理内存和剩余资源Memory(free-m)Java进程堆内存使用情况(jcmdGC.heap_info)Java进程GC状态(jstat-gcutil);得到以上信息后,判断堆内存是否过大。2)是否存在大量ARENA区域通过pmap命令获取内存分配列表,辅以awk命令提取内存信息,判断是否存在大量ARENA区域。3)非堆内存的开销是否太大。该步骤需要在JVM启动脚本中添加启动参数并重启进程。对于一个标准化的运行环境,如果知道启动脚本的位置和启动命令,就可以使用该工具来完成参数修改和进程启动。如果我们不知道启动脚本在哪里,我们可以复制当前进程的JVM参数来完成进程的启动。当JVM进程启动,我们再次进入工具,我们可以通过NativeMemoryTracking的结果来判断当前链接是否有问题。4)堆外内存量是否过多。该步骤的自动执行需要安装jxmterm、jemalloc等第三方工具。在生产环境外网访问受限的情况下,可以通过搭建内网资源服务器来解决这个问题。通过一键安装脚本,我们可以快速完成我们所依赖的工具的安装和配置,剩下的就是让工具收集和分析数据来定位问题了。2.工具实现目前公司内部很多运维工具都是以C+B/S的方式实现的。这样工程师就可以在不申请目标服务器权限的情况下实现很多运维操作。但是这种实现方式比较复杂,我们的工具是实验性的,所以暂时使用shell+toolkit来实现,也就是用shell脚本把主流程串起来。如果缺少各个节点使用的工具,yum可以安装使用yum安装,如果yum无法安装,提前下载并构建到工具包中。一切准备工作完成后,编写脚本的工作就比较简单了。当然,这需要大量的shell、linux、java命令,这里就不赘述了。脚本最终运行效果如下:四、总结通过整理RSS使用率高问题的排查套路和排查工具,我们实现了一个简单的快速排查脚本。当然,在这个过程中,可以发现很多问题的排查都可以用类似的思路进行工具化。随着时间的推移,一个用于故障排除的工具包已经形成。以排查内存问题为例,我们积累了以下快捷工具,如图:这只是工具包的一部分。对于CPU、磁盘、网络、GC等问题,借助Arthas、Async-Profiler等优秀的开源工具,我们都积累了很多快捷工具,希望能帮助工程师提高效率故障排除。前面说了,这种完全基于shell的方式需要登录目标服务器进行操作,而且大部分功能还需要sudo权限,有点不方便。另外,有些公司的生产环境受到严格限制,无法使用shell方式。因此,在此基础上,可以扩展为Client+Server+Browser的模式,让工程师无需登录服务器即可完成故障排除。至此,本文的内容就结束了,但是我们的工具还在积累中,欢迎有兴趣的同学帮我们提供场景,我们继续丰富这个工具库。同时,受作者水平所限,文中内容难免有不当之处,欢迎大家提出意见和建议。
