Grafana Loki 架構

Grafana Loki 是一套可以組合成一個功能齊全的日誌堆棧組件,與其他日誌記錄系統不同,Loki 是基於僅索引有關日誌元數據的想法而構建的:標籤(就像 Prometheus 標籤一樣)。日誌數據本身被壓縮然後並存儲在對象存儲(例如 S3 或 GCS)的塊中,甚至存儲在本地文件系統上,輕量級的索引和高度壓縮的塊簡化了操作,並顯着降低了 Loki 的成本,Loki 更適合中小團隊。

Grafana Loki 主要由 3 部分組成:

多租戶

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(即被設置爲 querieringesterquery-frontenddistributor),則可以說 Loki 在水平伸縮微服務模式下運行。

Loki 的每個組件,例如 ingesterdistributors 都使用 Loki 配置中定義的 gRPC 偵聽端口通過 gRPC 相互通信。當以單體模式運行組件時,仍然是這樣的:儘管每個組件都以相同的進程運行,但它們仍將通過本地網絡相互連接進行組件之間的通信。

單體模式非常適合於本地開發、小規模等場景,單體模式可以通過多個進程進行擴展,但有以下限制:

組件

Loki 組件

Distributor

distributor 服務負責處理客戶端寫入的日誌,它本質上是日誌數據寫入路徑中的第一站,一旦 distributor 收到日誌數據,會將其拆分爲多個批次,然後並行發送給多個 ingester

distributor 通過 gRPC 與 ingester 通信,它們都是無狀態的,可以根據需要擴大或縮小規模。

哈希

Distributors 將一致性哈希和可配置的複製因子結合使用,以確定 Ingester 服務的哪些實例應該接收指定的流。

流是一組與租戶和唯一標籤集關聯的日誌,使用租戶 ID 和標籤集對流進行 hash 處理,然後使用哈希查詢要發送流的 Ingesters

存儲在 Consul 中的哈希環被用來實現一致性哈希,所有的 ingester 都會使用自己擁有的一組 Token 註冊到哈希環中,每個 Token 是一個隨機的無符號 32 位數字,與一組 Token 一起,ingester 將其狀態註冊到哈希環中,狀態 JOININGACTIVE 都可以接收寫請求,而 ACTIVELEAVINGingesters 可以接收讀請求。在進行哈希查詢時,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 上並行執行這些查詢,並將結果再次拼接起來。這可以防止大型查詢在單個查詢器中造成內存不足的問題,並有助於更快地執行這些查詢。

緩存

查詢前端支持緩存指標查詢結果,並在後續查詢中重複使用。如果緩存的結果不完整,查詢前端會計算所需的子查詢,並在下游 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                  |
  -------------------------------------------------------------------

mintmaxt分別描述了最小和最大的 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 的長期數據存儲,旨在支持交互式查詢和持續寫入,不需要後臺維護任務。它由以下部分組成:

與 Loki 的其他核心組件不同,塊存儲不是一個單獨的服務、任務或進程,而是嵌入到需要訪問 Loki 數據的 ingesterquerier 服務中的一個庫。

塊存儲依賴於一個統一的接口,用於支持塊存儲索引的 NoSQL 存儲(DynamoDB、Bigtable 和 Cassandra)。這個接口假定索引是由以下項構成的鍵的條目集合。

該接口在支持的數據庫中的工作方式有些不同:

一組模式集合被用來將讀取和寫入塊存儲時使用的匹配器和標籤集映射到索引上的操作。隨着 Loki 的發展,Schemas 模式也被添加進來,主要是爲了更好地平衡寫操作和提高查詢性能。

讀取路徑

日誌讀取路徑的流程如下所示:

寫入路徑

write path

整體的日誌寫入路徑如下所示:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dnG4Yye0cP5XtxY0VWiEDg