本篇文章针对一种 K8s 场景中常见的基于 ElasticSearch、Fluentd、Kibana 组合的日志解决方案进行介绍,并详细阐述了其配置使用和部署方案。
一、EFK 简介
EFK 是 ElasticSearch、Fluentd、Kibana 组合的缩写,这是 Kubernetes 场景中较为流行的开源日志解决方案,首先看一下 EFK 的能力范围。
1、Elasticsearch
ElasticSearch,是一个基于 Apache Lucene (TM)的开源搜索引擎,它提供了如下能力:
2、Fluentd
3、Kibana
Kibana,是 ElasticSearch 数据的可视化工具,它支持:
Index Pattern
Discover
Visualize
Dashboards
ELK Stack 及其他插件的操作支持
二、EFK 的配置与使用
EFK 在日志解决方案中,各自有明确的分工,Fluentd 负责日志的采集,Elasticsearch 负责日志的存储与搜索,Kibana 负责日志的可视化操作。
通常情况下,K8s 中运行的应用日志会输出至标准输出流,用户通过 kubectl logs 命令获取容器的日志,从而对调试问题与应用状态监控产生帮助。
当容器发生错误终止以及重启,kubectl 会保留其日志。不过当 Pod 在工作节点中被驱逐时,Pod 中所有容器也会被驱逐,包括其对应的日志也会丢失。此外,日志的输出也会存在 logrotate 需求,也会导致对日志管理产生一定需求。
K8s 针对集群的日志管理给了几个解决方案:
方法 1:使用在每个节点上运行的节点级日志记录代理。
方案 2:在应用程序的 Pod 中,添加专门记录日志的 Sidecar 容器。
方案 3:将日志直接从应用程序中推送到日志记录后端。
其中,方案 2、方案 3 都会涉及到对 Pod 或者应用的侵入,而 Fluentd 则是方案 1 的实践者。
Fluentd 通过以 DeamonSet 的形式运行 logging-agent(代理),在每个节点中运行一个 Pod,不需要对节点中已存在的其他应用做修改。
1、Fluentd 配置
Fluentd 的配置包含几个方面:sources 负责采集侧的配置,match 负责匹配 tag 字段将事件输出到指定位置,filter 可以链式修改事件流,output 负责事件的输出。
1)采集配置
用于指定采集数据的来源。
<source>
@id fluentd-containers.log # 唯一标识符
@type tail # 内置的输入方式,从源文件中获取新的日志。
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>
复制代码
针对上面配置部分参数说明如下:
id:表示引用该日志源的唯一标识符,该标识可用于进一步过滤和路由结构化日志数据
type:Fluentd 内置的指令,tail
表示 Fluentd 从上次读取的位置通过 tail 不断获取数据。也可以指定为 http
或者 forward
来接收 HTTP
与 TCP
数据。
path:tail 类型下的特定参数,告诉 Fluentd 采集 /var/log/containers
目录下的所有日志,这是 docker 在 Kubernetes 节点上用来存储运行容器 stdout 输出日志数据的目录。
pos_file:检查点,如果 Fluentd 程序重新启动了,它将使用此文件中的位置来恢复日志数据收集。
tag:用来将日志源与目标或者过滤器匹配的自定义字符串,Fluentd 匹配源/目标标签来路由日志数据。
2)路由配置
match 指令用于查找匹配的标签。
<match **>
@id elasticsearch # 唯一标识符
@type elasticsearch # elasticsearch 插件
@log_level info
include_tag_key true
type_name fluentd
host "#{ENV['OUTPUT_HOST']}"
port "#{ENV['OUTPUT_PORT']}"
logstash_format true
<buffer>
@type file # 使用文件将缓冲区块存储在磁盘上
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_thread_count 2
flush_interval 5s
retry_forever
retry_max_interval 30
chunk_limit_size "#{ENV['OUTPUT_BUFFER_CHUNK_LIMIT']}"
queue_limit_length "#{ENV['OUTPUT_BUFFER_QUEUE_LIMIT']}"
overflow_action block
</buffer>
</match>
复制代码
针对上面配置部分参数说明如下:
match:标识一个目标标签,后面是一个匹配日志源的正则表达式,我们这里想要捕获所有的日志并将它们发送给 Elasticsearch,所以需要配置成**。
log_level:指定要捕获的日志级别,我们这里配置成 info,表示任何该级别或者该级别以上(INFO、WARNING、ERROR)的日志都将被路由到 Elsasticsearch。
host/port:定义 Elasticsearch 的地址,也可以配置认证信息,我们的 Elasticsearch 不需要认证,所以这里直接指定 host 和 port 即可。
logstash_format:将 logstash_format 设置为 true,Fluentd 将会以 logstash 格式来转发结构化的日志数据,这有助于与 Kibana 进行适配。
Buffer:Fluentd 允许在目标不可用时进行缓存,比如,如果网络出现故障或者 Elasticsearch 不可用的时候。缓冲区配置也有助于降低磁盘的 IO。
3)过滤
过滤提供修改事件流的方式,可以对指定字段的值进行过滤、屏蔽、删除、添加等操作。
# 只保留具有 logging=true 标签的 Pod 日志
<filter kubernetes.**>
@id filter_log # 唯一标识符
@type grep
<regexp> # 保留 key 为 true
key $.kubernetes.labels.logging
pattern ^true$
</regexp>
<exclude> # 排除 key 为 true
key $.kubernetes.labels.exclude
pattern ^true$
</exclude>
</filter>
复制代码
针对上面配置部分参数说明如下:
4)输出
Fluentd 官方提供了很多种输出的插件,支持 hdfs、es、stdout、kafka 等常见的日志输出场景。此外也支持无缓存、同步缓存、异步缓存三种缓存机制。
配合路由(match)功能,可以指定输出的目标位置,例如配置 out_elasticsearch 插件:
<match my.logs>
@type elasticsearch # 指定输出使用 out_elasticsearch 插件
host localhost # 插件的配置信息
port 9200
logstash_format true
</match>
复制代码
2、ElasticSearch 配置
ES 自身提供了发现的机制,在同一网段下,ES 节点的 cluster.name 设置为同一名称,即可自动发现组成为一个 ES 集群。当然也可以由用户手动指定 Cluster 的节点位置。
ES 的节点随着 ES 集群的规划的功能复杂度以及规模的扩大,也承担着不同的作用,即 Node roles:
master-eligible :有资格被选举为集群控制集群的节点。
data :保存数据、执行数据相关操作的节点。例如 CRUD,搜索和聚合。这些操作是 I/O,内存和 CPU 密集型的。
data_content :为 data 的子类,主要用于存储内容,CRUD、搜索、聚合等操作。
data_hot :快速读写操作的节点,适用于 SSD 硬盘的节点。
data_warm :数据不再定期更新,但仍有查询操作的节点,但是数据使用率较低,适用于性能较低的硬件配置。
data_cold :索引只读,访问频率较低。
ingest :用于建索引前的数据转换等操作的节点,适用于数据处理较为频繁的业务。
ml:用于机器学习运算的节点。
此外还有一个逻辑的 Coordnating node,即协调节点。
ES 的索引分布在不同节点的不同分片中,因而搜索的时候需要把请求分发到各个节点或者依据 _routing 的设置分发到指定节点,从各个节点中获得查询数据,返回给某个节点,并在某个节点中进行汇总、排序等操作后,形成最终的数据提供给用户。
一个简单的 master 节点的配置参考如下:
cluster.name: lakehouse-es # 集群名称,用来发现同一网段的其他其他节点
node.name: es-1 # 节点名称,网段内唯一
node.roles: [ master ] # 节点角色为 master
path.data: /apps/data/es # 存储集群元数据信息,data 节点存储索引数据
path.repo: /apps/data/esbackup
path.logs: /apps/logs/es
network.host: 0.0.0.0
http.port: 9200 # Rest API 端口
transport.port: 9300 # 节点间通信端口
discovery.zen.ping_timeout: 30
discovery.seed_hosts: # 启动当前节点时,发现其他节点的初始列表
["hosts1:9300", "host2:9300", "host3:9300"]
cluster.initial_master_nodes: ["houst1"] # 初始候选的 es master 节点
复制代码
ES 的基础配置相对简单,对于 data、ingest 节点来说,除了 node.roles 之外,没有更多的不同。
Master 节点也需要配置 path.data 信息,与 data 节点不同之处,master 节点会用来保存集群相关的元数据信息,而不是索引数据。
3、Kibana 配置
Kibana 的配置较为简单:
server.port 5601 # 服务启动的端口
server.host: "localhost" # 服务地址
elasticsearch.hosts: # 指定连接的 es 集群地址,默认为 localhost
[ "<http://localhost:9200>" ]
复制代码
三、EFK 的部署
了解完 EFK 的主要配置后,下面尝试一下在 K8s 中如何把这些服务运行起来。
Elastic 官方提供了 Helm Chart 的服务,不过为了展示更多的细节,本文仍然使用配置的方式。(注:下文中没有展示整段的配置,完整配置可参考https://github.com/jkhhuse/efk-configure)
首先看一下整体的服务部署架构:
Fluentd 使用 daemonSet 的方式、ES 使用 StatefulSet、Kibana 使用 Deployment 的方式部署。下面分析下这样部署的原因与方法。
1、Fluentd 部署
DaemonSet 可以保证每个节点都运行一个 Pod,集群中有节点加入,也会新增一个 Pod。Fluentd 使用 DaemonSet 的方式运行,可以保证采集到 K8s 的各个节点的日志。
# 创建 configmap fluentd 配置文件 fluentd-config
# 创建 daemonSet.yaml
# fluentd-config configmap 通过 volumes 挂载到 Fluentd 容器中。
# 设置日志输出路径, 使用 hostpath 挂载到容器外
# 设置采集节点的范围
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /data/docker/containers
readOnly: true
- name: config-volume
mountPath: /etc/fluent/config.d
nodeSelector:
beta.kubernetes.io/fluentd-ds-ready: "true"
tolerations:
- operator: Exists
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /data/docker/containers
- name: config-volume
configMap:
name: fluentd-config
复制代码
除了 ConfigMap 与 DaemonSet 之外,还需要配置 fluentd 的集群权限:
# 为 fluentd pod 创建 ServiceAcount
# 创建 ClusterRole, 提供 pods、namespace 的 get/list/watch 权限
# 绑定 ClusterRole 至 ServiceAcount
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: default
复制代码
2、Elasticsearch 部署
考虑到大规模集群的复杂性,本文挑选了简单情况下的 ES 集群构成,即 master 与 data 构成的集群,为防止脑裂现象发生,master 节点设置为:minimum_master_nodes = (N/2)+1
,其中 N 为集群中符合主节点条件的总数。
ES 的索引分片会涉及到同步、路由、存储、备份等持久化层面问题,所以 ES 本质上是一个 StatefulSet 的应用。
首先分别创建 master、data 节点的 headless service,用于 Rest API 以及集群内节点之间的通信:
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-master / elasticsearch-data
labels:
component: elasticsearch
role: master / data
namespace: efk
spec:
ports:
- port: 9200
name: rest
- port: 9300
name: transport
clusterIP: None
复制代码
随后创建 StorageClass,用于 PV 动态供给,其中 Provisioner 用于选择使用哪个卷来制备 PV,可以选择 nfs、ceph、glusterfs、local 等方式,此处选用 local 方式:
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: es-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
复制代码
为了保证 PVC、PV 的自动生成、挂载与绑定,可以在 statefuleSet 的配置中增加如下配置:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "es-storage" # StorageClass name 保持一致
resources:
requests:
storage: 100Gi # 依据 provisioner 来定义存储空间
复制代码
在 containers 中的 env 中定义 elasticsearch 的配置,在 volumeMounts 中绑定自动生成的 pvc:
containers:
volumeMounts:
- name: data # 与 volumeClaimTemplates 定义的 name 一致
mountPath: /usr/share/elasticsearch/data
env:
... # es 的配置信息
-------- 自动生成的 pvc ------------
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-es-cluster-0 Bound es-pv1 1Gi RWO es-storage 81m
data-es-cluster-1 Bound es-pv3 1Gi RWO es-storage 80m
data-es-cluster-2 Bound es-pv2 1Gi RWO es-storage 78m
复制代码
不过需要注意的是由于本地 StorageClass 模式不支持 PV 的自动制备,所以本案例,PV 还是需要自动手动创建的:
apiVersion: v1
kind: PersistentVolume
metadata:
name: espv1 # 根据集群规模设置 N 个 pv
labels:
monitor: elasticsearch
namespace: efk
spec:
capacity:
storage: 30Gi # 根据情况 data 节点需要更大的存空间
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain # 回收策略设置为由管理员手工回收
storageClassName: scn
nfs:
path: /nfsdata/data1 # 路径不重复
server: <server-ip>
复制代码
3、Kibana 部署
Kibana 是一个前端无状态服务,且不需要在每个 Pod 都存在,所以选择使用 Deployment 来部署:
...
kind: Deployment
...
spec:
replicas: 1 # 不考虑高可用,配置一个节点即可
...
env: # 配置
- name: ELASTICSEARCH_URL
value: <http://localhost:9200> # 与 es 在一个节点,可以配置为 localhost
...
ports: # 端口
- containerPort: 5601
复制代码
在 Pod 启动后,选择配置一个 nodePort service 把 Kibana 提供给外部访问:
apiVersion: v1
kind: Service
metadata:
name: kibana
spec:
selector:
app: kibana
type: NodePort
ports:
- port: 80
targetPort: 5601
nodePort: 5601
复制代码
最后,向 Fluentd 指向的日志文件中输入数据,即可在 Kibana 界面中可以搜索出对应的数据。
四、总结
本文探讨了 K8s 的日志解决方案,并对 EFK 的功能范围、配置至 K8s 部署做了较为详细的讲述。不过如何让 EFK 更好的发挥作用,及更好的在生产环境中使用,本文讲述的不多。要让 EFK 发挥出更多能量,还需要进一步思考 Fluentd 的性能、ES 集群的高可用、ingest 与 pipeline 处理及日志分析、APM、全链路追踪等等进阶功能。
评论