Kafka Offset Storage

哥不是小蘿莉發表於2017-01-09

1.概述

  目前,Kafka 官網最新版[0.10.1.1],已預設將消費的 offset 遷入到了 Kafka 一個名為 __consumer_offsets 的Topic中。其實,早在 0.8.2.2 版本,已支援存入消費的 offset 到Topic中,只是那時候預設是將消費的 offset 存放在 Zookeeper 叢集中。那現在,官方預設將消費的offset儲存在 Kafka 的Topic中,同時,也保留了儲存在 Zookeeper 的介面,通過 offsets.storage 屬性來進行設定。

2.內容

  其實,官方這樣推薦,也是有其道理的。之前版本,Kafka其實存在一個比較大的隱患,就是利用 Zookeeper 來儲存記錄每個消費者/組的消費進度。雖然,在使用過程當中,JVM幫助我們完成了自一些優化,但是消費者需要頻繁的去與 Zookeeper 進行互動,而利用ZKClient的API操作Zookeeper頻繁的Write其本身就是一個比較低效的Action,對於後期水平擴充套件也是一個比較頭疼的問題。如果期間 Zookeeper 叢集發生變化,那 Kafka 叢集的吞吐量也跟著受影響。

  在此之後,官方其實很早就提出了遷移到 Kafka 的概念,只是,之前是一直預設儲存在 Zookeeper叢集中,需要手動的設定,如果,對 Kafka 的使用不是很熟悉的話,一般我們就接受了預設的儲存(即:存在 ZK 中)。在新版 Kafka 以及之後的版本,Kafka 消費的offset都會預設存放在 Kafka 叢集中的一個叫 __consumer_offsets 的topic中。

  當然,其實她實現的原理也讓我們很熟悉,利用 Kafka 自身的 Topic,以消費的Group,Topic,以及Partition做為組合 Key。所有的消費offset都提交寫入到上述的Topic中。因為這部分訊息是非常重要,以至於是不能容忍丟資料的,所以訊息的 acking 級別設定為了 -1,生產者等到所有的 ISR 都收到訊息後才會得到 ack(資料安全性極好,當然,其速度會有所影響)。所以 Kafka 又在記憶體中維護了一個關於 Group,Topic 和 Partition 的三元組來維護最新的 offset 資訊,消費者獲取最新的offset的時候會直接從記憶體中獲取。

3.實現

  那我們如何實現獲取這部分消費的 offset,我們可以在記憶體中定義一個Map集合,來維護消費中所捕捉到 offset,如下所示:

protected static Map<GroupTopicPartition, OffsetAndMetadata> offsetMap = new ConcurrentHashMap<>();

  然後,我們通過一個監聽執行緒來更新記憶體中的Map,程式碼如下所示:

private static synchronized void startOffsetListener(ConsumerConnector consumerConnector) {
        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
        topicCountMap.put(consumerOffsetTopic, new Integer(1));
        KafkaStream<byte[], byte[]> offsetMsgStream = consumerConnector.createMessageStreams(topicCountMap).get(consumerOffsetTopic).get(0);

        ConsumerIterator<byte[], byte[]> it = offsetMsgStream.iterator();
        while (true) {
            MessageAndMetadata<byte[], byte[]> offsetMsg = it.next();
            if (ByteBuffer.wrap(offsetMsg.key()).getShort() < 2) {
                try {
                    GroupTopicPartition commitKey = readMessageKey(ByteBuffer.wrap(offsetMsg.key()));
                    if (offsetMsg.message() == null) {
                        continue;
                    }
                    OffsetAndMetadata commitValue = readMessageValue(ByteBuffer.wrap(offsetMsg.message()));
                    offsetMap.put(commitKey, commitValue);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

  在拿到這部分更新後的offset資料,我們可以通過 RPC 將這部分資料共享出去,讓客戶端獲取這部分資料並視覺化。RPC 介面如下所示:

namespace java org.smartloli.kafka.eagle.ipc

service KafkaOffsetServer{
    string query(1:string group,2:string topic,3:i32 partition),
    string getOffset(),
    string sql(1:string sql),
    string getConsumer(),
    string getActiverConsumer()
}

  這裡,如果我們不想寫介面來操作 offset,可以通過 SQL 來操作消費的 offset 陣列,使用方式如下所示:

  • 引入依賴JAR
<dependency>
    <groupId>org.smartloli</groupId>
    <artifactId>jsql-client</artifactId>
    <version>1.0.0</version>
</dependency>
  • 使用介面
JSqlUtils.query(tabSchema, tableName, dataSets, sql);

  tabSchema:表結構;tableName:表名;dataSets:資料集;sql:操作的SQL語句。

4.預覽

  消費者預覽如下圖所示:

  正在消費的關係圖如下所示:

  消費詳細 offset 如下所示:

  消費和生產的速率圖,如下所示:

5.總結

  這裡,說明一下,當 offset 存入到 Kafka 的topic中後,消費執行緒ID資訊並沒有記錄,不過,我們通過閱讀Kafka消費執行緒ID的組成規則後,可以手動生成,其消費執行緒ID由:Group+ConsumerLocalAddress+Timespan+UUID(8bit)+PartitionId,由於消費者在其他節點,我們暫時無法確定ConsumerLocalAddress。最後,歡迎大家使用 Kafka 叢集監控 ——[ Kafka Eagle ],[ 操作手冊 ]。

6.結束語

  這篇部落格就和大家分享到這裡,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或傳送郵件給我,我會盡我所能為您解答,與君共勉!

相關文章