• 线上问题排查及GC排查案例 lcl


    一、线上问题分类及排查手段

      如果发现线上服务变慢等情况,应该如何排查?

      1、查询业务日志:

        可以发现这类问题:请求压力大,波峰,遭遇降级,熔断等等, 基础服务、外部 API 依赖出现故障。

      2、查看系统资源和监控信息:

        硬件信息、操作系统平台、系统架构;

        排查 CPU 负载、内存不足,磁盘使用量、硬件故障、磁盘分区用满、IO 等待、IO 密集、丢数据、并发竞争等情况;

        排查网络:流量打满,响应超时,无响应,DNS 问题,网络抖动,防火墙问题,物理故障,网络参数调整、超时、连接数。

      3、查看性能指标,包括实时监控、历史数据。可以发现假死,卡顿、响应变慢等现象;

        排查数据库, 并发连接数、慢查询、索引、磁盘空间使用量、内存使用量、网络带宽、死锁、TPS、查询数据量、redo 日志、undo、 binlog 日志、代理、工具 BUG。可以考虑的优化包括: 集群、主备、只读实例、分片、分区;

        可能还需要关注系统中使用的各类中间件。

      4、排查系统日志, 比如重启、崩溃、Kill 。

      5、APM,比如发现有些链路请求变慢等等。

      6、排查应用系统

        排查配置文件: 启动参数配置、Spring 配置、JVM 监控参数、数据库参数、Log 参数、内存问题,比如是否存在内存泄漏,内存溢出、批处理导致的内存放大等等;

        GC 问题, 确定 GC 算法,GC 总耗时、GC 最大暂停时间、分析 GC 日志和监控指标: 内存分配速度,分代提升速度,内存使用率等数据,适当时修改内存配置;

        排查线程, 理解线程状态、并发线程数,线程 Dump,锁资源、锁等待,死锁;

        排查代码, 比如安全漏洞、低效代码、算法优化、存储优化、架构调整、重构、解决业务代码 BUG、第三方库、XSS、CORS、正则;

      7、可以在性能测试中进行长期的压测和调优分析。

      8、其他:

        假设我们有一个提供高并发请求的服务,系统使用 Spring Boot框架,指标采集使用MicroMeter ,监控数据上报给 Datadog 服务。

        当然,Micrometer支持将数据上报给各种监控系统, 例如: AppOptics, Atlas,Datadog, Dynatrace, Elastic, Ganglia, Graphite, Humio, Influx, Instana, JMX,KairosDB, New Relic, Prometh eus, SignalFx, Stackdriver, StatsD, Wavefront 等等。有关MicroMeter的信息可参考:https://micrometer.io/doc

    二、线上问题分析实战

    (一)问题现象描述

      最近一段时间,通过监控指标发现,有一个服务节点的最大GC暂停时间经常会达到400ms以上。

      如下图所示:

            

       从图中可以看到,GC暂停时间的峰值达到了 546ms ,这里展示的时间点是 2020年02月04日09:20:00 左右。客户表示这种情况必须解决,因为服务调用的超时时间为1秒,要求最大GC暂停时间不超过200ms,平均暂停时间达到100ms以内,对客户的交易策略产生了极大的影响。

    (二)排查与分析

      1、CPU负载

        CPU的使用情况如下图所示:

            

        从图中可以看到:系统负载为 4.92 , CPU使用率 7% 左右,其实这个图中隐含了一些重要的线索,但我们此时并没有发现什么问题。

      2、GC内存使用情况 

        然后我们排查了这段时间的内存使用情况: 

            

         从图中可以看到,大约 09:25 左右 old_gen 使用量大幅下跌,确实是发生了FullGC 。但 09:20 前后,老年代空间的使用量在缓慢上升,并没有下降,也就是说引发最大暂停时间的这个点并没有发生FullGC。当然,这些是事后复盘分析得出的结论。 当时对监控所反馈的信息并不是特别信任,怀疑就是触发了FullGC导致的长时间GC暂停。 

        为什么有怀疑呢,因为Datadog这个监控系统,默认10秒钟上报一次数据。 有可能在这10秒内发生些什么事情但是被漏报了(当然,这是不可能的,如果上报失败会在日志系统中打印相关的错误)。 

        再分析上面这个图,可以看到老年代对应的内存池是 " ps_old_gen ",通过前面的学习,我们知道, ps 代表的是 ParallelGC 垃圾收集器。 

      3、JVM启动参数 

        查看JVM的启动参数,发现是这样的:

    ‐Xmx4g ‐Xms4g

         我们使用的是JDK8,启动参数中没有指定GC,确定这个服务使用了默认的并行垃圾收集器。于是怀疑问题出在这款垃圾收集器上面,因为很多情况下 ParallelGC 为了最大的系统处理能力,即吞吐量,而牺牲掉了单次的暂停时间,导致暂停时间会比较长。

    (三)处理

       怎么办呢? 准备换成G1,毕竟现在新版本的JDK8中G1很稳定,而且性能不错。然后换成了下面的启动参数: 

    # 这个参数有问题,启动失败
    ‐Xmx4g ‐Xms4g ‐XX:+UseG1GC ‐XX:MaxGCPauseMills=50ms

       结果启动失败, 忙中出错,参数名和参数值都写错了。修正如下: 

    ‐Xmx4g ‐Xms4g ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50

       接着服务启动成功,等待健康检测自动切换为新的服务节点,继续查看指标。

            

       看看暂停时间,每个节点的GC暂停时间都降下来了,基本上在50ms以内,比较符合我们的预期。嗯!事情到此结束了?远远没有。 

    (四)产生新的问题

      过了一段时间,我们发现了个下面这个惊喜(也许是惊吓),如下图所示: 

            

       中奖了,运行一段时间后,最大GC暂停时间达到了 1300ms 。情况似乎更恶劣了。继续观察,发现不是个别现象:

            

    (五)继续排查与分析

      1、注册GC事件监听

      于是想了个办法,通过JMX注册GC事件监听,把相关的信息直接打印出来。关键代码如下所示: 

    // 每个内存池都注册监听 
    for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
        if (!(mbean instanceof NotificationEmitter)) {
            continue; // 假如不支持监听... 
        }
        final NotificationEmitter emitter = (NotificationEmitter) mbean; 
        // 添加监听
        final NotificationListener listener = getNewListener(mbean); 
        emitter.addNotificationListener(listener, null, null); 
    }  

      通过这种方式,我们可以在程序中监听GC事件,并将相关信息汇总或者输出到日志。再启动一次,运行一段时间后,看到下面这样的日志信息:

    { 
        "duration":1869,
        "maxPauseMillis":1869, 
        "promotedBytes":"139MB", 
        "gcCause":"G1 Evacuation Pause", 
        "collectionTime":27281, 
        "gcAction":"end of minor GC", 
        "afterUsage": 
         { 
            "G1 Old Gen":"1745MB", 
            "Code Cache":"53MB", 
            "G1 Survivor Space":"254MB", 
            "Compressed Class Space":"9MB", 
            "Metaspace":"81MB", 
            "G1 Eden Space":"0" 
        }, 
        "gcId":326,
         "collectionCount":326, 
        "gcName":"G1 Young Generation", 
        "type":"jvm.gc.pause"
    }            

      情况确实有点不妙。这次实锤了,不是FullGC,而是年轻代GC,而且暂停时间达到了 1869 毫秒。一点道理都不讲,我认为这种情况不合理,而且观察CPU使用量也不高。找了一大堆资料,试图证明这个 1869 毫秒 不是暂停时间,而只是GC事件的结束时间减去开始时间。

      2、打印GC日志

      既然这些手段不靠谱,那就只有祭出我们的终极手段:打印GC日志。修改启动参数如下

    ‐Xmx4g ‐Xms4g ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50 
    ‐Xloggc:gc.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps

      重新启动,希望这次能排查出问题的原因。 

            

       运行一段时间,又发现了超长的暂停时间。 

      3、分析GC日志

      因为不涉及敏感数据,那么我们把GC日志下载到本地进行分析。定位到这次暂停时间超长的GC事件,关键的信息如下所示: 

                    

       前后的GC事件都很正常,也没发现FullGC或者并发标记周期,但找到了几个可疑的点。

        physical 144145548k(58207948k free) JVM启动时,物理内存 137GB, 空闲内存55GB.

        [Parallel Time: 1861.0 ms, GC Workers: 48] 垃圾收集器工作线程 48个。

        user=1.67 用户线程耗时 1.67秒;

        sys=14.00 系统调用和系统等待时间 14秒;

        real=1.87 secs 实际暂停时间 1.87 秒;

        GC之前,年轻代使用量2GB,堆内存使用量3.6GB, 存活区2MB,可推断出老年代使用量 1.6GB;

        GC之后,年轻代使用量为0,堆内存使用量2GB,存活区254MB, 那么老年代大约1.8GB,那么内存提升量为200MB左右 。

      这样分析之后,可以得出结论:

        年轻代转移暂停,复制了400MB左右的对象,却消耗了1.8秒,系统调用和系统等待的时间达到了14秒。

        JVM看到的物理内存137GB。。。

        推算出JVM看到的CPU内核数量 72个,因为 GC工作线程 72* 5/8 ~= 48个。

      看到这么多的GC工作线程我就开始警惕了,毕竟堆内存才指定了4GB。

      按照一般的CPU和内存资源配比,常见的比例差不多是 4核4GB , 4核8GB 这样的。看看对应的CPU负载监控信息: 

            

       通过和运维同学的沟通,得到这个节点的配置被限制为 4核8GB 。这样一来,GC暂停时间过长的原因就定位到了:K8S的资源隔离和JVM未协调好,导致JVM看见了72个CPU内核,默认的并行GC线程设置为: 72* 5/8 + 3 = 48 个 ,但是K8S限制了这个Pod只能使用4个CPU内核的计算量,致使GC发生时,48个线程在4个CPU核心上发生资源竞争,导致大量的上下文切换。

      处置措施为:限制GC的并行线程数量。事实证明,打印GC日志确实是一个很有用的排查分析方法。

    (六)限制GC的并行线程数量

      下面是新的启动参数配置: 

    ‐Xmx4g ‐Xms4g
    ‐XX:+UseG1GC ‐XX:MaxGCPauseMillis=50 ‐XX:ParallelGCThreads=4
    ‐Xloggc:gc.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps

      这里指定了 ‐XX:ParallelGCThreads=4 , 为什么这么配呢? 我们看看这个参数的说明。

        ‐XX:ParallelGCThreads=n  设置STW阶段的并行 worker 线程数量。如果逻辑处理器小于等于8个,则默认值 n 等于逻辑处理器的数量。如果逻辑处理器大于8个,则默认值 n 等于处理器数量的 5/8+3 。 在大多数情况下都是个比较合理的值。 如果是高配置的 SPARC 系统,则默认值 n 大约等于逻辑处理器数量的 5/16 。

        ‐XX:ConcGCThreads=n  设置并发标记的GC线程数量。 默认值大约是 ParallelGCThreads 的四分之一。一般来说不用指定并发标记的GC线程数量,只用指定并行的即可。

      重新启动之后,看看GC暂停时间指标:

            

       红色箭头所指示的点就是重启的时间点, 可以发现, 暂停时间基本上都处于50ms范围内。后续的监控发现,这个参数确实解决了问题。

      后续经过一段时间的运行,因为数据量爆发,定时任务执行时,每次从数据库加载几百万条数据到内存进行处理。。。

            

    (七)总结

      通过这个案例,我们可以看到,JVM问题排查和性能调优主要基于监控数据来进行。还是那句话: 没有量化,就没有改进 。

      简单汇总一下这里使用到的手段:

        指标监控

        指定JVM启动内存

        指定垃圾收集器

        打印和分析GC日志

      GC和内存是最常见的JVM调优场景, GC的性能维度:

        延迟,GC中影响延迟的主要因素就是暂停时间。

        吞吐量,主要看业务线程消耗的CPU资源百分比,GC占用的部分包括: GC暂停时间,以及高负载情况下并发GC消耗的CPU资源。

        系统容量,主要说的是硬件配置,以及服务能力。

      只要这些方面的指标都能够满足,各种资源占用也保持在合理范围内,就达成了我们的预期。 

     

  • 相关阅读:
    DOM——《JavaScript高级程序设计》笔记
    (转)用js无法获取style样式的问题解析与解决方法
    【2】可视化格式模型、背景、链接、表格表单——《精通CSS‘》
    安装sql server 2008 R2,提示错误:此计算机上安装了 Microsoft Visual Studio 2008 的早期版本。请在安装 SQL Server 2008 前将 Microsoft Visual Studio 2008 升级到 SP1。
    C#读取Word表格中的数据 (转)
    C#在Word文档指定位置处理表格
    c#操作word书签
    c# 操作word的
    函数参数压栈顺序2
    可变长参数
  • 原文地址:https://www.cnblogs.com/liconglong/p/16226971.html
Copyright © 2020-2023  润新知