Kafka 核心技術與實戰》學習筆記

簡介

Apach Kafka 是一款分佈式流處理框架,用於實時構建流處理應用。它有一個核心的功能廣爲人知,即作爲企業級的消息引擎被廣泛使用。

很多主流消息引擎系統都支持 JMS(Java Message Service)規範,比如 ActiveMQ、RabbitMQ、IBM 的 WebSphere MQ 和 Apache Kafka。Kafka 並未完全遵照 JMS 規範。

像 RabbitMQ 或 ActiveMQ 這樣的傳統消息中間件,它們處理和響應消息的方式是破壞性的(destructive),即一旦消息被成功處理,就會被從 Broker 上刪除。

集羣部署

硬件

操作系統:Linux。IO模型;零拷貝
磁盤
磁盤容量
帶寬

I/O 模型與 Kafka 的關係又是什麼呢?實際上 Kafka 客戶端底層使用了 Java 的 selector,selector 在 Linux 上的實現機制是 epoll,而在 Windows 平臺上的實現機制是 select。因此在這一點上將 Kafka 部署在 Linux 上是有優勢的,因爲能夠獲得更高效的 I/O 性能。

主流的 I/O 模型通常有 5 種類型:阻塞式 I/O、非阻塞式 I/O、I/O 多路複用、信號驅動 I/O 和異步 I/O。每種 I/O 模型都有各自典型的使用場景,比如 Java 中 Socket 對象的阻塞模式和非阻塞模式就對應於前兩種模型;而 Linux 中的系統調用 select 函數就屬於 I/O 多路複用模型;大名鼎鼎的 epoll 系統調用則介於第三種和第四種模型之間;至於第五種模型,其實很少有 Linux 系統支持,反而是 Windows 系統提供了一個叫 IOCP 線程模型屬於這一種。

重要的參數配置

Broker參數

log.dirs=/home/kafka1,/home/kafka2,/home/kafka3

zookeeper.connect
多個 Kafka 集羣使用同一套 ZooKeeper 集羣,zookeeper.connect參數可以這樣指定:zk1:2181,zk2:2181,zk3:2181/kafka1和zk1:2181,zk2:2181,zk3:2181/kafka2。切記 chroot 只需要寫一次,而且是加到最後的。我經常碰到有人這樣指定:zk1:2181/kafka1,zk2:2181/kafka2,zk3:2181/kafka3,這樣的格式是不對的。

listeners:學名叫監聽器,其實就是告訴外部連接者要通過什麼協議訪問指定主機名和端口開放的 Kafka 服務。
advertised.listeners:和 listeners 相比多了個 advertised。Advertised 的含義表示宣稱的、公佈的,就是說這組監聽器是 Broker 用於對外發布的。

log.retention.hours=168
log.retention.bytes:這是指定 Broker 爲消息保存的總磁盤容量大小
message.max.bytes:控制 Broker 能夠接收的最大消息大小

Topic參數

retention.ms:規定了該 Topic 消息被保存的時長。默認是 7 天,即該 Topic 只保存最近 7 天的消息。一旦設置了這個值,它會覆蓋掉 Broker 端的全局參數值。(待驗證。oracle上面創建了kafka,broker參數爲100天,topic刪除爲默認7天。今天生產了消息,7天后看是否刪除消息。2022-03-27 00:52)

max.message.bytes。它決定了 Kafka Broker 能夠正常接收該 Topic 的最大消息大小。注意與broker的參數message.max.bytes不同。

JVM參數

heap size:6GB,這是目前業界比較公認的一個合理值。默認 1GB 有點小,畢竟 Kafka Broker 在與客戶端進行交互時會在 JVM 堆上創建大量的 ByteBuffer 實例。

使用 G1 收集器。在沒有任何調優的情況下,G1 表現得要比 CMS 出色,主要體現在更少的 Full GC,需要調整的參數更少。

$> export KAFKA_HEAP_OPTS=--Xms6g  --Xmx6g
$> export KAFKA_JVM_PERFORMANCE_OPTS= -server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent -Djava.awt.headless=true

操作系統參數

文件描述符限制:通常情況下將它設置成一個超大的值是合理的做法,比如ulimit -n 1000000,避免“Too many open files”的錯誤。

文件系統類型:XFS 的性能要強於 ext4。

Swappiness:網上很多文章都提到設置其爲 0,將 swap 完全禁掉以防止 Kafka 進程使用 swap 空間。我個人反倒覺得還是不要設置成 0 比較好,我們可以設置成一個較小的值。爲什麼呢?因爲一旦設置成 0,當物理內存耗盡時,操作系統會觸發 OOM killer 這個組件,它會隨機挑選一個進程然後 kill 掉,即根本不給用戶任何的預警。但如果設置成一個比較小的值,當開始使用 swap 空間時,你至少能夠觀測到 Broker 性能開始出現急劇下降,從而給你進一步調優和診斷問題的時間。基於這個考慮,我個人建議將 swappniess 配置成一個接近 0 但不爲 0 的值,比如 1。

提交時間:即 Flush 落盤時間。向 Kafka 發送數據並不是真要等數據被寫入磁盤纔會認爲成功,而是隻要數據被寫入到操作系統的頁緩存(Page Cache)上就可以了,隨後操作系統根據 LRU 算法會定期將頁緩存上的“髒”數據落盤到物理磁盤上。這個定期就是由提交時間來確定的,默認是 5 秒。一般情況下我們會認爲這個時間太頻繁了,可以適當地增加提交間隔來降低物理磁盤的寫操作。當然你可能會有這樣的疑問:如果在頁緩存中的數據在寫入到磁盤前機器宕機了,那豈不是數據就丟失了。的確,這種情況數據確實就丟失了,但鑑於 Kafka 在軟件層面已經提供了多副本的冗餘機制,因此這裏稍微拉大提交間隔去換取性能還是一個合理的做法。

生產端和消費端

壓縮

Producer 端壓縮、Broker 端保持、Consumer 端解壓縮。

在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;而在壓縮比方面,zstd > LZ4 > GZIP > Snappy。因此,推薦LZ4

攔截器

在應用程序不修改的情況下,動態的實現一組可插拔的事件處理邏輯鏈。它能在主業務操作的前後多個時間點上插入對應的攔截邏輯。可用於客戶端監控、添加消息頭、審計、端到端系統性能監測等。

Properties props = new Properties();
List<String> interceptors = new ArrayList<>();
interceptors.add("com.yourcompany.kafkaproject.interceptors.AddTimestampInterceptor"); // 攔截器1
interceptors.add("com.yourcompany.kafkaproject.interceptors.UpdateCounterInterceptor"); // 攔截器2
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);

生產者攔截器

實現 org.apache.kafka.clients.producer.ProducerInterceptor 接口:

  • onSend:該方法會在消息發送之前被調用
  • onAcknowledgement:該方法會在消息成功提交或發送失敗之後被調用,早於 callback 的調用

消費者攔截器

實現 org.apache.kafka.clients.consumer.ConsumerInterceptor 接口:

  • onConsume:該方法在消息返回給 Consumer 程序之前調用
  • onCommit:Consumer 在提交位移之後調用該方法

生產者如何管理TCP連接

13 | Java生產者是如何管理TCP連接的? (geekbang.org)

  1. KafkaProducer 實例創建時啓動 Sender 線程,從而創建與 bootstrap.servers 中所有 Broker 的 TCP 連接。
  2. KafkaProducer 實例首次更新元數據信息之後,還會再次創建與集羣中所有 Broker 的 TCP 連接。
  3. 如果 Producer 端發送消息到某臺 Broker 時發現沒有與該 Broker 的 TCP 連接,那麼也會立即創建連接。
  4. 如果設置 Producer 端 connections.max.idle.ms 參數(默認9分鐘)大於 0,則步驟 1 中創建的 TCP 連接會被自動關閉;如果設置該參數 =-1,那麼步驟 1 中創建的 TCP 連接將無法被關閉,從而成爲“殭屍”連接(TCP 連接是在 Broker 端被關閉的,但其實這個 TCP 連接的發起方是客戶端,因此在 TCP 看來,這屬於被動關閉的場景,即 passive close。被動關閉的後果就是會產生大量的 CLOSE_WAIT 連接,因此 Producer 端或 Client 端沒有機會顯式地觀測到此連接已被中斷)。

消費者組

重平衡

重平衡很慢,效率低,應儘量避免。因爲參數或邏輯不合理(poll之後處理時間太長)而導致的組成員意外離組或退出:

  • session.timeout.ms 默認10s
  • heartbeat.interval.ms 默認3s
  • max.poll.interval.ms 默認300s
  • GC 參數。程序頻發 Full GC 引發的非預期 Rebalance

成員被動退出後,再次去提交offset時,會報錯:

org.apapache.kafka.clients.consumer.internals.ConsumerCoordinator: [Consumer clientId=xx, groupId=xx] Offset commit failed on partition xx-1 at offset xx: The coordinator is not aware of this member.

多線程消費

Java Consumer 是雙線程的設計。一個線程是用戶主線程,負責獲取消息;另一個線程是心跳線程,負責向 Kafka 彙報消費者存活情況。將心跳單獨放入專屬的線程,能夠有效地規避因消息處理速度慢而被視爲下線的“假死”情況。

多線程方案:

  1. 消費者程序啓動多個線程,每個線程維護專屬的 KafkaConsumer 實例,負責完整的消息獲取、消息處理流程。【實例是各自獨立的】

  2. 消費者程序使用單或多線程獲取消息,同時創建多個消費線程執行消息處理邏輯。拉取消息的consumer線程與實際幹活的worker是1:n的關係。

    1. 優勢:伸縮性好。worker線程池和拉消息的線程獨立,可以分別擴展
    2. 劣勢:無法保證順序。eg:一個consumer實例拉取一批消息,交給10個worker線程處理,這批消息的處理順序就打亂了。

消費者是如何管理TCP連接

何時創建 TCP 連接

和生產者不同的是,構建 KafkaConsumer 實例時是不會創建任何 TCP 連接的,也就是說,當你執行完 new KafkaConsumer(properties) 語句後,你會發現,沒有 Socket 連接被創建出來。這一點和 Java 生產者是有區別的,主要原因就是生產者入口類 KafkaProducer 在構建實例的時候,會在後臺默默地啓動一個 Sender 線程,這個 Sender 線程負責 Socket 連接的創建。

消費者的TCP 連接是在調用 KafkaConsumer.poll 方法時被創建的。再細粒度地說,在 poll 方法內部有 3 個時機可以創建 TCP 連接:

  1. 發起 FindCoordinator 請求時,希望 Kafka 集羣告訴它哪個 Broker 是管理它的協調者。消費者應該向哪個 Broker 發送這類請求呢?理論上任何一個 Broker 都能回答這個問題,也就是說消費者可以發送 FindCoordinator 請求給集羣中的任意服務器。在這個問題上,社區做了一點點優化:消費者程序會向集羣中當前負載最小的那臺 Broker 發送請求。負載是如何評估的呢?其實很簡單,就是看消費者連接的所有 Broker 中,誰的待發送請求最少。
  2. 連接協調者時
  3. 消費數據時

何時關閉 TCP 連接

和生產者類似,消費者關閉 Socket 也分爲主動關閉和 Kafka 自動關閉。

  • 主動關閉:KafkaConsumer.close() 方法,或 Kill -2 、Kill -9
  • 自動關閉:由消費者端參數 connection.max.idle.ms 控制,默認 9 分鐘

消費者組重平衡

消費者端參數 heartbeat.interval.ms 的真實用途:從字面上看,它就是設置了心跳的間隔時間,但這個參數的真正作用是控制重平衡通知的頻率。如果你想要消費者實例更迅速地得到通知,那麼就可以給這個參數設置一個非常小的值,這樣消費者就能更快地感知到重平衡已經開啓了。

消費者組的狀態 含義
Empty 組內沒有任何成員,但消費者組可能存在已提交的位移數據,而且這些位移尚未過期。
Dead
同樣是組內沒有任何成員,但組的元數據信息已經在協調者端被移除。
協調者組件保存着當前向它註冊過的所有組信息,所謂的元數據信息就類似於這個註冊信息。
PreparingRebalance 消費者組準備開啓重平衡,此時所有成員都要重新請求加入消費者組。
CompleingRebalance 消費者組下所有成員已經加入,各個成員正在等待分配方案。
該狀態在老一點的版本中被稱爲AwaitingSync,它和CompletingRebalance是等價的。
Stable 消費者組的穩定狀態。該狀態表明重平衡已經完成,組內各成員能夠正常消費數據了。

image-20220407184925-y5yc3d1.jpg

一個消費者組最開始是 Empty 狀態,當重平衡過程開啓後,它會被置於 PreparingRebalance 狀態等待成員加入,之後變更到 CompletingRebalance 狀態等待分配方案,最後流轉到 Stable 狀態完成重平衡。
當有新成員加入或已有成員退出時,消費者組的狀態從 Stable 直接跳到 PreparingRebalance 狀態,此時,所有現存成員就必須重新申請加入組。當所有成員都退出組後,消費者組狀態變更爲 Empty。Kafka 定期自動刪除過期位移的條件就是,組要處於 Empty 狀態。因此,如果你的消費者組停掉了很長時間(超過 7 天),那麼 Kafka 很可能就把該組的位移數據刪除了。我相信,你在 Kafka 的日誌中一定經常看到下面這個輸出:
Removed xxx expired offsets in xxx milliseconds.
這就是 Kafka 在嘗試定期刪除過期位移。現在你知道了,只有 Empty 狀態下的組,纔會執行過期位移刪除的操作。

重平衡分爲兩個步驟:分別是加入組和等待領導者消費者(Leader Consumer)分配方案。這兩個步驟分別對應兩類特定的請求:JoinGroup 請求和 SyncGroup 請求。第一個發送 JoinGroup 請求的成員自動成爲領導者。領導者消費者的任務是收集所有成員的訂閱信息,然後根據這些信息,制定具體的分區消費分配方案。

Broker端

控制器

它的主要作用是在 ZooKeeper 的幫助下管理和協調整個 Kafka 集羣。集羣中任意一臺 Broker 都能充當控制器的角色,但是,在運行過程中,只能有一個 Broker 成爲控制器。第一個在 ZooKeeper 成功創建 /controller 節點的 Broker 會被指定爲控制器。

  • 控制器是做什麼的?

    1. 主題管理(創建、刪除、增加分區)
    2. 分區重分配
    3. Preferred 領導者選舉:爲了避免部分 Broker 負載過重而提供的一種換 Leader 的方案
    4. 集羣成員管理(新增 Broker、Broker 主動關閉、Broker 宕機),依賴 ZooKeeper Watch、臨時節點
    5. 數據服務。控制器上保存了最全的集羣元數據信息,其他所有 Broker 會定期接收控制器發來的元數據更新請求,從而更新其內存中的緩存數據。這些數據其實在 ZooKeeper 中也保存了一份。每當控制器初始化時,它都會從 ZooKeeper 上讀取對應的元數據並填充到自己的緩存中。

控制器內部設計原理

多線程訪問共享可變數據是維持線程安全最大的難題。爲了保護數據安全性,控制器不得不在代碼中大量使用 ReentrantLock 同步機制。社區於 0.11 版本重構了控制器的底層設計,最大的改進就是,把多線程的方案改成了單線程加事件隊列的方案

高水位和Leader Epoch

Kafka 的水位不是時間戳,更與時間無關。它是和位置信息綁定的,具體來說,它是用消息位移來表徵的。另外,Kafka 源碼使用的表述是高水位,因此,今天我也會統一使用“高水位”或它的縮寫 HW 來進行討論。值得注意的是,Kafka 中也有低水位(Low Watermark),它是與 Kafka 刪除消息相關聯的概念,與今天我們要討論的內容沒有太多聯繫。

高水位的作用

  • 定義消息可見性,即用來標識分區下的哪些消息是可以被消費者消費的。
  • 幫助 Kafka 完成副本同步。

image.png

在分區高水位以下的消息被認爲是已提交消息,反之就是未提交消息。消費者只能消費已提交消息,即圖中位移小於 8 的所有消息。注意,這裏我們不討論 Kafka 事務,因爲事務機制會影響消費者所能看到的消息的範圍,它不只是簡單依賴高水位來判斷。它依靠一個名爲 LSO(Log Stable Offset)的位移值來判斷事務型消費者的可見性。
另外,需要關注的是,位移值等於高水位的消息也屬於未提交消息。也就是說,高水位上的消息是不能被消費者消費的。
圖中還有一個日誌末端位移的概念,即 Log End Offset,簡寫是 LEO。它表示副本寫入下一條消息的位移值。注意,數字 15 所在的方框是虛線,這就說明,這個副本當前只有 15 條消息,位移值是從 0 到 14,下一條新消息的位移是 15。顯然,介於高水位和 LEO 之間的消息就屬於未提交消息。這也從側面告訴了我們一個重要的事實,那就是:同一個副本對象,其高水位值不會大於 LEO 值。
Kafka 使用 Leader 副本的高水位來定義所在分區的高水位。

Leader Epoch

高水位在界定 Kafka 消息對外可見性以及實現副本機制等方面起到了非常重要的作用,但其設計上的缺陷給 Kafka 留下了很多數據丟失或數據不一致的潛在風險。爲此,社區引入了 Leader Epoch 機制,嘗試規避掉這類風險。

Leader Epoch 大致可以認爲是 Leader 版本。它由兩部分數據組成:

  • Epoch。一個單調增加的版本號。每當副本領導權發生變更時,都會增加該版本號。小版本號的 Leader 被認爲是過期 Leader,不能再行使 Leader 權力。
  • 起始位移(Start Offset)。Leader 副本在該 Epoch 值上寫入的首條消息的位移。

副本機制

  • 提供數據冗餘
  • 提供高伸縮性。增加機器->提升讀性能->提高讀操作吞吐量
  • 改善數據局部性

Leader Replica 和 Follower Replica,Follower 不對外提供服務,原因:

  • 方便實現“Read-your-writes”:當你使用生產者 API 向 Kafka 成功寫入消息後,馬上使用消費者 API 去讀取剛纔生產的消息。發微博時,你發完一條微博,肯定是希望能立即看到的,這就是典型的 Read-your-writes 場景。
  • 方便實現單調讀(Monotonic Reads):對於一個消費者用戶而言,在多次消費消息時,它不會看到某條消息一會兒存在一會兒不存在。

In-sync Replicas(ISR)

ISR 不只是追隨者副本集合,它必然包括 Leader 副本。

Broker 端參數 replica.lag.time.max.ms:指定 Follower 副本能夠落後 Leader 副本的最長時間間隔,默認值是 10 秒。

日誌截斷

類似數據庫的回滾,刪除無效的消息。

請求是怎麼被處理的

所有的請求都是通過 TCP 網絡以 Socket 的方式進行通訊的。

處理請求的 2 種常見方案

(1)順序處理請求。吞吐量太差


while (true) {
    Request request = accept(connection);
    handle(request);
}

(2)每個請求使用單獨線程處理,異步。每個請求都創建線程,開銷大


while (true) {
    Request = request = accept(connection);
    Thread thread = new Thread(() -> { 
        handle(request);});
    thread.start();
}

Kafka 使用 Reactor 模式

Reactor 模式是事件驅動架構的一種實現方式,用於處理多個客戶端併發向服務器端發送請求的場景。Reactor 有個請求分發線程 Dispatcher,也就是圖中的 Acceptor。Kafka 提供了 Broker 端參數 num.network.threads,用於調整該網絡線程池的線程數。其默認值是 3,表示每臺 Broker 啓動時會創建 3 個網絡線程,專門處理客戶端發送的請求。

image-20220407174001-v1ni35v.jpg

客戶端發來的請求會被 Broker 端的 Acceptor 線程分發到任意一個網絡線程中,網絡線程拿到請求後,它不是自己處理,而是將請求放入到一個共享請求隊列中。Broker 端還有個 IO 線程池,負責從該隊列中取出請求,執行真正的處理。Broker 端參數 num.io.threads 控制了這個線程池中的線程數。目前該參數默認值是 8,表示每臺 Broker 啓動後自動創建 8 個 IO 線程處理請求。

圖中有一個叫 Purgatory 的組件,這是 Kafka 中著名的“煉獄”組件。它是用來緩存延時請求(Delayed Request)的。所謂延時請求,就是那些一時未滿足條件不能立刻處理的請求。比如設置了 acks=all 的 PRODUCE 請求,一旦設置了 acks=all,那麼該請求就必須等待 ISR 中所有副本都接收了消息後才能返回,此時處理該請求的 IO 線程就必須等待其他 Broker 的寫入結果。

image-20220407174228-4mcpfbo.jpg

日誌

Kafka 源碼使用ConcurrentSkipListMap類型來保存日誌段對象。好處有兩個:線程安全;支持 Key 的排序。

消息語義

  1. 至多一次
  2. 至少一次
  3. 有且僅有一次

Kafka默認保證至少一次。也可以保證最多一次,只需讓 Producer 禁止重試。

無消息丟失配置

一句話概括,Kafka 只對“已提交”的消息(committed message)做有限度的持久化保證。

一句話概括,Kafka 只對“已提交”的消息(committed message)做有限度的持久化保證。有限度:假如你的消息保存在 N 個 Kafka Broker 上,那麼這個前提條件就是這 N 個 Broker 中至少有 1 個存活。只要這個條件成立,Kafka 就能保證你的這條消息永遠不會丟失。

Producer端:

  • 使用 producer.send(msg, callback),而非producer.send(msg)
  • acks = all
  • retries > 0,網絡抖動時重試

Broker端:

  • unclean.leader.election.enable = false,不允許落後太多的follower成爲leader
  • replication.factor >= 3,多副本
  • min.insync.replicas > 1,消息至少要被寫入到多少個副本纔算是“已提交”。設置成大於 1 可以提升消息持久性。默認值 1。
  • 確保 replication.factor > min.insync.replicas,如果兩者相等,那麼只要有一個副本掛機,整個分區就無法正常工作了。

Consumer端:

  • enable.auto.commit = false,手動提交位移
冪等
事務

如何在業務中去重

  1. 設置特別的業務字段,用於標識消息的id,再次遇到相同id則直接確認消息;
  2. 將業務邏輯設計爲冪等,即使發生重複消費,也能保證一致性;
如何保證分區級消息有序

max.in.flight.requests.per.connection=1

Kafka 使用 Compact 策略來刪除位移主題(__consumer_offsets)中的過期消息,避免該主題無限期膨脹(使用自送提交時,在閒時狀態,消費者會提交很多相同offset的進度)。後臺線程 Log Cleaner 會定期地巡檢待 Compact 的主題,刪除過期消息。

運維監控

自帶腳本

這些命令行腳本很多都是通過連接 ZooKeeper 來提供服務的。kafka-topics 腳本連接 ZooKeeper 時,不會考慮 Kafka 設置的用戶認證機制。

測試生產者/消費者性能

kafka-xx-perf-test

查看主題消息總數

Kafka 自帶的命令沒有提供這樣的功能。可以使用 Kafka 提供的工具類 GetOffsetShell 來計算給定主題特定分區當前的最早位移和最新位移,將兩者的差值累加起來,就能得到該主題當前總的消息數。對於本例來說,test-topic 總的消息數爲 5500000 + 5500000,等於 1100 萬條:


$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-host:port --time -2 --topic test-topic

test-topic:0:0
test-topic:1:0

$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-host:port --time -1 --topic test-topic

test-topic:0:5500000
test-topic:1:5500000

查看消息文件數據

kafka-dump-log:


$ bin/kafka-dump-log.sh --files ../data_dir/kafka_1/test-topic-1/00000000000000000000.log --deep-iteration --print-data-log

AdminClient

服務器端也有一個 AdminClient,包路徑是 kafka.admin。這是之前的老運維工具類,提供的功能也比較有限,社區已經不再推薦使用它了。所以,我們最好統一使用客戶端的 AdminClient。

工作原理

AdminClient 是一個雙線程的設計:前端主線程和後端 I/O 線程。前端線程負責將用戶要執行的操作轉換成對應的請求,然後再將請求發送到後端 I/O 線程的隊列中;而後端 I/O 線程從隊列中讀取相應的請求,然後發送到對應的 Broker 節點上,之後把執行結果保存起來,以便等待前端線程的獲取。

認證授權

認證

截止到當前最新的 2.3 版本,Kafka 支持基於 SSL 和基於 SASL 的安全認證機制。基於 SSL 的認證主要是指 Broker 和客戶端的雙路認證(2-way authentication)。通常來說,SSL 加密(Encryption)已經啓用了單向認證,即客戶端認證 Broker 的證書(Certificate)。如果要做 SSL 認證,那麼我們要啓用雙路認證,也就是說 Broker 也要認證客戶端的證書。

Kafka 還支持通過 SASL 做客戶端認證。SASL 是提供認證和數據安全服務的框架。Kafka 支持的 SASL 機制有 5 種:

  1. GSSAPI:也就是 Kerberos 使用的安全接口,是在 0.9 版本中被引入的。適用於本身已經做了 Kerberos 認證的場景,這樣的話,SASL/GSSAPI 可以實現無縫集成
  2. PLAIN:是使用簡單的用戶名 / 密碼認證的機制,在 0.10 版本中被引入。配置和運維成本小。不能動態地增減認證用戶:所有認證用戶信息全部保存在靜態文件中,所以只能重啓 Broker,才能重新加載變更後的靜態文件
  3. SCRAM:主要用於解決 PLAIN 機制安全問題的新機制,是在 0.10.2 版本中被引入的。如果你打算使用 SASL/PLAIN,不妨改用 SASL/SCRAM
  4. OAUTHBEARER:是基於 OAuth 2 認證框架的新機制,在 2.0 版本中被引進。
  5. Delegation Token:補充現有 SASL 機制的輕量級認證機制,是在 1.1.0 版本被引入的。

授權

34 | 雲環境下的授權該怎麼做? (geekbang.org)

所謂授權,一般是指對與信息安全或計算機安全相關的資源授予訪問權限,特別是存取控制。具體到權限模型,常見的有四種。

  • ACL:Access-Control List,訪問控制列表。
  • RBAC:Role-Based Access Control,基於角色的權限控制。
  • ABAC:Attribute-Based Access Control,基於屬性的權限控制。
  • PBAC:Policy-Based Access Control,基於策略的權限控制。

在典型的互聯網場景中,前兩種模型應用得多,後面這兩種則比較少用。ACL 模型很簡單,它表徵的是用戶與權限的直接映射關係;而 RBAC 模型則加入了角色的概念,支持對用戶進行分組。

Kafka 用的是 ACL 模型。簡單來說,這種模型就是規定了什麼用戶對什麼資源有什麼樣的訪問權限。我們可以借用官網的一句話來統一表示這種模型:“Principal P is [Allowed/Denied] Operation O From Host H On Resource R.” 這句話中出現了很多個主體:

  • Principal:表示訪問 Kafka 集羣的用戶。
  • Operation:表示一個具體的訪問類型,如讀寫消息或創建主題等。
  • Host:表示連接 Kafka 集羣的客戶端應用程序 IP 地址。Host 支持星號佔位符,表示所有 IP 地址。
  • Resource:表示 Kafka 資源類型。如果以最新的 2.3 版本爲例,Resource 共有 5 種,分別是 TOPIC、CLUSTER、GROUP、TRANSACTIONALID 和 DELEGATION TOKEN。

當前,Kafka 提供了一個可插拔的授權實現機制。該機制會將你配置的所有 ACL 項保存在 ZooKeeper 下的 /kafka-acl 節點中。你可以通過 Kafka 自帶的 kafka-acls 腳本動態地對 ACL 項進行增刪改查,並讓它立即生效。

開啓 ACL 的方法特別簡單,只需要在 Broker 端的配置文件 server.properties 文件中配置下面這個參數值:

authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer

Kafka 的授權機制可以不配置認證機制單獨使用,但是隻能爲IP地址設置權限。

跨集羣備份MirrorMaker

MirrorMaker本質是從源集羣消費數據,然後用生產者發送到目標集羣。

它提供的功能很有限,運維成本高,比如主題的管理就非常不便捷,同時也很難將其管道化。基於這些原因,業界很多公司選擇自己開發跨集羣鏡像工具:

  • Uber 的 uReplicator
  • LinkedIn 開發的 Brooklin Mirror Maker
  • Confluent 公司研發的 Replicator。最強,收費

監控

  • 主機監控
  • JVM監控:監控 Broker GC 日誌,即以 kafkaServer-gc.log 開頭的文件。注意不要出現 Full GC 的字樣。一旦發現 Broker 進程頻繁 Full GC,可以開啓 G1 的 -XX:+PrintAdaptiveSizePolicy 開關,讓 JVM 告訴你到底是誰引發了 Full GC。
  • 集羣監控

監控框架/工具

  • JMXTool 工具:kafka-run-class.sh kafka.tools.JmxTool,自帶的一個工,能夠實時查看 Kafka JMX 指標
  • Kafka Manager:久不更新,活躍的代碼維護者只有三四個人,無法追上 Apache Kafka 版本的更迭速度
  • Burrow:LinkedIn 開源的一個專門監控消費者進度的框架,用 Go 寫的,沒有 UI 界面,只是開放了一些 HTTP Endpoint,久不更新
  • JMXTrans + InfluxDB + Grafana
  • Confluent Control Center:最強大的,企業級,收費
  • Kafka Eagle:國人維護,積極演進。除了常規的監控功能之外,還開放了告警功能(Alert),非常值得一試

調優

優化漏斗:層級越靠上,其調優的效果越明顯

image.png

動態參數配置

無需重啓Broker就能立即生效的參數。

  • 動態調整Broker端各種線程池大小,實時應對突發流量
  • 動態調整Broker端Compact操作性能
  • 動態調整Broker端連接信息或安全配置信息
  • 動態更新SSL Keystore有效期
  • 實時變更JMX指標收集器
  • log.retention.ms
  • num.io.threads和num.network.threads
  • num.replica.fetchers,不超過CPU核數

node id: -1是什麼意思

如下的日誌裏面:

[2019-05-27 10:00:54,142] DEBUG [Consumer clientId=consumer-1, groupId=test] Initiating connection to node localhost:9092 (id: -1 rack: null) using address localhost/127.0.0.1 (org.apache.kafka.clients.NetworkClient:944)
[2019-05-27 10:00:54,188] DEBUG [Consumer clientId=consumer-1, groupId=test] Sending metadata request MetadataRequestData(topics=[MetadataRequestTopic(name=‘t4’)], allowAutoTopicCreation=true, includeClusterAuthorizedOperations=false, includeTopicAuthorizedOperations=false) to node localhost:9092 (id: -1 rack: null) (org.apache.kafka.clients.NetworkClient:1097)
[2019-05-27 10:00:54,188] TRACE [Consumer clientId=consumer-1, groupId=test] Sending FIND_COORDINATOR {key=test,key_type=0} with correlation id 0 to node -1 (org.apache.kafka.clients.NetworkClient:496)
[2019-05-27 10:00:54,203] TRACE [Consumer clientId=consumer-1, groupId=test] Completed receive from node -1 for FIND_COORDINATOR with correlation id 0, received {throttle_time_ms=0,error_code=0,error_message=null,

日誌的第一行是消費者程序創建的第一個 TCP 連接,就像我們前面說的,這個 Socket 用於發送 FindCoordinator 請求。由於這是消費者程序創建的第一個連接,此時消費者對於要連接的 Kafka 集羣一無所知,因此它連接的 Broker 節點的 ID 是 -1,表示消費者根本不知道要連接的 Kafka Broker 的任何信息。

那麼 2147483645 這個 id 是怎麼來的呢?它是由 Integer.MAX_VALUE 減去協調者所在 Broker 的真實 ID 計算得來的。看第四行標爲橙色的內容,我們可以知道協調者 ID 是 2,因此這個 Socket 連接的節點 ID 就是 Integer.MAX_VALUE 減去 2,即 2147483647 減去 2,也就是 2147483645。這種節點 ID 的標記方式是 Kafka 社區特意爲之的結果,目的就是要讓組協調請求和真正的數據獲取請求使用不同的 Socket 連接。

CommitFailedException

這個異常是由於消費者在提交進度時,發現自己已經被開除了,它消費的分區分給別人了。the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms。解決方法:

  1. 減少每一批拉取的消息數(max.poll.records)
  2. 增加poll間隔時間上限(max.poll.interval.ms)
  3. 加快處理速度——多線程消費方案。poll的一批消息交給多個線程處理,使用併發提升吞吐。
    前提:無順序要求。
    難點:進度的提交。poll消息的主線程應該等待所有子線程處理完畢再提交進度。如果某個線程失敗了,應用重啓後,其他線程再次處理重複消息,需手動去重。

另一個可能的原因,如果standalone組與某個消費group有相同的groupId,也會造成該異常。

  • groupId和clientId都相同的兩個客戶端,會造成什麼後果?

Kafka 能手動刪除消息嗎

提供了留存策略,能夠自動刪除過期消息,不需用戶手動刪除消息。當然,它支持手動刪除消息:

  • 對於設置了 Key 且參數 cleanup.policy=compact 的主題而言,我們可以構造一條key=null的消息發送給 Broker,依靠 Log Cleaner 組件提供的功能刪除掉該 Key 的消息。
  • 對於普通主題而言,我們可以使用 kafka-delete-records 命令,或編寫程序調用 Admin.deleteRecords 方法來刪除消息。這兩種方法殊途同歸,底層都是調用 Admin 的 deleteRecords 方法,通過將分區 Log Start Offset 值擡高的方式間接刪除消息。

流處理

Kafka Connect + Kafka Core + Kafka Streams

其他

社區最近正在花大力氣去優化消費者組機制,力求改善因 Rebalance 導致的各種場景,但其實,其他框架開發者反而是不用 Group 機制的。他們寧願自己開發一套機制來維護分區分配的映射。這些都說明 Kafka 中的消費者組還是有很大的提升空間的。

確實,我們的框架也是自己維護分區映射。

Reference

  1. 極客時間 - Kafka 核心技術與實戰
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章