时序数据库技术体系 – InfluxDB TSM存储引擎之数据读取
任何一個(gè)數(shù)據(jù)庫系統(tǒng)內(nèi)核關(guān)注的重點(diǎn)無非:數(shù)據(jù)在內(nèi)存中如何存儲(chǔ)、在文件中如何存儲(chǔ)、索引結(jié)構(gòu)如何存儲(chǔ)、數(shù)據(jù)寫入流程以及數(shù)據(jù)讀取流程。關(guān)于InfluxDB存儲(chǔ)內(nèi)核,筆者在之前的文章中已經(jīng)比較全面的介紹了數(shù)據(jù)的文件存儲(chǔ)格式、倒排索引存儲(chǔ)實(shí)現(xiàn)以及數(shù)據(jù)寫入流程,本篇文章重點(diǎn)介紹InfluxDB中時(shí)序數(shù)據(jù)的讀取流程。
InfluxDB支持類SQL查詢,稱為InfluxQL。InfluxQL支持基本的DDL操作和DML操作語句,詳見InfluxQL_Spec,比如Select語句:
?
select_stmt = "SELECT" fields from_clause [ into_clause ] [ where_clause ] [ group_by_clause ] [ order_by_clause ] [ limit_clause ] [ offset_clause ] [ slimit_clause ] [ soffset_clause ] .?
使用InfluxQL可以非常方便、人性化地對(duì)InfluxDB中的時(shí)序數(shù)據(jù)進(jìn)行多維聚合分析。那InfluxDB內(nèi)部是如何處理Query請(qǐng)求的呢?接下來筆者結(jié)合源碼對(duì)InfluxDB的查詢流程做一個(gè)剖析。另外,如果看官對(duì)源碼這部分感興趣,推薦先閱讀官方文檔對(duì)應(yīng)部分:https://docs.influxdata.com/influxdb/v1.0/query_language/spec/#query-engine-internals
本文篇幅相對(duì)較長(zhǎng)。為了方便閱讀,本文分為上下兩部分,上半部分會(huì)從原理層面介紹InfluxDB的數(shù)據(jù)讀取流程,下半部分會(huì)舉一個(gè)例子模擬整個(gè)數(shù)據(jù)讀取的過程。
上半部分:InfluxDB數(shù)據(jù)讀取流程原理
LSM(TSM)引擎對(duì)于讀流程的處理通常來說都比較復(fù)雜,建議保持足夠的耐心和專注力。理論部分會(huì)分兩個(gè)小模塊進(jìn)行介紹,第一個(gè)模塊會(huì)從宏觀框架層面簡(jiǎn)單梳理整個(gè)讀取流程,第二個(gè)模塊會(huì)從微觀細(xì)節(jié)層面分析TSM存儲(chǔ)引擎(TSDB)內(nèi)部詳細(xì)的執(zhí)行邏輯。
InfluxDB讀取流程框架
筆者對(duì)照源碼對(duì)整個(gè)流程做了一個(gè)簡(jiǎn)單的梳理(下圖讀者可能看不清楚,文末附有該圖的高清版):
整個(gè)讀取流程從宏觀上分為四個(gè)部分:
1.?Query:InfluxQL允許用戶使用類SQL語句執(zhí)行查詢分析聚合,InfluxQL語法詳見:https://docs.influxdata.com/influxdb/v1.0/query_language/spec/
?
2.?QueryParser:InfluxQL進(jìn)入系統(tǒng)之后,系統(tǒng)首先會(huì)對(duì)InfluxQL執(zhí)行切詞并解析為抽象語法樹(AST),抽象樹中標(biāo)示出了數(shù)據(jù)源、查詢條件、查詢列以及聚合函數(shù)等等,分別對(duì)應(yīng)上圖中Source、Condition以及Aggration。InfluxQL沒有使用通用的第三方AST解析庫,自己實(shí)現(xiàn)了一套解析庫,對(duì)細(xì)節(jié)感興趣的可以參考:https://github.com/influxdata/influxql。接著InfluxDB會(huì)將抽象樹轉(zhuǎn)化為一個(gè)Query實(shí)體對(duì)象,供后續(xù)查詢中使用。
?
3.?BuildIterators:InfluxQL語句轉(zhuǎn)換為Query實(shí)體對(duì)象之后,就進(jìn)入讀取流程中最重要最核心的一個(gè)環(huán)節(jié)?–?構(gòu)建Iterator體系。構(gòu)建Iterator體系是一個(gè)非常復(fù)雜的邏輯過程,其中細(xì)節(jié)非常繁復(fù),筆者盡可能化繁為簡(jiǎn),將其中的主線抽出來。為了方便理解,筆者將Iterator體系分為三個(gè)子體系:頂層Iterator子體系、中間層Iterator子體系以及底層Iterator子體系。
(1)頂層Iterator子體系
InfluxDB會(huì)為InfluxQL中所有查詢field構(gòu)造一個(gè)FieldIterator,FieldIterator表示每個(gè)查詢列都會(huì)創(chuàng)建一個(gè)Iterator(稱為ExprIterator),這是因?yàn)镮nfluxDB是列式存儲(chǔ)系統(tǒng),所有的列都是獨(dú)立存儲(chǔ)的,因此基于列分別構(gòu)建Iterator方便執(zhí)行查詢聚合操作。比如sum(click),sum(impressions)和sum(revenue)三個(gè)查詢列就分別對(duì)應(yīng)一個(gè)ExprIterator。
ExprIterator根據(jù)查詢列值是否需要聚合可以分為VarRefIterator和CallIterator,前者表示列值可以直接查詢返回,不需要聚合;后者表示查詢列需要執(zhí)行某些聚合操作。示例中查詢sum(click)就是典型的CallIterator,CallIterator實(shí)際實(shí)現(xiàn)分為兩步,首先通過VarRefIterator把對(duì)應(yīng)的列值查詢到,再通過對(duì)應(yīng)的Reduce函數(shù)執(zhí)行相應(yīng)聚合。比如sum(click)這個(gè)CallIterator就需要雇傭一個(gè)VarRefIterator把滿足條件的click列值拿上來,再執(zhí)行Reduce函數(shù)sum執(zhí)行聚合操作。
(2)中間層Iterator子體系
InfluxDB中一個(gè)查詢列的值可能分布在不同的Shard上,需要根據(jù)TimeRange決定給定時(shí)間段在哪些shard上,并為每個(gè)Shard構(gòu)建一個(gè)Iterator,雇傭這個(gè)邏輯Iterator負(fù)責(zé)查詢這個(gè)shard上對(duì)應(yīng)列的列值。目前單機(jī)版所有shard都在同一個(gè)InfluxDB實(shí)例上,如果實(shí)現(xiàn)分布式管理,需要在這一層做處理。
(3)底層Iterator子體系
底層Iterator子體系負(fù)責(zé)單個(gè)shard(engine)上滿足條件的某一列值的查找或者單機(jī)聚合,是Iterator體系中實(shí)際干活的Iterator。比如滿足where?advertiser?=?“baidu.com”?這個(gè)條件就需要先在倒排索引中根據(jù)advertiser?=?“baidu.com”查到包含該tag的所有series,再為每個(gè)series構(gòu)建一個(gè)TagsetIterator去查找對(duì)應(yīng)的列值,TagsetIterator會(huì)將查找指針置于最小的列值處。
縱觀整個(gè)Iterator體系的構(gòu)建,整體邏輯還是很清晰的。總結(jié)起來就是,查詢按照查詢列構(gòu)建最頂層FieldIterator,每個(gè)FieldIterator會(huì)根據(jù)TimeRange雇傭多個(gè)ShardIterator去處理單個(gè)Shard上面對(duì)應(yīng)列值的查找,對(duì)查找到的值要么直接返回要么執(zhí)行Reduce函數(shù)進(jìn)行聚合操作。每個(gè)Shard內(nèi)部首先會(huì)根據(jù)查詢條件利用倒排索引定位到所有滿足條件的series,再為每個(gè)series構(gòu)建一個(gè)TagsetIterator用來查找具體的列值數(shù)據(jù)。因此,TagsetIterator是整個(gè)體系中唯一干活的Iterator,所有其他上層Iterator都是邏輯Iterator。
另一個(gè)非常重要的點(diǎn)是,同一個(gè)Shard內(nèi)的所有TagsetIterator在構(gòu)建完成會(huì)合并成一個(gè)ShardIterator,這個(gè)合并過程是對(duì)這些TagsetIterator進(jìn)行排序的過程,排序規(guī)則是按照series由小到大排序或者由大到小排序(由用戶SQL對(duì)查詢結(jié)果是由小到大排序還是由大到小排序決定)。同理,一個(gè)列值對(duì)應(yīng)的多個(gè)ShardIterator構(gòu)建完成之后會(huì)合并成一個(gè)FieldIterator,合并過程亦是一個(gè)排序過程,不過排序是針對(duì)所有Shard中的TagsetIterator進(jìn)行的,排序規(guī)則是先比較series,再比較時(shí)間。可見,一個(gè)FieldIterator最終是由一系列排序過的TagsetIterator構(gòu)成的。
?
4.?Emitter.Emit:Iterator體系構(gòu)建完成之后就完成了查詢聚合前的準(zhǔn)備工作,接下來就開始干活了。干活邏輯簡(jiǎn)單來講是遍歷所有FieldIterator,對(duì)每個(gè)FieldIterator執(zhí)行一次Next函數(shù),就會(huì)返回每個(gè)查詢列的結(jié)果值,組裝到一起就是一行數(shù)據(jù)。FieldIterator執(zhí)行Next()函數(shù)會(huì)傳遞到最底層的TagsetIterator,TagsetIterator執(zhí)行Next函數(shù)實(shí)際返回真實(shí)的時(shí)序數(shù)據(jù)。
TSDB存儲(chǔ)引擎執(zhí)行邏輯
TSDB存儲(chǔ)引擎(實(shí)際上就是一個(gè)Shard)根據(jù)用戶的查詢請(qǐng)求執(zhí)行原始數(shù)據(jù)的查詢就是上文中提到的底層Iterator子體系的構(gòu)建。查詢過程分為兩個(gè)部分:倒排索引查詢過濾以及TSM數(shù)據(jù)層查詢,前者通過Query中的where條件結(jié)合倒排索引過濾掉不滿足條件的SeriesKey;后者根據(jù)留下的SeriesKey以及where條件中時(shí)間段信息(TimeRange)在TSMFile中以及內(nèi)存中查出最終滿足條件的數(shù)值列。TSDB存儲(chǔ)引擎會(huì)將查詢到的所有滿足條件的原始數(shù)值列返回給上層,上層根據(jù)聚合函數(shù)對(duì)原始數(shù)據(jù)進(jìn)行聚合并將聚合結(jié)果返回給用戶。整個(gè)過程如下圖所示:
?
上圖需要從底部向上瀏覽,整個(gè)流程可以整理為如下:
1.?根據(jù)where?condition以及所有倒排索引文件查處所有滿足條件的SeriesKey
2.?將滿足條件的SeriesKey根據(jù)GroupBy維度列進(jìn)行分組,不同分組后續(xù)的所有操作都可以獨(dú)立并發(fā)執(zhí)行,因此可以多線程處理
3.?針對(duì)某個(gè)分組的SeriesKey集合以及待查詢列,根據(jù)指定查詢時(shí)間段(TimeRange)在所有TSMFile中根據(jù)B+樹索引構(gòu)建查詢iterator
4.?將滿足條件的原始數(shù)據(jù)返回給上層進(jìn)行聚合運(yùn)算,并將聚合運(yùn)算的結(jié)果返回給用戶
實(shí)際執(zhí)行的過程可能比較抽象,為了更好的理解,筆者在下半部分舉了一個(gè)示例。沒有理解上面的邏輯沒關(guān)系,可以先看下面的示例,看完之后再看上面的理論邏輯相信會(huì)更加容易理解。
下半部分:InfluxDB查詢流程示例
文章上半部分從理論層面對(duì)InfluxDB查詢流程進(jìn)行了介紹。為了方便理解TSDB存儲(chǔ)引擎處理查詢流程的邏輯,筆者通過如下一個(gè)真實(shí)示例將其中的核心步驟進(jìn)行說明。下表為原始時(shí)序數(shù)據(jù)表,表中有3個(gè)維度列:publisher、advertiser以及gender,3個(gè)數(shù)值列:impression、click以及revenue:
| timestamp | publisher | advertiser | gender | impression | click | revenue |
| 2017-11-01T00:00:00 | ultrarimfast.com | baidu.com | male | 1800 | 23 | 11.24 |
| 2017-12-01T00:00:00 | bieberfever.com | google.com | male | 2074 | 72 | 31.22 |
| 2018-01-04T00:00:00 | ultrarimfast.com | baidu.com | false | 1079 | 54 | 9.72 |
| 2018-01-08T00:00:01 | ultrarimfast.com | google.com | male | 1912 | 11 | 3.74 |
| 2018-01-21T00:00:01 | bieberfever.com | baidu.com | male | 897 | 17 | 5.48 |
| 2018-01-26T00:00:01 | ultrarimfast.com | baidu.com | male | 1120 | 73 | 6.48 |
現(xiàn)在用戶想查詢2018年1月份發(fā)布在baidu.com平臺(tái)上的不同廣告商的曝光量、點(diǎn)擊量以及總收入,SQL如下所示:
select sum(click),sum(impression),sum(revenue) from table group by publisher where advertiser = "baidu.com" and timestamp > "2018-01-01" and timestamp < "2018-02-01"步驟一:倒排索引過濾+groupby分組
原始查詢語句:select?….??from?ad_datasource?where?advertiser?=?“baidu.com”?……?。倒排索引即根據(jù)條件advertiser=”baidu.com”在所有Index?File中遍歷查詢包含該tag的所有SeriesKey,具體原理(詳見《時(shí)序數(shù)據(jù)庫技術(shù)體系?–?InfluxDB?多維查詢之倒排索引》)如下:
1.?根據(jù)Index?File中Measurement?Block根據(jù)”ad_datasource”進(jìn)行過濾,可以直接定位到給定source對(duì)應(yīng)的所有TagKey所在的文件offset|size。
2.?加載出對(duì)應(yīng)TagKey區(qū)域的Hash?Index,使用給定TagKey(”advertiser”)進(jìn)行hash可以直接定位到該TagKey對(duì)應(yīng)的TagValue的文件offset|size。
3.?加載出TagKey對(duì)應(yīng)TagValue區(qū)域的Hash?Index,使用過濾條件TagValue(”baidu.com”)進(jìn)行hash可以直接定位到該TagValue對(duì)應(yīng)的所有SeriesID。
4.?SeriesID就是對(duì)應(yīng)SeriesKey在索引文件中的offset,直接根據(jù)SeriesID可以加載出對(duì)應(yīng)的SeriesKey。
滿足條件的所有SeriesKey如下表所示,共有3個(gè):
| publisher | advertiser | gender |
| ultrarimfast.com | baidu.com | male |
| ultrarimfast.com | baidu.com | false |
| bieberfever.com | baidu.com | male |
根據(jù)倒排索引查詢得到所有的SeriesKey之后,這里有一個(gè)非常重要的步驟:根據(jù)groupby條件對(duì)SeriesKey進(jìn)行分組,分組算法為hash。示例查詢中聚合條件為group?by?publisher,因此需要將上面得到的3個(gè)SeriesKey按照publisher的不同分成如下兩組:
| publisher | advertiser | gender |
| bieberfever.com | baidu.com | male |
| publisher | advertiser | gender |
| ultrarimfast.com | baidu.com | male |
| ultrarimfast.com | baidu.com | female |
在倒排索引之后執(zhí)行分組意義非常重大,分組后不同group的SeriesKey是可以并行獨(dú)立執(zhí)行查詢并最終執(zhí)行聚合的,因此后續(xù)的所有操作都可以使用多個(gè)線程并發(fā)執(zhí)行,極大提升整個(gè)查詢性能。
步驟二:TSM文件數(shù)據(jù)檢索
到這一步,我們已經(jīng)按照groupby得到分組后的SeriesKey集合。接下來需要根據(jù)SeriesKey以及TimeRange在TSM數(shù)據(jù)文件中查找滿足條件的待查詢列。在TSM數(shù)據(jù)文件中根據(jù)SeriesKey以及TimeRange查詢field的具體過程(詳見:《時(shí)序數(shù)據(jù)庫技術(shù)體系?–?InfluxDB?TSM存儲(chǔ)引擎之TSMFile》)如下:
上圖中中間部分為索引層,TSM在啟動(dòng)之后就會(huì)將TSM文件的索引部分加載到內(nèi)存,數(shù)據(jù)部分因?yàn)樘蟛⒉粫?huì)直接加載到內(nèi)存。用戶查詢可以分為三步:
1.?首先根據(jù)Key(SeriesKey+fieldKey)找到對(duì)應(yīng)的SeriesIndex?Block,因?yàn)镵ey是有序的,所以可以使用二分查找來具體實(shí)現(xiàn)
2.?找到SeriesIndex?Block之后再根據(jù)查找的時(shí)間范圍,使用[MinTime,?MaxTime]索引定位到可能的Series?Data?Block列表
3.?將滿足條件的Series?Data?Block加載到內(nèi)存中解壓進(jìn)一步使用二分查找算法查找即可找到
在TSM中查詢滿足TimeRange條件的SeriesKey對(duì)應(yīng)的待查詢列值,因?yàn)镮nfluxDB會(huì)根據(jù)不同的查詢列設(shè)置獨(dú)立的FieldIterator,因此查詢列有多少就有多少個(gè)FieldIterator,如下所示:
步驟三:原始數(shù)據(jù)聚合
查詢到滿足條件的所有原始數(shù)據(jù)之后,InfluxDB會(huì)根據(jù)查詢聚合函數(shù)對(duì)原始數(shù)據(jù)進(jìn)行聚合,如下圖所示:
| publisher | sum(impression) | sum(click) | sum(revenue) |
| bieberfever.com | 897 | 17 | 5.48 |
| ultrarimfast.com | 1079?+?1120 | 54?+?73 | 9.72?+?6.48 |
文章總結(jié)
本文主要結(jié)合InfluxDB源碼對(duì)查詢聚合請(qǐng)求在服務(wù)器端的處理框架進(jìn)行了系統(tǒng)理論介紹,同時(shí)深入介紹了InfluxDB?Shard?Engine是如何利用倒排索引、時(shí)序數(shù)據(jù)存儲(chǔ)文件(TSMFile)處理用戶的查詢請(qǐng)求。最后,舉了一個(gè)示例對(duì)Shard?Engine的執(zhí)行流程進(jìn)行了形象化說明。整個(gè)讀取的示意圖附件:
?
總結(jié)
以上是生活随笔為你收集整理的时序数据库技术体系 – InfluxDB TSM存储引擎之数据读取的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Centos7.x 安装 CDH 6.x
- 下一篇: Canal Mysql同步至ES/Hba