一、组件说明
Fluentd
负责从 Kubernetes 搜集日志,每个 node 节点上面的 fluentd 监控并收集该节点上面的系统+容器日志,并将处理过后的日志信息发送给 Elasticsearch。
fluentd 数据流逻辑:source –> parser –> filter –> output
Elasticsearch
搜索引擎,负责存储日志并提供查询接口。
Kibana
提供了一个 Web GUI,用户可以浏览和搜索存储在 Elasticsearch 中的日志。
主要的日志收集方案:
- 在节点上运行一个 agent 来收集日志(daemonSet)
- 在 Pod 中包含一个 sidecar 容器来收集应用日志
- 直接在应用程序中将日志信息推送到采集后端(一般不采用该方式)
二、二进制方式 / 容器方式部署
二进制方式
官网安装方式:https://docs.fluentd.org/installation/before-install
容器方式
仓库地址:https://hub.docker.com/r/fluent/fluentd
三、配置与使用
数据流逻辑:fluentd 以 tag 值为基准,决定数据的流经哪些处理器。
数据的流向:source -> parser -> filter -> output
http:从 http 接口获取日志来源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| # 创建配置文件 mkdir /tmp/fluentd && cd /tmp/fluentd cat > fluent.conf << EOF <source> @type http port 9880 bind 0.0.0.0 </source>
<match **> @type stdout </match> EOF
# 启动镜像,将 fluentd 目录挂载进容器,默认使用 fluent.conf 配置文件 docker run -p 9880:9880 --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd
# 测试请求 http 接口生成日志 curl -X POST 127.0.0.1:9880/yakir.test -d 'json={"a":"aaa"}'
|
tail:增量读取日志文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| cat > fluent.conf << EOF <source> @type tail path /var/log/httpd-access.log path_key tailed_path pos_file /var/log/td-agent/httpd-access.log.pos tag apache.access <parse> @type apache2 </parse> </source> EOF
# 启动镜像并测试日志 docker run --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd
|
exec:周期性执行命令,获取命令输出为 event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| cat > fluent.conf << EOF <source> @type exec tag yakir.test command cat /proc/loadavg | cut -d ' ' -f 1,2,3 run_interval 10s
<parse> @type tsv keys avg1,avg5,avg15 delimiter " " </parse> </source>
<match **> @type stdout </match> EOF
# 启动镜像,验证日志 docker run --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd 2022-06-30 08:43:20.377146682 +0000 yakir.test: {"avg1":"0.12","avg5":"0.16","avg15":"0.17"} 2022-06-30 08:43:30.347891525 +0000 yakir.test: {"avg1":"0.10","avg5":"0.15","avg15":"0.17"}
|
syslog:连接 rsyslog 系统日志,作为 rsyslog 接收端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| cat > fluent.conf << EOF <source> @type syslog port 5140 bind 0.0.0.0 tag system </source>
<match **> @type stdout </match> EOF
# 启动镜像,转发 udp 端口 docker run -p 5140:5140/udp --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd
# 添加 rsyslog 配置,转发日志到 fluent cat >> /etc/rsyslog.d/50-default.conf << "EOF" *.* @127.0.0.1:5140 EOF
# logger 产生日志,验证日志生成 logger -p mail.info 'this is info message' logger -p mail.warning 'this is warning message' 2022-07-04 10:27:48.000000000 +0000 system.mail.info: {"host":"minikube","ident":"root","message":"this is info message"} 2022-07-04 10:28:11.000000000 +0000 system.mail.warn: {"host":"minikube","ident":"root","message":"this is warning message"}
|
dummy:测试用数据源,周期生成假数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| cat > fluent.conf << "EOF" <source> @type dummy dummy {"foo": "bar"} size 3 rate 1 tag yakir.test auto_increment_key primary_key suspend true </source>
<match **> @type stdout </match> EOF
#参数说明 size #每次发送的 event 数量 rate #每秒产生多少个 event auto_increment_key #自增键名 suspend #重启后自增值是否重新开始
# 启动镜像,验证日志 docker run --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd 2022-07-04 02:51:37.044796145 +0000 yakir.test: {"foo":"bar","primary_key":0} 2022-07-04 02:51:37.044834743 +0000 yakir.test: {"foo":"bar","primary_key":1}
|
forward:接收其他 fluentd forward 的 event
1 2 3 4 5
| <source> @type forward port 24224 bind 0.0.0.0 </source>
|
output 配置
file:输出 event 为文件,默认每天输出一个日志文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| cat > fluent.conf << "EOF" <source> @type dummy dummy {"foo": "bar"} tag yakir.test size 1 rate 1 </source>
<match yakir.**> @type file path /tmp/fluent/yakir compress gzip <buffer> timekey 1d timekey_use_utc true timekey_wait 10m </buffer> #@type file #path /tmp/${tag[0]}/file.%Y-%m-%d-%H-%M-%S #<buffer tag,time> # timekey 10 # timekey_wait 10 # timekey_use_utc true #</buffer> </match> EOF
# 参数说明 path:支持 placeholder,可以在日志路径中嵌入时间,tag 和 record 中的字段值。例如:/path/to/${tag}/${key1}/file.%Y%m%d append:flush 的 chuck 是否追加到已存在的文件后。默认为 false,便于文件的并行处理。 format 标签,用来规定文件内容的格式,默认值为 out_file。 inject 标签,用来为 event 增加 time 和 tag 等字段。 add_path_suffix:是否增加 path 后缀 path_suffix:path 后缀内容,默认为.log。 compress:采用什么压缩格式,默认不压缩。 recompress:是否在 buffer chunk 已经压缩的情况再次压缩,默认为 false。
# 启动镜像验证日志 docker run --rm --name fluent -v /tmp/fluentd:/fluentd/etc fluent/fluentd docker exec -it fluent /bin/sh tail -2 /tmp/fluent/yakir/buffer.b5e2f5e063d4389bd9304563cf7f07656.log 2022-07-04T07:42:34+00:00 yakir.test {"foo":"bar"} 2022-07-04T07:42:35+00:00 yakir.test {"foo":"bar"}
|
buffer 标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <buffer> @type file </buffer> # @type 值:file(存文件)、memory(存内存,默认值)
<buffer ARGUMENT_CHUNK_KEYS> # ... </buffer> # buffer chunk keys(buffer 已 record 的什么字段分段存放),没有配置 chunk key,所有 event 写入同一个 chunk file 直到 buffer 滚动。 # 使用 time 为 chunk key,按照时间对 buffer 进行分段。 # timekey:时间跨度 timekey_wait:flush 延迟时间,用于等待迟到的数据
# 常用参数 timekey_use_utc:使用国际标准时间还是当地时间,默认是使用当地时间。 timekey_zone:指定时区。 chunk_limit_size:chunk 大小限制,默认 8MB。 chunk_limit_records:chunk event 条数限制。 total_limit_size:总 buffer 大小限制。 chunk_full_threshold:chunk 大小超过 chunk_limit_size * chunk_full_threshold 时会自动 flush。 queued_chunks_limit_size:限制队列中的 chunk 数目,防止频繁 flush 产生过多的 chunk。 compress:压缩格式,可使用 text 或 gzip。默认为 text。 flush_at_shutdown:关闭时候是否 flush。对于非持久化 buffer 默认值为 true,持久化 buffer 默认值为 false。 flush_interval:多长时间 flush 一次。 retry_timeout:重试 flush 的超时时间。在这个时间后不再会 retry。 retry_forever:是否永远尝试 flush。如果设置为 true 会忽略 retry_timeout 的配置。 retry_max_times:重试最大次数。 retry_type:有两个配置值:retry 时间间隔,指数级增长或者是固定周期重试。 retry_wait:每次重试等待时间。 retry_exponential_backoff_base:retry 时间指数扩大倍数。 retry_max_interval:最长 retry 时间间隔。 retry_randomize:是否随机 retry 时间间隔。
|
format 标签
1 2 3 4 5 6 7 8 9 10 11 12 13
| <match> ... <format> @type json </format>
<buffer> ... </buffer> </match>
|
forward:转发 event 到其他 fluentd 节点。配置多个 fluentd 节点时,使用负载均衡方式发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <match yakir.*> @type forward send_timeout 60s recover_wait 10s hard_timeout 60s
<server> name myserver1 host 192.168.1.3 port 24224 weight 60 </server> <server> name myserver2 host 192.168.1.4 port 24224 weight 60 </server> ...
<secondary> @type file path /var/log/fluent/forward-failed </secondary> </match>
#server 标签参数说明 host name port shared_key username password standby 标记 server 为备用,只有其他 node 不可用的时候才会启用 standby 的 node weight 负载均衡的权重配置
|
copy:多路输出,复制 event 到多个输出端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| cat > fluent.conf << "EOF" <source> @type dummy dummy {"foo": "bar"} tag yakir.test size 1 rate 1 </source>
<match yakir.**> @type copy <store> @type file path /tmp/yakir/file.%Y%m%d compress gzip </store> <store ignore_error> @type stdout </store> </match> EOF
# 参数说明 copy_mode 复制模式可选值 no_copy:每路输出共享 event。 shallow:浅拷贝,如果不修改嵌套字段可以使用。 deep:深拷贝,使用msgpack-ruby方式。 marshal:深拷贝,使用marshal方式。 store 标签 ignore_error 参数:标记的 store 出现错误时,不影响其他
|
http:通过 http 请求方式发送 event,payload 格式由 format 标签决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <match pattern> @type http endpoint http://logserver.com:9000/api open_timeout 2
<format> @type json </format> <buffer> flush_interval 10s </buffer> </match>
# 使用 post 方式,连接超时2s,输出格式为 json,每10s 输出一次到 endpoint。(content-type 为 application/x-ndjson)
|
stdout:标准输出,后台运行时输出到 fluentd 日志。
1 2 3 4 5 6 7 8 9 10 11
| <source> @type dummy dummy {"foo": "bar"} tag yakir.test size 1 rate 1 </source>
<match yakir.**> @type stdout </match>
|
第三方存储:Elasticsearch、Kafka
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| # elasticsearch 关键配置 <match yakir.logs> @type elasticsearch host localhost port 9200 logstash_format true </match> # 参数 host:单个 elasticsearch 节点地址 port:单个 elasticsearch 节点的端口号 hosts:elasticsearch 集群地址。格式为 ip1:port1,ip2:port2... user、password:elasticsearch 的认证信息 scheme:使用 https 还是 http。默认为 http 模式 path:REST 接口路径,默认为空 index_name:index 名称 logstash_format:index 是否使用 logstash 命名方式(logstash-%Y.%m.%d),默认不启用 logstash_prefix:logstash_format 启用的时候,index 命名前缀是什么。默认为logstash
# kafka 关键配置 <match pattern> @type kafka2
# list of seed brokers brokers <broker1_host>:<broker1_port>,<broker2_host>:<broker2_port> use_event_time true
# buffer settings <buffer topic> @type file path /var/log/td-agent/buffer/td flush_interval 3s </buffer>
# data type settings <format> @type json </format>
# topic settings topic_key topic default_topic messages
# producer settings required_acks -1 compression_codec gzip </match> # 参数 brokers:Kafka brokers 的地址和端口号 topic_key:record 中哪个 key 对应的值用作 Kafka 消息的 key default_topic:如果没有配置 topic_key,默认使用的 topic 名字 format 标签:确定发送的数据格式 use_event_time:是否使用 fluentd event 的时间作为 Kafka 消息的时间。默认为 false。意思为使用当前时间作为发送消息的时间 required_acks:producer acks 的值 compression_codec:压缩编码方式
|
webhdfs:通过 REST 方式写入 event 到 HDFS(配合 Hadoop)
1 2 3 4 5 6 7 8 9
| <store> @type webhdfs host 1.1.1.1 port 50070 path "/history/access.log.%Y%m%d_%H.#{Socket.gethostname}.log" <buffer> flush_interval 60s </buffer> </store>
|
parser 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| # 关键配置 <parse> @type regexp expression /^\[(?<logtime>[^\]]*)\] (?<name>[^ ]*) (?<title>[^ ]*) (?<id>\d*)$/ time_key logtime time_format %Y-%m-%d %H:%M:%S %z types id:integer </parse>
# 数据解析 #原日志 [2013-02-28 12:00:00 +0900] alice engineer 1 #解析后 time: 1362020400 (2013-02-28 12:00:00 +0900)
record: { "name" : "alice", "title": "engineer", "id" : 1 }
|
filter 配置
四、DaemonSet 方式部署
部署 Elasticsearch 和 Kibana
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| # 创建日志 namespace,方便清理 kubectl create ns logging
# 部署 Elasticsearch(StatefulSet 和 Service,service 资源使用无头服务) cat > elasticsearch.yaml << "EOF" apiVersion: apps/v1 kind: StatefulSet metadata: name: es namespace: logging spec: serviceName: elasticsearch replicas: 1 selector: matchLabels: app: elasticsearch template: metadata: labels: app: elasticsearch spec: # 初始化容器,调整内核参数 initContainers: - 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 containers: - name: elasticsearch image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2 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 #多 ES 节点时注意以下配置 - name: cluster.initial_master_nodes value: "es-0" - name: discovery.zen.minimum_master_nodes value: "1" - name: discovery.seed_hosts value: "elasticsearch" - name: ES_JAVA_OPTS value: "-Xms512m -Xmx512m" - name: network.host value: "0.0.0.0" # 持久化存储,线上环境建议使用 StorageClass 等存储资源对象 volumes: - name: data emptyDir: {} --- kind: Service apiVersion: v1 metadata: name: elasticsearch namespace: logging labels: app: elasticsearch spec: selector: app: elasticsearch #使用无头服务,确保 StatefulSet 中 Pod 固定 DNS 地址,如 es-0.elasticsearch.logging.svc.cluster.local clusterIP: None ports: - port: 9200 name: rest - port: 9300 name: inter-node EOF
# 部署 Kibana 资源 cat > kibana.yaml << "EOF" apiVersion: v1 kind: Service metadata: name: kibana namespace: logging labels: app: kibana spec: selector: app: kibana ports: - port: 5601 type: ClusterIP
--- 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:7.6.2 resources: limits: cpu: 1000m requests: cpu: 200m env: - name: ELASTICSEARCH_HOSTS value: http://elasticsearch:9200 ports: - containerPort: 5601 EOF
# 部署并查看部署结果 kubectl apply -f elasticsearch.yaml kubectl apply -f kibana.yaml kubectl get pod,svc -n logging -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/es-0 1/1 Running 0 4h54m 172.17.0.6 minikube <none> <none> pod/kibana-6c84594848-mdp76 1/1 Running 0 158m 172.17.0.5 minikube <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR service/elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP 4h50m app=elasticsearch service/kibana ClusterIP 10.106.67.32 <none> 5601/TCP 158m app=kibana
# 访问验证 kubectl port-forward services/elasticsearch -n logging --address 127.0.0.1 9200:9200 curl http://127.0.0.1:9200/_cluster/state?pretty curl 10.106.67.32:5601/app/kibana -I
|
部署 Fluentd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
| # 源码方式 # git clone https://github.com/fluent/fluentd-kubernetes-daemonset.git
# 自定义安装方式 #Fluentd 配置文件 cat > fluentd-cfg.yaml << "EOF" kind: ConfigMap apiVersion: v1 metadata: name: fluentd-config namespace: logging data: system.conf: |- <system> root_dir /tmp/fluentd-buffers/ </system> containers.input.conf: |- <source> @id fluentd-containers.log @type tail # Fluentd 内置的输入方式,其原理是不停地从源文件中获取新的日志。 path /var/log/containers/*.log # 挂载的服务器Docker容器日志地址 pos_file /var/log/es-containers.log.pos tag raw.kubernetes.* # 设置日志标签 read_from_head true <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> # 在日志输出中检测异常,并将其作为一条日志转发 # https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions <match raw.kubernetes.**> # 匹配tag为raw.kubernetes.**日志信息 @id raw.kubernetes @type detect_exceptions # 使用detect-exceptions插件处理异常栈信息 remove_tag_prefix raw # 移除 raw 前缀 message log stream stream multiline_flush_interval 5 max_bytes 500000 max_lines 1000 </match>
<filter **> # 拼接日志 @id filter_concat @type concat # Fluentd Filter 插件,用于连接多个事件中分隔的多行日志。 key message multiline_end_regexp /\n$/ # 以换行符“\n”拼接 separator "" </filter>
# 添加 Kubernetes metadata 数据 <filter kubernetes.**> @id filter_kubernetes_metadata @type kubernetes_metadata </filter>
# 修复 ES 中的 JSON 字段 # 插件地址:https://github.com/repeatedly/fluent-plugin-multi-format-parser <filter kubernetes.**> @id filter_parser @type parser # multi-format-parser多格式解析器插件 key_name log # 在要解析的记录中指定字段名称。 reserve_data true # 在解析结果中保留原始键值对。 remove_key_name_field true # key_name 解析成功后删除字段。 <parse> @type multi_format <pattern> format json </pattern> <pattern> format none </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>
# 只保留具有logging=true标签的Pod日志 <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 </source>
output.conf: |- <match **> @id elasticsearch @type elasticsearch @log_level info include_tag_key true host elasticsearch port 9200 logstash_format true logstash_prefix k8s # 设置 index 前缀为 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> EOF
cat > fluentd-daemonset.yaml << "EOF" 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 kubernetes.io/cluster-service: "true" addonmanager.kubernetes.io/mode: Reconcile spec: selector: matchLabels: k8s-app: fluentd-es template: metadata: labels: k8s-app: fluentd-es kubernetes.io/cluster-service: "true" # 此注释确保如果节点被驱逐,fluentd不会被驱逐,支持关键的基于 pod 注释的优先级方案。 annotations: priorityClassName: system-cluster-critical spec: serviceAccountName: fluentd-es containers: - name: fluentd-es image: quay.io/fluentd_elasticsearch/fluentd:v3.0.1 #image: fluent/fluentd-kubernetes-daemonset:v1.14.6-debian-elasticsearch7-1.1 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 # 打上 master 节点污点,收集 master 节点 tolerations: - operator: Exists terminationGracePeriodSeconds: 30 # 挂载需要收集日志的目录 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers - name: config-volume configMap: name: fluentd-config EOF
# 部署并查看部署结果 kubectl apply -f fluentd-cfg.yaml kubectl apply -f fluentd-daemonset.yaml kubectl get pod -n logging -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod/fluentd-es-cfdcx 1/1 Running 0 91m 172.17.0.7 minikube <none> <none>
|
部署测试应用,输出日志容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| # 直接标准输出日志容器 cat > stdin.yaml << "EOF" apiVersion: v1 kind: Pod metadata: name: counter1 labels: # 配置该标签,日志才能进行收集 logging: "true" spec: containers: - image: busybox args: ["/bin/sh","-c", 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 5; done'] name: counter EOF
# sidecar 方式获取输出到文件的容器日志 cat > sidecar.yaml << "EOF" apiVersion: v1 kind: Pod metadata: name: counter2 labels: logging: "true" spec: containers: - name: counter2 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 3; 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 restartPolicy: Never volumes: - name: varlog emptyDir: {} EOF # 获取日志方式:kubectl logs counter2 count-log-2 -f --tail 3
# 输出 JSON 格式日志,用于分析 cat > dummylogs.yaml << "EOF" apiVersion: apps/v1 kind: Deployment metadata: name: dummylogs spec: replicas: 1 selector: matchLabels: app: dummylogs template: metadata: labels: app: dummylogs logging: "true" # 要采集日志需要加上该标签 spec: containers: - name: dummy image: cnych/dummylogs:latest args: - msg-processor --- apiVersion: apps/v1 kind: Deployment metadata: name: dummylogs2 spec: replicas: 1 selector: matchLabels: app: dummylogs2 template: metadata: labels: app: dummylogs2 logging: "true" # 要采集日志需要加上该标签 spec: containers: - name: dummy image: cnych/dummylogs:latest args: - msg-receiver-api EOF
# 部署 kubectl apply -f counter.yaml kubectl apply -f dummylogs.yaml
|
Kibana & Elasticsearch 查询数据验证
- Kibana 图表聚合展示
日志告警功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| cat > elastalert.yaml << "EOF" apiVersion: v1 kind: ConfigMap metadata: name: elastalert-config namespace: logging labels: app: elastalert data: elastalert_config: |- --- rules_folder: /opt/rules # 指定规则的目录 scan_subdirectories: false run_every: # 多久从 ES 中查询一次 minutes: 1 buffer_time: minutes: 15 es_host: elasticsearch es_port: 9200 writeback_index: elastalert use_ssl: False verify_certs: True alert_time_limit: # 失败重试限制 minutes: 720 --- apiVersion: v1 kind: ConfigMap metadata: name: elastalert-rules namespace: logging labels: app: elastalert data: rule_config.yaml: |- name: dummylogs error # 规则名字,唯一值 es_host: elasticsearch es_port: 9200 type: any # 报警类型 index: k8s-* # es索引 filter: # 过滤 - query: query_string: query: "LOGLEVEL:ERROR" # 报警条件 alert: # 报警类型 - "email" smtp_host: 127.0.0.1 smtp_port: 587 smtp_auth_file: /opt/auth/smtp_auth_file.yaml email_reply_to: xxx@gmail.com from_addr: xxx@gmail.com email: # 接受邮箱 - "xxx@gmail.com" --- apiVersion: apps/v1 kind: Deployment metadata: name: elastalert namespace: logging labels: app: elastalert spec: selector: matchLabels: app: elastalert template: metadata: labels: app: elastalert spec: containers: - name: elastalert image: jertel/elastalert-docker:0.2.4 imagePullPolicy: IfNotPresent volumeMounts: - name: config mountPath: /opt/config - name: rules mountPath: /opt/rules - name: auth mountPath: /opt/auth resources: limits: cpu: 50m memory: 256Mi requests: cpu: 50m memory: 256Mi volumes: - name: auth secret: secretName: smtp-auth - name: rules configMap: name: elastalert-rules - name: config configMap: name: elastalert-config items: - key: elastalert_config path: elastalert_config.yaml EOF
# 邮箱认证信息 cat > smtp_auth_file.yaml << EOF user: "xxxxx@gmail.com" password: "123xxx" EOF
# 部署验证 kubectl create secret generic smtp-auth --from-file=smtp_auth_file.yaml -n logging kubectl apply -f elastalert.yaml kubectl get pods -n logging -l app=elastalert #查看 Kibana 是否生成对应索引
|
五、fluentd-operator 方式部署
CRD 资源
fluentbit.fluent.io 资源
- FluentBit:定义 Fluent Bit 属性,如镜像版本、污点、亲和性等参数。
- ClusterFluentBitConfig:定义 Fluent Bit 的配置文件。
- ClusterInput::定义 Fluent Bit 的 input 插件。
- ClusterFilter::定义 Fluent Bit 的 filter 插件。
- ClusterParser:定义 Fluent Bit 的 parser 插件。
- ClusterOutput:定义 Fluent Bit 的 output 插件。
fluentd.fluent.io 资源
- Fluentd:定义 Fluentd 属性,如镜像版本、污点、亲和性等参数。
- FluentdConfig:定义 Fluentd namespace 级别配置文件。
- ClusterFluentdConfig:定义 Fluentd 集群级别配置文件。
- Filter:定义 Fluentd namespace 级别的 filter 插件。
- ClusterFilter:定义 Fluentd 集群级别的 filter 插件。
- Output:定义 Fluentd namespace 级别的 output 插件。
- ClusterOutput:定义 Fluentd 集群级别的 output 插件。
部署 CRD 资源与 fluent-operator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| # 下载源码,创建 CRD 资源与部署 fluent-operator git clone https://github.com/fluent/fluent-operator cd fluent-operator && kubectl apply -f manifests/setup/setup.yaml
# 验证资源 kubectl get pod,crd -n fluent NAME READY STATUS RESTARTS AGE pod/fluent-operator-86858cfc87-cg4ct 1/1 Running 1 (5h20m ago) 22h
NAME CREATED AT customresourcedefinition.apiextensions.k8s.io/clusterfilters.fluentbit.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusterfilters.fluentd.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusterfluentbitconfigs.fluentbit.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusterfluentdconfigs.fluentd.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusterinputs.fluentbit.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusteroutputs.fluentbit.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusteroutputs.fluentd.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/clusterparsers.fluentbit.fluent.io 2022-07-05T07:45:26Z customresourcedefinition.apiextensions.k8s.io/filters.fluentd.fluent.io 2022-07-05T07:45:27Z customresourcedefinition.apiextensions.k8s.io/fluentbits.fluentbit.fluent.io 2022-07-05T07:45:27Z customresourcedefinition.apiextensions.k8s.io/fluentdconfigs.fluentd.fluent.io 2022-07-05T07:45:27Z customresourcedefinition.apiextensions.k8s.io/fluentds.fluentd.fluent.io 2022-07-05T07:45:27Z customresourcedefinition.apiextensions.k8s.io/outputs.fluentd.fluent.io 2022-07-05T07:45:28Z
|
Fluent Bit Only 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| # 部署 Fluent Bit 收集日志 cat > fluent-bit.yaml << "EOF" apiVersion: fluentbit.fluent.io/v1alpha2 kind: FluentBit metadata: name: fluent-bit namespace: fluent labels: app.kubernetes.io/name: fluent-bit spec: image: kubesphere/fluent-bit:v1.8.11 positionDB: hostPath: path: /var/lib/fluent-bit/ resources: requests: cpu: 10m memory: 25Mi limits: cpu: 500m memory: 200Mi fluentBitConfigName: fluent-bit-config tolerations: - operator: Exists --- apiVersion: fluentbit.fluent.io/v1alpha2 kind: ClusterFluentBitConfig metadata: name: fluent-bit-config labels: app.kubernetes.io/name: fluent-bit spec: service: parsersFile: parsers.conf inputSelector: matchLabels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/mode: "k8s" filterSelector: matchLabels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/mode: "k8s" outputSelector: matchLabels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/mode: "k8s" --- apiVersion: fluentbit.fluent.io/v1alpha2 kind: ClusterInput metadata: name: tail labels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/mode: "k8s" spec: tail: tag: kube.* path: /var/log/containers/*.log parser: docker refreshIntervalSeconds: 10 memBufLimit: 5MB skipLongLines: true db: /fluent-bit/tail/pos.db dbSync: Normal --- apiVersion: fluentbit.fluent.io/v1alpha2 kind: ClusterOutput metadata: name: es labels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/mode: "k8s" spec: matchRegex: (?:kube|service)\.(.*) es: host: elasticsearch port: 9200 generateID: true logstashPrefix: fluent-log-fb-only logstashFormat: true timeKey: "@timestamp" EOF # 需要先部署最后端日志 output 层 elasticsearch 资源 kubectl apply -f elasticsearch.yaml kubectl apply -f fluent-bit.yaml
# 查看部署资源 kubectl get pod,svc -n fluent NAME READY STATUS RESTARTS AGE pod/es-0 1/1 Running 1 (56m ago) 17h pod/fluent-bit-rjqsd 1/1 Running 0 2m34s pod/fluent-operator-86858cfc87-cg4ct 1/1 Running 1 (56m ago) 17h NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP 17h
# 请求 Elasticsearch 搜索验证日志内容 kubectl port-forward services/elasticsearch -n fluent --address 127.0.0.1 9200:9200 #查看所有索引 curl "127.0.0.1:9200/_cat/indices?pretty" yellow open fluent-log-fb-only-2022.07.06 lLmstFnwQLa89Jb7TFe-0Q 1 1 152 0 155.2kb 155.2kb #查看索引所有文档 curl "127.0.0.1:9200/fluent-log-fb-only-2022.07.06/_search?pretty" ... #根据文档 ID 搜索具体日志 curl "127.0.0.1:9200/fluent-log-fb-only-2022.07.06/_doc/b641529e-255c-f260-f911-f7d00d84e3fe?pretty" { "_index" : "fluent-log-fb-only-2022.07.06", "_type" : "_doc", "_id" : "b641529e-255c-f260-f911-f7d00d84e3fe", "_version" : 1, "_seq_no" : 94, "_primary_term" : 1, "found" : true, "_source" : { "@timestamp" : "2022-07-06T02:57:38.035Z", "log" : "[2022/07/06 02:57:38] [ info] [input:tail:tail.0] inotify_fs_add(): inode=2050771 watch_fd=3 name=/var/log/containers/dashboard-metrics-scraper-58549894f-q9lpg_kubernetes-dashboard_dashboard-metrics-scraper-51061ac5d9c2c7c2da734ab35b9252edb29f4101ade5679a22181d0d735dc364.log\n", "time" : "2022-07-06T02:57:38.035319145Z", "kubernetes" : { "pod_name" : "fluent-bit-8v2qn", "namespace_name" : "fluent", "container_name" : "fluent-bit", "docker_id" : "098d8ac65b201686b7a2945df6ee1a919b7220b637aea4de6490d92569c9c455", "container_image" : "kubesphere/fluent-bit:v1.8.11" } } }
|
Fluent Bit + Fluentd 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| # 修改 Fluent Bit output 资源配置,启用 forward 插件,转发到 Fluentd cat >> fluent-bit.yaml << "EOF" apiVersion: fluentbit.fluent.io/v1alpha2 kind: ClusterOutput metadata: name: fluentd labels: fluentbit.fluent.io/enabled: "true" fluentbit.fluent.io/component: logging spec: matchRegex: (?:kube|service)\.(.*) forward: host: fluentd.fluent.svc port: 24224 EOF
# 部署 Fluentd cat > fluentd.yaml << "EOF" apiVersion: fluentd.fluent.io/v1alpha1 kind: Fluentd metadata: name: fluentd namespace: fluent labels: app.kubernetes.io/name: fluentd spec: globalInputs: - forward: bind: 0.0.0.0 port: 24224 replicas: 1 image: kubesphere/fluentd:v1.14.4 fluentdCfgSelector: matchLabels: config.fluentd.fluent.io/enabled: "true" EOF
# 配置 Fluentd #1.使用 ClusterFluentdConfig 配置,发送 kube-system 与 default 下 namesapce 日志到 ClusterOutput cat >> fluentd.yaml << "EOF" apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterFluentdConfig metadata: name: cluster-fluentd-config labels: config.fluentd.fluent.io/enabled: "true" spec: watchedNamespaces: - kube-system - default clusterOutputSelector: matchLabels: output.fluentd.fluent.io/scope: "cluster" output.fluentd.fluent.io/enabled: "true" --- apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterOutput metadata: name: cluster-fluentd-output-es labels: output.fluentd.fluent.io/scope: "cluster" output.fluentd.fluent.io/enabled: "true" spec: outputs: - elasticsearch: host: elasticsearch-master.elastic.svc port: 9200 logstashFormat: true logstashPrefix: fluent-log-cluster-fd EOF #2.使用 FluentdConfig + ClusterFluentdConfig 配置,发送集群范围和 namespace 范围日志到 Output 或 ClusterOutput cat >> fluentd.yaml << "EOF" apiVersion: fluentd.fluent.io/v1alpha1 kind: FluentdConfig metadata: name: namespace-fluentd-config-user1 namespace: fluent labels: config.fluentd.fluent.io/enabled: "true" spec: outputSelector: matchLabels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/user: "user1" clusterOutputSelector: matchLabels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/user: "user1" --- apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterFluentdConfig metadata: name: cluster-fluentd-config-cluster-only labels: config.fluentd.fluent.io/enabled: "true" spec: watchedNamespaces: - kube-system - kubesphere-system clusterOutputSelector: matchLabels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/scope: "cluster-only" --- apiVersion: fluentd.fluent.io/v1alpha1 kind: Output metadata: name: namespace-fluentd-output-user1 namespace: fluent labels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/user: "user1" spec: outputs: - elasticsearch: host: elasticsearch-master.elastic.svc port: 9200 logstashFormat: true logstashPrefix: fluent-log-user1-fd --- apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterOutput metadata: name: cluster-fluentd-output-user1 labels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/user: "user1" spec: outputs: - elasticsearch: host: elasticsearch-master.elastic.svc port: 9200 logstashFormat: true logstashPrefix: fluent-log-cluster-user1-fd --- apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterOutput metadata: name: cluster-fluentd-output-cluster-only labels: output.fluentd.fluent.io/enabled: "true" output.fluentd.fluent.io/scope: "cluster-only" spec: outputs: - elasticsearch: host: elasticsearch-master.elastic.svc port: 9200 logstashFormat: true logstashPrefix: fluent-log-cluster-only-fd EOF
# Fluentd 输出使用 buffer 缓冲区 cat >> fluentd.yaml << "EOF" apiVersion: fluentd.fluent.io/v1alpha1 kind: ClusterOutput metadata: name: cluster-fluentd-output-buffer labels: output.fluentd.fluent.io/type: "buffer" output.fluentd.fluent.io/enabled: "true" spec: outputs: - stdout: {} buffer: type: file path: /buffers/stdout.log - elasticsearch: host: elasticsearch-master.elastic.svc port: 9200 logstashFormat: true logstashPrefix: fluent-log-buffer-fd buffer: type: file path: /buffers/es.log EOF
|
Fluentd Only 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| cat > fluentd.yaml << "EOF" apiVersion: fluentd.fluent.io/v1alpha1 kind: Fluentd metadata: name: fluentd-http namespace: fluent labels: app.kubernetes.io/name: fluentd spec: globalInputs: - http: bind: 0.0.0.0 port: 9880 replicas: 1 image: kubesphere/fluentd:v1.14.4 fluentdCfgSelector: matchLabels: config.fluentd.fluent.io/enabled: "true" --- apiVersion: fluentd.fluent.io/v1alpha1 kind: FluentdConfig metadata: name: fluentd-only-config namespace: fluent labels: config.fluentd.fluent.io/enabled: "true" spec: filterSelector: matchLabels: filter.fluentd.fluent.io/mode: "fluentd-only" filter.fluentd.fluent.io/enabled: "true" outputSelector: matchLabels: output.fluentd.fluent.io/mode: "fluentd-only" output.fluentd.fluent.io/enabled: "true" --- apiVersion: fluentd.fluent.io/v1alpha1 kind: Filter metadata: name: fluentd-only-filter namespace: fluent labels: filter.fluentd.fluent.io/mode: "fluentd-only" filter.fluentd.fluent.io/enabled: "true" spec: filters: - stdout: {} --- apiVersion: fluentd.fluent.io/v1alpha1 kind: Output metadata: name: fluentd-only-stdout namespace: fluent labels: output.fluentd.fluent.io/mode: "fluentd-only" output.fluentd.fluent.io/enabled: "true" spec: outputs: - stdout: {} EOF
# 查看部署资源 kubectl get all -n fluent NAME READY STATUS RESTARTS AGE pod/es-0 1/1 Running 1 (163m ago) 19h pod/fluent-operator-86858cfc87-cg4ct 1/1 Running 1 (163m ago) 19h pod/fluentd-http-0 1/1 Running 0 2m53s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP 19h service/fluentd-http ClusterIP 10.97.96.1 <none> 9880/TCP 2m54s
NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/fluent-operator 1/1 1 1 19h
NAME DESIRED CURRENT READY AGE replicaset.apps/fluent-operator-86858cfc87 1 1 1 19h
NAME READY AGE statefulset.apps/es 1/1 19h statefulset.apps/fluentd-http 1/1 2m54s
|
参考文档:
1、https://www.qikqiak.com/post/install-efk-stack-on-k8s/
2、fluentd 官网:https://docs.fluentd.org/
3、fluentd-operator 官网:https://github.com/fluent/fluent-operator
4、fluent-operator-walkthrough:https://github.com/kubesphere-sigs/fluent-operator-walkthrough
5、KubeSphere:https://kubesphere.com.cn/blogs/fluent-operator-logging