如何用利特爾法則調整執行緒池大小

一直奮鬥的程式猿發表於2020-05-26

利特爾法則

利特爾法則派生於排隊論,用以下數學公式表示:

L = λW

L 系統中存在的平均請求數量。

λ 請求有效到達速率。例如:5/s 表示每秒有5個請求到達系統。

W 請求在系統中的平均等待執行時間。

排隊論:研究服務系統中排隊現象隨機規律的學科,探究排隊有關的數量指標的概率規律性。

場景

我們先假設一個店鋪員工調整場景。

前提

  • 每個客戶一次只買一隻炸雞;

  • 每位員工製作一個炸雞需要1分鐘。

  • 客戶買炸雞時等待時間越短,體驗越好。

如果你是一家炸雞店老闆,今年受疫情影響需要對店裡的員工進行調整,你會如何處理?

這個問題本質就是員工利用率客戶體驗之間的權衡。

  1. 為了讓客戶保持極佳體驗,需要保持員工數量或增加員工;

  2. 為避免資源浪費,控制人力成本,需要裁減空閒員工。

假設店裡目前有3名員工。你如何進行員工調整決策。我們分析以下幾種情形。

平均客流量 = 3人/分鐘 客戶等待時間稍短,體驗良好,並且員工工作都是飽和。此時不需要調整。

=3人/分鐘

平均客流量 < 3人/分鐘 客戶等待時間稍短,體驗良好,但是始終有一個員工在打醬油,此時可以考慮減裁一人。

< 3人/分鐘

平均客流量 > 3人/分鐘 客戶5,6,7等待時間延長體驗稍差,此時可以根據實際情況增加員工。

客流量  > 3人/分鐘

平均每分鐘客流量 ≈ 員工數 為最佳。

執行緒池

其實執行緒池處理也算是一個排隊模型。簡化Java執行緒池處理模型如下:

執行緒池任務執行大致階段:提交 --> 入佇列或直接執行 ---> 實際執行

執行緒池

  • 任務提交頻率:每秒任務提交數;

  • 任務佇列等待平均耗時:任務佇列等待總耗時除以實際執行數;

  • 任務實際執行平均耗時:任務實際執行總耗時除以實際執行數;

  • 任務執行平均耗時:任務佇列等待平均耗時加任務實際執行平均耗時;

我們可以根據以下指標來評估調整執行緒池引數

執行緒池中平均任務數 = 任務提交頻率 * 任務執行平均耗時

執行緒等待耗時與響應時間比率 = 任務佇列等待總耗時 / (任務佇列等待總耗時 + 任務實際執行總耗時


執行緒等待耗時與響應時間比率 過高,說明任務排隊較多,評估當前執行緒池大小是否合理,結合系統負載進行相應調整。

執行緒池中平均任務數 < 目前執行緒池大小 應適當減少執行緒數量。

系統平均處理任務數 > 目前執行緒池大小 在這種情況下,先評估當前系統是否有能力支撐更大的執行緒數量(如CPU數,記憶體等),然後再進行調整。

程式碼片段

@Slf4j
public class MonitoredThreadPoolExecutor extends ThreadPoolExecutor {

    //任務提交成功時間
    private final ConcurrentHashMap<Runnable, Long> timeOfRequest = new ConcurrentHashMap<>();
    //任務實際開始執行時間
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();
    //上一個任務提交成功時間
    private long lastArrivalTime;

    // 任務實際執行總數
    private final AtomicInteger numberOfRequestsRetired = new AtomicInteger();
    // 任務提交總數
    private final AtomicInteger numberOfRequests = new AtomicInteger();
    // 任務實際執行總耗時
    private final AtomicLong totalServiceTime = new AtomicLong();
    // 任務在佇列等待總耗
    private final AtomicLong totalPoolTime = new AtomicLong();
    // 新任務提交總耗時
    private final AtomicLong aggregateInterRequestArrivalTime = new AtomicLong();


    public MonitoredThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                       BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    protected void beforeExecute(Thread worker, Runnable task) {
        super.beforeExecute(worker, task);
        startTime.set(System.nanoTime());
    }

    @Override
    protected void afterExecute(Runnable task, Throwable t) {
        try {
            long start = startTime.get();
            totalServiceTime.addAndGet(System.nanoTime() - start);
            totalPoolTime.addAndGet(start - timeOfRequest.remove(task));
            numberOfRequestsRetired.incrementAndGet();
        } finally {
            if (null != t) {
                log.error(AppSystem.ERROR_LOG_PREFIX + "執行緒池處理異常:", Throwables.getRootCause(t));
            }
            super.afterExecute(task, t);
        }
    }

    @Override
    public void execute(Runnable task) {
        long now = System.nanoTime();
        numberOfRequests.incrementAndGet();
        synchronized (this) {
            if (lastArrivalTime != 0L) {
                aggregateInterRequestArrivalTime.addAndGet(now - lastArrivalTime);
            }
            lastArrivalTime = now;
            timeOfRequest.put(task, now);
        }
        super.execute(task);
    }
}

測試

兩組迭代請求,一次提交10個任務,執行緒數為1

執行緒數1

兩組迭代請求,一次提交10個任務,執行緒數為10

執行緒數10

兩組迭代請求,一次提交10個任務,執行緒數為50

執行緒數50

上面測試比較片面。現實應根據系統長期平均指標進行調整。

總結

利特爾法則應用場景很多。歡迎大家留言交流!

相關文章