Sentry 監(jiān)控 - Snuba 數(shù)據(jù)中臺(tái)架構(gòu)(Query Processing 簡(jiǎn)介)
本文轉(zhuǎn)載自微信公眾號(hào)「黑客下午茶」,作者為少 。轉(zhuǎn)載本文請(qǐng)聯(lián)系黑客下午茶公眾號(hào)。
Snuba 有一個(gè)查詢處理管道,首先將 Snuba 查詢語言( legacy 和 SnQL)解析為 AST,然后在 Clickhouse 上執(zhí)行 SQL 查詢。在這兩個(gè)階段之間,在 AST 上執(zhí)行幾次傳遞以應(yīng)用查詢處理轉(zhuǎn)換。
處理管道有兩個(gè)主要目標(biāo):優(yōu)化查詢并防止對(duì)我們的基礎(chǔ)設(shè)施構(gòu)成危險(xiǎn)的查詢。
在數(shù)據(jù)模型上,查詢處理流水線分為邏輯部分,進(jìn)行產(chǎn)品相關(guān)處理,物理部分專注于優(yōu)化查詢。
邏輯部分包含查詢驗(yàn)證等步驟,以確保它與數(shù)據(jù)模型匹配或應(yīng)用自定義函數(shù)。物理部分包括諸如提升標(biāo)簽(promoting tags)和選擇預(yù)聚合視圖(pre-aggregated view)來為查詢提供服務(wù)等步驟。
查詢處理階段
本節(jié)介紹了上述各階段的代碼和示例,并提供了一些提示。
Legacy 和 SnQL 解析器
Snuba 支持兩種語言,傳統(tǒng)的基于 JSON 的語言和新的名為 SnQL 的語言。除了傳統(tǒng)語言不支持的連接和復(fù)合查詢之外,查詢處理管道不會(huì)更改是否使用一種或另一種語言。
Snuba 支持兩種語言,一種是基于 JSON 的舊語言,另一種是名為 SnQL 的新語言。除了遺留語言不支持的連接和復(fù)合查詢之外,無論使用哪種語言,查詢處理管道都不會(huì)改變。
它們都生成一個(gè)邏輯查詢AST,該查詢由下面數(shù)據(jù)結(jié)構(gòu)表示。
- https://github.com/getsentry/snuba/tree/master/snuba/query
基于 JSON 的語言舊解析器源碼:
- https://github.com/getsentry/snuba/blob/master/snuba/query/parser/__init__.py
SnQL 解析器:
- https://github.com/getsentry/snuba/tree/master/snuba/query/snql
查詢驗(yàn)證(Query Validation)
此階段確??梢赃\(yùn)行查詢(大多數(shù)情況下,我們還沒有捕獲所有可能的無效查詢)。這個(gè)階段的職責(zé)是在無效查詢的情況下返回一個(gè) HTTP400,并向用戶提供適當(dāng)?shù)挠杏孟ⅰ?/p>
這分為兩個(gè)子階段:一般驗(yàn)證(general validation)和實(shí)體特定驗(yàn)證(entity specific validation)。
一般驗(yàn)證由一組檢查組成,這些檢查在解析器生成查詢之后立即應(yīng)用于每個(gè)查詢。這在 QueryEntity 函數(shù)中發(fā)生。這包括防止別名陰影(alias shadowing)和函數(shù)簽名驗(yàn)證(function signature validation)等驗(yàn)證。
- QueryEntity:https://github.com/getsentry/snuba/blob/master/snuba/query/parser/__init__.py#L91
每個(gè)實(shí)體也可以以必需列的形式提供一些驗(yàn)證邏輯。這發(fā)生在 class Entity(Describable, ABC):。這允許查詢處理拒絕在 project_id 上沒有條件或沒有時(shí)間范圍的查詢。
- https://github.com/getsentry/snuba/blob/master/snuba/datasets/entity.py#L46-L47
邏輯查詢處理器(Logical Query Processors)
查詢處理器是無狀態(tài)轉(zhuǎn)換,接收查詢對(duì)象(及其 AST)并就地轉(zhuǎn)換。這是為邏輯處理器實(shí)現(xiàn)的接口。在邏輯階段,每個(gè)實(shí)體提供按順序應(yīng)用的查詢處理器。常見的用例是像 apdex 這樣的自定義函數(shù),或者像時(shí)間序列處理器(time series processor)那樣的計(jì)時(shí)。
- apdex: https://github.com/getsentry/snuba/blob/10b747da57d7d833374984d5eb31151393577911/snuba/query/processors/performance_expressions.py#L12-L20
- time series processor:https://github.com/getsentry/snuba/blob/master/snuba/query/processors/timeseries_processor.py
查詢處理器不應(yīng)該依賴于在之前或之后執(zhí)行的其他處理器,并且應(yīng)該彼此獨(dú)立。
存儲(chǔ)選擇器(Storage Selector)
如 Snuba 數(shù)據(jù)模型中所述,每個(gè)實(shí)體可以定義多個(gè)存儲(chǔ)。多個(gè)存儲(chǔ)代表多個(gè)表,并且出于性能原因可以定義物化視圖(materialized views),因?yàn)槟承┮晥D可以更快地響應(yīng)某些查詢。
在邏輯處理階段(完全基于實(shí)體)結(jié)束時(shí),存儲(chǔ)選擇器可以檢查查詢并為查詢選擇合適的存儲(chǔ)。存儲(chǔ)選擇器在實(shí)體數(shù)據(jù)模型中定義并實(shí)現(xiàn)此接口。一個(gè)例子是 Errors 實(shí)體,它有兩個(gè)存儲(chǔ),一個(gè)用于一致查詢(它們被路由到寫入事件的相同節(jié)點(diǎn)),另一個(gè)只包括我們沒有寫入的副本來服務(wù)大多數(shù)查詢。這減少了我們寫入的節(jié)點(diǎn)上的負(fù)載。
- https://github.com/getsentry/snuba/blob/master/snuba/datasets/storage.py#L155-L165
查詢轉(zhuǎn)換器(Query Translator)
不同的 storage 有不同的 schema(這些反映了 clickhouse 表或視圖的 schema)。它們通常都與實(shí)體模型不同,最顯著的例子是用于標(biāo)簽 tags[abc] 的可下標(biāo)表達(dá)式,它在 clickhouse 中不存在,其中訪問標(biāo)簽看起來像 tags.values[indexOf(tags.key, 'abc')]。
選擇 storage 后,需要將查詢轉(zhuǎn)換為物理查詢。Translator 是一個(gè)基于規(guī)則的系統(tǒng),規(guī)則由實(shí)體(針對(duì)每個(gè) storage)定義并按順序應(yīng)用。
與查詢處理器相反,翻譯規(guī)則在查詢上沒有完整的上下文,只能翻譯單個(gè)表達(dá)式。這使我們能夠輕松地編寫翻譯規(guī)則并跨實(shí)體重用它們。
這些是 transactions 實(shí)體的轉(zhuǎn)換規(guī)則。
- https://github.com/getsentry/snuba/blob/master/snuba/datasets/entities/transactions.py#L33-L81
物理查詢處理器(Physical Query Processors)
與邏輯查詢處理器相比,物理查詢處理器的工作方式非常相似。它們的接口非常相似,語義相同。不同之處在于它們對(duì)物理查詢進(jìn)行操作,因此,它們主要是為優(yōu)化而設(shè)計(jì)的。例如,該處理器在標(biāo)簽上找到相等條件,并將它們替換為標(biāo)簽哈希圖(有布隆過濾器索引)上的等效條件,從而使過濾操作更快。
- https://github.com/getsentry/snuba/blob/master/snuba/query/processors/mapping_optimizer.py
查詢拆分器(Query Splitter)
通過將某些查詢拆分為多個(gè)單獨(dú)的 Clickhouse 查詢并組合每個(gè)查詢的結(jié)果,可以以優(yōu)化的方式執(zhí)行某些查詢。
兩個(gè)例子是時(shí)間拆分和列拆分。兩者都在下面這個(gè)文件中。
- https://github.com/getsentry/snuba/blob/master/snuba/web/split.py
時(shí)間拆分(Time splitting)將一個(gè)查詢(不包含聚合且已正確排序)在一個(gè)可變的時(shí)間范圍內(nèi)拆分為多個(gè)查詢,該時(shí)間范圍的大小逐漸增大,并在得到足夠的結(jié)果后按順序停止執(zhí)行。
列拆分(Column splitting)拆分篩選和列獲取。它對(duì)最少數(shù)量的列執(zhí)行查詢的篩選部分,以便 Clickhouse 加載較少的列,然后通過第二個(gè)查詢,僅為第一個(gè)查詢篩選的行獲取缺少的列。
查詢格式化器(Query Formatter)
該組件只是將查詢格式化為 Clickhouse 查詢字符串。
復(fù)合查詢處理
上面的討論僅適用于簡(jiǎn)單查詢、復(fù)合查詢(連接和包含子查詢的查詢遵循稍微不同的路徑)。
上面討論的簡(jiǎn)單查詢管道不適用于連接查詢或包含子查詢的查詢。為了使這項(xiàng)工作發(fā)揮作用,每個(gè)步驟都必須考慮連接的查詢和子查詢,這會(huì)增加過程的復(fù)雜性。
為了解決這個(gè)問題,我們將每個(gè)連接查詢轉(zhuǎn)換為多個(gè)簡(jiǎn)單子查詢的連接。每個(gè)子查詢都是一個(gè)簡(jiǎn)單的查詢,可以通過上述管道進(jìn)行處理。這也是運(yùn)行 Clickhouse 連接(join)的首選方式,因?yàn)樗试S我們?cè)谶B接之前應(yīng)用過濾器。
此類查詢的查詢處理管道由與上述內(nèi)容相關(guān)的幾個(gè)附加步驟組成。
子查詢生成器(Subquery Generator)
該組件采用一個(gè)簡(jiǎn)單的 SnQL 連接查詢,并為連接中的每個(gè)表創(chuàng)建一個(gè)子查詢。
表達(dá)式下推(Expressions Push Down)
上一步生成的查詢將是一個(gè)有效的連接,但效率極低。這一步基本上是一個(gè)連接優(yōu)化器(join optimizer),它將所有可以成為子查詢一部分的表達(dá)式下推到子查詢中。這是一個(gè)獨(dú)立于子查詢處理的必要步驟,因?yàn)?Clickhouse join 引擎不執(zhí)行任何表達(dá)式下推,所以它由 Snuba 來優(yōu)化查詢。
簡(jiǎn)單查詢處理管道(Simple Query Processing Pipeline)
這與上面討論的從邏輯查詢驗(yàn)證到物理查詢處理器的管道相同。
連接優(yōu)化(Join Optimizations)
在處理結(jié)束時(shí),我們可以對(duì)整個(gè)復(fù)合查詢應(yīng)用一些優(yōu)化,例如將 join 轉(zhuǎn)換為 Semi Join。