Pinterest使用MemQ、Singer和Kafka最佳化大資料攝取

banq發表於2022-05-06

在 Pinterest,Logging Platform 團隊維護著每天攝取數 TB 資料的資料攝取基礎設施的骨幹。

MemQ:使用 Netty 實現記憶體高效的批次資料交付

MemQ是內部構建的下一代資料攝取平臺,最近由 Logging Platform 團隊開源。
在設計服務時,我們努力最大化我們的資源效率,特別是我們專注於透過使用堆外記憶體來減少 GC。
Netty 被選為我們的低階網路框架,因為它在靈活性、效能和複雜的開箱即用功能之間取得了很好的平衡。
例如,我們在整個專案中大量使用了 ByteBuf。ByteBufs 是 Netty 中資料的構建塊。它們類似於 Java NIO ByteBuffers,但透過提供“智慧指標”方法使用手動引用計數進行自定義記憶體管理,允許開發人員更多地控制物件的生命週期. 透過使用 ByteBufs,我們設法透過傳遞堆外網路緩衝區指標來傳輸帶有單個資料副本的訊息,從而進一步減少了垃圾收集所使用的週期。

訊息在MemQ代理中的典型旅程:

  • 每個從網路上收到的訊息都將透過一個長度編碼的協議進行重構,該協議將被分配到JVM堆外的ByteBuf中(用Netty的話說是直接記憶體),並且將是整個管道中唯一存在的有效載荷副本。
  • 這個ByteBuf引用將被傳遞到主題處理器中,並與其他也在等待被上傳到儲存目的地的訊息一起放入一個Batch。
  • 一旦滿足上傳限制,無論是由於時間閾值還是大小閾值,Batch將被分派。
  • 在上傳到S3這樣的遠端物件儲存的情況下,整批訊息將被儲存在一個CompositeByteBuf中(這是一個由多個ByteBuf組成的虛擬包裝ByteBuf),並使用netty-reactor庫上傳到目的地,允許我們在處理路徑中不建立額外的資料副本。
  • 透過建立在ByteBufs和其他Netty結構之上,我們能夠在不犧牲效能的情況下快速迭代,避免重蹈覆轍。


Singer:利用非同步處理減少執行緒開銷

Singer已經在 Pinterest 工作了很長時間,可靠地將訊息傳遞到 PubSub 後端。隨著越來越多的用例載入 Singer,我們開始遇到記憶體使用瓶頸,導致頻繁出現 OOM 問題和事件。

隨著越來越多的用例進入Singer,我們開始遇到了記憶體使用的瓶頸,導致了頻繁的OOM問題和事件。
Singer在Pinterest的幾乎所有團隊都限制了記憶體和CPU資源,以避免對主機服務的影響,例如我們的API服務層。
在檢查了程式碼並利用VisualVM、本地記憶體跟蹤(NMT)和pmap等除錯工具後,我們注意到了各種潛在的改進措施,最明顯的是減少執行緒的數量。在進行NMT結果分析後,我們注意到了執行緒的數量以及這些執行緒(由於Singer執行器和生產者執行緒池的分配)導致的堆疊所使用的記憶體。

深入研究這些執行緒的來源,這些執行緒大部分來自Singer釋出的每個Kafka叢集的執行緒池。這些執行緒池中的執行緒是用來等待Kafka完成向分割槽寫入訊息,然後報告寫入的狀態。當執行緒完成工作時,JVM中的每個執行緒(預設情況下)將分配1MB的記憶體用於執行緒的堆疊。

KafkaWriteTask的每次提交都會佔用一個執行緒。完整的程式碼可以在這裡here找到

透過仔細檢查這些執行緒的使用情況,我們很快就會發現,這些執行緒大多在做非阻塞操作,比如更新指標,而且完全適合使用Java 8中提供的CompletableFutures進行非同步處理。
CompletableFuture允許我們透過非同步連鎖階段來解決阻塞性呼叫,從而取代了這些執行緒的使用,這些執行緒必須等待結果從Kafka回來。
透過利用KafkaProducer.send(record, callback)方法中的回撥,我們依靠Kafka生產者的網路客戶端來完全控制網路的複用。

使用CompletableFutures後的結果程式碼的一個簡單例子。完整的程式碼可以在這裡here找到

一旦我們將原來的邏輯轉換為幾個連鎖的非阻塞階段,很明顯,使用一個單一的公共執行緒池來處理它們,而不考慮日誌流,所以我們使用JVM已經提供的公共ForkJoinPool。這極大地減少了Singer的執行緒使用量,從幾百個執行緒到幾乎沒有額外的執行緒。這一改進表明了非同步處理的力量,以及受網路約束的應用程式如何從中受益。


Kafka和Singer:用可控的差異來平衡效能和效率
操作我們的Kafka叢集一直都是在效能、容錯和效率之間取得微妙的平衡。我們的日誌代理Singer處於向Kafka釋出訊息的第一線,是一個關鍵的元件,在這些因素中起著重要的作用,特別是在透過決定我們為一個主題提供資料的分割槽來路由流量。

1、預設分割槽器。均勻分佈的流量
在Singer中,來自一臺機器的日誌會被拾取並路由到它所屬的相應主題,併發布到Kafka中的該主題。在早期,Singer會使用我們的預設分割槽器,以輪流的方式統一發布到該主題的所有分割槽。例如,如果某臺主機上有3000條訊息需要釋出到30個分割槽的主題上,每個分割槽大概會收到100條訊息。這對大多數用例來說效果很好,而且有一個很好的好處,即所有分割槽都收到相同數量的訊息,這對這些主題的消費者來說是很好的,因為工作負載在他們之間平均分配。

隨著Pinterest的發展,我們的團隊擴充套件到了數千臺主機,這種均勻分佈的方法開始給我們的Kafka經紀人帶來一些問題:高連線數和大量的生產請求開始提升經紀人broker的CPU使用率,而分散訊息意味著每個分割槽的批處理量更小,或者壓縮效率更低,導致聚合的網路流量更大。

為了解決這個問題,我們實施了一個新的分割槽器:SinglePartitionPartitioner。這個分割槽器透過強迫Singer在每個主機的每個主題上只寫一個隨機分割槽來解決這個問題,將所有經紀商broker的扇出減少到一個經紀商。這個分割槽在生產者的整個生命週期中保持不變,直到Singer重新啟動。

對於擁有大量生產者隊伍和各主機間相對統一的訊息速率的管道來說,這是非常有效的。大數法則對我們有利,從統計學上看,如果生產者的數量明顯大於分割槽,每個分割槽仍然會收到類似的流量。連線數從(服務於該主題的經紀商數量)乘以(生產者數量)下降到只有(生產者數量),對於較大的主題,這可能會減少100倍。同時,將每個生產者的所有訊息批次化到一個分割槽,在大多數使用情況下,壓縮率至少提高了10%。

固定分割槽分割槽器:用於調整權衡的可配置方差
儘管想出了這個新的解決方案,但仍有一些管道處於兩種解決方案都不理想的中間地帶,例如當生產者的數量不足以超過分割槽的數量時。
在這種情況下,SinglePartitionPartitioner會在分割槽之間引入明顯的傾斜:有些分割槽會有多個生產者向其寫入,而有些分割槽則被分配到非常少甚至沒有生產者。這種傾斜可能會導致下游消費者的工作負載不平衡,也會增加我們團隊管理叢集的負擔,特別是在儲存緊張的時候。
因此,我們最近推出了一個新的分割槽器,可以用在這些情況上,甚至涵蓋了原來的用例:FixedPartitionsPartitioner,它基本上允許我們不只是像SinglePartitionPartitioner那樣釋出到一個固定的分割槽,而是在固定數量的分割槽中隨機發布。

這種方法有點類似於一致性雜湊中的虛擬節點的概念,我們人為地創造更多的 "有效生產者 "以實現更連續的分佈。
由於每個主機的分割槽數量可以配置,我們可以將其調整到效率和效能都達到理想水平的甜蜜點。
這種分割槽器也可以透過分散流量,同時保持合理的連線數,來幫助解決 "熱生產者 "的問題。
雖然是一個簡單的概念,但事實證明,有能力配置差異程度可能是管理權衡的一個強大工具。
 

相關文章