本文系美圖網際網路技術沙龍第 11 期嘉賓分享內容,公眾號後臺回覆「naix」獲取 PPT。
大資料技術和應用系統目前已經在各個行業中發揮著巨大的作用,各種各樣的開源技術也給大資料從業人員帶來了很大的便利。Bitmap 作為一種大資料需求下產生的計算體系,有著計算速度快、資訊密度高、支援海量資料等眾多優勢。
美圖擁有海量使用者資料,每天都有大量資料計算任務。而 Bitmap 技術能大幅度減少計算的開銷,節省資料儲存的成本,儘管有不少公司做過 Bitmap 的相關嘗試,但是到目前為止還沒有一個相對成熟的分散式 Bitmap 開源應用,因此美圖研發了自己的分散式 Bitmap 系統,應用於美圖各個場景下的相關資料計算任務。
Bitmap簡介
Bitmap 作為被各種框架廣泛引用的一門技術,它的原理其實很簡單。
bit 即位元,而 Bitmap 則是通過 bit 位來標識某個元素對應的值(支援 0、1 兩種狀態),簡單而言,Bitmap 本身就是一個 bit 陣列。
舉個簡單的例子,假設有 10 個使用者(ID 分別為 1~10),某天 1、3、5、7、8、9 登入系統,如何簡單的表示使用者的登入狀態呢?只需要找到使用者對應的位,並置 1 即可。
更多地,如果需要檢視某使用者當天是否登入系統,僅需檢視該使用者 ID 位對應的值是否為1。並且,通過統計 Bitmap 中 1 的個數便可知道登入系統的使用者總數。Bitmap 已支援的運算操作(如 AND、OR、ANDNOT 等)可以使維度交叉等計算更加便捷。
Bitmap兩個重要的特性
高效能
Bitmap 在其主戰場的計算效能相當驚人。在美圖,早期的統計主要基於 Hive。以美圖系某 APP 資料為基準進行了簡單的留存計算(即統計新增使用者在次日依然活躍的使用者數量),通過 Hive(採用 left out join)耗時 139 秒左右,而 Bitmap(交集計算)僅需 226 毫秒,Hive 的耗時是 Bitmap 的 617 倍左右。如下圖所示,其中,Hive 基於 4 節點的 Hadoop 叢集,而 Bitmap 僅使用單節點單程式。
儲存空間小
由於 Bitmap 是通過 bit 位來標識狀態,資料高度壓縮故佔用儲存空間極小。假設有 10 億活躍裝置 ID(數值型別),若使用常規的 Int 陣列儲存大概需 3.72G,而 Bitmap 僅需 110M 左右。當然,若要進行去重、排序等操作,儲存空間的節省帶來的效能紅利(如記憶體消耗等)也非常可觀。
美圖 Bitmap 應用
美圖公司擁有眾多 APP,如美圖秀秀、美顏相機、美拍、美妝相機、潮自拍等。大家熟知的美圖秀秀和美顏相機都擁有千萬級別的日活,歷史累積的使用者量更達幾十億。
大部分主要日常統計功能均基於 Bitmap,如新增、活躍、留存、升級、回訪等。 同時,我們還支援時間粒度(比如天、周、月甚至年)及 APP、渠道、版本、地區等多種維度,以及各維度的交叉計算等。
Bitmap 原理簡單,但是僅通過 Bitmap 服務來支撐海量資料和需求比想象中更復雜。從 2015 年至今,從單機版到分散式,從單 APP 到各種各樣 APP 的接入,從少量指標的「少量」資料到目前的海量資料和需求,我們在 Bitmap 的實踐過程中也遇到了不少的挑戰,其中較典型的有:
百 T 級 Bitmap 索引。這是單個節點難以維護的量級,通常情況下需要藉助外部儲存或自研一套分散式資料儲存來解決;
序列化和反序列化問題。雖然 Bitmap 儲存空間佔用小、計算快,但使用外部儲存時,對於大型 Bitmap 單個檔案經壓縮後仍可達幾百兆甚至更多,存在非常大的優化空間。另外,儲存及查詢反序列化資料也是非常耗時的;
如何在分散式 Bitmap 儲存上比較好的去做多維度的交叉計算,以及如何在高併發的查詢場景做到快速的響應
美圖分散式 Bitmap—Naix
Naix,即美圖 Bitmap 服務的最終形態,是美圖自主研發的通用分散式 Bitmap 服務。為了使 Naix 適用於各種場景,我們在設計時都儘可能讓相關元件和結構通用化。
Naix 的名字來源於 Dota,在美圖資料技術團隊有各種「Dota 系列」的專案,如 Kunkka、Puck、Arachnia 等。將分散式 Bitmap 稱作 Naix 的原因十分簡單,其諧音 Next 寓意著下一代 Bitmap。
Naix系統設計
整個 Naix 系統如下圖所示主要分為三層:外部呼叫層、系統核心節點層、依賴的外部儲存層。
外部呼叫層
外部呼叫層分為 generator 和 tcp client。generator 是負責生成 Bitmap 的工具,原始資料、常規資料通常儲存在 HDFS 或者其他儲存介質中,需要通過 generator 節點將對應的文字資料或其他資料轉化成 Bitmap 相關的資料,然後再同步到系統中。tcp client 主要負責客戶端應用與分散式系統的互動。
核心節點層
核心節點層主要包含三種:
Master 節點,即 Naix 的核心,主要是對叢集進行相關的管理和維護,如新增 Bitmap、節點管理等操作;
Transport 節點是查詢操作的中間節點,在接收到查詢相關的請求後,由 Transport 對其進行分發;
Data Nodes(Naix中最核心的資料儲存節點),我們採用 Paldb 作為 Bitmap 的基礎資料儲存。
依賴的外部儲存層
Naix 對於外部儲存有輕量級的、依賴,其中 mysql 主要用於對後設資料進行管理,並維護排程中間狀態、資料儲存等,而 redis 更多的是作為計算過程中的快取。
Naix 資料結構
index group
如上圖所示 index group 是 Naix 最基本的資料結構,類似於常規資料庫中的 DataBase,主要用於隔離各種不同的業務。比如在美圖的業務中,美圖秀秀和美拍就是兩個不同的 index group。每個 index group 中可以有多個i ndex,index 類似於常規資料庫中的 table,如新增和活躍的 Bitmap 就屬於兩個不同的 index。
index
在每個 index 中,都有一個固化的時間屬性。由於 Bitmap 資料可能涉及不同的時間週期,通過格式化的時間方式將不同時間週期的資料放入同一個 index。對應時間段內的 index 涉及多個維度,如版本、渠道、地區等,每個維度涉及不同的維度值(如 v1.0.0、v1.2.0 等),而我們所指的 Bitmap 檔案則是針對具體的維度值而言的。
資料資訊字典管理
Bitmap 用於標識某個使用者或元素的狀態通常是指 ID,但在現實業務應用中往往並非如此。如果需要對 imei、idfa 進行統計,則需要將裝置標識通過資料字典的對映轉化為 ID 後再生成 Bitmap 並完成相關統計。同時,為了方便資料的維護和使用,我們對維度、維度值也做了字典對映管理。
Naix genertor
對於 Bitmap 原始資料通常指是類似於 Mysql 記錄資料、HDFS 文字檔案等,而 Naix generator 的作用是將原始資料轉化成 Bitmap 相關資料並同步到 Naix 系統,generator 以外掛的方式支援各種不同場景的 Bitmap 生成,再由業務方基於外掛開發各自的業務邏輯。
simple plugin 是最簡單的方式,也是我們最早使用的外掛。在美圖,大部分的資料都是 HDFS 的原始資料,通過 Hive Client 過濾相關資料到處理伺服器,再通過外掛轉換成 Bitmap 資料。
由於美圖資料量大、業務複雜,在之前的某個階段,每天的資料生成需要消耗近 3 小時。如果中間出現問題再重跑,勢必會影響其他的統計業務而造成嚴重後果。因此我們研發了 mapreduce plugin,期望通過分發自身的優勢,加快資料生成的速度。
實踐證明,使用 mapreduce plugin 最終可將接近 3 小時的 generate 過程壓縮至 8 分鐘左右(基於 4 節點的測試叢集)。基於 mapreduce 的特點,在業務和資料量持續增加的情況下我們也可以通過節點擴容或者 map、reduce 數量調整很容易的保持持續快的 generate 速度。
第三個外掛是 bitmap to bitmap plugin,針對各種時間週期的 Bitmap 資料,使用者可以通過我們提供的 plugin 進行配置,在系統中定期地根據 bitmap 生成 bitmap。類似周、月、年這樣的 Bitmap,該外掛可以通過原生的 Bitmap 生成周期性的 Bitmap(例如通過天生成周、通過周生成月等),使用者僅需提交生成計劃,最終在系統中便會定期自動生成 Bitmap 資料結果。
Naix 儲存
分片
如何將海量資料儲存到分散式系統中?通常情況下,常規的分散式 Bitmap 都是依賴於一些類似 hbase 之類的外度儲存或者按照業務切割去做分散式的儲存,在計算過程中資料的查詢以及資料的拷貝都是極大的瓶頸。經過各種嘗試,最終我們採取分片的方式,即通過固定的寬度對所有的 Bitmap 做分片;同一分片、相同副本序號的資料儲存至相同節點,不同分片的資料可能會被存放在相同或者不同的節點。
分片的設計帶來了不少的好處:
百 T 級別的資料分散式儲存問題迎刃而解;
平行計算:Bitmap 結構十分特殊,基本上的 Bitmap 操作都可以按分片平行計算,再彙總整合。對於巨大的 bitmap 資料,也可按此方式提升速度;
資料 copy 問題:通常情況下,在未分片前大部分 Bitmap 實踐會按照業務將資料分開,但當資料量大時,單個業務的資料在單節點也無法儲存。當涉及到跨業務的計算時,必然需要進行資料 copy。但進行分片後,天然就將這些計算按照分片分發到不同節點獨自進行計算,避免了資料 copy;
序列化和反序列化的問題:通常會出現在大型 Bitmap 中,但分片後所有 Bitmap 大小基本可控,便不再有序列化和發序列化的問題;
跨越 int 屏障:通常 Bitmap 實現僅支援 int 範圍,而隨著美圖業務的發展,其使用者增長很快便會超過 int 範圍。採取資料分片的方式,通過分片內的 id 位移,可以輕易地將分片進行橫向疊加從而支援到 long 的長度。
replication
replication 是常規的分散式系統極其重要的特性,防止由於機器當機、磁碟損壞等原因導致的資料丟失。在 Naix 中,replication 支援 index group level。
如圖所示,用深藍色標識主分片,淺藍色和藍綠色標識剩下的副本分片。通過兩個不同 replication 數量設定的 index group,以及兩個 index 內部對應的兩個 index,在圖中我們可以看到對應同一個 replication 下標的同一個分片,都會被儲存在相同資料節點。而對於同一個分片的不同副本則必然是儲存在不同節點中。
空間和檔案碎片相關的優化
空間和檔案碎片的優化是 Bitmap 實踐中嘗試最多的一部分。Bitmap 基於 Long 陣列或其他數字型陣列實現,其資料往往過於密集或稀疏,有很大的優化空間。大部分Bitmap的壓縮演算法類似對齊壓縮,通過壓縮節省空間並減少計算量,在美圖 Bitmap 中,早期使用 ewah(採用類似 RLE 的方式),後續切換為 RoaringBitmap,這也是目前 Spark、Hive、Kylin、Druid 等框架常用的 Bitmap 壓縮方式中。對 ewah 和 RoaringBitmap 進行效能對比,在我們真實業務場景中測試,空間節省了 67.3%,資料耗時節省了 58%。在 query 方面,實際場景測試略有提升,但不及空間和 generate 耗時效能提升明顯。
早期 Bitmap 採用檔案儲存,在讀取時我們進行了類似 mmap 的優化。但日常業務中存在很多細粒度維度拆分的Bitmap(比如某個渠道的新增使用者較少),而處理分片操作會將這些小型 Bitmap 拆分得更小。小檔案對於作業系統本身的 block 和 inode 利用率極低,嘗試通過優化儲存方案來解決小檔案碎片的問題。主要調研了以下幾個方案:
Redis
Redis 本身支援 bitset 操作,但其實現效果達不到期望。假設進行簡單的 Bitmap 資料 kv 儲存,以 200T 的資料容量為例,每臺機器為 256G,保留一個副本備份,大概需要 160 臺伺服器,隨著業務量的增長,成本也會逐步遞增;
HBase
在美圖大資料,HBase 的使用場景非常多。若採用 HBase 儲存 Bitmap 資料,在讀寫效能上優化空間不大,且對 HBase 的依賴過重,很難達到預期的效果;
RocksDB
RocksDB 目前在業界的使用較為普遍。經測試,RocksDB 在開啟壓縮的場景下,CPU 和磁碟佔用會由於壓縮導致不穩定;而在不開啟壓縮的場景下,RocksDB 在資料量上漲時效能衰減嚴重,同時多DB的使用上效能並沒有提升;
PalDB
PalDB 是 linkedin 開發的只讀 kv 儲存,在官方測試效能是 RocksDB 和 LevelDB 的 8 倍左右,當資料量達某個量級。PalDB 的效能甚至比 java 內建的 HashSet、HashMap 效能更優。PalDB 本身的設計有利有弊,其設計導致使用場景受限,但已基本滿足 Naix 的使用場景。PalDB 原始碼量少,我們也基於具體使用場景進行了簡單調整。經測試,最終儲存空間節省 13%,query 耗時在實際併發場景中,使用 PalDB 會有 60%以上的提升。generator 耗時略長,但由於增加了 mr 的 generator 方式故此影響可忽略;
Naix query
在 Naix 系統中,整體的互動是在我們自研的 rpc 服務基礎上實現的(rpc 服務基於 Netty,採用 TCP 協議)。在 rpc 底層和 Naix 業務端協議都採用了 ProtoBuf。
分散式計算的過程包括節點、分片、資料儲存等,而針對查詢場景,我們該如何找到相關資料?如何進行計算呢?
在 Naix 中,當 client 發起查詢請求到 transport 節點,transport 節點通過查詢條件選擇最優的節點分發請求。在對應的節點中根據請求條件確定分片的選擇,每個節點找到對應分片後進行計算,將計算完成的結果結點進行聚合並返回 client,類似於 fork-join 疊加 fork-join 的計算過程。
Naix 系統以通用的方式支援查詢:支援操作符 ∩ ∪ - ( ) 組合表示式;使用者根據使用場景選擇所需的查詢 Tuple 和操作符組裝查詢表示式即可,當然,我們也封裝了查詢表示式的組裝轉換工具由於 Naix 支援的查詢本身與業務無關,使用者可以通過組裝表示式做各種查詢操作。
舉幾個簡單的例子:
最簡單的裝置或使用者定位,比如查詢某個是否是某天的新增使用者或者活躍使用者;
多維度的組合分析,比如檢視某天美拍 vivo 這個渠道新增使用者在第二天的留存情況;
多維度的區域性組合交叉分析(資料分析常見場景),比如統計某天美拍在百度、vivo 渠道對應的 v6.0 和 v8.0 版本的活躍使用者數,這就涉及兩個渠道和兩個版本交叉共 4 個組合的查詢。這種操作通常用於資料分析。包括前面兩種,這些簡單的查詢操作平均響應僅需幾毫秒;
多維度的全交叉計算,類似於需要知道某天美拍中的渠道和版本所有資訊做交叉,產出這麼大量級的資料結果。類似操作的效能主要看查詢計算的維度數量以及涉及的資料量,通常是在秒到分鐘級的響應。
未來展望
為了將 Naix 系統推廣到更多的公司業務甚至是外部場景,我們也還在不斷的完善和優化,目前正在做以下幾個嘗試:
早期我們更多的是集中精力進行系統研發,保證能夠滿足需求。目前也在不斷的豐富運維工具,期望能夠更方便運維來維護和管理 Naix;
嘗試各種各樣的計算優化,力求將 query 效能再提升一個臺階;
sql query 也在我們的規劃內,因為 sql 是被大家更廣泛接受的方式,希望能使用類似這種通用的 query 表達方式降低各種不同使用方的學習成本。