一个公式看懂:为什么Dubbo线程池会打满

开发 后端
大家可能都遇到过DUBBO线程池打满这个问题,刚开始遇到这个问题可能会比较慌,常见方案可能就是重启服务,但也不知道重启是否可以解决。

[[393776]]

本文转载自微信公众号「JAVA前线」,作者IT徐胖子 。转载本文请联系JAVA前线公众号。

 0 文章概述

大家可能都遇到过DUBBO线程池打满这个问题,刚开始遇到这个问题可能会比较慌,常见方案可能就是重启服务,但也不知道重启是否可以解决。我认为重启不仅不能解决问题,甚至有可能加剧问题,这是为什么呢?本文我们就一起分析DUBBO线程池打满这个问题。

1 基础知识

1.1 DUBBO线程模型

1.1.1 基本概念

DUBBO底层网络通信采用Netty框架,我们编写一个Netty服务端进行观察:

  1. public class NettyServer { 
  2.     public static void main(String[] args) throws Exception { 
  3.         EventLoopGroup bossGroup = new NioEventLoopGroup(1); 
  4.         EventLoopGroup workerGroup = new NioEventLoopGroup(8); 
  5.         try { 
  6.             ServerBootstrap bootstrap = new ServerBootstrap(); 
  7.             bootstrap.group(bossGroup, workerGroup) 
  8.             .channel(NioServerSocketChannel.class) 
  9.             .option(ChannelOption.SO_BACKLOG, 128) 
  10.             .childOption(ChannelOption.SO_KEEPALIVE, true
  11.             .childHandler(new ChannelInitializer<SocketChannel>() { 
  12.                 @Override 
  13.                 protected void initChannel(SocketChannel ch) throws Exception { 
  14.                     ch.pipeline().addLast(new NettyServerHandler()); 
  15.                 } 
  16.             }); 
  17.             ChannelFuture channelFuture = bootstrap.bind(7777).sync(); 
  18.             System.out.println("服务端准备就绪"); 
  19.             channelFuture.channel().closeFuture().sync(); 
  20.         } catch (Exception ex) { 
  21.             System.out.println(ex.getMessage()); 
  22.         } finally { 
  23.             bossGroup.shutdownGracefully(); 
  24.             workerGroup.shutdownGracefully(); 
  25.         } 
  26.     } 

BossGroup线程组只有一个线程处理客户端连接请求,连接完成后将完成三次握手的SocketChannel连接分发给WorkerGroup处理读写请求,这两个线程组被称为「IO线程」。

我们再引出「业务线程」这个概念。服务生产者接收到请求后,如果处理逻辑可以快速处理完成,那么可以直接放在IO线程处理,从而减少线程池调度与上下文切换。但是如果处理逻辑非常耗时,或者会发起新IO请求例如查询数据库,那么必须派发到业务线程池处理。

DUBBO提供了多种线程模型,选择线程模型需要在配置文件指定dispatcher属性:

  1. <dubbo:protocol name="dubbo" dispatcher="all" /> 
  2. <dubbo:protocol name="dubbo" dispatcher="direct" /> 
  3. <dubbo:protocol name="dubbo" dispatcher="message" /> 
  4. <dubbo:protocol name="dubbo" dispatcher="execution" /> 
  5. <dubbo:protocol name="dubbo" dispatcher="connection" /> 

不同线程模型在选择是使用IO线程还是业务线程,DUBBO官网文档说明:

  1. all 
  2. 所有消息都派发到业务线程池,包括请求,响应,连接事件,断开事件,心跳 
  3.  
  4. direct 
  5. 所有消息都不派发到业务线程池,全部在IO线程直接执行 
  6.  
  7. message 
  8. 只有请求响应消息派发到业务线程池,其它连接断开事件,心跳等消息直接在IO线程执行 
  9.  
  10. execution 
  11. 只有请求消息派发到业务线程池,响应和其它连接断开事件,心跳等消息直接在IO线程执行 
  12.  
  13. connection 
  14. 在IO线程上将连接断开事件放入队列,有序逐个执行,其它消息派发到业务线程池 

1.1.2 确定时机

生产者和消费者在初始化时确定线程模型:

  1. // 生产者 
  2. public class NettyServer extends AbstractServer implements Server { 
  3.     public NettyServer(URL url, ChannelHandler handler) throws RemotingException { 
  4.         super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME))); 
  5.     } 
  6.  
  7. // 消费者 
  8. public class NettyClient extends AbstractClient { 
  9.     public NettyClient(final URL url, final ChannelHandler handler) throws RemotingException { 
  10.      super(url, wrapChannelHandler(url, handler)); 
  11.     } 

生产者和消费者默认线程模型都会使用AllDispatcher,ChannelHandlers.wrap方法可以获取Dispatch自适应扩展点。如果我们在配置文件中指定dispatcher,扩展点加载器会从URL获取属性值加载对应线程模型。本文以生产者为例进行分析:

  1. public class NettyServer extends AbstractServer implements Server { 
  2.     public NettyServer(URL url, ChannelHandler handler) throws RemotingException { 
  3.         // ChannelHandlers.wrap确定线程策略 
  4.         super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME))); 
  5.     } 
  6.  
  7. public class ChannelHandlers { 
  8.     protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) { 
  9.         return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class).getAdaptiveExtension().dispatch(handler, url))); 
  10.     } 
  11.  
  12. @SPI(AllDispatcher.NAME
  13. public interface Dispatcher { 
  14.     @Adaptive({Constants.DISPATCHER_KEY, "channel.handler"}) 
  15.     ChannelHandler dispatch(ChannelHandler handler, URL url); 

1.1.3 源码分析

我们分析其中两个线程模型源码,其它线程模型请阅读DUBBO源码。AllDispatcher模型所有消息都派发到业务线程池,包括请求,响应,连接事件,断开事件,心跳:

  1. public class AllDispatcher implements Dispatcher { 
  2.  
  3.     // 线程模型名称 
  4.     public static final String NAME = "all"
  5.  
  6.     // 具体实现策略 
  7.     @Override 
  8.     public ChannelHandler dispatch(ChannelHandler handler, URL url) { 
  9.         return new AllChannelHandler(handler, url); 
  10.     } 
  11.  
  12.  
  13. public class AllChannelHandler extends WrappedChannelHandler { 
  14.  
  15.     @Override 
  16.     public void connected(Channel channel) throws RemotingException { 
  17.         // 连接完成事件交给业务线程池 
  18.         ExecutorService cexecutor = getExecutorService(); 
  19.         try { 
  20.             cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED)); 
  21.         } catch (Throwable t) { 
  22.             throw new ExecutionException("connect event", channel, getClass() + " error when process connected event", t); 
  23.         } 
  24.     } 
  25.  
  26.     @Override 
  27.     public void disconnected(Channel channel) throws RemotingException { 
  28.         // 断开连接事件交给业务线程池 
  29.         ExecutorService cexecutor = getExecutorService(); 
  30.         try { 
  31.             cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED)); 
  32.         } catch (Throwable t) { 
  33.             throw new ExecutionException("disconnect event", channel, getClass() + " error when process disconnected event", t); 
  34.         } 
  35.     } 
  36.  
  37.     @Override 
  38.     public void received(Channel channel, Object message) throws RemotingException { 
  39.         // 请求响应事件交给业务线程池 
  40.         ExecutorService cexecutor = getExecutorService(); 
  41.         try { 
  42.             cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message)); 
  43.         } catch (Throwable t) { 
  44.             if(message instanceof Request && t instanceof RejectedExecutionException) { 
  45.                 Request request = (Request)message; 
  46.                 if(request.isTwoWay()) { 
  47.                     String msg = "Server side(" + url.getIp() + "," + url.getPort() + ") threadpool is exhausted ,detail msg:" + t.getMessage(); 
  48.                     Response response = new Response(request.getId(), request.getVersion()); 
  49.                     response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR); 
  50.                     response.setErrorMessage(msg); 
  51.                     channel.send(response); 
  52.                     return
  53.                 } 
  54.             } 
  55.             throw new ExecutionException(message, channel, getClass() + " error when process received event", t); 
  56.         } 
  57.     } 
  58.  
  59.     @Override 
  60.     public void caught(Channel channel, Throwable exception) throws RemotingException { 
  61.         // 异常事件交给业务线程池 
  62.         ExecutorService cexecutor = getExecutorService(); 
  63.         try { 
  64.             cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CAUGHT, exception)); 
  65.         } catch (Throwable t) { 
  66.             throw new ExecutionException("caught event", channel, getClass() + " error when process caught event", t); 
  67.         } 
  68.     } 

DirectDispatcher策略所有消息都不派发到业务线程池,全部在IO线程直接执行:

  1. public class DirectDispatcher implements Dispatcher { 
  2.  
  3.     // 线程模型名称 
  4.     public static final String NAME = "direct"
  5.  
  6.     // 具体实现策略 
  7.     @Override 
  8.     public ChannelHandler dispatch(ChannelHandler handler, URL url) { 
  9.         // 直接返回handler表示所有事件都交给IO线程处理 
  10.         return handler; 
  11.     } 

1.2 DUBBO线程池策略

1.2.1 基本概念

上个章节分析了线程模型,我们知道不同的线程模型会选择使用还是IO线程还是业务线程。如果使用业务线程池,那么使用什么线程池策略是本章节需要回答的问题。DUBBO官网线程派发模型图展示了线程模型和线程池策略的关系:

DUBBO提供了多种线程池策略,选择线程池策略需要在配置文件指定threadpool属性:

  1. <dubbo:protocol name="dubbo" threadpool="fixed" threads="100" /> 
  2. <dubbo:protocol name="dubbo" threadpool="cached" threads="100" /> 
  3. <dubbo:protocol name="dubbo" threadpool="limited" threads="100" /> 
  4. <dubbo:protocol name="dubbo" threadpool="eager" threads="100" /> 

不同线程池策略会创建不同特性的线程池:

  1. fixed 
  2. 包含固定个数线程 
  3.  
  4. cached 
  5. 线程空闲一分钟会被回收,当新请求到来时会创建新线程 
  6.  
  7. limited 
  8. 线程个数随着任务增加而增加,但不会超过最大阈值。空闲线程不会被回收 
  9.  
  10. eager 
  11. 当所有核心线程数都处于忙碌状态时,优先创建新线程执行任务,而不是立即放入队列 

1.2.2 确定时机

本文我们以AllDispatcher为例分析线程池策略在什么时候确定:

  1. public class AllDispatcher implements Dispatcher { 
  2.     public static final String NAME = "all"
  3.  
  4.     @Override 
  5.     public ChannelHandler dispatch(ChannelHandler handler, URL url) { 
  6.         return new AllChannelHandler(handler, url); 
  7.     } 
  8.  
  9. public class AllChannelHandler extends WrappedChannelHandler { 
  10.     public AllChannelHandler(ChannelHandler handler, URL url) { 
  11.         super(handler, url); 
  12.     } 

在WrappedChannelHandler构造函数中如果配置指定了threadpool属性,扩展点加载器会从URL获取属性值加载对应线程池策略,默认策略为fixed:

  1. public class WrappedChannelHandler implements ChannelHandlerDelegate { 
  2.  
  3.     public WrappedChannelHandler(ChannelHandler handler, URL url) { 
  4.         this.handler = handler; 
  5.         this.url = url; 
  6.         // 获取线程池自适应扩展点 
  7.         executor = (ExecutorService) ExtensionLoader.getExtensionLoader(ThreadPool.class).getAdaptiveExtension().getExecutor(url); 
  8.         String componentKey = Constants.EXECUTOR_SERVICE_COMPONENT_KEY; 
  9.         if (Constants.CONSUMER_SIDE.equalsIgnoreCase(url.getParameter(Constants.SIDE_KEY))) { 
  10.             componentKey = Constants.CONSUMER_SIDE; 
  11.         } 
  12.         DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension(); 
  13.         dataStore.put(componentKey, Integer.toString(url.getPort()), executor); 
  14.     } 
  15.  
  16. @SPI("fixed"
  17. public interface ThreadPool { 
  18.     @Adaptive({Constants.THREADPOOL_KEY}) 
  19.     Executor getExecutor(URL url); 

1.2.3 源码分析

(1) FixedThreadPool

  1. public class FixedThreadPool implements ThreadPool { 
  2.  
  3.     @Override 
  4.     public Executor getExecutor(URL url) { 
  5.  
  6.         // 线程名称 
  7.         String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME); 
  8.  
  9.         // 线程个数默认200 
  10.         int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS); 
  11.  
  12.         // 队列容量默认0 
  13.         int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES); 
  14.  
  15.         // 队列容量等于0使用阻塞队列SynchronousQueue 
  16.         // 队列容量小于0使用无界阻塞队列LinkedBlockingQueue 
  17.         // 队列容量大于0使用有界阻塞队列LinkedBlockingQueue 
  18.         return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, 
  19.                                       queues == 0 ? new SynchronousQueue<Runnable>() 
  20.                                       : (queues < 0 ? new LinkedBlockingQueue<Runnable>() 
  21.                                          : new LinkedBlockingQueue<Runnable>(queues)), 
  22.                                       new NamedInternalThreadFactory(nametrue), new AbortPolicyWithReport(name, url)); 
  23.     } 

(2) CachedThreadPool

 

  1. public class CachedThreadPool implements ThreadPool { 
  2.  
  3.     @Override 
  4.     public Executor getExecutor(URL url) { 
  5.  
  6.         // 获取线程名称 
  7.         String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME); 
  8.  
  9.         // 核心线程数默认0 
  10.         int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS); 
  11.  
  12.         // 最大线程数默认Int最大值 
  13.         int threads = url.getParameter(Constants.THREADS_KEY, Integer.MAX_VALUE); 
  14.  
  15.         // 队列容量默认0 
  16.         int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES); 
  17.  
  18.         // 线程空闲多少时间被回收默认1分钟 
  19.         int alive = url.getParameter(Constants.ALIVE_KEY, Constants.DEFAULT_ALIVE); 
  20.  
  21.         // 队列容量等于0使用阻塞队列SynchronousQueue 
  22.         // 队列容量小于0使用无界阻塞队列LinkedBlockingQueue 
  23.         // 队列容量大于0使用有界阻塞队列LinkedBlockingQueue 
  24.         return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS, 
  25.                                       queues == 0 ? new SynchronousQueue<Runnable>() 
  26.                                       : (queues < 0 ? new LinkedBlockingQueue<Runnable>() 
  27.                                          : new LinkedBlockingQueue<Runnable>(queues)), 
  28.                                       new NamedInternalThreadFactory(nametrue), new AbortPolicyWithReport(name, url)); 
  29.     } 

(3) LimitedThreadPool

  1. public class LimitedThreadPool implements ThreadPool { 
  2.  
  3.     @Override 
  4.     public Executor getExecutor(URL url) { 
  5.  
  6.         // 获取线程名称 
  7.         String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME); 
  8.  
  9.         // 核心线程数默认0 
  10.         int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS); 
  11.  
  12.         // 最大线程数默认200 
  13.         int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS); 
  14.  
  15.         // 队列容量默认0 
  16.         int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES); 
  17.  
  18.         // 队列容量等于0使用阻塞队列SynchronousQueue 
  19.         // 队列容量小于0使用无界阻塞队列LinkedBlockingQueue 
  20.         // 队列容量大于0使用有界阻塞队列LinkedBlockingQueue 
  21.         // keepalive时间设置Long.MAX_VALUE表示不回收空闲线程 
  22.         return new ThreadPoolExecutor(cores, threads, Long.MAX_VALUE, TimeUnit.MILLISECONDS, 
  23.                                       queues == 0 ? new SynchronousQueue<Runnable>() 
  24.                                       : (queues < 0 ? new LinkedBlockingQueue<Runnable>() 
  25.                                          : new LinkedBlockingQueue<Runnable>(queues)), 
  26.                                       new NamedInternalThreadFactory(nametrue), new AbortPolicyWithReport(name, url)); 
  27.     } 

(4) EagerThreadPool

我们知道ThreadPoolExecutor是普通线程执行器。当线程池核心线程达到阈值时新任务放入队列,当队列已满开启新线程处理,当前线程数达到最大线程数时执行拒绝策略。

但是EagerThreadPool自定义线程执行策略,当线程池核心线程达到阈值时,新任务不会放入队列而是开启新线程进行处理(要求当前线程数没有超过最大线程数)。当前线程数达到最大线程数时任务放入队列。

  1. public class EagerThreadPool implements ThreadPool { 
  2.  
  3.     @Override 
  4.     public Executor getExecutor(URL url) { 
  5.  
  6.         // 线程名 
  7.         String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME); 
  8.  
  9.         // 核心线程数默认0 
  10.         int cores = url.getParameter(Constants.CORE_THREADS_KEY, Constants.DEFAULT_CORE_THREADS); 
  11.  
  12.         // 最大线程数默认Int最大值 
  13.         int threads = url.getParameter(Constants.THREADS_KEY, Integer.MAX_VALUE); 
  14.  
  15.         // 队列容量默认0 
  16.         int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES); 
  17.  
  18.         // 线程空闲多少时间被回收默认1分钟 
  19.         int alive = url.getParameter(Constants.ALIVE_KEY, Constants.DEFAULT_ALIVE); 
  20.  
  21.         // 初始化自定义线程池和队列重写相关方法 
  22.         TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(queues <= 0 ? 1 : queues); 
  23.         EagerThreadPoolExecutor executor = new EagerThreadPoolExecutor(cores, 
  24.                 threads, 
  25.                 alive, 
  26.                 TimeUnit.MILLISECONDS, 
  27.                 taskQueue, 
  28.                 new NamedInternalThreadFactory(nametrue), 
  29.                 new AbortPolicyWithReport(name, url)); 
  30.         taskQueue.setExecutor(executor); 
  31.         return executor; 
  32.     } 

1.3 一个公式

现在我们知道DUBBO会选择线程池策略进行业务处理,那么应该如何估算可能产生的线程数呢?我们首先分析一个问题:一个公司有7200名员工,每天上班打卡时间是早上8点到8点30分,每次打卡时间系统执行时长为5秒。请问RT、QPS、并发量分别是多少?

RT表示响应时间,问题已经告诉了我们答案:

RT = 5

QPS表示每秒查询量,假设签到行为平均分布:

QPS = 7200 / (30 * 60) = 4

并发量表示系统同时处理的请求数量:

并发量 = QPS x RT = 4 x 5 = 20

根据上述实例引出如下公式:

并发量 = QPS x RT

如果系统为每一个请求分配一个处理线程,那么并发量可以近似等于线程数。基于上述公式不难看出并发量受QPS和RT影响,这两个指标任意一个上升就会导致并发量上升。

但是这只是理想情况,因为并发量受限于系统能力而不可能持续上升,例如DUBBO线程池就对线程数做了限制,超出最大线程数限制则会执行拒绝策略,而拒绝策略会提示线程池已满,这就是DUBBO线程池打满问题的根源。下面我们分析RT上升和QPS上升这两个原因。

2 RT上升

2.1 生产者发生慢服务

2.1.1 原因分析

(1) 生产者配置

  1. <beans> 
  2.     <dubbo:registry address="zookeeper://127.0.0.1:2181" /> 
  3.     <dubbo:protocol name="dubbo" port="9999" /> 
  4.     <dubbo:service interface="com.java.front.dubbo.demo.provider.HelloService" ref="helloService" /> 
  5. </beans>     

(2) 生产者业务

  1. package com.java.front.dubbo.demo.provider; 
  2. public interface HelloService { 
  3.     public String sayHello(String name) throws Exception; 
  4.  
  5. public class HelloServiceImpl implements HelloService { 
  6.     public String sayHello(String name) throws Exception { 
  7.         String result = "hello[" + name + "]"
  8.         // 模拟慢服务 
  9.        Thread.sleep(10000L);  
  10.        System.out.println("生产者执行结果" + result); 
  11.        return result; 
  12.     } 

(3) 消费者配置

  1. <beans> 
  2.     <dubbo:registry address="zookeeper://127.0.0.1:2181" /> 
  3.     <dubbo:reference id="helloService" interface="com.java.front.dubbo.demo.provider.HelloService" /> 
  4. </beans>     

(4) 消费者业务

  1. public class Consumer { 
  2.  
  3.     @Test 
  4.     public void testThread() { 
  5.         ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "classpath*:METAINF/spring/dubbo-consumer.xml" }); 
  6.         context.start(); 
  7.         for (int i = 0; i < 500; i++) { 
  8.             new Thread(new Runnable() { 
  9.                 @Override 
  10.                 public void run() { 
  11.                     HelloService helloService = (HelloService) context.getBean("helloService"); 
  12.                     String result; 
  13.                     try { 
  14.                         result = helloService.sayHello("微信公众号「JAVA前线」"); 
  15.                         System.out.println("客户端收到结果" + result); 
  16.                     } catch (Exception e) { 
  17.                         System.out.println(e.getMessage()); 
  18.                     } 
  19.                 } 
  20.             }).start(); 
  21.         } 
  22.     } 

依次运行生产者和消费者代码,会发现日志中出现报错信息。生产者日志会打印线程池已满:

  1. Caused by: java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-x.x.x.x:9999, Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 201 (completed: 1), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://x.x.x.x:9999! 
  2. at org.apache.dubbo.common.threadpool.support.AbortPolicyWithReport.rejectedExecution(AbortPolicyWithReport.java:67) 
  3. at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) 
  4. at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) 
  5. at org.apache.dubbo.remoting.transport.dispatcher.all.AllChannelHandler.caught(AllChannelHandler.java:88) 

消费者日志不仅会打印线程池已满,还会打印服务提供者信息和调用方法,我们可以根据日志找到哪一个方法有问题:

  1. Failed to invoke the method sayHello in the service com.java.front.dubbo.demo.provider.HelloService.  
  2. Tried 3 times of the providers [x.x.x.x:9999] (1/1) from the registry 127.0.0.1:2181 on the consumer x.x.x.x  
  3. using the dubbo version 2.7.0-SNAPSHOT. Last error is: Failed to invoke remote method: sayHello,  
  4. provider: dubbo://x.x.x.x:9999/com.java.front.dubbo.demo.provider.HelloService?anyhost=true&application=xpz-consumer1&check=false&dubbo=2.0.2&generic=false&group=&interface=com.java.front.dubbo.demo.provider.HelloService&logger=log4j&methods=sayHello&pid=33432&register.ip=x.x.x.x&release=2.7.0-SNAPSHOT&remote.application=xpz-provider&remote.timestamp=1618632597509&side=consumer&timeout=100000000&timestamp=1618632617392,  
  5. cause: Server side(x.x.x.x,9999) threadpool is exhausted ,detail msg:Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-x.x.x.x:9999, Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 401 (completed: 201), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://x.x.x.x:9999! 

2.1.2 解决方案

(1) 找出慢服务

DUBBO线程池打满时会执行拒绝策略:

  1. public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy { 
  2.     protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class); 
  3.     private final String threadName; 
  4.     private final URL url; 
  5.     private static volatile long lastPrintTime = 0; 
  6.     private static Semaphore guard = new Semaphore(1); 
  7.  
  8.     public AbortPolicyWithReport(String threadName, URL url) { 
  9.         this.threadName = threadName; 
  10.         this.url = url; 
  11.     } 
  12.  
  13.     @Override 
  14.     public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 
  15.         String msg = String.format("Thread pool is EXHAUSTED!" + 
  16.                                    " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," + 
  17.                                    " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!"
  18.                                    threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(), 
  19.                                    e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(), 
  20.                                    url.getProtocol(), url.getIp(), url.getPort()); 
  21.         logger.warn(msg); 
  22.         // 打印线程快照 
  23.         dumpJStack(); 
  24.         throw new RejectedExecutionException(msg); 
  25.     } 
  26.  
  27.     private void dumpJStack() { 
  28.         long now = System.currentTimeMillis(); 
  29.  
  30.         // 每10分钟输出线程快照 
  31.         if (now - lastPrintTime < 10 * 60 * 1000) { 
  32.             return
  33.         } 
  34.         if (!guard.tryAcquire()) { 
  35.             return
  36.         } 
  37.  
  38.         ExecutorService pool = Executors.newSingleThreadExecutor(); 
  39.         pool.execute(() -> { 
  40.             String dumpPath = url.getParameter(Constants.DUMP_DIRECTORY, System.getProperty("user.home")); 
  41.             System.out.println("AbortPolicyWithReport dumpJStack directory=" + dumpPath); 
  42.             SimpleDateFormat sdf; 
  43.             String os = System.getProperty("os.name").toLowerCase(); 
  44.  
  45.             // linux文件位置/home/xxx/Dubbo_JStack.log.2021-01-01_20:50:15 
  46.             // windows文件位置/user/xxx/Dubbo_JStack.log.2020-01-01_20-50-15 
  47.             if (os.contains("win")) { 
  48.                 sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); 
  49.             } else { 
  50.                 sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); 
  51.             } 
  52.             String dateStr = sdf.format(new Date()); 
  53.             try (FileOutputStream jStackStream = new FileOutputStream(new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr))) { 
  54.                 JVMUtil.jstack(jStackStream); 
  55.             } catch (Throwable t) { 
  56.                 logger.error("dump jStack error", t); 
  57.             } finally { 
  58.                 guard.release(); 
  59.             } 
  60.             lastPrintTime = System.currentTimeMillis(); 
  61.         }); 
  62.         pool.shutdown(); 
  63.     } 

拒绝策略会输出线程快照文件,在分析线程快照文件时BLOCKED和TIMED_WAITING线程状态需要我们重点关注。如果发现大量线程阻塞或者等待状态则可以定位到具体代码行:

  1. DubboServerHandler-x.x.x.x:9999-thread-200 Id=230 TIMED_WAITING 
  2. at java.lang.Thread.sleep(Native Method) 
  3. at com.java.front.dubbo.demo.provider.HelloServiceImpl.sayHello(HelloServiceImpl.java:13) 
  4. at org.apache.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) 
  5. at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:56) 
  6. at org.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:85) 
  7. at org.apache.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:56) 
  8. at org.apache.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56) 

(2) 优化慢服务

现在已经找到了慢服务,此时我们就可以优化慢服务了。优化慢服务就需要具体问题具体分析了,这不是本文的重点在此不进行展开。

2.2 生产者预热不充分

2.2.1 原因分析

还有一种RT上升的情况是我们不能忽视的,这种情况就是提供者重启后预热不充分即被调用。因为当生产者刚启动时需要预热,需要和其它资源例如数据库、缓存等建立连接,建立连接是需要时间的。如果此时大量消费者请求到未预热的生产者,链路时间增加了连接时间,RT时间必然会增加,从而也会导致DUBBO线程池打满问题。

2.2.2 解决方案

(1) 等待生产者充分预热

因为生产者预热不充分导致线程池打满问题,最容易发生在系统发布时。例如发布了一台机器后发现线上出现线程池打满问题,千万不要着急重启机器,而是给机器一段时间预热,等连接建立后问题大概率消失。同时我们在发布时也要分多批次发布,不要一次发布太多机器导致服务因为预热问题造成大面积影响。

(2) DUBBO升级版本大于等于2.7.4

DUBBO消费者在调用选择生产者时本身就会执行预热逻辑,为什么还会出现预热不充分问题?这是因为2.5.5之前版本以及2.7.2版本预热机制是有问题的,简而言之就是获取启动时间不正确,2.7.4版本彻底解决了这个问题,所以我们要避免使用问题版本。下面我们阅读2.7.0版本预热机制源码,看看预热机制如何生效:

  1. public class RandomLoadBalance extends AbstractLoadBalance { 
  2.  
  3.     public static final String NAME = "random"
  4.  
  5.     @Override 
  6.     protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { 
  7.  
  8.         // invokers数量 
  9.         int length = invokers.size(); 
  10.  
  11.         // 权重是否相同 
  12.         boolean sameWeight = true
  13.  
  14.         // invokers权重数组 
  15.         int[] weights = new int[length]; 
  16.  
  17.         // 第一个invoker权重 
  18.         int firstWeight = getWeight(invokers.get(0), invocation); 
  19.         weights[0] = firstWeight; 
  20.  
  21.         // 权重值之和 
  22.         int totalWeight = firstWeight; 
  23.         for (int i = 1; i < length; i++) { 
  24.             // 计算权重值 
  25.             int weight = getWeight(invokers.get(i), invocation); 
  26.             weights[i] = weight; 
  27.             totalWeight += weight; 
  28.  
  29.             // 任意一个invoker权重值不等于第一个invoker权重值则sameWeight设置为FALSE 
  30.             if (sameWeight && weight != firstWeight) { 
  31.                 sameWeight = false
  32.             } 
  33.         } 
  34.         // 权重值不等则根据总权重值计算 
  35.         if (totalWeight > 0 && !sameWeight) { 
  36.             int offset = ThreadLocalRandom.current().nextInt(totalWeight); 
  37.             // 不断减去权重值当小于0时直接返回 
  38.             for (int i = 0; i < length; i++) { 
  39.                 offset -= weights[i]; 
  40.                 if (offset < 0) { 
  41.                     return invokers.get(i); 
  42.                 } 
  43.             } 
  44.         } 
  45.         // 所有服务权重值一致则随机返回 
  46.         return invokers.get(ThreadLocalRandom.current().nextInt(length)); 
  47.     } 
  48.  
  49. public abstract class AbstractLoadBalance implements LoadBalance { 
  50.  
  51.     static int calculateWarmupWeight(int uptime, int warmup, int weight) { 
  52.         // uptime/(warmup*weight) 
  53.         // 如果当前服务提供者没过预热期,用户设置的权重将通过uptime/warmup减小 
  54.         // 如果服务提供者设置权重很大但是还没过预热时间,重新计算权重会很小 
  55.         int ww = (int) ((float) uptime / ((float) warmup / (float) weight)); 
  56.         return ww < 1 ? 1 : (ww > weight ? weight : ww); 
  57.     } 
  58.  
  59.     protected int getWeight(Invoker<?> invoker, Invocation invocation) { 
  60.  
  61.         // 获取invoker设置权重值默认权重=100 
  62.         int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); 
  63.  
  64.         // 如果权重大于0 
  65.         if (weight > 0) { 
  66.  
  67.             // 服务提供者发布服务时间戳 
  68.             long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L); 
  69.             if (timestamp > 0L) { 
  70.  
  71.                 // 服务已经发布多少时间 
  72.                 int uptime = (int) (System.currentTimeMillis() - timestamp); 
  73.  
  74.                 // 预热时间默认10分钟 
  75.                 int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP); 
  76.  
  77.                 // 生产者发布时间大于0但是小于预热时间 
  78.                 if (uptime > 0 && uptime < warmup) { 
  79.  
  80.                     // 重新计算权重值 
  81.                     weight = calculateWarmupWeight(uptime, warmup, weight); 
  82.                 } 
  83.             } 
  84.         } 
  85.         // 服务发布时间大于预热时间直接返回设置权重值 
  86.         return weight >= 0 ? weight : 0; 
  87.     } 

3 QPS上升

上面章节大篇幅讨论了由于RT上升造成的线程池打满问题,现在我们讨论另一个参数QPS。当上游流量激增会导致创建大量线程池,也会造成线程池打满问题。这时如果发现QPS超出了系统承受能力,我们不得不采用降级方案保护系统,请参看我之前文章《从反脆弱角度谈技术系统的高可用性》

4 文章总结

本文首先介绍了DUBBO线程模型和线程池策略,然后我们引出了公式,发现并发量受RT和QPS两个参数影响,这两个参数任意一个上升都可以造成线程池打满问题。生产者出现慢服务或者预热不充分都有可能造成RT上升,而上游流量激增会造成QPS上升,同时本文也给出了解决方案。DUBBO线程池打满是一个必须重视的问题,希望本文对大家有所帮助。

 

责任编辑:武晓燕 来源: JAVA前线
相关推荐

2022-02-10 11:43:54

DUBBO线程池QPS

2022-03-09 09:43:01

工具类线程项目

2020-10-27 13:24:35

线程池系统模型

2022-03-02 07:36:37

池化技术Java线程池

2022-03-21 07:40:08

线程池Executors方式

2020-04-29 14:10:44

Java线程池编程语言

2021-10-27 06:49:34

线程池Core函数

2019-11-13 14:38:34

Executors阿里线程池

2024-09-30 08:54:10

2021-04-13 08:54:28

dubbo线程池事故排查

2022-04-29 08:00:06

Linux目录网络

2024-07-15 08:20:24

2023-09-27 23:03:01

Java虚拟线程

2023-03-08 07:43:07

DUCC配置平台

2024-05-06 08:17:50

线程池机制线程数

2012-09-03 09:52:39

虚拟化

2021-03-29 08:47:24

线程面试官线程池

2019-08-08 15:24:06

开源软件软件Linux

2022-12-30 08:29:07

Nacos动态化线程池

2018-06-19 16:04:27

Dubbo应用Java
点赞
收藏

51CTO技术栈公众号