踩坑①
現象:新版生產者發不出去消息,broker端也沒收到消息
原因:舊版生產者有個配置"producer.type",async爲異步發送,sync爲同步發送,默認爲同步發送;而新版本廢棄了該配置,於是每次調用send方法時候會將消息緩存在本地的buffer中而不是立即發送,只有等到消息總大小或到達批處理髮送的間隔時間纔會把消息發出去,而發送代碼如下:
ZzKafkaProducer producer = KafkaManager.registerKafkaProducer("lxr", "xxx.xxx.xxx.1:9092,xxx.xxx.xxx.2:9092,xxx.xxx.xxx.3:9092"); for (int i = 0; i <= 100; i++) {
producer.send("k1", "lxrlxr" + i + " 0");
}
System.out.println("finish!");
使用舊版不會有問題,而使用新版本的生產者就會有問題,消息沒來得及發送出去就因程序執行完了而清理掉,可以通過設置batch.size來自定義批量發送的大小
ps:這裏新版默認是大於1.x.x的,舊版默認爲小於1.x.x的
踩坑②
現象:消費者端阻塞,消費不到數據,不拋異常,服務器上的zookeeper會不斷打印如下日誌
原因:使用新版消費者的連接配置爲bootstrap.servers,對應的是broker的ip與端口,誤使用了zookeeper的ip與端口,便會導致一直打開zk連接而zk會自動斷開連接
踩坑③
現象:多線程下消費者一運行就拋ConcurrentModificationException
原因:新版消費者不是線程安全的,一個消費者只能對應一個線程去消費,KafkaConsumer很多絕大多數方法執行前都會調用如下方法:
private final AtomicInteger refcount = new AtomicInteger(0);
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();
}
方法執行後會調用如下方法:
private void release() {
if (refcount.decrementAndGet() == 0)
currentThread.set(NO_CURRENT_THREAD);
}
相當於jdk的Lock接口的lock()與unlock()之間的關係
踩坑④
現象:生產者發送數據都成功,消費者也能消費,但必定會丟一部分數據,而且客戶端也沒報任何異常,到服務器查看 broker與zookeeper的日誌均沒任何異常信息
原因:kafka客戶端新舊版本2個版本的生產者與消費者之間不兼容,生產者使用老版本(kafka.javaapi.producer.Producer),而消費者使用的是新版本(org.apache.kafka.clients.consumer.Consumer)
踩坑⑤
現象:在某個時間段內,消費者消費數據速度越來越慢,最後不消費,到zookeeper上看發現消費者全部死亡
原因:消費者端爲保證每個分區的消費者消費數據的進度都差不多,於是當有分區消費速度比其他分區的快的時候,在消費者線程執行了Thread.sleep()方法,導致該分區消費者一直在sleep,而kafka消費者端有個delay task是做客戶端與服務端的心跳任務的,該delay task是每次消費者到broker拉取數據時候觸發的,消費者一直在sleep,因此沒去拉取數據,delay task沒觸發,導致心跳任務沒執行,broker端認爲該消費者死亡,從zookeeper中移除了
消費速度遠低於生產速度或各個分區消費能力差別較大時候的解決方案
上面之所以會有踩坑⑤,歸根到底就是生產者生產消息速度遠大於消費者的消費能力,且各個分區消費者的消費能力差別大,因此需要解決消費者端的問題
1.第一種方式,最簡單粗暴,直接給topic增加分區,增加消費者數量,這個方案的缺點就是,當生產者的生產速度不是恆定的時候,比如早上是消息生產的高峯期,而中午是低峯期,高峯與低峯消息數量差距很大,這種方案會導致資源閒置,浪費資源
2.第二種方式,比較適合實時性要求比較高且容許丟失部分數據的場景,可以要求生產者端發送消息時候帶上create_time字段,對於消費能力差的,直接丟棄一批過時or不滿足要求的數據,直接跳過去拉取較新的數據
3.第三種方式,kafka的消費者api提供了pause方法,可以暫停消費某個topic指定的分區數據,但是delay task依舊會觸發,消費者不會被認爲是死亡,但是這種方式,需要消費者線程之間進行通信,如:發現分區1消費進度比其他分區快很多時,分區1的消費者線程調用pause方法暫停消費,當其他分區消費進度趕上來時候,分區1的消費者線程再調用resume方法繼續消費,實現起來較爲複雜
4.第四種方式,壓軸的放最後,使用spring-kafka吧,spring-kafka會爲消費者線程創建一個阻塞隊列,從broker拉取數據後就丟到阻塞隊列中,然後再從阻塞隊列取數據進行處理,並且如果阻塞隊列滿了導致取到的數據塞不進去的話,spring-kafka會調用kafka的pause方法,則consumer會停止從kafka裏面繼續再拿數據,接着spring-kafka還會處理一些異常的情況,比如失敗之後是不是需要commit offset這樣的邏輯等等
這部分完~