动态线程池组件-应用

1,011 阅读7分钟

1.序言

经过前面两篇文章的介绍,线程池组件主要是为了解决监控线程池的状态以及支持统计、推送告警等功能;组件的基本架构已经搭建完毕,已在项目中进行使用

2.背景

当前所有服务接口都是共用一个线程池,如果某个接口出现延时则会阻塞队列,影响到其它接口服务的正常使用

3.目标

  • 线程池资源隔离
  • 增强Dubbo线程池

4.具体实现

利用Dubbo SPI 机制,使用自定义线程池替换默认线程池

基于Xml格式:

4.1 先定义线程池组件Bean

@Bean(name = "userThreadPool")
public ThreadPoolExecutor executor1() {
    PhxThreadPool pool = new PhxThreadPool(10, 10, 30, TimeUnit.SECONDS,
                                           new PhxResizeLinkedBlockingQueue<>(100));
    pool.setName("USER-PROVIDER-1");
    return pool;
}

@Bean(name = "roleThreadPool")
public ThreadPoolExecutor executor2() {
    PhxThreadPool pool = new PhxThreadPool(20, 20, 30, TimeUnit.SECONDS,
                                           new PhxResizeLinkedBlockingQueue<>(200));
    pool.setName("ROLE-PROVIDER-2");
    return pool;
}

4.2 配置xml

Provider 使用了 20880 和 20881 端口,自定义线程池名称为 custom

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo
        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="demo-provider"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <dubbo:protocol id="p1" name="dubbo" port="20880" threadpool="custom" />
    <dubbo:protocol id="p2" name="dubbo" port="20881"  threadpool="custom"/>

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.bingo.provider.api.service.IUserService" protocol="p1"
                   class="com.bingo.impl.serviceImpl.UserServiceImpl" timeout="120000"/>

    <dubbo:service interface="com.bingo.provider.api.service.IRoleService" protocol="p2"
                   class="com.bingo.impl.serviceImpl.RoleServiceServiceImpl" timeout="120000"/>

</beans>

4.3 配置自定义线程池扩展

在 META-INF/dubbo/ 目录下,创建 com.alibaba.dubbo.common.threadpool.ThreadPool 文件,内容为

custom=com.custom.CustomThreadPool

4.4 编写自定义线程拓展类

参数URL中只能获取到端口信息,无法与Spring中自定义线程池Bean相关联,需提供方设置 端口- 线程池Bean 的映射关系

@Slf4j
public class CustomThreadPool implements ThreadPool {

    private static Map<Integer, String> portAndBeanNameRefMap = new HashMap<>(4);
    private static boolean ALREADY_LOAD_FLAG = false;

    @Override
    public Executor getExecutor(URL url) {
		// 自定义线程池
        Executor executor = customerExecutor(url);
        if(executor != null){
            return executor;
        }
		// 默认线程池
        return defaultExecutor(url);
    }

    private Executor customerExecutor(URL url){
        Map<String, PhxThreadPool> phxThreadPoolMap = ApplicationContextHolder.getBeansOfType(PhxThreadPool.class);
        if(MapUtils.isEmpty(phxThreadPoolMap)) {
            return null;
        }
        int bindPort = url.getPort();

        if(!ALREADY_LOAD_FLAG){
            portAndBeanNameRefMap = getPortAndBeanNameRef();
        }

        if(MapUtils.isEmpty(portAndBeanNameRefMap)) {
            return null;
        }

       for(Map.Entry<Integer, String> entry : portAndBeanNameRefMap.entrySet()){
           Integer port = entry.getKey();
           String beanName = entry.getValue();

           if(StringUtils.isBlank(beanName)){
               continue;
           }

           PhxThreadPool threadPool = phxThreadPoolMap.get(beanName);
           if(bindPort == port && threadPool != null){
               return  threadPool;
           }
       }

        return null;
    }

    private Executor defaultExecutor(URL url){
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<>() :
                        (queues < 0 ? new LinkedBlockingQueue<>()
                                : new LinkedBlockingQueue<>(queues)),
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
	
    private Map<Integer,String> getPortAndBeanNameRef(){
        Map<Integer, String> resultMap = new HashMap<>();
        try {
            ClassLoader classLoader = CustomThreadPool.class.getClassLoader();
            Enumeration<java.net.URL> urls = classLoader.getResources("META-INF/dubbo/phxThreadPool");
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                loadResource(resultMap, resourceURL);
            }
        }catch (Exception ex){
            log.error("RefreshContainlListener.threadPoolAndPortRef error: ", ex);
            return resultMap;
        }finally {
            ALREADY_LOAD_FLAG = true;
        }

        return resultMap;
    }

    private void loadResource(Map<Integer,String> resultMap, java.net.URL resourceURL) {
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    final int ci = line.indexOf('#');
                    if (ci >= 0) line = line.substring(0, ci);
                    line = line.trim();
                    if (line.length() > 0) {
                        int i = line.indexOf('=');
                        if (i > 0) {
                            Integer port;
                            try {
                                port = Integer.parseInt(line.substring(0, i).trim());
                            }catch (Exception ex){
                                log.error("loadResource parseInt error : ", ex);
                                continue;
                            }
                            line = line.substring(i + 1).trim();
                            resultMap.put(port, line);
                        }
                    }
                }
            }
        } catch (Throwable t) {
            log.error("loadResource error: ", t);
        }
    }
}

4.5 配置端口-线程池映射关系

参考SPI读取资源的方式(可以从源码中直接拷贝读取资源代码,不香吗?)

在 META-INF/dubbo 目录下新建文件 phxThreadPool,配置内容:

20880=userThreadPool
20881=roleThreadPool

4.6 效果

效果一:

效果二:

5.原理解析

5.1 暴露服务部分流程

Dubbo是基于Netty进行传输的,那么Dubbo是怎么做的呢? 先来一张时序图,进入到开启Netty服务这一环节!

跟随时序图到达远程暴露节点 DubboProtocol,开启了服务 openServer,代码如下:

private void openServer(URL url) {
    // find server.
    String key = url.getAddress();
    //client can export a service which's only for server to invoke
    boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
    if (isServer) {
        ExchangeServer server = serverMap.get(key);
        if (server == null) {
            serverMap.put(key, createServer(url));
        } else {
            // server supports reset, use together with override
            server.reset(url);
        }
    }
}

/**
大致逻辑就是获取服务机器的地址(一般为 ip:port)作为KEY,如果是一个服务则根据地址信息从缓存的Map获取已经暴露的服务:
    如果不存在,则暴露服务
    如果存在,则将服务的信息添加到已暴露的服务中
*/

上面几行的逻辑并不复杂,但确是一个关键节点;根据上述逻辑,只要地址不一致,则会重新创建Netty服务 ;

如果想对接口做资源隔离,那么可以创建多个Netty服务,只需要改变服务暴露对应的端口,即可做到资源隔离的效果

紧跟时序图,可以看到最后创建了 NettyServer 对象;

下面部分为对NettyHandler的封装增强

进入构造器中查看,发现对ChannelHandler对象进行了包装处理;

再次进入wrap方法:

ExtensionLoader.getExtensionLoader(Dispatcher.class).getAdaptiveExtension().dispatch(handler, url) 

这行代码主要是基于SPI机制加载线程模型分发策略的具体实现 (Dispatcher 默认线程为 all)

可以看到该接口有5个实现类:

官网介绍

进入默认的分发类: AllDispatcher

/**
 * default thread pool configure
 */
public class AllDispatcher implements Dispatcher {

    public static final String NAME = "all";

    @Override
    public ChannelHandler dispatch(ChannelHandler handler, URL url) {
        return new AllChannelHandler(handler, url);
    }

}

进入封装的 AllChannelHandler 对象:

可以看到 AllChannelHandler 中有一些 接收、连接、异常捕获等方法,是否可以大胆假设,这些就是消费方调用时执行真正处理的方法 ?

这里调用了父类 WrappedChannelHandler 有参构造器,再来一张图看一下 ChannelHandler 的关系:

所有的子类都调用了父类的有参构造器,代码如下:

这一段代码的逻辑是基于 SPI 获取拓展线程池

5.2 SPI 解析

官方文档介绍

SPI的大概逻辑是从资源配置中加载名称和全限定名,并且利用反射机制创建对象,部分对象还会使用set形式做一些增强;创建好的对象会缓存在 ExtensionLoader 的属性中存储;

与SPI经常结合使用的有 @Adaptive 注解:

  • 1、该注解在类上时,会成为一个装饰者类,利用dubbo的 ioc 机制做一些功能增强(set 形式)
  • 2、该注解在方法上时,会使用 javassist 在运行期间动态生成字节码类文件并加载(代理类)

可以看看加载生成线程池的代码:

executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url);

可以看到 ThreadPool 类的方法上使用到 @Adaptive 注解

/**
 * ThreadPool
 */
@SPI("fixed")
public interface ThreadPool {

    /**
     * Thread pool
     *
     * @param url URL contains thread parameter
     * @return thread pool
     */
    @Adaptive({Constants.THREADPOOL_KEY})
    Executor getExecutor(URL url);

}

因此会先组装好完整的类文件结构,运用操作字节码的javaassist类库,在运行期间动态生成并加载该类;

(生成类文件结构的方法为:com.alibaba.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClassCode)

默认情况下是不会显示的,为了调试方便,可以将Logger级别设置为debug,即可在控制台中输出该类结构

生成的文件如下:

public class ThreadPool$Adaptive implements com.alibaba.dubbo.common.threadpool.ThreadPool {

    public java.util.concurrent.Executor getExecutor(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) {
            throw new IllegalArgumentException("url == null");
        }

        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("threadpool", "fixed");
        if(extName == null){
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.common.threadpool.ThreadPool) name from url(" + url.toString() + ") use keys([threadpool])");
        }
        com.alibaba.dubbo.common.threadpool.ThreadPool extension =
                ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.threadpool.ThreadPool.class).getExtension(extName);
        return extension.getExecutor(arg0);
    }
}

URL中带有提供者自定义的一些属性,上述逻辑会先从url中获取 threadPool 属性,如果获取不到则默认使用fixed,之前我们在xml中配置为 custom ,拿到该属性名称后,会从 ExtensionLoader 的属性中获取之前创建好的 CustomThreadPool 对象,并且调用其 getExecutor 方法,执行我们自定义的一些逻辑。

继续回到主线:

进入父类中,除了参数获取赋值外,重点关注 doOpen 方法,可以看到调用了本类抽象的方法;

这里使用了钩子方法,重新进入子类 NettyServer 的 doOpen 方法;

5.3 处理请求

Netty分为boss线程池和work线程池,boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理;

服务接收到请求后,会委派到上图的 NettyHandler 中进行处理; 即刚才包装过的 ChannelHandler 对象

NettyHandler 类关系图可以看到实现了处理IO事件的 SimpleChannelHandler, 并且重新覆写了部分逻辑处理的关键方法;

有请求连接时,Netty识别分发至开启的服务中方法,如上述的 20880 端口,对应的Handler是 AllChannelHandler 为例:

@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
    NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
    try {
        if (channel != null) {
            channels.put(NetUtils.toAddressString((InetSocketAddress) ctx.getChannel().getRemoteAddress()), channel);
        }
        handler.connected(channel);
    } finally {
        NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
    }
}

调用了 handler.connected(channel);

/**
获取对象中线程池属性,先获取自定义的线程池,如果拿不到就用系统默认共享的线程池;
会将任务委派到线程中进行处理;
**/
@Override
public void connected(Channel channel) throws RemotingException {
    ExecutorService cexecutor = getExecutorService();
    try {
        cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
    } catch (Throwable t) {
        throw new ExecutionException("connect event", channel, getClass() + " error when process connected event .", t);
    }
}

public ExecutorService getExecutorService() {
    ExecutorService cexecutor = executor;
    if (cexecutor == null || cexecutor.isShutdown()) {
        cexecutor = SHARED_EXECUTOR;
    }
    return cexecutor;
}

6. 总结

  • 写文章期间也倒逼自己更深入的了解源码底层的一些机制原理,期间也踩了一些坑,收获满满

参考资料

www.jianshu.com/nb/6137390