導讀:首先跟大家解釋一下,大家在看到海報的時候,上面寫的是HermesDB,為了規避一下法律風險,現在我們正式把他改名成了MercsDB,它們是同一個引擎的不同的說法。
全文按照以下五個方面來展開:
- 背景
- 存儲
- 計算
- 測試及應用
- 未來規劃
01
MercsDB背景介紹
騰訊在過去十幾年的發展中,對資料倉庫有着非常大的需求,他們的要求可以說是非常複雜。
首先是資料的需求:
- 單表可以存在上千列,百億行,非常的大。
- 需要對任意列中的任意行進行索引,而且可能沒有主鍵
- 實時更新,實時寫入
- 根據不同場景,不同需求,進行列存或行存
- 索引需求旺盛,需要既适合點查,有适用範圍查詢,還會有空間中的位址查詢
除了資料的複雜性和變化之外,還對性能有很高的的需求:
- 百億行的查詢秒級響應
- 既要支援互動式查詢,也支援報表分析
- 寫入要求很高,每天要寫入千億行
是以MercsDB業務場景非常複雜,基本涵蓋我們業界可以說是主流的查詢場景,和我們現在主流引擎,比如presto、impala、es或MercsDB既有相似之處,又有不同之處:
- 相似之處:MercsDB本身也是一個MPP架構的引擎,而且他們覆寫的這些場景,presto、es、ck等引擎都會覆寫到。
- 不同之處:MercsDB總體來講是一個非常綜合的引擎,它要求非常的全面,不是對使用者的某一個方面進行優化。
1. 發展曆史
那清楚了這些需求,我們來看下MercsDB的曆史,2013年就開始在騰訊内部進行孵化,迄今為止已經9年多接近10年了,那他先後經曆了4個版本,在之前我們都稱之為Hermes1.0、2.0、3.0,到我們4.0這個版本改成了MercsDB,那麼為什麼會這麼多版本呢,其實主要是騰訊内部對實時分析有這逐漸提升的需求:
- 2013.01-2016.12,Hermes1.0:第一個版本主要是使用者畫像這種常見的bi場景,需要做實時的OLAP分析,當時我們主要使用反向索引,支援了一些基礎的OLAP場景。
- 2016.12-2019.06,Hermes2.0:這一年需要對日志進行分析,日志的特點是資料量非常大,騰訊的業務上還需要實時更新,是以就進一步擴充了能力。
- 2019.06-2021.12,Hermes3.0:這個時候廣告業務也想用MercsDB,是以進一步引入了Spark支援了一個完整的OLAP能力,這時不隻能完成簡單的聚合和點查功能,還能進行複雜的JOIN、視窗函數的查詢。
- 20221.12至今,Hermes4.0(MercsDB):這個時候因為像ck這些MPP引擎發展非常迅速,大家對性能的要求就越來越高了,是以在4.0我們立志要把性能優化上去,比如說把Spark替換成Presto,做一些向量化的改造,這就誕生了今天的MercsDB。
可以說MercsDB經過了這10年的研發改造,成為了之前的各個特征的集大成者,性能也達到了一個非常理想的狀态。
2. 目前使用情況
這個是MercsDB在騰訊内部的使用情況:
- 整個叢集有5000+節點
- 每天查詢在千萬級别
- 存儲總量在百PB左右,每天新增在1PB的資料
- 高峰期單機查詢每秒處理1億行的寫入
MercsDB目前在騰訊主流APP下基本都有使用,比如微信、qq、騰訊視訊、騰訊遊戲以及廣告财付通等。
--
02
存儲架構
介紹完背景,我們來介紹一下MercsDB的核心,我們先從存儲開始介紹。
這是一個基礎的架構,上面是計算引擎,下面是存儲,可以注意到MercsDB的存儲以下兩種。
(1)Local:本地磁盤,和jvm放在同一台機器上
①低延遲,适合對于延遲敏感,資料量不是特别大的業務。
②服務有損,如果某台機器挂了,會缺失部分資料。
(2)遠端存儲:hdfs,ceph、Ozone
①資料可靠,有多個副本
②有容錯,還支援異構
選擇方法可以根據具體的業務模式來選擇具體的存儲模式,比如
- 是否需要HA高可用
- 是否需要極低的延遲
- 是否是熱資料,要每天都能查到,還是說冷熱分離的,隻查少量資料,大部分都查不到
1. 列存和索引概況
MercsDB還在列存和索引上做了很多優化,主要還是跟業務的需求有關:
- 查詢非常簡單,但是QPS非常高
- 查詢資料量非常大,比如百億行的資料,但是需要秒級内響應
- 資料比較海量,對成本非常敏感,比如說日志服務,需要MercsDB能在成本上進行一些優化
不同的使用者有不同的業務需求,是以對MercsDB的資料和索引就需要進行多方面的優化。
(1)資料存儲
- 檢索類型的主要支援低延遲查詢
- 排序類型的主要針對海量資料
- 壓縮類型的主要是節省成本
- 嵌套類型的主要支援一些像parquet或類似于parquet這樣的存儲
(2)資料索引
- 稀疏索引
- 跳表索引
- 反向索引
- 點查索引
- LBS索引
下面我們先來針對這些索引進行詳細的介紹。
2. 檢索列
檢索型列查是OLAP場景最常見的列存方式,使用者資料量可能不是特别大,大概就是百G,并且查詢非常簡單,單表聚合或點查,但是要求延遲非常低,這時可以用檢索列存。
這個類型使用了空間換時間的思想,添加了很多索引。比如右邊這個圖,使用者需要根據某個值對原來的資料進行檢索,先擷取到rowId,然後根據rowId擷取offset,再通過offset拿到label(這裡是字典的label,不會直接存儲string類型字段的值),之後他再根據label拿到資料。
這樣在檢索的中間計算過程中,隻要用rowId或label進行一個代表就可以了,不需要用到string字段的元素值,這樣整個計算或整個檢索就會非常快。
按照目前使用情況來看,所有列都進行索引的情況下,索引使用的空間占比大概在40%左右,這裡就是利用對資料進行索引支付一些額外的開銷,進而帶來一些比較不錯的性能提升。
3. 排序列
在日志分析的場景中,要分析海量的資料,是以資料量會非常的大,但是使用者的查詢模式有點查也有聚合,列也沒有主鍵,資料也沒有特殊的特征,可能任意一列都會被查到,是以就沒有一個特别好的加速方法,這個場景就推薦使用者使用排序列。
好處是會盡量把相似的列放在一起,不管是從檢索角度講,還是從存儲壓縮角度講,性能都比較好。
比如說圖檔右邊這種情況,有一個對元素值的索引ValueIndex,它是一個排序索引,另外對資料的還增加了一個offset的索引OffsetIndex,這樣就相當于有兩個索引,對資料的索引和對資料值位置的索引,這樣在檢索的時候性能非常的高。
在日志服務的場景中,使用排序列存的方式,性能能達到不做排序的10倍。
4. 壓縮列
這種列适合那種高基數的列,比如說使用者的很多标簽值,或者metric值,這類資料有兩個特點:
- 沒有固定的特征,也不像性别、地區這種基數比較少的,它可能是成千上萬,甚至上千萬個,基數特别高。
- 直接壓縮性能很低,這種編碼沒有什麼特征,比如對string進行編碼,編碼的結果可能沒有有序性或聚合性這種特征。
這種的就可以采用壓縮列,這用BitShuffle+lz4這種壓縮,lz4是一個很簡單而且應用範圍很廣的壓縮方法。
BitShuffle是什麼呢,比如我們要存儲4、5、8、9(可能是string,也可能是double),轉換成二進制辨別之後,聯系非常少,仔細觀察可以發現高幾位全是0,如果順序排列存在檔案裡,壓縮性能會非常差,那如果放在一起,根據第一位對齊,可以發現前面大概十幾位都是0,對這些資料進行壓縮,性能就會非常好。
使用BitShuffle+lz4的方式和老版(bitpacking壓縮)相比性能提升非常明顯,至少提高50%,準确地說存儲所需容量降低一倍。
5. 索引
除了列存之外,MercsDB還有很多索引:
- 反向索引:主要用來進行點查或檢索。
- FST索引:主要用于處理字元串,類似Trie樹,Trie樹是處理字元串非常有用的一個工具,FST可以了解為是Trie樹的一個更新版。
- KDB的索引:主要對空間/location查詢會有很大的性能提升。
--
03
計算架構
剛剛介紹了很多存儲方面的東西,接下來介紹一下本次的重頭戲,計算。
這張圖是整個MercsDB的計算流程。
(1)上面是接口層,可以通過jdbc、http、grpc等多種方式接入
(2)中間是路由層:
① SQL route路由層:通過對SQL的簡單的解析和判斷,自動為使用者選擇是使用左邊的還是右邊的引擎。
② 兩套引擎,自研的native引擎和開源的presto引擎。
- native引擎:原生的查詢引擎,完全自研,沒有借助任何正常的mpp的引擎,适合簡單的query,比如單表的點查和聚合,要求查詢場景比較簡單和固定。
- presto引擎:為了支援完整的OLAP場景。
(3)最終将計算通過mpp架構下推到下面的worker上
雖然有兩個引擎,但都是用java編寫的,是以代碼都在同一個jvm裡面,可以了解為native和presto都在同一個java程序裡的,接下來我們就詳細介紹一下這兩個引擎。
1. Native引擎
Native引擎對将列存或者是索引進行計算,先做過濾(圖有些問題),做完過濾之後,通過本地索引進行聚合。
比如計算count,sum,min和max,在Native模式進行聚合時,不論聚合哪一列,都會把最大值,最小值同時傳回回來,如果你隻需要sum,那我們就把sum的值,傳回給你。
總結一下就是,通過索引加上預處理的聚合,最終将這個結果傳回給使用者。
2. 延遲物化
另外一個是在查詢過程中為了加速性能,做的延遲物化。
比如使用者想以作業系統聚合查詢一個點查,真正查詢時,引擎不會用os列進行查詢,會内部對os列的值做一個标簽,它是一個123這種數值類型的标簽,先對标簽進行聚合,再對标簽進行解碼,最後傳回給使用者。
這樣的好處是在進行聚合的時候,記憶體消耗比直接用string消耗小很多,性能也要高很多。
3. Presto引擎
另外就是presto引擎,這裡主要是用到了presto的connector,把MercsDB原生的查詢模式接入了presto。
4. 算子下推
接入肯定也不是簡單的接入,這裡也做了很多優化。
首先是算子下推,把很多SQL的算子,都下推到MercsDB之中。
目前下推了5種算子,filter、agg、limit、order等,如果用傳統的優化模式,需要寫5條規則,比較麻煩。
我們發現在下推時有一個特征,下推的算子一般都是連續的,一般都靠近葉子節點,比如說這一系列算子都可以下推,如果有一個算子,比如說filter下推不了,那上面的算子都不應該下推。
是以在周遊這棵AST的時候,會生成一個Query Generator的棧,發現算子可以下推就把它推到棧中,假如下面的算子依舊可以下推,就繼續推到棧中,一直到Table Scan算子,說明整個Generator棧中算子都能下推到MercsDB中,這時會把棧中的算子轉化成一個MercsDB的計算節點。
還有一種情況,比如下推到filter,發現filter的某一個過濾條件,非常複雜,沒辦法下推,這說明之前的所有算子都不能下推,就會把棧清空。
這樣做的好處是:
- 規則非常簡單,隻需要寫一條規則。
- 整個ATS的周遊,隻需要周遊一遍,而傳統方式則需要寫5條規則,總共需要周遊5遍。
這就是算子下推,這是第一點。
5. 資料轉換
在MercsDB中把資料讀取出來計算好之後,要傳回給presto,presto原生支援把外來的資料轉換為presto block,用的是block builder,這個是一個接口,有很多方法,比如說writeInt、writeBytes等等。正常類型都是通過接口的抽象方法,寫到各種presto底層庫之中的。
這樣就會有兩個問題:
(1)這是個虛函數,虛函數的調用性能非常差
(2)不能很好的利用原來資料的存儲格式
這裡如果深入的解構一下presto和MercsDB的資料,就會發現presto的block隻存了兩類資料:
(1)第一個是value數組
很多人可能會問,為啥是long數組,不是還有很多double、float等等的資料類型,這個是java有讀裸二進制資料的能力,64位以内的類型比如說double、float、int等都可以用long或int數組來存,而MercsDB的api傳回給presto或其他引擎的資料也是這個類型,是以這裡直接使用了MercsDB的資料。
(2)第二個是isNulls數組
它對應着位是不是為空,這個在MercsDB中是沒有的,是以我們通過向量化的方法,寫了一個向量化的isNulls的api,也就是把values這個數組作為一個整體傳給MercsDB,MercsDB通過向量化計算,直接計算出某一位是否為空,具體怎麼計算呢,其實就是把列存或索引的方法用向量化的方法,翻譯了過來,這樣在資料加載上,性能也比較好。
可以在直接建構和builder的對比中看到,優化後的性能也有一定提升,可以說做了這兩步的優化後,presto和MercsDB的融合就做的非常好了。
6. Java Vector API
除此之外,還做了一些顯示向量化的優化,這裡介紹下MercsDB使用的Java Vector API。
presto也好,MercsDB也好,都是java寫的,是以在向量化方法上是沒有像ck這種引擎可以使用C++做向量化優化,MercsDB仍然采用的是java生态,這主要是引擎曆史和騰訊這邊的開發要求來選擇的。
那Java Vector API實際上是Java在孵化的一個feature,是從jdk16開始的,jvm通過一些api能夠向量化的調用一些底層CPU的AVX等等的這些SIMD指令集,這樣可以批量的進行一些計算。
比如這裡有很多Vector,可能是512位的,也可能是256的。由于float是32位的,512位意味着可以存16個float類型的資料,這說明在一個CPU指令中,可以對16個float資料進行計算,這樣就達到了提升接近16倍效率的效果,當然實際上達不到16倍性能那麼好,但是依然有成倍的性能的提升,這個就是Java Vector API。
我可以舉個例子,右邊上方這個就是标量版本的一個寫法,一個很簡單的寫法,右邊下方就是使用Vector API方式的寫法,使用fromArray進行加載,然後計算,最後還原到數組中。
具體的Vector API大家可以參考Java官網,這裡需要注意下MercsDB使用的是騰訊自研的KONA JDK,這個是開源的,因為KONA JDK的Vector API在騰訊的其他業務線上已經有了比較長時間的積累了,是以相對而言我們還是相對更加信任一下我們内部的這個版本。
7. 向量化優化
在MercsDB内部的很多代碼進行的Vector API的改造,比如說這個decode函數。不過這裡就不帶着大家讀代碼了,主要來總結一下過去幾個月,用Vector API對Java原生API的改造,遇到的一些經驗教訓。
第一個是,在做Vector API時,盡量用循環展開的技術,比如這裡可能隻有一個資料,但是我們希望多提取幾個資料,這樣盡量利用JVM或JIT對Java Code的優化,這就可以通過循環展開,對這段代碼進行預熱,進而使用JIT對這段代碼進行性能優化。
第二個是,在整個計算中,我們使用到的Java Vector的類型是一緻的,比如說計算最開始傳進來的是一個long類型的數組,但計算到一半的時候要把類型轉換成int,傳回的時候又要轉成long類型,如果按照原生的Java 代碼進行改寫,意味着既要用到long Vector又要用到int Vector,而且還涉及到這兩者的轉換。
這沒有必要且性能還差,是以建議在代碼裡盡量保持一緻,不要做Vector的轉換,那可能很多同學會問,那這塊代碼怎麼辦呢,畢竟有一些轉換,我們經過研究之後發現都是正數或者是無符号的,是以可以直接忽略long類型的前面的32位,直接對後面32位進行操作,是以我們通過源碼的方式或者通過位運算的方式,直接把整個計算轉換成long類型,遇到int類型的計算時,把前面的高位進行忽略。
第三點值得注意的就是計算本身,計算中不要做一些額外操作,比如裝箱開箱;還有對象建立,包括long Vector數組的建立,盡量都不要有,都放在代碼的外部,進行複用;還有函數調用,盡量把邏輯都放在一起。
這些是在Vector改造時的一些經驗,做完這些改造之後,可以從benchmark中看到,改造前後性能提升還是比較明顯的。
這隻是向量化改造中的一點,實際上改造的非常多,隻在這舉了一個例子。
8. 其他優化
除此之外,還有一些其他的優化。
第一個是把原來MercsDB中很多單次調用的函數,都改成批處理的,減少虛函數調用。
第二個是把string還原給presto,因為presto最後傳回給使用者的是一個真正的string,但在中間計算、傳輸的過程中其實傳回的是一個index,此時就需要把index轉為string,但這個操作很耗時,原因是使用者在存string的時候,順序是随機的,字典在記憶體中進行查詢轉換的時候,也是一個随機記憶體通路,是以這裡把index做了一個排序,不是原始的資料排序,是在計算的時候,在最後轉換成string之前,我們對index做了一個排序,這樣對字典的通路就是一個順序的通路了,通路完之後,再還原成原來的順序。
9. 優化前後性能對比
計算方面的優化就講到這,我們做了這麼多優化,包括把Presto引入進來和把Vector引入進來,接下來看下測試的benchmark。
這裡是用的SSB進行的測試,這個SSB是一個星型benchmark,應該是一個比較知名的,表用的是flat_lineorder的表,使用了6億和60億的資料,大概是200G和2T。
第一個比較的是優化前後的MercsDB對比,藍色的是優化前的,橙色的是優化前的,可以看到不管是6億還是60億資料,都是非常的明顯的,而且是整體都有一個不錯的性能提升。
10. 單機查詢和CK性能對比
第二個是和目前主流的引擎ck做的一個對比,可以看到MercsDB在很多産品下也是要比ck快的,這裡要提一下,ck也加了很多索引,包括主鍵索引,分區列,稀疏索引等等。可以看到總體上來講也是有一些提升的,當然也有幾條SQL還是比ck慢,但主體上還是MercsDB比較快的。
11. 并發查詢和CK性能對比
剛才那個圖是單機的,就是一條SQL一條SQL比的,這裡以20的并發量進行比較,可以看到MercsDB的提升非常明顯。
當然這裡主要是因為MercsDB本身是利用索引進行查詢,意味着它的查詢本身是不需要讀取整個資料的,而ck需要對整個磁盤做掃描,是以時間是線性的或者近線性的增加。
這個是單機和多并發場景下和ck的一個對比。
--
04
測試及應用
1. 應用場景:微信支付日志檢索
最後再講幾個應用。
首先是MercsDB在微信支付的日志檢索裡的使用,這裡對寫入要求比較高,而且查詢模式既有點查,也有全局的查詢,這裡是海量資料,剛好MercsDB對寫入海量資料,對查詢模式的支援都比較好,是以就選用了MercsDB。
- 使用TubeMQ支援了實時寫入。
- 對資料進行了分詞和索引,既支援了全局查,也支援點查。
- 對存儲進行了分離,把索引放到了本地,這樣檢索的時候會更快,原始資料放到HDFS。
2. 應用場景:廣告業務AB測試
另一個是在AB測試中用到了MercsDB,對于廣告這種AB測,都有廣告主id或廣告id這種主鍵,是以我們做了下面的優化:
- 對主鍵做了排序。
- 對其餘列做了壓縮,節省了大量空間。
- 通過cache+presto的方式支援了ab測試中各種複雜的join。
--
05
未來規劃
這是一些應用場景,最後來說一下未來的規劃。
1.開源加上雲,這是未來最重要的一些事情,這也是改名的原因
2.會繼續做一些向量化的優化
3.容錯方面,比如失敗之後的重試,還需要在容錯方面做的更好
4.記憶體管理和可用性方面這些正常的優化,會繼續做下去
--
06
問答環節
Q1:高并發和即時的查詢支援的怎麼樣?
A1:高并發:支援量還是比較大的,QPS在百肯定是沒有問題。
Q2:索引用的lucene還是自己研發的?
A2:最開始用的lucene,但是後面都是我們自己進行的一些改造和研發。
Q3:分詞用的什麼算法?
A3:用的lucene自帶的算法。
DataFunTalk
今天的分享就到這裡,謝謝大家。
分享嘉賓:龍躍 騰訊TEG 技術專家
編輯整理:張建闖 BOSS直聘
出品平台:DataFunTalk
01/分享嘉賓
龍躍|騰訊TEG 技術專家
北京大學計算機本碩,多年OLAP從業經驗,聚焦于以Spark, Presto等為代表的OLAP引擎。曾任位元組跳動Presto負責人,現為騰訊TEG資料中心OLAP方向技術專家。
02/關于我們
DataFun:專注于大資料、人工智能技術應用的分享與交流。發起于2017年,在北京、上海、深圳、杭州等城市舉辦超過100+線下和100+線上沙龍、論壇及峰會,已邀請超過2000位專家和學者參與分享。其公衆号 DataFunTalk 累計生産原創文章800+,百萬+閱讀,15萬+精準粉絲。