在 EKS 中實現基於 Promtail - Loki - Grafana 容器日誌解決方案

ELK/EFK 日誌系統

如果今天談論到要部署一套日誌系統,相信用戶首先會想到的就是經典的 ELK 架構,或者現在被稱爲 Elastic Stack。Elastic Stack 架構爲 Elasticsearch + Logstash + Kibana + Beats 的組合,其中,Beats 負責日誌的採集, Logstash 負責做日誌的聚合和處理,Elasticsearch 作爲日誌的存儲和搜索系統,Kibana 作爲可視化前端展示,整體架構如下圖所示:

此外,在容器化場景中,尤其是在 Kubernetes 環境中,用戶經常使用的另一套框架是 EFK 架構。其中,E 還是 Elasticsearch,K 還是 Kibana,其中的 F 代表 Fluent Bit,一個開源多平臺的日誌處理器和轉發器。Fluent Bit 可以讓用戶從不同的來源收集數據 / 日誌,統一併發送到多個目的地,並且它完全兼容 Docker 和 Kubernetes 環境。

PLG 日誌系統

但是,Grafana Labs 提供的另一個日誌解決方案 PLG 目前也逐漸變得流行起來。PLG 架構爲 Promtail + Loki + Grafana 的組合,整體架構圖下所示:

其中,Grafana 大家應該都比較熟悉,它是一款開源的可視化和分析軟件,它允許用戶查詢、可視化、警告和探索監控指標。Grafana 主要提供時間序列數據的儀表板解決方案,支持超過數十種數據源(還在陸續添加支持中)。

這裏稍微介紹下另外兩個軟件 Promtail 和 Loki。官方介紹 Grafana Loki 是一組可以組成一個功能齊全的日誌堆棧組件,與其它日誌系統不同的是,Loki 只建立日誌標籤的索引而不索引原始日誌消息,而是爲日誌數據設置一組標籤,這意味着 Loki 的運營成本更低,效率也能提高几個數量級。

Loki 的設計理念收到了很多 Prometheus 的啓發,可以實現可水平擴展、高可用的多租戶日誌系統。Loki 整體架構也是由不同的組件來協同完成日誌收集、索引、存儲等工作的,各個組件如下所示,有關 Loki 架構的更多信息這裏不再展開描述,可以參考官方文檔 Loki’s Architecture 進一步深入瞭解。最後,一句話形容下 Loki 就是 like Prometheus, but for logs。

Promtail 是一個日誌收集的代理,它會將本地日誌的內容發送到一個 Loki 實例,它通常部署到需要監視應用程序的每臺機器 / 容器上。Promtail 主要是用來發現目標、將標籤附加到日誌流以及將日誌推送到 Loki。截止到目前,Promtail 可以跟蹤兩個來源的日誌:本地日誌文件和 systemd 日誌(僅支持 AMD64 架構)。

這樣看上去,PLG 和 ELK 都能完成類似的日誌管理工作,那它們之間的差別在哪裏呢?

日誌方案對比

首先,ELK/EFK 架構功能確實強大,也經過了多年的實際環境驗證,其中存儲在 Elasticsearch 中的日誌通常以非結構化 JSON 對象的形式存儲在磁盤上,並且 Elasticsearch 爲每個對象都建立了索引,以便進行全文搜索,然後用戶可以特定查詢語言來搜索這些日誌數據。與之對應的 Loki 的數據存儲是解耦的,既可以在磁盤上存儲數據,也可以使用如 Amazon S3 的雲存儲系統。Loki 中的日誌帶有一組標籤名和值,其中只有標籤對被索引,這種權衡使得它比完整索引的操作成本更低,但是針對基於內容的查詢,需要通過 LogQL 再單獨查詢。

和 Fluentd 相比,Promtail 是專門爲 Loki 量身定製的,它可以爲運行在同一節點上的 Kubernetes Pods 做服務發現,從指定文件夾讀取日誌。Loki 採用了類似於 Prometheus 的標籤方式。因此,當與 Prometheus 部署在同一個環境中時,因爲相同的服務發現機制,來自 Promtail 的日誌通常具有與應用程序指標相同的標籤,統一了標籤管理。

Kibana 提供了許多可視化工具來進行數據分析,高級功能比如異常檢測等機器學習功能。Grafana 專門針對 Prometheus 和 Loki 等時間序列數據打造,可以在同一個儀表板上查看日誌的指標。

在 EKS 上部署 Promtail + Loki + Grafana 解決方案

接下來,我們將演示如何在 EKS 上部署 Promtail + Loki + Grafana 組合,下面演示需要有滿足一些前提條件:

演示環境如下:

  1. 部署 Promtail + Loki + Grafana

首先,添加 helm 的 repo 信息。

$ helm repo add grafana https://grafana.github.io/helm-charts

然後,更新 helm repo。

$ helm repo update

更新完成後,使用 helm 安裝 loki 和 grafana。默認情況下,Loki 和 Grafana 都是安裝在 default 命名空間的,可以添加 –namespace <命名空間> 參數將 Loki 和 Grafana 部署在指定的命名空間,這裏演示創建一個新的命名空間 loki,並將 Loki 和 Grafana 都安裝在這裏。其中 grafana.enabled=true 選項可以將 Grafana 一起進行部署,如果希望同時安裝 Prometheus,則也可以選擇配置 prometheus.enabled=true 參數,演示中並未開啓此參數。

$ kubectl create namespace loki
$ helm upgrade --install loki --namespace=loki grafana/loki-stack  --set grafana.enabled=true

正常安裝會返回以下輸出結果:

Binding
NAME: loki
LAST DEPLOYED: Thu May 13 12:38:52 2021
NAMESPACE: loki
STATUS: deployed
REVISION: 1
NOTES:
The Loki stack has been deployed to your cluster. Loki can now be added as a datasource in Grafana.

See http://docs.grafana.org/features/datasources/loki/ for more detail.

部署完成後,我們來檢查下使用 helm 部署的資源情況:

$ kubectl -n loki get all

NAME                               READY   STATUS    RESTARTS   AGE
pod/loki-0                         1/1     Running   0          113s
pod/loki-grafana-b664d6c4f-qlg87   1/1     Running   0          113s
pod/loki-promtail-jm8x8            1/1     Running   0          113s
pod/loki-promtail-lb8jq            1/1     Running   0          113s

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/loki            ClusterIP   10.100.52.51    <none>        3100/TCP   114s
service/loki-grafana    ClusterIP   10.100.134.81   <none>        80/TCP     114s
service/loki-headless   ClusterIP   None            <none>        3100/TCP   114s

NAME                           DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/loki-promtail   2         2         2       2            2           <none>          114s

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/loki-grafana   1/1     1            1           114s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/loki-grafana-b664d6c4f   1         1         1       114s

NAME                    READY   AGE
statefulset.apps/loki   1/1     114s

可以看到通過 Helm 部署後自動完成了 Promtail + Loki + Grafana 組合的安裝,其中 Promtail 部署模式爲 daemonset,在每個計算節點上都有部署,來收集節點以及 Pod 上的日誌信息,具體配置如下所示:

$ kubectl describe ds loki-promtail -n loki

Name:           loki-promtail
Selector:       app=promtail,release=loki
Node-Selector:  <none>
Labels:         app=promtail
                app.kubernetes.io/managed-by=Helm
                chart=promtail-2.2.0
                heritage=Helm
                release=loki
Annotations:    deprecated.daemonset.template.generation: 1
                meta.helm.sh/release-name: loki
                meta.helm.sh/release-namespace: loki
Desired Number of Nodes Scheduled: 2
Current Number of Nodes Scheduled: 2
Number of Nodes Scheduled with Up-to-date Pods: 2
Number of Nodes Scheduled with Available Pods: 2
Number of Nodes Misscheduled: 0
Pods Status:  2 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:           app=promtail
                    release=loki
  Annotations:      checksum/config: 8c87a13d751c87f1b8726a725bffbe18c827c5e60d4a7aea47cd4871ea8271f3
                    prometheus.io/port: http-metrics
                    prometheus.io/scrape: true
  Service Account:  loki-promtail
  Containers:
   promtail:
    Image:      grafana/promtail:2.1.0
    Port:       3101/TCP
    Host Port:  0/TCP
    Args:
      -config.file=/etc/promtail/promtail.yaml
      -client.url=http://loki:3100/loki/api/v1/push
    Readiness:  http-get http://:http-metrics/ready delay=10s timeout=1s period=10s #success=1 #failure=5
    Environment:
      HOSTNAME:   (v1:spec.nodeName)
    Mounts:
      /etc/promtail from config (rw)
      /run/promtail from run (rw)
      /var/lib/docker/containers from docker (ro)
      /var/log/pods from pods (ro)
  Volumes:
   config:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      loki-promtail
    Optional:  false
   run:
    Type:          HostPath (bare host directory volume)
    Path:          /run/promtail
    HostPathType:  
   docker:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/docker/containers
    HostPathType:  
   pods:
    Type:          HostPath (bare host directory volume)
    Path:          /var/log/pods
    HostPathType:  
Events:
  Type    Reason            Age   From                  Message
  ----    ------            ----  ----                  -------
  Normal  SuccessfulCreate  14m   daemonset-controller  Created pod: loki-promtail-lb8jq
  Normal  SuccessfulCreate  14m   daemonset-controller  Created pod: loki-promtail-jm8x8

Loki 本身默認是通過 statefulset 的方式部署,這是爲了避免在數據攝入組件崩潰時丟失索引,因此官方建議將 Loki 通過 statefulset 運行,並使用持久化存儲來存儲索引文件,具體配置如下所示:

$ kubectl describe sts loki -n loki

Name:               loki
Namespace:          loki
CreationTimestamp:  Thu, 13 May 2021 12:38:53 +0000
Selector:           app=loki,release=loki
Labels:             app=loki
                    app.kubernetes.io/managed-by=Helm
                    chart=loki-2.3.0
                    heritage=Helm
                    release=loki
Annotations:        meta.helm.sh/release-name: loki
                    meta.helm.sh/release-namespace: loki
Replicas:           1 desired | 1 total
Update Strategy:    RollingUpdate
Pods Status:        1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:           app=loki
                    name=loki
                    release=loki
  Annotations:      checksum/config: fd74389a2862aeb2df7c193d74824ebd1a14c8c061df4bc9f5bb6ce1cbae4b8c
                    prometheus.io/port: http-metrics
                    prometheus.io/scrape: true
  Service Account:  loki
  Containers:
   loki:
    Image:      grafana/loki:2.1.0
    Port:       3100/TCP
    Host Port:  0/TCP
    Args:
      -config.file=/etc/loki/loki.yaml
    Liveness:     http-get http://:http-metrics/ready delay=45s timeout=1s period=10s #success=1 #failure=3
    Readiness:    http-get http://:http-metrics/ready delay=45s timeout=1s period=10s #success=1 #failure=3
    Environment:  <none>
    Mounts:
      /data from storage (rw)
      /etc/loki from config (rw)
  Volumes:
   config:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  loki
    Optional:    false
   storage:
    Type:       EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:     
    SizeLimit:  <unset>
Volume Claims:  <none>
Events:
  Type    Reason            Age   From                    Message
  ----    ------            ----  ----                    -------
  Normal  SuccessfulCreate  10m   statefulset-controller  create Pod loki-0 in StatefulSet loki successful

最後 Grafana 是通過 deployment 來完成的,具體配置如下所示:

$ kubectl describe deployment loki-grafana -n loki

Name:                   loki-grafana
Namespace:              loki
CreationTimestamp:      Thu, 13 May 2021 12:38:53 +0000
Labels:                 app.kubernetes.io/instance=loki
                        app.kubernetes.io/managed-by=Helm
                        app.kubernetes.io/name=grafana
                        app.kubernetes.io/version=6.7.0
                        helm.sh/chart=grafana-5.7.10
Annotations:            deployment.kubernetes.io/revision: 1
                        meta.helm.sh/release-name: loki
                        meta.helm.sh/release-namespace: loki
Selector:               app.kubernetes.io/instance=loki,app.kubernetes.io/name=grafana
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:           app.kubernetes.io/instance=loki
                    app.kubernetes.io/name=grafana
  Annotations:      checksum/config: 19aac1c3228c4f4807da30538c8541c01e6b17fa3b518f80ab4f400621bb175c
                    checksum/dashboards-json-config: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
                    checksum/sc-dashboard-provider-config: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
                    checksum/secret: 416bf8ba1672c41e905574cab63bd1f658e30bc29309dcb7e68effdfbcb989f6
  Service Account:  loki-grafana
  Init Containers:
   grafana-sc-datasources:
    Image:      kiwigrid/k8s-sidecar:0.1.209
    Port:       <none>
    Host Port:  <none>
    Environment:
      METHOD:    LIST
      LABEL:     grafana_datasource
      FOLDER:    /etc/grafana/provisioning/datasources
      RESOURCE:  both
    Mounts:
      /etc/grafana/provisioning/datasources from sc-datasources-volume (rw)
  Containers:
   grafana:
    Image:       grafana/grafana:6.7.0
    Ports:       80/TCP, 3000/TCP
    Host Ports:  0/TCP, 0/TCP
    Liveness:    http-get http://:3000/api/health delay=60s timeout=30s period=10s #success=1 #failure=10
    Readiness:   http-get http://:3000/api/health delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:
      GF_SECURITY_ADMIN_USER:      <set to the key 'admin-user' in secret 'loki-grafana'>      Optional: false
      GF_SECURITY_ADMIN_PASSWORD:  <set to the key 'admin-password' in secret 'loki-grafana'>  Optional: false
    Mounts:
      /etc/grafana/grafana.ini from config (rw,path="grafana.ini")
      /etc/grafana/provisioning/datasources from sc-datasources-volume (rw)
      /var/lib/grafana from storage (rw)
  Volumes:
   config:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      loki-grafana
    Optional:  false
   storage:
    Type:       EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:     
    SizeLimit:  <unset>
   sc-datasources-volume:
    Type:       EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium:     
    SizeLimit:  <unset>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  loki-grafana-b664d6c4f (1/1 replicas created)
NewReplicaSet:   <none>
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  16m   deployment-controller  Scaled up replica set loki-grafana-b664d6c4f to 1

接下來,訪問 Grafana UI 界面來查看部署結果。首先,通過以下命令獲取 Grafana 管理員的密碼:

$ kubectl get secret --namespace loki loki-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

然後通過以下命令轉發 Grafana 的接口,以便通過 Web UI 進行訪問。默認情況下,端口轉發的地址 localhost,可以根據 kubectl 所在實例的情況補充設置–address <IP 地址>。

$ kubectl port-forward --namespace loki service/loki-grafana 3000:80

打開 localhost:3000 或者 <IP 地址>:3000 來查看 Grafana,登錄過程輸入用戶名 admin 和上面獲得到的密碼。

成功登錄後可以正常進入到 Grafana 首頁,如下圖所示。

默認 Loki 數據源(http://loki:3100)已經添加進去了。

在 Grafana 側邊欄選擇 Explore 進行快速日誌查看,進入到 Explore 頁面後選擇 Loki 數據源,然後選擇 Logs 標籤,最後在 Logs Labels 中輸入標籤的查詢條件,例如 {namespace=”loki”},執行查詢後就可以看到類似下圖中的日誌信息。

上面的日誌信息是通過默認部署的 Daemon Set 的 Promtail 收集到的日誌。

默認情況下,Loki 的索引存儲是通過 boltdb-shipper 來實現的,關於 boltdb-shipper 的更多信息請參考官方文檔 Single Store Loki (boltdb-shipper index type)(https://grafana.com/docs/loki/latest/operations/storage/boltdb-shipper/)。這些配置是通過 secret(內容爲 loki.yaml 的詳細配置)掛載到 Pod 中的,查看 Loki 配置文件的默認值:

$ kubectl get secrets loki -n loki -o "jsonpath={.data['loki\.yaml']}" | base64 -d

auth_enabled: false
chunk_store_config:
  max_look_back_period: 0s
compactor:
  shared_store: filesystem
  working_directory: /data/loki/boltdb-shipper-compactor
ingester:
  chunk_block_size: 262144
  chunk_idle_period: 3m
  chunk_retain_period: 1m
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  max_transfer_retries: 0
limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h
schema_config:
  configs:
  - from: "2020-10-24"
    index:
      period: 24h
      prefix: index_
    object_store: filesystem
    schema: v11
    store: boltdb-shipper
server:
  http_listen_port: 3100
storage_config:
  boltdb_shipper:
    active_index_directory: /data/loki/boltdb-shipper-active
    cache_location: /data/loki/boltdb-shipper-cache
    cache_ttl: 24h
    shared_store: filesystem
  filesystem:
    directory: /data/loki/chunks
table_manager:
  retention_deletes_enabled: false
  retention_period: 0s

其中 store: boltdb-shipper 和 object_store: filesystem 分別指定了使用 boltdb-shipper 和文件系統來作爲索引和日誌文件的存儲,這些都需要額外的維護,因爲 Loki 實現了計算存儲分離,所以這裏可以充分藉助雲上的資源來減輕運維管理的負擔,在亞馬遜雲平臺上可以使用Amazon DynamoDB(https://aws.amazon.com/dynamodb) 作爲索引實現快速的鍵值存儲的讀寫,使用Amazon S3(https://aws.amazon.com/s3) 作爲日誌存儲實現大規模日誌存儲,同時也具備極高的存儲性價比,下面將演示這些內容的配置。

2) 使用 DynamoDB 作爲索引,S3 作爲日誌存儲

首先,節點要操作 DynamoDB 和 S3 就需要有足夠的 IAM 權限:

具體權限請參考官方文檔Loki Storage(https://grafana.com/docs/loki/latest/operations/storage/) 爲 EKS 的節點配置相應權限。

接下來,要想真正使用 DynamoDB 作爲 Loki 的索引存儲、S3 作爲日誌存儲,需要配置 loki.yaml 文件,這裏可以修改 secret 文件,也可以配置新的 configmap 來掛載到 Pod 上。無論採用哪一種方式,主要的修改內容爲 schema_config 和 storage_config,具體配置如下所示:

schema_config:
  configs:
  - from: "2020-10-24"
    index:
      period: 0
      prefix: loki_index
    object_store: s3
    schema: v11
    store: aws
server:
  http_listen_port: 3100
storage_config:
  aws:
    s3: s3://us-east-1/loki-shtian
    dynamodb:
      dynamodb_url: dynamodb://us-east-1

其中,schema_config 中的 store: aws 設置指定索引存儲,object_store: s3 設置指定日誌存儲,需要注意的是 period 的值需要設置爲 0,否則 Loki 將會爲每個時間段的日誌都創建出單獨的索引表,設置爲 0 可以保證只有一個 DynamaDB 表被創建出來,存儲所有索引信息。prefix 爲我們指定的 DynamoDB 表的名稱。

存儲配置 storage_config 中分別填寫了 DynamaDB 和 S3 的相關信息,這裏的 S3 存儲桶以之前創建的 loki-shtian 爲例,請根據實際情況進行調整,示例選擇的區域以美東區(us-east-1)爲例。其他配置保持默認不變。

完成上述配置編寫後,前文提到既可以通過修改 secret 對象 loki 來生效,也可以使用 configmap 單獨配置掛載,這裏以更新 secrets 對象爲例,通過以下命令更新 secret 對象(假設當前路徑下有配置好的 loki.yaml 文件):

$ kubectl -n loki create secret generic loki --from-file=./loki.yaml -o yaml --dry-run=client | kubectl apply -f -

之後,通過以下命令重啓 statefulset 中的 Pod:

$ kubectl -n loki rollout restart statefulset loki

查看 Pod 日誌信息,如下所示,可以看到 Loki 會自動創建 DynamoDB 表 loki_index,並按照默認的參數配置 DynamoDB 的 WCU(1000)和 RCU 值(300),這些都可以參考官方文檔Configuring Loki(https://grafana.com/docs/loki/latest/configuration/) 進行定製化配置。

$ kubectl -n loki logs -f loki-0

level=info ts=2021-05-13T15:17:41.673886077Z caller=table_manager.go:476 msg="creating table" table=loki_index
level=info ts=2021-05-13T15:19:41.603526262Z caller=table_manager.go:324 msg="synching tables" expected_tables=1
level=info ts=2021-05-13T15:19:42.627187815Z caller=table_manager.go:531 msg="provisioned throughput on table, skipping" table=loki_index read=300 write=1000
level=info ts=2021-05-13T15:21:41.603525185Z caller=table_manager.go:324 msg="synching tables" expected_tables=1
level=info ts=2021-05-13T15:21:42.623189111Z caller=table_manager.go:531 msg="provisioned throughput on table, skipping" table=loki_index read=300 write=1000

關於 DynamaDB 和 S3 的配置示例可以參考官方文檔 Loki Configuration Examples,詳細的配置信息可以參考官方文檔 Configuring Loki。配置後的 DynamoDB 表使用 h 作爲分區鍵,使用 r 作爲排序鍵,如下圖所示:

根據日誌中的信息可以看到 DynamoDB 的 WCU 和 RCU 值配置爲 1000 和 300,如下圖所示:

DynamoDB 表使用 c 作爲索引的內容列,如下圖所示:

查看 S3 中的日誌數據,如下圖所示:

img

再次查看 Grafana 界面,查詢日誌信息一切正常運行。

小結

本文首先簡單介紹了經典的日誌系統 ELK/EFK 架構,引出了 Grafana 新推出的 PLG 架構,並探討了兩種架構之間的對比和重點發展的方向。然後,本文介紹了在亞馬遜雲平臺的 EKS 服務上部署 Promtail + Loki + Grafana 解決方案,以及配置使用 Amazon DynamoDB 和 Amazon S3,以充分藉助雲服務的高性價比優勢,降低用戶維護管理成本。

由於篇幅有限,關於 Loki 的詳細架構介紹和更多高級功能(如多租戶)和高級配置(如 DynamoDB 詳細配置)都沒有展開,希望有機會會再進行討論。關於和 Prometheus 共同部署的方案也是用戶考慮使用 PLG 的重要因素,以此實現整體的可觀測性解決方案,用戶可以結合實際情況進行配合使用。

此外,亞馬遜雲科技也提供了 Grafana 和 Prometheus 的託管服務 Amazon Managed Service for Grafana(AMG)和 Amazon Managed Service for Prometheus(AMP),可以非常方便地與其他雲服務快速集成,使用戶可以輕鬆地可視化和分析規模的運營數據以及大規模監控容器化的應用程序。

參考資料

原文鏈接:https://aws.amazon.com/cn/blogs/china/from-elk-efk-to-plg-implement-in-eks-a-container-logging-solution-based-on-promtail-loki-grafana/

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