kafka確保數據不丟失

一、關於acks、retries、replication.factor、min.insync.replicas

Producer在發佈消息到某個Partition時,先通過ZooKeeper找到該Partition的Leader,然後無論該Topic的Replication Factor爲多少(也即該Partition有多少個Replica),Producer只將該消息發送到該Partition的Leader。Leader會將該消息寫入其本地Log。每個Follower都從Leader中pull數據。

針對上述情況,得出如下分析

 

(1)生產者丟數據

在kafka生產中,基本都有一個leader和多個follwer。follwer會去同步leader的信息。因此,爲了避免生產者丟數據,做如下兩點配置:

  • 第一個配置要在producer端設置acks=all。這個配置保證了,follwer同步完成後,才認爲消息發送成功。

  • 在producer端設置retries=MAX,一旦寫入失敗,這無限重試

 

(2)消息隊列丟數據

針對消息隊列丟數據的情況,無外乎就是,數據還沒同步,leader就掛了,這時zookpeer會將其他的follwer切換爲leader,那數據就丟失了。針對這種情況,應該做兩個配置:

  • replication.factor參數,這個值必須大於1,即要求每個partition必須有至少2個副本

  • min.insync.replicas參數,這個值必須大於1,這個是要求一個leader至少感知到有一個follower還跟自己保持聯繫

這兩個配置加上上面生產者的配置聯合起來用,基本可確保kafka不丟數據;

官方文檔:https://kafka.apache.org/documentation/ 可以查到一下建議:

A typical scenario would be to create a topic with a replication factor of 3, set min.insync.replicas to 2,and produce with acks of "all". This will ensure that the producer raises an exception if a majority of replicas do not receive a write.

一個典型的配置是:一個topic配置副本因數爲3,min.insync.replicas=2,acks=all,這樣可以讓生產者在多數副本沒有接收到寫應答的時候拋出一個異常。

數據複製及ISR隊列

leader會維護一個與其基本保持同步的replica列表,這個列表被稱爲isr(in-sync-replica)

如果一個follower比leader落後太多,或者超過一定時間未發起數據複製請求,則leader將其從ISR移除

當Isr中所有replica都向leader發送ACK時,leader就commit同步的數據,之後consumer才能夠消費這部分commit的數據

COMMIT策略

server配置:

replica.lag.time.max.ms  = 10000

replica.lag.max.messages = 4000

topic配置:

min.insync.replicas = 6  (isr隊列的備份數)

producer配置:

request.required.acks = ALL   (0就是不需要任何ack,相當於異步發送,只管發不管ack; 設成all或者-1,要求所有的fowller給leader回覆,然後leader給producer確認,是最保險的,但是犧牲吞吐量)

1. 可以看到SR={A,B,C},Leader(A)節點中存在m1,m2,m3三條消息,F(B)存在m1,m2兩條消息,f(c)只存在m1一消息,所以這裏只會提交m1這條消息,因爲m2這條消息還沒有在ISR中完成複製。它只會提交三個ISR中都存在的消息。

  2. 當L(A)在將消息m2複製到B,C之後掛掉,此時ISR中只有{B,C},B被選舉成爲新的主節點,當m2,m1都存在於B,C節點中時,B將會提交m1,m2兩條消息,不會提交m3消息。

  3. 此時消息將都會發送到B節點上,C節點同步了B節點中的新發的消息m4,m5之後,將會提交m4,m5.

  5.此時A節點連接集羣成功或重啓,可以使用了,它會從B節點中同步從m1,到m5的消息,直到它的消息與B和C中的一致爲止,此時的Replica將會變成ISR={A,B,C},完成了Replica的恢復。這裏我們發現m3並沒有存在了,這裏並不是丟失了,只是當沒有主節點提交m3這條消息時,它將會自動反饋到Producer,Producer會重試,或做其他處理,當重試成功後可能m3消息將會append到m5的後面,所以consumer消費消息時,我們保證的順序性不是producer發送消息的順序,而是commit時的順序

 

(3)消費者丟數據

這種情況一般是自動提交了offset,然後你處理程序過程中掛了。kafka以爲你處理好了。再強調一次offset是幹嘛的

offset:指的是kafka的topic中的每個消費組消費的下標。簡單的來說就是一條消息對應一個offset下標,每次消費數據的時候如果提交offset,那麼下次消費就會從提交的offset加一那裏開始消費。

比如一個topic中有100條數據,我消費了50條並且提交了,那麼此時的kafka服務端記錄提交的offset就是49(offset從0開始),那麼下次消費的時候offset就從50開始消費。

解決方案也很簡單,改成手動提交即可。

二、Kafka無消息丟失配置

Kafka到底會不會丟數據(data loss)? 通常不會,但有些情況下的確有可能會發生。下面的參數配置及Best practice列表可以較好地保證數據的持久性(當然是trade-off,犧牲了吞吐量)。筆者會在該列表之後對列表中的每一項進行討論,有興趣的同學可以看下後面的分析。

  • block.on.buffer.full = true

  • acks = all

  • retries = MAX_VALUE

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

  • 使用KafkaProducer.send(record, callback)

  • callback邏輯中顯式關閉producer:close(0)

  • unclean.leader.election.enable=false

  • replication.factor = 3

  • min.insync.replicas = 2

  • replication.factor > min.insync.replicas

  • enable.auto.commit=false

  • 消息處理完成之後再提交位移

給出列表之後,我們從兩個方面來探討一下數據爲什麼會丟失:

 

1、producer端:

    -- Accumulator(batch丟失)-- 目前比較新版本的Kafka正式替換了Scala版本的old producer,使用了由Java重寫的producer。新版本的producer採用異步發送機制。KafkaProducer.send(ProducerRecord)方法僅僅是把這條消息放入一個緩存中(即RecordAccumulator,本質上使用了隊列來緩存記錄),同時後臺的IO線程會不斷掃描該緩存區,將滿足條件的消息封裝到某個batch中然後發送出去。顯然,這個過程中就有一個數據丟失的窗口:若IO線程發送之前producer的client端掛掉了,累積在accumulator中的數據的確有可能會丟失。

    -- Producer的另一個問題是消息的亂序問題 --。假設客戶端代碼依次執行下面的語句將兩條消息發到相同的分區producer.send(record1); producer.send(record2); 如果此時由於某些原因(比如瞬時的網絡抖動)導致record1沒有成功發送,同時Kafka又配置了重試機制和max.in.flight.requests.per.connection大於1(默認值是5,本來就是大於1的),那麼重試record1成功後,record1在分區中就在record2之後,從而造成消息的亂序。很多某些要求強順序保證的場景是不允許出現這種情況的。

    鑑於producer的這兩個問題,我們應該如何規避呢??對於消息丟失的問題,很容易想到的一個方案就是:既然異步發送有可能丟失數據, 我改成同步發送總可以吧?比如這樣:

producer.send(record).get();這樣當然是可以的,但是性能會很差,不建議這樣使用。

    因此特意總結了一份配置列表。個人認爲該配置清單應該能夠比較好地規避producer端數據丟失情況的發生:(特此說明一下,軟件配置的很多決策都是trade-off(權衡),下面的配置也不例外:應用了這些配置,你可能會發現你的producer/consumer 吞吐量會下降,這是正常的,因爲你換取了更高的數據安全性)

  • block.on.buffer.full = true  儘管該參數在0.9.0.0已經被標記爲“deprecated”(反對),但鑑於它的含義非常直觀,所以這裏還是顯式設置它爲true,使得producer將一直等待緩衝區直至其變爲可用。否則如果producer生產速度過快耗盡了緩衝區,producer將拋出異常

  • acks=all  很好理解,所有follower都響應了才認爲消息提交成功,即"committed"

  • retries = MAX 無限重試,直到你意識到出現了問題:)

  • max.in.flight.requests.per.connection = 1 限制客戶端在單個連接上能夠發送的未響應請求的個數。設置此值是1表示kafka broker在響應請求之前client不能再向同一個broker發送請求。注意:設置此參數是爲了避免消息亂序

  • 使用KafkaProducer.send(record, callback)而不是send(record)方法   自定義回調邏輯處理消息發送失敗

  • callback邏輯中最好顯式關閉producer:close(0) 注意:設置此參數是爲了避免消息亂序

  • unclean.leader.election.enable=false   關閉unclean leader選舉,即不允許非ISR(in-sync Replica,同步複製因子列表)中的副本被選舉爲leader,以避免數據丟失

  • replication.factor >= 3 這個是kafka官網建議,參考了Hadoop及業界通用的三備份原則

  • min.insync.replicas > 1 消息至少要被寫入到這麼多副本纔算成功,也是提升數據持久性的一個參數。與acks(設置爲all的前提下生效)配合使用

  • 保證replication.factor > min.insync.replicas  如果兩者相等,當一個副本掛掉了分區也就沒法正常工作了。通常設置replication.factor = min.insync.replicas + 1即可

     

2、consumer端:

consumer端丟失消息的情形比較簡單:如果在消息處理完成前就提交了offset,那麼就有可能造成數據的丟失。由於Kafka consumer默認是自動提交位移的,所以在後臺提交位移前一定要保證消息被正常處理了,因此不建議採用很重的處理邏輯,如果處理耗時很長,則建議把邏輯放到另一個線程中去做。爲了避免數據丟失,現給出兩點建議:

三、關於maybeExpire函數:

    此問題我們實際開發中出現過(具體可以看https://wiki.xesv5.com/pages/viewpage.action?pageId=12953052),最終落到的問題在於異常org.apache.kafka.common.errors.TimeoutException上面,解決方法除了文中提到的增加request.timeout.ms從10s增大到30s外,我覺得這一步很重要:在produce要設置回調函數callback,回調函數返回時檢查兩個參數(RecordMetadata和Exception),通過Exception我們可以知道拋出的錯誤是什麼,通過RecordMetadata我們可以知道topic、partiton、offset的相關信息,這些信息可以幫助我們在邏輯層進行重試(kafka自動重試外的)或者對錯誤進行告警;

1、kafka中producer/Callback.java定義了Callback的onComplete:

2、kafka中producer/RecordMetadata.java定義了RecordMetadata:

3、kafka中發生丟棄消息的maybeExpire函數(注意其中的三個丟棄判斷):

 

 

(1).producer.properties:生產端的配置文件

#指定kafka節點列表,用於獲取metadata,不必全部指定
#需要kafka的服務器地址,來獲取每一個topic的分片數等元數據信息。
metadata.broker.list=kafka01:9092,kafka02:9092,kafka03:9092

#生產者生產的消息被髮送到哪個block,需要一個分組策略。
#指定分區處理類。默認kafka.producer.DefaultPartitioner,表通過key哈希到對應分區
#partitioner.class=kafka.producer.DefaultPartitioner

#生產者生產的消息可以通過一定的壓縮策略(或者說壓縮算法)來壓縮。消息被壓縮後發送到broker集羣,
#而broker集羣是不會進行解壓縮的,broker集羣只會把消息發送到消費者集羣,然後由消費者來解壓縮。
#是否壓縮,默認0表示不壓縮,1表示用gzip壓縮,2表示用snappy壓縮。
#壓縮後消息中會有頭來指明消息壓縮類型,故在消費者端消息解壓是透明的無需指定。
#文本數據會以1比10或者更高的壓縮比進行壓縮。
compression.codec=none

#指定序列化處理類,消息在網絡上傳輸就需要序列化,它有String、數組等許多種實現。
serializer.class=kafka.serializer.DefaultEncoder

#如果要壓縮消息,這裏指定哪些topic要壓縮消息,默認empty,表示不壓縮。
#如果上面啓用了壓縮,那麼這裏就需要設置
#compressed.topics= 
#這是消息的確認機制,默認值是0。在面試中常被問到。
#producer有個ack參數,有三個值,分別代表:
#(1)不在乎是否寫入成功;
#(2)寫入leader成功;
#(3)寫入leader和所有副本都成功;
#要求非常可靠的話可以犧牲性能設置成最後一種。
#爲了保證消息不丟失,至少要設置爲1,也就
#是說至少保證leader將消息保存成功。
#設置發送數據是否需要服務端的反饋,有三個值0,1,-1,分別代表3種狀態:
#0: producer不會等待broker發送ack。生產者只要把消息發送給broker之後,就認爲發送成功了,這是第1種情況;
#1: 當leader接收到消息之後發送ack。生產者把消息發送到broker之後,並且消息被寫入到本地文件,才認爲發送成功,這是第二種情況;#-1: 當所有的follower都同步消息成功後發送ack。不僅是主的分區將消息保存成功了,
#而且其所有的分區的副本數也都同步好了,纔會被認爲發動成功,這是第3種情況。
request.required.acks=0

#broker必須在該時間範圍之內給出反饋,否則失敗。
#在向producer發送ack之前,broker允許等待的最大時間 ,如果超時,
#broker將會向producer發送一個error ACK.意味着上一次消息因爲某種原因
#未能成功(比如follower未能同步成功)
request.timeout.ms=10000

#生產者將消息發送到broker,有兩種方式,一種是同步,表示生產者發送一條,broker就接收一條;
#還有一種是異步,表示生產者積累到一批的消息,裝到一個池子裏面緩存起來,再發送給broker,
#這個池子不會無限緩存消息,在下面,它分別有一個時間限制(時間閾值)和一個數量限制(數量閾值)的參數供我們來設置。
#一般我們會選擇異步。
#同步還是異步發送消息,默認“sync”表同步,"async"表異步。異步可以提高發送吞吐量,
#也意味着消息將會在本地buffer中,並適時批量發送,但是也可能導致丟失未發送過去的消息
producer.type=sync

#在async模式下,當message被緩存的時間超過此值後,將會批量發送給broker,
#默認爲5000ms
#此值和batch.num.messages協同工作.
queue.buffering.max.ms = 5000

#異步情況下,緩存中允許存放消息數量的大小。
#在async模式下,producer端允許buffer的最大消息量
#無論如何,producer都無法儘快的將消息發送給broker,從而導致消息在producer端大量沉積
#此時,如果消息的條數達到閥值,將會導致producer端阻塞或者消息被拋棄,默認爲10000條消息。
queue.buffering.max.messages=20000

#如果是異步,指定每次批量發送數據量,默認爲200
batch.num.messages=500

#在生產端的緩衝池中,消息發送出去之後,在沒有收到確認之前,該緩衝池中的消息是不能被刪除的,
#但是生產者一直在生產消息,這個時候緩衝池可能會被撐爆,所以這就需要有一個處理的策略。
#有兩種處理方式,一種是讓生產者先別生產那麼快,阻塞一下,等會再生產;另一種是將緩衝池中的消息清空。
#當消息在producer端沉積的條數達到"queue.buffering.max.meesages"後阻塞一定時間後,
#隊列仍然沒有enqueue(producer仍然沒有發送出任何消息)
#此時producer可以繼續阻塞或者將消息拋棄,此timeout值用於控制"阻塞"的時間
#-1: 不限制阻塞超時時間,讓produce一直阻塞,這個時候消息就不會被拋棄
#0: 立即清空隊列,消息被拋棄
queue.enqueue.timeout.ms=-1


#當producer接收到error ACK,或者沒有接收到ACK時,允許消息重發的次數
#因爲broker並沒有完整的機制來避免消息重複,所以當網絡異常時(比如ACK丟失)
#有可能導致broker接收到重複的消息,默認值爲3.
message.send.max.retries=3

#producer刷新topic metada的時間間隔,producer需要知道partition leader
#的位置,以及當前topic的情況
#因此producer需要一個機制來獲取最新的metadata,當producer遇到特定錯誤時,
#將會立即刷新
#(比如topic失效,partition丟失,leader失效等),此外也可以通過此參數來配置
#額外的刷新機制,默認值600000
topic.metadata.refresh.interval.ms=60000

(2).consumer.properties:消費端的配置文件

#消費者集羣通過連接Zookeeper來找到broker。
#zookeeper連接服務器地址
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181

#zookeeper的session過期時間,默認5000ms,用於檢測消費者是否掛掉
zookeeper.session.timeout.ms=5000

#當消費者掛掉,其他消費者要等該指定時間才能檢查到並且觸發重新負載均衡
zookeeper.connection.timeout.ms=10000

#這是一個時間閾值。
#指定多久消費者更新offset到zookeeper中。
#注意offset更新時基於time而不是每次獲得的消息。
#一旦在更新zookeeper發生異常並重啓,將可能拿到已拿到過的消息
zookeeper.sync.time.ms=2000

#指定消費
group.id=xxxxx

#這是一個數量閾值,經測試是500條。
#當consumer消費一定量的消息之後,將會自動向zookeeper提交offset信息#注意offset信息並不是每消費一次消息就向zk提交
#一次,而是現在本地保存(內存),並定期提交,默認爲true
auto.commit.enable=true

# 自動更新時間。默認60 * 1000
auto.commit.interval.ms=1000

# 當前consumer的標識,可以設定,也可以有系統生成,
#主要用來跟蹤消息消費情況,便於觀察
conusmer.id=xxx

# 消費者客戶端編號,用於區分不同客戶端,默認客戶端程序自動產生
client.id=xxxx

# 最大取多少塊緩存到消費者(默認10)
queued.max.message.chunks=50

# 當有新的consumer加入到group時,將會reblance,此後將會
#有partitions的消費端遷移到新  的consumer上,如果一個
#consumer獲得了某個partition的消費權限,那麼它將會向zk
#註冊 "Partition Owner registry"節點信息,但是有可能
#此時舊的consumer尚沒有釋放此節點, 此值用於控制,
#註冊節點的重試次數.
rebalance.max.retries=5

#每拉取一批消息的最大字節數
#獲取消息的最大尺寸,broker不會像consumer輸出大於
#此值的消息chunk 每次feth將得到多條消息,此值爲總大小,
#提升此值,將會消耗更多的consumer端內存
fetch.min.bytes=6553600

#當消息的尺寸不足時,server阻塞的時間,如果超時,
#消息將立即發送給consumer
#數據一批一批到達,如果每一批是10條消息,如果某一批還
#不到10條,但是超時了,也會立即發送給consumer。
fetch.wait.max.ms=5000
socket.receive.buffer.bytes=655360

# 如果zookeeper沒有offset值或offset值超出範圍。
#那麼就給個初始的offset。有smallest、largest、
#anything可選,分別表示給當前最小的offset、
#當前最大的offset、拋異常。默認largest
auto.offset.reset=smallest

# 指定序列化處理類
derializer.class=kafka.serializer.DefaultDecoder

(3).server.properties:服務端的配置文件

#broker的全局唯一編號,不能重複
broker.id=0

#用來監聽鏈接的端口,producer或consumer將在此端口建立連接
port=9092

#處理網絡請求的線程數量,也就是接收消息的線程數。
#接收線程會將接收到的消息放到內存中,然後再從內存中寫入磁盤。
num.network.threads=3

#消息從內存中寫入磁盤是時候使用的線程數量。
#用來處理磁盤IO的線程數量
num.io.threads=8

#發送套接字的緩衝區大小
socket.send.buffer.bytes=102400

#接受套接字的緩衝區大小
socket.receive.buffer.bytes=102400

#請求套接字的緩衝區大小
socket.request.max.bytes=104857600

#kafka運行日誌存放的路徑
log.dirs=/export/servers/logs/kafka

#topic在當前broker上的分片個數
num.partitions=2

#我們知道segment文件默認會被保留7天的時間,超時的話就
#會被清理,那麼清理這件事情就需要有一些線程來做。這裏就是
#用來設置恢復和清理data下數據的線程數量
num.recovery.threads.per.data.dir=1

#segment文件保留的最長時間,默認保留7天(168小時),
#超時將被刪除,也就是說7天之前的數據將被清理掉。
log.retention.hours=168

#滾動生成新的segment文件的最大時間
log.roll.hours=168

#日誌文件中每個segment的大小,默認爲1G
log.segment.bytes=1073741824

#上面的參數設置了每一個segment文件的大小是1G,那麼
#就需要有一個東西去定期檢查segment文件有沒有達到1G,
#多長時間去檢查一次,就需要設置一個週期性檢查文件大小
#的時間(單位是毫秒)。
log.retention.check.interval.ms=300000

#日誌清理是否打開
log.cleaner.enable=true

#broker需要使用zookeeper保存meta數據
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181

#zookeeper鏈接超時時間
zookeeper.connection.timeout.ms=6000

#上面我們說過接收線程會將接收到的消息放到內存中,然後再從內存
#寫到磁盤上,那麼什麼時候將消息從內存中寫入磁盤,就有一個
#時間限制(時間閾值)和一個數量限制(數量閾值),這裏設置的是
#數量閾值,下一個參數設置的則是時間閾值。
#partion buffer中,消息的條數達到閾值,將觸發flush到磁盤。
log.flush.interval.messages=10000

#消息buffer的時間,達到閾值,將觸發將消息從內存flush到磁盤,
#單位是毫秒。
log.flush.interval.ms=3000

#刪除topic需要server.properties中設置delete.topic.enable=true否則只是標記刪除
delete.topic.enable=true

#此處的host.name爲本機IP(重要),如果不改,則客戶端會拋出:
#Producer connection to localhost:9092 unsuccessful 錯誤!
host.name=kafka01

advertised.host.name=192.168.239.128

 

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