简介
项目地址 官方文档
Grafana Loki
是一个水平可扩展,高可用性,多租户的日志聚合系统,Loki 是基于仅索引有关日志元数据的想法而构建的:标签(就像 Prometheus 标签一样)。日志数据本身被压缩然后并存储在对象存储(例如 S3 或 GCS)的块中,甚至存储在本地文件系统上,轻量级的索引和高度压缩的块简化了操作,并显著降低了 Loki 的成本,Loki 更适合中小团队。由于 Loki 使用和 Prometheus 类似的标签概念,所以如果你熟悉 Prometheus 那么将很容易上手,也可以直接和 Grafana 集成,只需要添加 Loki 数据源就可以开始查询日志数据了。
Loki 还提供了一个专门用于日志查询的 LogQL
查询语句,类似于 PromQL
,通过 LogQL 我们可以很容易查询到需要的日志,也可以很轻松获取监控指标。Loki 还能够将 LogQL 查询直接转换为 Prometheus 指标。此外 Loki 允许我们定义有关 LogQL 指标的报警,并可以将它们和 Alertmanager 进行对接。
Grafana Loki 主要由 3 部分组成:
loki
: 日志记录引擎,负责存储日志和处理查询promtail
: 代理,负责收集日志并将其发送给 lokigrafana
: UI 界面
整体架构
在 Loki 架构中有以下几个概念:
- Grafana:相当于 EFK 中的 Kibana ,用于 UI 的展示。
- Loki:相当于 EFK 中的 ElasticSearch ,用于存储日志和处理查询。
- Promtail:相当于 EFK 中的 Filebeat/Fluentd ,用于采集日志并将其发送给 Loki 。
- LogQL:Loki 提供的日志查询语言,类似 Prometheus 的 PromQL,而且 Loki 支持 LogQL 查询直接转换为 Prometheus 指标。
运行模式
Loki 针对本地运行(或小规模运行)和水平扩展进行了优化,Loki 带有单一进程模式,可在一个进程中运行所有必需的微服务。单进程模式非常适合测试 Loki 或以小规模运行。为了实现水平可伸缩性,可以将 Loki 的服务拆分为单独的组件,从而使它们彼此独立地扩展。每个组件都产生一个用于内部请求的 gRPC 服务器和一个用于外部 API 请求的 HTTP 服务,所有组件都带有 HTTP 服务器,但是大多数只暴露就绪接口、运行状况和指标端点。
Loki 运行哪个组件取决于命令行中的 -target
标志或 Loki 的配置文件中的 target:<string>
配置。 当 target 的值为 all
时,Loki 将在单进程中运行其所有组件。,这称为单进程
或单体模式
。 使用 Helm 安装 Loki 时,单体模式是默认部署方式。
当 target 未设置为 all(即被设置为 querier
、ingester
、query-frontend
或 distributor
),则可以说 Loki 在水平伸缩
或微服务模式
下运行。
Loki 的每个组件,例如 ingester
和 distributors
都使用 Loki 配置中定义的 gRPC 监听端口通过 gRPC 相互通信。当以单体模式运行组件时,仍然是这样的,尽管每个组件都以相同的进程运行,但它们仍将通过本地网络相互连接进行组件之间的通信。
单体模式非常适合于本地开发、小规模等场景,单体模式可以通过多个进程进行扩展,但有以下限制:
- 当运行带有多个副本的单体模式时,当前无法使用本地索引和本地存储,因为每个副本必须能够访问相同的存储后端,并且本地存储对于并发访问并不安全。
- 各个组件无法独立缩放,因此读取组件的数量不能超过写入组件的数量。
组件
Distributor
distributor
服务负责处理客户端写入的日志,它本质上是日志数据写入路径中的第一站,一旦 distributor
收到日志数据,会将其拆分为多个批次,然后并行发送给多个 ingester
。distributor
通过 gRPC 与 ingester
通信,它们都是无状态的,所以可以根据需要扩大或缩小规模。
Hashing
distributor
将一致性 Hash和可配置的复制因子结合使用,以确定 ingester
服务的哪些实例应该接收指定的数据流。
流是一组与租户和唯一标签集关联的日志,使用租户 ID 和标签集对流进行 hash 处理,然后使用哈希查询要发送流的 ingester
。
存储在 Consul/Etcd 中的哈希环被用来实现一致性哈希,所有的 ingester
都会使用自己拥有的一组 Token 注册到哈希环中,每个 Token 是一个随机的无符号 32 位数字,与一组 Token 一起,ingester
将其状态注册到哈希环中,状态 JOINING
和 ACTIVE
都可以接收写请求,而 ACTIVE
和 LEAVING
的 ingester
可以接收读请求。在进行哈希查询时,distributor
只使用处于请求的适当状态的 ingester 的 Token。
为了进行哈希查找,distributor
找到最小合适的 Token,其值大于日志流的哈希值,当复制因子大于 1 时,属于不同 ingester
的下一个后续 Token(在环中顺时针方向)也将被包括在结果中。
这种哈希配置的效果是,一个 ingester
拥有的每个 Token 都负责一个范围的哈希值,如果有三个值为 0、25 和 50 的 Token,那么 3 的哈希值将被给予拥有 25 这个 Token 的 ingester
,拥有 25 这个 Token 的 ingester
负责1-25
的哈希值范围。
Ingester
ingester
负责接收 distributor
发送过来的日志数据,存储日志的索引数据以及内容数据。此外 ingester
会验证摄取的日志行是否按照时间戳递增的顺序接收的(即每条日志的时间戳都比前面的日志晚一些),当 ingester
收到不符合这个顺序的日志时,该日志行会被拒绝并返回一个错误。
- 如果传入的行与之前收到的行完全匹配(与之前的时间戳和日志文本都匹配),传入的行将被视为完全重复并被忽略。
- 如果传入的行与前一行的时间戳相同,但内容不同,则接受该日志行,表示同一时间戳有两个不同的日志行是可能的。
来自每个唯一标签集的日志在内存中被建立成 chunks(块)
,然后可以根据配置的时间间隔刷新到支持的后端存储。在下列情况下,块被压缩并标记为只读:
- 当前块容量已满(该值可配置)
- 过了太长时间没有更新当前块的内容
- 刷新了
每当一个数据块被压缩并标记为只读时,一个可写的数据块就会取代它。如果一个 ingester
进程崩溃或突然退出,所有尚未刷新的数据都会丢失,Loki 通常配置为多个副本来降低这种风险。
当向持久存储刷新时,该块将根据其租户、标签和内容进行哈希处理,这意味着具有相同数据副本的多个 ingester
实例不会将相同的数据两次写入备份存储中,但如果对其中一个副本的写入失败,则会在备份存储中创建多个不同的块对象。
WAL
上面我们提到了 ingester
将数据临时存储在内存中,如果发生了崩溃,可能会导致数据丢失,而 WAL
就可以帮助我们来提高这方面的可靠性。
在计算机领域,WAL(Write-ahead logging,预写式日志)是数据库系统提供原子性和持久化的一系列技术。
在使用 WAL 的系统中,所有的修改都先被写入到日志中,然后再被应用到系统状态中。通常包含 redo 和 undo 两部分信息。为什么需要使用 WAL,然后包含 redo 和 undo 信息呢?举个例子,如果一个系统直接将变更应用到系统状态中,那么在机器断电重启之后系统需要知道操作是成功了,还是只有部分成功或者是失败了(为了恢复状态)。如果使用了 WAL,那么在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是撤销操作。
redo log
称为重做日志,每当有操作时,在数据变更之前将操作写入 redo log
,这样当发生断电之类的情况时系统可以在重启后继续操作。undo log
称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。
Loki 中的 WAL 记录了传入的数据,并将其存储在本地文件系统中,以保证在进程崩溃的情况下持久保存已确认的数据。重新启动后,Loki 将重放日志中的所有数据,然后将自身注册,准备进行后续写操作。这使得 Loki 能够保持在内存中缓冲数据的性能和成本优势,以及持久性优势(一旦写被确认,它就不会丢失数据)。
Querier
Querier
接收日志数据查询、聚合统计请求,使用 LogQL 查询语言处理查询,从 ingester
和长期存储中获取日志。
查询器查询所有 ingester
的内存数据,然后再到后端存储运行相同的查询。由于复制因子,查询器有可能会收到重复的数据。为了解决这个问题,查询器在内部对具有相同纳秒时间戳、标签集和日志信息的数据进行重复数据删除。
Query Frontend
Query Frontend
查询前端是一个可选的服务,可以用来加速读取路径。当查询前端就位时,将传入的查询请求定向到查询前端,而不是 querier
, 为了执行实际的查询,群集中仍需要 querier
服务。
查询前端在内部执行一些查询调整,并在内部队列中保存查询。querier
作为 workers 从队列中提取作业,执行它们,并将它们返回到查询前端进行汇总。querier
需要配置查询前端地址,以便允许它们连接到查询前端。
查询前端是无状态的,然而,由于内部队列的工作方式,建议运行几个查询前台的副本,以获得公平调度的好处,在大多数情况下,两个副本应该足够了。
队列
查询前端的排队机制用于:
- 确保可能导致
querier
出现内存不足(OOM)错误的查询在失败时被重试。这样管理员就可以为查询提供稍低的内存,或者并行运行更多的小型查询,这有助于降低总成本。 - 通过使用先进先出队列(FIFO)将多个大型请求分配到所有
querier
上,以防止在单个 querier
中进行多个大型请求。 - 通过在租户之间公平调度查询。
分割
查询前端将较大的查询分割成多个较小的查询,在下游 querier
上并行执行这些查询,并将结果再次拼接起来。这可以防止大型查询在单个查询器中造成内存不足的问题,并有助于更快地执行这些查询。
缓存
查询前端支持缓存查询结果,并在后续查询中重复使用。如果缓存的结果不完整,查询前端会计算所需的子查询,并在下游 querier
上并行执行这些子查询。查询前端可以选择将查询与其 step
参数对齐,以提高查询结果的可缓存性。
读取路径
日志读取路径的流程如下所示:
- 查询器收到一个对数据的 HTTP 请求。
- 查询器将查询传递给所有
ingester
。 ingester
收到读取请求,并返回与查询相匹配的数据。- 如果没有
ingester
返回数据,查询器会从后端存储加载数据,并对其运行查询。 - 查询器对所有收到的数据进行迭代和重复计算,通过 HTTP 连接返回最后一组数据。
写入路径
整体的日志写入路径如下所示:
distributor
收到一个 HTTP 请求,以存储流的数据。- 每个流都使用哈希环进行哈希操作。
distributor
将每个流发送到合适的 ingester
和他们的副本(基于配置的复制因子)。- 每个
ingester
将为日志流数据创建一个块或附加到一个现有的块上。每个租户和每个标签集的块是唯一的。
Promtail
官方文档
promtail 是 loki 架构中最常用的采集器, 相当于 EFK 中的 filebeat/fluentd
它的主要工作流程:
- 使用 fsnotify 监听指定目录下(例如:/var/log/*.log)的文件创建与删除
- 对每个活跃的日志文件起一个 goroutine 进行类似 tail -f 的读取,读取到的内容发送给 channel
- 有一个单独的 goroutine 会读取 channel 中的日志行,分批并附加上标签后推送给 Loki
安装部署
这次部署的 loki 整体架构如下, loki 使用 StatefulSet
的方式运行, Promtail 以 DaemonSet
的方式运行在 k8s 集群的每个节点。
Promtail部署
1 2 3 4 5
| apiVersion: v1 kind: Namespace metadata: name: logging
|
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
| apiVersion: v1 kind: ServiceAccount metadata: name: loki-promtail labels: app: promtail namespace: logging
--- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: labels: app: promtail name: promtail-clusterrole namespace: logging rules: - apiGroups: [""] resources: - nodes - nodes/proxy - services - endpoints - pods verbs: ["get", "watch", "list"]
--- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: promtail-clusterrolebinding labels: app: promtail namespace: logging subjects: - kind: ServiceAccount name: loki-promtail namespace: logging roleRef: kind: ClusterRole name: promtail-clusterrole apiGroup: rbac.authorization.k8s.io
|
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
| apiVersion: v1 kind: ConfigMap metadata: name: loki-promtail namespace: logging labels: app: promtail data: promtail.yaml: | client: # 配置Promtail如何连接到Loki的实例 backoff_config: # 配置当请求失败时如何重试请求给Loki max_period: 1s max_retries: 20 min_period: 500ms batchsize: 10240 # 发送给Loki的最大批次大小(以字节为单位) batchwait: 2s # 发送批处理前等待的最大时间(即使批次大小未达到最大值) external_labels: {} # 所有发送给Loki的日志添加静态标签 timeout: 15s # 等待服务器响应请求的最大时间 positions: filename: /run/promtail/positions.yaml server: http_listen_port: 3101 target_config: sync_period: 10s scrape_configs: - 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__ - source_labels: - __meta_kubernetes_pod_node_name target_label: __host__ - action: drop regex: '' source_labels: - __service__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - action: replace replacement: $1 separator: / source_labels: - __meta_kubernetes_namespace - __service__ target_label: job - action: replace source_labels: - __meta_kubernetes_namespace target_label: namespace - action: replace source_labels: - __meta_kubernetes_pod_name target_label: pod - action: replace source_labels: - __meta_kubernetes_pod_container_name target_label: container - replacement: /var/log/pods/*$1/*.log separator: / source_labels: - __meta_kubernetes_pod_uid - __meta_kubernetes_pod_container_name target_label: __path__ - job_name: kubernetes-pods-app pipeline_stages: - docker: {} kubernetes_sd_configs: - role: pod relabel_configs: - action: drop regex: .+ source_labels: - __meta_kubernetes_pod_label_name - source_labels: - __meta_kubernetes_pod_label_app target_label: __service__ - source_labels: - __meta_kubernetes_pod_node_name target_label: __host__ - action: drop regex: '' source_labels: - __service__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - action: replace replacement: $1 separator: / source_labels: - __meta_kubernetes_namespace - __service__ target_label: job - action: replace source_labels: - __meta_kubernetes_namespace target_label: namespace - action: replace source_labels: - __meta_kubernetes_pod_name target_label: pod - action: replace source_labels: - __meta_kubernetes_pod_container_name target_label: container - replacement: /var/log/pods/*$1/*.log separator: / source_labels: - __meta_kubernetes_pod_uid - __meta_kubernetes_pod_container_name target_label: __path__ - job_name: kubernetes-pods-direct-controllers pipeline_stages: - docker: {} kubernetes_sd_configs: - role: pod relabel_configs: - action: drop regex: .+ separator: '' source_labels: - __meta_kubernetes_pod_label_name - __meta_kubernetes_pod_label_app - action: drop regex: '[0-9a-z-.]+-[0-9a-f]{8,10}' source_labels: - __meta_kubernetes_pod_controller_name - source_labels: - __meta_kubernetes_pod_controller_name target_label: __service__ - source_labels: - __meta_kubernetes_pod_node_name target_label: __host__ - action: drop regex: '' source_labels: - __service__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - action: replace replacement: $1 separator: / source_labels: - __meta_kubernetes_namespace - __service__ target_label: job - action: replace source_labels: - __meta_kubernetes_namespace target_label: namespace - action: replace source_labels: - __meta_kubernetes_pod_name target_label: pod - action: replace source_labels: - __meta_kubernetes_pod_container_name target_label: container - replacement: /var/log/pods/*$1/*.log separator: / source_labels: - __meta_kubernetes_pod_uid - __meta_kubernetes_pod_container_name target_label: __path__ - job_name: kubernetes-pods-indirect-controller pipeline_stages: - docker: {} kubernetes_sd_configs: - role: pod relabel_configs: - action: drop regex: .+ separator: '' source_labels: - __meta_kubernetes_pod_label_name - __meta_kubernetes_pod_label_app - action: keep regex: '[0-9a-z-.]+-[0-9a-f]{8,10}' source_labels: - __meta_kubernetes_pod_controller_name - action: replace regex: '([0-9a-z-.]+)-[0-9a-f]{8,10}' source_labels: - __meta_kubernetes_pod_controller_name target_label: __service__ - source_labels: - __meta_kubernetes_pod_node_name target_label: __host__ - action: drop regex: '' source_labels: - __service__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - action: replace replacement: $1 separator: / source_labels: - __meta_kubernetes_namespace - __service__ target_label: job - action: replace source_labels: - __meta_kubernetes_namespace target_label: namespace - action: replace source_labels: - __meta_kubernetes_pod_name target_label: pod - action: replace source_labels: - __meta_kubernetes_pod_container_name target_label: container - replacement: /var/log/pods/*$1/*.log separator: / source_labels: - __meta_kubernetes_pod_uid - __meta_kubernetes_pod_container_name target_label: __path__ - job_name: kubernetes-pods-static pipeline_stages: - docker: {} kubernetes_sd_configs: - role: pod relabel_configs: - action: drop regex: '' source_labels: - __meta_kubernetes_pod_annotation_kubernetes_io_config_mirror - action: replace source_labels: - __meta_kubernetes_pod_label_component target_label: __service__ - source_labels: - __meta_kubernetes_pod_node_name target_label: __host__ - action: drop regex: '' source_labels: - __service__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - action: replace replacement: $1 separator: / source_labels: - __meta_kubernetes_namespace - __service__ target_label: job - action: replace source_labels: - __meta_kubernetes_namespace target_label: namespace - action: replace source_labels: - __meta_kubernetes_pod_name target_label: pod - action: replace source_labels: - __meta_kubernetes_pod_container_name target_label: container - replacement: /var/log/pods/*$1/*.log separator: / source_labels: - __meta_kubernetes_pod_annotation_kubernetes_io_config_mirror - __meta_kubernetes_pod_container_name target_label: __path__
|
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| apiVersion: apps/v1 kind: DaemonSet metadata: name: loki-promtail namespace: logging labels: app: promtail spec: selector: matchLabels: app: promtail updateStrategy: rollingUpdate: maxUnavailable: 1 type: RollingUpdate template: metadata: labels: app: promtail spec: serviceAccountName: loki-promtail containers: - name: promtail image: grafana/promtail:2.9.2 imagePullPolicy: IfNotPresent args: - -config.file=/etc/promtail/promtail.yaml - -client.url=http://loki:3100/loki/api/v1/push env: - name: HOSTNAME valueFrom: fieldRef: apiVersion: v1 fieldPath: spec.nodeName volumeMounts: - mountPath: /etc/promtail name: config - mountPath: /run/promtail name: run - mountPath: /data/docker/containers name: docker readOnly: true - mountPath: /var/log/pods name: pods readOnly: true ports: - containerPort: 3101 name: http protocol: TCP securityContext: readOnlyRootFilesystem: true runAsGroup: 0 runAsUser: 0 readinessProbe: failureThreshold: 5 httpGet: path: /ready port: http scheme: HTTP initialDelaySeconds: 10 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 tolerations: - operator: Exists volumes: - name: config configMap: defaultMode: 420 name: loki-promtail - name: run hostPath: path: /run/promtail type: "" - name: docker hostPath: path: /data/docker/containers - name: pods hostPath: path: /var/log/pods
|
scrape_config 配置详解
主要解释一下 promtail 中的匹配规则, 因为采集的日志可以说非常地杂乱, 如何将应用日志分类就尤为重要, 可以说匹配规则是 promtail
的核心所在
通常我们分类 pod 的手段基本为 namespace + labels + controller , 在 loki 中也一样, 在上述 configmap 的配置中将 k8s 中的所有 pod 分为了五类:
- 定义了 label_name
- 未定义 label_name, 定义了 label_app
- 未定义 label_name & label_app, 由 Daemonset 控制
- 未定义 label_name & label_app, 由非 Daemonset 控制
- 未定义 label_name & label_app, 由 kubelet 直接控制
对应上述 configmap 中配置的五个 job:
- kubernetes-pods-name
job=namespace/label_name
- kubernetes-pods-app
job=namespace/label_app
- kubernetes-pods-direct-controllers
job=namespace/controller
- kubernetes-pods-indirect-controllers
job=namespace/controller
- kubernetes-pods-static
job=namespace/label_component
每个指标数据将由上述规则分类, 添加一个 job
的 label
然后基于指标数据对应 pod 的所有 label 附加到指标数据上
1 2
| - action: labelmap regex: __meta_kubernetes_pod_label_(.+)
|
再加上指标数据本身携带的一些 label, 我们就可以对 pod 日志做一个十分细致的区分
Loki部署
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
| apiVersion: v1 kind: ServiceAccount metadata: name: loki namespace: logging --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: loki namespace: logging rules: - apiGroups: - extensions resourceNames: - loki resources: - podsecuritypolicies verbs: - use --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: loki namespace: logging roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: loki subjects: - kind: ServiceAccount name: loki
|
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 61 62 63 64 65 66 67 68 69 70
| apiVersion: v1 kind: ConfigMap metadata: name: loki namespace: logging labels: app: loki data: loki.yaml: | # 通过 X-Scope-OrgID Header 启用身份验证,如果为 true,该 Header 必须存在。 # 如果为 false,OrgID 将始终设置为 "fake"。 auth_enabled: false ingester: chunk_idle_period: 3m # 如果块没有达到最大的块大小,那么在刷新之前,块应该在内存中不更新多长时间 chunk_block_size: 65535 chunk_retain_period: 1m # 块刷新后应该在内存中保留多长时间 max_transfer_retries: 0 # Number of times to try and transfer chunks when leaving before falling back to flushing to the store. Zero = no transfers are done. lifecycler: # 配置ingester的生命周期,以及在哪里注册以进行发现 ring: kvstore: store: inmemory # 用于ring的后端存储,支持consul、etcd、inmemory replication_factor: 1 # 写入和读取的ingesters数量,至少为1(为了冗余和弹性,默认情况下为3) wal: # 配置日志预写 enabled: true dir: /data/wal limits_config: enforce_metric_name: false reject_old_samples: true # 旧样品是否会被拒绝 reject_old_samples_max_age: 8h # 拒绝旧样本的最大时限 schema_config: # 配置从特定时间段开始应该使用哪些索引模式 configs: - from: 2023-12-05 # 创建索引的日期。如果这是唯一的schema_config,则使用过去的日期,否则使用希望切换模式时的日期 store: boltdb-shipper # 索引使用哪个存储,如:cassandra, bigtable, dynamodb,或boltdb object_store: filesystem # 用于块的存储,如:gcs, s3, inmemory, filesystem, cassandra,如果省略,默认值与store相同 schema: v11 index: # 配置如何更新和存储索引 prefix: index_ # 所有周期表的前缀 period: 24h # 表周期 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 chunk_store_config: # 配置如何缓存块,以及在将它们保存到存储之前等待多长时间 max_look_back_period: 0s # 限制查询数据的时间,默认是禁用的,这个值应该小于或等于table_manager.retention_period中的值 table_manager: retention_deletes_enabled: true # 日志保留周期开关,用于表保留删除 retention_period: 48h # 日志保留周期,保留期必须是索引/块的倍数 compactor: working_directory: /data/loki/boltdb-shipper-compactor shared_store: filesystem # ruler: # 日志告警 # storage: # rules规则存储 # type: local # 主要支持本地存储(local)和对象文件系统(azure, gcs, s3, swift) # local: # directory: /etc/loki/rules # 告警文件存放目录 # rule_path: /data/loki/rules-temp # rules临时规则文件存储路径 # flush_period: 1m # rules规则加载时间 # alertmanager_url: http://alertmanager-main.monitoring.svc:9093 # alertmanager地址 # external_url: http://alertmanager.od.com # 外部访问 alertmanager # ring: # kvstore: # store: inmemory # enable_api: true # enable_alertmanager_v2: true
|
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
| kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: loki-storage labels: app: loki provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer --- apiVersion: v1 kind: PersistentVolume metadata: name: loki-local labels: app: loki spec: accessModes: - ReadWriteOnce capacity: storage: 10Gi storageClassName: loki-storage local: path: /data/volumes/loki nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - k8s-node1
|
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| apiVersion: apps/v1 kind: StatefulSet metadata: name: loki namespace: logging labels: app: loki spec: podManagementPolicy: OrderedReady replicas: 1 selector: matchLabels: app: loki serviceName: loki updateStrategy: type: RollingUpdate template: metadata: labels: app: loki spec: serviceAccountName: loki securityContext: fsGroup: 10001 runAsGroup: 10001 runAsNonRoot: true runAsUser: 10001 initContainers: - name: fix-permissions image: busybox:latest securityContext: privileged: true runAsGroup: 0 runAsNonRoot: false runAsUser: 0 command: - sh - -c - >- id; mkdir -p /data/loki; chown 10001:10001 /data -R; ls -la /data/ volumeMounts: - mountPath: /data name: storage containers: - name: loki image: grafana/loki:2.9.2 imagePullPolicy: IfNotPresent args: - -config.file=/etc/loki/config/loki.yaml volumeMounts: - name: config mountPath: /etc/loki/config/loki.yaml subPath: loki.yaml - name: storage mountPath: "/data" ports: - name: http-metrics containerPort: 3100 protocol: TCP livenessProbe: httpGet: path: /ready port: http-metrics scheme: HTTP initialDelaySeconds: 45 timeoutSeconds: 1 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: http-metrics scheme: HTTP initialDelaySeconds: 45 timeoutSeconds: 1 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 securityContext: readOnlyRootFilesystem: true terminationGracePeriodSeconds: 4800 volumes: - name: config configMap: defaultMode: 0640 name: loki volumeClaimTemplates: - metadata: name: storage labels: app: loki spec: storageClassName: "loki-storage" accessModes: - ReadWriteOnce resources: requests: storage: "2Gi"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| apiVersion: v1 kind: Service metadata: name: loki namespace: logging labels: app: loki spec: type: ClusterIP ports: - port: 3100 protocol: TCP name: http targetPort: http-metrics selector: app: loki
|
Grafana部署
grafana 部署请参考 Linkerd服务网格安装部署
配置
在 grafana 中添加 loki 作为 data source, 这里我的 grafana 是直接部署在 k8s 中的, 所以可以通过 <svc-name>.<namespace>
访问到 loki
在 Explore => loki => {job="kube-system/coredns"}
可以看到 k8s 的 coredns 相关日志
Dashboard 示例
Traefik
traefik 部署参考 traefik 系列文章
如下图所示, 已经可以看到收集到的 traefik 日志
我们还可以通过 dashboard 实时展示 traefik 的信息, 在 grafana 导入 13713 号模板
此 dashboard 默认的 traefik 的采集语句是 {job="/var/log/traefik.log"}
, 我们需要按照实际情况进行修改, 这里我改成了 {app="traefik-v2"}
,导入修改好的 json, 选择数据源
可以看到已经可以正常展示数据了
但是还有一个小报错, 是因为这个 dashboard 依赖 grafana-piechart-panel
这个插件, 我们在 grafana 容器内执行安装插件
1 2 3 4 5 6 7
| $ kubectl exec -it grafana-6d946f4d88-cw4b4 -n monitoring -- grafana cli plugins install grafana-piechart-panel ✔ Downloaded grafana-piechart-panel v1.6.4 zip successfully
Please restart Grafana after installing plugins. Refer to Grafana documentation for instructions if necessary.
$ kubectl rollout restart deployment -n monitoring grafana deployment.apps/grafana restarted
|
等待重建 pod, 可以看到这里已经可以正常显示了