使用多執行緒增加kafka消費能力

小姐姐味道發表於2019-03-25

前提:本例適合那些沒有順序要求的訊息主題。

kafka通過一系列優化,寫入和讀取速度能夠達到數萬條/秒。通過增加分割槽數量,能夠通過部署多個消費者增加並行消費能力。但還是有很多情況下,某些業務的執行速度實在是太慢,這個時候我們就要用到多執行緒去消費,提高應用機器的利用率,而不是一味的給kafka增加壓力。

使用多執行緒增加kafka消費能力
使用Spring建立一個kafka消費者是非常簡單的。我們選擇的方式是繼承kafka的ShutdownableThread,然後實現它的doWork方法即可。


參考:github.com/apache/kafk…

多執行緒消費某個分割槽的資料

即然是使用多執行緒,我們就需要新建一個執行緒池。

使用多執行緒增加kafka消費能力
我們建立了一個最大容量為20的執行緒池,其中有兩個引數需要注意一下。(參考《JAVA多執行緒使用場景和注意事項簡版》)。

我們使用了了零容量的SynchronousQueue,一進一出,避免佇列裡緩衝資料,這樣在系統異常關閉時,就能排除因為阻塞佇列丟訊息的可能。 然後使用了CallerRunsPolicy飽和策略,使得多執行緒處理不過來的時候,能夠阻塞在kafka的消費執行緒上。

然後,我們將真正處理業務的邏輯放在任務中多執行緒執行,每次執行完畢,我們都手工的commit一次ack,表明這條訊息我已經處理了。由於是執行緒池認領了這些任務,順序性是無法保證的,可能有些任務沒有執行完畢,後面的任務就已經把它的offset給提交了。o.O

不過這暫時不重要,首先讓它並行化執行就好。

使用多執行緒增加kafka消費能力
可惜的是,當我們執行程式,直接丟擲了異常,無法進行下去。
使用多執行緒增加kafka消費能力
程式直接說了:

KafkaConsumer is not safe for multi-threaded access
複製程式碼

顯然,kafka的消費端不是執行緒安全的,它拒絕你這麼呼叫它的api。kafka的初衷是好的,想要避免一些併發環境的問題,但我確實需要使用多執行緒處理。

kafka消費者通過比較呼叫者的執行緒id來判斷是否是由外部執行緒發起請求。

private void acquire() {
複製程式碼
    long threadId = Thread.currentThread().getId();
    if (threadId != currentThread.get() && !currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))
        throw new ConcurrentModificationException("KafkaConsumer is not safe for multi-threaded access");
    refcount.incrementAndGet();
複製程式碼

}

複製程式碼

得,只能將commitSync函式放線上程外面了,先提交ack、再執行任務。

加入管道

我們獲取的訊息,可能在真正被執行之前,會進行一些過濾,比如一些空值或者特定條件的判斷。雖然可以直接放在消費者執行緒裡執行,但顯的特別的亂,可以加入一個生產者消費者模型(你可以認為這是畫蛇添足)。這裡採用的是阻塞佇列依然是SynchronousQueue,它充當了管道的功能。

使用多執行緒增加kafka消費能力

我們把任務放入管道後,立馬commit。如果執行緒池已經滿了,將一直阻塞在消費者執行緒裡,直到有空缺。然後,我們單獨啟動了一個執行緒,用來接收這些資料,然後提交到這部分的程式碼看起來大概這樣。

使用多執行緒增加kafka消費能力

應用能夠啟動了,消費速度賊快。

引數配置

kafka的引數非常的多,我們比較關心的有以下幾個引數。

max.poll.records

呼叫一次poll,返回的最大條數。這個值設定的大,那麼處理的就慢,很容易超出max.poll.interval.ms的值(預設5分鐘),造成消費者的離線。在耗時非常大的消費中,是需要特別注意的。

enable.auto.commit

是否開啟自動提交(offset)如果開啟,consumer已經消費的offset資訊將會間歇性的提交到kafka中(持久儲存)

當開啟offset自動提交時,提交請求的時間頻率由引數auto.commit.interval.ms控制。

fetch.max.wait.ms

如果broker端反饋的資料量不足時(fetch.min.bytes),fetch請求等待的最長時間。如果資料量滿足需要,則立即返回。

session.timeout.ms

consumer會話超時時長,如果在此時間內,server尚未接收到consumer任何請求(包括心跳檢測),那麼server將會判定此consumer離線。此值越大,server等待consumer失效、rebalance時間就越長。

heartbeat.interval.ms

consumer協調器與kafka叢集之間,心跳檢測的時間間隔。kafka叢集通過心跳判斷consumer會話的活性,以判斷consumer是否線上,如果離線則會把此consumer註冊的partition分配(assign)給相同group的其他consumer。此值必須小於“session.timeout.ms”,即會話過期時間應該比心跳檢測間隔要大,通常為session.timeout.ms的三分之一,否則心跳檢測就失去意義。


在本例中,我們的引數簡單的設定如下,主要調整了每次獲取的條數和檢測時間。其他的都是預設。

使用多執行緒增加kafka消費能力

訊息保證

仔細的同學可能會看到,我們的程式碼依然不是完全安全的。這是由於我們提前提交了ack導致的。程式正常執行下,這無傷大雅。但在應用異常關閉的時候,那些正在執行中的訊息,很可能會丟失,對於一致性要求非常高的應用,我們要從兩個手段上進行保證。

使用關閉鉤子

第一種就是考慮kill -15的情況。這種方式比較簡單,只要覆蓋ShutdownableThread的shutdown方法即可,應用將有機會執行執行緒池中的任務,確保消費完畢再關閉應用。

@Override
    public void shutdown() {
        super.shutdown();
        executor.shutdown();
}
複製程式碼

使用日誌處理

應用oom,或者直接kill -9了,事情就變得麻煩起來。

維護一個單獨的日誌檔案(或者本地db),在commit之前寫入一條日誌,然後在真正執行完畢之後寫入一條對應的日誌。當系統啟動時,讀取這些日誌檔案,獲取沒有執行成功的任務,重新執行。

想要效率,還想要可靠,是得下點苦力氣的。

藉助redis處理

這種方式與日誌方式類似,但由於redis的效率很高(可達數萬),而且方便,是優於日誌方式的。

可以使用Hash結構,提交任務的同時寫入Redis,任務執行完畢刪掉這個值,那麼剩下的就是出現問題的訊息。

使用多執行緒增加kafka消費能力
在系統啟動時,首先檢測一下redis中是否有異常資料。如果有,首先處理這些資料,然後正常消費。

End

多執行緒是為了增加效率,redis等是為了增加可靠性。業務程式碼是非常好編寫的,搞懂了邏輯就搞定了大部分;業務程式碼有時候又是困難的,你要編寫大量輔助功能增加它的效率、照顧它的邊界。

以程式設計師的角度來說,最有競爭力的程式碼都是為了照顧小概率發生的邊界異常。

kafka在吞吐量和可靠性方面,有各種的權衡,很多都是魚和熊掌的關係。不必糾結於它本身,我們可以藉助外部的工具,獲取更大的收益。在這種情況下,redis當機與應用同時當機的概率還是比較小的。5個9的訊息保證是可以做到的,剩下的那點不完美問題訊息,你為什麼不從日誌裡找呢?


擴充套件閱讀:

1、JAVA多執行緒使用場景和注意事項簡版

2、Kafka基礎知識索引

3、360度測試:KAFKA會丟資料麼?其高可用是否滿足需求?

使用多執行緒增加kafka消費能力

相關文章