线上故障之-CPU飙高

线上故障之-CPU飙高

  • 序列化问题引起的应用服务CPU飙高
    • 序列化问题引起的应用服务CPU飙高
  • FULL GC引起的应用服务CPU飙高
    • gc问题导致调用端出现RpcException问题排查
    • 批处理数据过大引起的应用服务CPU飙高
      • 操作步骤
      • 结论
      • 优化方案
      • 问题引申
        • parallelStream()最佳实践
  • 非标导入引发CPU彪高
    • 背景
    • 排查思路
    • 问题再现
    • 改善措施
  • 慢sql引起的数据库服务器CPU飙高
    • 一、摘要
    • 二、事故背景
    • 三、事故处理过程

序列化问题引起的应用服务CPU飙高

CPU飙高的现象很常见,但其实发现和解决起来并不是特别复杂,此处列举一些常见的CPU飙高案例,并给出解决方案和相关故障排查解决过程。
分析之前,复习几个知识点:

CPU性能指标:
load average:负载,linux查看的时候,通常显示如下:
线上故障之-CPU飙高_第1张图片
代表了系统1分钟,5分钟,15分钟平均负载。
形象的类比可参考Understanding Linux CPU Load - when should you be worried? | Scout APM Blog
另一个形象地比喻:CPU的load和使用率傻傻分不清 - 昀溪 - 博客园 (cnblogs.com)
线上故障之-CPU飙高_第2张图片
上图1个电话亭可以理解为一个CPU核心。从上图的过程中可以看到load的概念,而使用率始终100%。
使用率
%Cpu(s):用户空间占用CPU百分比
%CPU:上次更新到现在的CPU时间占用百分比
线上故障之-CPU飙高_第3张图片
查看
cat /proc/cpuinfo

grep -c ‘model name’ /proc/cpuinfo

实验1:观察CPU使用率

 public static void main(String[] args) {
        try {
            //模拟CPU占用率
            cpu50(Integer.parseInt(args[0]));
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }

    public static void cpu50(int num) {
        for (int i = 0; i < num; i++) {
            new Thread(() -> {
                while (true) {
                    long time_start;

                    int fulltime = 100;
                    int runtime = 50;
                    while (true) {
                        time_start = System.currentTimeMillis();
                        while ((System.currentTimeMillis() - time_start) < runtime) {
                        }
                        try {
                            Thread.sleep(fulltime - runtime);
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                }
            }).start();
        }


    }
启动:java -jar 2_cpu-0.0.1-SNAPSHOT.jar 8 > log.file 2>&1 &

实验2:定位CPU标高

方法1:
 1-启动:java -jar 
 2_cpu-0.0.1-SNAPSHOT.jar 8 > log.file 2>&1 & 2-一般来说,应用服务器通常只部署了java应用,可以top一下先确认,是否是java应用导致的:命令:top 
 3-如果是,查看java进场ID,命令:jps -l 
 4-找出该进程内最好非CPU的线程,命令:top -Hp pid 25128 
 5-将线程ID转化为16进制,命令:printf "%x\n" 线程ID 623c 25148 
 6-导出java堆栈信息,根据上一步的线程ID查找结果:命令: jstack 11976 >stack.txt grep 2ed7 stack.txt -A 20 

方法2: 
在线工具:https://gceasy.io/ft-index.jsp 
1-方法1中导出的对快照文件,上传到该网站即可

序列化问题引起的应用服务CPU飙高

1.发现问题
7.8日 17:09监控大盘发现xxx-web的CPU快跑满,导致机器不断扩容增加机器。
平均负载图如下:
线上故障之-CPU飙高_第4张图片
CPU利用率:

2.分析阶段
根据对线程dump文件进行分析,主要是否存在死锁、阻塞现象,以及CPU线程的占用情况,分析
工具采用在线fastThread工具。地址:https://gceasy.io/ft-index.jsp
(1)查看线程数情况
数据图示可知,创建的线程等待线程有3000,提示高线程数可能导致内存泄露异常,从而可能影响后面任务的创建线程。
线上故障之-CPU飙高_第5张图片
(2)查看当前CPU线程使用情况
根据CPU线程情况,查询CPU正在执行的线程堆栈列表,可以发现大部分日志都是类似于:catalina-exec-879
线上故障之-CPU飙高_第6张图片
(3)定位出现问题的线程堆栈
查看是新版头像圈的一段代码逻辑,其中有个步骤需要深拷贝对象,以便以后逻辑更改使用。
线上故障之-CPU飙高_第7张图片
3.问题恢复
从而猜测可能是跟8号开放一批白名单规则用户有关,所以临时采用更改config的白名单策略配置,降低灰度用户范围。通过配置推送后,CPU利用率恢复正常情况。
调整后机器CPU利用率:
线上故障之-CPU飙高_第8张图片
服务池平均CPU利用率情况如下:

4.相关代码:

public Map<String, TopHeadInfoV3> getTopHeadInfoGroupByLiveIdCache(){
        Map<String, TopHeadInfoV3> topHeadInfoGroupByLiveIdMap =
                topHeadInfoGroupByLiveIdCache.getUnchecked("top_head_info_group_by_live_id");
        if (MapUtils.isEmpty(topHeadInfoGroupByLiveIdMap)) {
            return Collections.emptyMap();
        }
// guava cache 对象对外不可⻅,防⽌上游对cache⾥对象进⾏修改,并发场景下出现问题 
        Map<String, TopHeadInfoV3> topHeadInfoGroupByLiveIdMapDeepCopy =
                Maps.newHashMapWithExpectedSize(topHeadInfoGroupByLiveIdMap.size());
        for (Map.Entry<String, TopHeadInfoV3> entry : topHeadInfoGroupByLiveIdMap.entrySet()) {
            topHeadInfoGroupByLiveIdMapDeepCopy.put(entry.getKey(),
                    SerializationUtils.clone(entry.getValue()));
        }
        return topHeadInfoGroupByLiveIdMapDeepCopy;
    }

其中影响性能的问题方法是apache commongs工具包提供的对象克隆工具:
SerializationUtils.clone(entry.getValue())),基本操作就是利用ObjectInputStream和ObjectOutputSTream进行,先序列化再发序列化。频繁克隆且耗时较长,导致占用其他任务的执行。

 public static <T extends Serializable> T clone(final T object) {
        if (object == null) {
            return null;
        }
        final byte[] objectData = serialize(object);
        final ByteArrayInputStream bais = new ByteArrayInputStream(objectData);

        try (ClassLoaderAwareObjectInputStream in = new ClassLoaderAwareObjectInputStream(bais,
                object.getClass().getClassLoader())) {
            /*
             * when we serialize and deserialize an object,
             * it is reasonable to assume the deserialized object
             * is of the same type as the original serialized object
             */
            @SuppressWarnings("unchecked") // see above
            final T readObject = (T) in.readObject();
            return readObject;

        } catch (final ClassNotFoundException ex) {
            throw new SerializationException("ClassNotFoundException while reading cloned object data", ex);
        } catch (final IOException ex) {
            throw new SerializationException("IOException while reading or closing cloned object data", ex);
        }
    }

源代码注释:
使用序列化来深度克隆一个对象。 这比在你的对象图中的所有对象上手工编写克隆方法要慢很多倍。然而,对于复杂的对象图,或者那些不支持深度克隆的对象,这可以是一个简单的替代实现。当然,所有的对象必须是可序列化的。

5.优化方案
经过讨论临时采用创建对象和属性设置的方式进行对象复制,先不采用对象序列化工具。实现java.lang.Cloneable接口并实现clone方法。
主要对象拷贝代码如下:

@Override 
public TopHeadInfoV3 clone() {
    Object object = null; 
try {
        object = super.clone();
        } catch (CloneNotSupportedException e) {
        return null;
        }
        TopHeadInfoV3 topHeadInfoV3 = (TopHeadInfoV3) object;
        topHeadInfoV3.playEnums = Sets.newHashSet(topHeadInfoV3.playEnums);
        topHeadInfoV3.recallTypeEnums = Sets.newHashSet(topHeadInfoV3.recallTypeEnums);
        topHeadInfoV3.behaviorEnums = Sets.newHashSet(topHeadInfoV3.behaviorEnums);
        topHeadInfoV3.micLinkUserList =
        topHeadInfoV3.micLinkUserList.stream().map(MicLinkUser::clone).collect(Collectors.toList());
        return topHeadInfoV3;
        }

6.上线情况
优化方案上线,13号下午两点全量后,看监控大盘cpu利用率为正常情况,cpu利用率如下:
线上故障之-CPU飙高_第9张图片
7.问题总结
针对相对大流量的接口可以提前做好压测分析;
上线后定期通过监控大盘查看线上运行情况,留意机器监控告警便于及时发现问题;
若告警或大盘发现问题CPU或内存使用情况异常,其中可以打印线程堆栈日志,通过堆栈分析工具帮助分析线程使用的情况。
序列化参考:Home · eishay/jvm-serializers Wiki · GitHub

FULL GC引起的应用服务CPU飙高

报表worker-CPU使用率过高【618备战】
一、排查过程
1:查看机器监控,初步判断可能有耗CPU的线程
线上故障之-CPU飙高_第10张图片
2:导出jstat信息,发现JVM老年代占用过高(达到97%),Full-GC频率超高,FULL-GC总共占用了36小时。初步定位是频繁FULL-GC导致CPU负载过高。
线上故障之-CPU飙高_第11张图片
3:使用jmap –histo导出堆概要信息,发现有个超大的HashMap。
线上故障之-CPU飙高_第12张图片
4:使用jmap –dump导出堆。得出hashMap中的KEY是运单号
线上故障之-CPU飙高_第13张图片
线上故障之-CPU飙高_第14张图片
二、总结
1:使用缓存时要做容量估算,并考虑数据增长率
2:缓存要有过期时间。

gc问题导致调用端出现RpcException问题排查

场景问题案例解读:
1、该应用上线弹性数据库后,调用端通过接口查询历史库应用服务时,出现大面积
RpcException,如下图所示

2、观察该应用,full gc 情况,如下图所示,会出现高频full gc 情况
线上故障之-CPU飙高_第15张图片
3、观察应用,young gc情况,如下图所示
线上故障之-CPU飙高_第16张图片
4、查看jvm配置参数时,配置内容如下(可以通过ump、应用配置、堡垒机打印应用信息等方式查看)

-Xss512k 
-Xmn2048m 
-XX:+UseConcMarkSweepGC 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:+CMSParallelRemarkEnabled 
-XX:+CMSClassUnloadingEnabled 
-XX:CMSInitiatingOccupancyFraction=60 
-XX:CMSInitiatingPermOccupancyFraction=60 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintClassHistogram 
-Xloggc:/export/Logs/jvm/gc.log

5、通过jstat命令打印内存情况如下图所示

命令:jstat -gcutil pid
S0 S1 E O P YGC YGCT FGC FGCT GCT
4.26 0.00 71.92 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 72.17 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 72.43 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 73.08 28.36 59.99 3374 427.474 599 1071.941 1499.415
4.26 0.00 73.09 28.36 59.99 3374 427.474 599 1071.941 1499.415

6、之前我们已经配置了jvm参数来打印gc日志,如下所示

66289.394: [GC66289.394: [ParNew: 1685902K->8995K(1887488K), 0.1182020 secs] 2289213K- 
>612400K(3984640K), 0.1188790 secs] [Times: user=0.45 sys=0.01, real=0.12 secs] 
66312.916: [GC66312.916: [ParNew: 1686819K->8220K(1887488K), 0.1287130 secs] 2290224K- 
>611674K(3984640K), 0.1296130 secs] [Times: user=0.48 sys=0.02, real=0.13 secs] 
66317.050: [GC [1 CMS-initial-mark: 603454K(2097152K)] 884210K(3984640K), 0.1249350 secs] [Times: 
user=0.13 sys=0.01, real=0.12 secs] 
66317.176: [CMS-concurrent-mark-start] 
66317.567: [CMS-concurrent-mark: 0.391/0.391 secs] [Times: user=1.45 sys=0.06, real=0.40 secs] 
66317.567: [CMS-concurrent-preclean-start] 
66317.586: [CMS-concurrent-preclean: 0.017/0.018 secs] [Times: user=0.02 sys=0.00, real=0.01 
secs] 
66317.586: [CMS-concurrent-abortable-preclean-start] 
CMS: abort preclean due to time 66322.639: [CMS-concurrent-abortable-preclean: 3.043/5.053 secs] 
[Times: user=3.36 sys=0.32, real=5.06 secs] 
66322.643: [GC[YG occupancy: 525674 K (1887488 K)]66322.643: [Rescan (parallel) , 2.3838210 
secs]66325.027: [weak refs processing, 0.0014400 secs]66325.029: [class unloading, 0.0305350 
secs]66325.059: [scrub symbol table, 0.0141910 secs]66325.074: [scrub string table, 0.0032960 
secs] [1 CMS-remark: 603454K(2097152K)] 1129128K(3984640K), 2.4410070 secs] [Times: user=9.16 
sys=0.40, real=2.44 secs] 
66325.085: [CMS-concurrent-sweep-start] 
66325.444: [CMS-concurrent-sweep: 0.318/0.358 secs] [Times: user=0.51 sys=0.04, real=0.36 secs] 
66325.444: [CMS-concurrent-reset-start] 
66325.450: [CMS-concurrent-reset: 0.006/0.006 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
66347.427: [GC66347.427: [ParNew: 1686044K->11073K(1887488K), 0.1323420 secs] 2269768K- 
>595648K(3984640K), 0.1330670 secs] [Times: user=0.49 sys=0.02, real=0.13 secs]

综上所述:
通过gc日志可以看到CMS remark阶段耗时较长,如果频繁的full gc且remark时间比较长,会导致调用端大面积超时,接下来需要通过jstat命令查看内存情况结合配置的jvm启动参数看一下为啥会频繁的full gc。应用jvm启动参数配置了-XX:CMSInitiatingPermOccupancyFraction=60,持久带使用空间占60%的时候就会触发一次full gc,由于持久带存放的是静态文件,持久带一般情况下对垃圾回收没有显著影响。所以可考虑去掉该配置项。
解决方案:
持久带用于存放静态文件,如今Java类、方法等, 持久代一般情况下对垃圾回收没有显著影响,应用启动后持久带使用量占比即接近60%,所以可考虑去掉该配置项。
同时增加配置项-XX:+CMSScavengeBeforeRemark,在CMS GC前启动一次young gc,目的在于减少old gen对ygc gen的引用,降低remark时的开销(通过上述案例可观察到,一般CMS的GC耗时 80%都在remark阶段 )
jvm启动参数更正如下:

-Xss512k 
-XX:+UseConcMarkSweepGC 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=80 
-XX:+CMSScavengeBeforeRemark 
-XX:+CMSParallelRemarkEnabled 
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-Xloggc:/export/Logs/jvm/gc.log 
-javaagent:/export/servers/jtrace-agent/pinpoint-bootstrap-1.6.2.jar 
-Dpinpoint.applicationName=afs 
-Dpinpoint.sampling.rate=100

备注:相比有问题版本,去除了配置项:-XX:CMSInitiatingPermOccupancyFraction=60。
-XX:CMSInitiatingOccupancyFraction 修改为80(Concurrent Mark Sweep (CMS)
Collector (oracle.com)),添加配置项-XX:+CMSScavengeBeforeRemark,为了个更好的观察
gc日志,修改时间戳打印格式为-XX:+PrintGCDateStamps
修正后结果如下图所示:
1、调用端调用服务正常,不再出现rpc exception
2、应用young gc情况如下

线上故障之-CPU飙高_第17张图片
3、 应用full gc情况如下
线上故障之-CPU飙高_第18张图片

批处理数据过大引起的应用服务CPU飙高

问题场景
某定时任务job 收到cpu连续(配置的时间是180s)使用超过90%的报警;
问题定位
a) 观察报警中的jvm监控,发现周期性出现即每天8:00,cpu从5%-99%大约持续3分钟左右然后恢复正常,平时cpu使用率较为平稳(排除因为应用发布导致的cpu升高)

b) 任务系统周期性出现很可能是定时执行了大量运算导致的,查看任务系统页面,确实存在一个8点执行的定时任务
c) 分析代码梳理该任务的业务逻辑为:一个兜底的定时的job任务;其中涉及大量复杂运算,现在猜测基本是改任务导致的,那么可以复现一下,确认下。
复现操作很简单:
c1)找一台机器,观察jvm相关监控;观察日志
c2) 修改分片数量为1且指定分片容器ip为监控的容器ip, 点击执行分片,查看分片确认成功后,点击执行一次。通过观察步骤c1)成功复现。
d) 至此基本方向确定是这个任务导致的CPU升高,接下来分析为何CPU升高,以及如何优化问题。
线上故障之-CPU飙高_第19张图片
问题分析

  1. a)也可能是程序运行过程创建大量对象触发GC,GC线程占用CPU过高导致;b)CPU升高原因可能是程序大量运算导致;
  2. 不管是情况a) 还是情况b) 都需要复现问题,观察使用cpu高的线程有哪些,使用jstack命令导出线程栈的信息进行观察,观察每个线程CPU使用率

操作步骤

  1. 预发环境触发任务执行,复现场景。注意:此步骤需要谨慎操作,由于预发和线上是相同的数据库,所以预发环境部署代码需要把相关的操作屏蔽掉了,比如发送MQ,更新数据库等。避免影响线上数据和MQ等。
  2. 登录堡垒机使用top命令查看目前的进程信息
    3.发现java进程33907 使用的cpu已经达到200%,使用命令 top-H -p 33907 命令查看该进程下的哪个线程使用的资源最多
    线上故障之-CPU飙高_第20张图片
    线上故障之-CPU飙高_第21张图片
    线上故障之-CPU飙高_第22张图片
    340271 5312f Curator-TreeCache-18" #648 daemon prio=5 os_prio=0
    tid=0x00007f10dc156000 nid=0x5312f waiting on condition [0x00007f1158bcb000]

结论

  1. ThreadPoolTaskExecutor为该任务显示创建,核心线程数为2,最大为6,队列大小为300,根据线程栈信息可以看出与程序配置一致,根据实际配置可以发现此部分正常配置符合预期。
  2. 程序中没有创建FrokJoinPool,但是程序使用了大量的 parallelStream();由于parallelStream默认使用的是公共的forkJoinPool线程池,且该线程池的线程数量配置为系统的 Runtime.getRuntime().availableProcessors() - 1; 现在对于parallelStream的使用等于在多线程中,嵌套了多线程。
  3. Curator-TreeCache 线程的来源,通过发现线程池的初始化参数可知:线程的的拒绝策略为CallerRunsPolicy(),该策略为:如果线程池队列和核心线程数满了后,继续提交到线程池的任务会通过方法线程即调用线程池的那个方法来执行,可以理解为主线程。

优化方案

  1. 修改所有的parallelStream() 为stream();4c8G配置的机器cpu使用率稳定在50%左右。
    线上故障之-CPU飙高_第23张图片


    线上故障之-CPU飙高_第24张图片
  2. 2C4G的机器执行,CPU使用率仍然高达90%,通过分析线程栈的dump文件发现主要是业务线程占用资源过高,其中不足2%的还有部分是由于并行流导致的,在job执行时候引用的第三方jar包中,此部分暂时不处理。

线上故障之-CPU飙高_第25张图片线上故障之-CPU飙高_第26张图片
对于2C4G优化方案:a) 由于这个任务是兜底的,不需要立即执行完成,且执行频率为1天1次可以将线程池调整为1个线程;
b) 申请容器升级为4c*8G
2C4G配置的执行job单线程跑任务
线上故障之-CPU飙高_第27张图片

问题引申

parallelStream()原理:

  1. parallerlStream是jdk8的特性,是在Stream的基础上实现的并行流式操作;旨在简化并行编码,提升运算效率;
  2. 并行流的底层是基于ForkJoinPool实现的,其中ForkJoinPool采用的思想类似MapReduce的思想,将一个大的运算任务拆分为子任务(fork的过程);然后执行所有的子任务运算后的结果合并在一起(join过程); 通过分治的方式完成一个计算

parallelStream()最佳实践

并行流使用问题分析:

  1. 根据parallelSteam的原理我们知道底层是使用的ForkJoinPool;那么我们程序通常会有如下代码:
//code1 
WORDS.entrySet().parallelStream().sorted((a,b)- 
>b.getValue().compareTo(a.getValue())).collect(Collectors.toList()); 
//code2 
Set<String> words = new ConcurrentHashSet<>(); 
words1.parallelStream().forEach(word -> words.add(word.getText())); 
  1. 那么我们并没有创建ForkJoinPool,且不同的集合都在调用parallerStream(), 那么最终用的是哪个线程池呢?很显然既然没有报错,就说明jdk应该会给一个默认的ForkJoinPool。源码如下:
    注意:默认的如果使用默认的线程池执行的话,forkJoinPool会使用当前系统默认的cpu核心数量-1,但是主线程也会参与计算。
    执行结果: 可以看出默认的ForkJoinPool线程池,除了worker线程参与运算 ,方法线程也会参与预算。
    最佳实践总结:
  2. 并行流如果使用,最好使用自定义的线程池,避免使用默认的线程池即线程池隔离思想,造成阻塞或者资源竞争等问题。
  3. parallelStream 适用的场景是CPU密集型的,假如本身电脑CPU的负载很大,那还到处用并行流,那并不能起到作用,切记不要再paralelSreram操作是中使用IO流;
  4. 不要在多线程中使用parallelStream,如本次案例,大家都抢着CPU是没有提升效果,反而还会加大线程切换开销;
    踩坑记录:
    Runtime.getRuntime().availableProcessors() ;是jdk提供的获取当前系统的可用的核心数,本次踩坑在于,现在多数应用都是发布在容器中的,虽然应用部署的容器是2C4G的,但是ForkJoinPool创建的ForkJoinPool.commonPool-worker-线程却有几十个,登录容器所在的物理机查看机器配置如下:
    线上故障之-CPU飙高_第28张图片
    实际为2个cpu每个cpu 32核 总共是64核,编写测试程序验证也是如此:

由此可知,默认的ForkJoinPool获取的是当前系统的核心数量,如果应用部署在docker容器中,那么就获取的是宿主机的CPU核心数
Runtime.getRuntime().availableProcessors()问题 :
容器明明分配的是2个逻辑核心数,为什么java获取的会是物理机的核心数呢?如何解决这个问题呢。
是否是容器构建的时候某些参数配置的原因导致的? 将问题反馈给运维,但是对方并未解决该问题。
那么java是否可以解决呢?毕竟如果我们自定义线程池设置线程数量也会使用
Runtime.getRuntime().availableProcessors()这个方法,
这其实是JDK的一个问题,已经trace在JDK-8140793,原因是获取CPU核数是通过读取两个环境变量,其中
线上故障之-CPU飙高_第29张图片
其中_SC_NPROCESSORS_CONF 就是我们需要容器真实的CPU数量。
获取CPU数量的源码
第一种办法是使用新版本的Jdku131以上的版本1。
另外一个办法是使用自编译上面的源代码,通过LD_PRLOAD的方式将修改后的so文件加载进去
Mock掉CPU的核数
jdk官方链接声明:Java SE support for Docker CPU and memory limits

使用方法一测试:测试环境容器验证,验证通过,结果如下:
jdk版本 jdk1.8.0_20 :返回cpu核心数28
线上故障之-CPU飙高_第30张图片
jdk版本: jdk-1.8.0_192 返回cpu核心数2
线上故障之-CPU飙高_第31张图片
建议
尽量使用lambda表达式遍历数据,推荐使用常规的for、for-each模式
原因:

  1. 性能比传统foreach低
    lambda内部有着一套复杂的处理机制(反射、类型转换、拷贝),性能开销要比常规for、for-eatch大的多。在普通业务场景下这种性能差异可以忽略不计,但是在某些高频场景下(N万次调用/秒)就不能忽略了。它虽然不会导致一次迭代卡顿,但是会持续增加cpu的消耗,以及增加GC的压力。
  2. 不便于代码调试
    反例:
// lambda并没有起到简化代码的作用,反而会增加系统压力 
List<ValidationResult> results = children.stream() 
.map(e -> e.validate(context, nextCell, nextCoverCells, occupyAreas)) 
.collect(Collectors.toList()); 
List<ValidationResult> failureList = 
results.stream().filter(ValidationResult::isFailure).collect(Collectors.toList()); 
List<ValidationResult> successList = 
results.stream().filter(ValidationResult::isSuccess).collect(Collectors.toList());

正例:

// 优化后总cpu下降2%-4%左右。 
// 备注:cpu下降如此明显的原因是该代码的调用频率非常高,真正的N万次/秒调用 
List<ValidationResult> failureList = new ArrayList<>(4); 
List<ValidationResult> successList = new ArrayList<>(4); 
for (PathPreAllocationValidator child : children) { 
ValidationResult result = child.validate(context, currCell, previousCell, nextCell); 
    if (result.isSuccess()) { 
        successList.add(result); 
    } else {
        failureList.add(result); 
    } 
}

非标导入引发CPU彪高

背景

非标导入使用easyexcel组件进行导入处理,10几万的数据量引发CPU彪高

排查思路

1、查看线程栈相关信息
2、pinpoint监控查看性能及代码调用情况
3、是否存在大量阻塞慢SQL
4、是否存在短时间内频繁日志输出

问题再现

使用之前分表导入的30万数据进行导入操作,myops查看排名前十线程栈相关信息(如下图),发现lbs_non_standard_account_common单个线程消费占用CPU 8%左右,启动的线程数量很多

改善措施

1、AnalysisEventListener的子类CommonImportExcelListener的invoke方法已将近每秒1万的速率迅速完成业务逻辑校验发送MQ,该MQ内部消费进行业务处理逻辑处理。
30万的数据将近30秒内发送MQ完成,给消费内部逻辑带来压力导致CPU彪高,使用RateLimiter进行了MQ发送的限流。
2、调整消费者主题“lbs_top” 的maxConcurrent参数(该参数设置为单组启动线程数),单个应用实例启动线程的最大数=maxConcurrent参数* 分组数量.
3、针对lbs_top消费逻辑中的部分业务查询进行内存缓存改造

慢sql引起的数据库服务器CPU飙高

数据库 cpu 占用率过高事故分析

一、摘要

时间:2018.2.5 18:00:00 --2018.2.6 10:30:00
问题:中台数据库 cpu 占用率超过 89%
原因:系统 DB sql 存在慢查询,并且读写均在主库,随着系统流量的增长, 数据库 cpu 占用持续增高,2 月 5 日当天数据库 cpu 占用率达到了 89%,2 月 6 日 cpu 占 用率达到 91%;

二、事故背景

该系统最近每天超于 30%的量增长,2 月 5 日下午 18:00:00 左右田xx 通过 mdc 监控观察行情数据库的 cpu 占用率达到 89%,而且业务方第二天 2 月 6 日要上 6 个新的产品,预估流量会高于 5 日流量峰值,系统风险非常高。

三、事故处理过程

  1. 和中台应用读写分离方案可行性确认【5 日 20:00:00-22:00:00】
    与金xx同事确认是否可以在应用层实现主从库读写分离,释放主库压力。由于当晚 金xx同事均已下班,并且相关人员未全部参与问题处理中,金xx当晚确认不能再 不改代码的情况下实现数据库的读写分离,若需改代码,当晚不能进行发布升级, 第二日还是存在数据库崩的风险;
    此方案不能解决问题;
  2. 数据库拆分方案讨论及开展【5 日 22:00:00-6 日 03:00:00】
    读写分离方案 pass 后,多次与金xx沟通讨论后,决定将中台数据库拆分到新 的数据库,缓解现用数据压力,防止第二天开市后数据库 cpu 占用率超负荷, 作为临时解决方案。
    方案确定后,与 dba 申请 dokcer 资源数据库,由于现在公司资源池 docker 数 据库紧缺,当前情形只能申请弹性数据库,申请后联系 DBA 修改数据库 为线上相同规格(12C/49G/1T)。
    数据库资源到位后,电话联系 dba 确定切库方案:先停掉应用,防止数据库备 份期间有数据写入(此项与业务方确认,可以操作),dba 同事通过 dump 全 库将中台数据库全量备份,然后拿到新的数据库 source 导入数据数据库全量迁移至新库后,将中台 nginx_bin 的 mysql_config.py 配置文件中 的数据库连接信息替换为新库配置,启动应用;
    经过功能测试验证后无问题,但是由于时间紧急,没有经过压力测试,这也导 致了第二天系统登录出现问题;
  3. 新增行情 BP,设置行情数据库读写分离【2 月 6 日 09:20:00-】
    2 月 6 日上午项目开市前,大概 9 点 20 分左右,出现无法登录问 题;为不影响业务正常运营,
    紧急将昨天的中台配置恢复,大概在 9 点 45 分 左右系统可重新登录;
    操作流程:将中台 nginx_app 的数据库配置文件 mysql_config.py 用昨天原有 配置文件替换,先重启 redis,再重启 nginx;
    由于金xx的行情慢查询问题一直没有解决,业务开市后数据库 cpu 占用率持续 飙升,情况非常紧急。与金xx电话在线同步问题情况,并要求其紧急提供 nginx 测限流方案。
    线上故障之-CPU飙高_第32张图片
    金xx同事评估限流方案在未经过测试的情况下实施可能会对业务造成影响。最后在数据库占用率在 90%以上的情况下增加两个行情 BP,配置读取数据库地 址为从库地址 10.191.237.73 ,缓解主库读压力。新增 BP 配置方式
    增加行情 BP,实现行情数据库读写分离后,数据库主库 cpu 使用率降至 60% 左右,从库 cpu使用率开始上升,大概在 20%左右趋于稳定
    线上故障之-CPU飙高_第33张图片
  4. 增加索引,数据库 cpu 占用率急速下降
    读写分离实现后,cpu 使用率降至正常阈值范围内,肖xx提出 ordwth 表创建 索引的方案,申请创建后,cpu 占用率急速下降至 10%以内 创建索引:alter table ordwth add index idx_security_validate(Security,ValidDate) 增加索引后性能增加原因: 原来的 ordth 表只有(AppID,DateTime)和(TransAcct,SecurityID)这两组组合索 引。
    在这里插入图片描述
    原来查的慢的 sql,where 条件的两组索引没有用到 。
    加上了之后(SecurityID,ValidDate)这组组合索引后 ,查询开始走这组组合索 引。
    在这里插入图片描述
    四、事故反思总结
  5. 与金xx同事沟通不透彻,没有找到对应的问题的接口人,导致 5 日晚未能实施数据 库读写分离方案;
  6. 对弹性数据库了解不足,导致应用连接切换到弹性数据库后,流量增加后,数据库 性能不能支撑线上系统;
  7. 前期没有与金xx同事沟通限流应急方案,导致流量增加后,无法第一时间限制流 量

你可能感兴趣的:(架构,lamda,CPU,序列化,慢sql)