场景描述:
车端会不断发送有告警的全量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 页面友好,支持多个分组服务器管理,也挺快的。