更快更強,SLS 推出高性能 SPL 日志查詢模式
引言
隨著數字化進程的持續深化,可觀測性一直是近年來非常火熱的話題,在可觀測的三大支柱 Log/Trace/Metric 中,日志(Log)數據一般是最為常見的,企業邁向可觀測性的第一步,也往往始于日志數據的采集上云。日志完成收集后,最直接的需求就是從海量日志數據中檢索分析出有價值的信息。隨著日志數據量的不斷增長,數據種類不斷增多并日益朝著非結構化、多場景、多模態等方向演進,傳統的日志搜索方式已經越來越難以滿足不同場景下多樣化、個性化的分析需求。
日志數據的查詢分析需求是多樣化的
日志(Log)數據作為可觀測場景中最基礎的數據類型之一,具備以下特點 :
- 不可變:日志數據一旦產生就不會被再次修改,是對事件原始信息的忠實記錄,往往結構不太固定。
- 數據隨機:比如異常事件日志、用戶行為日志,一般天然就是隨機的、難以預測的。
- 來源多樣:日志數據種類繁多,不同來源的數據難以具有統一的 Schema。
- 業務復雜:不同的業務參與方對數據的理解不同,寫日志過程中難以預見到后期具體的分析需求。
這些因素導致日志數據在采集過程中往往并不存在一個理想的數據模型可以用來預處理日志數據,因此更常見的做法是直接采集存儲原始的日志數據,這可以稱為是一種 Schema-on-Read 的模式,或者是所謂的壽司原則(The Sushi Principle:Raw data is better than cooked, since you can cook it in as many different ways as you like)。
這種直接存儲原始數據的做法,意味著在分析的時候往往需要動態實時的對數據進行處理(比如 json 處理、正則提取、數學計算等等);而且由于不同分析人員對數據特征缺乏先驗知識,一般也需要對數據先進行一定的探索式分析。
也就是說,在日志查詢分析的過程,既需要能夠展現非結構化的文檔結構,又需要豐富的算子進行實時處理,同時最好還能夠便捷的支持級聯式、探索式的分析方式。
理想的日志查詢語法應該是什么樣的
日志數據分析通常可以分為兩大類場景:
一類是查詢類場景,或者說是搜索場景、純過濾場景,即按照特定的條件過濾掉不需要的日志,并針對符合條件的日志直接輸出日志原文。
一類是分析類場景,主要包括聚合分析(比如 sum、sort)、關聯分析(比如多個表 join),需要對數據進行更復雜的計算,輸出結果一般是表格模式。
這里我們重點關注純查詢過濾的場景,在 SLS 中既可以用傳統的搜索語法(如 Key:XXX ),也可以在標準 SQL 中使用 where 語句(如 * | select * where Key like '%XXX%'),兩種方式各有優點,卻也都有著各自的局限性。
對于查詢語法來說,天然就是為過濾搜索場景而生的,但是可惜表達能力有限,只能支持關鍵詞匹配,以及多個條件的 And/Not/Or 的邏輯組合,無法支持更為復雜的處理邏輯。
而對于 SQL 語法來說,優點是表達能力強,但 SQL 是表格模型,不便于查看原始日志結果(因為要將字段對齊,輸出結果中對于不存在的列就會填充大量 null),而且對于 select * 這樣的語句,也只能輸出開啟字段索引的字段。
詳細對比如下:
純查詢場景的挑戰 | 搜索查詢語法 | 標準SQL語法 |
需要復雜的處理邏輯 | 弱,主要就是支持關鍵詞匹配 | 強,具備豐富的處理函數和算子,如正則匹配、json提取 |
輸出內容是非結構化的 | 強,輸出的是原文,便于查看 | 弱,輸出的是表格模式,不存在的字段全部要補空值,不利于查看 |
翻頁邏輯 | 簡單,控制臺可以直接點,API傳遞offset+lines即可 | 較復雜,需要在SQL中通過limit x,y的方式,并且要指定排序方式 |
查看結果的時間分布 | 簡單,histogram柱狀圖直觀展示出不同時間的分布 | 較復雜,需要在SQL中按照時間分組求和,再按時間排序,然后再畫線圖查看 |
結果中輸出所有原文字段 | 輸出的是原文,天然包含所有字段 | 較麻煩,select * 只能輸出建了字段索引的列 |
獲取部分字段 | 不支持 | select指定列即可 |
計算出新的列 | 不支持 | select中可以計算新的列 |
多級級聯處理能力 | 無法表達 | 可以通過with語句、SQL嵌套,但寫起來較為復雜 |
既然兩種方式各有所長,那么我們是否可以結合這兩種方式的優點,支持一種新的查詢語法,既能遵從文檔模型(直接輸出日志原文、不按表格模式、不要求所有輸出列有索引),又能支持各種好用的 SQL 算子,同時還能夠支持一種更便捷的級聯處理(而不需要復雜的多層嵌套)呢?
SPL 管道式查詢語言
SPL 基本語法如下:
<data-source> | <spl-expr> ... | <spl-expr> ...
其中 <data-source> 是數據源,對于日志查詢的場景,指的就是索引查詢語句。<spl-expr> 是 SPL 指令,支持正則取值、字段分裂、字段投影、數值計算等多種豐富的操作,具體參考 SPL 指令介紹[2]。
從語法定義上可以看到,SPL 是支持多個 SPL 指令組成管道級聯的。對于日志查詢的場景來說,在索引查詢語句之后,可以根據需要通過管道符不斷追加 SPL 指令,從而獲得類似 Unix 管道處理文本數據的體驗,對日志進行靈活的探索式分析。
SPL 能做什么?
篩選字段獲得更精確的視圖
在查詢日志的時候,往往是帶著某個目的去檢索,這個時候一般是只關心其中的部分字段。這時就可以使用 SPL 中的 project 指令,只保留自己關心的字段。(或者使用 project-away 指令,移除不需要看到的字段)
實時計算出新的字段
使用 Extend 指令,可以基于已有字段加工提取出新的字段,可以使用豐富的函數(這些大部分是和 SQL 語法通用的)進行標量處理。
Status:200 | extend urlParam=split_part(Uri, '/', 3)
同時也可以根據多個字段計算出新的字段,比如計算兩個數字字段的差值。(注意字段默認是被視為 varchar,進行數字類型計算的時候要先通過 cast 轉換類型)
Status:200 | extend timeRange = cast(BeginTime as bigint) - cast(EndTime as bigint)
并且也可以在后續管道中,再對這個計算后的值進行 where 判斷過濾:
Status:200
| where UserAgent like '%Chrome%'
| extend timeRange = cast(BeginTime as bigint) - cast(EndTime as bigint)
| where timeRange > 86400
自由的展開半結構化數據
SPL 提供了 parse-json、parse-csv 這樣的指令,可以將 json、csv 類型的字段,直接完全展開出為獨立的字段,之后就可以直接對這些字段進行操作。省去了書寫字段提取函數的開銷,在交互式查詢場景中這種寫法是更為便捷的。
SPL 之前已經在掃描查詢模式上全地域支持,詳見掃描查詢[3]。掃描查詢可以不依賴索引,直接掃描原始日志數據計算。下圖中這個例子,就是在原始日志數據上,通過 SPL 管道完成了模糊過濾、json 展開、字段提取等多種操作。
當前掃描模式 SPL 難以處理大規模數據
掃描模式具備很好的靈活性,但最大的問題是性能不足,特別是面對大規模數據時難以在有限時間內處理完。現有的掃描查詢限制單次最多掃描 10 萬行,超出限制后需要控制臺手動點擊觸發下一次掃描(或者 SDK 觸發下一次調用)。
由于性能受限,導致現有的 SPL 查詢在使用上存在以下問題:
- 對于過濾結果較為稀疏的查詢,由于單次掃描的原始數據量太少,很難在有限時間內掃描到結果。
- 查詢界面的直方圖展示的是索引過濾后的結果(以及掃描進度),而無法展示出 SPL 條件過濾后的最終結果分布。
- 無法支持針對最終過濾后的結果隨機翻頁,只能按照已經掃描的原文的 offset 進行連續翻頁掃描。
這些約束,導致掃描模式下的 SPL,面對具備較大規模的日志數據,使用體驗較差,也就很難發揮出實際用處。
極致優化,高性能 SPL 模式
計算下推,并行化加速
首先要在架構上解決水平擴展的問題。原有的架構下,因為存儲節點不具備復雜表達式的計算能力,只能將原始數據全量拉取到計算節點處理,大數據量的讀取、傳輸、序列化是很大的瓶頸。
在查詢場景下,實際單次請求每次需要的最終結果行數是比較少的(一般單次請求 100 行以內,超出后通過翻頁請求獲取),關鍵在于當 SPL 語句中包含 where 條件的時候,就存在從大量數據中計算 where 條件過濾的過程。為了能夠處理大規模數據并減少傳輸開銷,我們就需要將 where 條件的計算下推到各個 shard 所在的存儲節點上處理。相應的,也就必須要求存儲節點具備對 SPL 中豐富算子的高效處理能力。
為此我們在存儲節點上,引入 C++ 向量化計算引擎,在存儲節點上讀取了原始的數據后,直接原地就可以進行高效的過濾計算。只有對滿足 where 條件的日志,才需要進行剩余的 SPL 計算并輸出最終結果。
計算下推之后,整個的處理能力就可以隨著 shard 數目水平擴展,同時也大幅減少了存儲節點和計算節點之間的數據傳輸、網絡序列化開銷。
向量化計算,多級火箭加速
計算下推解決了按 shard 水平擴展的問題,接下來我們還要進一步的大幅提升每個 shard 上的處理能力。
掃描模式的 SPL,最大性能瓶頸還是在于直接掃描讀取原始的行數據。這樣讀放大會比較嚴重,IO 效率很低。正如使用 SQL 分析能力時需要開啟字段索引(并開啟統計),這些字段的數據就可以被高效的讀取和計算,那 SPL 同樣也可以基于字段索引來進行高性能的數據 IO,然后再基于 SIMD 向量化技術進行高性能計算,同時在過程中盡可能減少額外計算量。
以圖中的 SPL 為例,在下推到存儲節點后,會經過“多級火箭”進行層層加速:
- 按照查詢時間范圍過濾(當數據量非常大時,建議選擇必要的時間范圍進行分析)。
- 處理第一級管道 Status:200 ,關鍵詞索引條件過濾(這個是最快的,有索引過濾條件盡量寫上過濾條件)。
- 處理 SPL 中的 where 過濾條件,基于字段索引(并開啟統計),高效讀取對應的數據。
- 向量化高性能計算,獲得過濾結果,然后再計算剩余的 SPL 部分,得到最終結果
- 同時在計算過程中,如果發現過濾結果行數已經滿足要求,則盡量提前終止(特別對于高命中率的情形,可以盡量減少不必要的計算)。
經過這些優化之后,高性能 SPL 的執行性能相比掃描模式,得到了質的飛躍。
高性能 SPL 的性能表現
我們以單個 shard 處理 1 億行數據為例,來評估高性能 SPL 的性能表現。在線上真實環境創建一個 Logstore,10 個 shard,查詢時間范圍內有 10 億數據。(服務訪問日志數據)
選取如下幾個典型的場景:
場景 1:通過字符串函數處理后過濾
SPL 語句:* | where split_part(Uri, '#', 2) = 'XXX'
場景 2:短語查詢、模糊查詢
SPL 語句:* | where Content like '%XXX%'
場景 3:json提取子字段,然后再過濾
SPL 語句: * | where json_extract_scalar(Params, 'Schema') = 'XXX'
在上述語句中選擇不同的比較參數,構造出不同的命中率的場景(比如命中率 1%,指的是原始 10 億條數據中,有 1000 萬條滿足 where 條件的結果數據),并請求前 20 條滿足條件數據(對應 GetLogs 接口的 API 參數是 offset=0, lines=20),測試平均耗時。
命中率 | 場景1 耗時 | 場景2 耗時 | 場景3 耗時 |
1% | 52 ms | 73 ms | 89 ms |
0.1% | 65 ms | 94 ms | 126 ms |
0.01% | 160 ms | 206 ms | 586 ms |
0.001% | 1301 ms | 2185 ms | 3074 ms |
0.0001% | 2826 ms | 3963 ms | 6783 ms |
可以看出:
- 當命中率較高時,不同場景下都有很好的性能表現,甚至可以接近關鍵詞索引查詢。
- 當命中率很低時,由于要實時計算大量數據,需要更長一些的執行時間,具體實際性能表現和數據字段的長度、語句中算子復雜度、命中結果在原始數據的分布位置等因素都有關。
- 整體來看,高性能 SPL 對于數十億級別的日志量級,可以在數秒內完成計算。
控制臺交互升級,展示過濾后結果的直方圖
高性能模式 SPL,由于計算性能有了大幅提升,因此控制臺展示 histogram,直接展示的是整個 SPL 語句過濾后的結果分布。(意味著整個范圍內的數據也進行了全量的計算)
舉個例子,原始日志有 1000 萬條,SPL 語句是 Status:200 | where Category like '%xx%',符合 Status:200 條件的日志是 10 萬條,這其中再符合 where Category like '%xx%' 條件的日志是 1000 條,則查詢界面上 histogram 柱狀圖展示的是這最終的 1000 條日志隨時間的分布情況。
相應的,和純索引查詢模式下的交互完全相同,高性能模式 SPL 支持隨機翻頁,也支持點擊柱狀圖直接跳轉到對應區間的查詢結果。
API 調用簡化,統一的 offset 語義
在高性能 SPL 模式下,調用 GetLogs 通過 SPL 語句查詢日志時,offset 直接表示的就是過濾后的結果偏移量,從而大大簡化了 API 調用方式。也就是說,使用上,和純索引查詢完全統一。直接按照過濾后最終結果的 offset 來翻頁即可。
使用說明
如何開啟高性能 SPL?
無須顯式指定運行模式。當 SPL 語句中所有參與 where 條件計算的列,全都已經創建了字段索引(并開啟了統計),則自動按照高性能模式執行;否則以掃描模式執行。
是否計費?
高性能 SPL 模式,查詢本身不產生任何額外費用。
?? 注意:如果沒有完全命中索引列導致走的是掃描模式 SPL(并且當前 Logstore 是按功能計費模式),則按照查詢過程中的掃描原始日志數據量計費。
最佳實踐
盡可能增加索引查詢語句預過濾
如果有關鍵詞索引過濾條件,盡可能使用,放在多級 SPL 管道的第一級。索引查詢的效率總是最高的。
復雜過濾場景,建議使用 SPL 代替 SQL
特別是對于模糊匹配、短語匹配、正則匹配、json 提取以及更復雜的各種純過濾場景,以前只能使用 SQL 語法(* | select * where XXX),現在建議替換為 SPL 語法(* | where XXX)。可以能更好的輸出日志原文(而不是表格模式),更便捷的看到過濾后的結果柱狀圖分布,以及更簡潔的輸入體驗。
更多功能,敬請期待
SPL 也能支持聚合操作
目前 SPL 僅支持純查詢過濾場景下的使用,接下來在日志查詢場景下,SPL 語法會進一步支持排序、聚合等操作(聚合后按照表格模式輸出),從而使得 SPL 的多級管道級聯處理能力更強大、更完善,能夠更好的對日志進行更靈活的查詢分析。
總結
企業的日志數據上云后,從海量日志中搜索出想要的信息,是一項最基本的需求。SLS 推出 SPL 查詢語法,支持類似 Unix 管道的級聯語法,并支持 SQL 的各種豐富的函數。同時,基于計算下推、向量化計算等優化,支持高性能模式 SPL 查詢,可以在數秒內處理億級數據,并且支持 SPL 過濾后最終結果的分布直方圖、隨機翻頁等特性,具備和純索引查詢模式類似的體驗。對于模糊、短語、正則、json 提取以及各種復雜過濾場景,推薦使用 SPL 語句來進行查詢。
高性能模式 SPL 目前正在按區域逐步發布中,有任何使用上的問題或者需求,可以通過工單或者直接在 SLS 的釘釘群咨詢。SLS 會一直持續不斷的優化,提供更強大、更好用的可觀測存儲分析引擎。