在之前的文章中,我們從高級抽象到底層細節各個層面全面介紹了 Flink 網絡棧的工作機制。作為這一系列的第二篇文章,本文將在第一篇的基礎上更進一步,主要探討如何監視與網絡相關的指標,從而識別背壓等因素帶來的影響,或找出吞吐量和延遲的瓶頸所在。本文將簡要介紹處理背壓的手段,而之后的文章將進一步研究網絡棧微調的話題。如果你不是很熟悉網絡棧的知識,強烈建議先閱讀本系列的第一篇文章 《原理解析 | 深入了解 Apache Flink 的網絡協議棧》。
監控
網絡監控工作中最重要的環節可能就是監控背壓了,所謂背壓是指系統接收數據的速率高於其處理速度 [1]。這種現象將給發送者帶來壓力,而導致它的原因可能有兩種情況:
- 接收器很慢。
這可能是因為接收器本身就遇到了背壓,所以無法以與發送方相同的速率繼續處理數據;也有可能是接收器因為垃圾回收工作、缺少系統資源或 I/O 瓶頸而暫時卡住了。
- 網絡通道很慢。
這種情況可能和接收器沒有(直接)關系,我們說這時是發送器遇到了背壓,因為在同一台機器上運行的所有子任務共享的網絡帶寬可能供不應求了。請注意,除了 Flink 的網絡棧之外可能還有其他網絡用戶,例如源(source)和匯(sink)、分布式文件系統(檢查點、網絡附加存儲)、日志記錄和指標監測等。我們之前的一篇關於容量規划的文章(https://www.ververica.com/blog/how-to-size-your-apache-flink-cluster-general-guidelines)介紹了更多相關內容。
[1] 如果你不熟悉背壓,不了解它與 Flink 的交互方式,建議閱讀我們在 2015 年發表的關於背壓的文章(https://www.ververica.com/blog/how-flink-handles-backpressure)。
當背壓出現時,它將一路向上游傳導並最終到達你的源,還會減慢它們的速度。這本身並不是一件壞事,只是表明你缺乏足夠的資源處理當前的負載。但你可能想要做一些改進,在不動用更多資源的前提下處理更高的負載。為此你需要找到(1)瓶頸在哪里(位於哪個任務 / 操作符)和(2)產生瓶頸的原因。Flink 提供了兩種識別瓶頸的機制:
- 直接通過 Flink 的 Web UI 及其背壓監視器識別
- 間接通過一些網絡指標識別。
Flink 的 Web UI 大概是快速排除故障時的首選,但它存在一些缺點,我們將在下面解釋。另一方面,Flink 的網絡指標更適合持續監控和推斷是哪些瓶頸導致了背壓,並分析這些瓶頸的本質屬性。我們將在下文中具體介紹這兩個部分。在這兩種情況下,你都需要從所有的源和匯中找出背壓的根源。調查工作的起點一般來說是最后一個承受背壓的操作符;而且最后這個操作符很可能就是背壓產生的源頭。
背壓監視器
背壓監視器只暴露在 Flink 的 WebUI[2] 中。由於它是僅在請求時才會觸發的活動組件,因此目前無法通過監控指標來提供給用戶。背壓監視器通過 Thread.getStackTrace() 對 TaskManager 上運行的所有任務線程采樣,並計算緩存請求中阻塞任務的樣本數。這些任務之所以會阻塞,要么是因為它們無法按照網絡緩沖區生成的速率發送這些緩存,要么就是下游任務處理它們的速度很慢,無法保證發送的速率。背壓監視器將顯示阻塞請求與總請求的比率。由於某些背壓被認為是正常 / 臨時的,所以監視器將顯示以下狀態:
- OK,比率 ≤ 0.10
- LOW,0.10 < 比率 ≤ 0.5
- HIGH,0.5 < 比率 ≤ 1
雖說你也可以調整刷新間隔、樣本數或樣本之間的延遲等參數,但通常情況下這些參數用不着你來調整,因為默認值提供的結果已經夠好了。
[2] 你還可以通過 REST API 訪問背壓監視器:/jobs/:jobid/vertices/:vertexid/backpressure
背壓監視器可以幫助你找到背壓源自何處(位於哪個任務 / 操作符)。但你沒法用它進一步推斷背壓產生的原因。此外,對於較大的作業或較高的並行度來說,背壓監視器顯示的信息就太亂了,很難分析,還可能要花些時間才能完整收集來自 TaskManager 的數據。另請注意,采樣工作可能還會影響你當前作業的性能。
網絡指標
網絡指標和任務 I/O 指標比背壓監視器更輕量一些,而且會針對當前運行的每個作業不斷更新。我們可以利用這些指標獲得更多信息,收集到的信息除了用來監測背壓外還有其他用途。和用戶關系最大的指標有:
- Flink 1.8 及更早版本:outPoolUsage、inPoolUsage。它們是對各個本地緩沖池中已用緩存與可用緩存的比率估計。在使用基於信用的流控制解析 Flink 1.5-1.8 中的 inPoolUsage 時,請注意它只與浮動緩存有關(獨占緩存不算在緩沖池里)。
- Flink 1.9 及更新版本:outPoolUsage、inPoolUsage、floatingBuffersUsage、exclusiveBuffersUsage
它們是對各個本地緩沖池中已用緩存與可用緩存的比率估計。從 Flink 1.9 開始,inPoolUsage 是 floatingBuffersUsage 和 exclusiveBuffersUsage 的總和。 - numRecordsOut、numRecordsIn。這兩個指標都帶有兩個作用域:一個是運算符,另一個是子任務。網絡監視使用的是子任務作用域指標,並顯示它已發送 / 接收的記錄總數。你可能需要進一步研究這些數字來找出特定時間跨度內的記錄數量,或使用等效的 PerSecond 指標。
- numBytesOut、numBytesInLocal、numBytesInRemote。表示這個子任務從本地 / 遠程源發出或讀取的字節總數。也可以通過 PerSecond 指標獲取。
- numBuffersOut、numBuffersInLocal、numBuffersInRemote。與 numBytes 類似,但這里計算的是網絡緩沖區的數量。
警告:為了完整起見,我們將簡要介紹 outputQueueLength 和 inputQueueLength 這兩個指標。它們有點像 [out、in] PoolUsage 指標,但這兩個指標分別顯示的是發送方子任務的輸出隊列和接收方子任務的輸入隊列中的緩存數量。但想要推斷緩存的准確數量是很難的,而且本地通道也有一個很微妙的特殊問題:由於本地輸入通道沒有自己的隊列(它直接使用輸出隊列),因此通道的這個值始終為 0(參見 FLINK-12576,https://issues.apache.org/jira/browse/FLINK-12576);在只有本地輸入通道的情況下 inputQueueLength = 0。
總的來說,我們不鼓勵使用 outputQueueLength 和 inputQueueLength,因為它們的解析很大程度上取決於運算符當前的並行度以及獨占緩存和浮動緩存的配置數量。相比之下,我們建議使用各種 *PoolUsage 指標,它們會為用戶提供更詳盡的信息。
注意:如果你要推斷緩存的使用率,請記住以下幾點:
任何至少使用過一次的傳出通道總是占用一個緩存(Flink 1.5 及更高版本)。
Flink 1.8 及較早版本:這個緩存(即使是空的!)總是在 backlog 中計 1,因此接收器試圖為它保留一個浮動緩存區。
Flink 1.9 及以上版本:只有當一個緩存已准備好消費時才在 backlog 中計數,比如說它已滿或已刷新時(請參閱 FLINK-11082)。
接收器僅在反序列化其中的最后一條記錄后才釋放接收的緩存。
后文會綜合運用這些指標,以了解背壓和資源的使用率 / 效率與吞吐量的關系。后面還會有一個獨立的部分具體介紹與延遲相關的指標。
背壓
有兩組指標可以用來監測背壓:它們分別是(本地)緩沖池使用率和輸入 / 輸出隊列長度。這兩種指標的粒度粗細各異,可惜都不夠全面,怎樣解讀這些指標也有很多說法。由於隊列長度指標解讀起來有一些先天困難,我們將重點關注輸入和輸出池的使用率指標,該指標也提供了更多細節信息。
- 如果一項子任務的 outPoolUsage 為 100%,則它正在經受背壓。子任務是已經阻塞了,還是仍在將記錄寫入網絡緩沖區,取決於 RecordWriter 當前正在寫入的緩存有沒有寫滿。這與背壓監視器顯示的結果是不一樣的!
- 當 inPoolUsage 為 100%時表示所有浮動緩存都分配給了通道,背壓最終將傳遞到上游。這些浮動緩存處於以下任一狀態中:由於一個獨占緩存正被占用(遠程輸入通道一直在嘗試維護 #exclusive buffer 的信用),這些浮動緩存被保留下來供將來在通道上使用;它們為一個發送器的 backlog 保留下來等待數據;它們可能包含數據並在輸入通道中排隊;或者它們可能包含數據並正由接收器的子任務讀取(一次一個記錄)。
- Flink 1.8 及更早的版本:根據 FLINK-11082(https://issues.apache.org/jira/browse/FLINK-11082),即使在正常情況下 100% 的 inPoolUsage 也很常見。
- Flink 1.9 及以上版本:如果 inPoolUsage 持續在 100%左右,這就是出現上游背壓的強烈信號。
下表總結了所有組合及其解釋。但請記住,背壓可能是次要的的或臨時的(也就是無需查看),或者只出現在特定通道上,或是由特定 TaskManager 上的其他 JVM 進程(例如 GC、同步、I/O、資源短缺等)引起的,源頭不是某個子任務。
我們甚至可以通過查看兩個連續任務的子任務的網絡指標來深入了解背壓產生的原因:
- 如果接收器任務的所有子任務的 inPoolUsage 值都很低,並且有任一上游子任務的 outPoolUsage 較高,則可能是網絡瓶頸導致了背壓。由於網絡是 TaskManager 的所有子任務共享的資源,因此瓶頸可能不是直接源自這個子任務,而是來自於各種並發操作,例如檢查點、其他流、外部連接或同一台計算機上的其他 TaskManager/ 進程。
- 背壓也可以由一個任務的所有並行實例或單個任務實例引起。
第一種情況通常是因為任務正在執行一些應用到所有輸入分區的耗時操作;后者通常是某種偏差的結果,可能是數據偏斜或資源可用性 / 分配偏差。后文的“如何處理背壓”一節中會介紹這兩種情況下的應對措施。
Flink 1.9 及以上版本
- 如果 floatingBuffersUsage 沒到 100%,那么就不太可能存在背壓。如果它達到了 100% 且所有上游任務都在承受背壓,說明這個輸入正在單個、部分或全部輸入通道上承受背壓。你可以使用 exclusiveBuffersUsage 來區分這三種情況:
假設 floatingBuffersUsage 接近 100%,則 exclusiveBuffersUsage 越高,輸入通道承受的背壓越大。在 exclusiveBuffersUsage 接近 100%的極端情況下,所有通道都在承受背壓。 -
- 下表總結了 exclusiveBuffersUsage、floatingBuffersUsage 和上游任務的 outPoolUsage 之間的關系,還比上表多了一個 inPoolUsage = floatingBuffersUsage + exclusiveBuffersUsage:
[3] 不應該出現這種情況
資源使用率 / 吞吐量
除了上面提到的各個指標的單獨用法外,還有一些組合用法可以用來探究網絡棧的深層狀況:
- 吞吐量較低時 outPoolUsage 值卻經常接近 100%,但同時所有接收器的 inPoolUsage 都很低,這表明我們的信用通知的往返時間(取決於你的網絡延遲)太久,導致默認的獨占緩存數量無法充分利用你的帶寬。可以考慮增加每通道緩存參數或嘗試禁用基於信用的流量控制。
- numRecordsOut 和 numBytesOut 這個組合可以用來確定序列化記錄的平均大小,進而幫助你針對峰值場景做容量規划。
- 如果要了解緩存填充率和輸出刷新器的影響,可以考察 numBytesInRemote 與 numBuffersInRemote 的組合。在調整吞吐量(而不是延遲!)時,較低的緩存填充率可能意味着網絡效率較低。在這種情況下請考慮增加緩存超時時間。請注意,在 Flink 1.8 和 1.9 中,numBuffersOut 僅在緩存快填滿或某事件停用某緩存(例如一個檢查點屏障)時才會增加,這個動作還可能滯后。還請注意,由於緩存是針對遠程信道的優化技術,對本地信道影響有限,因此不需要在本地信道上考察緩存填充率。
- 你還可以使用 numBytesInLocal 和 numBytesInRemote 的組合區分本地與遠程流量,但在大多數情況下沒這個必要。
如何處理背壓?
假設你確定了背壓的來源,也就是瓶頸所在,下一步就是分析為什么會發生這種情況。下面我們按照從基本到復雜的順序列出了導致背壓的一些潛在成因。我們建議首先檢查基本成因,然后再深入研究更復雜的成因,否則就可能得出一些錯誤的結論。
另外回想一下,背壓可能是暫時的,可能是由於負載高峰、檢查點或作業重啟時數據 backlog 待處理導致的結果。如果背壓是暫時的,那么忽略它就行了。此外還要記住,分析和解決問題的過程可能會受到瓶頸本身的影響。話雖如此,這里還是有幾件事需要檢查一下。
系統資源
首先,你應該檢查受控機器的基本資源使用情況,如 CPU、網絡或磁盤 I/O 等指標。如果某些資源在被全部或大量占用,你可以執行以下操作:
- 嘗試優化你的代碼。此時代碼分析器是很有用的。
- 調整這項資源的 Flink。
- 通過增加並行度和 / 或增加群集中的計算機數量來擴展資源。
垃圾收集
一般來說,長時間的垃圾回收工作會引發性能問題。你可以打印 GC 調試日志(通過 -XX: +PrintGCDetails)或使用某些內存 /GC 分析器來驗證你是否處於這種狀況下。由於 GC 問題的處理與應用程序高度相關,並且獨立於 Flink,因此我們不會在此詳細介紹(可參考 Oracle 的垃圾收集調整指南,https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html 或 Plumbr 的 Java 垃圾回收手冊,https://plumbr.io/java-garbage-collection-handbook)。
CPU/ 線程瓶頸
如果 CPU 瓶頸來自於一個或幾個線程,而整台機器的 CPU 使用率仍然相對較低,則 CPU 瓶頸可能就很難被發現了。例如,48 核計算機上的單個 CPU 線程瓶頸只會帶來 2%的 CPU 使用率。可以考慮使用代碼分析器,因為它們可以顯示每個線程的 CPU 使用情況,這樣就能識別出熱線程。
線程爭用
與上面的 CPU/ 線程瓶頸問題類似,共享資源上較高的線程爭用率可能會導致子任務瓶頸。還是要請出 CPU 分析器,考慮查找用戶代碼中的同步開銷 / 鎖爭用——雖然我們應該避免在用戶代碼中添加同步性,這可能很危險!還可以考慮調查共享系統資源。例如,默認 JVM 的 SSL 實現可以從共享的 /dev/urandom 資源周圍獲取數據。
加載不均衡
如果你的瓶頸是由數據偏差引起的,可以嘗試將數據分區更改為幾個獨立的重鍵,或實現本地 / 預聚合來清除偏差或減輕其影響。
除此之外還有很多情況。一般來說,為了削弱瓶頸從而減少背壓,首先要分析它發生的位置,然后找出原因。最好從檢查哪些資源處於充分利用狀態開始入手。
延遲追蹤
追蹤各個可能環節出現的延遲是一個獨立的話題。在本節中,我們將重點關注 Flink 網絡棧中的記錄的等待時間——包括系統網絡連接的情況。在吞吐量較低時,這些延遲會直接受輸出刷新器的緩存超時參數的影響,或間接受任何應用程序代碼延遲的影響。處理記錄的時間比預期的要長或者(多個)計時器同時觸發——並阻止接收器處理傳入的記錄——時,網絡棧內后續記錄的等待時間會大大延長。我們強烈建議你將自己的指標添加到 Flink 作業中,以便更好地跟蹤作業組件中的延遲,並更全面地了解延遲產生的原因。
Flink 為追蹤通過系統(用戶代碼之外)的記錄延遲提供了一些支持。但默認情況下此功能被禁用(原因參見下文!),必須用 metrics.latency.interval 或 ExecutionConfig #setLatencyTrackingInterval() 在 Flink 的配置中設置延遲追蹤間隔才能啟用此功能。啟用后,Flink 將根據 metrics.latency.granularity 定義的粒度生成延遲直方圖:
- single:每個操作符子任務有一個直方圖
- operator(默認值):源任務和操作符子任務的每個組合有一個直方圖
- subtask:源子任務和操作符子任務的每個組合有一個直方圖(並行度翻了兩番!)
這些指標通過特殊的“延遲標記”收集:每個源子任務將定期發出包含其創建時間戳的特殊記錄。然后,延遲標記與正常記錄一起流動,不會在線路上或緩存隊列中超過正常記錄。但是,延遲標記不會進入應用程序邏輯,並會在那里超過正常記錄。因此,延遲標記僅測量用戶代碼之間的等待時間,而不是完整的“端到端”延遲。但用戶代碼會間接影響這些等待時間!
由於 LatencyMarker 就像普通記錄一樣位於網絡緩沖區中,它們也會因緩存已滿而等待,或因緩存超時而刷新。當信道處於高負載時,網絡緩沖區數據不會增加延遲。但是只要一個信道處於低負載狀態,記錄和延遲標記就會承受最多 buffer_timeout/2 的平均延遲。這個延遲會加到每個連接子任務的網絡連接上,在分析子任務的延遲指標時應該考慮這一點。
只要查看每個子任務暴露的延遲追蹤指標,例如在第 95 百分位,你就應該能識別出是哪些子任務在顯著影響源到匯延遲,然后對其做針對性優化。
注意:Flink 的延遲標記假設集群中所有計算機上的時鍾都是同步的。我們建議設置自動時鍾同步服務(如 NTP)以避免延遲結果出錯。
警告:啟用延遲指標會顯著影響集群的性能(設置為 subtask 粒度時尤其明顯),因為多出來了大量的指標以及使用維護成本非常高的直方圖。強烈建議僅將它們用於調試目的。
總結
本文討論了如何監控 Flink 的網絡棧,主要涉及識別背壓:發生的位置,源頭位置,以及(可能)發生的原因。這可以通過兩種方式執行:使用背壓監視器處理簡單狀況並調試會話;使用 Flink 的任務和網絡棧指標實現持續監控、更深入的分析和更低的運行時開銷。背壓可以由網絡層本身引起,但在大多數情況下是由高負載下的某些子任務引起的。通過對這些指標的分析研究可以區分這兩種場景。我們還提供了一些監控資源使用情況和追蹤可能來自源到匯的網絡延遲的手段。
雙11福利來了!先來康康#怎么買雲服務器最便宜# [並不簡單]參團購買指定配置雲服務器僅86元/年,開團拉新享三重禮:1111紅包+瓜分百萬現金+31%返現,爆款必買清單,還有iPhone 11 Pro、衛衣、T恤等你來抽,馬上來試試手氣!https://www.aliyun.com/1111/2019/home?utm_content=g_1000083110
本文作者:Nico Krube
本文為雲棲社區原創內容,未經允許不得轉載。