前言
在本篇文章中,您将学习 Kubernetes 集群日志中涉及的关键概念和工作流。
当涉及到 Kubernetes 生产调试时,日志起着至关重要的作用。它可以帮助你理解正在发生的事情,哪里出了问题,甚至是哪里可能出问题。
作为一名 DevOps 工程师,您应该清楚地了解 Kubernetes 日志以解决集群和应用程序问题。
k8s 日志收集架构
总体分为三种方式:
使用节点级日志代理
容器日志驱动:
https://docs.docker.com/config/containers/logging/configure/
查看当前的 docker 主机的驱动:
$ docker info --format '{{.LoggingDriver}}'
复制代码
json-file
格式,docker 会默认将标准和错误输出保存为宿主机的文件,路径为:
/var/lib/docker/containers/<container-id>/<container-id>-json.log
并且可以设置日志轮转:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"labels": "production_status",
"env": "os,customer"
}
}
复制代码
优势:
劣势:
使用 sidecar 容器和日志代理
思路:在 pod 中启动一个 sidecar 容器,把容器内的日志文件吐到标准输出,由宿主机中的日志收集 agent 进行采集。
$ cat count-pod.yaml
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: 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
volumes:
- name: varlog
emptyDir: {}
$ kubectl apply -f counter-pod.yaml
$ kubectl logs -f counter -c count-log-1
复制代码
优势:
劣势:
每个业务 pod 都需要做一次改造
增加了一次日志的写入,对磁盘使用率有一定影响
思路:直接在业务 Pod 中使用 sidecar 的方式启动一个日志收集的组件(比如 fluentd),这样日志收集可以将容器内的日志当成本地文件来进行收取。
优势:不用往宿主机存储日志,本地日志完全可以收集
劣势:每个业务应用额外启动一个日志 agent,带来额外的资源损耗
从应用中直接暴露日志目录
企业日志方案选型
目前来讲,最建议的是采用节点级的日志代理。
方案一:自研方案,实现一个自研的日志收集 agent,大致思路:
针对容器的标准输出及错误输出,使用常规的方式,监听宿主机中的容器输出路径即可。
针对容器内部的日志文件。
在容器内配置统一的环境变量,比如 LOG_COLLECT_FILES,指定好容器内待收集的日志目录及文件
agent 启动的时候挂载 docker.sock 文件及磁盘的根路径。
监听 docker 的容器新建、删除事件,通过 docker 的 api,查出容器的存储、环境变量、k8s 属性等信息。
配置了 LOG_COLLECT_FILES 环境变量的容器,根据 env 中的日志路径找到主机中对应的文件路径,然后生成收集的配置文件 agent 与开源日志收集工具(Fluentd 或者 filebeat 等)配合。
agent 负责下发配置到收集工具中并对进程做 reload
fluentd-pilot
方案二:日志使用开源的 Agent 进行收集(EFK 方案),适用范围广,可以满足绝大多数日志收集、展示的需求。
fluentd 概念及工作流程
EFK 架构工作流程
一个开源的分布式、Restful 风格的搜索和数据分析引擎,它的底层是开源库 Apache Lucene。它可以被下面这样准确地形容:
Kibana 是一个开源的分析和可视化平台,设计用于和 Elasticsearch 一起工作。可以通过 Kibana 来搜索,查看,并和存储在 Elasticsearch 索引中的数据进行交互。也可以轻松地执行高级数据分析,并且以各种图标、表格和地图的形式可视化数据。
一个针对日志的收集、处理、转发系统。通过丰富的插件系统,可以收集来自于各种系统或应用的日志,转化为用户指定的格式后,转发到用户所指定的日志存储系统之中。
Fluentd 通过一组给定的数据源抓取日志数据,处理后(转换成结构化的数据格式)将它们转发给其他服务,比如 Elasticsearch、对象存储、kafka 等等。Fluentd 支持超过 300 个日志存储和分析服务,所以在这方面是非常灵活的。主要运行步骤如下:
首先 Fluentd 从多个日志源获取数据
结构化并且标记这些数据
然后根据匹配的标签将数据发送到多个目标服务
Fluentd 架构
为什么推荐使用 fluentd 作为 k8s 体系的日志收集工具?
基于 C 和 Ruby 语言, 30-40MB,13,000 events/second/core
极强的可靠性
基于内存和本地文件的缓存
强大的故障转移
fluentd 事件流的生命周期及指令配置
Input -> filter 1 -> ... -> filter N -> Buffer -> Output
复制代码
启动命令
指令介绍:
<source>
@type tail
path /var/log/httpd-access.log
pos_file /var/log/td-agent/httpd-access.log.pos
tag myapp.access
format apache2
</source>
复制代码
filter 可以串联成 pipeline,对数据进行串行处理,最终再交给 match 输出。 如下可以对事件内容进行处理:
<source>
@type http
port 9880
</source>
<filter myapp.access>
@type record_transformer
<record>
host_param “#{Socket.gethostname}”
</record>
</filter>
复制代码
filter 获取数据后,调用内置的 @type record_transformer 插件,在事件的 record 里插入了新的字段 host_param,然后再交给 match 输出。
可以在 source
里指定 @label
,这个 source 所触发的事件就会被发送给指定的 label 所包含的任务,而不会被后续的其他任务获取到。
<source>
@type forward
</source>
<source>
### 这个任务指定了 label 为 @SYSTEM
### 会被发送给 <label @SYSTEM>
### 而不会被发送给下面紧跟的 filter 和 match
@type tail
@label @SYSTEM
path /var/log/httpd-access.log
pos_file /var/log/td-agent/httpd-access.log.pos
tag myapp.access
format apache2
</source>
<filter access.**>
@type record_transformer
<record>
# …
</record>
</filter>
<match **>
@type elasticsearch
# …
</match>
<label @SYSTEM>
### 将会接收到上面 @type tail 的 source event
<filter var.log.middleware.**>
@type grep
# …
</filter>
<match **>
@type s3
# …
</match>
</label>
复制代码
查找匹配 “tags” 的事件,并处理它们。match 命令的最常见用法是将事件输出到其他系统(因此,与 match 命令对应的插件称为 “输出插件”)
<source>
@type http
port 9880
</source>
<filter myapp.access>
@type record_transformer
<record>
host_param “#{Socket.gethostname}”
</record>
</filter>
<match myapp.access>
@type file
path /var/log/fluent/access
</match>
复制代码
事件的结构:
time:事件的处理时间
tag:事件的来源,在 fluentd.conf 中配置
record:真实的日志内容,json 对象
比如,下面这条原始日志:
192.168.0.1 - - [28/Feb/2013:12:00:00 +0900] "GET / HTTP/1.1" 200 777
复制代码
经过 fluentd 引擎处理完后的样子可能是:
2020-07-16 08:40:35 +0000 apache.access: {"user":"-","method":"GET","code":200,"size":777,"host":"192.168.0.1","path":"/"}
复制代码
fluentd 的 buffer 事件缓冲模型
Input -> filter 1 -> ... -> filter N -> Buffer -> Output
复制代码
因为每个事件数据量通常很小,考虑数据传输效率、稳定性等方面的原因,所以基本不会每条事件处理完后都会立马写入到 output 端,因此 fluentd 建立了缓冲模型,模型中主要有两个概念:
可以设置的参数,主要有:
buffer_type,缓冲类型,可以设置 file 或者 memory
buffer_chunk_limit,每个 chunk 块的大小,默认 8MB
buffer_queue_limit ,chunk 块队列的最大长度,默认 256
flush_interval ,flush 一个 chunk 的时间间隔
retry_limit ,chunk 块发送失败重试次数,默认 17 次,之后就丢弃该 chunk 数据
retry_wait ,重试发送 chunk 数据的时间间隔,默认 1s,第 2 次失败再发送的话,间隔 2s,下次 4 秒,以此类推
大致的过程为:
随着 fluentd 事件的不断生成并写入 chunk,缓存块持变大,当缓存块满足 buffer_chunk_limit 大小或者新的缓存块诞生超过 flush_interval 时间间隔后,会推入缓存 queue 队列尾部,该队列大小由 buffer_queue_limit 决定。
比较理想的情况是每次有新的缓存块进入缓存队列,则立马会被写入到后端,同时,新缓存块也持续入列,但是入列的速度不会快于出列的速度,这样基本上缓存队列处于空的状态,队列中最多只有一个缓存块。
但是实际情况考虑网络等因素,往往缓存块被写入后端存储的时候会出现延迟或者写入失败的情况,当缓存块写入后端失败时,该缓存块还会留在队列中,等 retry_wait 时间后重试发送,当 retry 的次数达到 retry_limit 后,该缓存块被销毁(数据被丢弃)。
此时缓存队列持续有新的缓存块进来,如果队列中存在很多未及时写入到后端存储的缓存块的话,当队列长度达到 buffer_queue_limit 大小,则新的事件被拒绝,fluentd 报错,error_class=Fluent:BufferOverflowError error="buffer space has too many data"。
还有一种情况是网络传输缓慢的情况,若每 3 秒钟会产生一个新块,但是写入到后端时间却达到了 30s 钟,队列长度为 100,那么每个块出列的时间内,又有新的 10 个块进来,那么队列很快就会被占满,导致异常出现。
fluentd 配置实践
实践一:实现业务应用日志的收集及字段解析
目标:收集容器内的 nginx 应用的 access.log 日志,并解析日志字段为 JSON 格式,原始日志的格式为:
$ tail -f access.log
...
53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https
复制代码
收集并处理成:
{
"serverIp": "53.49.146.149",
"timestamp": "1561620585.973",
"respondTime": "0.005",
"httpCode": "502",
"eventTime": "27/Jun/2019:15:29:45 +0800",
"clientIp": "178.73.215.171",
"clientPort": "33337",
"method": "GET",
"protocol": "https"
}
复制代码
思路:
fluent.conf
<source>
@type tail
@label @nginx_access
path /var/log/nginx/access.log
pos_file /var/log/nginx/nginx_access.posg
tag nginx_access
format none
@log_level trace
</source>
<label @nginx_access>
<filter nginx_access>
@type parser
key_name message
format /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/
</filter>
<match nginx_access>
@type stdout
</match>
</label>
复制代码
启动服务,追加文件内容:
$ docker run -u root --rm -ti quay.io/fluentd_elasticsearch/fluentd:v3.1.0 sh
/ # cat /etc/fluent/fluent.conf
/ # mkdir /etc/fluent/config.d
/ # fluentd -c /etc/fluent/fluent.conf
/ # echo '53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https' >>/var/log/nginx/access.log
复制代码
使用该网站进行正则校验: http://fluentular.herokuapp.com
实践二:使用 ruby 实现日志字段的转换及自定义处理
<source>
@type tail
@label @nginx_access
path /var/log/nginx/access.log
pos_file /var/log/nginx/nginx_access.posg
tag nginx_access
format none
@log_level trace
</source>
<label @nginx_access>
<filter nginx_access>
@type parser
key_name message
format /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/
</filter>
<filter nginx_access>
@type record_transformer
enable_ruby
<record>
host_name "#{Socket.gethostname}"
my_key "my_val"
tls ${record["protocol"].index("https") ? "true" : "false"}
</record>
</filter>
<match nginx_access>
@type stdout
</match>
</label>
复制代码
configMap 挂载场景
开始之前,我们先来回顾一下,configmap 的常用的挂载场景。
场景一:单文件挂载到空目录
假如业务应用有一个配置文件,名为 application.yml
,如果想将此配置挂载到 pod 的/etc/application/
目录中。
application.yml
的内容为:
$ cat application.yml
spring:
application:
name: svca-service
cloud:
config:
uri: http://config:8888
fail-fast: true
username: user
password: ${CONFIG_SERVER_PASSWORD:password}
retry:
initial-interval: 2000
max-interval: 10000
multiplier: 2
max-attempts: 10
复制代码
该配置文件在 k8s 中可以通过 configmap 来管理,通常我们有如下两种方式来管理配置文件:
# 通过文件直接创建
$ kubectl -n default create configmap application-config --from-file=application.yml
# 会生成配置文件,查看内容,configmap的key为文件名字
$ kubectl -n default get cm application-config -oyaml
复制代码
$ cat application-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: application-config
namespace: default
data:
application.yml: |
spring:
application:
name: svca-service
cloud:
config:
uri: http://config:8888
fail-fast: true
username: user
password: ${CONFIG_SERVER_PASSWORD:password}
retry:
initial-interval: 2000
max-interval: 10000
multiplier: 2
max-attempts: 10
# 创建configmap
$ kubectl apply -f application-config.yaml
复制代码
准备一个demo-deployment.yaml
文件,挂载上述 configmap 到/etc/application/
中
$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
namespace: default
spec:
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
volumes:
- configMap:
name: application-config
name: config
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: "/etc/application"
name: config
复制代码
创建并查看:
$ kubectl apply -f demo-deployment.yaml
复制代码
修改 configmap 文件的内容,观察 pod 中是否自动感知变化:
$ kubectl edit cm application-config
复制代码
整个 configmap 文件直接挂载到 pod 中,若 configmap 变化,pod 会自动感知并拉取到 pod 内部。
但是 pod 内的进程不会自动重启,所以很多服务会实现一个内部的 reload 接口,用来加载最新的配置文件到进程中。
场景二:多文件挂载
假如有多个配置文件,都需要挂载到 pod 内部,且都在一个目录中
$ cat application.yml
spring:
application:
name: svca-service
cloud:
config:
uri: http://config:8888
fail-fast: true
username: user
password: ${CONFIG_SERVER_PASSWORD:password}
retry:
initial-interval: 2000
max-interval: 10000
multiplier: 2
max-attempts: 10
$ cat supervisord.conf
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisord]
logfile=/var/logs/supervisor/supervisord.log
logfile_maxbytes=200MB
logfile_backups=10
loglevel=info
pidfile=/var/run/supervisord.pid
childlogdir=/var/cluster_conf_agent/logs/supervisor
nodaemon=false
复制代码
同样可以使用两种方式创建:
$ kubectl delete cm application-config
$ kubectl create cm application-config --from-file=application.yml --from-file=supervisord.conf
$ kubectl get cm application-config -oyaml
复制代码
观察 Pod 已经自动获取到最新的变化
$ kubectl exec demo-55c649865b-gpkgk ls /etc/application/
application.yml
supervisord.conf
复制代码
此时,是挂载到 pod 内的空目录中/etc/application
,假如想挂载到 pod 已存在的目录中,比如:
$ kubectl exec demo-55c649865b-gpkgk ls /etc/profile.d
color_prompt
locale
复制代码
更改 deployment 的挂载目录:
$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
namespace: default
spec:
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
volumes:
- configMap:
name: application-config
name: config
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: "/etc/profile.d"
name: config
复制代码
重建 pod
$ kubectl apply -f demo-deployment.yaml
# 查看pod内的/etc/profile.d目录,发现已有文件被覆盖
$ kubectl exec demo-77d685b9f7-68qz7 ls /etc/profile.d
application.yml
supervisord.conf
复制代码
场景三 挂载子路径
实现多个配置文件,可以挂载到 pod 内的不同的目录中。比如:
configmap 保持不变,修改 deployment 文件:
$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
namespace: default
spec:
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
volumes:
- name: config
configMap:
name: application-config
items:
- key: application.yml
path: application
- key: supervisord.conf
path: supervisord
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: "/etc/application/application.yml"
name: config
subPath: application
- mountPath: "/etc/profile.d/supervisord.conf"
name: config
subPath: supervisord
复制代码
测试挂载:
$ kubectl apply -f demo-deployment.yaml
$ kubectl exec demo-78489c754-shjhz ls /etc/application
application.yml
$ kubectl exec demo-78489c754-shjhz ls /etc/profile.d/
supervisord.conf
color_prompt
locale
复制代码
使用 subPath 挂载到 Pod 内部的文件,不会自动感知原有 ConfigMap 的变更
EFK 基于 k8s 部署
部署分析
es 生产环境是部署 es 集群,通常会使用 statefulset 进行部署
es 默认使用 elasticsearch 用户启动进程,es 的数据目录是通过宿主机的路径挂载,因此目录权限被主机的目录权限覆盖,因此可以利用 initContainer 容器在 es 进程启动之前把目录的权限修改掉,注意 init container 要用特权模式启动。
若希望使用 helm 部署,参考 https://github.com/helm/charts/tree/master/stable/elasticsearch
使用 StatefulSet 管理有状态服务
使用 Deployment 创建多副本的 pod 的情况:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
labels:
app: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx-deployment
template:
metadata:
labels:
app: nginx-deployment
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
复制代码
使用 StatefulSet 创建多副本 pod 的情况:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-statefulset
namespace: default
labels:
app: nginx-sts
spec:
replicas: 3
serviceName: "nginx"
selector:
matchLabels:
app: nginx-sts
template:
metadata:
labels:
app: nginx-sts
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
复制代码
无头服务 Headless Service
kind: Service
apiVersion: v1
metadata:
name: nginx
namespace: default
spec:
selector:
app: nginx-sts
ports:
- protocol: TCP
port: 80
targetPort: 80
clusterIP: None
复制代码
$ kubectl -n default exec -ti nginx-statefulset-0 sh
/ # curl nginx-statefulset-2.nginx
复制代码
部署并验证
es-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: es-config
namespace: logging
data:
elasticsearch.yml: |
cluster.name: "luffy-elasticsearch"
node.name: "${POD_NAME}"
network.host: 0.0.0.0
discovery.seed_hosts: "es-svc-headless"
cluster.initial_master_nodes: "elasticsearch-0,elasticsearch-1,elasticsearch-2"
复制代码
es-svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: es-svc-headless
namespace: logging
labels:
k8s-app: elasticsearch
spec:
selector:
k8s-app: elasticsearch
clusterIP: None
ports:
- name: in
port: 9300
protocol: TCP
复制代码
es-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: logging
labels:
k8s-app: elasticsearch
spec:
replicas: 3
serviceName: es-svc-headless
selector:
matchLabels:
k8s-app: elasticsearch
template:
metadata:
labels:
k8s-app: elasticsearch
spec:
initContainers:
- command:
- /sbin/sysctl
- -w
- vm.max_map_count=262144
image: alpine:3.6
imagePullPolicy: IfNotPresent
name: elasticsearch-logging-init
resources: {}
securityContext:
privileged: true
- name: fix-permissions
image: alpine:3.6
command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: es-data-volume
mountPath: /usr/share/elasticsearch/data
containers:
- name: elasticsearch
image: elasticsearch:7.4.2
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
resources:
limits:
cpu: '1'
memory: 2Gi
requests:
cpu: '1'
memory: 2Gi
ports:
- containerPort: 9200
name: db
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: es-config-volume
mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
subPath: elasticsearch.yml
- name: es-data-volume
mountPath: /usr/share/elasticsearch/data
volumes:
- name: es-config-volume
configMap:
name: es-config
items:
- key: elasticsearch.yml
path: elasticsearch.yml
volumeClaimTemplates:
- metadata:
name: es-data-volume
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "nfs"
resources:
requests:
storage: 5Gi
复制代码
es-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: es-svc
namespace: logging
labels:
k8s-app: elasticsearch
spec:
selector:
k8s-app: elasticsearch
ports:
- name: out
port: 9200
protocol: TCP
复制代码
$ kubectl create namespace logging
## 部署服务
$ kubectl apply -f es-config.yaml
$ kubectl apply -f es-svc-headless.yaml
$ kubectl apply -f es-sts.yaml
$ kubectl apply -f es-svc.yaml
## 等待片刻,查看一下es的pod部署到了k8s-slave1节点,状态变为running
$ kubectl -n logging get po -o wide
NAME READY STATUS RESTARTS AGE IP
elasticsearch-0 1/1 Running 0 15m 10.244.0.126
elasticsearch-1 1/1 Running 0 15m 10.244.0.127
elasticsearch-2 1/1 Running 0 15m 10.244.0.128
# 然后通过curl命令访问一下服务,验证es是否部署成功
$ kubectl -n logging get svc
es-svc ClusterIP 10.104.226.175 <none> 9200/TCP 2s
es-svc-headless ClusterIP None <none> 9300/TCP 32m
$ curl 10.104.226.175:9200
{
"name" : "elasticsearch-2",
"cluster_name" : "luffy-elasticsearch",
"cluster_uuid" : "7FDIACx9T-2ajYcB5qp4hQ",
"version" : {
"number" : "7.4.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date" : "2019-10-28T20:40:44.881551Z",
"build_snapshot" : false,
"lucene_version" : "8.2.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
复制代码
部署 kibana
kibana 需要暴露 web 页面给前端使用,因此使用 ingress 配置域名来实现对 kibana 的访问
kibana 为无状态应用,直接使用 Deployment 来启动
kibana 需要访问 es,直接利用 k8s 服务发现访问此地址即可,http://es-svc:9200
部署并验证
efk/kibana.yaml
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: kibana:7.4.2
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
env:
- name: ELASTICSEARCH_HOSTS
value: http://es-svc:9200
- name: SERVER_NAME
value: kibana-logging
- name: SERVER_REWRITEBASEPATH
value: "false"
ports:
- containerPort: 5601
volumeMounts:
- name: config
mountPath: /usr/share/kibana/config/
volumes:
- name: config
configMap:
name: kibana-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: kibana-config
namespace: logging
data:
kibana.yml: |-
elasticsearch.requestTimeout: 90000
server.host: "0"
xpack.monitoring.ui.container.elasticsearch.enabled: true
---
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: logging
labels:
app: kibana
spec:
ports:
- port: 5601
protocol: TCP
targetPort: 5601
type: ClusterIP
selector:
app: kibana
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kibana
namespace: logging
spec:
rules:
- host: kibana.luffy.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kibana
port:
number: 5601
复制代码
$ kubectl apply -f kibana.yaml
deployment.apps/kibana created
service/kibana created
ingress/kibana created
## 配置域名解析 kibana.luffy.com,并访问服务进行验证,若可以访问,说明连接es成功
# GET /_cat/health?v
# GET /_cat/indices
复制代码
Fluentd 服务部署
部署分析
fluentd 为日志采集服务,kubernetes 集群的每个业务节点都有日志产生,因此需要使用 daemonset 的模式进行部署
为进一步控制资源,会为 daemonset 指定一个选择标签,fluentd=true 来做进一步过滤,只有带有此标签的节点才会部署 fluentd
日志采集,需要采集哪些目录下的日志,采集后发送到 es 端,因此需要配置的内容比较多,我们选择使用 configmap 的方式把配置文件整个挂载出来
部署服务
efk/fluentd-es-config-main.yaml
apiVersion: v1
data:
fluent.conf: |-
# This is the root config file, which only includes components of the actual configuration
#
# Do not collect fluentd's own logs to avoid infinite loops.
<match fluent.**>
@type null
</match>
@include /fluentd/etc/config.d/*.conf
kind: ConfigMap
metadata:
labels:
addonmanager.kubernetes.io/mode: Reconcile
name: fluentd-es-config-main
namespace: logging
复制代码
配置文件,fluentd-config.yaml
,注意点:
1、数据源 source 的配置,k8s 会默认把容器的标准和错误输出日志重定向到宿主机中
2、默认集成了 kubernetes_metadata_filter 插件,来解析日志格式,得到 k8s 相关的元数据,raw.kubernetes
3、match 输出到 es 端的 flush 配置
efk/fluentd-configmap.yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: fluentd-config
namespace: logging
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
containers.input.conf: |-
<source>
@id fluentd-containers.log
@type tail
path /var/log/containers/*.log
pos_file /var/log/es-containers.log.pos
time_format %Y-%m-%dT%H:%M:%S.%NZ
localtime
tag raw.kubernetes.*
format json
read_from_head false
</source>
# Detect exceptions in the log output and forward them as one log entry.
# https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions
<match raw.kubernetes.**>
@id raw.kubernetes
@type detect_exceptions
remove_tag_prefix raw
message log
stream stream
multiline_flush_interval 5
max_bytes 500000
max_lines 1000
</match>
# Concatenate multi-line logs
<filter **>
@id filter_concat
@type concat
key message
multiline_end_regexp /\n$/
separator ""
</filter>
output.conf: |-
# Enriches records with Kubernetes metadata
<filter kubernetes.**>
@type kubernetes_metadata
</filter>
<match **>
@id elasticsearch
@type elasticsearch
@log_level info
include_tag_key true
hosts elasticsearch-0.es-svc-headless:9200,elasticsearch-1.es-svc-headless:9200,elasticsearch-2.es-svc-headless:9200
#port 9200
logstash_format true
#index_name kubernetes-%Y.%m.%d
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>
复制代码
daemonset 定义文件,fluentd.yaml,注意点:
需要配置 rbac 规则,因为需要访问 k8s api 去根据日志查询元数据
需要将/var/log/containers/目录挂载到容器中
需要将 fluentd 的 configmap 中的配置文件挂载到容器内
想要部署 fluentd 的节点,需要添加 fluentd=true 的标签
efk/fluentd.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:
labels:
addonmanager.kubernetes.io/mode: Reconcile
k8s-app: fluentd-es
name: fluentd-es
namespace: logging
spec:
selector:
matchLabels:
k8s-app: fluentd-es
template:
metadata:
labels:
k8s-app: fluentd-es
spec:
containers:
- image: quay.io/fluentd_elasticsearch/fluentd:v3.1.0
imagePullPolicy: IfNotPresent
name: fluentd-es
resources:
limits:
memory: 500Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/log
name: varlog
- mountPath: /var/lib/docker/containers
name: varlibdockercontainers
readOnly: true
- mountPath: /etc/fluent/config.d
name: config-volume
nodeSelector:
fluentd: "true"
securityContext: {}
serviceAccount: fluentd-es
serviceAccountName: fluentd-es
volumes:
- hostPath:
path: /var/log
name: varlog
- hostPath:
path: /var/lib/docker/containers
name: varlibdockercontainers
- configMap:
defaultMode: 420
name: fluentd-config
name: config-volume
复制代码
## 给slave1打上标签,进行部署fluentd日志采集服务
$ kubectl label node k8s-slave1 fluentd=true
$ kubectl label node k8s-slave2 fluentd=true
# 创建服务
$ kubectl apply -f fluentd-es-config-main.yaml
configmap/fluentd-es-config-main created
$ kubectl apply -f fluentd-configmap.yaml
configmap/fluentd-config created
$ kubectl apply -f fluentd.yaml
serviceaccount/fluentd-es created
clusterrole.rbac.authorization.k8s.io/fluentd-es created
clusterrolebinding.rbac.authorization.k8s.io/fluentd-es created
daemonset.extensions/fluentd-es created
## 然后查看一下pod是否已经在k8s-slave1
$ kubectl -n logging get po -o wide
NAME READY STATUS RESTARTS AGE
elasticsearch-logging-0 1/1 Running 0 123m
fluentd-es-246pl 1/1 Running 0 2m2s
kibana-944c57766-ftlcw 1/1 Running 0 50m
复制代码
日志收集功能验证
EFK 功能验证
验证思路
在 slave 节点中启动服务,同时往标准输出中打印测试日志,到 kibana 中查看是否可以收集
创建测试容器
efk/test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
nodeSelector:
fluentd: "true"
containers:
- name: count
image: alpine:3.6
args: [/bin/sh, -c,
'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']
复制代码
$ kubectl get po
NAME READY STATUS RESTARTS AGE
counter 1/1 Running 0 6s
复制代码
配置 kibana
登录 kibana 界面,按照截图的顺序操作:
也可以通过其他元数据来过滤日志数据,比如可以单击任何日志条目以查看其他元数据,如容器名称,Kubernetes 节点,命名空间等,比如 kubernetes.pod_name : counter
到这里,我们就在 Kubernetes 集群上成功部署了 EFK ,要了解如何使用 Kibana 进行日志数据分析,可以参考 Kibana 用户指南文档:https://www.elastic.co/guide/en/kibana/current/index.html
评论