基于EFK的Kubernetes日志采集方案

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 在本篇文章中,您将学习 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.yaml
apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  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体系的日志收集工具?

网络异常,图片无法展示
|

  • 可插拔架构设计

网络异常,图片无法展示
|

  • 极小的资源占用

基于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.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

该配置文件在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.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: application-config
  namespace: default
data:
  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.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  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.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
$ cat supervisord.conf
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisord]
logfile=/var/logs/supervisor/supervisord.log
logfile_maxbytes=200MB
logfile_backups=10
loglevel=info
pidfile=/var/run/supervisord.pid
childlogdir=/var/cluster_conf_agent/logs/supervisor
nodaemon=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.yml
supervisord.conf

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

$  kubectl exec   demo-55c649865b-gpkgk ls /etc/profile.d
color_prompt
locale

更改deployment的挂载目录:

$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  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.d
application.yml
supervisord.conf
场景三 挂载子路径

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

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

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

$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  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/application
application.yml
$ kubectl exec demo-78489c754-shjhz ls /etc/profile.d/
supervisord.conf
color_prompt
locale

使用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/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
  labels:
    app: nginx-deployment
spec:
  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/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
  namespace: default
  labels:
    app: nginx-sts
spec:
  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: Service
apiVersion: v1
metadata:
  name: nginx
  namespace: default
spec:
  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: v1
kind: ConfigMap
metadata:
  name: es-config
  namespace: logging
data:
  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: v1
kind: Service
metadata:
  name: es-svc-headless
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  selector:
    k8s-app: elasticsearch
  clusterIP: None
  ports:
  - name: in
    port: 9300
    protocol: TCP

es-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  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: v1
kind: Service
metadata:
  name: es-svc
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  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.127
elasticsearch-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   2s
es-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/v1
kind: Deployment
metadata:
  name: kibana
  namespace: logging
  labels:
    app: kibana
spec:
  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: v1
kind: ConfigMap
metadata:
  name: kibana-config
  namespace: logging
data:
  kibana.yml: |-
    elasticsearch.requestTimeout: 90000
    server.host: "0"
    xpack.monitoring.ui.container.elasticsearch.enabled: true
---
apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: logging
  labels:
    app: kibana
spec:
  ports:
  - port: 5601
    protocol: TCP
    targetPort: 5601
  type: ClusterIP
  selector:
    app: kibana
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kibana
  namespace: logging
spec:
  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 created
service/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: v1
data:
  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/*.conf
kind: ConfigMap
metadata:
  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: ConfigMap
apiVersion: v1
metadata:
  name: fluentd-config
  namespace: logging
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  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: v1
kind: ServiceAccount
metadata:
  name: fluentd-es
  namespace: logging
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd-es
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
  - ""
  resources:
  - "namespaces"
  - "pods"
  verbs:
  - "get"
  - "watch"
  - "list"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd-es
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
  name: fluentd-es
  namespace: logging
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: fluentd-es
  apiGroup: ""
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    k8s-app: fluentd-es
  name: fluentd-es
  namespace: logging
spec:
  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 wide
NAME                      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: v1
kind: Pod
metadata:
  name: counter
spec:
  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

相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
4天前
|
Kubernetes 监控 Cloud Native
Kubernetes自动伸缩方案的终极指南
【4月更文挑战第18天】
27 0
Kubernetes自动伸缩方案的终极指南
|
4天前
|
Kubernetes Perl 容器
K8s查看集群 状态事件描述以及Pod日志信息
K8s查看集群 状态事件描述以及Pod日志信息
55 3
|
4天前
|
Prometheus 监控 Kubernetes
Kubernetes 集群监控与日志管理实践
【2月更文挑战第29天】 在微服务架构日益普及的当下,Kubernetes 已成为容器编排的事实标准。然而,随着集群规模的扩大和业务复杂度的提升,有效的监控和日志管理变得至关重要。本文将探讨构建高效 Kubernetes 集群监控系统的策略,以及实施日志聚合和分析的最佳实践。通过引入如 Prometheus 和 Fluentd 等开源工具,我们旨在为运维专家提供一套完整的解决方案,以保障系统的稳定性和可靠性。
|
4天前
|
存储 数据采集 Kubernetes
一文详解K8s环境下Job类日志采集方案
本文介绍了K8s中Job和Cronjob控制器用于非常驻容器编排的场景,以及Job容器的特点:增删频率高、生命周期短和突发并发大。文章重点讨论了Job日志采集的关键考虑点,包括容器发现速度、开始采集延时和弹性支持,并对比了5种采集方案:DaemonSet采集、Sidecar采集、ECI采集、同容器采集和独立存储采集。对于短生命周期Job,建议使用Sidecar或ECI采集,通过调整参数确保数据完整性。对于突发大量Job,需要关注服务端资源限制和采集容器的资源调整。文章总结了不同场景下的推荐采集方案,并指出iLogtail和SLS未来可能的优化方向。
|
4天前
|
Prometheus 监控 Kubernetes
Kubernetes 集群的监控与日志管理策略
【4月更文挑战第30天】 在微服务架构日益普及的当下,容器化技术与编排工具如Kubernetes成为了运维领域的重要话题。有效的监控和日志管理对于保障系统的高可用性和故障快速定位至关重要。本文将探讨在Kubernetes环境中实施监控和日志管理的最佳实践,包括选用合适的工具、部署策略以及如何整合这些工具来提供端到端的可见性。我们将重点讨论Prometheus监控解决方案和EFK(Elasticsearch, Fluentd, Kibana)日志管理堆栈,分析其在Kubernetes集群中的应用,并给出优化建议。
|
4天前
|
存储 Kubernetes 调度
K8S常见的持久化(存储)方案用法详解
K8S常见的持久化(存储)方案用法详解
|
4天前
|
运维 Prometheus 监控
Kubernetes 集群的监控与日志管理实践
【4月更文挑战第8天】在微服务架构日益普及的背景下,容器化技术成为支撑快速迭代和部署的关键。其中,Kubernetes 作为容器编排的事实标准,承载着服务的稳定性和扩展性。然而,随着集群规模的扩大,如何有效监控和管理集群状态、确保服务的高可用性成为一个挑战。本文将深入探讨 Kubernetes 集群的监控和日志管理策略,从系统资源利用到服务健康检查,再到日志的收集与分析,提供一个全面的运维视角,帮助运维人员构建一个健壮、可观察的 Kubernetes 环境。
25 0
|
4天前
|
Prometheus 监控 Kubernetes
Kubernetes 集群的监控与日志管理实践
【2月更文挑战第31天】 在微服务架构日益普及的今天,容器编排工具如Kubernetes已成为部署、管理和扩展容器化应用的关键平台。然而,随着集群规模的扩大和业务复杂性的增加,如何有效监控集群状态、及时响应系统异常,以及管理海量日志信息成为了运维人员面临的重要挑战。本文将深入探讨 Kubernetes 集群监控的最佳实践和日志管理的高效策略,旨在为运维团队提供一套系统的解决思路和操作指南。
37 0
|
4天前
|
Web App开发 监控 应用服务中间件
全新架构!日志服务 SLS 自研免登录方案发布
全新架构!日志服务 SLS 自研免登录方案发布
87531 7
|
4天前
|
消息中间件 设计模式 Java
spdlog中的异步日志方案
spdlog中的异步日志方案
268 2