一個公式看懂:爲什麼DUBBO線程池會打滿

轉載:https://blog.csdn.net/lianggzone/article/details/115986471?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-115986471-blog-121764780.235%5Ev27%5Epc_relevant_recovery_v2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-115986471-blog-121764780.235%5Ev27%5Epc_relevant_recovery_v2&utm_relevant_index=2

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.  
        }
  27.  
    }

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.  
    // 消費者
  9.  
    public class NettyClient extends AbstractClient {
  10.  
        public NettyClient(final URL url, final ChannelHandler handler) throws RemotingException {
  11.  
         super(url, wrapChannelHandler(url, handler));
  12.  
        }
  13.  
    }

生產者和消費者默認線程模型都會使用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.  
     
  8.  
    public class ChannelHandlers {
  9.  
        protected ChannelHandler wrapInternal(ChannelHandler handler, URL url) {
  10.  
            return new MultiMessageHandler(new HeartbeatHandler(ExtensionLoader.getExtensionLoader(Dispatcher.class).getAdaptiveExtension().dispatch(handler, url)));
  11.  
        }
  12.  
    }
  13.  
     
  14.  
    @SPI(AllDispatcher.NAME)
  15.  
    public interface Dispatcher {
  16.  
        @Adaptive({Constants.DISPATCHER_KEY, "channel.handler"})
  17.  
        ChannelHandler dispatch(ChannelHandler handler, URL url);
  18.  
    }

 

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.  
     
  14.  
    public class AllChannelHandler extends WrappedChannelHandler {
  15.  
     
  16.  
        @Override
  17.  
        public void connected(Channel channel) throws RemotingException {
  18.  
            // 連接完成事件交給業務線程池
  19.  
            ExecutorService cexecutor = getExecutorService();
  20.  
            try {
  21.  
                cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
  22.  
            } catch (Throwable t) {
  23.  
                throw new ExecutionException("connect event", channel, getClass() + " error when process connected event", t);
  24.  
            }
  25.  
        }
  26.  
     
  27.  
        @Override
  28.  
        public void disconnected(Channel channel) throws RemotingException {
  29.  
            // 斷開連接事件交給業務線程池
  30.  
            ExecutorService cexecutor = getExecutorService();
  31.  
            try {
  32.  
                cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED));
  33.  
            } catch (Throwable t) {
  34.  
                throw new ExecutionException("disconnect event", channel, getClass() + " error when process disconnected event", t);
  35.  
            }
  36.  
        }
  37.  
     
  38.  
        @Override
  39.  
        public void received(Channel channel, Object message) throws RemotingException {
  40.  
            // 請求響應事件交給業務線程池
  41.  
            ExecutorService cexecutor = getExecutorService();
  42.  
            try {
  43.  
                cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
  44.  
            } catch (Throwable t) {
  45.  
                if(message instanceof Request && t instanceof RejectedExecutionException) {
  46.  
                    Request request = (Request)message;
  47.  
                    if(request.isTwoWay()) {
  48.  
                        String msg = "Server side(" + url.getIp() + "," + url.getPort() + ") threadpool is exhausted ,detail msg:" + t.getMessage();
  49.  
                        Response response = new Response(request.getId(), request.getVersion());
  50.  
                        response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR);
  51.  
                        response.setErrorMessage(msg);
  52.  
                        channel.send(response);
  53.  
                        return;
  54.  
                    }
  55.  
                }
  56.  
                throw new ExecutionException(message, channel, getClass() + " error when process received event", t);
  57.  
            }
  58.  
        }
  59.  
     
  60.  
        @Override
  61.  
        public void caught(Channel channel, Throwable exception) throws RemotingException {
  62.  
            // 異常事件交給業務線程池
  63.  
            ExecutorService cexecutor = getExecutorService();
  64.  
            try {
  65.  
                cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CAUGHT, exception));
  66.  
            } catch (Throwable t) {
  67.  
                throw new ExecutionException("caught event", channel, getClass() + " error when process caught event", t);
  68.  
            }
  69.  
        }
  70.  
    }

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.  
        }
  12.  
    }

 

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.  
     
  10.  
    public class AllChannelHandler extends WrappedChannelHandler {
  11.  
        public AllChannelHandler(ChannelHandler handler, URL url) {
  12.  
            super(handler, url);
  13.  
        }
  14.  
    }

在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.  
     
  17.  
    @SPI("fixed")
  18.  
    public interface ThreadPool {
  19.  
        @Adaptive({Constants.THREADPOOL_KEY})
  20.  
        Executor getExecutor(URL url);
  21.  
    }

 

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(name, true), new AbortPolicyWithReport(name, url));
  23.  
        }
  24.  
    }

 

(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(name, true), new AbortPolicyWithReport(name, url));
  29.  
        }
  30.  
    }

 

(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(name, true), new AbortPolicyWithReport(name, url));
  27.  
        }
  28.  
    }

 

(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(name, true),
  29.  
                    new AbortPolicyWithReport(name, url));
  30.  
            taskQueue.setExecutor(executor);
  31.  
            return executor;
  32.  
        }
  33.  
    }

 

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.  
     
  6.  
    public class HelloServiceImpl implements HelloService {
  7.  
        public String sayHello(String name) throws Exception {
  8.  
            String result = "hello[" + name + "]";
  9.  
            // 模擬慢服務
  10.  
           Thread.sleep(10000L); 
  11.  
           System.out.println("生產者執行結果" + result);
  12.  
           return result;
  13.  
        }
  14.  
    }

 

(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.  
        }
  23.  
    }

依次運行生產者和消費者代碼,會發現日誌中出現報錯信息。生產者日誌會打印線程池已滿:

  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.  
        }
  64.  
    }

拒絕策略會輸出線程快照文件,在分析線程快照文件時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.  
     
  50.  
    public abstract class AbstractLoadBalance implements LoadBalance {
  51.  
     
  52.  
        static int calculateWarmupWeight(int uptime, int warmup, int weight) {
  53.  
            // uptime/(warmup*weight)
  54.  
            // 如果當前服務提供者沒過預熱期,用戶設置的權重將通過uptime/warmup減小
  55.  
            // 如果服務提供者設置權重很大但是還沒過預熱時間,重新計算權重會很小
  56.  
            int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
  57.  
            return ww < 1 ? 1 : (ww > weight ? weight : ww);
  58.  
        }
  59.  
     
  60.  
        protected int getWeight(Invoker<?> invoker, Invocation invocation) {
  61.  
     
  62.  
            // 獲取invoker設置權重值默認權重=100
  63.  
            int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
  64.  
     
  65.  
            // 如果權重大於0
  66.  
            if (weight > 0) {
  67.  
     
  68.  
                // 服務提供者發佈服務時間戳
  69.  
                long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
  70.  
                if (timestamp > 0L) {
  71.  
     
  72.  
                    // 服務已經發布多少時間
  73.  
                    int uptime = (int) (System.currentTimeMillis() - timestamp);
  74.  
     
  75.  
                    // 預熱時間默認10分鐘
  76.  
                    int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
  77.  
     
  78.  
                    // 生產者發佈時間大於0但是小於預熱時間
  79.  
                    if (uptime > 0 && uptime < warmup) {
  80.  
     
  81.  
                        // 重新計算權重值
  82.  
                        weight = calculateWarmupWeight(uptime, warmup, weight);
  83.  
                    }
  84.  
                }
  85.  
            }
  86.  
            // 服務發佈時間大於預熱時間直接返回設置權重值
  87.  
            return weight >= 0 ? weight : 0;
  88.  
        }
  89.  
    }

 

3 QPS上升

上面章節大篇幅討論了由於RT上升造成的線程池打滿問題,現在我們討論另一個參數QPS。當上遊流量激增會導致創建大量線程池,也會造成線程池打滿問題。這時如果發現QPS超出了系統承受能力,我們不得不採用降級方案保護系統,請參看我之前文章《從反脆弱角度談技術系統的高可用性》

 

4 文章總結

本文首先介紹了DUBBO線程模型和線程池策略,然後我們引出了公式,發現併發量受RT和QPS兩個參數影響,這兩個參數任意一個上升都可以造成線程池打滿問題。生產者出現慢服務或者預熱不充分都有可能造成RT上升,而上游流量激增會造成QPS上升,同時本文也給出瞭解決方案。DUBBO線程池打滿是一個必須重視的問題,希望本文對大家有所幫助。

— 本文結束 —

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章