一、问题介绍
线上服务A上线后,一切正常。等到晚上八点左右,服务A开始报警,很多接口出现超时的问题,因为降级做的不好,进而引起连锁反应,导致很多其他服务也开始出现超时,不可用等报警。
二、问题原因
服务A上线是为了修复一个异步发送PUSH的问题,最初的问题是发现线上PUSH很多没有发出,初步定位的是异步线程没有执行,于是同事把异步改成同步调用,修复上线了,并线上验证通过。
在晚上八点左右,有一个JOB批量的调用这个发送PUSH的接口,接口是同步HTTP调用第三方接口,同时HttpClient没有设置超时时间,而第三方接口的响应时间较长,于是很多线程阻塞在HTTP调用上,又因为接口量调用较大,导致Pigeon线程池打满,该接口出现大面积超时。
三、分析
这个事故发生根本原因是两个问题:
- 对Pigeon线程模型没有深入了解,把会长时间阻塞线程的任务放在同步线程池中执行
- HttpClient不设置超时时间,第三方接口响应时间不确定。
Pigeon是一个类似dubbo的服务化框架,支持远程调用,服务注册,服务发现,负载均衡等功能。
他的底层网络处理依赖Netty,所以分析Pigeon的线程模型,需要从Pigeon和Netty两个方面来看。
3.1、Netty线程模型
Netty是一款NIO框架,它封装了JAVA NIO ,帮助我们解决了很多的网络处理的底层问题,提供出易用的API,方便我们快速的开发出NIO的程序。
Netty的采用了Reactor模型,事件驱动。

其中Accept Pool 只负责处理新的连接请求和一些简单的非业务逻辑,比如权限认证,登录等。
IO Pool 负责处理channel的读写,编码解码和业务逻辑
我们再看一下Netty是如何对应这种模式的:

服务端启动时新建了两个EventLoopGroup ,其中bossGroup就对应上图的Accept Pool ,workerGroup对应IO Pool。
启动时会随机从workerGroup选择一个NIOEventLoop作为Accept Thread 处理所有的网络连接,连接成功后,会把新建的socketChannel 扔给workerGroup,workerGroup会随机选择一个NIOEventLoop来负责处理这个Channel的读写事件。
一个NIOEventLoop处理很多Channel,而一个Channel的编码,解码,业务逻辑,都是在一个线程中完成,不会出现多个线程处理一个Channel的问题,也就不需要锁。
进而如果你在业务逻辑中有IO操作,比如读写数据库,HTTP调用等。就会阻塞这个线程,导致这个线程处理的所有Channel都会收到影响,降低吞吐率。所以Netty 里面的Handler一定不要有同步的阻塞操作,这一部分要放到异步线程池中执行。
3.2、Pigeon线程模型
阅读Pigeon源码,我们可以发现,Pigeon执行业务逻辑前,需要选择线程池
private ThreadPool selectThreadPool(final InvocationRequest request) {
ThreadPool pool = null;
String serviceKey = request.getServiceName();
String methodKey = serviceKey + "#" + request.getMethodName();
// spring poolConfig
pool = getConfigThreadPool(request);
// spring actives
if (pool == null && !CollectionUtils.isEmpty(methodThreadPools)) {
pool = methodThreadPools.get(methodKey);
}
if (pool == null && !CollectionUtils.isEmpty(serviceThreadPools)) {
pool = serviceThreadPools.get(serviceKey);
}
// lion poolConfig
if (pool == null && poolConfigSwitchable && !CollectionUtils.isEmpty(apiPoolNameMapping)) {
PoolConfig poolConfig = null;
String poolName = apiPoolNameMapping.get(methodKey);
if (StringUtils.isNotBlank(poolName)) { // 方法级别
poolConfig = poolConfigs.get(poolName);
if (poolConfig != null) {
pool = DynamicThreadPoolFactory.getThreadPool(poolConfig);
}
} else { // 服务级别
poolName = apiPoolNameMapping.get(serviceKey);
if (StringUtils.isNotBlank(poolName)) {
poolConfig = poolConfigs.get(poolName);
if (poolConfig != null) {
pool = DynamicThreadPoolFactory.getThreadPool(poolConfig);
}
}
}
}
// 默认方式
if (pool == null) {
if (enableSlowPool && requestTimeoutListener.isSlowRequest(request)) {
pool = slowRequestProcessThreadPool;
} else {
pool = sharedRequestProcessThreadPool;
}
}
return pool;
}
首先去看spring配置里有没有对指定接口,指定方法需独立的线程池,如果有,则用独立的线程池,如果没有,则去看是否已存在方法级别或者接口级别的线程池,如果没有,则确认方法是否是满调用,如果是则用满调用线程池,如果不是则用共享线程池。
通过这种方式,我们可以利用配置在方法维度上进行隔离,防止一个接口出问题拖垮整个服务。
四、总结
- 理解netty pigeon 线程模型
- IO任务尽量采用异步执行,并做好线程池任务队列的监控。
- HttpClient 一定要设置超时时间,避免线程长时间阻塞
- 做好code review
网友评论