日志多租戶架構(gòu)下的 Loki 方案
當(dāng)我們?cè)诳碙oki的架構(gòu)文檔時(shí),社區(qū)都會(huì)宣稱Loki是一個(gè)可以支持多租戶模式下運(yùn)行的日志系統(tǒng),但我們?cè)傧脒M(jìn)一步了解時(shí),它卻含蓄的表示Loki開啟多租戶只需要滿足兩個(gè)條件:
配置文件中添加 auth_enabled: true請(qǐng)求頭內(nèi)帶上租戶信息 X-Scope-OrgID
這一切似乎都在告訴你,"快來(lái)用我吧,這很簡(jiǎn)單",事實(shí)上當(dāng)我們真的要在kubernetes中構(gòu)建一個(gè)多租戶的日志系統(tǒng)時(shí),我們需要考慮的遠(yuǎn)不止于此。
通常當(dāng)我們?cè)诿鎸?duì)一個(gè)多租戶的日志系統(tǒng)架構(gòu)時(shí),出于對(duì)日志存儲(chǔ)的考慮,我們一般會(huì)有兩種模式來(lái)影響系統(tǒng)的架構(gòu)。
1. 日志集中存儲(chǔ)(后文以方案A代稱)
和Loki原生一樣,在日志進(jìn)入到集群內(nèi),經(jīng)過(guò)一系列校驗(yàn)和索引后集中的將日志統(tǒng)一寫入后端存儲(chǔ)上。

2. 日志分區(qū)存儲(chǔ)(后文以方案B代稱)
反中心存儲(chǔ)架構(gòu),每個(gè)租戶或項(xiàng)目都可以擁有獨(dú)立的日志服務(wù)和存儲(chǔ)區(qū)塊來(lái)保存日志。

從直覺(jué)上來(lái)看,日志分區(qū)帶來(lái)的整體結(jié)構(gòu)會(huì)更為復(fù)雜,除了需要自己開發(fā)控制器來(lái)管理loki服務(wù)的生命周期外,它還需要為網(wǎng)關(guān)提供正確的路由策略。不過(guò),不管多租戶的系統(tǒng)選擇何種方案,在本文我們也需從日志的整個(gè)流程來(lái)闡述不同方案的實(shí)現(xiàn)。
第一關(guān):Loki劃分
Loki是最終承載日志存儲(chǔ)和查詢的服務(wù),在多租戶的模式下,不管是大集群還是小服務(wù),Loki本身也存在一些配置空間需要架構(gòu)者去適配。其中特別是在面對(duì)大集群場(chǎng)景下,保證每個(gè)租戶的日志寫入和查詢所占資源的合理分配調(diào)度就顯得尤為重要。
在原生配置中,大部分關(guān)于租戶的調(diào)整可以在下面兩個(gè)配置區(qū)塊中完成:
query_frontend_config limits_config
query_frontend_config
query_frontend是Loki分布式集群模式下的日志查詢最前端,它承擔(dān)著用戶日志查詢請(qǐng)求的分解和聚合工作。那么顯然,query_frontend對(duì)于請(qǐng)求的處理資源應(yīng)避免被單個(gè)用戶過(guò)分搶占。
每個(gè)frontend處理的租戶
[max_outstanding_per_tenant: <int> | default = 100]
limits_config
limits_config基本控制了Loki全局的一些流控參數(shù)和局部的租戶資源分配,這里面可以通過(guò)Loki的-runtime-config啟動(dòng)參數(shù)來(lái)讓服務(wù)動(dòng)態(tài)定期的加載租戶限制。這部分可以通過(guò)runtime_config.go中的runtimeConfigValues結(jié)構(gòu)體內(nèi)看到
type runtimeConfigValues struct {
TenantLimits map[string]*validation.Limits `yaml:"overrides"`
Multi kv.MultiRuntimeConfig `yaml:"multi_kv_config"`
}
可以看到對(duì)于TenantLimits內(nèi)的限制配置是直接繼承l(wèi)imits_config的,那么這部分的結(jié)構(gòu)應(yīng)該就是下面這樣:
overrides:
tenantA:
ingestion_rate_mb: 10
max_streams_per_user: 100000
max_chunks_per_query: 100000
tenantB:
max_streams_per_user: 1000000
max_chunks_per_query: 1000000
當(dāng)我們?cè)谶x擇采用方案A的日志架構(gòu)時(shí),關(guān)于租戶部分的限制邏輯就應(yīng)該要根據(jù)租戶內(nèi)的日志規(guī)模靈活的配置。如果選擇方案B,由于每個(gè)租戶占有完整的Loki資源,所以這部分邏輯就直接由原生的limits_config控制。
第二關(guān):日志客戶端
在Kubernetes環(huán)境下,最重要是讓日志客戶端知道被采集的容器所屬的租戶信息。這部分實(shí)現(xiàn)可以是通過(guò)日志Operator或者是解析kubernetes元數(shù)據(jù)來(lái)實(shí)現(xiàn)。雖然這兩個(gè)實(shí)現(xiàn)方式不同,不過(guò)最終目的都是讓客戶端在采集日之后,在日志流的請(qǐng)求上添加租戶信息頭。下面我分別以logging-operator和fluentbit/fluentd這兩種實(shí)現(xiàn)方式來(lái)描述他們的實(shí)現(xiàn)邏輯
Logging Operator
Logging Operator是BanzaiCloud下開源的一個(gè)云原生場(chǎng)景下的日志采集方案。它可以通過(guò)創(chuàng)建NameSpace級(jí)別的CRD資源flow和output來(lái)控制日志的解析和輸出。

通過(guò)Operator的方式可以精細(xì)的控制租戶內(nèi)的日志需要被采集的容器,以及控制它們的流向。以輸出到loki舉例,通常在只需在租戶的命名空間內(nèi)創(chuàng)建如下資源就能滿足需求。
output.yaml,在創(chuàng)建資源時(shí)帶入租戶相關(guān)的信息
apiVersion: logging.banzaicloud.io/v1beta1
kind: Output
metadata:
name: loki-output
namespace: <tenantA-namespace>
spec:
loki:
url: http://loki:3100
username: <tenantA>
password: <tenantA>
tenant: <tenantA>
...
flow.yaml,在創(chuàng)建資源時(shí)關(guān)聯(lián)租戶需要被采集日志的容器,以及指定輸出
apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: flow
namespace: <tenantA-namespace>
spec:
localOutputRefs:
- loki-output
match:
- select:
labels:
app: nginx
filters:
- parser:
remove_key_name_field: true
reserve_data: true
key_name: "log"
可以看到通過(guò)operator來(lái)管理多租戶的日志是一個(gè)非常簡(jiǎn)單且優(yōu)雅的方式,同時(shí)通過(guò)CRD的方式創(chuàng)建資源對(duì)開發(fā)者集成到項(xiàng)目也十分友好。這也是我比較推薦的日志客戶端方案。
FluentBit/FluentD
FluentBit和FluentD的Loki插件同樣支持對(duì)多租戶的配置。對(duì)于它們而言最重要的是讓其感知到日志的租戶信息。與Operator在CRD中直接聲明租戶信息不同,直接采用客戶端方案就需要通過(guò)Kubernetes Metadata的方式來(lái)主動(dòng)抓取租戶信息。對(duì)租戶信息的定義,我們會(huì)聲明在資源的label中。不過(guò)對(duì)于不同的客戶端,label定義的路徑還是比較有講究的。它們總體處理流程如下:

FluentD
fluentd的kubernetes-metadata-filter可以抓取到namespaces_label,所以我比較推薦將租戶信息定義在命名空間內(nèi)。
apiVersion: v1
kind: Namespace
metadata:
labels:
tenant: <tenantA>
name: <taenant-namespace>
這樣在就可以loki的插件中直接提取namespace中的租戶標(biāo)簽內(nèi)容,實(shí)現(xiàn)邏輯如下
<match loki.**>
@type loki
@id loki.output
url "http://loki:3100"
# 直接提取命名空間內(nèi)的租戶信息
tenant ${$.kubernetes.namespace_labels.tenant}
username <username>
password <password>
<label>
tenant ${$.kubernetes.namespace_labels.tenant}
</label>
FluentBit
fluentbit的metadata是從pod中抓取,那么我們就需要將租戶信息定義在workload的template.metadata.labels當(dāng)中,如下:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
spec:
template:
metadata:
labels:
app: nginx
tenant: <tanant-A>
之后就需要利用rewrite_tag將容器的租戶信息提取出來(lái)進(jìn)行日志管道切分。并在output階段針對(duì)不同日志管道進(jìn)行輸出。它的實(shí)現(xiàn)邏輯如下:
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Merge_Log On
[FILTER]
Name rewrite_tag
Match kube.*
#提取pod中的租戶信息,并進(jìn)行日志管道切分
Rule $kubernetes['labels']['tenant'] ^(.*)$ tenant.$kubernetes['labels']['tenant'].$TAG false
Emitter_Name re_emitted
[Output]
Name grafana-loki
Match tenant.tenantA.*
Url http://loki:3100/api/prom/push
TenantID "tenantA"
[Output]
Name grafana-loki
Match tenant.tenantB.*
Url http://loki:3100/api/prom/push
TenantID "tenantB"
可以看到不管是用FluentBit還是Fluentd的方式進(jìn)行多租戶的配置,它們不但對(duì)標(biāo)簽有一定的要求,對(duì)日志的輸出路徑配置也不是非常靈活。所以fluentd它比較做適合方案A的日志客戶端,而fluentbit比較適合做方案B的日志客戶端。
第三層:日志網(wǎng)關(guān)
日志網(wǎng)關(guān)準(zhǔn)確的說(shuō)是Loki服務(wù)的網(wǎng)關(guān),對(duì)于方案A來(lái)說(shuō),一個(gè)大Loki集群前面的網(wǎng)關(guān),只需要簡(jiǎn)單滿足能夠橫向擴(kuò)展即可,租戶的頭信息直接傳遞給后方的Loki服務(wù)處理。這類方案相對(duì)簡(jiǎn)單,并無(wú)特別說(shuō)明。只需注意針對(duì)查詢接口的配置需調(diào)試優(yōu)化,例如網(wǎng)關(guān)服務(wù)與upstream之間的連接超時(shí)時(shí)間、網(wǎng)關(guān)服務(wù)response數(shù)據(jù)包大小等。
本文想說(shuō)明的日志網(wǎng)關(guān)是針對(duì)方案B場(chǎng)景下,解決針對(duì)不同租戶的日志路由問(wèn)題。從上文可以看到,在方案B中,我們引入了一個(gè)控制器來(lái)解決租戶Loki實(shí)例的管理問(wèn)題。但是這樣就帶來(lái)一個(gè)新的問(wèn)題需要解決,那就是Loki的服務(wù)需要注冊(cè)到網(wǎng)關(guān),并實(shí)現(xiàn)路由規(guī)則的生成。這部分可以由集群的控制器CRD資源作為網(wǎng)關(guān)的upsteam源配置。控制器的邏輯如下:

網(wǎng)關(guān)服務(wù)在處理租戶頭信息時(shí),路由部分的邏輯為判斷Header中X-Scope-OrgID帶租戶信息的日志請(qǐng)求,并將其轉(zhuǎn)發(fā)到對(duì)應(yīng)的Loki服務(wù)。我們以nginx作為網(wǎng)關(guān)舉個(gè)例,它的核心邏輯如下:
#upstream內(nèi)地址由sidecar從CRD中獲取loki實(shí)例后渲染生成
upstream tenantA {
server x.x.x.x:3100;
}
upstream tenantB {
server y.y.y.y:3100;
}
server {
location / {
set tenant $http_x_scope_orgid;
proxy_pass http://$tenant;
include proxy_params;
總結(jié)
本文介紹了基于Loki在多租戶模式下的兩種日志架構(gòu),分別為日志集中存儲(chǔ)和日志分區(qū)存儲(chǔ)。他們分別具備如下的特點(diǎn):
| 方案 | Loki架構(gòu) | 客戶端架構(gòu) | 網(wǎng)關(guān)架構(gòu) | 開發(fā)難度 | 運(yùn)維難度 | 自動(dòng)化程度 |
|---|---|---|---|---|---|---|
| 日志集中存儲(chǔ) | 集群、復(fù)雜 | fluentd / fluentbit | 簡(jiǎn)單 | 簡(jiǎn)單 | 中等 | 低 |
| 日志分區(qū)存儲(chǔ) | 簡(jiǎn)單 | Logging Opeator | 較復(fù)雜 | 較復(fù)雜(控制器部分) | 中等 | 高 |
對(duì)于團(tuán)隊(duì)內(nèi)具備kubernetes operator相關(guān)開發(fā)經(jīng)驗(yàn)的同學(xué)可以采用日志分區(qū)存儲(chǔ)方案,如果團(tuán)隊(duì)內(nèi)偏向運(yùn)維方向,可以選擇日志集中存儲(chǔ)方案。
