quick understanding Loki for k8s

2021-03-09 16:17:16

什么是Loki

Loki是一个水平可扩展,高可用性,多租户的日志聚合系统,受到Prometheus的启发。它的设计非常经济高效且易于操作,因为它不会为日志内容编制索引,而是为每个日志流编制一组标签。官方介绍说到:Like Prometheus, but for logs.

Loki由3个组成部分组成:

  • loki 是主服务器,负责存储日志和处理查询。
  • promtail 是代理,负责收集日志并将其发送给loki。
  • 用户界面的Grafana。

Loki 的架构如下:
Loki
Loki 的架构非常简单,使用了和 Prometheus 一样的标签来作为索引

Loki 将使用与 Prometheus 相同的服务发现和标签重新标记库,编写了 Pormtail,在 Kubernetes 中 Promtail 以 DaemonSet 方式运行在每个节点中,通过 Kubernetes API 等到日志的正确元数据,并将它们发送到 Loki

日志的存储架构:
Log Storage

日志数据的写主要依托的是 Distributor 和 Ingester 两个组件,整体的流程如下:

  • Promtail 收集日志并将其发送给 Loki,Distributor 就是第一个接收日志的组件
    如果日志的写入量很大,所以不能在传入时写入数据库,Loki 通过构建压缩数据块来实现,方法是在日志进入时对其进行 Gzip 操作,组件 Ingester 是一个有状态的组件,负责构建和刷新 Chunck,当 Chunk 达到一定的数量或者时间后,刷新到存储中去
    每个流的日志对应一个 Ingester,当日志到达 Distributor 后,根据元数据和 Hash 算法计算出应该到哪个 Ingester 上面。
    Ingester 组件是一个负责构建压缩数据库的有状态组件。我们使用了多个 Ingester,属于每个流的日志应该始终在同一个 Ingester 中,这样所有相关条目就会被压缩在同一个数据块中。我们创建了一个 Ingester 环,并使用了一致性散列。当一个条目进入,Distributor 对日志的标签进行散列,然后根据散列值将条目发送给相应的 Ingester
    Distributor

    此外,为了获得冗余和弹性,我们复制了 n(默认为 3)个副本。

  • Ingester 接收到日志并开始构建 Chunk:
    基本上就是将日志进行压缩并附加到 Chunk 上面。一旦 Chunk”填满”(数据达到一定数量或者过了一定期限),Ingester 将其刷新到数据库
    刷新一个 Chunk 之后,Ingester 然后创建一个新的空 Chunk 并将新条目添加到该 Chunk 中

  • Querier
    Querier 负责给定一个时间范围和标签选择器,Querier 查看索引以确定哪些块匹配,并通过 greps 将结果显示出来。它还从 Ingester 获取尚未刷新的最新数据
    Querier

loki qury example

对于查询表达式的标签部分,将其用大括号括起来{},然后使用键值语法选择标签。多个标签表达式用逗号分隔:
= 完全相等。
!= 不相等。
=~ 正则表达式匹配。
!~ 不进行正则表达式匹配。

根据任务名称来查找日志

{job=”xiaoke/svc-job-admin”}
{job=”kube-system/kube-controller-manager”}
{job=”nginx-ingress/nginx-ingress”}
{namespace=”kube-system”,container=”kuboard”}

最佳实践

  • 使用静态标签
    物理机:kubernetes/hosts
    应用名:kubernetes/labels/app_kubernetes_io/name
    组件名:kubernetes/labels/name
    命名空间:kubernetes/namespace
    其他kubernetes/label/* 的静态标签,如环境、版本等信息

  • 谨慎使用动态标签
    过多的标签组合会造成大量的流,它会让Loki存储大量的索引和小块的对象文件。这些都会显著消耗Loki的查询性能。为避免这些问题,在你知道需要之前不要添加标签!loki的优势在于并行查询,使用过滤器表达式( lable = “text”, |~ “regex”, …)来查询日志会更有效,并且速度也很快

可扩展性

  • 我们将数据块放入对象存储中,它是可扩展的。
  • 我们将索引放入 Cassandra/Bigtable/DynamoDB,也是可扩展的。
  • Distributor 和 Querier 是可以水平扩展的无状态组件。

部署Loki

1
2
3
4
5
6
7
8
9
10
11
# 增加源并更新
$ helm repo add loki https://grafana.github.io/loki/charts
$ helm repo update
# 拉取 chart
$ helm fetch loki/loki-stack --untar --untardir .
$ cd loki-stack
# 将 values.yaml 中的 grafana.enable 改成 true, 因为我们需要部署 grafana
# 生成 k8s 配置
$ helm template loki . > loki.yaml
# 部署
$ kubectl apply -f loki.yaml

loki数据持久化

  • 使用共享存储nfs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60

    $ kubectl get statefulset loki -o yaml |grep -C 10 volumes
    --
    restartPolicy: Always
    schedulerName: default-scheduler
    securityContext:
    fsGroup: 10001
    runAsGroup: 10001
    runAsNonRoot: true
    runAsUser: 10001
    serviceAccount: loki
    serviceAccountName: loki
    terminationGracePeriodSeconds: 4800
    volumes:
    - name: config
    secret:
    defaultMode: 420
    secretName: loki
    - name: storage
    emptyDir: {}
    updateStrategy:
    type: RollingUpdate
    status:

    这边看到是emptDir, 没有做storage。
    首先, 我们创建一个pvc.我这边是基于nfs存储建立的, 如果是ceph或者其他分布式存储, 原理是一样的。

    $ cat loki-strorage.yaml
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: loki
    namespace: default
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 200Gi
    storageClassName: nfs-23

    $ kubectl apply -f loki-strorage.yaml
    其次 ,将我们上面的statefulset/loki保存为yaml文件。

    $ kubectl get statefulset loki -o yaml >> loki-sf.yaml
    $ vim loki-sf.yaml
    ...
    volumes:
    - name: config
    secret:
    defaultMode: 420
    secretName: loki
    - name: storage
    persistentVolumeClaim: ## 将emtypDir改成pvc, 名称是上面创建的。
    claimName: loki

    ...

    $ kubectl apply -f loki-sf.yaml
  • 使用hostPath volume
    想让promtail pod收集到我们pod的日志,先要让promtail 读取到日志,我们先让pod挂载相同的宿主机hostPath volume.我们查看loki promtail 部分的k8s配置文件,可以看到promtail挂载了宿主机/var/log/pods 目录作为volume,pod标准流输出会被存储到这里
    假设我们选择/mnt/log作为我们应用日志文件挂载目录,这样就可以减少promtail pod挂载的volume 数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

kind: DaemonSet
metadata:
name: loki-promtail
...
volumeMounts:
...
- mountPath: /var/log/pods
name: pods
readOnly: true
- mountPath: /mnt/log
name: custom
readOnly: true
...
volumes:
...
- hostPath:
path: /var/log/pods
name: pods
- hostPath:
path: /mnt/log
type: DirectoryOrCreate # 目录不存在会自动创建
name: custom

我们应用 pod 也需要挂载这个 hostPath 下的目录作为日志输出目录

1
2
3
4
5
6
7
8
9
10

...
volumeMounts:
- mountPath: /var/log/custom/winston
name: log
...
volumes:
- name: log
hostPath:
path: /mnt/log/winston

接着就只剩下增加 promtail 配置, 使得我们的日志也能够被收集

我们通过kubernetes-discovery进行配置,需要先了解一下relabeling. 简单概括下, 就是使用 k8s node, pod, service 的一些 label 或者 annotation 信息, 来生成 promtail 配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

- job_name: kubernetes-pods-name
pipeline_stages:
- docker: {}
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels:
- __meta_kubernetes_pod_label_name
target_label: __service__
- action: drop
regex: ''
source_labels:
- __service__
...
- replacement: /var/log/pods/*$1/*.log
separator: /
source_labels:
- __meta_kubernetes_pod_uid
- __meta_kubernetes_pod_container_name
target_label: __path__

#关注 action: drop 和 target_label: __path__ 这两部分,
action: drop 表示,如果目标 pod 没有 __service__ 这个 label 就不收集这个 pod 的日志,
__service__ 其实就是 __meta_kubernetes_pod_label_name
最终就是 pod config 里面的 metadata.labels.name 的值;
target_label: __path__ 这个是告诉 promtail 这个 pod 对应的日志文件路径,
最终路径为 /var/log/pods/*<pod_uid>/<container_name>/*.log.
这样我们就可以动态配置 promtail 了.

对于我们要收集文件的 pod 我们可以配置一个 annotation, 例如: loki.io/logfile:

  • 告诉 promtail 该收集哪些日志文件
  • 忽略掉没有这条 annotation 的 pod

所以我们可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- job_name: kubernetes-pods-custom
pipeline_stages:
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 忽略掉没有 loki.io/logfile annotation 的 pod
- action: drop
regex: ''
source_labels:
- __meta_kubernetes_pod_annotation_loki_io_logfile
...
# 直接使用 loki.io/logfile annotation 的值作为改 pod 日志文件路径
- action: replace
regex: (.+)
source_labels:
- __meta_kubernetes_pod_annotation_loki_io_logfile
target_label: __path__

1
2
3
4
# 输出 grafana 登录密码
$ kubectl get secret --namespace default loki-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
# port forward 让我们能够访问 grafana service
$ kubectl port-forward --namespace default service/loki-grafana 3000:80

这里打开 http://localhost:3000 进入 grafana 界面(用户名使用 admin), 点击 Explore 并且选择 label 就可以查看日志了.


loki