1.動機
設計 kafka 初衷,作為統一平臺處理大公司的實時資料。所以 必須具有如下特性:
- 支援海量資料
- 高吞吐量
- 低延遲(實時性)
- 支援分割槽,分散式
- 容錯
2.持久化
kafka 高度依賴 檔案系統 儲存和快取訊息。通過對磁碟的順序讀寫,並藉助 OS 層面的 頁快取(page cache),保證優於快取在記憶體中或其他結構中。
為何使用磁碟效率仍然很高:
利用磁碟的順序讀寫,操作一個檔案,將資料追加到檔案的末尾。相比於隨機讀寫,效率很高。 利用 OS 層面的頁快取(page cache),順序讀檔案可以預讀資料到 page cache。通過自動訪問所有可用記憶體 以及 儲存緊湊型位元組結構而非單個物件提高記憶體使用率。OS快取相對於程式內的快取,重啟後仍然可用,不需要重建。 所有的操作時間複雜度都是 常量時間O(1),與資料大小無關,讀 和 寫 不會互相阻塞。
3.效率
使用磁碟效率低下主要有兩個原因:
過多的小 I/O 操作:發生在客戶端和服務端之間,以及 服務端自己的持久化操作中 過多的位元組複製 針對 小 I/O 操作,kafka 根據 "message set" 抽象構建了一個協議,該 抽象 自然地將訊息分組在一起。該協議允許網路請求將訊息分組在一起,並分攤網路往返的開銷,而不是一次傳送一條訊息。伺服器依次將訊息塊一次附加到其日誌中,而消費者一次獲取大型線性塊。
針對過多的位元組複製,使用了由生產者、代理 和 消費者共享的標準化二進位制訊息格式(這樣,資料塊就可以在它們之間不進行修改的情況下進行傳輸)。伺服器所持有的訊息日誌 本身是一個檔案目錄,每個檔案都由一系列 "message set" 填充。這些訊息集以生產者和消費者使用的相同格式寫入磁碟。維護這種通用格式可以優化 持久化日誌塊的 網路傳輸。
儲存在檔案中的資訊通過網路傳送給客戶,經歷的幾個路徑:
- 作業系統在核心空間將資料從磁碟讀取到 page cache 中。
- 應用程式從核心空間讀取到 使用者空間緩衝區。
- 應用程式將資料寫回到核心空間的套接字緩衝區。
- 作業系統將資料從套接字緩衝區複製到 NIC 緩衝區(NIC:網路介面控制器)。
- 以上產生了四個副本拷貝,2個系統呼叫開銷,效率低下。
大致流程
上下文切換開銷基於 零拷貝技術:訊息資料直接從 page cache 傳送到網路。linux 中使用 sendfile 完成零拷貝技術。java 中 java.nio.channels.FileChannel 的 transferTo() 方法也使用了零拷貝技術。
減少開銷
kafka 通過 page cache 和 sendfile 的組合,將看不到磁碟上的任何讀取活動,因為它們將完全從快取中提供資料。
端到端的批量壓縮 Kafka通過遞迴訊息集來支援同時壓縮多個訊息而減少相同訊息的冗餘。 一批訊息可以一起壓縮並以此形式傳送到伺服器。 這批訊息將以壓縮形式寫入,並將在日誌中保持壓縮,並且只能由消費者解壓縮。Kafka支援GZIP和Snappy壓縮協議。
4.生產者
4.1負載均衡
生產者將資料直接傳送給分割槽對應的 leader。為了實現這一點,所有的 kafka 節點要能夠在 任何時候應答 哪個伺服器還活著以及 topic分割槽的leader在哪裡的 後設資料請求。
客戶端自己控制 訊息傳送到哪個分割槽,這可以隨機完成,實現一種隨機的負載平衡,也可以通過一些語義分割槽函式完成。
4.2非同步傳送
啟用 kafka 生產者 的批處理,kafka 將在記憶體中累積資料然後一次性批量傳送。可以配置 累計不超過固定數量的訊息(bach.size),等待不超過固定延遲時間(linger.ms)。
5.消費者
5.1拉 VS 推送
消費者主動拉取訊息缺點:如果 broker 沒有資料,消費者會輪詢,忙等待直到資料到達。kafka 可以在拉請求中設定一些引數,允許使用者請求在“長輪詢”中阻塞,等待資料到達(也可以選擇等待,直到給定的位元組數可用,以確保傳輸大小很大)
消費者被動推送訊息缺點:很難適應消費速率不同的消費者,訊息傳送速率是由 broker 決定的,broker 是儘可能快的將訊息傳送出去,這樣會造成消費者來不及處理訊息,典型的表現就是 網路阻塞 和 拒絕服務。
5.2消費者的定位
topic 被分為一組有序的分割槽,每個分割槽在任何給定的時間都由每個訂閱消費者組中的一個消費者消費。這意味著消費者在每個分割槽中的位置只是一個整數,這個整數代表了即將要消費的訊息的偏移量。這樣做的好處是可以返回到舊的偏移量進行消費。
5.3離線資料載入
可伸縮永續性允許消費者只定期使用,例如批量資料載入,定期將資料批量載入到離線系統(如Hadoop或關係資料倉儲)中。
6.訊息傳遞語義
很明顯,訊息傳遞保證能夠提供多種可能:
- 最多一次:訊息可能丟失,但是絕不會重發
- 至少一次:訊息絕不會丟失,但是可能會重發
- 正好一次:每條訊息被傳遞一次 kafka 的訊息傳遞語義:
一旦釋出的訊息已提交到日誌,只要副本分割槽寫入了此訊息的一個broker仍然"活著”,它就不會丟失。
0.11.0.0 版本之前,如果一個生產者沒有收到訊息提交的響應,那麼生產者只能重新傳送該訊息。這就保證了至少一次的傳遞語義。如果上一次的請求實際上是成功的,那麼訊息就會再次寫到日誌中,造成重複消費。
0.11.0.0 版本之後,kafka 生產者支援冪等傳遞,保證重新傳送不會導致日誌中有重複記錄。為了實現這一點,broker 為 每一個生產者 分配一個 ID,使用生產者隨每條訊息一起傳送的序列號來消除重複的訊息。
同時也是從 0.11.0.0 版本之後,生產者支援使用事務類語義將訊息傳送到多個 topic 分割槽的能力:即,要麼所有訊息都已成功寫入,要麼都未成功寫入。這方面的主要用例是在Kafka topic 之間進行一次處理。
當然,不是所有的使用場景都需要如此嚴謹的保障,對於延遲敏感的,我們允許生產者指定它想要的耐用性水平。如生產者可以指定它獲取需等待10毫秒量級上的響應。生產者也可以指定非同步傳送,或只等待leader(不需要副本的響應)有響應。
從消費者的角度描述語義:
- 讀取到訊息,在日誌中儲存位置,最後處理訊息。這種順序 如果消費者在儲存位置之後,處理訊息之前崩潰,資料會丟失,屬於 最多一次的語義。
- 讀取訊息,處理訊息,在日誌中儲存位置。這種順序,如果消費者在處理訊息之後,日誌中儲存位置之前崩潰,資料會被多次處理,屬於至少一次的語義。在多數情況下,訊息都有一個主鍵,所以更新是冪等的(一次執行和多次執行的影響相同)。 kafka 預設是保證“至少一次”傳遞,並允許使用者通過禁止生產者重試和處理一批訊息前提交它的偏移量來實現 “最多一次”傳遞。而“正好一次”傳遞需要與目標儲存系統合作,但kafka提供了偏移量,所以實現這個很簡單。
7.副本
kafka 在各個伺服器上備份 每個 topic 的 partition (通過 replication factor 設定副本數)。當叢集中的某個伺服器發生故障時,自動轉移到這些副本,以便在故障時,訊息仍然可用。
kafka 的預設 副本因子為 1,即不建立副本。副本因子是指副本的總數,包括 leader 。
副本以 topic 的 partition 為單位。在非故障的情況下,kafka 中的每個 partition 都有一個 leader,0 個或者多個 follower。所有的讀 和寫都指向 分割槽的 leader。通常,分割槽數 多於 broker 的數量,leader 均勻的分佈在 broker 上。follower 的日誌與 leader 的日誌相同,即相同的 偏移量 offset 和 訊息順序 。(當然,有可能在某個時間點,leader 上比 follower 多幾條還未同步的訊息)。
kafka 節點存活的2個條件:
- 一個節點必須能維持與 zookeeper 的會話(通過 zookeeper 的心跳機制)。
- 如果該節點是 slave,它必須複製 leader 的寫資料,並且不能落後太多。 如果節點 死掉,卡主,或者落後太多,leader 將 從 同步副本 ISR (In Sync Replicas)中移除該節點。落後多少是由 replica.lag.max.messages 控制,卡主多久算卡主是由 replica.lag.time.max.ms 控制。
kafka 動態維護一組同步 leader 資料的副本(ISR),只有這個組中的成員才有資格當選 leader。在所有同步副本都收到寫操作之前,不會認為已提交對Kafka分割槽的寫操作。這組 ISR 儲存在 zookeeper 中,正因為如此,在ISR中的任何副本都有資格當選leader。對於 f+1 個 副本的 kafka, topic 可以容忍f失敗而不會丟失已提交的訊息。
如果所有的節點都死掉,有兩種可以實現的方式:
- 等待 ISR 列表中的節點活過來,並且選擇該節點作為 leader.
- 選擇第一個活過來的節點(不管它在 ISR 列表中)作為 leader. 從 0.11.0.0 開始 kafka 預設選擇第一種策略,等待一致性的副本;可以通過配置 unclean.leader.election.enable 為 true 來選用第二種策略。這兩種策略是 可用性 和一致性的權衡,需要根據實際業務來決定。
可用性 和 耐久性保證
當寫訊息到 kafka 時,生產者可以 配置 需要 leader 收到的確認數 來確定是否完成請求,通過 配置 acks 滿足多種情況:
- acks = 0 :生產者不會等待伺服器的任何確認,訊息記錄將被立刻新增到 socket 緩衝區並視為已傳送。這種情況無法確保伺服器已經接收到訊息記錄,重試的配置也不會生效。每個記錄返回的偏移量始終被設定為 1.
- acks = 1 :伺服器端的 leader 寫入訊息到本地日誌就立即響應生產者,而不等待 follower 應答。這種情況,如果在伺服器響應生產者之後,複製到 follower 之前掛掉 就會丟失資料。
- acks = all(-1):伺服器端的 leader 會等待 ISR 中所有副本同步響應來確認訊息記錄。這保證了只要 ISR 中還有一個副本存活就不會丟失記錄,也可以設定為 -1; 提供兩種 topic 級別的配置 來確保 永續性 而非 可用性。
unclean.leader.election.enable 設為 false,(預設即為 false)即 所有的副本都不可用時,分割槽才不可用。只有當 ISR 中的節點 活過來 分割槽才能可用。 指定 一個最小的 ISR 數量值,通過 min.insync.replicas 來配置,只有當 ISR 中的數量 超過最小值,分割槽才會接受寫入操作,以此來防止僅寫入單個副本而後副本不可用而導致的訊息的丟失。該設定僅在 acks = all 並保證至少有這麼多同步副本確認訊息時生效。 副本管理
上面關於複製日誌的討論實際上只涉及了一個日誌,例如 一個 topic 的partition,然而,kafka 叢集管理著成百上千個這樣的分割槽。通過 round-robin 的方式平衡 叢集中的分割槽,避免 大部分的分割槽分佈在少量的及誒單上,同樣,平衡 leader,使在分割槽份額上的每個節點都是 leader。
kafka 選擇 其中一個 broker 作為 controller(到 zookeeper 上註冊,先到先得)。該 controller 檢測 broker 級別的故障,並負責更改 故障 broker 上受影響的 分割槽的 leader。這樣就可以批量處理 leader 的變更。如果 controller 故障,其他存活的 broker 將會成為新的 controller(同樣需要到 zookeeper 上註冊)。
歡迎關注 程式設計那點事兒,隨時隨地想學就學~