在 GreptimeDB v0.9 版本我們加入了對日誌相關的支援:Pipeline 引擎和全文索引。GreptimeDB 致力於成為統一處理指標(Metric)、日誌(Log)、事件(Event)和追蹤(Trace)的時序資料庫。在 v0.9 之前,使用者雖然可以寫入文字(string)型別的資料,但無法進行專門的解析和查詢。有了 Pipeline 引擎和全文索引之後,使用者可以直接使用 GreptimeDB 完成日誌資料的處理,並極大提升資料的壓縮率,並且支援透過模糊查詢語法快速檢索目標日誌資料。
本文會從設計思路出發,簡單介紹 GreptimeDB 中 Pipeline 引擎的實現原理和方案步驟。
明確設計目標和優勢
提到日誌,我們會首先想到一個長字串。以下是一行非常經典的 nginx access log:
192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0"
不難發現,雖然這行日誌整體是一個大的字串,但是其中已經包含了一些結構化的資訊,例如 IP(192.168.97.8)、時間戳([15/Oct/2024:08:41:09 +0000])、請求方法、請求路徑、HTTP 協議版本號、HTTP 狀態碼等等。
儘管日誌本質上是非結構化的資料,但在實際應用中,我們常見的日誌大多由系統日誌中介軟體列印,而日誌中介軟體通常會在日誌的前面附帶上一些特定的資訊,例如日誌的時間戳,日誌等級,以及一些特定的標籤(例如應用名或者方法名)。
如果我們將這個字串視為一個整體,儲存在資料庫的一列中,那麼後續只能透過 like 語法進行模糊查詢。這樣,若使用者需要查詢某特定請求路徑下所有 HTTP 狀態碼為 200 的日誌,過程會十分繁瑣且低效。
如果我們可以在接收到日誌的時候,直接將日誌內容進行解析成不同的列,會使得寫入和查詢的效率大大提高。有的讀者可能已經想到了,這就是 ETL 的流程。目前市面上有一些產品支援將輸入的文字行進行轉換並輸出,但是這些產品大多需要獨立部署,也就是在資料庫寫入流程的前面多部署一個元件。這不僅會帶來資源的開銷,也提升了運維的複雜度。
到這裡,我們稍微明確了我們的目標。我們希望在 GreptimeDB 接收到日誌資料庫的時候,增加一個簡單的處理流程,能使得一個文字行日誌,能被提取和轉換成不同的資料型別和欄位值,並將這些欄位值分別儲存同一張資料庫表的不同列中。
當然最重要的是,這個轉換的規則是可以用配置檔案來描述的,不同的日誌可以用不同的轉換規則來處理。
相比於直接儲存字串文字,先解析再入庫帶來了兩個顯著的好處:
提取並儲存帶有語義的資料,提高查詢效率。比如我們可以將 HTTP 狀態碼單獨儲存成一個列,這樣後續需要查詢所有狀態碼是 200 的日誌行的時候就會非常方便。
提高資料壓縮率。對於文字的壓縮,我們可以使用 gzip 等工具得到一個近似極限的壓縮率;資料庫對於純文字的壓縮和儲存大機率不會比這個壓縮率更優。而透過解析日誌中的欄位並轉換成對應的資料型別(例如將 HTTP 狀態碼轉換成 int 型別),資料庫可以透過 Run-Length Encoding (遊程編碼)、列式壓縮等技術手段,進一步提高壓縮率。透過我們的實測,容量佔用相比通用文字壓縮可以至少降低 50%。
方案設計
我們依然使用上述這條日誌作為示例來展示處理流程。
192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0"
在配置規則的選擇和設計上,我們參考了 Elasticsearch 的 Ingest processor。每個 Processor 足夠獨立,便於擴充套件;同時我們可以使用 Processor 組合來應對複雜的情況。
Processor 用來將欄位進行初步處理,例如切分,從而獲得子字串,例如 HTTP 狀態碼 "200"。我們希望進一步將字串轉換成更高效的型別,例如數值型別。因此我們需要一種能將解析後的子字串欄位轉換成資料庫可以支援的資料型別的處理方式。在這裡我們需要引入一個簡單的內建型別系統和轉換處理器 Transform 用來處理這種情況。
根據我們觀測到的日誌行的結構,我們可以大概寫出以下配置規則:
processors:
- dissect:
fields:
- line
patterns:
- '%{ip} %{?ignored} %{?ignored} [%{ts}] "%{method} %{path} %{protocol}" %{status} %{size} "%{referer}" "%{ua}"' - date:
fields:
- ts
formats:
- "%d/%b/%Y:%H:%M:%S %Z"
transform:
- fields:
- status
- size
type: int32
- fields:
- ip
- method
- path
- protocol
- referer
- ua
type: string
- field: ts
type: time
index: time
首先,我們使用 Processor 對資料進行處理。使用 Dissect Processor 將一行日誌提取出不同的欄位,並使用 Date Processor 將日期時間戳文字解析成 timestamp。然後,我們使用 Transform 將提取解析出來的欄位轉換成資料庫支援的資料型別,例如 int32 和 string。需要注意的是,在 Transform 中設定的欄位名會被作為資料庫的列名。最後,我們可以在 Transform 中指定欄位是否需要設定為索引,即上述配置中的 index: time,會將 ts 欄位設定成 GreptimeDB 中的 time index。
資料處理的流程簡潔明瞭,如下圖所示:
實現細節
有了方案之後我們就可以開工了!
我們的介面支援一次接受多行日誌,即一個日誌行的陣列。實際上對於陣列,我們只需要迴圈對每一行進行處理即可,並沒有什麼特殊的操作。我們在下文中還是以上述日誌行為例,介紹資料的處理流程。
對於一次處理,我們在空間上將整體邏輯分為兩部分:資料空間(上下文)和“程式碼”。
資料空間是一個上下文,資料在其中透過 key-value 結構儲存:每個資料有它的名稱(key)和值(value)。資料的初始狀態即原始的日誌輸入行,為了方便我們直接用 JSON 格式來表示資料空間,示意如下:
{
"line": "192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0""
}
而程式碼就是我們透過配置檔案定義的 Processor 和 Transform。首先我們需要將配置檔案進行解析匯入到程式中,這部分的邏輯主要是對配置檔案的定義解析並載入,不涉及日誌的處理,因此不在本文中展開。我們以 Date Processor 為例,程式碼結構大體如下:
pub struct DateProcessor {
// input fields
fields: Fields,
// the format for parsing date string
formats: Formats,
// optional timezone param for parsing
timezone: Option
}
Processor 最主要的方法如下:
pub trait Processor {
// execute processor
fn exec_field(&self, val: &Value) -> Result<Map, String>;
}
對於每一個 Processor,我們呼叫 exec_field 方法對資料空間中的資料進行處理。Processor 中記錄了配置檔案中指定的 field 名稱(即資料空間中的 key),因此我們可以透過這個 key 在獲取到對應的 value。我們使用 Processor 的程式碼處理完成這個 value 後,將它重新放置回資料空間中。這樣,我們就完成了一個 Processor 的處理流程。
我們以第一個 Dissect Processor 為例,簡單描述一下處理流程。在初始狀態下,資料空間中只含有原始輸入,如下所示:
{
"line": "192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0""
}
以 Pipeline 中的第一個 Dissect Processor 為例,規則定義如下:
processors:
- dissect:
fields:
- line
patterns:
- '%{ip} %{?ignored} %{?ignored} [%{ts}] "%{method} %{path} %{protocol}" %{status} %{size} "%{referer}" "%{ua}"'
Processor 處理的流程也很簡單,我們首先透過 fields 指定的 key 從資料空間中獲取對應的值,然後使用該 Processor 來處理這個值,得到一個或者多個輸出,最後我們將輸出的結果儲存回到資料空間中,這個 Processor 就處理完成了。
Dissect Processor 的作用是根據空格或者簡單的標點符號對文字進行分割,並將分割形成的子字串透過格式中指定的 key 名進行關聯,最後儲存在資料空間中。在這個例子中,首先透過 line 這個 key 從資料空間中獲取到對應的值,然後對文字進行切分,將切分後的結果依次賦值給 ip、ts 等欄位,最後將這些欄位儲存回資料空間中。此時資料空間的狀態示例如下:
{
"line": "192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0"",
"ip": "192.168.97.8",
"ts": "15/Oct/2024:08:41:09 +0000",
"method": "GET",
"path": "/query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38",
"protocol": "HTTP/1.1",
"status": "200",
"size": "664",
"referer": "https://www.github.com",
"ua": "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0"
}
cqgn.ousnled.com,cqgn.syshuangyihe.com,cqgn.eyeql.com
cqgn.xyfhm.com,cqgn.nc-lh.com
依次執行定義在規則配置中的 Processor,我們就完成了對資料的處理流程。
經過 Processor 處理的資料,有時候依然不是我們想要的最終結果。例如上面的例子中,雖然我們透過分本切分得到了 status 這個欄位,但它依然是一個字串。如果我們能將它轉換成一個數字進行儲存,不管是儲存效率還是查詢效率都會得到提升。我們把這一部分的處理定義為 Transform。
Transform 的結構大致如下:
pub struct Transforms {
transforms: Vec
}
pub struct Transform {
// input fields
pub fields: Fields,
// target datatype for database
pub type_: Value,
// database index hint
pub index: Option
}
同樣非常簡單明瞭。對於每個 Transform,同樣我們透過 fields 獲取到資料空間中的值,轉換成 type_ 指定的資料型別,然後放回到資料空間中。
到這一步,我們就完成了對原始輸入的非結構化的日誌行解析,獲得了相對結構化的欄位(列)和對應的值。大致結果如下:
{
"line": "192.168.97.8 - - [15/Oct/2024:08:41:09 +0000] "GET /query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38 HTTP/1.1" 200 664 "https://www.github.com" "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0"",
"status": 200,
"size": 664,
"ip": "192.168.97.8",
"method": "GET",
"path": "/query/myelosyphilis-anatomicopathologic-polarography-b8be0a5b-8a68-48a4-8a4e-e92f9fcb0a38",
"protocol": "HTTP/1.1",
"referer": "https://www.github.com",
"ua": "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:116.0) Gecko/20100101 Firefox/116.0",
"ts": 1728981669000000000
}
cqgn.xpdahan.com,cqgn.yubingame.com,cqgn.lhfeshop.com
cqgn.juwanci.com,cqgn.gztdzk.com
至此,我們已經完成了對資料的提取和處理。剩下的只是將這個資料轉換成插入請求寫入到資料庫中了,這部分就不在本文中展開。
感興趣的小夥伴可以在此檢視該版本的程式碼,瞭解詳細的程式碼執行過程。
結束語
本文簡單介紹了 GreptimeDB v0.9.0 中引入的 Pipeline 引擎的設計思路和實現原理。聯想力豐富的讀者可以發現整個過程其實是一個非常簡單的 interpreter 實現,對此感興趣的讀者可以訪問參考此處教程進行進一步的瞭解。而在實際中我們針對 Pipeline 的執行進行了多次最佳化和重構,目前的實現相比較於原版的可以說已經是“面目全非”了。對此感興趣的讀者可以期待我們的後續文章。
關於 Greptime
Greptime 格睿科技專注於為可觀測、物聯網及車聯網等領域提供實時、高效的資料儲存和分析服務,幫助客戶挖掘資料的深層價值。目前基於雲原生的時序資料庫 GreptimeDB 已經衍生出多款適合不同使用者的解決方案,更多資訊或 demo 展示請聯絡下方小助手(微訊號:greptime)。
歡迎對開源感興趣的朋友們參與貢獻和討論,從帶有 good first issue 標籤的 issue 開始你的開源之旅吧~期待在開源社群裡遇見你!新增小助手微信即可加入“技術交流群”與志同道合的朋友們面對面交流哦~
Star us on GitHub Now: https://github.com/GreptimeTeam/greptimedb
官網:https://greptime.cn/
文件:https://docs.greptime.cn/
Twitter: https://twitter.com/Greptime
Slack: https://greptime.com/slack
LinkedIn: https://www.linkedin.com/company/greptime/