一次线上cpu近200%的问题排查

场景描述:

车端会不断发送有告警的全量can, 通过kafka下发到下游,下游拿到数据,按照can标识分别处理每个can标识中的每个故障,多得can标识有大几十,少的也有近10个,区分1,2,3级别,普通级别,并且1,2,3告警还要下发给推送kafka,推送状态给用户。

流向如下:

 这里有很多,我想的是,在数据流量在大,消费能力达不到的时候就横向在扩展告警服务,但是告警服务把大量的访问都转入了redis,而减轻了mysql压力,所以Redis也要根据每秒访问量做横向集群扩展,这里由于最终的落地告警才500万条,就还用的mysql,这边随数据量的增大,可以做分库分表,也可以直接上大数据分布式存储Hbase+ ES,es做统计查询,Hbase存放具体告警信息。  不懂大数据哈,不知道实时/离线应该用哪些方案,后面也要加入学习,就知道Flink-es/redis,离线Hive啥的。有建议的可以评论讨论哈。

这次是几个服务cpu都升到100以上了,然后看redis的访问也都50以上,看看,车辆销售信息,最近是加了不少车,应该是Kakfa消费的数据变多了。

步骤一 : 定位选择一个服务器,由于docker容器部署的

docker stats 查看容器占用cpu量

然后定位cpu高的容器,负责容器id

docker exec -it 容器id /bin/bash

进入容器

这里有个插曲,就是这个容器部署之前人竟然打了一个简易的Openjdk,不能执行jstack等命令查看线程状态,咋弄,重新打一个呗。

再次进入docker容器,可以top   --> top -Hp 进程id  --> printf %x线程id --> jstack 进程 

jstack pId | grep  ox线程转六进制  -A 行数

 这里定位是lettuce-epollEventLoop,  源码定位

这个类是netty中的一个单线程池类,应该是redis做命令操作,远程连接用的lettuce线程池。

netty的线程池概念和jdk的线程池概念不太一样。netty的线程池其实是一个事件循环组(MultithreadEventLoopGroup),每个线程实际上是一个事件循环(SingleThreadEventLoop),每个事件循环的线程启动之后,会开启一个死循环不断拿事件队列的事件去消费。

这边可以理解成A线程还在执行,大量的BCDED线程都已经进来了,越多,cpu就会占用高。

是不是觉得麻烦,可以使用阿里的arthes直接看占比高的线程:

进入docke容器/如果在容器构建就带有arthes就不需要下面啦。

执行:curl -O https://arthas.aliyun.com/arthas-boot.jar 
          java -jar arthas-boot.jar

选择我们观察的进程  1也就是服务进程

thread查看线程的使用情况:

 这里看到前3个挺高的,这还是一轮优化后的,之前的都占到70%

查看详细

发现和jstack命令差不多,第一下称都是lettuce-epollEventLoop netty下的一个但线程池在等待线程,细心的会发现有lettuce-eventExecutorLoop-x-x的线程都处于WAITING状态。

 另一个线程高的,是kafka中消费对象获取消息的地方,说明消费对象cpu处理过高,导致了消费能力下降。

 线程是又redis中lettuce下发命令netty线程池发出的,这里初步定位是redis的问题。

查看当前redis的并发度

进入redis命令执行info  这里推荐大家使用quickRedis这个redis数据查看工具,也支持直接输出命令操作

 输入info 查看Stats 看到对应的

instantaneous_ops_per_sec:228800 纳尼,这也太高了,每秒处理命令

然后每个redis的cpu都70左右,单机redis最大QPS是10万,70%接近100%,100%那服务不就挂了嘛。

然后在去其他几台部署消费者的docker中看cpu占用一下,确定是redis的瓶颈问题。

安心的处理方法,cpu,一些指标整理一下找经理申请在部署1台redis。

但是   哈哈,但是代码不是自己写的,就想看看咋就那么多。

代码逻辑就像第一流程图一样。

第二步:定位问题点,去看代码

1. 方向肯定是看操作redis的地方,发现一个方法

handleAlarm 有大量的地方调用,原因是告警的类型太多

 这个方法,明显进来一次要调用2次hasKey,而且有大量的线程要执行这方法,普通的业务这个可能还没有发现,这次是redis有瓶颈,刚好想到代码有问题,就把hasKey提取出来,做一次查询定义变量,放到下面2个判断中,这样不相当于减少一半的redis命令执行。

hasKey源码简单看了,是要有连接redis和释放redis的操作的,没有深挖,按照上面thread中排名第一个的leetuce-epoll ,推断会由netty创建很多的连接线程(这里是个人判断,没有看到后面的源码,不要喷哈)数据量小还好,一大这个地方就会放大。

重新部署,cpu下降确实了近一半在100左右徘徊。

继续看代码:有一块看着不爽的代码,我就给优化了,一开始不知道是不是这里的问题。

1. kafka接受can数据,并解析,减少传输,kafka中发送的都是一串字符串,里面每个byte都有自己意思,这里调用内部开发的解析程序,去解析没有字符串,并分析告警信息

 通过接受数据发现,这里是所有can的标识都发送过来了,但是有告警的目前只有10几个,全部近100个,每个can标识还有10-100不等的告警类型。告警类型还有1,2,3等级

原始的代码是for循环3次 第一次处理1级 第二次处理二级,第三次处理三级

代码结构就是  1. 处理每个级别的告警

 2. 1级告警处理10个can,一开始是通过swtich case对应到满足的can就进入执行当前can的所有告警

 3. 单个can里面还有大量的告警类型处理

 看到每个告警类型都会handleAlarm也就是hasKey的那个方法

这给的代码只是1级告警的1个can几个告警类型的处理

一共有3个告警级别 10几个can 每个can有1-100左右的告警类型   在加上几万台车大约10秒上报一次数据。

就导致handleAlarm的方法有很多的线程,而主要是卡在redis的处理能力上。

就这段代码,别的不说,这么多for循环swtich case 太雷人了。

优化点1:和写解析的同时沟通了一下,只传来10几个告警的can数据过来,也就是for循环不满足的直接跳出,避免了无辜的1个消息大几十次空循环。而上线1秒能产生2-3万条消息(高峰期)。

优化点2:去掉for swtich case 采用策略/模板模式,将10几个can,写成10几个处理类,各自处理各自can的告警类型。

核心代码如下:

/**
 * 策略接口
 *
 * @author wuhaiming
 */
public interface AlarmService {

    /**
     * 分析告警方法
     * @param data
     * @param vin
     * @param alarmTime
     * @param position
     */
    void analysisAlarm(DataPackBe11CanData data, String vin, long alarmTime, String position);
}

2 公共实现类,并提取公共方法,比如handleAlarm等

/**
 * 创建抽象模板类,实现策略接口,并抽取公共行为逻辑
 *
 * @author wuhaiming
 */
@Slf4j
@Service
public class AlarmPublicServiceImpl implements AlarmService {

   hadnleAlarm()...
   B()..

       /**
     * 处理告警方法
     * @param data 数据
     * @param vin 车架号
     * @param alarmTime 告警时间
     * @param position 位置
     */
    @Override
    public void analysisAlarm(DataPackBe11CanData data, String vin, long alarmTime, String position) {

    }
}

3. 实现各自子类

代码结构如下

 其中一个的代码示例

@Slf4j
@Component("Handle3E5Alarm")
public class Handle3E5AlarmServiceImpl extends AlarmPublicServiceImpl {

    @Override
    public void analysisAlarm(DataPackBe11CanData data, String vin, long alarmTime, String position) {
        // 1级告警
        handle3E5AlarmByLevel(data, vin, alarmTime, position, LEVEL_ONE);
        // 2级告警
        handle3E5AlarmByLevel(data, vin, alarmTime, position, LEVEL_TWO);
        // 3级告警
        handle3E5AlarmByLevel(data, vin, alarmTime, position, LEVEL_THREE);
        DataPackBe11Id3E5 value = ((DataPackBe11Id3E5) data);
        //处理特殊1级告警
        handle3E5Level1Alarm(value, vin, alarmTime, position);
        //处理特殊2级告警
        handle3E5Level2Alarm(value, vin, alarmTime, position);
        //处理特殊3级告警
        handle3E5Level3Alarm(value, vin, alarmTime, position);
    }

 注意@Component("Handle3E5Alarm"),每个子类都命名不一样(很重要)

提取了一个枚举类去记录

public enum AlarmType {

    ALARM_1F7("1F7", "Handle1F7Alarm"),
    ALARM_200("200", "Handle200Alarm"),
    ALARM_246("246", "Handle246Alarm"),
    ALARM_297("297", "Handle297Alarm"),
    ALARM_311("311", "Handle311Alarm"),
    ALARM_3E5("3E5", "Handle3E5Alarm"),
    ALARM_117("117", "Handle117Alarm"),
    ALARM_248("248", "Handle248Alarm"),
    ALARM_104("104", "Handle104Alarm"),
    ALARM_118("118", "Handle118Alarm"),
    ;
    /**
     * can标记
     */
    private final String canKey;
    /**
     * 告警实现类
     */
    private final String alarmMethod;

    /**
     * 通过code获取alarmMethod
     *
     * @param canKey
     * @return
     */
    public static String getAlarmMethodByCanKey(String canKey) {
        for (AlarmType alarmType : AlarmType.values()) {
            if (alarmType.getCanKey().equals(canKey)) {
                return alarmType.getAlarmMethod();
            }
        }
        return null;
    }

4. 策略访问中心

@Slf4j
@Service
public class AnalyseContext {

    @Autowired
    private final Map<String, AlarmService> strategyMap = new HashMap<>();

    public void analysisAlarm(String canId, DataPackBe11CanData data, String vin, long alarmTime, String position) {
        String alarmMethodByCanKey = AlarmType.getAlarmMethodByCanKey(canId);
        if (alarmMethodByCanKey == null) {
            return;
        }
        if (strategyMap.get(alarmMethodByCanKey) != null) {
            strategyMap.get(alarmMethodByCanKey).analysisAlarm(data, vin, alarmTime, position);
        } else {
            log.info("没有找到canId为:{} 对应的实现类", canId);
        }
    }
}

注意这里的strategyMap是加上@Autowired的,这个常量在spring启动后会自动把实现AlarmService的子类都按照@Component("Handle3E5Alarm")配置的方法名称,加入到strategyMap中。这里通过传来的canId 获取对应的告警类,通过告警类型名找到对应的告警实现类,执行analysusAlarm,也就是strategyMap.get(alarmMethodByCanKey).analysisAlarm逻辑。

这里就执行到对应can的所有告警,看起来代码清晰点,如果新增can类型,就新增一个子类去是实现内部的告警逻辑。

调用的地方直接注入AnalyseContext,传入不同的canId就行。

然后在陆陆续续的把5台服务部署起来,发现有10%左右的cpu下降,还是有一定的效果的,至少减少了很多次的无效循环。

最终5台服务器cpu固定在50%左右,由于内部有大量的计算和访问redis,所以cpu还是占用比较高但是,redis的cpu也下降到30左右。kafka消费组也正常消费,没有影响业务。

经过这次线程排查,能定位到代码问题,做出调整,看到了cpu的大幅下降,还是挺开心的。

由于排查发现了很多底层的代码,终于明白了为什么要看源码,至少看到问题点爆出的标识,知道是哪个底层源码,定位到哪个框架导致的,能快速反应是哪里的问题。

      不足,1.  对redis,kafka原理和常见问题还要积累和学习  2. 线程排查问题的方式记录,收集和学习跟多排查问题的方式   3 提升自己代码能力,优雅的设计自己的代码。

      最后这个是我的一次线程调优的记录,终结就是线程线程占比高的排查,定位问题,定位是业务代码,还是框架瓶颈。定位到业务代码,做相应的代码优化。 

     最后给大家推荐2个远程登入服务器的工具,1个redis操作工具QuickRedis

1.MobaXterm_Personal  页面友好、方便快捷直接运行,但是免费的只能保存10个,刚开始都用的。

2. Termius 页面友好,支持多个分组服务器管理,也挺快的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值