系统Cpu利用率降低改造之路

系统Cpu利用率降低改造之路

一.背景

1.1 系统背景

该系统是一个专门爬取第三方数据的高并发系统,该系统单台机器以每秒700次的频次查询第三方数据,并回推给内部第三方系统。从应用类型上看属于IO密集型应用,为了提高系统的吞吐量和并发,我们引入了协程(Quasar框架)。但是由于业务的特殊性,高峰期的时候我们仍然需要管理快300台机器,抛开单台机器每个月成本不算,光是通过后台管理系统更改快300台机器的配置和将机器出局Ip加入到第三方代理提供商的白名单中这一过程就比较繁琐,且容易出现问题。为了解决这一历史遗留问题降低机器成本,以及提高运维效率,就有了下面的改造之路。

1.2 知识背景

1.2.1 什么是平均负载

平均负载是指单位时间内,处于可运行状态和不可中断状态的进程数。它不仅包括了正在使用 CPU 的进程,还包括等待 CPU等待 I/O 的进程。

1.2.2 什么是CPU使用率

CPU 使用率,是单位时间内 CPU 繁忙情况的统计。

1.2.3 平均负载和CPU使用率的关系

  • CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
  • I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
  • 大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。

二.过程

2.1 发现问题

通过监控发现我们的系统单台机器(8C,16G)的CPU利用率竟然维持在80%-90%左右,负载也接近过载
在这里插入图片描述
在这里插入图片描述
由于我们的系统是IO密集型应用,理论上CPU利用率不可能这么高,所以就需要分析为什么CPU利用率高,因为CPU利用率过高会间接的导致CPU负载过高,导致CPU队列中的任务调度不过来,从而出现吞吐量降低,扫描次数下降等问题。

2.2 Arthas工具使用

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

登录应用所在服务器

**安装与启动arthas

3.1 输入命令切换用户: sudo -Esu deploy (切换到deploy用户,就是启动应用的用户)
3.2 切到logs文 件夹:cd /opt/logs/
3.3 下载arthas执行文件curl -O https://arthas.aliyun.com/arthas-boot.jar
3.4 启动arthas:java -jar arthas-boot.jar
在这里插入图片描述

3.5 使用命令 thread -n 8 指定最忙的前 8个线程并打印堆栈

2.1 痛点一:日志打印过多,存在大量序列化

通过Arthasthread -n 8 命令我们发现打印日志时存在大量序列化,由于系统并发高且返回报文较大,一分钟能产生大概1G的日志,由此我们采取了抽样打印日志的方式,以及将输入到CLog的日志不采取序列化的方式,而是直接调用对象的toString方法直接转换成String输入。

2.2 痛点二: 代码中存在常量字符串频繁反序列化成对象操作

通过Arthasthread -n 8 发现有些类中定义一个静态成员变量,每次调用方法都将该成员变量反序列化成一个对象,在并发高的场景下,调用次数过多,显而易见会产生性能问题。下面只列举其中一个,在每次调用test方法时,都将SPECIAL_SEAT_NAME_MAPPING这个常量反序列化成一个Map,并且每次都会将Mapvalue切割这样显而易见会产生性能开销,下面是更改前的示例代码。

public class Test {
    private static final String SPECIAL_SEAT_NAME_MAPPING = "{\"3\": \"硬卧|二等卧\", \"J\": \"二等卧|硬卧\",\"4\": \"软卧|一等卧\", \"I\": \"一等卧|软卧\"}";
    
    private void test(SeatInventory seat, String seatTypes) {
        if (StringUtils.isNotBlank(seatTypes)) {
            HashSet<String> seatTypeList = Sets.newHashSet(seatTypes.split(""));
            Map<String, Object> specialSeatNameMap = JacksonUtils.toMap(SPECIAL_SEAT_NAME_MAPPING);
            for (String seatCode : seatTypeList) {
                String special_name = String.valueOf(specialSeatNameMap.get(seatCode));
                String oldName = seat.getSeat_name();
                if (StringUtils.contains(special_name, oldName) && !StringUtils.startsWith(special_name, oldName)) {
                  seat.setSeat_name(Splitter.onPattern("\\|").splitToList(special_name).get(0));
                }
            }
        }

    }
}

更改后代码如下:将反序列化操作前置,避免每次都反序列化对象,并将其中对value进行切割的操作也前置,避免每次都切割,以此来减少性能开销

public class Test {
    private static final String SPECIAL_SEAT_NAME_MAPPING = "{\"3\": \"硬卧|二等卧\", \"J\": \"二等卧|硬卧\",\"4\": \"软卧|一等卧\", \"I\": \"一等卧|软卧\"}";
    public static final Map<String, List<String>> specialSeatNameMap = new ConcurrentHashMap<>();

    static {
        Map<String, Object> result = JacksonUtils.toMap(SPECIAL_SEAT_NAME_MAPPING);
        for (Map.Entry<String, Object> entry : result.entrySet()) {
            String specialName = String.valueOf(entry.getValue());
            List<String> specialNameList = Splitter.onPattern("\\|").splitToList(specialName);
            specialSeatNameMap.put(entry.getKey(), specialNameList);
        }
    }

    private void test2(SeatInventory seat, String seatTypes) {
        if (StringUtils.isNotBlank(seatTypes)) {
            HashSet<String> seatTypeList = Sets.newHashSet(seatTypes.split(""));
            for (String seatCode : seatTypeList) {
                List<String> specialNameList = specialSeatNameMap.get(seatCode);
                String oldName = seat.getSeat_name();
                if (CollectionUtils.isNotEmpty(specialNameList) && specialNameList.contains(oldName) && !Objects.equals(specialNameList.get(0),oldName)) {
                    seat.setSeat_name(specialNameList.get(0));
                }
            }
        }
    }
}

2.3 痛点三 :压缩回推业务数据导致CPU高

由于系统原先部署在公司外部,回推数据的报文较大且是外网和内网交互,当时带宽不足,可能会出现数据丢失现象,所以当时做了数据压缩功能。目前系统已迁入公司内部,内网之间交互无需考虑带宽的影响,经过验证可以关闭压缩功能。

2.4 痛点四: 系统内存在大量深拷贝(通过序列化和反序列化方式实现)

通过Arthas命令我们发现,系统内存在大量JacksonUtils.toBeanJacksonUtils.toList调用来实现深拷贝功能,查看代码我们发现由于数据只有一份,会对这一份数据进行过滤并更改数据中的某些字段的值,所以需要拷贝两份完全一样的数据,来达到更改数据互不影响对方的功能。当然在并发这么高的系统,频繁的通过序列化反序列化的方式来实现深拷贝明显是不合理的,下图是Arthas打印的Cpu利用率占用较高的堆栈信息

在这里插入图片描述在这里插入图片描述
调研了几种能实现拷贝的工具spring的BeanUtils.copyProperties,Apache BeanUtils.copyProperties,发现都存在很多坑,对于List的拷贝是属于浅拷贝,需要自己写兼容逻辑才能实现深拷贝,并且底层采用反射的方式,从一定程度上来讲,采用反射的方式在并发高的情况下也会出现性能问题,不仅不能解决目前的问题,可能还要引发其他问题,对此我们采用了最原始的方式,自己定义一个专门用于深拷贝的工具类,采用new 对象的方式来实现深拷贝。

三.结果

经过上面的优化,效果是显而易见的,我们系统的CPU使用率从原先的80%-90%左右直接下降到了30%左右,负载也由原来的接近过载降到了原先的三分之一,我们发现原先我们一台机器开600并发去干活会出现瓶颈,按照优化之后理论上我们可以单台机器开1800的并发去干活,这样就可以省下2台机器的费用。

3.1 优化前后CPU利用率对比图和优化后负载情况

在这里插入图片描述
在这里插入图片描述

3.3 增大并发后CPU利用率和负载情况

在这里插入图片描述在这里插入图片描述

从上图可知,当我们将并发增大至原先的三倍,CPU利用率甚至比原来的还要低,负载也比原先的低,说明我们的优化是有效果的,查询第三方系统的次数从原先单台机器的每秒700次变成了每秒2100多次,平峰期我们日常需要维护90台机器可以直接缩减成30台,高峰期维护的300台机器可以直接缩减成100台,每年可以直接为公司省去机器成本几十万

四.总结

在日常编码中,我们不仅要注重业务代码的实现,还需要考虑到系统的性能问题,要学会发现问题,并解决问题。对个人能力来说不仅是一个很大的提升,在一定程度上也能为公司带来效益。

  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值