概述
在Kubernetes集群上运行多个服务和应用程序时,日志收集系统可以帮助我们快速分类和分析由Pod生成的大量日志数据。Kubernetes中比较流行的日志收集解决方案是Elasticsearch、Fluentd和Kibana(EFK)技术栈,也是官方推荐的一种方案。
EFK 正是从 ELK 演变而来的,这一演变的核心,是日志收集器从 Logstash 替换成了更轻量、高效的 Fluentd 或 Filebea
一、ELK/EFK日志流程的几种常见方案
| 方案编号 | 数据流向 |
|---|---|
| 方案1 | Logstash(采集、处理)→ Elasticsearch(存储)→ Kibana(展示) |
| 方案2 | Logstash(采集)→ Logstash(聚合、处理)→ Elasticsearch(存储)→ Kibana |
| 方案3 | Filebeat(采集、处理)→ Elasticsearch(存储)→ Kibana |
| 方案4 | Filebeat(采集)→ Logstash(聚合、处理)→ Elasticsearch(存储)→ Kibana |
| 方案5 | Filebeat(采集)→ Kafka/Redis(消峰)→ Logstash(聚合、处理)→ Elasticsearch(存储)→ Kibana |
说明:方案5引入Kafka或Redis作为消息队列,用于缓解突增日志流量对Elasticsearch的压力,起到流量削峰填谷的作用。
在K8s的日志采集方案中,使用 Filebeat 进行采集的方案最常见,最泛用
二、Fluentd介绍
2.1 什么是Fluentd
Fluentd是一个针对日志的收集、处理、转发系统。通过丰富的插件系统,可以收集来自于各种系统或应用的日志,转化为用户指定的格式后,转发到用户所指定的日志存储系统之中。
Fluentd是随着Docker和Elasticsearch一起流行起来的日志采集Agent。
2.2 Fluentd优势
和多数Logstash插件一样,Fluentd插件是用Ruby语言开发的,非常易于编写维护。所以它数量很多,几乎所有的源和目标存储都有插件(各个插件的成熟度也不太一样)。这也意味着我们可以用Fluentd来串联所有的东西。
三、安装Elasticsearch组件
3.1 创建命名空间
在安装Elasticsearch集群之前,我们先创建一个名称空间,在这个名称空间下安装日志收集工具Elasticsearch、Fluentd、Kibana。我们创建一个kube-logging名称空间,将EFK组件安装到该名称空间中。
bash
[root@hd1 ~]# vim kube-logging.yaml
yaml
kind: Namespace
apiVersion: v1
metadata:
name: kube-logging
或者直接使用命令创建:kubectl create namespace kube-logging
bash
[root@hd1 ~]# kubectl apply -f kube-logging.yaml
# 查看命名空间
[root@hd1 ~]# kubectl get namespaces | grep kube-logging
# 显示如下,说明创建成功:
kube-logging Active 86s
3.2 安装Elasticsearch的Service
创建一个Headless Service 的Kubernetes服务,服务名称是elasticsearch。Headless Service不具备负载均衡也没有ClusterIP。
普通Service与Headless Service对比
| 特性 | 普通Service(ClusterIP) | Headless Service(clusterIP: None) |
|---|---|---|
| 负载均衡实现 | 内置 :由kube-proxy(如iptables/IPVS)实现,流量在多个Pod间自动分发 |
不内置 :不提供负载均衡,kube-proxy不参与处理(通过core dns实现"dns层面"的负载均衡) |
| 访问方式 | 通过一个**虚拟IP(ClusterIP)**访问 | 无虚拟IP,客户端直接获取所有Pod的IP列表 |
| DNS解析结果 | 返回服务的虚拟IP(ClusterIP) | 返回一组Pod的IP地址列表 |
bash
[root@hd1 ~]# vim elasticsearch_svc.yaml
yaml
kind: Service
apiVersion: v1
metadata:
name: elasticsearch
namespace: kube-logging
labels:
app: elasticsearch
spec:
selector: #确认选择的标签
app: elasticsearch
clusterIP: None #无头服务
ports: #这里暴露es的两个端口
- port: 9200 #es业务端口
name: rest
- port: 9300 # Elasticsearch 集群内部的节点通信接口
name: inter-node
bash
[root@hd1 ~]# kubectl apply -f elasticsearch_svc.yaml
配置解析:
- 在
kube-logging名称空间定义了一个名为elasticsearch的Service,带有app=elasticsearch标签 - 当将Elasticsearch StatefulSet与此服务关联时,服务将返回带有标签
app=elasticsearch的Elasticsearch Pods的DNS A记录 - 设置
clusterIP: None,将该服务设置成无头服务 - 定义端口9200(用于REST API交互)和9300(用于节点间通信)
bash
# 查看elasticsearch的service是否创建成功
[root@hd1 ~]# kubectl get services --namespace=kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP
3.3 创建StorageClass(存储类动态供给)
通过StatefulSet创建Elasticsearch集群,需要先创建StorageClass实现存储类动态供给。
3.3.1 安装NFS服务
注意:选择K8s集群的hd1节点作为NFS服务端,其IP为192.168.1.11。
bash
# yum安装nfs(所有节点执行)
[root@hd1 ~]# yum install nfs-utils -y
# 启动nfs服务
[root@hd1 ~]# systemctl start nfs
# 设置nfs开机自启动
[root@hd1 ~]# systemctl enable nfs.service
# 在hd1上创建一个nfs共享目录
[root@hd1 ~]# mkdir /data/v1 -p
# 编辑/etc/exports文件
[root@hd1 ~]# vim /etc/exports
/data/v1 192.168.1.0/24(rw,no_root_squash)
# 加载配置,使配置生效
[root@hd1 ~]# exportfs -arv
[root@hd1 ~]# systemctl restart nfs
3.3.2 创建NFS Provisioner
(1)创建ServiceAccount
bash
[root@hd1 efk]# vim serviceaccount.yaml
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-provisioner
bash
[root@hd1 efk]# kubectl apply -f serviceaccount.yaml
#查看
[root@hd1 ~]# kubectl get sa
NAME SECRETS AGE
nfs-provisioner 0 28s
(2)RBAC授权
bash
[root@hd1]# vim rbac.yaml
yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-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"]
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get"]
- apiGroups: ["extensions"]
resources: ["podsecuritypolicies"]
resourceNames: ["nfs-provisioner"]
verbs: ["use"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-provisioner
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: default
roleRef:
kind: ClusterRole
name: nfs-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-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-provisioner
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-provisioner
apiGroup: rbac.authorization.k8s.io
bash
[root@hd1]# kubectl apply -f rbac.yaml
配置详细解析:
bash
# =============================================================================
# 第一段:ClusterRole - 定义 NFS Provisioner 在整个集群中的"权力清单"
# =============================================================================
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-provisioner-runner
rules:
# 核心:监听 PVC 请求,动态创建/删除 PV
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"] # 增删改查 PV
# 核心:触发源,创建 PV 后需更新 PVC 状态完成绑定
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"] # 监听 PVC,更新绑定状态
# 配置:读取 StorageClass 参数(如 NFS 服务端地址)
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
# 排错:记录失败事件到 kubectl describe 中
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
# 探测:基础环境发现(可忽略,不影响主流程)
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get"]
# 安全:若集群启用 PSP,此条必须保留(K8s >=1.25 需将 apiGroups 改为 ["policy"])
- apiGroups: ["extensions"]
resources: ["podsecuritypolicies"]
resourceNames: ["nfs-provisioner"]
verbs: ["use"]
---
# =============================================================================
# 第二段:ClusterRoleBinding - 把上述"权力"授予服务账户(SA)
# =============================================================================
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-provisioner
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: default # ⚠️ 陷阱:此处必须改为实际的部署命名空间
roleRef:
kind: ClusterRole
name: nfs-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
# =============================================================================
# 第三段:Role - 实现"抢锁选主"(Leader Election),防止多副本同时创建 PV
# =============================================================================
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-provisioner
# namespace: 未指定,默认归 default ⚠️ 陷阱:必须显式加上,与 Deployment 同命名空间
rules:
- apiGroups: [""]
resources: ["endpoints"] # 充当分布式锁对象
verbs: ["get", "list", "watch", "create", "update", "patch"] # 抢锁(写)、续约(更新)
---
# =============================================================================
# 第四段:RoleBinding - 把"选主权"绑定给 SA
# =============================================================================
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-provisioner
# namespace: 未指定,默认归 default ⚠️ 陷阱:必须与第三段及 SA 命名空间一致
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: default # ⚠️ 陷阱:必须改为实际部署的命名空间
roleRef:
kind: Role
name: leader-locking-nfs-provisioner
apiGroup: rbac.authorization.k8s.io
不会在生产环境徒手敲这上百行的 YAML。通常,更加建议:
- 使用 Helm(包管理器)------ 绝对的主流
- 拷贝官方文档的"Production Ready"示例
- 使用 Operator(针对复杂有状态应用)
- AI 辅助
(3)导入NFS Provisioner镜像
bash
# 把nfs-subdir-external-provisioner.tar.gz上传到hd2节点,手动导入
[root@hd2 ~]# ctr -n k8s.io images import nfs-subdir-external-provisioner.tar.gz
# 或者(如果使用docker)
[root@hd2 ~]# docker load -i nfs-subdir-external-provisioner.tar.gz
(4)通过Deployment创建NFS Provisioner
bash
[root@hd1 ~]# vim deployment.yaml
yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-provisioner
spec:
selector:
matchLabels:
app: nfs-provisioner
replicas: 1
strategy: #部署策略
type: Recreate
template:
metadata:
labels:
app: nfs-provisioner
spec:
serviceAccount: nfs-provisioner #关联SA
containers:
- name: nfs-provisioner
image: registry.cn-beijing.aliyuncs.com/mydlq/nfs-subdir-external-provisioner:v4.0.0
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env: #关键环境变量
- name: PROVISIONER_NAME
value: example.com/nfs
- name: NFS_SERVER
value: 192.168.1.11
- name: NFS_PATH
value: /data/v1
volumes:
- name: nfs-client-root
nfs:
server: 192.168.1.11
path: /data/v1
bash
[root@hd1 ~]# kubectl apply -f deployment.yaml
# 验证nfs是否创建成功
[root@hd1 ~]# kubectl get pods | grep nfs
# 显示如下说明创建成功:
nfs-provisioner-5975849bb4-92dhq 1/1 Running 3 11h
(5)创建StorageClass
bash
[root@hd1 ~]# vim class.yaml
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: do-block-storage
provisioner: example.com/nfs
bash
[root@hd1 ~]# kubectl apply -f class.yaml
重要 :
provisioner: example.com/nfs该值需要和NFS Provisioner配置的PROVISIONER_NAME处的value值保持一致。
3.4 安装Elasticsearch集群(StatefulSet)
bash
# 把elasticsearch_7_2_0.tar.gz和busybox.tar.gz上传到hd2,手动导入:
[root@hd2 ~]# ctr -n k8s.io images import elasticsearch_7_2_0.tar.gz
[root@hd3 ~]# ctr -n k8s.io images import busybox.tar.gz
# 或者(如果使用docker)
[root@hd2 ~]# docker load -i elasticsearch_7_2_0.tar.gz
[root@hd3 ~]# docker load -i busybox.tar.gz
bash
[root@hd1 ~]# cat elasticsearch-statefulset.yaml
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es-cluster
namespace: kube-logging
spec:
serviceName: elasticsearch # 关联到之前创建的 Headless Service(无头服务)
replicas: 3
selector:
matchLabels:
app: elasticsearch
template: #pod模板
metadata:
labels:
app: elasticsearch
spec: #容器规格
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.2.0
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
ports:
- containerPort: 9200 # 对外提供 Restful API(增删改查)
name: rest
protocol: TCP
- containerPort: 9300 # 集群内部节点间通信(选主、数据同步)
name: inter-node
protocol: TCP
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
env: #环境变量(Env
- name: cluster.name ## 给集群起名
value: k8s-logs
- name: node.name #节点名,这里是读取 Pod 的 metadata.name(即 Pod 的名字)作为 ES 的节点名
valueFrom:
fieldRef: #属性来自字段
fieldPath: metadata.name #前面我们定义metadata中的name
- name: discovery.seed_hosts #种子节点发现列表,为什么必须写完整的 FQDN
value: "es-cluster-0.elasticsearch.kube-logging.svc.cluster.local,es-cluster-1.elasticsearch.kube-logging.svc.cluster.local,es-cluster-2.elasticsearch.kube-logging.svc.cluster.local"
- name: cluster.initial_master_nodes # 第一次启动谁有资格当主节点
value: "es-cluster-0,es-cluster-1,es-cluster-2"
- name: ES_JAVA_OPTS # 给 JVM 分配 512MB 堆内存
value: "-Xms512m -Xmx512m"
initContainers: #初始化容器
- name: fix-permissions # 作用:修复数据目录权限。ES 进程用户 ID 是 1000,不修改权限,写不进 NFS 目录。
image: busybox:1.28
imagePullPolicy: IfNotPresent
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 # 作用:修改宿主机内核参数。ES 需要大量内存映射区,默认值太小,不调这个 Pod 会因内存不足报错崩溃。
image: busybox:1.28
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext: #安全上下文
privileged: true
- name: increase-fd-ulimit # 作用:提高文件描述符限制。ES 会打开大量文件(索引段),默认 1024 太小。
image: busybox:1.28
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "ulimit -n 65536"]
securityContext:
privileged: true
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: nfs
resources:
requests:
storage: 10Gi
bash
[root@hd1 ~]# kubectl apply -f elasticsearch-statefulset.yaml
资源清单详细解析
(1)StatefulSet主体部分
在kube-logging的名称空间中定义了一个名为es-cluster的StatefulSet。使用serviceName: elasticsearch字段与之前创建的Headless ElasticSearch服务相关联。这样可以确保可以使用以下DNS地址访问StatefulSet中的每个Pod:es-cluster-[0,1,2].elasticsearch.kube-logging.svc.cluster.local,其中[0,1,2]与Pod分配的序号数相对应。
指定3个replicas(3个Pod副本),将selector.matchLabels设置为app: elasticsearch。.spec.selector.matchLabels和.spec.template.metadata.labels字段必须匹配。
(2)Pod模板中的容器配置
| 配置项 | 值 | 说明 |
|---|---|---|
| 容器名 | elasticsearch | - |
| 镜像 | docker.elastic.co/elasticsearch/elasticsearch:7.2.0 | - |
| CPU请求 | 100m | 容器至少需要0.1个vCPU |
| CPU限制 | 1000m | 容器最多可以使用1个vCPU |
| 端口9200 | rest | REST API交互端口 |
| 端口9300 | inter-node | 节点间通信端口 |
| 数据持久化 | volumeMounts: data → /usr/share/elasticsearch/data | 挂载数据卷 |
(3)环境变量说明
| 环境变量 | 值 | 说明 |
|---|---|---|
cluster.name |
k8s-logs | Elasticsearch集群的名称 |
node.name |
metadata.name |
节点名称,解析为es-cluster-0,1,2 |
discovery.seed_hosts |
es-cluster-0.elasticsearch... | 节点相互发现的静态主机列表 |
cluster.initial_master_nodes |
es-cluster-0,es-cluster-1,es-cluster-2 | 初始主节点列表 |
ES_JAVA_OPTS |
-Xms512m -Xmx512m | JVM堆内存大小(最小和最大都是512MB) |
补充 :由于所有Pod都在同一个
kube-logging名称空间下,discovery.seed_hosts可以简写为es-cluster-[0,1,2].elasticsearch(省略svc.cluster.local后缀)。
(4)InitContainer(初始化容器)
这里定义了几个在主应用程序之前运行的Init容器,这些初始容器按照定义的顺序依次执行,执行完成后才会启动主应用容器。
| InitContainer名称 | 作用 | 命令 |
|---|---|---|
fix-permissions |
将Elasticsearch数据目录的用户和组更改为1000:1000(Elasticsearch用户的UID) | chown -R 1000:1000 /usr/share/elasticsearch/data |
increase-vm-max-map |
增加操作系统对mmap计数的限制(默认值可能太低,导致内存不足错误) | sysctl -w vm.max_map_count=262144 |
increase-fd-ulimit |
增加打开文件描述符的最大数量 | ulimit -n 65536 |
重要 :默认情况下,Kubernetes用root用户挂载数据目录,这会使得Elasticsearch无法访问该数据目录,因此需要通过
fix-permissions初始化容器修复权限。
补充:Elasticsearch生产环境文档还建议禁用swap分区以提高性能。
(5)volumeClaimTemplates(持久化存储模板)
yaml
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: nfs #指定已存在的存储类
resources:
requests:
storage: 10Gi
- 使用
volumeClaimTemplates定义持久化模板,Kubernetes会使用它为每个Pod创建PersistentVolume - 访问模式为
ReadWriteOnce,意味着只能被mount到单个节点上进行读写 - 使用名为
do-block-storage的StorageClass对象(提前创建好的NFS Provisioner)
验证Elasticsearch集群是否部署成功
bash
#### 验证Elasticsearch集群是否部署成功
```bash
# 查看es的pod是否创建成功
[root@hd1 ~]# kubectl get pods -n kube-logging
执行上述命令后,预期会看到类似如下的输出,表明 Elasticsearch 集群的三个 Pod 已成功创建并正常运行:
bash
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 55s
es-cluster-1 1/1 Running 0 35s
es-cluster-2 1/1 Running 0 28s
输出字段解读:
- NAME : Pod 名称,遵循 StatefulSet 的命名规则
es-cluster-<序号>。 - READY :
1/1表示 Pod 中唯一的容器已准备就绪。 - STATUS :
Running是期望状态,表示 Pod 正在运行。如果看到Pending、ContainerCreating或Error,则需要进一步排查。 - RESTARTS : 重启次数。
0表示运行稳定。非零值可能表明容器曾崩溃或健康检查失败。 - AGE: Pod 已运行的时间。
常见问题与排查:
- Pod 处于
Pending状态 :通常是由于资源(CPU/内存)不足或节点选择器/污点导致无法调度。使用kubectl describe pod es-cluster-0 -n kube-logging查看事件详情。 - Pod 处于
ContainerCreating状态过久:可能是镜像拉取失败或持久卷声明(PVC)未成功绑定。检查镜像仓库可访问性及 StorageClass 配置。 - Pod 处于
CrashLoopBackOff状态 :容器启动后立即退出。查看容器日志:kubectl logs es-cluster-0 -n kube-logging。常见原因包括:- 数据目录
/usr/share/elasticsearch/data权限问题(fix-permissionsInitContainer 未成功执行)。 - JVM 内存参数
ES_JAVA_OPTS设置不当,导致内存不足。 - Elasticsearch 节点发现失败(检查
discovery.seed_hosts配置的 DNS 解析)。
- 数据目录
进一步验证服务 :
确认与 StatefulSet 关联的 Headless Service 也创建成功:
bash
[root@hd1 ~]# kubectl get svc -n kube-logging
预期输出应包含 elasticsearch 服务,且 CLUSTER-IP 为 None:
bash
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP
root@hd1 \~# kubectl get svc -n kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
elasticsearch ClusterIP None 9200/TCP,9300/TCP
#### 通过REST API验证Elasticsearch集群状态
使用`kubectl port-forward`将本地端口9200转发到Elasticsearch节点:
```bash
[root@hd1 ~]# kubectl port-forward es-cluster-0 9200:9200 --namespace=kube-logging
在另一个终端窗口中执行如下请求:
bash
curl http://localhost:9200/_cluster/state?pretty
输出示例:
json
{
"cluster_name" : "k8s-logs",
"compressed_size_in_bytes" : 348,
"cluster_uuid" : "QD06dK7CQgids-GQZooNVw",
"version" : 3,
"state_uuid" : "mjNIWXAzQVuxNNOQ7xR-qg",
"master_node" : "IdM5B7cUQWqFgIHXBp0JDg",
"blocks" : { },
"nodes" : {
"u7DoTpMmSCixOoictzHItA" : {
"name" : "es-cluster-1",
"ephemeral_id" : "ZlBflnXKRMC4RvEACHIVdg",
"transport_address" : "10.244.8.2:9300",
"attributes" : { }
},
"IdM5B7cUQWqFgIHXBp0JDg" : {
"name" : "es-cluster-0",
"ephemeral_id" : "JTk1FDdFQuWbSFAtBxdxAQ",
"transport_address" : "10.244.44.3:9300",
"attributes" : { }
},
"R8E7xcSUSbGbgrhAdyAKmQ" : {
"name" : "es-cluster-2",
"ephemeral_id" : "9wv6ke71Qqy9vk2LgJTqaA",
"transport_address" : "10.244.40.4:9300",
"attributes" : { }
}
......
}
}
说明 :看到上面的信息就表明我们名为
k8s-logs的Elasticsearch集群成功创建了3个节点:es-cluster-0、es-cluster-1和es-cluster-2,当前主节点是es-cluster-0。
四、安装Kibana
bash
# 把kibana_7_2_0.tar.gz上传到hd2和hd3节点,手动导入:
[root@hd2 ~]# ctr -n k8s.io images import kibana_7_2_0.tar.gz
# 或者(如果使用docker)
[root@hd2 ~]# docker load -i kibana_7_2_0.tar.gz
查看yaml清单内容
bash
[root@hd1.com]# vim kibana.yaml
yaml
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
ports:
- port: 5601 #端口
selector:
app: kibana
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana
namespace: kube-logging
labels:
app: kibana
spec:
replicas: 1
selector:
matchLabels:
app: kibana
template:
metadata:
labels:
app: kibana
spec:
containers:
- name: kibana
image: docker.elastic.co/kibana/kibana:7.2.0
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
env:
- name: ELASTICSEARCH_URL
value: http://elasticsearch:9200 : #这里是 es 的 service 的短域名
ports:
- containerPort: 5601
service 没有显式指定 type 字段,它默认就是 ClusterIP 类型
bash
[root@hd1 ~]# kubectl apply -f kibana.yaml
配置解析:
- 定义了两个资源对象:一个Service和一个Deployment
- Kibana Pod中使用
ELASTICSEARCH_URL环境变量设置Elasticsearch集群的端点和端口 - 直接使用Kubernetes DNS
http://elasticsearch:9200,此端点对应服务名称为elasticsearch - 由于是一个Headless Service,该域将解析为3个Elasticsearch Pod的IP地址列表
bash
# 查看Kibana是否部署成功
[root@hd1 ~]# kubectl get pods -n kube-logging
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 170m
es-cluster-1 1/1 Running 0 170m
es-cluster-2 1/1 Running 0 170m
kibana-5749b5778b-c9djr 1/1 Running 0 4m3s
[root@hd1 ~]# kubectl get svc -n kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP
kibana ClusterIP 10.106.209.160 <none> 5601/TCP
可以看到,因为service的类型没有指定,实验默认是cluster,并没有暴露出来
修改Service类型为NodePort(对外暴露访问)
bash
[root@hd1.com]# kubectl edit svc kibana -n kube-logging
# 把 type: ClusterIP 改成 type: NodePort
# 保存退出
bash
[root@hd1 ~]# kubectl get svc -n kube-logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP
kibana NodePort 10.106.209.160 <none> 5601:31966/TCP
#这时候它的映射端口就暴露出来了
在浏览器中打开 http://<任意节点IP>:31966 即可访问Kibana界面。

五、安装Fluentd组件
Fluentd 是我们用来进行信息采集 的组件,使用DaemonSet 控制器部署Fluentd组件,这样可以保证集群中的每个节点都可以运行同样的Fluentd Pod副本,从而可以收集K8s集群中每个节点的日志。
在K8s集群中,容器应用程序的输入输出日志会重定向到Node节点里的JSON文件中,Fluentd可以tail(跟踪)和过滤以及把日志转换成指定的格式发送到Elasticsearch集群中。
补充:除了容器日志,Fluentd也可以采集kubelet、kube-proxy、docker的日志。
bash
# 离线镜像压缩包fluentd.tar.gz上传到hd1,hd2和hd3上,手动导入:
[root@hd1 ~]# ctr -n k8s.io images import fluentd.tar.gz
# 或者(如果使用docker)
[root@hd1 ~]# docker load -i fluentd.tar.gz
bash
[root@hd1 ~]# cat fluentd.yaml
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
labels:
app: fluentd
rules:
- apiGroups:
- ""
resources:
- pods
- namespaces
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: fluentd
roleRef:
kind: ClusterRole
name: fluentd
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: fluentd
namespace: kube-logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: kube-logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccount: fluentd
serviceAccountName: fluentd
tolerations: #容忍度
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1.4.2-debian-elasticsearch-1.1
imagePullPolicy: IfNotPresent
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "elasticsearch.kube-logging.svc.cluster.local"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: "http"
- name: FLUENTD_SYSTEMD_CONF
value: disable
resources:
limits:
memory: 512Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log #把宿主机对应路径挂载到这个目录
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers #将宿主机容器的日志路径挂载到容器中这个路径
readOnly: true
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
这段配置的作用:
- 第一段:
- 创建了一个名为 fluentd 的服务账户,专门用于 Fluentd Pod
- 第二段和第三段:
- 定义 Fluentd 对 pods 和 namespaces 这两个资源拥有读取权限(get、list、watch)
- 把第二段定义的权限绑定给 kube-logging 命名空间下的 fluentd 服务账户
- 第四段
- 允许 Fluentd Pod 调度到控制平面节点(Master/Control-plane)
- 告诉 Fluentd 要将日志发送到哪个 Elasticsearch 集群
- 告诉 Fluentd 要将日志发送到哪个 Elasticsearch 集群
- 这里使用的是 K8s 集群内的完整 DNS 域名(FQDN):elasticsearch.kube-logging.svc.cluster.local,其中elasticsearch是之前创建的 Headless Service 名称
- 将宿主机的日志目录挂载到 Fluentd 容器内部
bash
[root@hd1 ~]# kubectl apply -f fluentd.yaml
配置解析:
- ServiceAccount和RBAC:为Fluentd Pod赋予访问Kubernetes API的权限,用于获取Pod和Namespace的元数据信息(如Pod名称、Namespace名称等),这些信息会作为日志的标签发送到Elasticsearch,方便后续按Pod/Namespace维度检索。
- DaemonSet:保证每个节点运行一个Fluentd Pod
- Tolerations:允许调度到控制平面节点(master节点),采集master组件的日志
- 环境变量:配置Elasticsearch的地址(使用完整的Kubernetes DNS域名)、端口和协议
- VolumeMounts :
/var/log:宿主机系统日志目录(包含kubelet、kube-proxy等组件日志)/var/lib/docker/containers:Docker容器日志存储目录(只读挂载)
- hostPath:直接挂载宿主机目录到容器中
bash
# 查看fluentd是否部署成功
[root@hd1 ~]# kubectl get pods -n kube-logging
NAME READY STATUS RESTARTS
es-cluster-0 1/1 Running 0
es-cluster-1 1/1 Running 0
es-cluster-2 1/1 Running 0
fluentd-dszg7 1/1 Running 0
fluentd-dwk7t 1/1 Running 0
kibana-84cf7f59c-2546x 1/1 Running 0
六、Kibana对接Elasticsearch
6.1 Fluentd启动成功后,前往Kibana Dashboard
在浏览器中打开 http://<任意节点IP>:31966。
建议选择edge这种自带翻译的浏览器,操作起来会方便一些
6.2 配置索引模式
Fluentd配置文件中采集的日志使用的是logstash格式 ,所以需要创建匹配logstash-*的索引模式来匹配Elasticsearch集群中的所有日志数据。
操作步骤:
- 点击左侧的Discover

- 在文本框中输入
logstash-*匹配索引,点击 Next step

- 选择
@timestamp作为时间过滤器字段,点击 Create index pattern(创建索引模式)

6.3 查看日志
点击左侧的Discover,即可看到来自Kubernetes集群的所有日志数据。

七、测试:收集Pod容器日志
7.1 创建一个测试Pod
bash
[root@hd1 ~]# vim pod.yaml
yaml
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
containers:
- name: count
image: busybox:1.28
imagePullPolicy: IfNotPresent
args: [/bin/sh, -c, 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']
bash
[root@hd1 ~]# kubectl apply -f pod.yaml
7.2 在Kibana中查看和过滤Pod日志
登录Kibana控制面板,在Discover页面的搜索栏中输入 kubernetes.pod_name:counter,或者直接输入容器名counter,就能过滤名为counter的Pod的日志数据。

或者直接搜索节点名,就可以检索到所有hd1节点的日志:

八、总结
通过上面几个步骤,我们已经在Kubernetes集群成功部署了EFK日志系统,包括:
- 3个Elasticsearch Pod(StatefulSet + 持久化存储)
- 1个Kibana Pod(Deployment + NodePort Service)
- 一组Fluentd Pod(DaemonSet,每个节点一个)
EFK系统的工作流程:
- Fluentd (DaemonSet)从每个节点的
/var/log和/var/lib/docker/containers目录采集日志 - Fluentd将日志添加Kubernetes元数据(Pod名称、Namespace、标签等)后,发送到Elasticsearch集群
- Elasticsearch对日志进行存储和索引
- Kibana提供Web界面供用户检索、分析和可视化日志数据
这套方案可以收集Kubernetes集群中所有节点上的容器日志和系统组件日志,并通过统一的Web界面进行查询和分析。