日誌多租戶架構下的 Loki 方案

當我們在看 Loki 的架構文檔時,社區都會宣稱 Loki 是一個可以支持多租戶模式下運行的日誌系統,但我們再想進一步瞭解時,它卻含蓄的表示 Loki 開啓多租戶只需要滿足兩個條件:

這一切似乎都在告訴你,"快來用我吧,這很簡單",事實上當我們真的要在 kubernetes 中構建一個多租戶的日誌系統時,我們需要考慮的遠不止於此。

通常當我們在面對一個多租戶的日誌系統架構時,出於對日誌存儲的考慮,我們一般會有兩種模式來影響系統的架構。

1. 日誌集中存儲(後文以方案 A 代稱)

和 Loki 原生一樣,在日誌進入到集羣內,經過一系列校驗和索引後集中的將日誌統一寫入後端存儲上。

2. 日誌分區存儲(後文以方案 B 代稱)

反中心存儲架構,每個租戶或項目都可以擁有獨立的日誌服務和存儲區塊來保存日誌。

從直覺上來看,日誌分區帶來的整體結構會更爲複雜,除了需要自己開發控制器來管理 loki 服務的生命週期外,它還需要爲網關提供正確的路由策略。不過,不管多租戶的系統選擇何種方案,在本文我們也需從日誌的整個流程來闡述不同方案的實現。

第一關:Loki 劃分

Loki 是最終承載日誌存儲和查詢的服務,在多租戶的模式下,不管是大集羣還是小服務,Loki 本身也存在一些配置空間需要架構者去適配。其中特別是在面對大集羣場景下,保證每個租戶的日誌寫入和查詢所佔資源的合理分配調度就顯得尤爲重要。

在原生配置中,大部分關於租戶的調整可以在下面兩個配置區塊中完成:

query_frontend_config

query_frontend 是 Loki 分佈式集羣模式下的日誌查詢最前端,它承擔着用戶日誌查詢請求的分解和聚合工作。那麼顯然,query_frontend 對於請求的處理資源應避免被單個用戶過分搶佔。

每個 frontend 處理的租戶

[max_outstanding_per_tenant: <int> | default = 100]

limits_config

limits_config 基本控制了 Loki 全局的一些流控參數和局部的租戶資源分配,這裏面可以通過 Loki 的-runtime-config啓動參數來讓服務動態定期的加載租戶限制。這部分可以通過runtime_config.go中的runtimeConfigValues結構體內看到

type runtimeConfigValues struct {
 TenantLimits map[string]*validation.Limits `yaml:"overrides"`

 Multi kv.MultiRuntimeConfig `yaml:"multi_kv_config"`
}

可以看到對於 TenantLimits 內的限制配置是直接繼承 limits_config 的,那麼這部分的結構應該就是下面這樣:

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

當我們在選擇採用方案 A 的日誌架構時,關於租戶部分的限制邏輯就應該要根據租戶內的日誌規模靈活的配置。如果選擇方案 B,由於每個租戶佔有完整的 Loki 資源,所以這部分邏輯就直接由原生的 limits_config 控制。

第二關:日誌客戶端

在 Kubernetes 環境下,最重要是讓日誌客戶端知道被採集的容器所屬的租戶信息。這部分實現可以是通過日誌 Operator 或者是解析 kubernetes 元數據來實現。雖然這兩個實現方式不同,不過最終目的都是讓客戶端在採集日之後,在日誌流的請求上添加租戶信息頭。下面我分別以 logging-operator 和 fluentbit/fluentd 這兩種實現方式來描述他們的實現邏輯

Logging Operator

Logging Operator 是 BanzaiCloud 下開源的一個雲原生場景下的日誌採集方案。它可以通過創建 NameSpace 級別的 CRD 資源 flow 和 output 來控制日誌的解析和輸出。

通過 Operator 的方式可以精細的控制租戶內的日誌需要被採集的容器,以及控制它們的流向。以輸出到 loki 舉例,通常在只需在租戶的命名空間內創建如下資源就能滿足需求。

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>
...
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"

可以看到通過 operator 來管理多租戶的日誌是一個非常簡單且優雅的方式,同時通過 CRD 的方式創建資源對開發者集成到項目也十分友好。這也是我比較推薦的日誌客戶端方案。

FluentBit/FluentD

FluentBit 和 FluentD 的 Loki 插件同樣支持對多租戶的配置。對於它們而言最重要的是讓其感知到日誌的租戶信息。與 Operator 在 CRD 中直接聲明租戶信息不同,直接採用客戶端方案就需要通過Kubernetes Metadata的方式來主動抓取租戶信息。對租戶信息的定義,我們會聲明在資源的 label 中。不過對於不同的客戶端,label 定義的路徑還是比較有講究的。它們總體處理流程如下:

fluentd 的 kubernetes-metadata-filter 可以抓取到 namespaces_label,所以我比較推薦將租戶信息定義在命名空間內。

apiVersion: v1
kind: Namespace
metadata:
  labels:
    tenant: <tenantA>
  name: <taenant-namespace>

這樣在就可以 loki 的插件中直接提取 namespace 中的租戶標籤內容,實現邏輯如下

<match loki.**>
  @type loki
  @id loki.output
  url "http://loki:3100"
  # 直接提取命名空間內的租戶信息
  tenant ${$.kubernetes.namespace_labels.tenant}
  username <username>
  password <password>
  <label>
    tenant ${$.kubernetes.namespace_labels.tenant}
  </label>

fluentbit 的 metadata 是從 pod 中抓取,那麼我們就需要將租戶信息定義在 workload 的template.metadata.labels當中,如下:

apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
   app:  nginx
spec:
 template:
   metadata:
     labels:
       app: nginx
       tenant: <tanant-A>

之後就需要利用 rewrite_tag 將容器的租戶信息提取出來進行日誌管道切分。並在 output 階段針對不同日誌管道進行輸出。它的實現邏輯如下:

[FILTER]
   Name           kubernetes
   Match          kube.*
   Kube_URL       https://kubernetes.default.svc:443
   Merge_Log      On
[FILTER]
   Name           rewrite_tag
   Match          kube.*
   #提取pod中的租戶信息,並進行日誌管道切分
   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 的方式進行多租戶的配置,它們不但對標籤有一定的要求,對日誌的輸出路徑配置也不是非常靈活。所以fluentd它比較做適合方案A的日誌客戶端,而fluentbit比較適合做方案B的日誌客戶端

第三層:日誌網關

日誌網關準確的說是 Loki 服務的網關,對於方案 A 來說,一個大 Loki 集羣前面的網關,只需要簡單滿足能夠橫向擴展即可,租戶的頭信息直接傳遞給後方的 Loki 服務處理。這類方案相對簡單,並無特別說明。只需注意針對查詢接口的配置需調試優化,例如網關服務與upstream之間的連接超時時間網關服務response數據包大小等。

本文想說明的日誌網關是針對方案 B 場景下,解決針對不同租戶的日誌路由問題。從上文可以看到,在方案 B 中,我們引入了一個控制器來解決租戶 Loki 實例的管理問題。但是這樣就帶來一個新的問題需要解決,那就是 Loki 的服務需要註冊到網關,並實現路由規則的生成。這部分可以由集羣的控制器 CRD 資源作爲網關的 upsteam 源配置。控制器的邏輯如下:

網關服務在處理租戶頭信息時,路由部分的邏輯爲判斷 Header 中X-Scope-OrgID帶租戶信息的日誌請求,並將其轉發到對應的 Loki 服務。我們以 nginx 作爲網關舉個例,它的核心邏輯如下:

#upstream內地址由sidecar從CRD中獲取loki實例後渲染生成

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;

總結

本文介紹了基於 Loki 在多租戶模式下的兩種日誌架構,分別爲日誌集中存儲日誌分區存儲。他們分別具備如下的特點:

eH2KEf

對於團隊內具備 kubernetes operator 相關開發經驗的同學可以採用日誌分區存儲方案,如果團隊內偏向運維方向,可以選擇日誌集中存儲方案。

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