kubernetes日志采集方案EFK

https://www.qikqiak.com/post/install-efk-stack-on-k8s/

EFK简介

Kubernetes中比较流行的日志收集解决方案是Elasticsearch、Fluentd 和 Kibana(EFK)技术栈,也是官方现在比较推荐的一种方案。

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

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

Kibana是Elasticsearch的一个功能强大的数据可视化 dashboard,Kibana提供了web 界面来浏览 Elasticsearch 日志数据。

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

创建 Elasticsearch 集群

在创建Elasticsearch集群之前,我们先创建一个命名空间,用来部署所有日志相关的资源对象。

[root@k8s-87 efk]# cat logging-ns.yaml 
apiVersion: v1
kind: Namespace
metadata:
  name: logging

通过kubectl创建该资源清单,创建一个名为logging的 namespace:

[root@k8s-87 efk]# kubectl apply -f logging-ns.yaml 
namespace/logging created
[root@k8s-87 efk]# kubectl get ns
NAME              STATUS   AGE
default           Active   7d5h
kube-node-lease   Active   7d5h
kube-public       Active   7d5h
kube-system       Active   7d5h
logging           Active   5s

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

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

首先创建一个名为es的headless service,新建文件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@k8s-87 efk]# kubectl apply -f elasticsearch-svc.yaml 
service/elasticsearch created
[root@k8s-87 efk]# kubectl get svc --namespace=logging
NAME   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
elasticsearch     ClusterIP   None                 9200/TCP,9300/TCP   22s

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

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

我们先列出完整的资源清单文件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: 192.168.200.197:80/apm/elasticsearch-oss:6.7.0
        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: 192.168.200.197:80/test-private/busybox:latest
        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: 192.168.200.197:80/test-private/busybox:latest
        command: ["sysctl", "-w", "vm.max_map_count=262144"]
        securityContext:
          privileged: true
      - name: increase-fd-ulimit
        image: 192.168.200.197:80/test-private/busybox:latest
        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: 20Gi
  1. 首先我们定义了一个名为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。

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
  1. 然后定义Pod模板部分内容:
 spec:
      containers:
      - name: es
        image: 192.168.200.197:80/apm/elasticsearch-oss:6.7.0
        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。
  • discovery.zen.minimum_master_nodes:我们将其设置为(N/2) + 1,N是我们的群集中符合主节点的节点的数量。我们有3个 Elasticsearch 节点,因此我们将此值设置为2(向下舍入到最接近的整数)。
  • ES_JAVA_OPTS:这里我们设置为-Xms512m -Xmx512m,告诉JVM使用512 MB的最小和最大堆。
  1. 添加关于initContainer的内容:
    initContainers:
      - name: fix-permissions
        image: 192.168.200.197:80/test-private/busybox:latest
        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: 192.168.200.197:80/test-private/busybox:latest
        command: ["sysctl", "-w", "vm.max_map_count=262144"]
        securityContext:
          privileged: true
      - name: increase-fd-ulimit
        image: 192.168.200.197:80/test-private/busybox:latest
        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 分区的。

  1. 添加数据目录的持久化相关的配置,在StatefulSet 中,使用 volumeClaimTemplates 来定义 volume 模板即可:
  volumeClaimTemplates:
  - metadata:
      name: data
      labels:
        app: elasticsearch
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: es-data-db
      resources:
        requests:
          storage: 20Gi

我们这里使用volumeClaimTemplates来定义持久化模板,Kubernetes 会使用它为Pod创建PersistentVolume,设置访问模式为ReadWriteOnce,这意味着它只能被mount到单个节点上进行读写,然后最重要的是使用了一个名为es-data-db的StorageClass对象,所以我们需要提前创建该对象,我们这里使用的NFS作为存储后端。

1) nfs-client-provisioner.yaml:

kind: ServiceAccount
apiVersion: v1
metadata:
  name: nfs-client-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: default
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
  1. nfs-deployment.yaml:
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-provisioner
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: 192.168.200.197:80/storage/nfs-client-provisioner:latest 
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: fuseim.pri/nfs
            - name: NFS_SERVER
              value: 192.168.200.13
            - name: NFS_PATH
              value: /nfs_data
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.200.13
            path: /nfs_data

3)nfs-class.yaml:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: es-data-db
provisioner: fuseim.pri/nfs 
parameters:
  archiveOnDelete: "false"  
[root@k8s-master01 nfs-client]#kubectl apply -f nfs-client-provisioner.yaml
[root@k8s-master01 nfs-client]# kubectl apply -f nfs-deployment.yaml 
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
serviceaccount/nfs-client-provisioner configured
deployment.extensions/nfs-client-provisioner created

[root@k8s-master01 nfs-client]# kubectl apply -f nfs-class.yaml 
storageclass.storage.k8s.io/es-data-db created

最后,我们指定了每个 PersistentVolume 的大小为 20GB,我们可以根据自己的实际需要进行调整该值。

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

$ kubectl create -f elasticsearch-storageclass.yaml
storageclass.storage.k8s.io "es-data-db" created
$ kubectl create -f elasticsearch-statefulset.yaml
statefulset.apps/es-cluster created

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

$ kubectl get sts -n logging
NAME         DESIRED   CURRENT   AGE
es-cluster   3         3         20h
$ kubectl get pods -n logging
NAME                      READY     STATUS    RESTARTS   AGE
es-cluster-0              1/1       Running   0          20h
es-cluster-1              1/1       Running   0          20h
es-cluster-2              1/1       Running   0          20h
$ kubectl get svc -n logging
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)             AGE
elasticsearch   ClusterIP   None                     9200/TCP,9300/TCP   20h

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

$ kubectl port-forward es-cluster-0 9200:9200  -n logging
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200

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

$ curl http://localhost:9200/_cluster/state?pretty

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

[root@k8s-87 ~]# curl http://localhost:9200/_cluster/state?pretty
{
  "cluster_name" : "k8s-logs",
  "cluster_uuid" : "AqSP93rdRqOq4nU4ukUTQA",
  "version" : 3,
  "state_uuid" : "-y36242YQMSPjfzWPE7Mwg",
  "master_node" : "GzddgY4MSNWdHFRTvt-pzA",
  "blocks" : { },
  "nodes" : {
    "JeVCX_McT16c-GM5Bs7rJg" : {
      "name" : "es-cluster-2",
      "ephemeral_id" : "dINEZ3WzRFauKDHpfud4qQ",
      "transport_address" : "10.244.0.10:9300",
      "attributes" : { }
    },
    "GzddgY4MSNWdHFRTvt-pzA" : {
      "name" : "es-cluster-0",
      "ephemeral_id" : "DIkhHzRXQ9SlnuDdm4oodg",
      "transport_address" : "10.244.2.10:9300",
      "attributes" : { }
    },
    "nJPdgyLvTZ2IPd7XvZiU3Q" : {
      "name" : "es-cluster-1",
      "ephemeral_id" : "xgsMropWT9i5LwWz6YVdSA",
      "transport_address" : "10.244.1.8:9300",
      "attributes" : { }
    }
  },
  "metadata" : {
    "cluster_uuid" : "AqSP93rdRqOq4nU4ukUTQA",
    "templates" : { },
    "indices" : { },
    "index-graveyard" : {
      "tombstones" : [ ]
    }
  },
  "routing_table" : {
    "indices" : { }
  },
  "routing_nodes" : {
    "unassigned" : [ ],
    "nodes" : {
      "JeVCX_McT16c-GM5Bs7rJg" : [ ],
      "nJPdgyLvTZ2IPd7XvZiU3Q" : [ ],
      "GzddgY4MSNWdHFRTvt-pzA" : [ ]
    }
  },
  "snapshot_deletions" : {
    "snapshot_deletions" : [ ]
  },
  "snapshots" : {
    "snapshots" : [ ]
  },
  "restore" : {
    "snapshots" : [ ]
  }
}

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

创建 Kibana 服务

Elasticsearch 集群启动成功了,我们就可以部署Kibana服务了,新建一个名为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: 192.168.200.197:80/efk/kibana-oss:6.7.2
        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 即可,此端点对应服务名称为es,由于是一个headless service,所以该域将解析为3个Elasticsearch Pod的IP地址列表。

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

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

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

[root@k8s-87 efk]# kubectl get all -n logging
NAME                          READY   STATUS    RESTARTS   AGE
pod/es-cluster-0              1/1     Running   0          15h
pod/es-cluster-1              1/1     Running   0          15h
pod/es-cluster-2              1/1     Running   0          15h
pod/kibana-69d97dbb7c-blllj   1/1     Running   0          48s

NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/elasticsearch       ClusterIP   None                    9200/TCP,9300/TCP   16h
service/kibana   NodePort    10.103.80.207           5601:32638/TCP      48s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kibana   1/1     1            1           48s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/kibana-69d97dbb7c   1         1         1       48s

NAME                          READY   AGE
statefulset.apps/es-cluster   3/3     15h

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

可以增加一点kibana自带的数据,直观地感受下:

登录kibana容器查看是否能调通es:

root@k8s-87 efk]# kubectl exec -it  kibana-7fdbd69496-64v49 -n logging   /bin/sh 
sh-4.2$ curl http://elasticsearch:9200
{
  "name" : "es-cluster-0",
  "cluster_name" : "k8s-logs",
  "cluster_uuid" : "_na_",
  "version" : {
    "number" : "6.7.0",
    "build_flavor" : "oss",
    "build_type" : "docker",
    "build_hash" : "8453f77",
    "build_date" : "2019-03-21T15:32:29.844721Z",
    "build_snapshot" : false,
    "lucene_version" : "7.7.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

部署 Fluentd

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

工作原理

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

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

配置

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

日志源配置

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



@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


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

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

路由配置

接下来看看如何将日志数据发送到 Elasticsearch:



@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



@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


  • 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。

安装Fluentd

要收集 Kubernetes 集群的日志,直接用 DasemonSet 控制器来部署 Fluentd 应用,这样,它就可以从 Kubernetes 节点上采集日志,确保在集群中的每个节点上始终运行一个 Fluentd容器。

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

kind: ConfigMap
apiVersion: v1
metadata:
  name: fluentd-config
  namespace: logging
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  system.conf: |-
    
      root_dir /tmp/fluentd-buffers/
    
  containers.input.conf: |-
    
      @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
    
    
      @id raw.kubernetes
      @type detect_exceptions
      remove_tag_prefix raw
      message log
      stream stream
      multiline_flush_interval 5
      max_bytes 500000
      max_lines 1000
    
  system.input.conf: |-
    
      @id journald-docker
      @type systemd
      filters [{ "_SYSTEMD_UNIT": "docker.service" }]
      
        @type local
        persistent true
      
      read_from_head true
      tag docker
    
    
      @id journald-kubelet
      @type systemd
      filters [{ "_SYSTEMD_UNIT": "kubelet.service" }]
      
        @type local
        persistent true
      
      read_from_head true
      tag kubelet
    
  forward.input.conf: |-
    
      @type forward
    
  output.conf: |-
    
      @type kubernetes_metadata
    
    
      @id elasticsearch
      @type elasticsearch
      @log_level info
      include_tag_key true
      host elasticsearch
      port 9200
      logstash_format true
      request_timeout    30s
      
        @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
      
    

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

然后新建一个 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
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
    spec:
      serviceAccountName: fluentd-es
      containers:
      - name: fluentd-es
        image: 192.168.200.197:80/efk/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"

意思就是要想采集节点的日志,那么我们就需要给节点打上上面的标签:

[root@k8s-87 efk]# kubectl label  nodes k8s-87 beta.kubernetes.io/fluentd-ds-ready=true
node/k8s-87 labeled
[root@k8s-87 efk]# kubectl label  nodes k8s-206 beta.kubernetes.io/fluentd-ds-ready=true
node/k8s-206 labeled
[root@k8s-87 efk]# kubectl label  nodes k8s-209 beta.kubernetes.io/fluentd-ds-ready=true
node/k8s-209 labeled

比如我们这里3个节点都打上了该标签:

[root@k8s-87 efk]# kubectl get nodes --show-labels
NAME      STATUS   ROLES    AGE   VERSION   LABELS
k8s-206   Ready       8d    v1.14.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-206,kubernetes.io/os=linux
k8s-209   Ready       8d    v1.14.1   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-209,kubernetes.io/os=linux
k8s-87    Ready    master   8d    v1.14.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/fluentd-ds-ready=true,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-87,kubernetes.io/os=linux,node-role.kubernetes.io/master=

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

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

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

$ docker info
...
Docker Root Dir:  /var/lib/docker
...

所以上面要获取docker的容器目录需要更改成/var/lib/docker/containers,这个是默认的目录。

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

$ kubectl create -f fluentd-configmap.yaml
configmap "fluentd-config" created
$ 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@k8s-87 efk]# kubectl get all -n logging                                                   
NAME                          READY   STATUS    RESTARTS   AGE
pod/es-cluster-0              1/1     Running   0          3h
pod/es-cluster-1              1/1     Running   0          3h
pod/es-cluster-2              1/1     Running   0          3h
pod/fluentd-es-mh4cg          1/1     Running   0          17s
pod/fluentd-es-ndhr4          1/1     Running   0          24s
pod/fluentd-es-sh2qk          1/1     Running   0          33s
pod/kibana-7fdbd69496-64v49   1/1     Running   0          161m

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/elasticsearch   ClusterIP   None                    9200/TCP,9300/TCP   170m
service/kibana          NodePort    10.106.112.51           5601:31439/TCP      161m

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                              AGE
daemonset.apps/fluentd-es   3         3         3       3            3           beta.kubernetes.io/fluentd-ds-ready=true   11m

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kibana   1/1     1            1           3h

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/kibana-67685dfc6c   0         0         0       3h
replicaset.apps/kibana-74b45566c8   0         0         0       170m
replicaset.apps/kibana-7fdbd69496   1         1         1       161m

NAME                          READY   AGE
statefulset.apps/es-cluster   3/3     3h

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

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

在该页面中配置使用哪个字段按时间过滤日志数据,在下拉列表中,选择@timestamp字段,然后点击Create index pattern,创建完成后,点击左侧导航菜单中的Discover,然后就可以看到一些直方图和最近采集到的日志数据了:

测试

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

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

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: 192.168.200.197:80/test-private/busybox:latest
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

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

$ kubectl create -f counter.yaml

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


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

至此我们就在Kubernetes 集群上成功部署了 EFK ,要了解如何使用 Kibana 进行日志数据分析,可以参考 Kibana 用户指南文档

当然对于在生产环境上使用Elaticsearch或者Fluentd,还需要结合实际的环境做一系列的优化工作。

参考

How To Set Up an Elasticsearch, Fluentd and Kibana (EFK) Logging Stack on Kubernetes
https://www.qikqiak.com/post/install-efk-stack-on-k8s/

你可能感兴趣的:(云原生)