在 8 月 13 日(ri)的 TDengine 開發者大會上,TDengine 存(cun)(cun)儲引(yin)擎架(jia)構(gou)師程(cheng)洪澤帶(dai)來(lai)題(ti)為《TDengine 的存(cun)(cun)儲引(yin)擎升(sheng)級之路——從(cong) 1.0 到(dao) 3.0》的主題(ti)演講,詳細闡述了(le) TDengine 3.0 存(cun)(cun)儲引(yin)擎的技術優化與升(sheng)級。本文根(gen)據此演講整(zheng)理(li)而成。
點擊【這里】查看完整演講視頻

相比前兩(liang)個版(ban)本,3.0 的存(cun)儲(chu)引擎更(geng)注(zhu)重(zhong)各種場景下的存(cun)儲(chu)和查詢效率,不僅要對管理節點進一(yi)步“減負”,提供高效合理的更(geng)新、刪(shan)除功能,支(zhi)持數據(ju)備份、流式處理等功能,還要考慮(lv)到數據(ju)維度(du)膨(peng)脹下的高效處理、多表場景下的開機啟動(dong)速度(du)、合理高效且準確地(di)使用系統資源等需求。
TDengine 3.0 存儲引擎的更新可以分為三大塊,首先是 TQ,基于 WAL 的消息隊列;其次是 META,基于 TDB 的元數據存儲引擎;第三是 TSDB(Time Series Database),用(yong)來存(cun)儲時序數據的類 LSM 存(cun)儲引擎(TSDB SE)。

消息隊列存儲引擎

在 1.0 和 2.0 時(shi)我們提供了一(yi)個基(ji)于 TSDB 存儲的(de)連續查詢功能,當時(shi)的(de)想法是用它(ta)來代替流(liu)式處理(li)。工作流(liu)程可以概括(kuo)為:App 定時(shi)發查詢任務下(xia)去(qu),查詢引擎執行查詢,TSDB 返(fan)回結(jie)果給(gei)查詢引擎,查詢引擎再把結(jie)果返(fan)回給(gei) App。
這(zhe)一功能的(de)優(you)點(dian)是(shi)可以復用(yong)查詢(xun)(xun)引(yin)(yin)(yin)擎,簡單(dan)易開發(fa),但缺(que)點(dian)也很(hen)明顯,不僅會(hui)出現計算實(shi)時性差、查詢(xun)(xun)壓力大導(dao)致算力浪費的(de)情況(kuang),更重要的(de)是(shi)亂(luan)(luan)序(xu)問題無法處理(li)。數據在進(jin)入 TSDB 之后(hou)都會(hui)按(an)照(zhao)時間戳進(jin)行排序(xu),一個(ge)亂(luan)(luan)序(xu)的(de)數據進(jin)來(lai)后(hou)插(cha)到前面(mian),TSDB 是(shi)無法推出這(zhe)條數據的(de),這(zhe)就會(hui)導(dao)致亂(luan)(luan)序(xu)問題的(de)出現;另(ling)外由于查詢(xun)(xun)引(yin)(yin)(yin)擎是(shi)復用(yong)的(de),TSDB 的(de)查詢(xun)(xun)引(yin)(yin)(yin)擎也不會(hui)對新的(de)亂(luan)(luan)序(xu)數據進(jin)行處理(li)、對結(jie)果進(jin)行更改校驗(yan)。
從這一技術背景出發,TDengine 3.0 中需要設計一個存儲引擎來支持消息隊列和流式計算。這(zhe)個存(cun)儲引擎能(neng)告訴我們什(shen)么樣的(de)數據是增(zeng)(zeng)量數據,這(zhe)樣一來,流(liu)式計算只(zhi)需處理增(zeng)(zeng)量數據就(jiu)好了,其(qi)他的(de)數據就(jiu)不用管了。
此外這個存(cun)儲引(yin)(yin)擎(qing)需要構(gou)建在(zai)一(yi)個 Pipe 之上,保(bao)證數據進入和出去的前(qian)后順序一(yi)致。在(zai)設計時,我們(men)發現 TDengine 的 WAL 其實(shi)就是一(yi)個天然(ran)的 Pipe。于是我們(men)在(zai) WAL 之上加了一(yi)層索引(yin)(yin),并進行大量的適(shi)配(pei)開發,實(shi)現了 TQ 存(cun)儲引(yin)(yin)擎(qing)。
如果大家深入研(yan)究過 TDengine 的(de)模型,就(jiu)會發(fa)現(xian)它(ta)的(de)架構模型和(he) Kafka 的(de)很多設計(ji)都(dou)是(shi)相對應的(de),超級表和(he) Kafka 的(de) Topic 相似(si)、Vnode 跟 Kafka 中的(de) Partition 也很接近,子表的(de)表名跟 Kafka 中的(de) Event Key 對應,因此這個架構設計(ji)天(tian)然(ran)地就(jiu)帶(dai)有消息(xi)隊(dui)列(lie)的(de)特點,從(cong)這點出發(fa),TDengine 3.0 想要實現(xian)一個消息(xi)隊(dui)列(lie)是(shi)非常容易的(de)。

基于 TQ 存儲引擎,在實際操作時,查詢引擎只會處理增量數據,將計算結果修正后返回給 App,而不會再進行全量數據的再查詢。它帶來的優點是實時性非常高,因為能對增量數據進行明確地區分,亂序數據也得以高效處理,同時還節省了更多的計算資源,將計算結果修正。
元數據存儲引擎

在元數(shu)據(ju)存(cun)儲(chu)(chu)這塊,此前的(de)(de) 1.0 和 2.0 采取的(de)(de)都(dou)是(shi)比較簡單(dan)的(de)(de)存(cun)儲(chu)(chu)機(ji)制(zhi),即全內(nei)存(cun)存(cun)儲(chu)(chu),數(shu)據(ju)在內(nei)存(cun)中以(yi) hash 表(biao)(biao)的(de)(de)方(fang)(fang)式存(cun)儲(chu)(chu),并輔(fu)以(yi)跳表(biao)(biao)索引,這個 hash 表(biao)(biao)中有一個 Backup Storage Engine,它可以(yi)保證數(shu)據(ju)的(de)(de)持久化。該(gai)方(fang)(fang)式的(de)(de)優點(dian)是(shi)全內(nei)存(cun)、效率高,但缺點(dian)也很明顯(xian),當啟動時,這部分數(shu)據(ju)就會(hui)全部加載到(dao)內(nei)存(cun)之(zhi)中,不僅內(nei)存(cun)占(zhan)用無法精準控制(zhi),還(huan)會(hui)導致開機(ji)啟動時間長。
為了解決這些問題,在 3.0 中我們研發了 TDB(一個 B+ 樹格式的的存儲引擎),來存儲元數據及元數據索引。TDB 的 B+ 樹存儲適合元數據讀多寫少的場景,能夠支持百億時間線的存儲,避免了(le)元數據全內存(cun)存(cun)儲以及長(chang)時間的(de)加載,同時解決了(le)在有限內存(cun)下,表數量膨脹的(de)問題。對于 TDB 是如何實現(xian)的(de),大家如果感興趣,可以去(qu) GitHub()上看(kan)一下源代碼。
TDB 的優點是內存可以精確控制,開機啟動速度快,在有限內存下也可以存儲海量的元數據,此外如果 TDB 外加 Cache 輔助的話,在一定程度上可以提供接近全內存 hash 表的查詢速度。
時序數據存儲引擎
時序數據的更新和刪除
在 2.0 中,更(geng)新刪除(chu)功能是在引(yin)(yin)擎開發(fa)完(wan)后(hou)補充開發(fa)的(de)一個功能,因此 2.0 的(de)更(geng)新和刪除(chu)功能相(xiang)對簡單,但(dan)功能較弱。2.0 的(de)更(geng)新是基(ji)于一個分布在橫(heng)軸上的(de)時間戳,更(geng)新數(shu)(shu)據的(de)操作就是在后(hou)面追加相(xiang)同時間戳的(de)數(shu)(shu)據,簡單來講(jiang)就是用亂序(xu)數(shu)(shu)據的(de)方法(fa)來處理(li)更(geng)新,然(ran)后(hou)查詢引(yin)(yin)擎把這些(xie)亂序(xu)數(shu)(shu)據進行合并,就得到了更(geng)新后(hou)的(de)結果。刪除(chu)的(de)實現更(geng)加簡單,近似于物(wu)理(li)刪除(chu),要刪除(chu)的(de)數(shu)(shu)據會在內存、硬盤上被直接“干掉”,效(xiao)率相(xiang)對較低。
TDengine 3.0 完全拋棄了 2.0 的更新刪除機制,在設計層面考慮了更新和刪除的實現,引入了版本號,把時序數據變成了二維圖形上(shang)的點(dian),每個寫(xie)入請求(qiu)都帶(dai)有一個版(ban)本號(hao),版(ban)本號(hao)按照(zhao)寫(xie)入請求(qiu)處理順(shun)序遞增。

那 3.0 具體是如何做更新(xin)的?如上(shang)圖(tu)所示,這些藍色(se)的點是你要更新(xin)的數(shu)據(ju)(ju),數(shu)據(ju)(ju)的版本號(hao)肯定比要更新(xin)的舊數(shu)據(ju)(ju)版本號(hao)大(da),所以我們就引入了版本號(hao)機制。當時(shi)間戳相同(tong)時(shi),版本號(hao)大(da)的數(shu)據(ju)(ju)將更新(xin)版本號(hao)小的數(shu)據(ju)(ju),因(yin)為版本號(hao)大(da)的數(shu)據(ju)(ju)是后寫入的數(shu)據(ju)(ju),相對較“新(xin)”。
以前每(mei)張(zhang)表中的(de)數(shu)據(ju),不論在(zai)(zai)內存里還是(shi)在(zai)(zai)硬盤中,都是(shi)按(an)照時(shi)(shi)間戳(chuo)進(jin)行(xing)排(pai)(pai)(pai)序(xu)(xu)(xu)的(de),但(dan)在(zai)(zai)引入了版本號之(zhi)后排(pai)(pai)(pai)序(xu)(xu)(xu)規則(ze)也進(jin)行(xing)了修改(gai)。首先還是(shi)按(an)時(shi)(shi)間戳(chuo)進(jin)行(xing)排(pai)(pai)(pai)序(xu)(xu)(xu),在(zai)(zai)時(shi)(shi)間戳(chuo)相(xiang)同的(de)情(qing)況下要按(an)照版本號進(jin)行(xing)排(pai)(pai)(pai)序(xu)(xu)(xu),在(zai)(zai)這樣的(de)排(pai)(pai)(pai)序(xu)(xu)(xu)流程下,我們就可以把(ba)數(shu)據(ju)更新用(yong)一個近乎于追加的(de)方式處理(li),查詢引擎負責將(jiang)最后的(de)數(shu)據(ju)合并整(zheng)理(li)后得到最終結果。
在 3.0 中,時序數據的(de)刪除機(ji)制也完全(quan)重(zhong)做。相比 2.0,3.0 支(zhi)持的(de)過濾條件也明顯增加,比如 where tag、where timestamp 等等。那具(ju)體底層是如何(he)實現(xian)的(de)呢?首先還(huan)是基于(yu)版本號機(ji)制。

對于刪除操作來說,我們需要記錄開始和結束的時間區間,以及刪除請求的版本號,如上圖所示,一個刪除請求對應二維圖上的一個紫色矩形,這個矩形內部的所有點都被刪除了。在 3.0 中,時序數據刪除時會追加一條(st, et, version)的記錄元組,在查詢時,查詢引擎會將寫入的數據和刪除記錄元組進行最終的合并,并得到刪除后的最終結果。
采用這種機制,刪除功(gong)能對(dui)于寫操(cao)作(zuo)變得相對(dui)簡單(dan)了,但是對(dui)于查(cha)詢(xun)而言,則(ze)變得更加復(fu)雜了。查(cha)詢(xun)在合并數據時,要判(pan)斷記錄(lu)是不是被(bei)刪除了,即檢查(cha)記錄(lu)是不是在所有的(de)刪除區間(矩形)里(li)面(mian),這是相當耗時的(de)。
TDengine 3.0 采用 Skyline 算法來(lai)提高(gao)有刪除(chu)數據(ju)下的(de)查(cha)(cha)詢速度(du)。這一(yi)算(suan)法的(de)本質是(shi)構造一(yi)個點數據(ju),用來(lai)代替所有的(de)刪除(chu)區(qu)間,如上(shang)圖中的(de)兩個矩形(xing)刪除(chu)區(qu)域可以用三個點來(lai)表(biao)示。原來(lai)對(dui)于每(mei)條記錄(lu)都要檢(jian)查(cha)(cha)是(shi)否被刪除(chu)的(de)算(suan)法,現在變成了一(yi)個單向(xiang)掃描過濾(lv)的(de)操作,從(cong)而大(da)(da)大(da)(da)提高(gao)查(cha)(cha)詢速度(du)。
多表場景下的存儲優化
在時序(xu)數(shu)(shu)據(ju)場(chang)景下(xia),海量數(shu)(shu)據(ju)代表(biao)的(de)也有可能是(shi)海量的(de)表(biao)。在有些業務場(chang)景下(xia)表(biao)數(shu)(shu)量非常之多,但(dan)采集的(de)數(shu)(shu)據(ju)卻很少,比如有一千萬張表(biao),但(dan)每天每張表(biao)采集數(shu)(shu)據(ju)只有兩條,這種場(chang)景對于(yu) 2.0 的(de)存儲(chu)結構并不是(shi)很友好。
在 TDengine 2.0 中(zhong),一張(zhang)(zhang)表(biao)會落在硬盤上,一個(ge)數(shu)(shu)(shu)據塊(kuai)里面只有一張(zhang)(zhang)表(biao)的(de)數(shu)(shu)(shu)據;如果有一千萬張(zhang)(zhang)表(biao),每張(zhang)(zhang)表(biao)兩條數(shu)(shu)(shu)據,那一個(ge)數(shu)(shu)(shu)據塊(kuai)就只有兩條記錄(lu),壓(ya)縮都(dou)沒(mei)法壓(ya)縮,而且這(zhe)種情況下壓(ya)縮的(de)話還會導致數(shu)(shu)(shu)據的(de)膨脹。
TDengine 的超(chao)級(ji)表(biao)下(xia)面所有(you)子表(biao)都(dou)共享同(tong)一(yi)(yi)(yi)個(ge)(ge) schema,這樣的話在 last 文(wen)件里我們(men)(men)就可以將同(tong)一(yi)(yi)(yi)個(ge)(ge)超(chao)級(ji)表(biao)下(xia)不同(tong)子表(biao)的數(shu)據合(he)并(bing)成一(yi)(yi)(yi)個(ge)(ge)數(shu)據塊(kuai)——原來一(yi)(yi)(yi)個(ge)(ge)表(biao)里只有(you)兩(liang)條(tiao)記(ji)(ji)(ji)錄(lu)(lu),但(dan)如果(guo)能把一(yi)(yi)(yi)百張表(biao)的兩(liang)條(tiao)記(ji)(ji)(ji)錄(lu)(lu)合(he)并(bing)成一(yi)(yi)(yi)個(ge)(ge)數(shu)據塊(kuai),那(nei)就有(you)兩(liang)百條(tiao)記(ji)(ji)(ji)錄(lu)(lu)。但(dan)是(shi)這可能需要(yao)讀(du)(du)一(yi)(yi)(yi)百次(ci)表(biao),如果(guo)我們(men)(men)就想讀(du)(du)一(yi)(yi)(yi)次(ci)那(nei)要(yao)怎(zen)么(me)操作呢?
為了解決這個問題,3.0 在數據塊中又加了一個屬性,那就是表的 UID。在 3.0 的(de)(de) last 文(wen)件中(zhong),數據塊(kuai)中(zhong)的(de)(de)數據會按照 UID、Timestamp、Version 排序,即(ji)先比較(jiao) UID、UID 相同的(de)(de)情況(kuang)(kuang)下(xia)(xia)比較(jiao)時(shi)間戳,時(shi)間戳相同的(de)(de)情況(kuang)(kuang)下(xia)(xia)比較(jiao)版(ban)本(ben)號,這樣(yang)的(de)(de)話(hua)就可以更有效地處(chu)理(li)多表低頻的(de)(de)場景了(le)。

結語
TDengine 是一個開源產品,3.0 的(de)代(dai)碼也已(yi)經開放在了 上,非常希(xi)望大家能夠(gou)積極地參與進來,去下載和(he)體驗。也歡迎大家加入 TDengine 的(de)生態交流群,和(he)我們以及 TDengine 的(de)關(guan)注者(zhe)和(he)支(zhi)持者(zhe)一起交流和(he)探(tan)討。


























