Kafka基礎-可靠性數據傳輸

可靠的數據傳輸是系統的一個必要屬性,就像性能一樣,必須從一開始就設計到系統中。Apache Kafka在可靠的數據傳輸方面非常靈活,支持非常多的配置參數。

1. 可靠性保證

當我們討論可靠性時,通常會提到保證這個術語。最著名的可靠性保證ACID,它是關係型數據庫普遍支持的標準可靠性保證。理解Kafka提供的保證對於創建可靠的應用是至關重要的,Kafka能夠保證:

  • 同一個分區消息的順序保證。在同一分區裏使用相同的生產者,如果消息B是在消息A之後寫入的,那麼Kafka保證消息B的offset會大於消息A,並且消費者會先讀取消息A。
  • 只要至少有一個副本保持alive狀態,已經提交的消息就不會丟失。
  • 消費者只能讀取已經提交的消息。

2. Broker配置

Broker有三個可以改變Kafka關於可靠消息存儲行爲的配置參數:

2.1 複製因子

default.replication.factor:自動創建topic時默認的複製因子,默認爲1
replication.factor:在Kafka Streams client library配置,默認爲1

複製因子N允許在N-1個brokers故障時仍能夠可靠地讀取和寫入數據。因此,設置更大的複製因子可以提高可用性和可靠性。在另一方面,對於複製因子N需要至少N個brokers,存儲N個數據副本,意味着需要N倍的磁盤空間,基本上就是使用更多的硬件換取更高的可用性。

那麼如何決定一個topic的複製因子?答案取決於topic的重要程度和預算。如果對於重啓一個broker(集羣的正常操作之一)導致特定的topic不可用時覺得OK,那麼複製因子1可能就足夠了。複製因子2表示可以在一個broker故障時仍然可用,但要注意的是,在一個broker故障的情況下也有可能導致集羣不穩定(大多數發生在舊版本)。因此,我們建議設置複製因子至少爲3。

副本的放置位置也是非常重要的,默認地,Kafka將確保分區的每個副本都放到不同的broker。然而,在某些情況下,這還不夠安全。如果分區所有副本的brokers都放在同一機架上,而且機架頂部的交換機出現問題,那麼無論複製因子是多少,分區都不可用。爲了防止機架的故障,建議把brokers放到多個機架上,並使用broker.rack配置參數爲每個broker配置機架名字。如果配置了機架名字,Kafka將確保分區的副本分佈在多個機架上,以確保更高的可用性。

2.2 Unclean Leader選舉

如之前所述,當分區的leader不再可用時,其中一個同步副本會被選舉爲新的leader。這種leader選舉是“clean”的,因爲它保證已經提交的數據不會丟失。但如果除了剛剛變爲不可用的leader之外沒有in-sync副本時,Kafka會怎樣處理?這種情況可能發生在以下兩種情況之一:

  • 分區有3個副本,其中2個followers因故障變爲不可用。在這種情況下,當生產者繼續寫入數據到leader時,所有消息都會被認爲已經提交(因爲這個時候leader是唯一的同步副本)。如果leader也出現故障,而其中一個非同步副本重新啓動,那麼該非同步副本將變爲分區唯一可用的副本。
  • 分區有3個副本,但由於網絡問題,其中2個followers在默認10秒內沒有同步到最新的消息從而變爲非同步副本。這個時候leader是唯一的同步副本,如果leader出現故障,2個followers不會再變爲同步副本。

在這兩種情況下,我們要決定:

  • 如果不允許非同步副本變成新的leader,相應的分區將一直保持不可用直到原來的leader重新恢復可用。
  • 如果確實要允許非同步副本變成新的leader,這樣會丟失已經寫入到原來的leader但沒有同步到副本的數據,並且還會導致消費者的某些不一致。

總之,如果允許非同步副本變成新的leader就會面臨數據丟失和不一致的風險。如果不允許,將面臨較低的可用性。unclean.leader.election.enable設置默認爲false。

2.3 最少同步副本數

在topic和broker的配置都是min.insync.replicas,默認是1。

正如上述提到的,有些情況下即使配置了三個副本,也可能會留下只有一個同步副本。如果此副本變得不可用,我們不得不在可用性和一致性之間進行取捨,這通常不是一個容易的選擇。如果希望確保已提交的數據寫入多個副本,需要增大最少同步副本數。例如一個topic有3個副本,把min.insync.replicas設置爲2,那麼只有至少有2個副本完成同步數據時纔算真正寫入到分區。當所有3個副本都同步時,一切都正常工作。如果其中1個副本變得不可用,也可以正常工作。但是,如果有2個副本不可用,brokers將不再接受寫入消息的請求。生產者會收到NotEnoughReplicasException的異常,消費者可以繼續讀取存在的消息,換句話說僅存的單個同步副本會變爲只讀。爲了從這種只讀的情況中恢復正常,必須使2個不可用的分區中的一個恢復可用(例如重啓broker)並等待它完成數據同步。

3. 相應的生產者配置

即使盡可能地以最可靠的方式配置brokers,但如果不把生產者也按相應可靠性配置,整個系統仍可能會意外丟失數據。以下是兩個示例場景:

  • brokers配置3個副本,並禁用unclean leader選舉,生產者的acks設置爲1。當發送並已寫入消息到leader但還沒有同步到副本時,該leader向生產者返回一條“Message was written successfully”的信息,並在消息同步到副本前發生故障。其它的副本仍被認爲是同步的(在replica.lag.time.max.ms時間範圍內),其中一個副本被選舉爲新的leader。由於之前提到的消息沒有同步到副本,因此會丟失,但生產者的應用會認爲已經寫入成功。整個系統還是處於一致的,因爲沒有消費者能讀取那些消息,但從生產者的角度來看,消息是丟失的。
  • brokers配置3個副本,並禁用unclean leader選舉,生產者的acks設置爲all。當嘗試向leader發送一條消息,但該leader發生故障,新的leader正在選舉中,Kafka向生產者返回”Leader not Available“的信息。此時,如果生產者沒有異常處理和重試機制,消息會丟失。這個不是broker的可靠性問題,因爲broker從未收到那些消息;也不是一致性的問題,因爲消費者也從未讀取到那些消息。但是如果生產者沒有正確處理異常,它們可能會導致消息丟失。

那麼我們如何避免丟失消息呢?如示例所示,必須注意以下兩個重要事項:

  • 根據可靠性需求使用正確的acks配置
  • 代碼端要正確處理異常

前文已經詳細介紹過生產者相關的配置,但下面讓我們再次複習一下重點:

3.1 確認配置-acks

  • acks=0,消息一旦通過網絡發送就會被認爲已經發送成功。如果發送的消息不能被序列化或者網絡出現故障,生產者仍然會收到錯誤信息,但如果分區處於下線狀態或整個集羣離線,生產者是不會收到任何錯誤信息。這意味着即使出現預期的clean leader選舉,生產者也會丟失消息,因爲當正在選舉新的leader時,生產者是不會知道原來的leader不可用。使用配置acks=0,運行的性能是非常快的(這就是爲什麼許多benchmarks測試使用該配置的原因)。你可以獲得驚人的吞吐量和使用大部分的帶寬,但肯定會丟失一些消息。
  • acks=1,leader在收到消息並寫入分區數據文件(但不一定同步到磁盤)會返回確認給生產者。如果在寫入消息時發生故障,也會返回錯誤信息。這意味着在正常的leader選舉情況下,當正在選舉leader時,生產者會收到LeaderNotAvailableException,如果生產者正確處理該異常,重新發送消息,那麼消息是不會丟失的。如果leader宕機並且消息沒有被同步到副本,該消息仍然會丟失。
  • acks=all,在返回確認或異常信息之前,leader將一直等待直到消息同步到所有副本。與broker的min.insync.replicas配置結合使用,可以控制同步到消息的副本數量。這是最安全的配置-在消息完全提交之前,生產者會一直嘗試發送消息。但這也是最慢的配置-生產者會等待所有副本完成消息同步才能繼續發送下一批消息。通過使用異步模式和發送更大批次的消息可以減輕這些影響,但此配置始終會降低吞吐量。

3.2 重試配置-retries

生產者處理異常有2種方式:生產者自動處理和客戶代碼處理。

生產者可以自動處理broker返回的可重試的retriable異常。當生產者向broker發送消息時,broker返回成功或異常信息。異常信息有2類-重試後可以解決的和無法解決的異常。例如,如果broker返回異常信息LEADER_NOT_AVAILABLE,生產者可以嘗試重新發送消息,有可能新的broker已經選舉出來,重新發送的消息將會寫入成功。這意味着LEADER_NOT_AVAILABLE是一個可重試的retriable異常。另一方面,如果broker返回異常信息INVALID_CONFIG,重試發送信息是不能更改配置的,這就是不可重試的nonretriable異常。

一般來說,如果你的目標是永遠不會丟失消息,那麼最好的方法是配置生產者爲在遇到可重試異常時重試發送消息。因爲像leader不可用或網絡連接問題通常需要幾秒鐘才能恢復,如果配置生產者重試發送消息直到成功,你就不需要自己處理這些異常。那麼應該配置重試多少次呢?如果當捕捉到異常後你想重試很多次,那麼肯定需要配置更多的重試次數。如果可以丟棄消息,或者把它寫到其它地方稍後處理,那麼可以配置少的重試次數或不需要重試。例如,Kafka的跨數據中心複製工具(MirrorMaker)默認配置爲無休止地重試,retries = MAX_INT,因爲作爲一個高可靠性的複製工具,它永遠不應該丟棄消息。

注意,重試發送失敗的消息有小的機率會導致寫入相同的消息。例如,如果網絡問題導致broker無法向生產者發送確認信息,但消息實際已經寫入成功並複製到副本,這種情況下生產者因爲沒有收到確認信息將會重發消息,broker就會收到相同的消息。重試和小心的異常處理可以保證每條消息至少保存一次,但在版本0.10.0不能保證僅保存一次。有些應用程序會爲每條消息添加一個唯一標識,以便在讀取消息時可以檢測重複性並去重。有些會把消息設計爲冪等性-即使相同的消息被髮送2次也不會對正確性產生負面影響。例如,消息“賬戶餘額爲110元”,因爲多次發送它是不會改變結果。但消息“向賬戶添加10元”不是冪等的,因爲每次發送它都會改變結果。

3.3 異常處理

使用生產者內置的重試機制是一種在不丟失消息的情況下能夠正確處理大部分類型的異常的簡單方法,但作爲開發人員,仍然必須處理其它類型的異常,包括:

  • 不可重試的異常,例如關於消息大小、授權等等的異常
  • 消息在被髮送給broker之前的異常,例如序列化異常
  • 生產者重試次數到達上限的異常或者生產者的可用內存由於在重試時全部用來保存消息導致不足而發生的異常

前文已經介紹過如何在同步和異步發送消息方法裏編寫異常處理代碼,這裏不重複介紹。異常處理可以是記錄錯誤日誌,把消息保存在文件系統以便後續處理,觸發一個callback函數調用另一個應用程序等等。需要注意的是,如果異常處理僅僅是重試發送消息,那麼最好使用生產者的重試功能。

4. 相應的消費者配置

至此我們已經介紹瞭如何在保證可靠性的前提下發送消息,下面會介紹如何讀取消息。

正如之前所述,消息只有在提交後才能被消費者讀取,也就是說消息被寫入到所有的同步副本,這意味着消費者可以讀取到一致的數據。消費者唯一要做的就是確保記錄哪些消息是已經讀取,哪些沒有,這是不丟失消息的關鍵。當從一個分區讀取消息時,消費者是批量讀取的,然後記錄當前批次的最後一個offset,下次從記錄的offset開始讀取下一批消息。這樣可以保證消費者以正確的順序讀取新消息而不會遺漏任何消息。

當一個消費者停止時,另一個消費者需要知道從哪裏接手工作-也就是上一個消費者在停止前處理的最後一個消息的offset,這個消費者甚至可以是重啓後的原來那個消費者。這就是消費者需要提交它們的offsets的原因。消費者可能丟失消息的主要原因是當提交offsets時,對應的消息實際上是沒有處理完成的。這樣,當另一個消費者接手工作時,它將跳過那些消息導致它們永遠不會被處理。這就是爲什麼要特別注意何時以及怎樣提交offset的重要原因。

4.1 消費者的重要配置

爲了使消費者有可靠性的行爲,有4個重要的配置需要了解:

第一個是group.id,如果2個消費者有相同的group id並且訂閱相同的topic,那麼每個消費者將分配分區中的一個子集,因此只會讀取相應子集的消息,但所有消息會被整個組讀取。如果需要一個消費者讀取topic的每一條消息,那麼需要爲這個消費者設置唯一的group.id(該組只有唯一一個消費者)。

第二個相關配置是auto.offset.reset,此參數控制消費者在開始讀取一個沒有提交offset或該offset爲非法的分區時如何重置offset。根據前文所述,可用的配置有earliest、latest(默認)、none。如果選擇earliest,消費者將從最開始讀取分區的所有消息,這可能導致消費者重複處理大量的消息,但可以保證丟失很少數據。如果選擇latest,消費者將會從最新的消息開始讀取,這減少了消息的重複處理,但幾乎肯定會導致數據丟失。

第三個相關配置是enable.auto.commit,是否自動週期性提交offset(默認爲5秒)。如果消費者在poll循環中對消息進行所有處理,那麼自動提交offset可以保證永遠不會提交未處理消息的offset。自動提交offset的主要缺點是你無法控制可能需要處理的重複消息數量(例如消費者在處理某些消息後因某些原因停止了,但還沒有自動提交offset)。如果消費者把消息發給另一個background線程來處理,可能會提交已經讀取但還沒有被處理消息的offsets。

第四個相關配置與第三個相關,它是auto.commit.interval.ms。如果選擇自動提交offset,該配置允許你設置自動提交offset的間隔,默認是5秒。一般來說,更頻繁地提交會增加資源的使用,但會減少由於消費者停止可能導致的消息重複數量。

4.2 消費者自己提交offsets

4.2.1 每次處理完消息後都提交

如果在poll循環中對消息進行所有處理,可以在每次poll循環結束時使用自動提交或自己提交offsets。

4.2.2 提交的頻率是決定性能和發生故障時有多少重複消息的因數

即使在最簡單的情況下,在poll循環中對消息進行所有處理,也可以選擇在每次循環多次提交或者每幾次循環才提交一次。提交offsets會使用一定的資源(類似於acks=all),所以提交頻率的選擇一切取決於你的需求。

4.2.3 確保知道提交的offset是哪個

在poll循環中提交offsets的常見陷阱是意外地提交最後讀取消息的offset而不是最後處理消息的offset。始終在處理消息後提交offset是至關重要的,提交讀取消息的offset會導致丟失消息。

4.2.4 負載再均衡

在設計應用時,請記住消費者再均衡是會發生的,重要的是在分區被移除之前提交offsets並在分配新分區時清理任何維護的狀態。

4.2.5 消費者重試

在某些情況下,在調用poll循環處理消息後,某些消息沒有處理完畢需要稍後處理。例如,你可能嘗試把消息寫入數據庫,但數據庫剛好不可用需要稍後重試。注意,與傳統的發佈/訂閱消息系統不同的是,你提交的是offsets而不是消息。這意味着如果你處理消息#30失敗,但處理消息#31成功,則不應該提交offset #31,否則會導致提交所有offset小於#31的消息,也就是包括#30。以下是2種正確處理的選項:

選項1,當遇到可重試異常時,提交成功處理的最後一個消息的offset。然後把處理失敗的消息保存在一個緩衝區中(以便下一個poll循環不會覆蓋它們)並繼續嘗試處理它們。你可以使用consumer.pause(Collection<TopicPartition> partitions)方法暫停從指定分區讀取消息(這時如果調用poll方法將不會返回任何消息),直到完成重試處理之前的消息。

選項2,當遇到可重試異常時,把處理失敗的消息寫到另外一個topic並繼續處理下一批消息,然後使用另外一組消費者來重試處理這些消息。

4.2.6 消費者維護狀態

在某些應用程序中,你需要在不同poll循環之間維護狀態。例如,計算移動的平均值,你需要在每次調用poll循環後更新該平均值。如果進程被重新啓動,你不僅需要從上一個offset開始讀取消息,而且還需要恢復匹配之前計算的移動平均值,實現的其中一種方法可以在提交offset的同時把最新的累加值寫到另外一個topic中。這意味着當一個線程啓動時,它可以獲取最新的累加值並在上次停止的地方開始讀取消息。但是,也有可能在把最新的累加值寫到另外一個topic後但在提交offsets前發生故障。一般來說,這是一個相當複雜的問題,建議嘗試使用類似Kafka Streams來解決這樣的問題,因爲它爲aggregation、joins、windows和其它複雜的分析提供了high level DSL-like APIs。

4.2.7 處理長時間的任務

有時處理消息需要很長的時間,例如與一個可能阻塞或執行非常複雜計算的service進行交互。在某些版本的Kafka消費者中,你無法停止poll循環超過幾秒鐘。即使你不想處理更多的消息,也必須繼續調用poll方法以便客戶端可以向broker發送心跳。在這些情況下,一種常見的模式是把這些消息放到一個線程池裏使用多線程來加快處理速度。一旦把消息放到線程池裏,你可以暫停消費者從指定分區讀取消息(調用consumer.pause方法,而且不會觸發負載再均衡)並繼續調用poll方法以便發送心跳(這個時候不會讀取到任何消息),直到消息被處理完畢。

4.2.8 保證只發送一次

有些應用程序不僅需要至少發送一次(保證沒有數據丟失),而且只需要發送僅僅一次。雖然Kafka目前還沒有完全支持只發送一次,但消費者可以採用一些技巧保證每條消息將會被寫入到外部系統僅一次。

最簡單且可能最常見的方法是將結果寫入一個支持唯一鍵的系統,這包括所有的key-value存儲,所有關係型數據庫,Elasticsearch以及其它數據存儲。你只需要創建一個唯一的key,可以使用topic、分區和offset的結合。當寫入相同的消息時,後者會覆蓋前者因爲它們有相同的key,這種模式稱爲冪等寫。

另一種方法是將結果寫入一個支持事務的系統,關係型數據庫是最簡單的示例。例如在同一個事務裏把消息和它們的offsets寫入到數據庫裏以便它們保持一致,當消費者重啓時,先從數據庫獲取最新的offset然後使用consumer.seek()方法從broker指定的offset讀取消息。

5. 驗證系統的可靠性

5.1 驗證配置

從應用系統中單獨測試broker和客戶端配置是比較容易的,而且也建議這麼做,它有助於測試選擇的配置是否符合需求。Kafka包含兩個重要的工具來幫助驗證,org.apache.kafka.tools工具包包含VerifiableProducer和VerifiableConsumer類,它們可以作爲命令行工具運行,也可以嵌入到自動化測試框架裏面。

使用VerifiableProducer生成一些消息,其中包含從1到你選擇的值,然後設置acks、retries和生成消息的速率。當運行時,它將根據接收到的acks爲發送給broker的每條消息打印成功或錯誤信息。VerifiableConsumer讀取消息(通常是由VerifiableProducer生成)並按順序打印讀取到的消息,它還會打印提交和負載再均衡的信息。

也建議考慮執行以下測試:

  • Leader選舉:如果把leader kill掉會發生什麼情況?生產者和消費者需要多久才能恢復正常工作?
  • 控制器選舉:重啓控制器後系統需要多長時間才能恢復?
  • 滾動重啓:可以逐個重啓brokers而不會丟失任何消息嗎?
  • Unclean leader選舉:如果逐個kill掉一個分區的所有副本(確保每個副本脫離同步)然後啓動一個非同步副本會發生什麼情況?爲了恢復正常工作需要做些什麼?

然後選擇一個場景,啓動VerifiableProducer和VerifiableConsumer,並驗證該場景。例如,kill掉你正在寫入數據的leader。如果你期待短暫的停止,然後恢復正常而不會丟失消息,請確保生產者生成的消息數和消費者讀取的消息數一致。另外,Apache Kafka的源碼庫還包含一個擴展的測試套件。

5.2 驗證應用

一旦確定broker和客戶端配置符合需求,就可以測試整個應用程序是否也符合需求。這將驗證諸如自定義異常處理、offset的提交、負載再均衡等等的功能,特別是以下場景:

  • 客戶端失去與服務器的連接
  • Leader選舉
  • brokers的滾動重啓
  • 消費者的滾動重啓
  • 生產者的滾動重啓

5.3 監控系統的可靠性

測試應用程序很重要,但它不能取代持續監控生產系統以確保消息按預期傳輸的需要。但除了監控集羣是否正常運行,還必須監控客戶端和消息的傳輸。

首先,Kafka的Java客戶端包括允許監控客戶端狀態和事件的JMX指標。對於生產者來說,對可靠性最重要的2個指標是錯誤率和每條消息的重試率。除了監控生產者的錯誤日誌,還要注意在發送消息時記錄的WARN級別日誌,例如“Got error produce response with correlation id 5689 on topic-partition [topic-1,3], retrying (two attempts left). Error:…”。

在消費者方面,最重要的指標是消費者滯後時間,這個指標顯示消費者與提交到broker分區最新消息的距離。理想情況下,滯後總是0,也就是消費者將始終讀取最新的消息。實際上,因爲調用poll()方法返回批量消息,然後消費者在讀取下一批消息之前需要時間處理它們,所以滯後時間總是會有一點波動。重要的是確保消費者最終能夠追上而不是滯後時間變得更長。Burrow是linkedin開源的一個監控Apache Kafka的工具,可以用來監控消費者的滯後情況。

監控消息的傳輸還意味着確保所有生成的消息能夠被及時消費。爲了實現這個,你需要知道消息是何時生成的。從版本0.10.0開始,所有消息都包含一個timestamp,顯示消息的生成時間。爲了確保在一個合理的時間內消費生成的所有消息,你將需要應用程序來記錄生成的消息數(通常是每秒消息數)。消費者不僅需要記錄消費的消息數(每秒消息數),而且需要通過消息的timestamp記錄從消息的生成到被消費的滯後時間。然後你將需要一個系統來協調生產者和消費者的處理的每秒消息數(確保沒有丟失消息)並確保消息被處理的滯後時間在一個合理的範圍內。爲了實現更好地監控,你可以添加對關鍵topics的監控,但這些類型的end-to-end監控系統在實現起來可以說是極具挑戰和耗時的。

本文比較理論化,但希望你能把理論應用到實踐上面。

END O(∩_∩)O

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章