• 日志收集(10)


    日志收集(10)

    1. 日志收集架构

    前面的课程中和大家一起学习了 Kubernetes 集群中监控系统的搭建,除了对集群的监控报警之外,还有一项运维工作是非常重要的,那就是日志的收集。

    1.1 介绍

    应用程序和系统日志可以帮助我们了解集群内部的运行情况,日志对于我们调试问题和监视集群情况也是非常有用的。而且大部分的应用都会有日志记录,对于传统的应用大部分都会写入到本地的日志文件之中。对于容器化应用程序来说则更简单,只需要将日志信息写入到 stdout 和 stderr 即可,容器默认情况下就会把这些日志输出到宿主机上的一个 JSON 文件之中,同样我们也可以通过 docker logs 或者 kubectl logs 来查看到对应的日志信息。

    但是,通常来说容器引擎或运行时提供的功能不足以记录完整的日志信息,比如,如果容器崩溃了、Pod 被驱逐了或者节点挂掉了,我们仍然也希望访问应用程序的日志。所以,日志应该独立于节点、Pod 或容器的生命周期,这种设计方式被称为 cluster-level-logging,即完全独立于 Kubernetes 系统,需要自己提供单独的日志后端存储、分析和查询工具。

    1.2 Kubernetes 中的基本日志

    下面这个示例是 Kubernetes 中的一个基本日志记录的示例,直接将数据输出到标准输出流,如下:

    [root@node01 ~]# vim counter-pod.yaml
    [root@node01 ~]# cat counter-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)"; i=$((i+1)); sleep 1; done']
    

    将上面文件保存为 counter-pod.yaml,该 Pod 每秒输出一些文本信息,创建这个 Pod:

    [root@node01 ~]# kubectl create -f counter-pod.yaml
    pod "counter" created
    

    创建完成后,可以使用 kubectl logs 命令查看日志信息:

    [root@node01 ~]# kubectl logs counter
    0: Sun Dec  5 13:28:58 UTC 2021
    1: Sun Dec  5 13:28:59 UTC 2021
    2: Sun Dec  5 13:29:00 UTC 2021
    3: Sun Dec  5 13:29:01 UTC 2021
    4: Sun Dec  5 13:29:02 UTC 2021
    ...
    

    1.3 Kubernetes 日志收集

    Kubernetes 集群本身不提供日志收集的解决方案,一般来说有主要的3种方案来做日志收集:

    • 在节点上运行一个 agent 来收集日志
    • 在 Pod 中包含一个 sidecar 容器来收集应用日志
    • 直接在应用程序中将日志信息推送到采集后端

    节点日志采集代理

    图片.png-28.3kB

    通过在每个节点上运行一个日志收集的 agent 来采集日志数据,日志采集 agent 是一种专用工具,用于将日志数据推送到统一的后端。一般来说,这种 agent 用一个容器来运行,可以访问该节点上所有应用程序容器的日志文件所在目录。

    由于这种 agent 必须在每个节点上运行,所以直接使用 DaemonSet 控制器运行该应用程序即可。在节点上运行一个日志收集的 agent 这种方式是最常见的一直方法,因为它只需要在每个节点上运行一个代理程序,并不需要对节点上运行的应用程序进行更改,对应用程序没有任何侵入性,但是这种方法也仅仅适用于收集输出到 stdout 和 stderr 的应用程序日志。

    1.4 以 sidecar 容器收集日志

    我们看上面的图可以看到有一个明显的问题就是我们采集的日志都是通过输出到容器的 stdout 和 stderr 里面的信息,这些信息会在本地的容器对应目录中保留成 JSON 日志文件,所以直接在节点上运行一个 agent 就可以采集到日志。但是如果我们的应用程序的日志是输出到容器中的某个日志文件的话呢?这种日志数据显然只通过上面的方案是采集不到的了。

    用 sidecar 容器重新输出日志

    图片.png-28.4kB

    对于上面这种情况我们可以直接在 Pod 中启动另外一个 sidecar 容器,直接将应用程序的日志通过这个容器重新输出到 stdout,这样是不是通过上面的节点日志收集方案又可以完成了。

    由于这个 sidecar 容器的主要逻辑就是将应用程序中的日志进行重定向打印,所以背后的逻辑非常简单,开销很小,而且由于输出到了 stdout 或者 stderr,所以我们也可以使用 kubectl logs 来查看日志了。

    下面的示例是在 Pod 中将日志记录在了容器的两个本地文件之中:

    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
      volumes:
      - name: varlog
        emptyDir: {}
    

    由于 Pod 中容器的特性,我们可以利用另外一个 sidecar 容器去获取到另外容器中的日志文件,然后将日志重定向到自己的 stdout 流中,可以将上面的 YAML 文件做如下修改:(two-files-counter-pod-streaming-sidecar.yaml)

    [root@node01 ~]# vim two-files-counter-pod-streaming-sidecar.yaml
    [root@node01 ~]# cat two-files-counter-pod-streaming-sidecar.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: {}
    

    直接创建上面的 Pod:

    [root@node01 ~]# kubectl create -f two-files-counter-pod-streaming-sidecar.yaml
    pod "counter" created
    

    运行成功后,我们可以通过下面的命令来查看日志的信息:

    [root@node01 ~]# kubectl logs counter count-log-1
    0: Sun Dec  5 13:44:31 UTC 2021
    1: Sun Dec  5 13:44:32 UTC 2021
    2: Sun Dec  5 13:44:33 UTC 2021
    3: Sun Dec  5 13:44:34 UTC 2021
    ...
    
    [root@node01 ~]# kubectl logs counter count-log-2
    Sun Dec  5 13:44:31 UTC 2021 INFO 0
    Sun Dec  5 13:44:32 UTC 2021 INFO 1
    Sun Dec  5 13:44:33 UTC 2021 INFO 2
    Sun Dec  5 13:44:34 UTC 2021 INFO 3
    ...
    

    这样前面节点上的日志采集 agent 就可以自动获取这些日志信息,而不需要其他配置。

    这种方法虽然可以解决上面的问题,但是也有一个明显的缺陷,就是日志不仅会在原容器文件中保留下来,还会通过 stdout 输出后占用磁盘空间,这样无形中就增加了一倍磁盘空间。

    使用 sidecar 运行日志采集 agent

    图片.png-19kB

    如果你觉得在节点上运行一个日志采集的代理不够灵活的话,那么你也可以创建一个单独的日志采集代理程序的 sidecar 容器,不过需要单独配置和应用程序一起运行。

    不过这样虽然更加灵活,但是在 sidecar 容器中运行日志采集代理程序会导致大量资源消耗,因为你有多少个要采集的 Pod,就需要运行多少个采集代理程序,另外还无法使用 kubectl logs 命令来访问这些日志,因为它们不受 kubelet 控制。

    举个例子,你可以使用的Stackdriver,它使用fluentd作为记录剂。以下是两个可用于实现此方法的配置文件。第一个文件包含配置流利的ConfigMap。

    下面是 Kubernetes 官方的一个 fluentd 的配置文件示例,使用 ConfigMap 对象来保存:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: fluentd-config
    data:
      fluentd.conf: |
        <source>
          type tail
          format none
          path /var/log/1.log
          pos_file /var/log/1.log.pos
          tag count.format1
        </source>
    
        <source>
          type tail
          format none
          path /var/log/2.log
          pos_file /var/log/2.log.pos
          tag count.format2
        </source>
    
        <match **>
          type google_cloud
        </match>
    

    上面的配置文件是配置收集原文件 /var/log/1.log 和 /var/log/2.log 的日志数据,然后通过 google_cloud 这个插件将数据推送到 Stackdriver 后端去。

    下面是我们使用上面的配置文件在应用程序中运行一个 fluentd 的容器来读取日志数据:

    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-agent
        image: k8s.gcr.io/fluentd-gcp:1.30
        env:
        - name: FLUENTD_ARGS
          value: -c /etc/fluentd-config/fluentd.conf
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: config-volume
          mountPath: /etc/fluentd-config
      volumes:
      - name: varlog
        emptyDir: {}
      - name: config-volume
        configMap:
          name: fluentd-config
    

    上面的 Pod 创建完成后,容器 count-agent 就会将 count 容器中的日志进行收集然后上传。当然,这只是一个简单的示例,我们也完全可以使用其他的任何日志采集工具来替换 fluentd,比如 logstash、fluent-bit 等等。
    直接从应用程序收集日志

    图片.png-14.2kB

    除了上面的几种方案之外,我们也完全可以通过直接在应用程序中去显示的将日志推送到日志后端,但是这种方式需要代码层面的实现,也超出了 Kubernetes 本身的范围。

    2. 搭建 EFK 日志系统

    上节课和大家介绍了 Kubernetes 集群中的几种日志收集方案,Kubernetes 中比较流行的日志收集解决方案是 Elasticsearch、Fluentd 和 Kibana(EFK)技术栈,也是官方现在比较推荐的一种方案。

    Elasticsearch 是一个实时的、分布式的可扩展的搜索引擎,允许进行全文、结构化搜索,它通常用于索引和搜索大量日志数据,也可用于搜索许多不同类型的文档。

    Elasticsearch 通常与 Kibana 一起部署,Kibana 是 Elasticsearch 的一个功能强大的数据可视化 Dashboard,Kibana 允许你通过 web 界面来浏览 Elasticsearch 日志数据。

    Fluentd是一个流行的开源数据收集器,我们将在 Kubernetes 集群节点上安装 Fluentd,通过获取容器日志文件、过滤和转换日志数据,然后将数据传递到 Elasticsearch 集群,在该集群中对其进行索引和存储。

    我们先来配置启动一个可扩展的 Elasticsearch 集群,然后在 Kubernetes 集群中创建一个 Kibana 应用,最后通过 DaemonSet 来运行 Fluentd,以便它在每个 Kubernetes 工作节点上都可以运行一个 Pod。

    2.1 创建 Elasticsearch 集群

    在创建 Elasticsearch 集群之前,我们先创建一个命名空间,我们将在其中安装所有日志相关的资源对象。

    创建一个名为 logging 的 namespace:

    [root@node01 ~]# kubectl create ns logging
    namespace "logging" created
    
    [root@node01 ~]# kubectl get ns
    NAME          STATUS    AGE
    ......
    logging       Active    2s
    ......
    

    现在创建了一个命名空间来存放我们的日志相关资源,接下来可以部署 EFK 相关组件,首先开始部署一个3节点的 Elasticsearch 集群。

    这里我们使用3个 Elasticsearch Pod 来避免高可用下多节点集群中出现的“脑裂”问题,当一个或多个节点无法与其他节点通信时会产生“脑裂”,可能会出现几个主节点。

    了解更多 Elasticsearch 集群脑裂问题,可以查看文档https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html#split-brain

    一个关键点是您应该设置参数discover.zen.minimum_master_nodes=N/2+1,其中N是 Elasticsearch 集群中符合主节点的节点数,比如我们这里3个节点,意味着N应该设置为2。这样,如果一个节点暂时与集群断开连接,则另外两个节点可以选择一个新的主节点,并且集群可以在最后一个节点尝试重新加入时继续运行,在扩展 Elasticsearch 集群时,一定要记住这个参数。

    首先创建一个名为 elasticsearch 的无头服务,新建文件 elasticsearch-svc.yaml,文件内容如下:

    [root@node01 ~]# vim elasticsearch-svc.yaml
    [root@node01 ~]# cat elasticsearch-svc.yaml 
    kind: Service
    apiVersion: v1
    metadata:
      name: elasticsearch
      namespace: logging
      labels:
        app: elasticsearch
    spec:
      selector:
        app: elasticsearch
      clusterIP: None
      ports:
        - port: 9200
          name: rest
        - port: 9300
          name: inter-node
    

    定义了一个名为 elasticsearch 的 Service,指定标签app=elasticsearch,当我们将 Elasticsearch StatefulSet 与此服务关联时,服务将返回带有标签app=elasticsearch的 Elasticsearch Pods 的 DNS A 记录,然后设置clusterIP=None,将该服务设置成无头服务。最后,我们分别定义端口9200、9300,分别用于与 REST API 交互,以及用于节点间通信。

    使用 kubectl 直接创建上面的服务资源对象:

    [root@node01 ~]# kubectl create -f elasticsearch-svc.yaml
    service "elasticsearch" created
    
    [root@node01 ~]# kubectl get svc -n logging
    NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
    elasticsearch   ClusterIP   None         <none>        9200/TCP,9300/TCP   5s
    

    现在我们已经为 Pod 设置了无头服务和一个稳定的域名.elasticsearch.logging.svc.cluster.local,接下来我们通过 StatefulSet 来创建具体的 Elasticsearch 的 Pod 应用。

    Kubernetes StatefulSet 允许我们为 Pod 分配一个稳定的标识和持久化存储,Elasticsearch 需要稳定的存储来保证 Pod 在重新调度或者重启后的数据依然不变,所以需要使用 StatefulSet 来管理 Pod。

    要了解更多关于 StaefulSet 的信息,可以查看官网关于 StatefulSet 的相关文档:https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/。

    新建名为 elasticsearch-statefulset.yaml 的资源清单文件,首先粘贴下面内容:

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: es-cluster
      namespace: logging
    spec:
      serviceName: elasticsearch
      replicas: 3
      selector:
        matchLabels:
          app: elasticsearch
      template:
        metadata:
          labels:
            app: elasticsearch
    

    该内容中,我们定义了一个名为 es-cluster 的 StatefulSet 对象,然后定义serviceName=elasticsearch和前面创建的 Service 相关联,这可以确保使用以下 DNS 地址访问 StatefulSet 中的每一个 Pod:es-cluster-[0,1,2].elasticsearch.logging.svc.cluster.local,其中[0,1,2]对应于已分配的 Pod 序号。

    然后指定3个副本,将 matchLabels 设置为app=elasticsearch,所以 Pod 的模板部分.spec.template.metadata.lables也必须包含app=elasticsearch标签。

    然后定义 Pod 模板部分内容:

    ...
      spec:
        containers:
        - name: elasticsearch
          image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3
          resources:
            limits:
              cpu: 1000m
            requests:
              cpu: 100m
          ports:
          - containerPort: 9200
            name: rest
            protocol: TCP
          - containerPort: 9300
            name: inter-node
            protocol: TCP
          volumeMounts:
          - name: data
            mountPath: /usr/share/elasticsearch/data
          env:
          - name: cluster.name
            value: k8s-logs
          - name: node.name
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: discovery.zen.ping.unicast.hosts
            value: "es-cluster-0.elasticsearch,es-cluster-1.elasticsearch,es-cluster-2.elasticsearch"
          - name: discovery.zen.minimum_master_nodes
            value: "2"
          - name: ES_JAVA_OPTS
            value: "-Xms512m -Xmx512m"
    

    该部分是定义 StatefulSet 中的 Pod,我们这里使用一个-oss后缀的镜像,该镜像是 Elasticsearch 的开源版本,如果你想使用包含X-Pack之类的版本,可以去掉该后缀。然后暴露了9200和9300两个端口,注意名称要和上面定义的 Service 保持一致。然后通过 volumeMount 声明了数据持久化目录,下面我们再来定义 VolumeClaims。最后就是我们在容器中设置的一些环境变量了:

    • cluster.name:Elasticsearch 集群的名称,我们这里命名成 k8s-logs。
    • node.name:节点的名称,通过metadata.name来获取。这将解析为 es-cluster-[0,1,2],取决于节点的指定顺序。
    • discovery.zen.ping.unicast.hosts:此字段用于设置在 Elasticsearch 集群中节点相互连接的发现方法。我们使用 unicastdiscovery 方式,它为我们的集群指定了一个静态主机列表。由于我们之前配置的无头服务,我们的 Pod 具有唯一的 DNS 域es-cluster-[0,1,2].elasticsearch.logging.svc.cluster.local,因此我们相应地设置此变量。由于都在同一个 namespace 下面,所以我们可以将其缩短为es-cluster-[0,1,2].elasticsearch。要了解有关 Elasticsearch 发现的更多信息,请参阅 Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html。
    • discovery.zen.minimum_master_nodes:我们将其设置为(N/2) + 1N是我们的群集中符合主节点的节点的数量。我们有3个 Elasticsearch 节点,因此我们将此值设置为2(向下舍入到最接近的整数)。要了解有关此参数的更多信息,请参阅官方 Elasticsearch 文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html#split-brain。
    • ES_JAVA_OPTS:这里我们设置为-Xms512m -Xmx512m,告诉JVM使用512 MB的最小和最大堆。您应该根据群集的资源可用性和需求调整这些参数。要了解更多信息,请参阅设置堆大小的相关文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html。

    接下来添加关于 initContainer 的内容:

    ...
        initContainers:
        - name: fix-permissions
          image: busybox
          command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
          securityContext:
            privileged: true
          volumeMounts:
          - name: data
            mountPath: /usr/share/elasticsearch/data
        - name: increase-vm-max-map
          image: busybox
          command: ["sysctl", "-w", "vm.max_map_count=262144"]
          securityContext:
            privileged: true
        - name: increase-fd-ulimit
          image: busybox
          command: ["sh", "-c", "ulimit -n 65536"]
          securityContext:
            privileged: true
    

    这里我们定义了几个在主应用程序之前运行的 Init 容器,这些初始容器按照定义的顺序依次执行,执行完成后才会启动主应用容器。

    第一个名为 fix-permissions 的容器用来运行 chown 命令,将 Elasticsearch 数据目录的用户和组更改为1000:1000(Elasticsearch 用户的 UID)。因为默认情况下,Kubernetes 用 root 用户挂载数据目录,这会使得 Elasticsearch 无法方法该数据目录,可以参考 Elasticsearch 生产中的一些默认注意事项相关文档说明:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_notes_for_production_use_and_defaults。

    第二个名为 increase-vm-max-map 的容器用来增加操作系统对mmap计数的限制,默认情况下该值可能太低,导致内存不足的错误,要了解更多关于该设置的信息,可以查看 Elasticsearch 官方文档说明:https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html。

    最后一个初始化容器是用来执行ulimit命令增加打开文件描述符的最大数量的。

    此外 Elastisearch Notes for Production Use 文档还提到了由于性能原因最好禁用 swap,当然对于 Kubernetes 集群而言,最好也是禁用 swap 分区的。

    现在我们已经定义了主应用容器和它之前运行的 Init Containers 来调整一些必要的系统参数,接下来我们可以添加数据目录的持久化相关的配置,在 StatefulSet 中,使用 volumeClaimTemplates 来定义 volume 模板即可:

    ...
      volumeClaimTemplates:
      - metadata:
          name: data
          labels:
            app: elasticsearch
        spec:
          accessModes: [ "ReadWriteOnce" ]
          storageClassName: es-data-db
          resources:
            requests:
              storage: 50Gi
    

    我们这里使用 volumeClaimTemplates 来定义持久化模板,Kubernetes 会使用它为 Pod 创建 PersistentVolume,设置访问模式为ReadWriteOnce,这意味着它只能被 mount 到单个节点上进行读写,然后最重要的是使用了一个名为 es-data-db 的 StorageClass 对象,所以我们需要提前创建该对象,我们这里使用的 NFS 作为存储后端,所以需要安装一个对应的 provisioner 驱动,前面关于 StorageClass 的课程中已经和大家介绍过方法,新建一个 elasticsearch-storageclass.yaml 的文件,文件内容如下:

    [root@node01 ~]# vim elasticsearch-storageclass.yaml
    [root@node01 ~]# cat elasticsearch-storageclass.yaml 
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: es-data-db
    provisioner: fuseim.pri/ifs  # 该值需要和 provisioner 配置的保持一致
    

    最后,我们指定了每个 PersistentVolume 的大小为 50GB,我们可以根据自己的实际需要进行调整该值。最后,完整的 Elasticsearch StatefulSet 资源清单文件内容如下:

    [root@node01 ~]# vim elasticsearch-statefulset.yaml 
    [root@node01 ~]# cat elasticsearch-statefulset.yaml 
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: es-cluster
      namespace: logging
    spec:
      serviceName: elasticsearch
      replicas: 3
      selector:
        matchLabels:
          app: elasticsearch
      template:
        metadata:
          labels:
            app: elasticsearch
        spec:
          containers:
          - name: elasticsearch
            image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3
            resources:
                limits:
                  cpu: 1000m
                requests:
                  cpu: 100m
            ports:
            - containerPort: 9200
              name: rest
              protocol: TCP
            - containerPort: 9300
              name: inter-node
              protocol: TCP
            volumeMounts:
            - name: data
              mountPath: /usr/share/elasticsearch/data
            env:
              - name: cluster.name
                value: k8s-logs
              - name: node.name
                valueFrom:
                  fieldRef:
                    fieldPath: metadata.name
              - name: discovery.zen.ping.unicast.hosts
                value: "es-cluster-0.elasticsearch,es-cluster-1.elasticsearch,es-cluster-2.elasticsearch"
              - name: discovery.zen.minimum_master_nodes
                value: "2"
              - name: ES_JAVA_OPTS
                value: "-Xms512m -Xmx512m"
          initContainers:
          - name: fix-permissions
            image: busybox
            command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
            securityContext:
              privileged: true
            volumeMounts:
            - name: data
              mountPath: /usr/share/elasticsearch/data
          - name: increase-vm-max-map
            image: busybox
            command: ["sysctl", "-w", "vm.max_map_count=262144"]
            securityContext:
              privileged: true
          - name: increase-fd-ulimit
            image: busybox
            command: ["sh", "-c", "ulimit -n 65536"]
            securityContext:
              privileged: true
      volumeClaimTemplates:
      - metadata:
          name: data
          labels:
            app: elasticsearch
        spec:
          accessModes: [ "ReadWriteOnce" ]
          storageClassName: es-data-db
          resources:
            requests:
              storage: 50Gi
    

    现在直接使用 kubectl 工具部署即可:

    [root@node01 ~]# kubectl create -f elasticsearch-storageclass.yaml
    storageclass.storage.k8s.io "es-data-db" created
    
    [root@node01 ~]# kubectl create -f elasticsearch-statefulset.yaml
    statefulset.apps "es-cluster" created
    

    添加成功后,可以看到 logging 命名空间下面的所有的资源对象:

    [root@node01 ~]# kubectl get sts -n logging
    NAME         DESIRED   CURRENT   AGE
    es-cluster   3         3         2m
    
    [root@node01 ~]# kubectl get pods -n logging
    NAME           READY     STATUS    RESTARTS   AGE
    es-cluster-0   1/1       Running   0          2m
    es-cluster-1   1/1       Running   0          40s
    es-cluster-2   1/1       Running   0          27s
    
    [root@node01 ~]# kubectl get svc -n logging
    NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
    elasticsearch   ClusterIP   None         <none>        9200/TCP,9300/TCP   39m
    

    Pods 部署完成后,我们可以通过请求一个 REST API 来检查 Elasticsearch 集群是否正常运行。使用下面的命令将本地端口9200转发到 Elasticsearch 节点(如es-cluster-0)对应的端口:

    [root@node01 ~]# kubectl port-forward es-cluster-0 9200:9200 --namespace=logging
    Forwarding from 127.0.0.1:9200 -> 9200
    Forwarding from [::1]:9200 -> 9200
    

    然后,在另外的终端窗口中,执行如下请求:

    [root@node01 ~]# curl http://localhost:9200/_cluster/state?pretty
    

    正常来说,应该会看到类似于如下的信息:

    {
      "cluster_name" : "k8s-logs",
      "compressed_size_in_bytes" : 338,
      "cluster_uuid" : "atAGOsucR0qGF4dtp8VWtQ",
      "version" : 3,
      "state_uuid" : "Kr0PR4GIRZqhHcoQd1u7kA",
      "master_node" : "70rZOyeGTBqQ6GK0si2Mig",
      "blocks" : { },
      "nodes" : {
        "ML__L-X_SxK9h_DBalUnKA" : {
          "name" : "es-cluster-2",
          "ephemeral_id" : "MOt0cw8qT_uO6zs3nTL9PA",
          "transport_address" : "10.244.1.26:9300",
          "attributes" : { }
        },
        "70rZOyeGTBqQ6GK0si2Mig" : {
          "name" : "es-cluster-0",
          "ephemeral_id" : "eqM9u12eRPWFkT9ZycN2Tw",
          "transport_address" : "10.244.1.24:9300",
          "attributes" : { }
        },
        "yi-qShKoSi28UdXpscqjrQ" : {
          "name" : "es-cluster-1",
          "ephemeral_id" : "aeBFeB5tTdGmmh83JEVEQQ",
          "transport_address" : "10.244.1.25:9300",
          "attributes" : { }
        }
      },
      "metadata" : {
        "cluster_uuid" : "atAGOsucR0qGF4dtp8VWtQ",
        "templates" : { },
        "indices" : { },
        "index-graveyard" : {
          "tombstones" : [ ]
        }
      },
      "routing_table" : {
        "indices" : { }
      },
      "routing_nodes" : {
        "unassigned" : [ ],
        "nodes" : {
          "yi-qShKoSi28UdXpscqjrQ" : [ ],
          "70rZOyeGTBqQ6GK0si2Mig" : [ ],
          "ML__L-X_SxK9h_DBalUnKA" : [ ]
        }
      },
      "restore" : {
        "snapshots" : [ ]
      },
      "snapshots" : {
        "snapshots" : [ ]
      },
      "snapshot_deletions" : {
        "snapshot_deletions" : [ ]
      }
    }
    

    看到上面的信息就表明我们名为 k8s-logs 的 Elasticsearch 集群成功创建了3个节点:es-cluster-0,es-cluster-1,和es-cluster-2,当前主节点是 es-cluster-0。

    2.2 创建 Kibana 服务

    Elasticsearch 集群启动成功了,接下来我们可以来部署 Kibana 服务,新建一个名为 kibana.yaml 的文件,对应的文件内容如下:

    [root@node01 ~]# vim kibana.yaml
    [root@node01 ~]# cat kibana.yaml 
    apiVersion: v1
    kind: Service
    metadata:
      name: kibana
      namespace: logging
      labels:
        app: kibana
    spec:
      ports:
      - port: 5601
      type: NodePort
      selector:
        app: kibana
    
    ---
    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: docker.elastic.co/kibana/kibana-oss:6.4.3
            resources:
              limits:
                cpu: 1000m
              requests:
                cpu: 100m
            env:
              - name: ELASTICSEARCH_URL
                value: http://elasticsearch:9200
            ports:
            - containerPort: 5601
    

    上面我们定义了两个资源对象,一个 Service 和 Deployment,为了测试方便,我们将 Service 设置为了 NodePort 类型,Kibana Pod 中配置都比较简单,唯一需要注意的是我们使用 ELASTICSEARCH_URL 这个环境变量来设置Elasticsearch 集群的端点和端口,直接使用 Kubernetes DNS 即可,此端点对应服务名称为 elasticsearch,由于是一个 headless service,所以该域将解析为3个 Elasticsearch Pod 的 IP 地址列表。

    配置完成后,直接使用 kubectl 工具创建:

    $ kubectl create -f kibana.yaml
    service/kibana created
    deployment.apps/kibana created
    

    创建完成后,可以查看 Kibana Pod 的运行状态:

    [root@node01 ~]# kubectl get pods --namespace=logging
    NAME                    READY     STATUS    RESTARTS   AGE
    ......
    kibana-7844d64b-t99tm   1/1       Running   0          9s
    
    [root@node01 ~]# kubectl get svc --namespace=logging
    NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
    ......
    kibana          NodePort    10.101.151.42   <none>        5601:30659/TCP      45s
    

    如果 Pod 已经是 Running 状态了,证明应用已经部署成功了,然后可以通过 NodePort 来访问 Kibana 这个服务,在浏览器中打开http://192.168.200.11:30659即可,如果看到如下欢迎界面证明 Kibana 已经成功部署到了 Kubernetes集群之中。

    图片.png-171.6kB

    2.3 部署 Fluentd

    Fluentd 是一个高效的日志聚合器,是用 Ruby 编写的,并且可以很好地扩展。对于大部分企业来说,Fluentd 足够高效并且消耗的资源相对较少,另外一个工具Fluent-bit更轻量级,占用资源更少,但是插件相对 Fluentd 来说不够丰富,所以整体来说,Fluentd 更加成熟,使用更加广泛,所以我们这里也同样使用 Fluentd 来作为日志收集工具。

    2.4 工作原理

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

    • 首先 Fluentd 从多个日志源获取数据
    • 结构化并且标记这些数据
    • 然后根据匹配的标签将数据发送到多个目标服务去

    图片.png-198kB

    2.5 配置

    一般来说我们是通过一个配置文件来告诉 Fluentd 如何采集、处理数据的,下面简单和大家介绍下 Fluentd 的配置方法。

    2.5.1 日志源配置

    比如我们这里为了收集 Kubernetes 节点上的所有容器日志,就需要做如下的日志源配置:

    <source>
    
    @id fluentd-containers.log
    
    @type tail
    
    path /var/log/containers/*.log
    
    pos_file /var/log/fluentd-containers.log.pos
    
    time_format %Y-%m-%dT%H:%M:%S.%NZ
    
    tag raw.kubernetes.*
    
    format json
    
    read_from_head true
    
    </source>
    

    上面配置部分参数说明如下:

    • id:表示引用该日志源的唯一标识符,该标识可用于进一步过滤和路由结构化日志数据
    • type:Fluentd 内置的指令,tail表示 Fluentd 从上次读取的位置通过 tail 不断获取数据,另外一个是http表示通过一个 GET 请求来收集数据。
    • path:tail类型下的特定参数,告诉 Fluentd 采集/var/log/containers目录下的所有日志,这是 docker 在 Kubernetes 节点上用来存储运行容器 stdout 输出日志数据的目录。
    • pos_file:检查点,如果 Fluentd 程序重新启动了,它将使用此文件中的位置来恢复日志数据收集。
    • tag:用来将日志源与目标或者过滤器匹配的自定义字符串,Fluentd 匹配源/目标标签来路由日志数据。

    2.5.2 路由配置

    上面是日志源的配置,接下来看看如何将日志数据发送到 Elasticsearch:

    <match **>
    
    @id elasticsearch
    
    @type elasticsearch
    
    @log_level info
    
    include_tag_key true
    
    type_name fluentd
    
    host "#{ENV['OUTPUT_HOST']}"
    
    port "#{ENV['OUTPUT_PORT']}"
    
    logstash_format true
    
    <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 "#{ENV['OUTPUT_BUFFER_CHUNK_LIMIT']}"
    
    queue_limit_length "#{ENV['OUTPUT_BUFFER_QUEUE_LIMIT']}"
    
    overflow_action block
    
    </buffer>
    
    • match:标识一个目标标签,后面是一个匹配日志源的正则表达式,我们这里想要捕获所有的日志并将它们发送给 Elasticsearch,所以需要配置成**
    • id:目标的一个唯一标识符。
    • type:支持的输出插件标识符,我们这里要输出到 Elasticsearch,所以配置成 elasticsearch,这是 Fluentd 的一个内置插件。
    • log_level:指定要捕获的日志级别,我们这里配置成info,表示任何该级别或者该级别以上(INFO、WARNING、ERROR)的日志都将被路由到 Elsasticsearch。
    • host/port:定义 Elasticsearch 的地址,也可以配置认证信息,我们的 Elasticsearch 不需要认证,所以这里直接指定 host 和 port 即可。
    • logstash_format:Elasticsearch 服务对日志数据构建反向索引进行搜索,将 logstash_format 设置为true,Fluentd 将会以 logstash 格式来转发结构化的日志数据。
    • Buffer: Fluentd 允许在目标不可用时进行缓存,比如,如果网络出现故障或者 Elasticsearch 不可用的时候。缓冲区配置也有助于降低磁盘的 IO。

    2.6 安装

    要收集 Kubernetes 集群的日志,直接用 DasemonSet 控制器来部署 Fluentd 应用,这样,它就可以从 Kubernetes 节点上采集日志,确保在集群中的每个节点上始终运行一个 Fluentd 容器。当然可以直接使用 Helm 来进行一键安装,为了能够了解更多实现细节,我们这里还是采用手动方法来进行安装。

    首先,我们通过 ConfigMap 对象来指定 Fluentd 配置文件,新建 fluentd-configmap.yaml 文件,文件内容如下:

    [root@node01 ~]# vim fluentd-configmap.yaml
    [root@node01 ~]# cat fluentd-configmap.yaml 
    kind: ConfigMap
    apiVersion: v1
    metadata:
      name: fluentd-config
      namespace: logging
      labels:
        addonmanager.kubernetes.io/mode: Reconcile
    data:
      system.conf: |-
        <system>
          root_dir /tmp/fluentd-buffers/
        </system>
      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 true
        </source>
        # Detect exceptions in the log output and forward them as one log entry.
        <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>
      system.input.conf: |-
        # Logs from systemd-journal for interesting services.
        <source>
          @id journald-docker
          @type systemd
          filters [{ "_SYSTEMD_UNIT": "docker.service" }]
          <storage>
            @type local
            persistent true
          </storage>
          read_from_head true
          tag docker
        </source>
        <source>
          @id journald-kubelet
          @type systemd
          filters [{ "_SYSTEMD_UNIT": "kubelet.service" }]
          <storage>
            @type local
            persistent true
          </storage>
          read_from_head true
          tag kubelet
        </source>
      forward.input.conf: |-
        # Takes the messages sent over TCP
        <source>
          @type forward
        </source>
      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
          host elasticsearch
          port 9200
          logstash_format true
          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>
    

    上面配置文件中我们配置了 docker 容器日志目录以及 docker、kubelet 应用的日志的收集,收集到数据经过处理后发送到 elasticsearch:9200 服务。

    然后新建一个 fluentd-daemonset.yaml 的文件,文件内容如下:

    [root@node01 ~]# vim fluentd-daemonset.yaml
    [root@node01 ~]# cat fluentd-daemonset.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:
      name: fluentd-es
      namespace: logging
      labels:
        k8s-app: fluentd-es
        version: v2.0.4
        kubernetes.io/cluster-service: "true"
        addonmanager.kubernetes.io/mode: Reconcile
    spec:
      selector:
        matchLabels:
          k8s-app: fluentd-es
          version: v2.0.4
      template:
        metadata:
          labels:
            k8s-app: fluentd-es
            kubernetes.io/cluster-service: "true"
            version: v2.0.4
          # This annotation ensures that fluentd does not get evicted if the node
          # supports critical pod annotation based priority scheme.
          # Note that this does not guarantee admission on the nodes (#40573).
          annotations:
            scheduler.alpha.kubernetes.io/critical-pod: ''
        spec:
          serviceAccountName: fluentd-es
          containers:
          - name: fluentd-es
            image: cnych/fluentd-elasticsearch:v2.0.4
            env:
            - name: FLUENTD_ARGS
              value: --no-supervisor -q
            resources:
              limits:
                memory: 500Mi
              requests:
                cpu: 100m
                memory: 200Mi
            volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: config-volume
              mountPath: /etc/fluent/config.d
          nodeSelector:
            beta.kubernetes.io/fluentd-ds-ready: "true"
          tolerations:
          - key: node-role.kubernetes.io/master
            operator: Exists
            effect: NoSchedule
          terminationGracePeriodSeconds: 30
          volumes:
          - name: varlog
            hostPath:
              path: /var/log
          - name: varlibdockercontainers
            hostPath:
              path: /var/lib/docker/containers
          - name: config-volume
            configMap:
              name: fluentd-config
    

    我们将上面创建的 fluentd-config 这个 ConfigMap 对象通过 volumes 挂载到了 Fluentd 容器中,另外为了能够灵活控制哪些节点的日志可以被收集,所以我们这里还添加了一个 nodSelector 属性:

    nodeSelector:
      beta.kubernetes.io/fluentd-ds-ready: "true"
    

    意思就是要想采集节点的日志,那么我们就需要给节点打上上面的标签,比如我们这里3个节点都打上了该标签:

    [root@node01 ~]# kubectl label nodes master beta.kubernetes.io/fluentd-ds-ready=true
    node "master" labeled
    [root@node01 ~]# kubectl label nodes node01 beta.kubernetes.io/fluentd-ds-ready=true
    node "node01" labeled
    [root@node01 ~]# kubectl label nodes node02 beta.kubernetes.io/fluentd-ds-ready=true
    node "node02" labeled
    
    [root@node01 ~]# kubectl get nodes --show-labels
    NAME      STATUS    ROLES     AGE       VERSION   LABELS
    master    Ready     master    204d      v1.10.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/hostname=master,node-role.kubernetes.io/master=
    node01    Ready     <none>    204d      v1.10.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node01
    node02    Ready     <none>    102d      v1.10.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/hostname=node02
    

    另外由于我们的集群使用的是 kubeadm 搭建的,默认情况下 master 节点有污点,所以要想也收集 master 节点的日志,则需要添加上容忍:

    tolerations:
    - key: node-role.kubernetes.io/master
      operator: Exists
      effect: NoSchedule
    

    另外需要注意的地方是,我这里的测试环境更改了 docker 的根目录:

    $ docker info
    ...
    Docker Root Dir: /data/docker
    ...
    

    所以上面要获取 docker 的容器目录需要更改成/data/docker/containers,这个地方非常重要,当然如果你没有更改 docker 根目录则使用默认的/var/lib/docker/containers目录即可。

    分别创建上面的 ConfigMap 对象和 DaemonSet:

    [root@node01 ~]# kubectl create -f fluentd-configmap.yaml
    configmap "fluentd-config" created
    
    [root@node01 ~]# kubectl create -f fluentd-daemonset.yaml
    serviceaccount "fluentd-es" created
    clusterrole.rbac.authorization.k8s.io "fluentd-es" created
    clusterrolebinding.rbac.authorization.k8s.io "fluentd-es" created
    daemonset.apps "fluentd-es" created
    

    创建完成后,查看对应的 Pods 列表,检查是否部署成功:

    [root@node01 ~]# kubectl get pods -n logging
    NAME                    READY     STATUS    RESTARTS   AGE
    ......
    fluentd-es-5h8d8        1/1       Running   0          9s
    fluentd-es-fbvrx        1/1       Running   0          9s
    fluentd-es-jzhzs        1/1       Running   0          9s
    ......
    

    Fluentd 启动成功后,我们可以前往 Kibana 的 Dashboard 页面中,点击左侧的Discover,可以看到如下配置页面:

    图片.png-220.4kB

    在这里可以配置我们需要的 Elasticsearch 索引,前面 Fluentd 配置文件中我们采集的日志使用的是 logstash 格式,这里只需要在文本框中输入logstash-*即可匹配到 Elasticsearch 集群中的所有日志数据,然后点击下一步,进入以下页面:

    图片.png-201.2kB

    在该页面中配置使用哪个字段按时间过滤日志数据,在下拉列表中,选择@timestamp字段,然后点击Create index pattern

    图片.png-233.7kB

    创建完成后,点击左侧导航菜单中的Discover,然后就可以看到一些直方图和最近采集到的日志数据了:

    图片.png-384.5kB

    2.7 测试

    现在我们来将上一节课的计数器应用部署到集群中,并在 Kibana 中来查找该日志数据。

    新建 counter.yaml 文件,文件内容如下:

    [root@node01 ~]# vim counter.yaml
    [root@node01 ~]# cat counter.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)"; i=$((i+1)); sleep 1; done']
    

    该 Pod 只是简单将日志信息打印到 stdout,所以正常来说 Fluentd 会收集到这个日志数据,在 Kibana 中也就可以找到对应的日志数据了,使用 kubectl 工具创建该 Pod:

    [root@node01 ~]# kubectl create -f counter.yaml
    pod "counter" created
    

    Pod 创建并运行后,回到 Kibana Dashboard 页面,在上面的Discover页面搜索栏中输入kubernetes.pod_name:counter,就可以过滤 Pod 名为 counter 的日志数据:

    图片.png-390.7kB

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

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

    当然对于在生产环境上使用 Elaticsearch 或者 Fluentd,还需要结合实际的环境做一系列的优化工作,本文中涉及到的资源清单文件都可以在https://github.com/cnych/kubernetes-learning/tree/master/efkdemo找到。

  • 相关阅读:
    一个故事看懂CPU的SIMD技术
    MySQL 表数据多久刷一次盘?
    java高级用法之:无所不能的java,本地方法调用实况
    java高级用法之:调用本地方法的利器JNA
    网络协议之:socket协议详解之Datagram Socket
    网络协议之:socket协议详解之Socket和Stream Socket
    netty系列之:java中的base64编码器
    网络协议之:socket协议详解之Unix domain Socket
    git 常用命令
    在线curl转换
  • 原文地址:https://www.cnblogs.com/ywb123/p/15845000.html
Copyright © 2020-2023  润新知