Grafana Loki 架構(gòu)
Grafana Loki 是一套可以組合成一個(gè)功能齊全的日志堆棧組件,與其他日志記錄系統(tǒng)不同,Loki 是基于僅索引有關(guān)日志元數(shù)據(jù)的想法而構(gòu)建的:標(biāo)簽(就像 Prometheus 標(biāo)簽一樣)。日志數(shù)據(jù)本身被壓縮然后并存儲(chǔ)在對(duì)象存儲(chǔ)(例如 S3 或 GCS)的塊中,甚至存儲(chǔ)在本地文件系統(tǒng)上,輕量級(jí)的索引和高度壓縮的塊簡化了操作,并顯著降低了 Loki 的成本,Loki 更適合中小團(tuán)隊(duì)。
Grafana Loki 主要由 3 部分組成:
loki: 日志記錄引擎,負(fù)責(zé)存儲(chǔ)日志和處理查詢 promtail: 代理,負(fù)責(zé)收集日志并將其發(fā)送給 loki grafana: UI 界面
多租戶
Loki 支持多租戶,以使租戶之間的數(shù)據(jù)完全分離。當(dāng) Loki 在多租戶模式下運(yùn)行時(shí),所有數(shù)據(jù)(包括內(nèi)存和長期存儲(chǔ)中的數(shù)據(jù))都由租戶 ID 分區(qū),該租戶 ID 是從請(qǐng)求中的 X-Scope-OrgID HTTP 頭中提取的。當(dāng) Loki 不在多租戶模式下時(shí),將忽略 Header 頭,并將租戶 ID 設(shè)置為 fake,這將顯示在索引和存儲(chǔ)的塊中。
運(yùn)行模式

Loki 針對(duì)本地運(yùn)行(或小規(guī)模運(yùn)行)和水平擴(kuò)展進(jìn)行了優(yōu)化嗎,Loki 帶有單一進(jìn)程模式,可在一個(gè)進(jìn)程中運(yùn)行所有必需的微服務(wù)。單進(jìn)程模式非常適合測(cè)試 Loki 或以小規(guī)模運(yùn)行。為了實(shí)現(xiàn)水平可伸縮性,可以將 Loki 的微服務(wù)拆分為單獨(dú)的組件,從而使它們彼此獨(dú)立地?cái)U(kuò)展。每個(gè)組件都產(chǎn)生一個(gè)用于內(nèi)部請(qǐng)求的 gRPC 服務(wù)器和一個(gè)用于外部 API 請(qǐng)求的 HTTP 服務(wù),所有組件都帶有 HTTP 服務(wù)器,但是大多數(shù)只暴露就緒接口、運(yùn)行狀況和指標(biāo)端點(diǎn)。
Loki 運(yùn)行哪個(gè)組件取決于命令行中的 -target 標(biāo)志或 Loki 的配置文件中的 target:<string> 部分。當(dāng) target 的值為 all 時(shí),Loki 將在單進(jìn)程中運(yùn)行其所有組件。,這稱為單進(jìn)程或單體模式。使用 Helm 安裝 Loki 時(shí),單單體模式是默認(rèn)部署方式。
當(dāng) target 未設(shè)置為 all(即被設(shè)置為 querier、ingester、query-frontend 或 distributor),則可以說 Loki 在水平伸縮或微服務(wù)模式下運(yùn)行。
Loki 的每個(gè)組件,例如 ingester 和 distributors 都使用 Loki 配置中定義的 gRPC 偵聽端口通過 gRPC 相互通信。當(dāng)以單體模式運(yùn)行組件時(shí),仍然是這樣的:盡管每個(gè)組件都以相同的進(jìn)程運(yùn)行,但它們?nèi)詫⑼ㄟ^本地網(wǎng)絡(luò)相互連接進(jìn)行組件之間的通信。
單體模式非常適合于本地開發(fā)、小規(guī)模等場(chǎng)景,單體模式可以通過多個(gè)進(jìn)程進(jìn)行擴(kuò)展,但有以下限制:
當(dāng)運(yùn)行帶有多個(gè)副本的單體模式時(shí),當(dāng)前無法使用本地索引和本地存儲(chǔ),因?yàn)槊總€(gè)副本必須能夠訪問相同的存儲(chǔ)后端,并且本地存儲(chǔ)對(duì)于并發(fā)訪問并不安全。 各個(gè)組件無法獨(dú)立縮放,因此讀取組件的數(shù)量不能超過寫入組件的數(shù)量。
組件

Distributor
distributor 服務(wù)負(fù)責(zé)處理客戶端寫入的日志,它本質(zhì)上是日志數(shù)據(jù)寫入路徑中的第一站,一旦 distributor 收到日志數(shù)據(jù),會(huì)將其拆分為多個(gè)批次,然后并行發(fā)送給多個(gè) ingester。
distributor 通過 gRPC 與 ingester 通信,它們都是無狀態(tài)的,可以根據(jù)需要擴(kuò)大或縮小規(guī)模。
哈希
Distributors 將一致性哈希和可配置的復(fù)制因子結(jié)合使用,以確定 Ingester 服務(wù)的哪些實(shí)例應(yīng)該接收指定的流。
流是一組與租戶和唯一標(biāo)簽集關(guān)聯(lián)的日志,使用租戶 ID 和標(biāo)簽集對(duì)流進(jìn)行 hash 處理,然后使用哈希查詢要發(fā)送流的 Ingesters。
存儲(chǔ)在 Consul 中的哈希環(huán)被用來實(shí)現(xiàn)一致性哈希,所有的 ingester 都會(huì)使用自己擁有的一組 Token 注冊(cè)到哈希環(huán)中,每個(gè) Token 是一個(gè)隨機(jī)的無符號(hào) 32 位數(shù)字,與一組 Token 一起,ingester 將其狀態(tài)注冊(cè)到哈希環(huán)中,狀態(tài) JOINING 和 ACTIVE 都可以接收寫請(qǐng)求,而 ACTIVE 和 LEAVING 的 ingesters 可以接收讀請(qǐng)求。在進(jìn)行哈希查詢時(shí),distributors 只使用處于請(qǐng)求的適當(dāng)狀態(tài)的 ingester 的 Token。
為了進(jìn)行哈希查找,distributors 找到最小合適的 Token,其值大于日志流的哈希值,當(dāng)復(fù)制因子大于 1 時(shí),屬于不同 ingesters 的下一個(gè)后續(xù) Token(在環(huán)中順時(shí)針方向)也將被包括在結(jié)果中。
這種哈希配置的效果是,一個(gè) ingester 擁有的每個(gè) Token 都負(fù)責(zé)一個(gè)范圍的哈希值,如果有三個(gè)值為 0、25 和 50 的 Token,那么 3 的哈希值將被給予擁有 25 這個(gè) Token 的 ingester,擁有 25 這個(gè) Token 的 ingester負(fù)責(zé)1-25的哈希值范圍。
Quorum(仲裁)一致性
由于所有的 distributors 共享對(duì)同一哈希環(huán)的訪問權(quán),所以寫請(qǐng)求可以被發(fā)送到任何 distributor。
為了確保查詢結(jié)果的一致性,Loki 在讀和寫上使用 Dynamo 式的仲裁一致性方式,這意味著 distributor 將等待至少一半加一個(gè) ingesters 的響應(yīng),然后再對(duì)發(fā)送的客戶端進(jìn)行響應(yīng)。
Ingester
ingester 服務(wù)負(fù)責(zé)將日志數(shù)據(jù)寫入長期存儲(chǔ)后端(DynamoDB、S3、Cassandra 等)。此外 ingester 會(huì)驗(yàn)證攝取的日志行是按照時(shí)間戳遞增的順序接收的(即每條日志的時(shí)間戳都比前面的日志晚一些),當(dāng) ingester 收到不符合這個(gè)順序的日志時(shí),該日志行會(huì)被拒絕并返回一個(gè)錯(cuò)誤。
如果傳入的行與之前收到的行完全匹配(與之前的時(shí)間戳和日志文本都匹配),傳入的行將被視為完全重復(fù)并被忽略。 如果傳入的行與前一行的時(shí)間戳相同,但內(nèi)容不同,則接受該日志行。這意味著同一時(shí)間戳有兩個(gè)不同的日志行是可能的。
來自每個(gè)唯一標(biāo)簽集的日志在內(nèi)存中被建立成 chunks(塊),然后可以根據(jù)配置的時(shí)間間隔刷新到支持的后端存儲(chǔ)。在下列情況下,塊被壓縮并標(biāo)記為只讀:
當(dāng)前塊容量已滿(該值可配置) 過了太長時(shí)間沒有更新當(dāng)前塊的內(nèi)容 刷新了
每當(dāng)一個(gè)數(shù)據(jù)塊被壓縮并標(biāo)記為只讀時(shí),一個(gè)可寫的數(shù)據(jù)塊就會(huì)取代它。如果一個(gè) ingester 進(jìn)程崩潰或突然退出,所有尚未刷新的數(shù)據(jù)都會(huì)丟失。Loki 通常配置為多個(gè)副本(通常是 3 個(gè))來降低這種風(fēng)險(xiǎn)。
當(dāng)向持久存儲(chǔ)刷新時(shí),該塊將根據(jù)其租戶、標(biāo)簽和內(nèi)容進(jìn)行哈希處理,這意味著具有相同數(shù)據(jù)副本的多個(gè) ingesters 實(shí)例不會(huì)將相同的數(shù)據(jù)兩次寫入備份存儲(chǔ)中,但如果對(duì)其中一個(gè)副本的寫入失敗,則會(huì)在備份存儲(chǔ)中創(chuàng)建多個(gè)不同的塊對(duì)象。有關(guān)如何對(duì)數(shù)據(jù)進(jìn)行重復(fù)數(shù)據(jù)刪除,請(qǐng)參閱 Querier。
WAL
上面我們也提到了 ingesters 將數(shù)據(jù)臨時(shí)存儲(chǔ)在內(nèi)存中,如果發(fā)生了崩潰,可能會(huì)導(dǎo)致數(shù)據(jù)丟失,而 WAL 就可以幫助我們來提高這方面的可靠性。
在計(jì)算機(jī)領(lǐng)域,WAL(Write-ahead logging,預(yù)寫式日志)是數(shù)據(jù)庫系統(tǒng)提供原子性和持久化的一系列技術(shù)。
在使用 WAL 的系統(tǒng)中,所有的修改都先被寫入到日志中,然后再被應(yīng)用到系統(tǒng)狀態(tài)中。通常包含 redo 和 undo 兩部分信息。為什么需要使用 WAL,然后包含 redo 和 undo 信息呢?舉個(gè)例子,如果一個(gè)系統(tǒng)直接將變更應(yīng)用到系統(tǒng)狀態(tài)中,那么在機(jī)器斷電重啟之后系統(tǒng)需要知道操作是成功了,還是只有部分成功或者是失敗了(為了恢復(fù)狀態(tài))。如果使用了 WAL,那么在重啟之后系統(tǒng)可以通過比較日志和系統(tǒng)狀態(tài)來決定是繼續(xù)完成操作還是撤銷操作。
redo log 稱為重做日志,每當(dāng)有操作時(shí),在數(shù)據(jù)變更之前將操作寫入 redo log,這樣當(dāng)發(fā)生斷電之類的情況時(shí)系統(tǒng)可以在重啟后繼續(xù)操作。undo log 稱為撤銷日志,當(dāng)一些變更執(zhí)行到一半無法完成時(shí),可以根據(jù)撤銷日志恢復(fù)到變更之間的狀態(tài)。
Loki 中的 WAL 記錄了傳入的數(shù)據(jù),并將其存儲(chǔ)在本地文件系統(tǒng)中,以保證在進(jìn)程崩潰的情況下持久保存已確認(rèn)的數(shù)據(jù)。重新啟動(dòng)后,Loki 將重放日志中的所有數(shù)據(jù),然后將自身注冊(cè),準(zhǔn)備進(jìn)行后續(xù)寫操作。這使得 Loki 能夠保持在內(nèi)存中緩沖數(shù)據(jù)的性能和成本優(yōu)勢(shì),以及持久性優(yōu)勢(shì)(一旦寫被確認(rèn),它就不會(huì)丟失數(shù)據(jù))。
查詢前端
查詢前端是一個(gè)可選的服務(wù),提供 querier 的 API 端點(diǎn),可以用來加速讀取路徑。當(dāng)查詢前端就位時(shí),應(yīng)將傳入的查詢請(qǐng)求定向到查詢前端,而不是 querier, 為了執(zhí)行實(shí)際的查詢,群集中仍需要 querier 服務(wù)。
查詢前端在內(nèi)部執(zhí)行一些查詢調(diào)整,并在內(nèi)部隊(duì)列中保存查詢。querier 作為 workers 從隊(duì)列中提取作業(yè),執(zhí)行它們,并將它們返回到查詢前端進(jìn)行匯總。querier 需要配置查詢前端地址(通過-querier.frontend-address CLI 標(biāo)志),以便允許它們連接到查詢前端。
查詢前端是無狀態(tài)的,然而,由于內(nèi)部隊(duì)列的工作方式,建議運(yùn)行幾個(gè)查詢前臺(tái)的副本,以獲得公平調(diào)度的好處,在大多數(shù)情況下,兩個(gè)副本應(yīng)該足夠了。
隊(duì)列
查詢前端的排隊(duì)機(jī)制用于:
確保可能導(dǎo)致 querier出現(xiàn)內(nèi)存不足(OOM)錯(cuò)誤的查詢?cè)谑r(shí)被重試。這允許管理員可以為查詢提供不足的內(nèi)存,或者并行運(yùn)行更多的小型查詢,這有助于降低總成本。通過使用先進(jìn)先出隊(duì)列(FIFO)將多個(gè)大型請(qǐng)求分配到所有 querier上,以防止在單個(gè)querier中傳送多個(gè)大型請(qǐng)求。通過在租戶之間公平調(diào)度查詢。
分割
查詢前端將較大的查詢分割成多個(gè)較小的查詢,在下游 querier 上并行執(zhí)行這些查詢,并將結(jié)果再次拼接起來。這可以防止大型查詢?cè)趩蝹€(gè)查詢器中造成內(nèi)存不足的問題,并有助于更快地執(zhí)行這些查詢。
緩存
查詢前端支持緩存指標(biāo)查詢結(jié)果,并在后續(xù)查詢中重復(fù)使用。如果緩存的結(jié)果不完整,查詢前端會(huì)計(jì)算所需的子查詢,并在下游 querier 上并行執(zhí)行這些子查詢。查詢前端可以選擇將查詢與其 step 參數(shù)對(duì)齊,以提高查詢結(jié)果的可緩存性。結(jié)果緩存與任何 loki 緩存后端(當(dāng)前為 memcached、redis 和內(nèi)存緩存)兼容。
Querier
Querier 查詢器服務(wù)使用 LogQL 查詢語言處理查詢,從 ingesters 和長期存儲(chǔ)中獲取日志。
查詢器查詢所有 ingesters 的內(nèi)存數(shù)據(jù),然后再到后端存儲(chǔ)運(yùn)行相同的查詢。由于復(fù)制因子,查詢器有可能會(huì)收到重復(fù)的數(shù)據(jù)。為了解決這個(gè)問題,查詢器在內(nèi)部對(duì)具有相同納秒時(shí)間戳、標(biāo)簽集和日志信息的數(shù)據(jù)進(jìn)行重復(fù)數(shù)據(jù)刪除。
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 納秒時(shí)間戳。
Block 格式
一個(gè) block 由一系列日志 entries 組成,每個(gè) entry 都是一個(gè)單獨(dú)的日志行。
請(qǐng)注意,一個(gè) block 的字節(jié)是用 Gzip 壓縮存儲(chǔ)的。以下是它們未壓縮時(shí)的形式。
-------------------------------------------------------------------
| 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 納秒時(shí)間戳,而 len 是日志條目的字節(jié)長度。
Chunk 存儲(chǔ)
Chunk 存儲(chǔ)是 Loki 的長期數(shù)據(jù)存儲(chǔ),旨在支持交互式查詢和持續(xù)寫入,不需要后臺(tái)維護(hù)任務(wù)。它由以下部分組成:
一個(gè) chunks 索引,這個(gè)索引可以通過以下方式支持:Amazon DynamoDB、Google Bigtable、Apache Cassandra。 一個(gè)用于 chunk 數(shù)據(jù)本身的鍵值(KV)存儲(chǔ),可以是:Amazon DynamoDB、Google Bigtable、Apache Cassandra、Amazon S3、Google Cloud Storage。
與 Loki 的其他核心組件不同,塊存儲(chǔ)不是一個(gè)單獨(dú)的服務(wù)、任務(wù)或進(jìn)程,而是嵌入到需要訪問 Loki 數(shù)據(jù)的
ingester和querier服務(wù)中的一個(gè)庫。
塊存儲(chǔ)依賴于一個(gè)統(tǒng)一的接口,用于支持塊存儲(chǔ)索引的 NoSQL 存儲(chǔ)(DynamoDB、Bigtable 和 Cassandra)。這個(gè)接口假定索引是由以下項(xiàng)構(gòu)成的鍵的條目集合。
一個(gè)哈希 key,對(duì)所有的讀和寫都是必需的。 一個(gè)范圍 key,寫入時(shí)需要,讀取時(shí)可以省略,可以通過前綴或范圍進(jìn)行查詢。
該接口在支持的數(shù)據(jù)庫中的工作方式有些不同:
DynamoDB原生支持范圍和哈希鍵,因此,索引條目被直接建模為 DynamoDB 條目,哈希鍵作為分布鍵,范圍作為 DynamoDB 范圍鍵。對(duì)于 Bigtable和Cassandra,索引條目被建模為單個(gè)列值。哈希鍵成為行鍵,范圍鍵成為列鍵。
一組模式集合被用來將讀取和寫入塊存儲(chǔ)時(shí)使用的匹配器和標(biāo)簽集映射到索引上的操作。隨著 Loki 的發(fā)展,Schemas 模式也被添加進(jìn)來,主要是為了更好地平衡寫操作和提高查詢性能。
讀取路徑
日志讀取路徑的流程如下所示:
查詢器收到一個(gè)對(duì)數(shù)據(jù)的 HTTP 請(qǐng)求。 查詢器將查詢傳遞給所有 ingesters以獲取內(nèi)存數(shù)據(jù)。ingesters收到讀取請(qǐng)求,并返回與查詢相匹配的數(shù)據(jù)(如果有的話)。如果沒有 ingesters返回?cái)?shù)據(jù),查詢器會(huì)從后端存儲(chǔ)加載數(shù)據(jù),并對(duì)其運(yùn)行查詢。查詢器對(duì)所有收到的數(shù)據(jù)進(jìn)行迭代和重復(fù)計(jì)算,通過 HTTP 連接返回最后一組數(shù)據(jù)。
寫入路徑

整體的日志寫入路徑如下所示:
distributor收到一個(gè) HTTP 請(qǐng)求,以存儲(chǔ)流的數(shù)據(jù)。每個(gè)流都使用哈希環(huán)進(jìn)行哈希操作。 distributor將每個(gè)流發(fā)送到合適的ingester和他們的副本(基于配置的復(fù)制因子)。每個(gè) ingester將為日志流數(shù)據(jù)創(chuàng)建一個(gè)塊或附加到一個(gè)現(xiàn)有的塊上。每個(gè)租戶和每個(gè)標(biāo)簽集的塊是唯一的。distributor通過 HTTP 連接響應(yīng)一個(gè)成功代碼。
