團隊小伙伴突然找到我們團隊說,不得了了,線上的RocketMQ又出現了消費停滯,怎么辦? 要不要我們先重啟一下
其實早在之前也出現過一次,當時為了快速恢復業務的,就直接重啟解決的。
這次因為堆積量不多,所以想對運行環境進行一些環境快照保留。所以就和業務方溝通是否看見接受短暫的消息延時
得到肯定答案后就是放手干吧!
首先我們要確定業務反饋的是否屬實,所以需要去RocketMQ dashboard上看看具體的消費進度。
圖片
可以看到consumer中并不是全部的queue消費都堆積了,只有一個queuq消息堆積了。
這個消費者訂閱的topic是分區有序的,正常來說分區有序,如果某個分區的消息單條消息出現了消費異常,必須要等這條消息消費成功(或者是重試結束)后才能繼續消費后面的消息。
有時候會因為這個原因出現消息堆積是正常的,但是業務對消息重試進行了合理的設置,設置的重試次數比較合理,不會出現長時間的堆積。
一般出現這種問題很明顯就是線程出現了死鎖或者僵死之類的情況。
熟悉RocketMQ的都知道RocketMQ消費消息主要是依賴1個線程1個線程池。
圖片
圖片
直到了RocketMQ的消費線程模型后我們就好解決了。我們直接通過jstack命令查看線程的堆棧信息。
我們直接通過jstack命令生成線程快照。
jstack <pid> > thread_dump_$(date +%Y%m%d_%H%M%S).txt
pid 和后面的 thread_dump_$(date +%Y%m%d_%H%M%S).txt自己隨便取個名字就行。自己記得就行。
由于應用運行在pod中,生成了我們就下載到本地。
我們自己看還是比較難分析出分體。這里我們直接使用一個在線的網站進行線程快照的分析。
fastthread是一個在線的線程快照分析工具,可以直接將線程快照上傳到這個網站進行分析。
圖片
我們上傳我們下載的線程快照文件。
然后進行線程分析:
圖片
很快定位到阻塞其他線程的代碼。
這里的代碼被我打碼了。
如果我們使用arthas也可以很方便的找到阻塞的線程。
thread -b
arthas 提供了thread -b, 一鍵找出那個罪魁禍首。
通過阻塞代碼我們很快定位到是由于線程池使用不當導致的阻塞。
什么情況下會出現線程池使用不當導致的"死鎖"呢?
我們看看下面的demo:
public class XiaoZouExample { public static void main(String[] args) { ExecutorService executor = new ThreadPoolExecutor(2, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); // 提交兩個外部任務 for (int i = 0; i < 2; i++) { executor.submit(new OuterTask(executor)); } // 等待一段時間后關閉線程池 try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } executor.shutdown(); } static class OuterTask implements Runnable { private final ExecutorService executor; public OuterTask(ExecutorService executor) { this.executor = executor; } @Override public void run() { System.out.println("小奏技術 Outer task started by thread: " + Thread.currentThread().getName()); // 創建一個Future來等待內部任務的結果 Future<?> future = executor.submit(new InnerTask()); try { // 等待內部任務完成 future.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } System.out.println("小奏技術 Outer task finished by thread: " + Thread.currentThread().getName()); } } static class InnerTask implements Runnable { @Override public void run() { System.out.println("小奏技術 Inner task started by thread: " + Thread.currentThread().getName()); try { // 模擬長時間運行的任務 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("小奏技術 Inner task finished by thread: " + Thread.currentThread().getName()); } }}
圖片
可以看到沒有任何任務執行完成,線程池一直處于被阻塞狀態。
核心原因就是首先線程池的核心線程數是2,核心線程用來執行2個任務,用完了所有線程。
然后在核心線程執行的2個任務中又用原來的線程池進行執行任務,這時候因為沒有線程可以去執行任務了,所以會添加到阻塞隊列中等待核心線程執行完任務后再執行。
但是核心線程想要釋放任務又必須等待這兩個子任務執行完,這樣就形成了一個死鎖。
解決方式有多種,最簡單的方式可以考慮不要使用隊列,直接使用SynchronousQueue。
ExecutorService executor = new ThreadPoolExecutor(2, 5, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
讓多的任務直接通過主線程執行或者丟棄任務。
當然最好的方式就是避免這種情況的發生,合理的使用線程池,不要線程池中的任務還要使用這個線程池去執行任務。
這種情況是比較難避免的,因為現在大部分業務開發都是隱式使用線程池,自己也不知道自己用的哪個線程池。
比如spring的@Async注解,@Scheduled注解等等。
線程池的使用還是要盡量消息,要避免出現線程池中的任務繼續使用該線程池去執行任務,出現死鎖。
也可以考慮對線程池進行監控,避免出現大量任務阻塞。
這個問題想要復現需要大量任務并且超過核心線程數才能復現,還是比較難復現的,只有線上大流量的時候才能復現。
本文鏈接:http://www.tebozhan.com/showinfo-26-112791-0.html記一次線程池使用不當觸發死鎖導致RocketMQ消費停滯
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com