最近事情多有點犯懶,依然帶來一篇譯文:Apache Kafka Producer Improvements with the Sticky Partitioner
消息在系統中流轉的時間對於Kafka的性能來說至關重要。具體到Producer而言,Producer端的延時(Latency)通常被定義為:Producer發送消息到Kafka響應消息之間的時間間隔。俗話說得好,時間就是金錢。我們總是希望盡可能多地降低延時,讓系統跑的更快。如果Producer發送消息的速度加快,整個系統都能受益。
每個Kafka topic包括若干個分區。當Producer給一個主題發送消息時,它首先需要確認這條消息要發送到哪個分區上。如果我們同時給同一個分區發送多條消息,那么Producer可以將這些消息打包成批(batch)發送。處理batch是有額外開銷的。在這其中,batch中的每條消息都會貢獻自己的“力量”。如果batch很小,那么batch中消息貢獻的開銷就越大。通常來說,小batch會導致Producer端產生更多的請求,造成更嚴重的隊列積壓效果(queueing),從而整體上推高了延時。
當batch大小達到了閾值(batch.size)或batch積累消息的時間超過了linger.ms時batch被視為“已完成”或“准備就緒”。batch.size和linger.ms都是Producer端的參數。batch.size默認值是16KB,而linger.ms默認值是0毫秒。一旦達到了batch.size值或越過了linger.ms時間,系統就會立即發送batch。
乍看上去,設置linger.ms=0似乎只會導致batch中只包含一條消息。但其實不是這樣的。即使linger.ms=0,Producer依然會將多條消息打包進一個batch,只要它們是在很短的時間內被生產出來且都是發往同一個分區的。這是因為系統需要一定的時間來處理每個PRODUCE請求,而一旦系統無法及時處理所有消息,它就會將它們放入到一個batch里面。
決定batch如何形成的一個因素是分區策略(partitioning strategy)。如果多條消息不是被發送到相同的分區,它們就不能被放入到一個batch中。幸運的是,Kafka允許用戶通過設置Partitioner實現類的方式來選擇合適的分區策略。Partitioner接口負責為每條消息分配一個分區。默認的策略是對消息的Key進行哈希計算以獲取目標分區,但是很多時候消息是沒有指定Key的(或者說Key為null)。此時,Apache Kafka 2.4之前的默認策略是循環使用主題的所有分區,將消息以輪詢的方式發送到每一個分區上。不幸的是,這種策略打包效果很差,在實際使用中會增加延時。
鑒於小batch可能導致延時增加,之前對於無Key消息的分區策略效率很低。社區於2.4版本引入了黏性分區策略(Sticky Partitioning Strategy)。該策略是一種全新的策略,能夠顯著地降低給消息指定分區過程中的延時。
黏性分區策略
黏性分區器(Sticky Partitioner)解決無Key消息分散到小batch問題的主要思路是選擇單個分區發送所有的無Key消息。一旦這個分區的batch已滿或處於“已完成”狀態,黏性分區器會隨機地選擇另一個分區並會盡可能地堅持使用該分區——即所謂的粘住這個分區。這樣,一旦我們拉長整個運行時間,消息還是能均勻地發布到各個分區上,避免出現分區傾斜,同時Producer還能降低延時,因為這個分配過程中始終能確保形成較大的batch,而非小batch。簡單來說,整個過程如下圖所示:
為了實現這個黏性分區器,Kafka 2.4版本為Partitioner接口新增了一個名為onNewBatch的方法。該方法會在新batch被創建前輩調用,也就是Producer要變更黏性分區(Sticky Partition)的時候。Producer默認分區器DefaultPartitioner實現了這個功能。
基礎測試:Producer端延時
任何性能提升都必須要量化其效果。Kafka提供了一個名為Trogdor的測試框架(譯者:官網幾乎不見Trogdor的描述,有時間我寫一篇blog介紹下)用於運行多種基礎測試,這其中就包含測試Producer端延時。我使用了一個名為Castle的測試套件來執行ProduceBench測試。這些測試運行在一個3 Broker組成的集群上,配以1~3個Producer程序。
大多數的測試都使用如下的參數配置。你可以修改Castle配置,替換掉默認的任務參數配置。有些測試中設置稍有不同,后面會顯式提及。
Duration of test | 12 minutes |
Number of brokers | 3 |
Number of producers | 1–3 |
Replication factor | 3 |
Topics | 4 |
linger.ms |
0 |
acks |
all |
keyGenerator |
{"type":"null"} |
useConfiguredPartitioner |
true |
No flushing on throttle (skipFlush ) |
true |
為了獲得最好的測試效果,我們必須要設置useConfiguredPartitioner和skipFlush為true。這將確保Producer使用DefaultPartitioner執行分區策略,同時是否發送batch以batch.size和linger.ms參數來決定。當然了,你肯定要將keyGenerator設置成null,令消息無Key。
在與未修改前DefaultPartitioner的比較重,大多數測試表現出來的延時均低於之前版本。特別是在比較99百分位延時數值這一項時,3個Producer以每秒1000條消息的速度向16分區主題發送消息,測試結果表明使用新黏性分區器的Producer的延時是之前的一半。測試結果如下:
隨着分區數的增加,延時下降的效果更加明顯。這和我們之前的推測是吻合的:構造少量的大batch要比構造大量的小batch性能好。在我們的測試中分區數達到16時這種差異就已經很明顯了。
接下來的測試保持Producer端的負載不變但增加了分區數。下圖分別展示了16個、64個和128個分區的測試結果,結果表明之前分區策略的延時隨着分區數的增加惡化得很快。即使是16個分區,平均的99百分位延時值依然是黏性分區器的1.5倍。
linger.ms測試以及不同Key消息的性能
如前所述,等待linger.ms會人為地增加延時。黏性分區器的目標就是要阻止這種延時,具體辦法是把所有消息都發送到一個batch中,更快地填充batch。在一個相對吞吐量很低的環境中使用黏性分區器配以一個大於0的linger.ms值能夠顯著降低延時。我們的測試啟動了一個Producer程序,每秒發送1000條消息,同時linger.ms=1000,結果表明之前分區策略的99百分位延時值提高了5倍。下圖展示了ProduceBench測試的結果:
如果Producer發送的是無Key消息,那么黏性分區器能夠有效提升客戶端的性能。如果Producer同時發送無Key消息和有Key消息,那么性能會是如何呢?我們運行了一個測試,隨機為消息生成Key同時混以一些無Key的消息,結果表明在延時這個指標上無明顯區別。
在這個測試場景下,我檢查了有Key和無Key的混合消息。看上去這樣的打包效果更好,但是因為Key值會忽略黏性分區器,因而收益並不明顯。下圖展示了三次運行結果的99百分位延時值的中位數值。從測試結果來看,延時並無顯著變化,因此,該中位數能夠有效地表征一次典型的測試運行結果。
第二個測試場景是在高吞吐量下隨機生產Key。由於實現黏性分區器需要稍微變更代碼,因此我們要確保新增加的邏輯不會影響延時。鑒於這里並沒有任何打包或需要黏性行為出現,看上去延時和之前的差不多也是正常的結果。隨機Key測試的中位數結果如下圖所示。
最后,我測試了一個我認為最不適合使用黏性分區器的場景——順序Key + 超多分區的場景。因為新增邏輯主要發生在新batch被創建,而這個場景下差不多為每條消息都創建一個batch,因此我們必須要檢查這不會推高延時。結果顯示延時沒有明顯差異,如下圖所示:
測試過程中的CPU使用率
在測試過程中,我們注意到衡量黏性分區器能夠降低CPU使用率。
舉例來說,當3個Producer程序以每秒1萬條消息的速度向16個分區發送消息時,我們觀測到CPU使用率有明顯下降。下兩圖中的每一行都表示節點的CPU使用率。每個節點既運行Producer程序也運行Broker進程。所有節點的線重疊在一起。在分區數增加和吞吐量下降的測試場景中我們也觀測到了CPU的下降。
總結
黏性分區器的主要目標是為了增加每個batch中的消息數以減少總的batch數,從而消除不必要的queueing時間。一旦batch中消息變多,batch數變少,每條消息的成本就降低了。使用黏性分區策略能夠更快地發送消息。測試數據已經證明該策略確實能夠為無Key消息降低延時,並且當分區數增加時提升的效果更加明顯。另外,使用黏性分區策略通常還能降低CPU使用。通過將Producer“黏在”一個分區並發送更少的大batch的方式,Producer能夠有效地提升發送性能。
同時,最酷的是:這個黏性分區器默認集成在2.4版本,無需額外配置,開箱即用!