Grafana Loki 架構
Grafana Loki 是一套可以組合成一個功能齊全的日誌堆棧組件,與其他日誌記錄系統不同,Loki 是基於僅索引有關日誌元數據的想法而構建的:標籤(就像 Prometheus 標籤一樣)。日誌數據本身被壓縮然後並存儲在對象存儲(例如 S3 或 GCS)的塊中,甚至存儲在本地文件系統上,輕量級的索引和高度壓縮的塊簡化了操作,並顯着降低了 Loki 的成本,Loki 更適合中小團隊。
Grafana Loki 主要由 3 部分組成:
-
loki: 日誌記錄引擎,負責存儲日誌和處理查詢
-
promtail: 代理,負責收集日誌並將其發送給 loki
-
grafana: UI 界面
多租戶
Loki 支持多租戶,以使租戶之間的數據完全分離。當 Loki 在多租戶模式下運行時,所有數據(包括內存和長期存儲中的數據)都由租戶 ID 分區,該租戶 ID 是從請求中的 X-Scope-OrgID
HTTP 頭中提取的。當 Loki 不在多租戶模式下時,將忽略 Header 頭,並將租戶 ID 設置爲 fake
,這將顯示在索引和存儲的塊中。
運行模式
Loki 運行模式
Loki 針對本地運行(或小規模運行)和水平擴展進行了優化嗎,Loki 帶有單一進程模式,可在一個進程中運行所有必需的微服務。單進程模式非常適合測試 Loki 或以小規模運行。爲了實現水平可伸縮性,可以將 Loki 的微服務拆分爲單獨的組件,從而使它們彼此獨立地擴展。每個組件都產生一個用於內部請求的 gRPC 服務器和一個用於外部 API 請求的 HTTP 服務,所有組件都帶有 HTTP 服務器,但是大多數只暴露就緒接口、運行狀況和指標端點。
Loki 運行哪個組件取決於命令行中的 -target
標誌或 Loki 的配置文件中的 target:<string>
部分。當 target 的值爲 all
時,Loki 將在單進程中運行其所有組件。,這稱爲單進程
或單體模式
。使用 Helm 安裝 Loki 時,單單體模式是默認部署方式。
當 target 未設置爲 all(即被設置爲 querier
、ingester
、query-frontend
或 distributor
),則可以說 Loki 在水平伸縮
或微服務模式
下運行。
Loki 的每個組件,例如 ingester
和 distributors
都使用 Loki 配置中定義的 gRPC 偵聽端口通過 gRPC 相互通信。當以單體模式運行組件時,仍然是這樣的:儘管每個組件都以相同的進程運行,但它們仍將通過本地網絡相互連接進行組件之間的通信。
單體模式非常適合於本地開發、小規模等場景,單體模式可以通過多個進程進行擴展,但有以下限制:
-
當運行帶有多個副本的單體模式時,當前無法使用本地索引和本地存儲,因爲每個副本必須能夠訪問相同的存儲後端,並且本地存儲對於併發訪問並不安全。
-
各個組件無法獨立縮放,因此讀取組件的數量不能超過寫入組件的數量。
組件
Loki 組件
Distributor
distributor
服務負責處理客戶端寫入的日誌,它本質上是日誌數據寫入路徑中的第一站,一旦 distributor
收到日誌數據,會將其拆分爲多個批次,然後並行發送給多個 ingester
。
distributor
通過 gRPC 與 ingester
通信,它們都是無狀態的,可以根據需要擴大或縮小規模。
哈希
Distributors
將一致性哈希和可配置的複製因子結合使用,以確定 Ingester
服務的哪些實例應該接收指定的流。
流是一組與租戶和唯一標籤集關聯的日誌,使用租戶 ID 和標籤集對流進行 hash 處理,然後使用哈希查詢要發送流的 Ingesters
。
存儲在 Consul 中的哈希環被用來實現一致性哈希,所有的 ingester
都會使用自己擁有的一組 Token 註冊到哈希環中,每個 Token 是一個隨機的無符號 32 位數字,與一組 Token 一起,ingester
將其狀態註冊到哈希環中,狀態 JOINING
和 ACTIVE
都可以接收寫請求,而 ACTIVE
和 LEAVING
的 ingesters
可以接收讀請求。在進行哈希查詢時,distributors
只使用處於請求的適當狀態的 ingester 的 Token。
爲了進行哈希查找,distributors
找到最小合適的 Token,其值大於日誌流的哈希值,當複製因子大於 1 時,屬於不同 ingesters
的下一個後續 Token(在環中順時針方向)也將被包括在結果中。
這種哈希配置的效果是,一個 ingester
擁有的每個 Token 都負責一個範圍的哈希值,如果有三個值爲 0、25 和 50 的 Token,那麼 3 的哈希值將被給予擁有 25 這個 Token 的 ingester
,擁有 25 這個 Token 的 ingester
負責1-25
的哈希值範圍。
Quorum(仲裁) 一致性
由於所有的 distributors
共享對同一哈希環的訪問權,所以寫請求可以被髮送到任何 distributor
。
爲了確保查詢結果的一致性,Loki 在讀和寫上使用 Dynamo 式的仲裁一致性方式,這意味着 distributor
將等待至少一半加一個 ingesters
的響應,然後再對發送的客戶端進行響應。
Ingester
ingester
服務負責將日誌數據寫入長期存儲後端(DynamoDB、S3、Cassandra 等)。此外 ingester
會驗證攝取的日誌行是按照時間戳遞增的順序接收的(即每條日誌的時間戳都比前面的日誌晚一些),當 ingester
收到不符合這個順序的日誌時,該日誌行會被拒絕並返回一個錯誤。
-
如果傳入的行與之前收到的行完全匹配(與之前的時間戳和日誌文本都匹配),傳入的行將被視爲完全重複並被忽略。
-
如果傳入的行與前一行的時間戳相同,但內容不同,則接受該日誌行。這意味着同一時間戳有兩個不同的日誌行是可能的。
來自每個唯一標籤集的日誌在內存中被建立成 chunks(塊)
,然後可以根據配置的時間間隔刷新到支持的後端存儲。在下列情況下,塊被壓縮並標記爲只讀:
-
當前塊容量已滿(該值可配置)
-
過了太長時間沒有更新當前塊的內容
-
刷新了
每當一個數據塊被壓縮並標記爲只讀時,一個可寫的數據塊就會取代它。如果一個 ingester
進程崩潰或突然退出,所有尚未刷新的數據都會丟失。Loki 通常配置爲多個副本(通常是 3 個)來降低這種風險。
當向持久存儲刷新時,該塊將根據其租戶、標籤和內容進行哈希處理,這意味着具有相同數據副本的多個 ingesters
實例不會將相同的數據兩次寫入備份存儲中,但如果對其中一個副本的寫入失敗,則會在備份存儲中創建多個不同的塊對象。有關如何對數據進行重複數據刪除,請參閱 Querier。
WAL
上面我們也提到了 ingesters
將數據臨時存儲在內存中,如果發生了崩潰,可能會導致數據丟失,而 WAL 就可以幫助我們來提高這方面的可靠性。
在計算機領域,WAL(Write-ahead logging,預寫式日誌)是數據庫系統提供原子性和持久化的一系列技術。
在使用 WAL 的系統中,所有的修改都先被寫入到日誌中,然後再被應用到系統狀態中。通常包含 redo 和 undo 兩部分信息。爲什麼需要使用 WAL,然後包含 redo 和 undo 信息呢?舉個例子,如果一個系統直接將變更應用到系統狀態中,那麼在機器斷電重啓之後系統需要知道操作是成功了,還是隻有部分成功或者是失敗了(爲了恢復狀態)。如果使用了 WAL,那麼在重啓之後系統可以通過比較日誌和系統狀態來決定是繼續完成操作還是撤銷操作。
redo log
稱爲重做日誌,每當有操作時,在數據變更之前將操作寫入 redo log,這樣當發生斷電之類的情況時系統可以在重啓後繼續操作。undo log
稱爲撤銷日誌,當一些變更執行到一半無法完成時,可以根據撤銷日誌恢復到變更之間的狀態。
Loki 中的 WAL 記錄了傳入的數據,並將其存儲在本地文件系統中,以保證在進程崩潰的情況下持久保存已確認的數據。重新啓動後,Loki 將重放日誌中的所有數據,然後將自身註冊,準備進行後續寫操作。這使得 Loki 能夠保持在內存中緩衝數據的性能和成本優勢,以及持久性優勢(一旦寫被確認,它就不會丟失數據)。
查詢前端
查詢前端是一個可選的服務,提供 querier
的 API 端點,可以用來加速讀取路徑。當查詢前端就位時,應將傳入的查詢請求定向到查詢前端,而不是 querier
, 爲了執行實際的查詢,羣集中仍需要 querier
服務。
查詢前端在內部執行一些查詢調整,並在內部隊列中保存查詢。querier
作爲 workers 從隊列中提取作業,執行它們,並將它們返回到查詢前端進行彙總。querier
需要配置查詢前端地址(通過-querier.frontend-address
CLI 標誌),以便允許它們連接到查詢前端。
查詢前端是無狀態的,然而,由於內部隊列的工作方式,建議運行幾個查詢前臺的副本,以獲得公平調度的好處,在大多數情況下,兩個副本應該足夠了。
隊列
查詢前端的排隊機制用於:
-
確保可能導致
querier
出現內存不足(OOM)錯誤的查詢在失敗時被重試。這允許管理員可以爲查詢提供不足的內存,或者並行運行更多的小型查詢,這有助於降低總成本。 -
通過使用先進先出隊列(FIFO)將多個大型請求分配到所有
querier
上,以防止在單個querier
中傳送多個大型請求。 -
通過在租戶之間公平調度查詢。
分割
查詢前端將較大的查詢分割成多個較小的查詢,在下游 querier
上並行執行這些查詢,並將結果再次拼接起來。這可以防止大型查詢在單個查詢器中造成內存不足的問題,並有助於更快地執行這些查詢。
緩存
查詢前端支持緩存指標查詢結果,並在後續查詢中重複使用。如果緩存的結果不完整,查詢前端會計算所需的子查詢,並在下游 querier
上並行執行這些子查詢。查詢前端可以選擇將查詢與其 step 參數對齊,以提高查詢結果的可緩存性。結果緩存與任何 loki 緩存後端(當前爲 memcached、redis 和內存緩存)兼容。
Querier
Querier
查詢器服務使用 LogQL 查詢語言處理查詢,從 ingesters
和長期存儲中獲取日誌。
查詢器查詢所有 ingesters
的內存數據,然後再到後端存儲運行相同的查詢。由於複製因子,查詢器有可能會收到重複的數據。爲了解決這個問題,查詢器在內部對具有相同納秒時間戳、標籤集和日誌信息的數據進行重複數據刪除。
Chunk 格式
-------------------------------------------------------------------
| | |
| MagicNumber(4b) | version(1b) |
| | |
-------------------------------------------------------------------
| block-1 bytes | checksum (4b) |
-------------------------------------------------------------------
| block-2 bytes | checksum (4b) |
-------------------------------------------------------------------
| block-n bytes | checksum (4b) |
-------------------------------------------------------------------
| #blocks (uvarint) |
-------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint) | offset, len (uvarint) |
-------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint) | offset, len (uvarint) |
-------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint) | offset, len (uvarint) |
-------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint) | offset, len (uvarint) |
-------------------------------------------------------------------
| checksum(from #blocks) |
-------------------------------------------------------------------
| #blocks section byte offset |
-------------------------------------------------------------------
mint
和 maxt
分別描述了最小和最大的 Unix 納秒時間戳。
Block 格式
一個 block 由一系列日誌 entries 組成,每個 entry 都是一個單獨的日誌行。
請注意,一個 block 的字節是用 Gzip 壓縮存儲的。以下是它們未壓縮時的形式。
-------------------------------------------------------------------
| ts (varint) | len (uvarint) | log-1 bytes |
-------------------------------------------------------------------
| ts (varint) | len (uvarint) | log-2 bytes |
-------------------------------------------------------------------
| ts (varint) | len (uvarint) | log-3 bytes |
-------------------------------------------------------------------
| ts (varint) | len (uvarint) | log-n bytes |
-------------------------------------------------------------------
ts
是日誌的 Unix 納秒時間戳,而 len 是日誌條目的字節長度。
Chunk 存儲
Chunk 存儲是 Loki 的長期數據存儲,旨在支持交互式查詢和持續寫入,不需要後臺維護任務。它由以下部分組成:
-
一個 chunks 索引,這個索引可以通過以下方式支持:Amazon DynamoDB、Google Bigtable、Apache Cassandra。
-
一個用於 chunk 數據本身的鍵值(KV)存儲,可以是:Amazon DynamoDB、Google Bigtable、Apache Cassandra、Amazon S3、Google Cloud Storage。
與 Loki 的其他核心組件不同,塊存儲不是一個單獨的服務、任務或進程,而是嵌入到需要訪問 Loki 數據的
ingester
和querier
服務中的一個庫。
塊存儲依賴於一個統一的接口,用於支持塊存儲索引的 NoSQL
存儲(DynamoDB、Bigtable 和 Cassandra)。這個接口假定索引是由以下項構成的鍵的條目集合。
-
一個哈希 key,對所有的讀和寫都是必需的。
-
一個範圍 key,寫入時需要,讀取時可以省略,可以通過前綴或範圍進行查詢。
該接口在支持的數據庫中的工作方式有些不同:
-
DynamoDB
原生支持範圍和哈希鍵,因此,索引條目被直接建模爲 DynamoDB 條目,哈希鍵作爲分佈鍵,範圍作爲 DynamoDB 範圍鍵。 -
對於
Bigtable
和Cassandra
,索引條目被建模爲單個列值。哈希鍵成爲行鍵,範圍鍵成爲列鍵。
一組模式集合被用來將讀取和寫入塊存儲時使用的匹配器和標籤集映射到索引上的操作。隨着 Loki 的發展,Schemas 模式也被添加進來,主要是爲了更好地平衡寫操作和提高查詢性能。
讀取路徑
日誌讀取路徑的流程如下所示:
-
查詢器收到一個對數據的 HTTP 請求。
-
查詢器將查詢傳遞給所有
ingesters
以獲取內存數據。 -
ingesters
收到讀取請求,並返回與查詢相匹配的數據(如果有的話)。 -
如果沒有
ingesters
返回數據,查詢器會從後端存儲加載數據,並對其運行查詢。 -
查詢器對所有收到的數據進行迭代和重複計算,通過 HTTP 連接返回最後一組數據。
寫入路徑
write path
整體的日誌寫入路徑如下所示:
-
distributor
收到一個 HTTP 請求,以存儲流的數據。 -
每個流都使用哈希環進行哈希操作。
-
distributor
將每個流發送到合適的ingester
和他們的副本(基於配置的複製因子)。 -
每個
ingester
將爲日誌流數據創建一個塊或附加到一個現有的塊上。每個租戶和每個標籤集的塊是唯一的。 -
distributor
通過 HTTP 連接響應一個成功代碼。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dnG4Yye0cP5XtxY0VWiEDg