写点什么

基于 EFK 的 Kubernetes 日志采集方案

作者:Albert Edison
  • 2022 年 8 月 14 日
    四川
  • 本文字数:16095 字

    阅读完需:约 53 分钟

基于EFK的Kubernetes日志采集方案

前言

在本篇文章中,您将学习 Kubernetes 集群日志中涉及的关键概念和工作流。


当涉及到 Kubernetes 生产调试时,日志起着至关重要的作用。它可以帮助你理解正在发生的事情,哪里出了问题,甚至是哪里可能出问题。


作为一名 DevOps 工程师,您应该清楚地了解 Kubernetes 日志以解决集群和应用程序问题。

k8s 日志收集架构

总体分为三种方式:

  • 使用在每个节点上运行的节点级日志记录代理。

  • 在应用程序的 pod 中,包含专门记录日志的 sidecar 容器。

  • 将日志直接从应用程序中推送到日志记录后端。

使用节点级日志代理


容器日志驱动:

https://docs.docker.com/config/containers/logging/configure/

查看当前的 docker 主机的驱动:

$ docker info --format '{{.LoggingDriver}}'
复制代码

json-file 格式,docker 会默认将标准和错误输出保存为宿主机的文件,路径为:

/var/lib/docker/containers/<container-id>/<container-id>-json.log

并且可以设置日志轮转:

{  "log-driver": "json-file",  "log-opts": {    "max-size": "10m",    "max-file": "3",    "labels": "production_status",    "env": "os,customer"  }}
复制代码

优势:

  • 部署方便,使用 DaemonSet 类型控制器来部署 agent 即可

  • 对业务应用的影响最小,没有侵入性

劣势:

  • 只能收集标准和错误输出,对于容器内的文件日志,暂时收集不到

使用 sidecar 容器和日志代理
  • 方式一:sidecar 容器将应用程序日志传送到自己的标准输出。

思路:在 pod 中启动一个 sidecar 容器,把容器内的日志文件吐到标准输出,由宿主机中的日志收集 agent 进行采集。


$ cat count-pod.yamlapiVersion: v1kind: Podmetadata:  name: counterspec:  containers:  - name: count    image: busybox    args:    - /bin/sh    - -c    - >      i=0;      while true;      do        echo "$i: $(date)" >> /var/log/1.log;        echo "$(date) INFO $i" >> /var/log/2.log;        i=$((i+1));        sleep 1;      done    volumeMounts:    - name: varlog      mountPath: /var/log  - name: count-log-1    image: busybox    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']    volumeMounts:    - name: varlog      mountPath: /var/log  - name: count-log-2    image: busybox    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log']    volumeMounts:    - name: varlog      mountPath: /var/log  volumes:  - name: varlog    emptyDir: {}    $ kubectl apply -f counter-pod.yaml$ kubectl logs -f counter -c count-log-1
复制代码

优势:

  • 可以实现容器内部日志收集

  • 对业务应用的侵入性不大

劣势:

  • 每个业务 pod 都需要做一次改造

  • 增加了一次日志的写入,对磁盘使用率有一定影响


  • 方式二:sidecar 容器运行一个日志代理,配置该日志代理以便从应用容器收集日志。

思路:直接在业务 Pod 中使用 sidecar 的方式启动一个日志收集的组件(比如 fluentd),这样日志收集可以将容器内的日志当成本地文件来进行收取。

优势:不用往宿主机存储日志,本地日志完全可以收集

劣势:每个业务应用额外启动一个日志 agent,带来额外的资源损耗

从应用中直接暴露日志目录
企业日志方案选型

目前来讲,最建议的是采用节点级的日志代理。

方案一:自研方案,实现一个自研的日志收集 agent,大致思路:

  • 针对容器的标准输出及错误输出,使用常规的方式,监听宿主机中的容器输出路径即可。

  • 针对容器内部的日志文件。

  • 在容器内配置统一的环境变量,比如 LOG_COLLECT_FILES,指定好容器内待收集的日志目录及文件

  • agent 启动的时候挂载 docker.sock 文件及磁盘的根路径。

  • 监听 docker 的容器新建、删除事件,通过 docker 的 api,查出容器的存储、环境变量、k8s 属性等信息。

  • 配置了 LOG_COLLECT_FILES 环境变量的容器,根据 env 中的日志路径找到主机中对应的文件路径,然后生成收集的配置文件 agent 与开源日志收集工具(Fluentd 或者 filebeat 等)配合。

  • agent 负责下发配置到收集工具中并对进程做 reload

fluentd-pilot

方案二:日志使用开源的 Agent 进行收集(EFK 方案),适用范围广,可以满足绝大多数日志收集、展示的需求。

fluentd 概念及工作流程

EFK 架构工作流程
  • Elasticsearch

一个开源的分布式、Restful 风格的搜索和数据分析引擎,它的底层是开源库 Apache Lucene。它可以被下面这样准确地形容:

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索;

    一个分布式实时分析搜索引擎;

    能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。


  • Kibana

Kibana 是一个开源的分析和可视化平台,设计用于和 Elasticsearch 一起工作。可以通过 Kibana 来搜索,查看,并和存储在 Elasticsearch 索引中的数据进行交互。也可以轻松地执行高级数据分析,并且以各种图标、表格和地图的形式可视化数据。


一个针对日志的收集、处理、转发系统。通过丰富的插件系统,可以收集来自于各种系统或应用的日志,转化为用户指定的格式后,转发到用户所指定的日志存储系统之中。

Fluentd 通过一组给定的数据源抓取日志数据,处理后(转换成结构化的数据格式)将它们转发给其他服务,比如 Elasticsearch、对象存储、kafka 等等。Fluentd 支持超过 300 个日志存储和分析服务,所以在这方面是非常灵活的。主要运行步骤如下:

  1. 首先 Fluentd 从多个日志源获取数据

  2. 结构化并且标记这些数据

  3. 然后根据匹配的标签将数据发送到多个目标服务

Fluentd 架构

为什么推荐使用 fluentd 作为 k8s 体系的日志收集工具?

  • 云原生:https://github.com/kubernetes/kubernetes/tree/release-1.21/cluster/addons/fluentd-elasticsearch

  • 将日志文件 JSON 化

  • 可插拔架构设计

  • 极小的资源占用

基于 C 和 Ruby 语言, 30-40MB,13,000 events/second/core

  • 极强的可靠性

    基于内存和本地文件的缓存

    强大的故障转移

fluentd 事件流的生命周期及指令配置
Input -> filter 1 -> ... -> filter N -> Buffer -> Output
复制代码

启动命令

$ fluentd -c fluent.conf
复制代码

指令介绍:

  • source ,数据源,对应 Input 通过使用 source 指令,来选择和配置所需的输入插件来启用 Fluentd 输入源, source 把事件提交到 fluentd 的路由引擎中。使用 type 来区分不同类型的数据源。如下配置可以监听指定文件的追加输入:

<source>  @type tail  path /var/log/httpd-access.log  pos_file /var/log/td-agent/httpd-access.log.pos  tag myapp.access  format apache2</source>
复制代码
  • filter,Event processing pipeline(事件处理流)

filter 可以串联成 pipeline,对数据进行串行处理,最终再交给 match 输出。 如下可以对事件内容进行处理:

<source>  @type http  port 9880</source>
<filter myapp.access> @type record_transformer <record> host_param “#{Socket.gethostname}” </record></filter>
复制代码

filter 获取数据后,调用内置的 @type record_transformer 插件,在事件的 record 里插入了新的字段 host_param,然后再交给 match 输出。

  • label 指令

可以在 source 里指定 @label,这个 source 所触发的事件就会被发送给指定的 label 所包含的任务,而不会被后续的其他任务获取到。

<source>  @type forward</source>
<source>### 这个任务指定了 label 为 @SYSTEM### 会被发送给 <label @SYSTEM>### 而不会被发送给下面紧跟的 filter 和 match @type tail @label @SYSTEM path /var/log/httpd-access.log pos_file /var/log/td-agent/httpd-access.log.pos tag myapp.access format apache2</source>
<filter access.**> @type record_transformer <record> # … </record></filter>
<match **> @type elasticsearch # …</match>
<label @SYSTEM> ### 将会接收到上面 @type tail 的 source event <filter var.log.middleware.**> @type grep # … </filter>
<match **> @type s3 # … </match></label>
复制代码
  • match,匹配输出

查找匹配 “tags” 的事件,并处理它们。match 命令的最常见用法是将事件输出到其他系统(因此,与 match 命令对应的插件称为 “输出插件”)

<source>  @type http  port 9880</source>
<filter myapp.access> @type record_transformer <record> host_param “#{Socket.gethostname}” </record></filter>
<match myapp.access> @type file path /var/log/fluent/access</match>
复制代码

事件的结构:

time:事件的处理时间

tag:事件的来源,在 fluentd.conf 中配置

record:真实的日志内容,json 对象

比如,下面这条原始日志:

192.168.0.1 - - [28/Feb/2013:12:00:00 +0900] "GET / HTTP/1.1" 200 777
复制代码

经过 fluentd 引擎处理完后的样子可能是:

2020-07-16 08:40:35 +0000 apache.access: {"user":"-","method":"GET","code":200,"size":777,"host":"192.168.0.1","path":"/"}
复制代码
fluentd 的 buffer 事件缓冲模型
Input -> filter 1 -> ... -> filter N -> Buffer -> Output
复制代码


因为每个事件数据量通常很小,考虑数据传输效率、稳定性等方面的原因,所以基本不会每条事件处理完后都会立马写入到 output 端,因此 fluentd 建立了缓冲模型,模型中主要有两个概念:

  • buffer_chunk:事件缓冲块,用来存储本地已经处理完待发送至目的端的事件,可以设置每个块的大小。

  • buffer_queue:存储 chunk 的队列,可以设置长度

可以设置的参数,主要有:

  • buffer_type,缓冲类型,可以设置 file 或者 memory

  • buffer_chunk_limit,每个 chunk 块的大小,默认 8MB

  • buffer_queue_limit ,chunk 块队列的最大长度,默认 256

  • flush_interval ,flush 一个 chunk 的时间间隔

  • retry_limit ,chunk 块发送失败重试次数,默认 17 次,之后就丢弃该 chunk 数据

  • retry_wait ,重试发送 chunk 数据的时间间隔,默认 1s,第 2 次失败再发送的话,间隔 2s,下次 4 秒,以此类推


大致的过程为:

随着 fluentd 事件的不断生成并写入 chunk,缓存块持变大,当缓存块满足 buffer_chunk_limit 大小或者新的缓存块诞生超过 flush_interval 时间间隔后,会推入缓存 queue 队列尾部,该队列大小由 buffer_queue_limit 决定。


比较理想的情况是每次有新的缓存块进入缓存队列,则立马会被写入到后端,同时,新缓存块也持续入列,但是入列的速度不会快于出列的速度,这样基本上缓存队列处于空的状态,队列中最多只有一个缓存块。


但是实际情况考虑网络等因素,往往缓存块被写入后端存储的时候会出现延迟或者写入失败的情况,当缓存块写入后端失败时,该缓存块还会留在队列中,等 retry_wait 时间后重试发送,当 retry 的次数达到 retry_limit 后,该缓存块被销毁(数据被丢弃)。


此时缓存队列持续有新的缓存块进来,如果队列中存在很多未及时写入到后端存储的缓存块的话,当队列长度达到 buffer_queue_limit 大小,则新的事件被拒绝,fluentd 报错,error_class=Fluent:BufferOverflowError error="buffer space has too many data"。


还有一种情况是网络传输缓慢的情况,若每 3 秒钟会产生一个新块,但是写入到后端时间却达到了 30s 钟,队列长度为 100,那么每个块出列的时间内,又有新的 10 个块进来,那么队列很快就会被占满,导致异常出现。

fluentd 配置实践

实践一:实现业务应用日志的收集及字段解析

目标:收集容器内的 nginx 应用的 access.log 日志,并解析日志字段为 JSON 格式,原始日志的格式为:

$ tail -f access.log...53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https
复制代码

收集并处理成:

{    "serverIp": "53.49.146.149",    "timestamp": "1561620585.973",    "respondTime": "0.005",    "httpCode": "502",    "eventTime": "27/Jun/2019:15:29:45 +0800",    "clientIp": "178.73.215.171",    "clientPort": "33337",    "method": "GET",    "protocol": "https"}
复制代码

思路:

  • 配置 fluent.conf 使用 @tail 插件通过监听 access.log 文件用 filter 实现对 nginx 日志格式解析

  • 启动 fluentd 服务

  • 手动追加内容至 access.log 文件

  • 观察本地输出内容是否符合预期

  • input -> filter

fluent.conf

<source>    @type tail    @label @nginx_access    path /var/log/nginx/access.log    pos_file /var/log/nginx/nginx_access.posg    tag nginx_access    format none    @log_level trace</source><label @nginx_access>   <filter  nginx_access>       @type parser       key_name message       format  /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/   </filter>   <match  nginx_access>     @type stdout   </match></label>
复制代码

启动服务,追加文件内容:

$ docker run -u root --rm -ti quay.io/fluentd_elasticsearch/fluentd:v3.1.0 sh/ # cat /etc/fluent/fluent.conf/ # mkdir /etc/fluent/config.d/ # fluentd -c /etc/fluent/fluent.conf/ # echo '53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https' >>/var/log/nginx/access.log
复制代码

使用该网站进行正则校验: http://fluentular.herokuapp.com

实践二:使用 ruby 实现日志字段的转换及自定义处理
<source>    @type tail    @label @nginx_access    path /var/log/nginx/access.log    pos_file /var/log/nginx/nginx_access.posg    tag nginx_access    format none    @log_level trace</source><label @nginx_access>   <filter  nginx_access>       @type parser       key_name message       format  /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/   </filter>   <filter  nginx_access>          @type record_transformer       enable_ruby       <record>        host_name "#{Socket.gethostname}"        my_key  "my_val"        tls ${record["protocol"].index("https") ? "true" : "false"}       </record>   </filter>   <match  nginx_access>     @type stdout   </match></label>
复制代码

configMap 挂载场景

开始之前,我们先来回顾一下,configmap 的常用的挂载场景。

场景一:单文件挂载到空目录

假如业务应用有一个配置文件,名为 application.yml,如果想将此配置挂载到 pod 的/etc/application/目录中。

application.yml的内容为:

$ cat application.ymlspring:  application:    name: svca-service  cloud:    config:      uri: http://config:8888      fail-fast: true      username: user      password: ${CONFIG_SERVER_PASSWORD:password}      retry:        initial-interval: 2000        max-interval: 10000        multiplier: 2        max-attempts: 10
复制代码

该配置文件在 k8s 中可以通过 configmap 来管理,通常我们有如下两种方式来管理配置文件:

  • 通过 kubectl 命令行来生成 configmap

# 通过文件直接创建$ kubectl -n default create configmap application-config --from-file=application.yml
# 会生成配置文件,查看内容,configmap的key为文件名字$ kubectl -n default get cm application-config -oyaml
复制代码
  • 通过 yaml 文件直接创建

$ cat application-config.yamlapiVersion: v1kind: ConfigMapmetadata:  name: application-config  namespace: defaultdata:  application.yml: |    spring:      application:        name: svca-service      cloud:        config:          uri: http://config:8888          fail-fast: true          username: user          password: ${CONFIG_SERVER_PASSWORD:password}          retry:            initial-interval: 2000            max-interval: 10000            multiplier: 2            max-attempts: 10
# 创建configmap$ kubectl apply -f application-config.yaml
复制代码

准备一个demo-deployment.yaml文件,挂载上述 configmap 到/etc/application/

$ cat demo-deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: demo  namespace: defaultspec:  selector:    matchLabels:      app: demo  template:    metadata:      labels:        app: demo    spec:      volumes:      - configMap:          name: application-config        name: config      containers:      - name: nginx        image: nginx:alpine        imagePullPolicy: IfNotPresent        volumeMounts:        - mountPath: "/etc/application"          name: config
复制代码

创建并查看:

$ kubectl apply -f demo-deployment.yaml
复制代码

修改 configmap 文件的内容,观察 pod 中是否自动感知变化:

$ kubectl edit cm application-config
复制代码

整个 configmap 文件直接挂载到 pod 中,若 configmap 变化,pod 会自动感知并拉取到 pod 内部。


但是 pod 内的进程不会自动重启,所以很多服务会实现一个内部的 reload 接口,用来加载最新的配置文件到进程中。

场景二:多文件挂载

假如有多个配置文件,都需要挂载到 pod 内部,且都在一个目录中

$ cat application.ymlspring:  application:    name: svca-service  cloud:    config:      uri: http://config:8888      fail-fast: true      username: user      password: ${CONFIG_SERVER_PASSWORD:password}      retry:        initial-interval: 2000        max-interval: 10000        multiplier: 2        max-attempts: 10
$ cat supervisord.conf[unix_http_server]file=/var/run/supervisor.sockchmod=0700
[supervisord]logfile=/var/logs/supervisor/supervisord.loglogfile_maxbytes=200MBlogfile_backups=10loglevel=infopidfile=/var/run/supervisord.pidchildlogdir=/var/cluster_conf_agent/logs/supervisornodaemon=false
复制代码

同样可以使用两种方式创建:

$ kubectl delete cm application-config
$ kubectl create cm application-config --from-file=application.yml --from-file=supervisord.conf
$ kubectl get cm application-config -oyaml
复制代码

观察 Pod 已经自动获取到最新的变化

$ kubectl exec demo-55c649865b-gpkgk ls /etc/application/application.ymlsupervisord.conf
复制代码

此时,是挂载到 pod 内的空目录中/etc/application,假如想挂载到 pod 已存在的目录中,比如:

$  kubectl exec   demo-55c649865b-gpkgk ls /etc/profile.dcolor_promptlocale
复制代码

更改 deployment 的挂载目录:

$ cat demo-deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: demo  namespace: defaultspec:  selector:    matchLabels:      app: demo  template:    metadata:      labels:        app: demo    spec:      volumes:      - configMap:          name: application-config        name: config      containers:      - name: nginx        image: nginx:alpine        imagePullPolicy: IfNotPresent        volumeMounts:        - mountPath: "/etc/profile.d"          name: config
复制代码

重建 pod

$ kubectl apply -f demo-deployment.yaml
# 查看pod内的/etc/profile.d目录,发现已有文件被覆盖$ kubectl exec demo-77d685b9f7-68qz7 ls /etc/profile.dapplication.ymlsupervisord.conf
复制代码
场景三 挂载子路径

实现多个配置文件,可以挂载到 pod 内的不同的目录中。比如:

  • application.yml挂载到/etc/application/

  • supervisord.conf挂载到/etc/profile.d

configmap 保持不变,修改 deployment 文件:

$ cat demo-deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: demo  namespace: defaultspec:  selector:    matchLabels:      app: demo  template:    metadata:      labels:        app: demo    spec:      volumes:      - name: config        configMap:          name: application-config          items:          - key: application.yml            path: application          - key: supervisord.conf            path: supervisord      containers:      - name: nginx        image: nginx:alpine        imagePullPolicy: IfNotPresent        volumeMounts:        - mountPath: "/etc/application/application.yml"          name: config          subPath: application        - mountPath: "/etc/profile.d/supervisord.conf"          name: config          subPath: supervisord
复制代码

测试挂载:

$ kubectl apply -f demo-deployment.yaml
$ kubectl exec demo-78489c754-shjhz ls /etc/applicationapplication.yml
$ kubectl exec demo-78489c754-shjhz ls /etc/profile.d/supervisord.confcolor_promptlocale
复制代码

使用 subPath 挂载到 Pod 内部的文件,不会自动感知原有 ConfigMap 的变更

EFK 基于 k8s 部署

部署分析

  1. es 生产环境是部署 es 集群,通常会使用 statefulset 进行部署

  2. es 默认使用 elasticsearch 用户启动进程,es 的数据目录是通过宿主机的路径挂载,因此目录权限被主机的目录权限覆盖,因此可以利用 initContainer 容器在 es 进程启动之前把目录的权限修改掉,注意 init container 要用特权模式启动。

  3. 若希望使用 helm 部署,参考 https://github.com/helm/charts/tree/master/stable/elasticsearch

使用 StatefulSet 管理有状态服务

使用 Deployment 创建多副本的 pod 的情况:

apiVersion: apps/v1kind: Deploymentmetadata:  name: nginx-deployment  namespace: default  labels:    app: nginx-deploymentspec:  replicas: 3  selector:    matchLabels:      app: nginx-deployment  template:    metadata:      labels:        app: nginx-deployment    spec:      containers:      - name: nginx        image: nginx:alpine        ports:        - containerPort: 80
复制代码

使用 StatefulSet 创建多副本 pod 的情况:

apiVersion: apps/v1kind: StatefulSetmetadata:  name: nginx-statefulset  namespace: default  labels:    app: nginx-stsspec:  replicas: 3  serviceName: "nginx"  selector:    matchLabels:      app: nginx-sts  template:    metadata:      labels:        app: nginx-sts    spec:      containers:      - name: nginx        image: nginx:alpine        ports:        - containerPort: 80
复制代码

无头服务 Headless Service

kind: ServiceapiVersion: v1metadata:  name: nginx  namespace: defaultspec:  selector:    app: nginx-sts  ports:  - protocol: TCP    port: 80    targetPort: 80  clusterIP: None
复制代码


$ kubectl -n default exec  -ti nginx-statefulset-0 sh/ # curl nginx-statefulset-2.nginx
复制代码

部署并验证

es-config.yaml

apiVersion: v1kind: ConfigMapmetadata:  name: es-config  namespace: loggingdata:  elasticsearch.yml: |    cluster.name: "luffy-elasticsearch"    node.name: "${POD_NAME}"    network.host: 0.0.0.0    discovery.seed_hosts: "es-svc-headless"    cluster.initial_master_nodes: "elasticsearch-0,elasticsearch-1,elasticsearch-2"
复制代码

es-svc-headless.yaml

apiVersion: v1kind: Servicemetadata:  name: es-svc-headless  namespace: logging  labels:    k8s-app: elasticsearchspec:  selector:    k8s-app: elasticsearch  clusterIP: None  ports:  - name: in    port: 9300    protocol: TCP
复制代码

es-statefulset.yaml

apiVersion: apps/v1kind: StatefulSetmetadata:  name: elasticsearch  namespace: logging  labels:    k8s-app: elasticsearchspec:  replicas: 3  serviceName: es-svc-headless  selector:    matchLabels:      k8s-app: elasticsearch  template:    metadata:      labels:        k8s-app: elasticsearch    spec:      initContainers:      - command:        - /sbin/sysctl        - -w        - vm.max_map_count=262144        image: alpine:3.6        imagePullPolicy: IfNotPresent        name: elasticsearch-logging-init        resources: {}        securityContext:          privileged: true      - name: fix-permissions        image: alpine:3.6        command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]        securityContext:          privileged: true        volumeMounts:        - name: es-data-volume          mountPath: /usr/share/elasticsearch/data      containers:      - name: elasticsearch        image: elasticsearch:7.4.2        env:          - name: POD_NAME            valueFrom:              fieldRef:                fieldPath: metadata.name        resources:          limits:            cpu: '1'            memory: 2Gi          requests:            cpu: '1'            memory: 2Gi        ports:        - containerPort: 9200          name: db          protocol: TCP        - containerPort: 9300          name: transport          protocol: TCP        volumeMounts:          - name: es-config-volume            mountPath: /usr/share/elasticsearch/config/elasticsearch.yml            subPath: elasticsearch.yml          - name: es-data-volume            mountPath: /usr/share/elasticsearch/data      volumes:        - name: es-config-volume          configMap:            name: es-config            items:            - key: elasticsearch.yml              path: elasticsearch.yml  volumeClaimTemplates:  - metadata:      name: es-data-volume    spec:      accessModes: ["ReadWriteOnce"]      storageClassName: "nfs"      resources:        requests:          storage: 5Gi
复制代码

es-svc.yaml

apiVersion: v1kind: Servicemetadata:  name: es-svc  namespace: logging  labels:    k8s-app: elasticsearchspec:  selector:    k8s-app: elasticsearch  ports:  - name: out    port: 9200    protocol: TCP
复制代码


$ kubectl create namespace logging
## 部署服务$ kubectl apply -f es-config.yaml$ kubectl apply -f es-svc-headless.yaml$ kubectl apply -f es-sts.yaml$ kubectl apply -f es-svc.yaml
## 等待片刻,查看一下es的pod部署到了k8s-slave1节点,状态变为running$ kubectl -n logging get po -o wide NAME READY STATUS RESTARTS AGE IP elasticsearch-0 1/1 Running 0 15m 10.244.0.126 elasticsearch-1 1/1 Running 0 15m 10.244.0.127elasticsearch-2 1/1 Running 0 15m 10.244.0.128# 然后通过curl命令访问一下服务,验证es是否部署成功$ kubectl -n logging get svc es-svc ClusterIP 10.104.226.175 <none> 9200/TCP 2ses-svc-headless ClusterIP None <none> 9300/TCP 32m $ curl 10.104.226.175:9200{ "name" : "elasticsearch-2", "cluster_name" : "luffy-elasticsearch", "cluster_uuid" : "7FDIACx9T-2ajYcB5qp4hQ", "version" : { "number" : "7.4.2", "build_flavor" : "default", "build_type" : "docker", "build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96", "build_date" : "2019-10-28T20:40:44.881551Z", "build_snapshot" : false, "lucene_version" : "8.2.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search"
复制代码
部署 kibana
  1. kibana 需要暴露 web 页面给前端使用,因此使用 ingress 配置域名来实现对 kibana 的访问

  2. kibana 为无状态应用,直接使用 Deployment 来启动

  3. kibana 需要访问 es,直接利用 k8s 服务发现访问此地址即可,http://es-svc:9200

部署并验证

efk/kibana.yaml

apiVersion: apps/v1kind: Deploymentmetadata:  name: kibana  namespace: logging  labels:    app: kibanaspec:  selector:    matchLabels:      app: "kibana"  template:    metadata:      labels:        app: kibana    spec:      containers:      - name: kibana        image: kibana:7.4.2        resources:          limits:            cpu: 1000m          requests:            cpu: 100m        env:          - name: ELASTICSEARCH_HOSTS            value: http://es-svc:9200          - name: SERVER_NAME            value: kibana-logging          - name: SERVER_REWRITEBASEPATH            value: "false"        ports:        - containerPort: 5601        volumeMounts:          - name: config            mountPath: /usr/share/kibana/config/      volumes:        - name: config          configMap:            name: kibana-config---apiVersion: v1kind: ConfigMapmetadata:  name: kibana-config  namespace: loggingdata:  kibana.yml: |-    elasticsearch.requestTimeout: 90000    server.host: "0"    xpack.monitoring.ui.container.elasticsearch.enabled: true---apiVersion: v1kind: Servicemetadata:  name: kibana  namespace: logging  labels:    app: kibanaspec:  ports:  - port: 5601    protocol: TCP    targetPort: 5601  type: ClusterIP  selector:    app: kibana---apiVersion: networking.k8s.io/v1kind: Ingressmetadata:  name: kibana  namespace: loggingspec:  rules:  - host: kibana.luffy.com    http:      paths:      - path: /        pathType: Prefix        backend:          service:             name: kibana            port:              number: 5601
复制代码


$ kubectl apply -f kibana.yaml  deployment.apps/kibana createdservice/kibana created  ingress/kibana created
## 配置域名解析 kibana.luffy.com,并访问服务进行验证,若可以访问,说明连接es成功

# GET /_cat/health?v# GET /_cat/indices
复制代码
Fluentd 服务部署

部署分析

  1. fluentd 为日志采集服务,kubernetes 集群的每个业务节点都有日志产生,因此需要使用 daemonset 的模式进行部署

  2. 为进一步控制资源,会为 daemonset 指定一个选择标签,fluentd=true 来做进一步过滤,只有带有此标签的节点才会部署 fluentd

  3. 日志采集,需要采集哪些目录下的日志,采集后发送到 es 端,因此需要配置的内容比较多,我们选择使用 configmap 的方式把配置文件整个挂载出来

部署服务

efk/fluentd-es-config-main.yaml

apiVersion: v1data:  fluent.conf: |-    # This is the root config file, which only includes components of the actual configuration    #    #  Do not collect fluentd's own logs to avoid infinite loops.    <match fluent.**>    @type null    </match>
@include /fluentd/etc/config.d/*.confkind: ConfigMapmetadata: labels: addonmanager.kubernetes.io/mode: Reconcile name: fluentd-es-config-main namespace: logging
复制代码

配置文件,fluentd-config.yaml,注意点:

1、数据源 source 的配置,k8s 会默认把容器的标准和错误输出日志重定向到宿主机中

2、默认集成了 kubernetes_metadata_filter 插件,来解析日志格式,得到 k8s 相关的元数据,raw.kubernetes

3、match 输出到 es 端的 flush 配置

efk/fluentd-configmap.yaml

kind: ConfigMapapiVersion: v1metadata:  name: fluentd-config  namespace: logging  labels:    addonmanager.kubernetes.io/mode: Reconciledata:  containers.input.conf: |-    <source>      @id fluentd-containers.log      @type tail      path /var/log/containers/*.log      pos_file /var/log/es-containers.log.pos      time_format %Y-%m-%dT%H:%M:%S.%NZ      localtime      tag raw.kubernetes.*      format json      read_from_head false    </source>    # Detect exceptions in the log output and forward them as one log entry.    # https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions     <match raw.kubernetes.**>      @id raw.kubernetes      @type detect_exceptions      remove_tag_prefix raw      message log      stream stream      multiline_flush_interval 5      max_bytes 500000      max_lines 1000    </match>        # Concatenate multi-line logs    <filter **>      @id filter_concat      @type concat      key message      multiline_end_regexp /\n$/      separator ""    </filter>  output.conf: |-    # Enriches records with Kubernetes metadata    <filter kubernetes.**>      @type kubernetes_metadata    </filter>    <match **>      @id elasticsearch      @type elasticsearch      @log_level info      include_tag_key true      hosts elasticsearch-0.es-svc-headless:9200,elasticsearch-1.es-svc-headless:9200,elasticsearch-2.es-svc-headless:9200      #port 9200      logstash_format true      #index_name kubernetes-%Y.%m.%d      request_timeout    30s      <buffer>        @type file        path /var/log/fluentd-buffers/kubernetes.system.buffer        flush_mode interval        retry_type exponential_backoff        flush_thread_count 2        flush_interval 5s        retry_forever        retry_max_interval 30        chunk_limit_size 2M        queue_limit_length 8        overflow_action block      </buffer>    </match>
复制代码

daemonset 定义文件,fluentd.yaml,注意点:

  1. 需要配置 rbac 规则,因为需要访问 k8s api 去根据日志查询元数据

  2. 需要将/var/log/containers/目录挂载到容器中

  3. 需要将 fluentd 的 configmap 中的配置文件挂载到容器内

  4. 想要部署 fluentd 的节点,需要添加 fluentd=true 的标签

efk/fluentd.yaml

apiVersion: v1kind: ServiceAccountmetadata:  name: fluentd-es  namespace: logging  labels:    k8s-app: fluentd-es    kubernetes.io/cluster-service: "true"    addonmanager.kubernetes.io/mode: Reconcile---kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1metadata:  name: fluentd-es  labels:    k8s-app: fluentd-es    kubernetes.io/cluster-service: "true"    addonmanager.kubernetes.io/mode: Reconcilerules:- apiGroups:  - ""  resources:  - "namespaces"  - "pods"  verbs:  - "get"  - "watch"  - "list"---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: fluentd-es  labels:    k8s-app: fluentd-es    kubernetes.io/cluster-service: "true"    addonmanager.kubernetes.io/mode: Reconcilesubjects:- kind: ServiceAccount  name: fluentd-es  namespace: logging  apiGroup: ""roleRef:  kind: ClusterRole  name: fluentd-es  apiGroup: ""---apiVersion: apps/v1kind: DaemonSetmetadata:  labels:    addonmanager.kubernetes.io/mode: Reconcile    k8s-app: fluentd-es  name: fluentd-es  namespace: loggingspec:  selector:    matchLabels:      k8s-app: fluentd-es  template:    metadata:      labels:        k8s-app: fluentd-es    spec:      containers:      - image: quay.io/fluentd_elasticsearch/fluentd:v3.1.0        imagePullPolicy: IfNotPresent        name: fluentd-es        resources:          limits:            memory: 500Mi          requests:            cpu: 100m            memory: 200Mi        volumeMounts:        - mountPath: /var/log          name: varlog        - mountPath: /var/lib/docker/containers          name: varlibdockercontainers          readOnly: true        - mountPath: /etc/fluent/config.d          name: config-volume      nodeSelector:        fluentd: "true"      securityContext: {}      serviceAccount: fluentd-es      serviceAccountName: fluentd-es      volumes:      - hostPath:          path: /var/log        name: varlog      - hostPath:          path: /var/lib/docker/containers        name: varlibdockercontainers      - configMap:          defaultMode: 420          name: fluentd-config        name: config-volume
复制代码


## 给slave1打上标签,进行部署fluentd日志采集服务$ kubectl label node k8s-slave1 fluentd=true  $ kubectl label node k8s-slave2 fluentd=true
# 创建服务$ kubectl apply -f fluentd-es-config-main.yaml configmap/fluentd-es-config-main created $ kubectl apply -f fluentd-configmap.yaml configmap/fluentd-config created $ kubectl apply -f fluentd.yaml serviceaccount/fluentd-es created clusterrole.rbac.authorization.k8s.io/fluentd-es created clusterrolebinding.rbac.authorization.k8s.io/fluentd-es created daemonset.extensions/fluentd-es created
## 然后查看一下pod是否已经在k8s-slave1$ kubectl -n logging get po -o wideNAME READY STATUS RESTARTS AGE elasticsearch-logging-0 1/1 Running 0 123m fluentd-es-246pl 1/1 Running 0 2m2s kibana-944c57766-ftlcw 1/1 Running 0 50m
复制代码

日志收集功能验证

EFK 功能验证

验证思路

在 slave 节点中启动服务,同时往标准输出中打印测试日志,到 kibana 中查看是否可以收集

创建测试容器

efk/test-pod.yaml

apiVersion: v1kind: Podmetadata:  name: counterspec:  nodeSelector:    fluentd: "true"  containers:  - name: count    image: alpine:3.6    args: [/bin/sh, -c,            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']
复制代码


$ kubectl get po  NAME                          READY   STATUS    RESTARTS   AGE  counter                       1/1     Running   0          6s
复制代码
配置 kibana

登录 kibana 界面,按照截图的顺序操作:




也可以通过其他元数据来过滤日志数据,比如可以单击任何日志条目以查看其他元数据,如容器名称,Kubernetes 节点,命名空间等,比如 kubernetes.pod_name : counter


到这里,我们就在 Kubernetes 集群上成功部署了 EFK ,要了解如何使用 Kibana 进行日志数据分析,可以参考 Kibana 用户指南文档:https://www.elastic.co/guide/en/kibana/current/index.html


发布于: 刚刚阅读数: 3
用户头像

Albert Edison

关注

🏅️ InfoQ 专家博主 2022.03.08 加入

CSDN:Albert Edison

评论

发布
暂无评论
基于EFK的Kubernetes日志采集方案_Kubernetes_Albert Edison_InfoQ写作社区