圖解 kafka 的高可用機制
對於一個複雜的分散式系統,如果沒有豐富的經驗和牛逼的架構能力,很難把系統做得簡單易維護,我們都知道,一個軟體的生命週期中,後期維護佔了70%,所以系統的可維護性是極其重要的, kafka 能成為大資料領域的事實標準,很大原因是因為運維起來很方便簡單,今天我們來看下 kafka 是怎麼來簡化運維操作的。
kafka 使用多副本來保證訊息不丟失,多副本就涉及到kafka的複製機制,在一個超大規模的叢集中,時不時地這個點磁碟壞了,那個點cpu負載高了,出現各種各樣的問題,多個副本之間的複製,如果想完全自動化容錯,就要做一些考量和取捨了。我們舉個例子說明下運維中面對的複雜性,我們都知道 kafka 有個 ISR集合,我先說明下這個概念:
kafka不是完全同步,也不是完全非同步,是一種ISR機制:
1. leader會維護一個與其基本保持同步的Replica列表,該列表稱為ISR(in-sync Replica),每個Partition都會有一個ISR,而且是由leader動態維護
2. 如果一個follower比一個leader落後太多,或者超過一定時間未發起資料複製請求,則leader將其重ISR中移除
3. 當ISR中所有Replica都向Leader傳送ACK時,leader才commit,這時候producer才能認為一個請求中的訊息都commit了。
在這種機制下, 如果一個 producer 一個請求傳送的訊息條數太多,導致flower瞬間落後leader太多怎麼辦?如果 follower不停的移入移出 ISR 會不會影響效能?如果對這種情況加了報警,就有可能造成告警轟炸,如果我們不加報警,如果是broker 掛掉或者 broker 因為IO效能或者GC問題夯住的情況導致落後leader太多,這種真正需要報警情況怎麼辦呢? 今天我們來看下 kafka 是怎麼在設計上讓我們完全避免這種運維中頭疼的問題的。
kafka的複製機制
kafka 每個分割槽都是由順序追加的不可變的訊息序列組成,每條訊息都一個唯一的offset 來標記位置。
kafka中的副本機制是以分割槽粒度進行復制的,我們在kafka中建立 topic的時候,都可以設定一個複製因子,這個複製因子決定著分割槽副本的個數,如果leader 掛掉了,kafka 會把分割槽主節點failover到其他副本節點,這樣就能保證這個分割槽的訊息是可用的。leader節點負責接收producer 打過來的訊息,其他副本節點(follower)從主節點上拷貝訊息。
kakfa 日誌複製演算法提供的保證是當一條訊息在 producer 端認為已經 committed的之後,如果leader 節點掛掉了,其他節點被選舉成為了 leader 節點後,這條訊息同樣是可以被消費到的。
這樣的話,leader 選舉的時候,只能從 ISR集合中選舉,集合中的每個點都必須是和leader訊息同步的,也就是沒有延遲,分割槽的leader 維護ISR 集合列表,如果某個點落後太多,就從 ISR集合中踢出去。 producer 傳送一條訊息到leader節點後, 只有當ISR中所有Replica都向Leader傳送ACK確認這條訊息時,leader才commit,這時候producer才能認為這條訊息commit了,正是因為如此,kafka客戶端的寫效能取決於ISR集合中的最慢的一個broker的接收訊息的效能,如果一個點效能太差,就必須儘快的識別出來,然後從ISR集合中踢出去,以免造成效能問題。kafka 複製機制詳情參考 https://kafka.apache.org/documentation.html#replication
一個副本怎麼才算是跟得上leader的副本
一個副本不能 “caught up” leader 節點,就有可能被從 ISR集合中踢出去,我們舉個例子來說明,什麼才是真正的 “caught up” —— 跟leader節點訊息同步。
kafka 中的一個單分割槽的 topic — foo,複製因子為 3 ,分割槽分佈和 leader 和 follower 如下圖,現在broker 2和3 是 follower 而且都在 ISR 集合中。我們設定 replica.lag.max.messages 為4,只要 follower 只要不落後leader 大於3條訊息,就然後是跟得上leader的節點,就不會被踢出去, 設定 replica.lag.time.max.ms 為 500ms, 意味著只要 follower 在每 500ms內傳送fetch請求,就不會被認為已經dead ,不會從ISR集合中踢出去。
現在 producer 傳送一條訊息,offset 為3, 這時候 broker 3 發生了 GC, 入下圖:
因為 broker 3 現在在 ISR 集合中, 所以要麼 broker 3 拉取同步上這條 offset 為3 的訊息,要麼 3 被從 ISR集合中踢出去,不然這條訊息就不會 committed, 因為 replica.lag.max.messages=4 為4, broker 3 只落後一條訊息,不會從ISR集合中踢出去, broker 3 如果這時候 GC 100ms, GC 結束,然後拉取到 offset 為3的訊息,就再次跟 leader 保持完全同步,整個過程一直在 ISR集合中,如下圖:
什麼時候一個副本才會從ISR集合中踢出去
一個副本被踢出 ISR集合的幾種原因:
-
一個副本在一段時間內都沒有跟得上 leader 節點,也就是跟leader節點的差距大於 replica.lag.max.messages , 通常情況是 IO效能跟不上,或者CPU 負載太高,導致 broker 在磁碟上追加訊息的速度低於接收leader 訊息的速度。
-
一個 broker 在很長時間內(大於 replica.lag.time.max.ms )都沒有向 leader 傳送fetch 請求, 可能是因為 broker 發生了 full GC, 或者因為別的原因掛掉了。
-
一個新 的 broker 節點,比如同一個 broker id, 磁碟壞掉,新換了一臺機器,或者一個分割槽 reassign 到一個新的broker 節點上,都會從分割槽leader 上現存的最老的訊息開始同步。
所以說 kafka 0.8 版本後設置了兩個引數 , replica.lag.max.messages 用來識別效能一直很慢的節點, replica.lag.time.max.ms 用來識別卡住的節點。
一個節點在什麼情況下真正處於落後狀態
從上面的情況來看,兩個引數看似已經足夠了,如果一個副本超過 replica.lag.time.max.ms 還沒有傳送fetch同步請求, 可以認為這個副本節點卡住了,然後踢出去,但是還有一種比較特殊的情況沒有考慮到,我們上文中設定 replica.lag.max.messages 為4,之所以設定為 4, 是我們已經知道 producer 每次請求打過來的訊息數都在 4 以下,如果我們的引數是作用於多個 topic 的情況,那麼這個 producer 最大打過來的訊息數目就不好估計了,或者說在經常出現流量抖動的情況下,就會出現一個什麼情況呢,我們還是使用例子說明:
如果我們的 topic — foo 的 producer 因為流量抖動打過來一個 包含 4條訊息的請求,我們設定的 replica.lag.max.messages 還是為4, 這個時候,所有的 follower 都會因為超出落後條數被踢出 ISR集合:
然後,因為 follower 是正常的,所以下一次 fetch 請求就會又追上 leader, 這時候就會再次加入 ISR 集合,如果經常性的抖動,就會不斷的移入移出ISR集合,會造成令人頭疼的 告警轟炸。
這裡的核心問題是,在海量的 topic 情況下,或者經常性的流量抖動情況下,我們不能對 topic 的producer 每次打過來的訊息數目做任何假設,所以就不太好定出來一個 合適的 eplica.lag.max.messages
值
一個配置全部搞定
其實只有兩種情況是異常的,一種就是卡住,另外一種是follower 效能慢,如果我們只根據 follower 落後 leader 多少來判斷是否應該把 follower 提出ISR集合,就必須要對流量進行預測估計,怎麼才能避免這種不靠譜的估計呢,kafka 給出 的方案是這樣的,對 replica.lag.time.max.ms 這個配置的含義做了增強,和之前一樣,如果 follower 卡住超過這個時間不傳送fetch請求, 會被踢出ISR集合,新的增強邏輯是,在 follower 落後 leader 超過 eplica.lag.max.messages 條訊息的時候,不會立馬踢出ISR 集合,而是持續落後超過 replica.lag.time.max.ms 時間,才會被踢出 ,這樣就能避免流量抖動造成的運維問題,因為follower 在下一次fetch的時候就會跟上leader, 這樣就也不用對 topic 的寫入速度做任何的估計嘍。
大家都在看
▼
關注 【spark技術分享】
一起擼spark原始碼,一起玩spark最佳實踐