“ToB” 產品必備特性:Pravega的動態彈性伸縮

在當下衆多互聯網應用場景下,實時數據產生的速率根據時間的變化會有着翻天覆地的變化。我們既可能面對諸如外賣訂單、住房成交量、雙十一訂單這些場景,其數據量有週期性且在局部的時間內會有可預知的突發的數據峯值;也可能面對微博熱搜、路況事故信息這一類無法預知的突發的數據量激增。

以上特性通俗來講,就是流量數據到達量有峯有谷,且可能不可預知。正因如此,Pravega作爲一款流存儲的產品,必須能夠應對瞬時的數據洪峯,做到“削峯填谷”,讓系統自動地伴隨數據到達速率的變化而伸縮,既能夠在數據峯值時進行擴容提升瞬時處理能力,又能夠在數據谷值時進行縮容節省運行成本,而讀寫客戶端無需額外進行調整。

這一特性對於面向企業開發產品尤其重要,Devops開銷在企業中都會被歸入產品TCO(Total Cost of Ownership) , 所以產品自身的動態自適應能力將會是必備條件,這也是迴應我們在系列文章第二篇中提到的三大挑戰最後一條。

下面我們就詳細講述Pravega動態彈性伸縮特性的實現和應用實例。

動態伸縮性

對於分佈式消息系統來說,一個設計良好的,可擴展的分區機制必不可少。分區機制使得讀寫的並行化成爲可能,而一個良好的分區擴展機制使得企業在面臨業務增長時可以變得更得心應手。和許多基於靜態分區,或者需要手動擴展分區(如Kafka)的系統不同的是,Pravega可以根據數據負載動態地伸縮Stream,以此來實時地應對流量負載的變化。

當前解決方案的一些問題

在當前的大數據技術環境下,我們通過將數據拆分成多個分區並獨立處理來獲得並行性。 例如,Hadoop通過HDFS和map-reduce實現了批處理並行化。 對於流式工作負載,我們今天要使用多消息隊列或Kafka分區來實現並行化。 這兩個選項都有同樣的問題:分區機制會同時影響讀客戶端和寫客戶端。 面對持續數據處理的讀/寫,我們的擴展要求往往會有不同,而一個同時影響讀寫的分區機制會增加系統的複雜性。 此外,雖然你可以通過添加隊列或分區來進行擴展,但這需要分別對讀、寫客戶端和存儲進行手動調整,然後需要手動協調調整後的參數。 這樣的操作很複雜,而且不是動態的,並需要人工介入。

而使用Pravega的話,我們可以輕鬆地、彈性並且獨立地擴展數據的攝入、存儲和處理,即協調數據管道中每個組件的擴展。

Pravega Stream的動態伸縮實現

Pravega對動態伸縮的支持源自於把Stream被劃分成Segment的想法。 在之前的文章中有介紹過,一個Stream可以具有一個或多個Segment。我們可以把一個Segment類比成一個分區,寫入Stream的任何數據都會根據指定路由鍵,通過哈希計算路由至某一個Segment。 實際應用場景下,我們建議應用開發者基於一些有應用意義的字段,比如customer-id,timestamp,machine-id等來生成路由鍵,這樣就可以確保將同類的應用數據路由至同一個Segment。

Segment是Stream中最基本的並行單元。

  • 並行寫:一個具有更多個Segment的Stream可以支持更大的寫入並行度,多個寫客戶端可以並行地對多個Segment進行寫入,而這些Segment可能在物理上分佈於集羣中的多臺服務器上。
  • 並行讀:對於讀客戶端來說,Segment的數量意味着最大的讀並行度。一個具有N個讀客戶端的讀者組可以以最大爲N的並行度來消費同一個Stream。這樣,當一個Stream中的Segment數量被動態增加時,我們可以相應地增加同等數量的讀客戶端(同一讀者組)來增加並行度;反之亦然,當Segment數量動態減少時,我們也可以減少相應的讀客戶端來節省資源。

Stream可以被配置爲隨着更多數據寫入而增加Segment的數量,並在數據量下降時縮小Segment數。 我們將這種配置稱爲Stream的服務級目標(Service Level Objective,SLO)。Pravega監控輸入到Stream的數據速率,並根據SLO在Stream中動態增加或移除Segment。 當需要增加Segment時,Pravega會通過拆分Segment來生成更多的Segment;而當需要減少Segment數量時,Pravega通過合併Segment來減少Segment數量。

實際應用中,應用程序還可以對接Pravega提供的元數據,根據Stream的伸縮性來做相應的伸縮。舉例來講,Flink可以根據元數據中的Segment數量來調整Flink作業的並行度,或者可以依賴容器平臺(如Cloud Foundry,Mesos/Marathon,Kubernetes或者Docker Stack)提供的動態擴縮容機制來動態調整容器實例的數量,以此來應對數據流量的變化。

深入剖析

Pravega根據一致性散列算法將路由鍵散列至“鍵空間”,該鍵空間被劃分爲多個分區,分區數量和Segment數量相一致,同時保證每一個Segment保存着一組路由鍵落入同一區間的事件。

根據路由鍵,我們將一個Stream拆分成了若干個Segment,每一個Segment保存着一組路由鍵落入同一區間的事件,並且擁有着相同的SLO。

同時,Segment可以被封閉(seal),一個被封閉的Segment將禁止寫入。這一概念在動態伸縮中將發揮重要作用。

實例說明伸縮過程

假設某製造企業有400個傳感器,分別編號爲0~399,我們將編號做爲routing key,並將其散列分佈到(0, 1)的鍵空間中(Pravega也支持將非數值型的路由鍵散列到鍵空間中)。隨着部分傳感器傳輸頻率的變化,我們來觀察其Segment的變化。

如圖1所示,在0~1區間的鍵空間中,Segment的合併和拆分導致了路由鍵隨着時間的推移而被路由至不同的Segment。

image

圖 1: Segment的合併和拆分對事件路由的影響

上圖所示的Stream從時間t0開始,它被配置成具有動態伸縮功能。 如果寫入流的數據速率不變,則段的數量不會改變。

在時間點t1,Pravega監控器注意到數據速率的增加,並且選擇將Segment 1拆分成Segment 2和Segment 3兩部分,這個過程我們稱之爲Scale-up事件。在t1之前,路由鍵散列到鍵空間上半部的(值爲200~399)的事件將被放置在Segment 1中,而路由鍵散列到鍵空間下半部的(值爲0~199)的事件則被放置在Segment 0中。在t1之後,Segment 1被拆分成Segment 2和Segment 3;Segment 1則被封閉,即不再接受寫入。 此時,具有路由鍵300及以上的事件被寫入Segment 3,而路由鍵在200和299之間的事件將被寫入Segment 2。Segment 0則仍然保持接受與t1之前相同範圍的事件。

在t2時間點,我們看到另一個Scale-up事件。這次事件將Segment 0拆分成Segment 4和Segment 5。Segment 0因此被封閉而不再接受寫入。

具有相鄰路由鍵散列空間的Segment也可以被合併,比如在t3時間點,Segment 2和Segment 5被合併成爲Segment 6,Segment 2和Segment 5都會被封閉,而t3之後,之前寫入Segment 2和Segment 5的事件,也就是路由鍵在100和299之間的事件將被寫入新的Segment 6中。合併事件的發生表明Stream上的負載正在減少。

image

圖 2: 事件的路由

如圖2,在“現在”這個時刻,只有Segment 3,6和4處於活動狀態,並且所有活躍的Segment將會覆蓋整個鍵空間。在上述的規則2和3中,即使輸入負載達到了定義的閾值,Pravega也不會立即觸發scale-up/down的事件,而是需要負載在一段足夠長的時間內超越策略閾值,這也避免了過於頻繁的伸縮策略影響讀寫性能。

配置伸縮規則

我們在創建Stream時,會使用伸縮規則來配置Stream,該規則定義了Stream如何響應其負載變化。 目前Stream支持三種配置規則:

  1. 固定規則,即無動態伸縮。 在此規則下,Segment的數量不隨負載而變化。其配置接口如下:
static ScalingPolicy fixed(int numSegments)

其中numSegment指stream中固定的segment數量

  1. 基於大小的伸縮規則。在此規則下,當寫入Stream的每秒字節數超過某個目標速率時,Segment的數量將增加,部分Segment將被拆分;如果它低於某個程度,則Segment數量將減少,部分Segment將被合併。其配置接口如下:
static ScalingPolicy byEventRate(int targetRate, int scaleFactor, int minNumSegments)

其中targetRate指每個segment所能承受的最大負載(每秒的event數量),scaleFactor是指每一次scale-up事件中的分裂係數,即segment一分爲幾,如上例應設爲2,minNumSegments指stream中所有的segment數量的最小值,用以防止過度scale-down。

  1. 基於事件數的伸縮規則。此規則與規則2.相似,不同點在它是用事件數而不是字節數來作爲伸縮的判定依據。其配置接口如下:
static ScalingPolicy byDataRate(int targetKBps, int scaleFactor, int minNumSegments)

其中targetKBps指每個segment所能承受的最大負載(每秒數據量大小,以KB計數),其他同上。

使用時,在創建Stream時,將對應的ScalingPolicy對象傳遞給Stream的配置對象StreamConfig即可。

StreamManager streamManager = StreamManager.create(controllerURI); 
StreamConfiguration streamConfig = StreamConfiguration.builder()
    .scalingPolicy(ScalingPolicy.byEventRate(100, 2, 1))
    .build();
streamManager.createStream(scope, streamName, streamConfig);

讀寫客戶端的彈性獨立伸縮

Pravega從設計初始就旨在解決流式數據的讀寫客戶端獨立擴展問題,以求達到讀寫擴展具有彈性,互不影響。我們來看一下以下兩種場景:

場景1:寫速率<處理速率

image

圖 3: 寫速率 < 處理速率

在圖3中,處理速度大於寫入速度,所以雖然只有一個寫客戶端,我們仍然可以將Stream拆分成多個Segment,由讀客戶端reader#1來讀路由鍵區間爲ka … kc的事件,而客戶端reader#2讀路由鍵區間爲kd … kf的事件。在同一讀者組(Reader Group)內的讀客戶端會根據自身讀客戶端數量,自動以負載均衡的方式對應到零到多個不同的segment實現並行的讀。而Pravega的彈性伸縮機制也允許讀者組跟蹤segment的縮放並採取適當的措施,例如:在運行時添加或刪除讀客戶端實例,使整個系統能夠以協調的方式動態擴展。Pravega團隊已經和Flink社區合作,通過監聽segment數量改變Flink讀取和處理Pravega數據的並行度,實現了Flink Pravega Source的動態伸縮。

場景2: 寫速率 > 處理速率

image

圖 4: 寫速率 > 處理速率

在圖4中,處理速度小於寫入速度,所以我們可以在寫客戶端進行並行化(由應用完成),但只需分配一個讀客戶端來讀。由於有了stream和segment的抽象,數據存儲的真正的分區會在stream內部實現,只要路由鍵不發生改變,寫客戶端的並行、數據量的增加並不會影響數據的正常分區。

現實情況下,我們往往會處於上述兩種情況之間,並且伴隨着數據源的變化和時間的推進而發生改變。對寫客戶端來說,Segment的拓撲是透明的,它們只需負責路由鍵的分區。對讀客戶端來說,只需簡單指向Stream,而Segment的動態變化會自動反饋給讀客戶端。

至此,讀客戶端和寫客戶端可以分別獨立地進行彈性縮放,而不受彼此影響。

真實數據用例

我們使用由美國紐約市政府授權開源的出租車數據(http://www.nyc.gov/html/tlc/html/about/trip_record_data.shtml),包括上下車時間,地點,行程距離,逐項票價,付款類型、乘客數量等字段。我們把歷史數據集模擬成了流式數據實時地寫入Pravega。所取的數據集涵蓋的是2015年3月的黃色出租車的行程數據,其數據量爲1.9GB,包括近千萬條記錄,每條記錄17個字段。我們選取了其中12個小時的數據,形成如圖4所示數據統計:

黃色和綠色的出租車行程記錄包括捕獲提貨和下車日期/時間,接送和下車地點,行程距離,逐項票價,費率類型,付款類型和司機報告的乘客數量的字段。我們把歷史數據集模擬成了流式數據實時地寫入Pravega。所取的數據集涵蓋的是2015年3月的黃色出租車的行程數據,其數據量爲1.9GB,包括近千萬條記錄,每條記錄17個字段。我們選取了其中12個小時的數據,形成如圖5所示數據統計:

image

圖 5: 出租車數據流量記錄

由上圖我們可以觀察到,數據流量在早上4點左右處於谷點,而在早晨9點左右達到峯值。峯值流量的寫入字節數大約爲谷點流量的10倍。我們將Stream的伸縮規則配置爲上述規則2(基於大小的伸縮規則)。

相對應地,Stream的Segment熱點圖如圖6所示動態變化:

image

圖 6: Segment熱點圖

從上圖可以看出,從晚11點至凌晨2點,Segment逐漸合併;從早晨6點至10點,Segment逐步拆分。從拆分次數來看,大部分Segment總共拆分3次,小部分拆分4次,這也印證了流量峯值10倍於谷底的統計值(3 < lg10 < 4)。

我們使用出租車行程中的出發點座標位置來作爲路由鍵。當高峯來臨時,繁忙地段產生的大量事件會導致Segment被拆分,從而會有更多的讀客戶端來進行處理;當谷峯來臨時,非繁忙地段產生的事件所在的Segment會進行合併,部分的讀客戶端會下線,剩下的讀客戶端會處理更多地理區塊上產生的事件。

總結

Pravega的動態伸縮機制可以讓應用開發和運維人員不必關心因流量變化而導致的分區變化需要,無需手動調度集羣。分區的流量監控和相應變化由Pravega來進行,從而使流量變化能夠實時而且平滑地體現到應用程序的伸縮上。

獨立伸縮機制使得生產者和消費者可以各自獨立地進行伸縮,而不影響彼此。整個數據處理管道因此變得富有彈性,可以應對實時數據的不斷變化,結合實際處理能力而做出最爲適時的反應。

Pravega系列文章計劃

Pravega根據Apache 2.0許可證開源,0.4版本已於近日發佈。我們歡迎對流式存儲感興趣的大咖們加入Pravega社區,與Pravega共同成長。本篇文章爲Pravega系列第四篇,後面的文章標題如下(標題根據需求可能會有更新):

  1. 實時流處理(Streaming)統一批處理(Batch)的最後一塊拼圖:Pravega
  2. 開源Pravega架構解析:如何通過分層解決流存儲的三大挑戰?
  3. Pravega應用實戰:爲什麼雲原生特性對流存儲至關重要
  4. “ToB” 產品必備特性: Pravega的動態彈性伸縮
  5. Pravega的僅一次語義及事務支持
  6. 分佈式一致性解決方案:狀態同步器
  7. 與Apache Flink集成使用

作者簡介

  • 滕昱:就職於 DellEMC 非結構化數據存儲部門 (Unstructured Data Storage) 團隊並擔任軟件開發總監。2007 年加入 DellEMC 以後一直專注於分佈式存儲領域。參加並領導了中國研發團隊參與兩代 DellEMC 對象存儲產品的研發工作並取得商業上成功。從 2017 年開始,兼任 Streaming 存儲和實時計算系統的設計開發與領導工作。
  • 黃一帆,畢業於上海交通大學計算機專業,現就職於DellEMC,10年分佈式計算、搜索以及架構設計經驗,現從事流式系統相關的設計與開發工作。
  • 周煜敏,復旦大學計算機專業研究生,從本科起就參與 DellEMC 分佈式對象存儲的實習工作。現參與 Flink 相關領域研發工作。

參考:

  1. http://pravega.io/docs/latest/pravega-concepts/#autoscaling-the-number-of-stream-segments-can-vary-over-time

  2. http://pravega.io/docs/latest/key-features/#auto-scaling

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