Pigeon线程池打满问题

2017-10-09  本文已影响0人  卡拉_拉卡

一、问题介绍

线上服务A上线后,一切正常。等到晚上八点左右,服务A开始报警,很多接口出现超时的问题,因为降级做的不好,进而引起连锁反应,导致很多其他服务也开始出现超时,不可用等报警。

二、问题原因

服务A上线是为了修复一个异步发送PUSH的问题,最初的问题是发现线上PUSH很多没有发出,初步定位的是异步线程没有执行,于是同事把异步改成同步调用,修复上线了,并线上验证通过。
在晚上八点左右,有一个JOB批量的调用这个发送PUSH的接口,接口是同步HTTP调用第三方接口,同时HttpClient没有设置超时时间,而第三方接口的响应时间较长,于是很多线程阻塞在HTTP调用上,又因为接口量调用较大,导致Pigeon线程池打满,该接口出现大面积超时。

三、分析

这个事故发生根本原因是两个问题:

Pigeon是一个类似dubbo的服务化框架,支持远程调用,服务注册,服务发现,负载均衡等功能。
他的底层网络处理依赖Netty,所以分析Pigeon的线程模型,需要从Pigeon和Netty两个方面来看。

3.1、Netty线程模型

Netty是一款NIO框架,它封装了JAVA NIO ,帮助我们解决了很多的网络处理的底层问题,提供出易用的API,方便我们快速的开发出NIO的程序。
Netty的采用了Reactor模型,事件驱动。

Reactor
其中Accept Pool 只负责处理新的连接请求和一些简单的非业务逻辑,比如权限认证,登录等。
IO Pool 负责处理channel的读写,编码解码和业务逻辑
我们再看一下Netty是如何对应这种模式的:
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配置里有没有对指定接口,指定方法需独立的线程池,如果有,则用独立的线程池,如果没有,则去看是否已存在方法级别或者接口级别的线程池,如果没有,则确认方法是否是满调用,如果是则用满调用线程池,如果不是则用共享线程池。
通过这种方式,我们可以利用配置在方法维度上进行隔离,防止一个接口出问题拖垮整个服务。

四、总结

上一篇 下一篇

猜你喜欢

热点阅读