集群日志管理方案

日志对于业务分析和系统分析而言是非常重要的数据。在一个 Kubernetes 集群中,大量容器应用运行在众多 Node 上,各容器和 Node 的系统组件都会生成许多日志文件。但是容器具有不稳定性,在发生故障时可能会被 Kubernetes 重新调度,Node 也可能会由于故障无法使用,造成日志丢失,这就要求管理员对容器和系统组件生成的日志进行统一规划和管理。

1. 应用日志的输出形式

容器应用可以选择将日志输出到下列不同的目标位置:

  • 输出到标准输出(stdout)和标准错误输出(stderr)。
  • 输出到容器内的某个日志文件。
  • 输出到某个外部系统。

1.1 输出到标准输出和标准错误输出

输出到标准输出和标准错误输出的日志通常由容器引擎接管,并保存在容器运行的 Node 上,例如 containerd 容器引擎会将日志保存到 /var/log/pods 目录下,并将日志文件软连接到 /var/log/containers 目录下。

输出到标准输出和标准错误输出的日志可以通过 kubectl logs 命令查看。

1.2 输出到容器内的某个日志文件

日志的保存位置依赖于容器应用使用的存储类型。如果未指定特别的存储,则容器内的应用程序生成的日志文件在容器的文件系统内,在容器退出时会被删除。需要将日志持久化存储时,容器可以选择使用 Kubernetes 提供的某种存储卷(Volume),例如 hostpath (保存在Node上)、nfs(保存在 NFS 服务器上)、PVC(保存在某种网络共享存储上)。

该方式输出的日志路径由应用程序决定,并且不能通过 kubectl logs 命令查看日志。

1.3 输出到某个外部系统

应用程序代码层面实现,直接在应用程序中就可以将日志推送到一个后端日志存储中心,这种方式需要改动源代码,也超出了 Kubernetes 本身的范围,一般不容易实现。

2. k8s 环境的日志采集方案

2.1 在节点上运行一个 agent 来采集日志

如果 pod 内的应用会将日志输出到标准输出和标准错误输出,这时我们可以在每个节点上运行一个采集日志的 agent,将 /var/log/containers 目录下的日志采集到日志系统后端程序。

优缺点:

  • 优点:部署简单,侵入性小
    • 在节点上运行一个日志收集的 agent 这种方式是常见的一种方法,因为它只需要在每个节点上运行一个代理程序,并不需要对节点上运行的应用程序进行更改,对应用程序没有任何侵入性。
  • 缺点:适用场景有局限
    • 仅仅适用于将日志输出到 stdout 和 stderr 的应用程序。

如果 pod 内的应用并不是把日志输出到标准输出和标准错误输出,而是输出到容器内的某个日志文件中,那么上述方案就无法采集到日志,可以使用以下优化方案:

在应用程序 pod 内嵌入一个 sidecar 容器,与产生日志的业务容器共享日志存储卷,然后 sidecar 容器读取容器内日志文件的内容输出到 stdout 和 stderr ,其后的步骤就与上述方案一致。

优缺点:

  • 优点:优化了上个方案的适用场景有限的问题
  • 缺点:业务容器将日志输出到容器内的日志文件中,sidecar 又将该日志输出到 stdout 和 stderr,而输出到 stdout 和 stderr 的日志会被存到宿主机的文件中,于是同一份日志存了两次,造成存储空间的浪费。

2.2 使用日志采集 sidecar 采集日志

为了解决存储空间浪费的问题,直接将 agent 以 sidecar 形式集成到 pod 内,即使用 sidecar 运行日志采集 agent。

优缺点:

  • 优点:不会造成存储空间的浪费,并且配置灵活。
  • 缺点:会增加额外的管理成本。

方案示例:agent 采用 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: fluentd
      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
YAML

3. 日志平台的架构方案

3.1 ELK 与 EFK 介绍

主流的 ELK (Elasticsearch,Logstash,Kibana)目前已经转变为 EFK (Elasticsearch,Fluentd ,Kiban)对于容器云的日志方案业内也普遍推荐采用 Fluentd,EFK 也是官方现在比较推荐的一种方案。

ELK

  • Elasticsearch:Elasticsearch 是一个开源的分布式搜索和分析引擎,用于存储、搜索和分析大规模的数据。它提供了强大的全文搜索功能和实时分析能力。
  • Logstash:Logstash 是一个用于日志收集、处理和传输的工具,可以从多个来源收集日志数据,对数据进行处理和转换,然后将数据发送到 Elasticsearch 或其他存储后端。
  • Kibana:Kibana 是一个用于数据可视化和分析的工具,可以与 Elasticsearch 集成,帮助用户创建实时的仪表板、图表和可视化报告,以便更好地理解和分析数据。

EFK

  • Elasticsearch:同样是用于存储、搜索和分析数据的分布式搜索引擎,提供了强大的数据存储和检索功能。
  • Fluentd:Fluentd 是一个开源的数据收集器,用于收集、转换和传输日志数据。它支持多种数据源和目标,可以将数据发送到 Elasticsearch 进行存储和分析。
  • 与 ELK 中的 Kibana 相同,用于数据可视化和分析,与 Elasticsearch 集成,帮助用户创建仪表板和图表。

主要区别:Logstash 和 Fluentd 都是用于日志收集和处理的工具,但在一些方面有所不同。Logstash 更加灵活和功能丰富,但相对消耗更多资源;Fluentd 更轻量级,性能较好,适合大规模部署。

Filebeats、Logstash、Elasticsearch 和 Kibana 是属于同一家公司的开源项目,官方文档如下:https://www.elastic.co/guide/index.html
Fluentd 则是另一家公司的开源项目,官方文档:https://docs.fluentd.org

Filebeat 是轻量级的收集本地 log 数据的方案,适合简单的日志收集任务。Filebeats 功能比较单一,它仅仅只能收集本地的 log,但并不对收集到的 log 做处理,所以通常 Filebeats 通常需要将收集到的 log 发送到 Logstash 做进一步的处理。

3.2 EFK+kafka+logstash 方案

在大规模集群中,日志量庞大,Elasticsearch 的写入压力过大,可以引入 kafka 进行削峰填谷,并将多个组件结合使用,流程如下:

Log Sources -> Fluentd -> Kafka -> Logstash -> Elasticsearch -> Kibana

详细工作流程:

  • 日志来源 (Log Sources):
    • 各种日志来源,包括应用程序、服务器、容器(如 Kubernetes 集群中的 Pod 日志)、网络设备等。
  • Fluentd:
    • 采集:Fluentd 从各种来源收集日志数据。
    • 初步处理:Fluentd 可以对数据进行简单的处理、过滤、格式化等。
    • 转发到 Kafka:Fluentd 将处理后的日志数据推送到 Kafka 主题中。Kafka 在这里起到日志缓冲和传输的作用,确保日志数据能够可靠地传递到下游系统。
  • Kafka:
    • 消息缓冲:Kafka 作为中间缓冲层,可以高效地处理和存储大量的日志数据流。
    • 消息传输:Kafka 将日志数据持久化到主题中,并提供高效的数据分发机制,允许多个消费者订阅和消费这些数据。
  • Logstash:
    • 从 Kafka 接收数据:Logstash 配置 Kafka 输入插件,从指定的 Kafka 主题中消费日志数据。
    • 复杂处理:Logstash 对数据进行更复杂的处理任务,如字段提取、格式转换、数据增强等。
    • 输出到 Elasticsearch:处理后的数据被发送到 Elasticsearch 进行存储和索引。
  • Elasticsearch:
    • 存储和索引:Elasticsearch 接收来自 Logstash 的处理后的日志数据,并对其进行存储和索引,提供高效的搜索和分析能力。
    • 集群扩展:支持分布式和高可用性,可以处理大规模的数据存储需求。
  • Kibana:
    • 查询与搜索:Kibana 允许用户基于 Elasticsearch 中的数据进行复杂的查询和搜索。
    • 数据可视化:Kibana 提供丰富的可视化工具,帮助用户从不同角度分析日志数据。
    • 仪表板:用户可以创建和定制仪表板,实时监控关键日志指标和事件。

4. EFK 构架示例

4.1. 安装 Elasticsearch 集群

4.1.1. ElasticSearch 安装最低配置要求

ElasticSearch 节点CPU 最小要求内存最小要求
elasticsearch master核心数 > 2内存 > 2 G
elasticsearch data核心数 > 1内存 > 2 G
elasticsearch client核心数 > 1内存 > 2 G

建议配置:每台机器 cpu 为 4 c,内存 >= 4 G。

4.1.2. 部署规划

节点类型副本数目存储大小网络模式描述
Master35 GClusterIP主节点,用于控制 ES 集群
Data310 GClusterIP数据节点,用于存储 ES 数据
Client2NodePort负责处理用户请求,实现请求转发、负载均衡

4.1.3. 为 ES 准备持久化存储

为了能够持久化 Elasticsearch 的数据,需要准备一个存储,此处我们使用 NFS 类型的StorageClass ,线上环境建议使用 Local PV 或者 Ceph RBD。

找一台服务器安装 nfs 服务端:

# 内网环境可以直接关闭防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service

# 安装
yum install -y nfs-utils rpcbind

# 创建数据共享目录
mkdir -p /data/nfs
chmod 755 /data/nfs

# 配置共享目录
cat >  /etc/exports <<EOF
/data/nfs *(rw,sync,no_root_squash)
EOF

# *:表示任何人都有权限连接,当然也可以是一个网段,一个 IP,也可以是域名
# rw:读写的权限
# sync:表示文件同时写入硬盘和内存
# no_root_squash:当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份

# 启动nfs服务
systemctl start rpcbind.service
systemctl enable rpcbind
systemctl status rpcbind
systemctl start nfs
systemctl enable nfs
systemctl status nfs
ShellScript

k8s 集群节点上安装客户端:

sudo yum install -y nfs-utils 

sudo apt install -y nfs-common
ShellScript

搭建 StorageClass + NFS:

helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

# 镜像地址可以更换为自己的
helm upgrade --install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --set nfs.server=192.168.2.50 --set nfs.path=/data/nfs --set storageClass.defaultClass=true --set image.repository=crpi-sozjkv641zbs4m9x.cn-shenzhen.personal.cr.aliyuncs.com/pingk-k8s-test/nfs-subdir-external-provisioner --set image.tag=v4.0.2 -n kube-system
ShellScript

4.1.4. 为 ES 准备证书文件

生成证书文件:

# 运行容器生成证书
mkdir -p /logging/elastic-certs
nerdctl run --name elastic-certs -v /logging/elastic-certs:/app -it -w /app elasticsearch:7.17.3 /bin/sh -c "elasticsearch-certutil ca --out /app/elastic-stack-ca.p12 --pass '' && elasticsearch-certutil cert --name security-master --dns security-master --ca /app/elastic-stack-ca.p12 --pass '' --ca-pass '' --out /app/elastic-certificates.p12"

# 删除容器
nerdctl rm -f elastic-certs
# 将 pcks12(Public Key Cryptography Standards #12)文件中的证书和私钥提取出来,并保存为 PEM 格式的文件
# cd /logging/elastic-certs
# cd elastic-certs && openssl pkcs12 -nodes -passin pass:'' -in elastic-certificates.p12 -out elastic-certificate.pem
ShellScript

添加证书到 Kubernetes:

# 添加证书
cd /logging/elastic-certs
kubectl create ns logging
kubectl create secret -n logging generic elastic-certs --from-file=elastic-certificates.p12
# 设置集群用户名密码,用户名为 elastic,密码为 pingk123456
# 密码不能为纯数字,否则后面安装 kibana 会报错
kubectl create secret generic elastic-auth -n logging --from-literal=username=elastic --from-literal=password=pingk123456
ShellScript

4.1.5. 安装 ES 集群

添加 ELastic 的 Helm 仓库:

helm repo add elastic https://helm.elastic.co
helm repo update
ShellScript

ElaticSearch 安装需要安装三次,分别安装 Master、Data、Client 节点,Master 节点负责集群间的管理工作;Data 节点负责存储数据;Client 节点负责代理 ElasticSearch Cluster 集群,负载均衡。
首先使用 helm pull 拉取 Chart 并解压:

 helm pull elastic/elasticsearch --untar --version 7.17.3
 cd elasticsearch
ShellScript

在 Chart 目录下面创建用于 Master 节点安装配置的 values 文件:

# 创建一个新文件 values-master.yaml,内容如下

## 设置集群名称
clusterName: 'elasticsearch'
## 设置节点名称
nodeGroup: 'master'
## 设置角色
roles:
  master: 'true'
  ingest: 'false'
  data: 'false'

# ============镜像配置============
## 指定镜像与镜像版本
image: 'elasticsearch'  # 此处会去官网下载,对应的访问地址为:docker.io/library/elasticsearch:7.17.3
#image: 'registry.cn-hangzhou.aliyuncs.com/egon-k8s-test/elasticsearch' # 可以用自己的镜像,你的镜像仓库必须是公开的
imageTag: '7.17.3'
imagePullPolicy: 'IfNotPresent'
## 副本数: 
replicas: 3

# ============资源配置============
## JVM 配置参数
esJavaOpts: '-Xmx1g -Xms1g'
## 部署资源配置(生产环境要设置大些)
resources:
  requests:
    cpu: '2000m'
    memory: '2Gi'
  limits:
    cpu: '2000m'
    memory: '2Gi'
## 数据持久卷配置
persistence:
 enabled: true
## 存储数据大小配置
volumeClaimTemplate:
  storageClassName: nfs-client
  accessModes: ['ReadWriteOnce']
  resources:
    requests:
      storage: 5Gi

# ============安全配置============
## 设置协议,可配置为 http、https
protocol: http
## 证书挂载配置,这里我们挂入上面创建的证书
secretMounts:
  - name: elastic-certs
    secretName: elastic-certs
    path: /usr/share/elasticsearch/config/certs
    defaultMode: 0755

## 允许您在/usr/share/elasticsearch/config/中添加任何自定义配置文件,例如 elasticsearch.yml、log4j2.properties
## ElasticSearch 7.x 默认安装了 x-pack 插件,部分功能免费,这里我们配置下
## 下面注掉的部分为配置 https 证书,配置此部分还需要配置 helm 参数 protocol 值改为 https
esConfig:
  elasticsearch.yml: |
    xpack.security.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: certificate
    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.enabled: true
# xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12

## 环境变量配置,这里引入上面设置的用户名、密码 secret 文件
extraEnvs:
  - name: ELASTIC_USERNAME
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: username
  - name: ELASTIC_PASSWORD
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: password
        
# ============调度配置============
## 设置调度策略
## - hard:只有当有足够的节点时 Pod 才会被调度,并且它们永远不会出现在同一个节点上
## - soft:尽最大努力调度
antiAffinity: 'soft'
# tolerations:
#   - operator: "Exists" ##容忍全部污点
YAML

然后创建用于 Data 节点安装的 values 文件:

# 创建文件:values-data.yaml

# ============设置集群名称============
## 设置集群名称
clusterName: 'elasticsearch'
## 设置节点名称
nodeGroup: 'data'
## 设置角色
roles:
  master: 'false'
  ingest: 'true'
  data: 'true'
  
# ============镜像配置============
## 指定镜像与镜像版本
image: 'elasticsearch' 
#image: 'registry.cn-hangzhou.aliyuncs.com/egon-k8s-test/elasticsearch' 
imageTag: '7.17.3'
## 副本数(建议设置为3,资源不足可以只设置1个副本)
replicas: 3

# ============资源配置============
## JVM 配置参数
esJavaOpts: '-Xmx1g -Xms1g'
## 部署资源配置(生产环境一定要设置大些)
resources:
  requests:
    cpu: '1000m'
    memory: '2Gi'
  limits:
    cpu: '1000m'
    memory: '2Gi'
## 数据持久卷配置
persistence:
  enabled: true
## 存储数据大小配置
volumeClaimTemplate:
  storageClassName: nfs-client
  accessModes: ['ReadWriteOnce']
  resources:
    requests:
      storage: 10Gi
      
# ============安全配置============
## 设置协议,可配置为 http、https
protocol: http
## 证书挂载配置,这里我们挂入上面创建的证书
secretMounts:
  - name: elastic-certs
    secretName: elastic-certs
    path: /usr/share/elasticsearch/config/certs
## 允许您在/usr/share/elasticsearch/config/中添加任何自定义配置文件,例如 elasticsearch.yml
## ElasticSearch 7.x 默认安装了 x-pack 插件,部分功能免费,这里我们配置下
## 下面注掉的部分为配置 https 证书,配置此部分还需要配置 helm 参数 protocol 值改为 https
esConfig:
  elasticsearch.yml: |
    xpack.security.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: certificate
    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.enabled: true
# xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12

# 禁用了 Elasticsearch 从外部下载 GeoIP 数据库的功能(GeoIP 数据库用于将 IP 地址映射到地理位置)
# 这在以下场景中非常有用:
# 1、日志分析:了解用户访问来源的地理位置。
# 2、安全审计:检测异常的地理位置访问。
# 3、内容个性化:根据用户位置提供个性化内容
# 如果你没有需要使用 GeoIP 数据库的特定需求,禁用这个选项是完全可以的,特别是在内网环境中,这样可以避免连接外网的问题
   ingest.geoip.downloader.enabled: 'false'
## 环境变量配置,这里引入上面设置的用户名、密码 secret 文件
extraEnvs:
  - name: ELASTIC_USERNAME
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: username
  - name: ELASTIC_PASSWORD
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: password

# ============调度配置============
## 设置调度策略
## - hard:只有当有足够的节点时 Pod 才会被调度,并且它们永远不会出现在同一个节点上
## - soft:尽最大努力调度
antiAffinity: 'soft'
## 容忍配置
# tolerations:
#   - operator: "Exists" ##容忍全部污点
YAML

用于创建 Client 节点的 values 文件:

# 创建文件:values-client.yaml

# ============设置集群名称============
## 设置集群名称
clusterName: 'elasticsearch'
## 设置节点名称
nodeGroup: 'client'
## 设置角色
roles:
  master: 'false'
  ingest: 'false'
  data: 'false'
  
# ============镜像配置============
## 指定镜像与镜像版本
image: 'elasticsearch'
# image: 'registry.cn-hangzhou.aliyuncs.com/egon-k8s-test/elasticsearch'
imageTag: '7.17.3'
## 副本数
# 测试环境资源有限,所以设置为1吧
replicas: 2

# ============资源配置============
## JVM 配置参数
esJavaOpts: '-Xmx1g -Xms1g'
## 部署资源配置(生产环境一定要设置大些)
resources:
  requests:
    cpu: '1000m'
    memory: '2Gi'
  limits:
    cpu: '1000m'
    memory: '2Gi'
## 数据持久卷配置
persistence:
  enabled: false
  
# ============安全配置============
## 设置协议,可配置为 http、https
protocol: http
## 证书挂载配置,这里我们挂入上面创建的证书
secretMounts:
  - name: elastic-certs
    secretName: elastic-certs
    path: /usr/share/elasticsearch/config/certs
## 允许您在/usr/share/elasticsearch/config/中添加任何自定义配置文件,例如 elasticsearch.yml
## ElasticSearch 7.x 默认安装了 x-pack 插件,部分功能免费,这里我们配置下
## 下面注掉的部分为配置 https 证书,配置此部分还需要配置 helm 参数 protocol 值改为 https
esConfig:
  elasticsearch.yml: |
    xpack.security.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: certificate
    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.enabled: true
# xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
# xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
 
## 环境变量配置,这里引入上面设置的用户名、密码 secret 文件
extraEnvs:
  - name: ELASTIC_USERNAME
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: username
  - name: ELASTIC_PASSWORD
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: password
        
# ============Service 配置============
service:
  type: NodePort
  nodePort: '30200'
YAML

安装:

 # --------------->>>>>>>>>>> 注意install指定的release名字不能重复
# helm install 你起的release名 你的chart包的路径 -f values-master.yaml --namespace logging
# 如果是升级安装则用:helm upgrade --install 你起的release名 你的chart包的路径 -f values-master.yaml --namespace logging

cd elasticsearch/
# 安装 master 节点
helm install es-master ./ -f values-master.yaml --namespace logging
# 安装 data 节点
helm install es-data ./ -f values-data.yaml --namespace logging
# 安装 client 节点
helm install es-client ./ -f values-client.yaml --namespace logging

# 升级操作示例
helm upgrade --install es-master ./ -f values-master.yaml --namespace logging
ShellScript

4.2. 安装 Kibana

下载并解压 chart 包:

helm pull elastic/kibana --untar --version 7.17.3
cd kibana
ShellScript

创建用于安装 Kibana 的 values 文件:

# 创建全新文件:values-prod.yaml

## 指定镜像与镜像版本
image: 'registry.cn-hangzhou.aliyuncs.com/egon-k8s-test/kibana'
#image: 'docker.elastic.co/kibana/kibana'
imageTag: '7.17.3'
imagePullPolicy: "IfNotPresent"

## 配置连接 ElasticSearch 地址,使用的es-client的svc
elasticsearchHosts: 'http://elasticsearch-client:9200'

# ============环境变量配置============
## 环境变量配置,这里引入上面设置的用户名、密码 secret 文件
extraEnvs:
  - name: 'ELASTICSEARCH_USERNAME'
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: username
  - name: 'ELASTICSEARCH_PASSWORD'
    valueFrom:
      secretKeyRef:
        name: elastic-auth
        key: password

# ============资源配置============
resources:
  requests:
    cpu: '500m'
    memory: '1Gi'
  limits:
    cpu: '500m'
  memory: '1Gi'

# ============配置 Kibana 参数============
## kibana 配置中添加语言配置,设置 kibana 为中文
kibanaConfig:
  kibana.yml: |
    i18n.locale: "zh-CN"
    server.publicBaseUrl: "http://192.168.2.201:30601"   #这里地址改为你访问 kibana 的地址,不能以 / 结尾

# ============Service 配置============
service:
  type: NodePort
  nodePort: '30601'
YAML

部署:

helm install kibana ./ -f values-prod.yaml --namespace logging
ShellScript

上面我们安装 Kibana 的时候指定了 30601 的 NodePort 端口,所以我们可以使用任意 k8s 集群节点 IP+30601 访问,例如:http://192.168.2.201:30601。用户名和密码为 4.1.4. 小结设置的用户名和密码。

4.3. 安装 Fluentd 采集日志

Fluentd 的配置文件中主要三段:日志源配置(采集日志数据)、路由(日志发往的目标)、过滤(过滤掉一些无用数据)。

4.3.1. 日志源配置

<source>
  @id fluentd-containers.log              
  @type tail                     # Fluentd 内置的输入方式,其原理是不停地从源文件中获取最新的日志。
  path /var/log/containers/*.log # 挂载的宿主机容器日志地址
  
  pos_file /var/log/es-containers.log.pos  # 指定一个位置文件(position file)。位置文件是一个记录文件读取位置的文件,用于在文件轮转(log rotation)或重新启动时,帮助应用程序记住上次读取的位置,以便继续从上次位置读取日志文件。
  tag raw.kubernetes.*           # 设置日志标签        
  read_from_head true            # 表示 Fluentd 在第一次读取文件时,从文件头开始读取(而不是只读取新增的部分)。适用于启动时需要读取已有日志的场景。
  <parse>                        # 多行格式化成 JSON     
    @type multi_format           # 使用 multi-format-parser 解析器插件        
    <pattern>
      format json                # JSON 解析器       
      time_key time              # 指定事件时间的时间字段      
      time_format %Y-%m-%dT%H:%M:%S.%NZ  # 时间格式
    </pattern>
    <pattern>
      format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
      time_format %Y-%m-%dT%H:%M:%S.%N%:z
    </pattern>
  </parse>
</source>
YAML

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

  • <source>:表示 Fluentd 的输入源配置块的开始。
  • @id fluentd-containers.log:为输入源配置块指定一个唯一的标识符,用于在 Fluentd 配置中引用该输入源。
  • @type tail:tail 是 Fluentd 内置的输入插件类型,类似于 Linux 命令 tail -f,用于从日志文件中读取新增的日志行,适合处理容器或文件系统中的日志文件。
  • path /var/log/containers/*.log:tail 类型下的特定参数,告诉 Fluentd 采集 /var/log/containers 目录下的所有日志,这是 containerd 容器引擎在 Kubernetes 节点上用来存储运行容器 stdout 输出日志数据的目录。
  • pos_file:pos_file 是指定一个用于记录读取进度的文件(即日志文件读取到了哪个位置)。如果 Fluentd 重启或遇到故障,它会从上次停止的地方继续读取日志,避免日志丢失或重复处理。
  • tag raw.kubernetes.*:这个标签会应用于所有从日志文件中读取的数据,方便后续的过滤、转发和处理操作。raw.kubernetes.* 是一个通配符标签。
  • read_from_head true:表示 Fluentd 在第一次读取文件时,从文件头开始读取(而不是只读取新增的部分)。适用于启动时需要读取已有日志的场景。
  • <parse>:表示开始配置日志解析的部分,定义如何解析日志内容。
    • @type multi_format:指定使用 multi-format-parser 插件进行日志解析,支持多种日志格式。它会按照配置顺序依次尝试每个解析器,直到其中一个解析器成功解析当前日志行为止,如下定义了两个 <pattern> 就是两个解析器,第一个是 json 解析、第二个是正则解析,解析顺序为:
      • Fluentd 先会尝试使用 JSON 解析器解析日志。
      • 如果日志行是一个有效的 JSON 对象,解析成功,Fluentd 会提取对应字段并完成日志处理。
      • 如果日志行不是有效的 JSON(例如是纯文本或其他格式),JSON 解析器会失败,Fluentd 会继续下一步。
      • 如果 JSON 解析失败,Fluentd 会尝试使用正则表达式解析。
      • Fluentd 会用正则表达式匹配当前日志行,并提取指定的字段(如时间、流类型、日志内容)。
      • 如果日志行符合正则表达式的模式,则解析成功,Fluentd 会处理该日志。
      • 如果日志行不符合正则表达式,解析也会失败。
    • 第一个 <pattern>:定义日志解析的模式为 JSON 格式。
      • format json:第一个匹配规则是 JSON 格式,Fluentd 会尝试将日志解析为 JSON 格式。
      • time_key time:指定日志中的 time 字段为事件时间。
      • time_format %Y-%m-%dT%H:%M:%S.%NZ:设置时间字段的格式,采用 ISO8601 格式,精确到毫秒,后面有 Z 表示 UTC 时间。
    • 第二个 <pattern>:定义日志解析的模式为正则表达式格式。
      • (?<time>.+) :提取日志中的时间部分。
      • (?<stream>stdout|stderr) :提取日志输出的流类型,可能是 stdout 或 stderr 。
      • (?<log>.*) :提取日志的实际内容。

4.3.2. 路由配置

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

<match **>  # <match **> 中 math 后跟着的是一个匹配日志源的正则,此处的两个星号**代表捕获所有的日志并将它们发送给 Elasticsearch。
  @id elasticsearch  # 为这个输出插件配置指定了一个唯一的 ID elasticsearch,便于管理和识别。
  @type elasticsearch  # 使用了 Fluentd 的内置 elasticsearch 插件作为日志输出插件,将日志数据发送到 Elasticsearch。
  @log_level info  # 日志级别配置成i nfo,表示任何该级别及者该级别以上(INFO、WARNING、ERROR)的日志都将被路由到 ES。
  include_tag_key true  # 设置为 true,表示在发送到 Elasticsearch 的日志数据中会包含日志的标签(tag)。这是有用的,可以在 Elasticsearch 中使用标签进行筛选和查询
  type_name fluentd  # 用于指定 Elasticsearch 中索引的类型名称,这会影响数据在 Elasticsearch 中的存储方式。
  
# 定义 Elasticsearch 的地址,也可以配置认证信息,我们的 Elasticsearch 不需要认证,所以这里直接指定 host 和 port 即可。
  host "#{ENV['OUTPUT_HOST']}" 
# 表示环境变量 OUTPUT_HOST 的值会被插入到配置中。该环境变量的值是 Elasticsearch 的主机名或 IP 地址
  port "#{ENV['OUTPUT_PORT']}"
 
# 设置为 true,表示 Fluentd 将日志数据以 Logstash 格式发送到 Elasticsearch。Logstash 格式通常包括时间戳、日志级别、消息内容等,有助于结构化日志的处理。
  logstash_format true
  
# Fluentd 允许在目标(对接的下游服务)不可用时进行缓存,比如,如果网络出现故障或者 ES 不可用的时候。缓冲区配置也有助于降低磁盘的 IO。
  <buffer> 
    @type file # 设置缓存类型为 file,表示 Fluentd 将日志数据缓存在本地文件中。这对于处理大量日志或网络故障时非常有用。
    path /var/log/fluentd-buffers/kubernetes.system.buffer # 缓存文件存储的路径。日志数据会被写入这个路径下的文件中。
    flush_mode interval  # 设置为 interval,表示按照时间间隔进行数据刷新(将数据从缓冲区发送到 Elasticsearch)。
    retry_type exponential_backoff # 设置重试类型为 exponential_backoff,这意味着在网络错误或 Elasticsearch 不可用时,Fluentd 会进行指数退避重试,逐渐增加重试间隔。
    flush_thread_count 2  # 设置用于刷新缓冲区的线程数为 2。提高线程数可以提升数据的处理能力。
    flush_interval 5s # 设置刷新间隔为 5 秒。这意味着每隔 5 秒,Fluentd 将尝试将缓冲区中的数据发送到 Elasticsearch。
    retry_forever # 默认值为 true,表示在 Elasticsearch 不可用时会无限重试,不会放弃。
    retry_max_interval 30 # 设置重试的最大间隔时间为 30 秒。如果 Elasticsearch 长时间不可用,重试间隔会增加,直到达到这个最大值。
    chunk_limit_size "#{ENV['OUTPUT_BUFFER_CHUNK_LIMIT']}" # 设置缓冲区中单个数据块的最大尺寸,通过环境变量 OUTPUT_BUFFER_CHUNK_LIMIT 配置。限制数据块的大小有助于管理缓冲区内存使用。
    queue_limit_length "#{ENV['OUTPUT_BUFFER_QUEUE_LIMIT']}" # 设置缓冲区队列的最大长度,通过环境变量 OUTPUT_BUFFER_QUEUE_LIMIT 配置。超过这个长度的日志将会被阻塞或丢弃。
    overflow_action block # 设置溢出行为为 block,这意味着如果缓冲区队列长度超过 queue_limit_length,新的日志写入将被阻塞,直到队列长度减少到可接受的范围内。
  </buffer>
</match>
YAML

这段配置通过 Fluentd 将所有日志数据发送到 Elasticsearch。它设置了缓存策略、重试机制、缓冲区参数等,以确保在网络故障或 Elasticsearch 不可用时,日志数据能够被有效地缓冲并在恢复后重新发送。此外,它还配置了日志级别、标签处理和格式设置,以便更好地管理和查询日志数据。

4.3.3. 过滤

由于 Kubernetes 集群中应用太多,也还有很多历史数据,所以我们可以只将某些应用的日志进行收集,其他的都过滤掉,那就需要用配置过滤。
比如我们只想采集具有 logging=true 这个 Label 标签的 Pod 日志,如下所示:

# 删除无用的日志字段/属性,以简化日志数据并减小数据存储的负担。
<filter kubernetes.**> # 这个过滤器应用于所有日志标签以 kubernetes. 开头的日志
  @type record_transformer # 使用 record_transformer 插件,它允许对日志记录进行转换,如添加、修改或删除字段
  remove_keys $.docker.container_id,$.kubernetes.container_image_id,$.kubernetes.pod_id,$.kubernetes.namespace_id,$.kubernetes.master_url,$.kubernetes.labels.pod-template-hash
# remove_keys 指定了要从日志记录中删除的字段。这里列出的字段包括:
 # $.docker.container_id: Docker 容器 ID
 # $.kubernetes.container_image_id: Kubernetes 容器镜像 ID
 # $.kubernetes.pod_id: Kubernetes Pod ID
 # $.kubernetes.namespace_id: Kubernetes 命名空间 ID
 # $.kubernetes.master_url: Kubernetes Master URL
 # $.kubernetes.labels.pod-template-hash: Kubernetes Pod 模板哈希标签
</filter>


# 只保留具有 logging=true 标签的 Pod 日志
<filter kubernetes.**> # # 这个过滤器同样应用于所有标签以 kubernetes. 开头的日志。
  @id filter_log # 为这个过滤器配置一个唯一的 ID filter_log,便于                管理和调试。
  @type grep     # 使用 grep 插件,它用于按指定的模式过滤日志记录。             只有匹配模式的日志才会被保留下来。
  <regexp>
    key $.kubernetes.labels.logging  # 这是要检查的字段,表示 Kubernetes Pod 的标签 logging。
    pattern ^true$  # 这是正则表达式模式,用于匹配字段值。如果 $.kubernetes.labels.logging 字段的值是 true,则日志记录会被保留。否则,日志记录会被丢弃。 
  </regexp>
</filter>
YAML

4.3.4. 安装 Fluentd

要想在 k8 每个物理节点都能采集到数据,我们可以直接用 DasemonSet 控制器来部署 Fluentd 应用,确保在集群中的每个节点上始终运行一个 Fluentd 容器,可以直接使用 Helm 来进行一键安装,为了能够了解更多实现细节,我们这里还是采用手动方法来进行
安装。

官网部署参考:https://docs.fluentd.org/container-deployment/kubernetes

fluented 配置解析,采集容器日志与处理的流转流程如下:

  • 日志采集:从指定的路径读取日志文件并打上标签 raw.kubernetes.*。
    • 志源:从容器日志文件 /var/log/containers/*.log 中读取日志。
    • 标签:日志被打上 raw.kubernetes.* 标签,这里的 * 是通配符,表示所有相关的日志都会用这个标签。
    • 解析:使用 multi_format 解析器处理日志,先尝试 JSON 解析,如果失败,再用正则表达式解析。
  • 异常检测+处理 (处理 raw.kubernetes.** 标签的日志):
    • 标签匹配:匹配所有以 raw.kubernetes. 开头的日志。
    • 插件:使用 detect_exceptions 插件检测异常信息,并处理这些异常栈。
    • 标签处理:去除日志标签中的 raw 前缀,将 raw.kubernetes.some_log 转换为kubernetes.some_log。
    • 日志字段:指定 log 字段作为日志消息的主要内容。
    • 多行处理:确保多行日志在 5 秒内被合并为一条完整的日志记录。
  • 过滤器配置:
    • 拼接日志(针对所有日志)
      • 匹配:所有日志(**)。
      • 插件:使用 concat 插件将多行日志拼接成一条完整的日志记录。
      • 拼接规则:根据换行符 \n 拼接日志行,拼接后的日志条目之间没有额外分隔符。
    • 添加 Kubernetes 元数据。
      • 匹配:所有以 kubernetes. 开头的日志。
      • 插件:使用 kubernetes_metadata 插件为日志添加 Kubernetes 相关元数据。
    • 解析 JSON 字段。
      • 匹配:所有以 kubernetes. 开头的日志。
      • 插件:使用 parser 插件来处理 JSON 格式的日志字段,保留原始数据,并移除 log 字段后进行进一步解析。
    • 删除多余字段。
      • 匹配:所有以 kubernetes. 开头的日志。
      • 插件:使用 record_transformer 插件删除指定的字段,清理不必要的日志数据。
    • 筛选符合条件的日志。
      • 匹配:所有以 kubernetes. 开头的日志。
      • 插件:使用 grep 插件根据 $.kubernetes.labels.logging 字段的值过滤日志,仅保留 logging=true 的日志。
# fluentd-configmap.yaml
 
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-conf
  namespace: logging
data:
  # 配置采集与处理容器日志
  containers.input.conf: |-
    <source>
      @id fluentd-containers.log
      @type tail        
      path /var/log/containers/*.log
      pos_file /var/log/es-containers.log.pos
      tag raw.kubernetes.*
      read_from_head true
      <parse>   
        @type multi_format
        <pattern>
          format json
          time_key time
          time_format %Y-%m-%dT%H:%M:%S.%NZ
        </pattern>
        <pattern>
          format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
          time_format %Y-%m-%dT%H:%M:%S.%N%:z
        </pattern>
      </parse>
    </source>
    
    <match raw.kubernetes.**>
      @id raw.kubernetes
      @type detect_exceptions
      
      remove_tag_prefix raw
      message log
      multiline_flush_interval 5
    </match>
    
    <filter **>
      @id filter_concat
      @type concat
      key message
      multiline_end_regexp /\n$/  
      separator "" 
    </filter>

    <filter kubernetes.**>
      @id filter_kubernetes_metadata
      @type kubernetes_metadata 
    </filter>

    <filter kubernetes.**> 
      @id filter_parser
      @type parser
      key_name log
      reserve_data true           # 设置为 true,表示在解析过程中保留原始数据。这有助于保留日志数据的原始形式,便于调试和进一步分析。
      remove_key_name_field true  # 设置为 true,表示在成功解析日志后,移除 key_name 字段。这样可以在解析后去掉指定的日志字段名称。
      
      <parse>    # 依次运行多个解析pattern,运行不成功才会进行下一个
        @type multi_format
        <pattern>
          format json
        </pattern>
        <pattern>
          format none  # 对于无法解析为 JSON 的数据,none 表示不进行任何处理。可以用于处理非 JSON 格式的日志数据。
        </pattern>
      </parse>
    </filter>

    <filter kubernetes.**>
      @type record_transformer
      remove_keys $.docker.container_id,$.kubernetes.container_image_id,$.kubernetes.pod_id,$.kubernetes.namespace_id,$.kubernetes.master_url,$.kubernetes.labels.pod-template-hash
    </filter>
    
    <filter kubernetes.**>
      @id filter_log
      @type grep
      <regexp>
        key $.kubernetes.labels.logging
        pattern ^true$
      </regexp>
    </filter>
    

  # 监听配置,一般用于日志聚合用
  forward.input.conf: |-
    # 监听通过 TCP 发送的消息
    <source>
      @id forward
      @type forward  # forward 插件会监听通过 TCP 协议发送到 Fluentd 的日志消息。其他 Fluentd 实例或应用程序可以将日志数据发送到这个 Fluentd 实例,该实例会接收并处理这些日志消息
      
      # 默认情况下,forward 插件会监听 TCP 端口 24224。您可以根据需要修改端口号和其他设置。要进行自定义配置,如设置端口号、绑定地址等,可以添加相应的配置参数。例如
      # port 24224  # 设置监听的端口号
      # bind 0.0.0.0  # 绑定所有网络接口
      
      # 这个配置通常用于构建多实例 Fluentd 部署,其中一个实例作为集中式日志接收器,接收来自不同实例或服务的日志数据。这种设置有助于集中管理和处理日志数据,并将日志数据进一步转发到其他系统(如 Elasticsearch、Kafka、文件等)。
    </source>
    
    
  # 配置将日志数据发送到 Elasticsearch
  output.conf: |-
    <match **>
      @id elasticsearch
      @type elasticsearch
      @log_level info
      include_tag_key true
      
      # 配置访问ES的svc与端口,账号密码
      host elasticsearch-client # 指定ES的client的svc名
      port 9200                 
      user elastic # FLUENT_ELASTICSEARCH_USER | FLUENT_ELASTICSEARCH_PASSWORD
      password pingk123456
      
      # 配置采集的日志使用 logstash 格式,并且配置日志前缀为 k8s,后续在es中索引查询要用到该前缀。
      logstash_format true
      logstash_prefix k8s
    
      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>
YAML

上述配置会采集物理节点上的 /var/log/containers/*.log 日志,然后进行处理后发送到 elasticsearch-client: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
  namespace: logging
  labels:
    app: fluentd
    kubernetes.io/cluster-service: 'true'
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
        kubernetes.io/cluster-service: 'true'
    spec:
      # 1、控制只调度到带有下述标签的节点上,即只采集固定的节点
      # nodeSelector:
      # beta.kubernetes.io/fluentd-ds-ready: 'true'
      # 2、我们是kubeadm部署的k8s,默认情况下master节点有污点,如果想采集master,则需要加上容忍
      tolerations:
        - key: node-role.kubernetes.io/master
          effect: NoSchedule
      serviceAccountName: fluentd-es
      containers:
        - name: fluentd
          image: registry.cn-hangzhou.aliyuncs.com/egon-k8s-test/fluentd:v3.4.0
          # image: quay.io/fluentd_elasticsearch/fluentd:v3.4.0
          volumeMounts:
            - name: fluentconfig
              mountPath: /etc/fluent/config.d
            - name: varlog
              mountPath: /var/log
      volumes:
        - name: fluentconfig
          configMap:
            name: fluentd-conf
        - name: varlog
          hostPath:
            path: /var/log
YAML

想要只采集某些节点的日志,可以添加一个 nodSelector 属性:

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

对应着,你想采集哪个节点的日志,就要给该节点打上标签:

kubectl label nodes [node名字] beta.kubernetes.io/fiuentd-ds-ready=true
ShellScript

部署:

kubectl apply -f fluentd-configmap.yaml
kubectl apply -f fluentd-daemonset.yaml

# 查看部署结果
kubectl  -n  logging get pods  -o wide
ShellScript

5. 测试

Fluentd 启动成功后,这个时候就可以发送日志到 ES 了,但是我们这里是过滤了只采集具有 logging=true 标签的 Pod 日志,所以现在还没有任何数据会被采集。

创建测试 Pod:

# counter.yaml
apiVersion: v1
kind: Pod
metadata:
  name: counter
  labels:
    logging: 'true' # 一定要具有该标签才会被采集
spec:
  containers:
    - name: count
      image: centos
      args:
        [
          /bin/sh,
          -c,
          'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done',
        ]
ShellScript