STORM線上業務實踐-叢集空閑CPU飆高問題排查

來源:互聯網
上載者:User

標籤:

最近將公司的線上業務遷移到Storm叢集上,上線後遇到低峰期CPU耗費嚴重的情況。在解決問題的過程中深入瞭解了storm的內部實現原理,並且解決了一個storm0.9-0.10版本一直存在的嚴重bug,目前代碼已經合并到了storm新版本中,在這篇文章裡會介紹這個問題出現的情境、分析思路、解決的方式和一些個人的收穫。背景

首先簡單介紹一下Storm,熟悉的同學可以直接跳過這段。

Storm是Twitter開源的一個大資料處理架構,專註於流式資料的處理。Storm通過建立拓撲結構(Topology)來轉換資料流。和Hadoop的作業(Job)不同,Topology會持續轉換資料,除非被叢集關閉。

是一個簡單的Storm Topology結構圖。

可以看出Topology是由不同組件(Component)串/並聯形成的有向圖。資料元組(Tuple)會在Component之間通過資料流的形式進行有向傳遞。Component有兩種

  • Spout:Tuple來源節點,持續不斷的產生Tuple,形成資料流
  • Bolt:Tuple處理節點,處理收到的Tuple,如果有需要,也可以產生新的Tuple傳遞到其他Bolt

目前業界主要在離線或者對即時性要求不高業務中使用Storm。隨著Storm版本的更迭,可靠性和即時性在逐漸增強,已經有運行線上業務的能力。因此我們嘗試將一些即時性要求在百毫秒級的線上業務遷入Storm叢集。

現象
  1. 某次高峰時,Storm上的一個業務拓撲頻繁出現訊息處理延遲。延時達到了10s甚至更高。查看高峰時的物理機指標監控,CPU、記憶體和IO都有很大的餘量。判斷是隨著業務增長,服務流量逐漸增加,某個Bolt之前設定的並行度不夠,導致訊息堆積了。
  2. 臨時增加該Bolt並行度,解決了延遲的問題,但是第二天的低峰期,服務突然警示,CPU負載過高,達到了100%。
排查
  1. 用Top看了下CPU佔用,系統調用佔用了70%左右。再用wtool對Storm的背景工作處理序進行分析,找到了CPU佔用最高的線程
    java.lang.Thread.State: TIMED_WAITING (parking)        at sun.misc.Unsafe.park(Native Method)        - parking to wait for  <0x0000000640a248f8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2163)        at com.lmax.disruptor.BlockingWaitStrategy.waitFor(BlockingWaitStrategy.java:87)        at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:54)        at backtype.storm.utils.DisruptorQueue.consumeBatchWhenAvailable(DisruptorQueue.java:97)        at backtype.storm.disruptor$consume_batch_when_available.invoke(disruptor.clj:80)        at backtype.storm.daemon.executor$fn__3441$fn__3453$fn__3500.invoke(executor.clj:748)        at backtype.storm.util$async_loop$fn__464.invoke(util.clj:463)        at clojure.lang.AFn.run(AFn.java:24)        at java.lang.Thread.run(Thread.java:745)

    我們可以看到這些線程都在訊號量上等待。調用的來源是disruptor$consume_batch_when_available。

  2. disruptor是Storm內部訊息隊列的封裝。所以先瞭解了一下Storm內部的訊息傳輸機制。

    (圖片來源Understanding the Internal Message Buffers of Storm)

    Storm的工作節點稱為Worker(其實就是一個JVM進程)。不同Worker之間通過Netty(舊版Storm使用ZeroMQ)進行通訊。

    每個Worker內部包含一組Executor。Strom會為拓撲中的每個Component都分配一個Executor。在實際的資料處理流程中,資料以訊息的形式在Executor之間流轉。Executor會迴圈調用綁定的Component的處理方法來處理收到的訊息。

    Executor之間的訊息傳輸使用隊列作為訊息管道。Storm會給每個Executor分配兩個隊列和兩個處理線程。

    • 背景工作執行緒:讀取接收隊列,對訊息進行處理,如果產生新的訊息,會寫入發送隊列
    • 發送線程:讀取發送隊列,將訊息發送其他Executor

    當Executor的發送線程發送訊息時,會判斷目標Executor是否在同一Worker內,如果是,則直接將訊息寫入目標Executor的接收隊列,如果不是,則將訊息寫入Worker的傳輸隊列,通過網路發送。

    Executor工作/發送線程讀取隊列的代碼如下,這裡會迴圈調用consume-batch-when-available讀取隊列中的訊息,並對訊息進行處理。

    (async-loop  (fn []    ...    (disruptor/consume-batch-when-available receive-queue event-handler)                ...    ))
  3. 我們再來看一下consume_batch_when_available這個函數裡做了什麼。
    (defn consume-batch-when-available  [^DisruptorQueue queue handler]  (.consumeBatchWhenAvailable queue handler))

    前面提到Storm使用隊列作為訊息管道。Storm作為流式大資料處理架構,對訊息傳輸的效能很敏感,因此使用了高效記憶體隊列Disruptor Queue作為訊息佇列。

    Disruptor Queue是LMAX開源的一個無鎖記憶體隊列。內部實現如下。

    (圖片來源Disruptor queue Introduction)

    Disruptor Queue通過Sequencer來管理隊列,Sequencer內部使用RingBuffer儲存訊息。RingBuffer中訊息的位置使用Sequence表示。隊列的生產消費過程如下

    • Sequencer使用一個Cursor來儲存寫入位置。
    • 每個Consumer都會維護一個消費位置,並註冊到Sequencer。
    • Consumer通過SequenceBarrier和Sequencer進行互動。Consumer每次消費時,SequenceBarrier會比較消費位置和Cursor來判斷是否有可用訊息:如果沒有,會按照設定的策略等待訊息;如果有,則讀取訊息,修改消費位置。
    • Producer在寫入前會查看所有消費者的消費位置,在有可用位置時會寫入訊息,更新Cursor。

    查看DisruptorQueue.consumeBatchWhenAvailable實現如下

    final long nextSequence = _consumer.get() + 1;final long availableSequence = _barrier.waitFor(nextSequence, 10, TimeUnit.MILLISECONDS);if (availableSequence >= nextSequence) {    consumeBatchToCursor(availableSequence, handler);}

    繼續查看_barrier.waitFor方法

    public long waitFor(final long sequence, final long timeout, final TimeUnit units) throws AlertException, InterruptedException {    checkAlert();    return waitStrategy.waitFor(sequence, cursorSequence, dependentSequences, this, timeout, units);}

    Disruptor Queue為消費者提供了若干種訊息等待策略

    • BlockingWaitStrategy:阻塞等待,CPU佔用小,但是會切換線程,延遲較高
    • BusySpinWaitStrategy:自旋等待,CPU佔用高,但是無需切換線程,延遲低
    • YieldingWaitStrategy:先自旋等待,然後使用Thread.yield()喚醒其他線程,CPU佔用和延遲比較均衡
    • SleepingWaitStrategy:先自旋,然後Thread.yield(),最後調用LockSupport.parkNanos(1L),CPU佔用和延遲比較均衡

    Storm的預設等待策略為BlockingWaitStrategy。BlockingWaitStrategy的waitFor函數實現如下

    if ((availableSequence = cursor.get()) < sequence) {        lock.lock();        try {            ++numWaiters;            while ((availableSequence = cursor.get()) < sequence) {                barrier.checkAlert();                if (!processorNotifyCondition.await(timeout, sourceUnit)) {                    break;                }            }        }        finally {            --numWaiters;            lock.unlock();        }}

    BlockingWaitStrategy內部使用訊號量來阻塞Consumer,當await逾時後,Consumer線程會被自動喚醒,繼續迴圈查詢可用訊息。這裡的實現有個BUG,在processorNotifyCondition.await逾時時應該迴圈查詢,但是代碼中實際上跳出了迴圈,直接返回的當前的cursor,

  4. 而DisruptorQueue.consumeBatchWhenAvailable方法中可以看到,Storm此處設定逾時為10ms。推測在沒有訊息或者訊息量較少時,Executor在消費隊列時會被阻塞,由於逾時時間很短,背景工作執行緒會頻繁逾時,再加上BlockingWaitStrategy的BUG,consumeBatchWhenAvailable會被頻繁調用,導致CPU佔用飆高。

    嘗試將10ms修改成100ms,編譯Storm後重新部署叢集,使用Storm的demo拓撲,將bolt並發度調到1000,修改spout代碼為10s發一條訊息。經測試CPU佔用大幅減少。

    再將100ms改成1s,測試CPU佔用基本降為零。

  5. 但是隨著調高逾時,測試時並沒有發現訊息處理有延時。繼續查看BlockingWaitStrategy代碼,發現Disruptor Queu的Producer在寫入訊息後會喚醒等待的Consumer。

    if (0 != numWaiters){    lock.lock();    try    {        processorNotifyCondition.signalAll();    }    finally    {        lock.unlock();    }}

    這樣,Storm的10ms逾時就很奇怪了,沒有減少訊息延時,反而增加了系統負載。帶著這個疑問查看代碼的上下文,發現在構造DisruptorQueue對象時有這麼一句注釋

    ;; :block strategy requires using a timeout on waitFor (implemented in DisruptorQueue),         as sometimes the consumer stays blocked even when there‘s an item on the queue.(defnk disruptor-queue    [^String queue-name buffer-size :claim-strategy :multi-threaded :wait-strategy :block]    (DisruptorQueue. queue-name                ((CLAIM-STRATEGY claim-strategy) buffer-size)                (mk-wait-strategy wait-strategy)))

    Storm使用的Disruptor Queue版本為2.10.1。查看Disruptor Queue的change log,發現該版本的BlockingWaitStrategy有潛在的並發問題,可能導致某條訊息在寫入時沒有喚醒等待的消費者。

    2.10.2 Released (21-Aug-2012)

    • Bug fix, potential race condition in BlockingWaitStrategy.
    • Bug fix set initial SequenceGroup value to -1 (Issue #27).
    • Deprecate timeout methods that will be removed in version 3.

    因此Storm使用了短逾時,這樣在出現並發問題時,沒有被喚醒的消費方也會很快因為逾時重新查詢可用訊息,防止出現訊息延時。

    這樣如果直接修改逾時到1000ms,一旦出現並發問題,最壞情況下訊息會延遲1000ms。在權衡效能和延時之後,我們在Storm的設定檔中增加配置項來修改逾時參數。這樣使用者可以自己選擇保證低延時還是低CPU佔用率。

  6. 就BlockingWaitStrategy的潛在並發問題諮詢了Disruptor Queue的作者,得知2.10.4版本已經修複了這個並發問題(Race condition in 2.10.1 release
    )。

    將Storm依賴升級到此版本。但是對Disruptor Queue的2.10.1做了並發測試,無法複現這個並發問題,因此也無法確定2.10.4是否徹底修複。謹慎起見,在升級依賴的同時保留了之前的逾時配置項,並將預設逾時調整為1000ms。經測試,在叢集空閑時CPU佔用正常,並且壓測也沒有出現訊息延時。

總結
    1. 關於叢集空閑CPU反而飆高的問題,已經向Storm社區提交PR並且已被接受[STORM-935] Update Disruptor queue version to 2.10.4。線上業務流量通常起伏很大,如果被這個問題困擾,可以考慮應用此patch。
    2. Storm UI中可以看到很多有用的資訊,但是缺乏記錄,最好對其進行二次開發(或者直接讀取ZooKeeper中資訊),記錄每個時間段的資料,方便分析叢集和拓撲健全狀態
    3. 轉http://daiwa.ninja/index.php/2015/07/18/storm-cpu-overload/

STORM線上業務實踐-叢集空閑CPU飆高問題排查(轉)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.