篇五:Kubernetes日志采集
说起日志,我相信每个从业人员都不陌生。从分析问题、观测软件运行状态等都离不开日志,程序日志记录了程序的运行轨迹,往往还包含了一些关键数据、错误信息等。
因此在使用 Kubernetes 的过程中,对应的日志收集也是我们不得不考虑的问题,我们需要日志去了解集群内部的运行状况。
Kubernetes 中的日志收集 VS 传统日志收集
传统应用中,往往是在虚拟机或物理机直接运行程序,程序日志输出到本机的文件系统的某个目录中,或者是由rsyslog、systemd-journald等工具托管。在此类环境中,日志目录是相对固定不变的,因此收集日志只需要访问日志目录即可。
而在 Kubernetes 中,日志收集相比传统虚拟机、物理机方式要复杂得多。
首先,Kubernetes 日志的形式非常多样化,一个完整的日志系统至少需要收集以下三种:
- Kubernetes 各组件的运行日志,如 kubelet、docker、kube-prpoxy 等
- 业务容器的运行日志,如 tomcat、nginx 等
- Kubernetes 的各种 Event,如 Pod 的创建、删除、错误等事件
其次,我们知道 Pod 是“用完即焚”的,当 Pod 的生命周期结束后,其日志也会被删除。但是这个时候我们仍然希望可以看到具体的日志,用于查看和分析业务的运行情况,以及帮助我们发现出容器异常的原因。
再次,Kubernetes 集群中的资源状态可能随时会发生变化,Pod 实例数量随时都可能会受 HPA(Horizontal Pod Autoscaler ) 的影响或管理员的操作而变化。我们并无法预知 Pod 会在哪个节点上运行,而且 Kubernetes 工作节点也无时无刻可能会宕机。
在一切都是动态的场景下,Kubernetes 日志系统在设计时就要考虑这些不确定因素
几种常见的 Kubernetes 日志收集架构
Kubernetes 集群本身其实并没有提供日志收集的解决方案,但依赖 Kubernetes 自身提供的各项能力,可以帮助我们解决日志收集的诉求。根据上面提到的三大基本日志需求,一般来说我们有如下有三种方案来做日志收集:
- 应用程序自身将日志推送到日志系统
- 在每个节点上运行一个 Agent 来采集节点级别的日志
- 在一个 Pod 内使用一个 Sidecar 容器来收集应用日志
下面分别分析这三种方案的使用场景和优缺点。
应用程序自身将日志推送到日志系统
这个方案是依靠应用程序自身的日志输出逻辑,通常是使用日志系统的SDK,将程序产生的日志直接推送到日志系统中。在实际应用中,这个方案使用的很少。因为应用中包含了日志推送的代码,耦合太强。如果后期想要更换日志系统,应用程序还得重新修改日志推送代码以适配新的日志系统。
在每个节点上运行一个 Agent 来采集节点级别的日志
这个方案被广泛使用,通常是使用 DaemonSet 控制器将日志采集工具(通常是 Flunetd、Filebeat 等)部署到每一个节点中,采集相应的日志。应用程序无需关心日志是如何被收集的,只需要将日志输出到标准输出(STDOUT、STDERR)即可。如果你的应用程序不支持输出日志到标准输出,则使用这个方案前,需要将你的应用改造成支持将日志打到标准输出。
在一个 Pod 内使用一个 Sidecar 容器来收集应用日志
当你的应用程序输出日志到文件,那么可以采用这个方案。在 Pod 内运行一个日志采集工具,采集应用程序所输出的日志文件,将内容直接推送到日志系统中。
这个方案可能会在应用容器化初期使用的较多,初期应用可能还没有改造支持将日志打到标准输出中,因此只能采用这中形式。
因为在每个 Pod 中都运行一个日志采集工具,造成资源的浪费,因此这个方案是不太建议采用的。
这个方案还有一个变种,配合在每个节点上运行一个 Agent 来采集节点级别的日志
使用,如下所示
在此方案中,需要将日志目录作为emptyDir
共享出来,使得 Sidecar 容器能访问到,Sidecar 容器的主要功能是将应用程序日志输出到标准输出,最简单的使用tail -f 日志文件
即可实现将程序日志实时打到标准输出。如此实现,Sidecar 虽然看起来非常的轻量,但日志会存两份,消耗双倍的磁盘空间与磁盘IO,一份日志即被主容器打到容器内,又被 Sidecar 容器将日志输出到到节点的 Docker 日志中。
基于 Fluentd + ElasticSearch 的日志收集方案
Kubernetes 社区官方推荐的方案是使用 Fluentd+ElasticSearch+Kibana 进行日志的收集和管理,通过Fluentd将日志导入到Elasticsearch中,用户可以通过Kibana来查看到所有的日志。
官方 github 仓库的 addon 中有提供相关的部署清单,如果你想快速搭建一套环境用于测试,那么可以使用官方提供的清单来部署。地址:https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/。
在生产环境应用中,官方更加偏向于使用 Helm charts 的方式来部署,读者可以查阅 Helm 相关的使用手册,以及通过 Artifact hub、Kubeapps Hub 等 Helm chars 站点查找合适的 charts。
而我更喜欢自行编写部署清单,一可以了解其内部细节,二灵活易变。
本文中,所使用的镜像如下:
- Elasticsearch 镜像:docker.elastic.co/elasticsearch/elasticsearch:7.6.2
- Kibana 镜像:docker.elastic.co/kibana/kibana:7.6.2
- Fluentd 镜像:quay.io/fluentd_elasticsearch/fluentd:v3.0.1
- elastalert 镜像:anjia0532/elastalert-docker:v0.2.4
- nfs client provisioner 镜像:quay.io/external_storage/nfs-client-provisioner:latest
特别说明:由于 Elasticsearch 使用 StatefulSet 部署,数据持久化需要使用到 StorageClass,本文使用 nfs 作为持久化存储后端,且设定了 StorageClass 的名称为 managed-nfs-storage。如果你的 K8S 集群还没有可用的 StorageClass,那么可以参考我提供的这份 nfs.yaml 来创建一个。
nfs.yaml 需要修改以下内容
- value: 192.168.72.2:改为实际 nfs-server 的地址
- value: /volume1/k8s-lab:改为实际 nfs-server 的共享目录
- server: 192.168.72.2:改为实际 nfs-server 的地址
- path: /volume1/k8s-lab:改为实际 nfs-server 的共享目录
部署 EFK Stack
我将 EFK 部署在名为 logging 的 namespace 中,符合最佳实践原则之一。
部署 Elasticsearch
接下来部署 Elasticsearch,为避免 Elasticsearch 多节点集群中出现的“脑裂”问题,我们通常会部署单数个节点(实例),并设置 discover.zen.minimum_master_nodes=N/2+1
,N
为 Elasticsearch 集群中符合主节点的节点数。
这里直接上部署清单,需要根据实际情况修改部分内容
logging-namespace.yaml
elasticsearch-svc.yaml
elasticsearch-statefulset.yaml
elasticsearch-statefulset.yaml
- cluster.name:Elasticsearch 集群的名称,我们这里命名成 k8s-logs。
- node.name:节点的名称,通过 metadata.name 来获取。这将解析为 es-[0,1,2],取决于节点的指定顺序。
- discovery.seed_hosts:此字段用于设置在 Elasticsearch 集群中节点相互连接的发现方法。由于我们之前配置的无头服务,我们的 Pod 具有唯一的 DNS 域es-[0,1,2].elasticsearch.logging.svc.cluster.local,因此我们相应地设置此变量。要了解有关 Elasticsearch 发现的更多信息,请参阅 Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html。
- discovery.zen.minimum_master_nodes:我们将其设置为(N/2) + 1,N是我们的群集中符合主节点的节点的数量。我们有3个 Elasticsearch 节点,因此我们将此值设置为2(向下舍入到最接近的整数)。
- ES_JAVA_OPTS:这里我们设置为-Xms512m -Xmx512m,告诉JVM使用512 MB的最小和最大堆。您应该根据群集的资源可用性和需求调整这些参数。要了解更多信息,请参阅设置堆大小的相关文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html。
- storageClassName:需要设置为 StorageClass 的名称
创建资源:
kubectl apply -f logging-namespace.yaml
kubectl apply -f elasticsearch-svc.yaml
kubectl apply -f elasticsearch-statefulset.yaml
等待 Pod 创建完成,验证一下 Elasticsearch 的集群状态:
kubectl -n logging get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
es-0 1/1 Running 0 34m 10.244.2.9 k8s-node02 <none> <none>
es-1 1/1 Running 0 25m 10.244.1.5 k8s-node01 <none> <none>
es-2 1/1 Running 0 22m 10.244.3.8 k8s-node03 <none> <none>
访问其中一个节点的IP,需要在集群内访问:
curl http://10.244.2.9/
当有类似输出时,表示 ES 状态正常:
{
"name" : "es-0",
"cluster_name" : "k8s-logs",
"cluster_uuid" : "KfKaXAJ9ROqsH_gar3kl5Q",
"version" : {
"number" : "7.6.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
"build_date" : "2020-03-26T06:34:37.794943Z",
"build_snapshot" : false,
"lucene_version" : "8.4.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
还可以查看 ES 集群状态:
curl http://10.244.2.9:9200/_cluster/state?pretty | less
输出:
{
"cluster_name" : "k8s-logs",
"cluster_uuid" : "KfKaXAJ9ROqsH_gar3kl5Q",
"version" : 21,
"state_uuid" : "t-E9b-OjR9qmDRM2ybWN7w",
"master_node" : "KVxEJaYpR7qe4V-KNkgbkA",
"blocks" : { },
"nodes" : {
"4bGtXT7rTA6MSHiIzMAijw" : {
"name" : "es-0",
"ephemeral_id" : "RIctHOozS5CZTx6mylK71g",
"transport_address" : "10.244.2.9:9300",
"attributes" : {
"ml.machine_memory" : "8350658560",
"ml.max_open_jobs" : "20",
"xpack.installed" : "true"
}
},
"2-K-4t5rQ221BxOGxgyi9g" : {
"name" : "es-2",
"ephemeral_id" : "AnZlOQPTQyaAG0nBzHBdHg",
"transport_address" : "10.244.3.8:9300",
"attributes" : {
"ml.machine_memory" : "8350658560",
"ml.max_open_jobs" : "20",
"xpack.installed" : "true"
}
},
"KVxEJaYpR7qe4V-KNkgbkA" : {
"name" : "es-1",
"ephemeral_id" : "4VnCKfJESGes3p9cZr1NBw",
"transport_address" : "10.244.1.5:9300",
"attributes" : {
"ml.machine_memory" : "8350650368",
"ml.max_open_jobs" : "20",
"xpack.installed" : "true"
}
}
},
// 省略部分输出
}
到上面的信息就表明我们名为 k8s-logs 的 Elasticsearch 集群成功创建了3个节点:es-0,es-1,和es-2,当前主节点是 es-2。
部署 Fluentd
Elasticsearch 部署完后,我们就可以部署 Fluentd 采集日志了。
还是直接上部署清单:
fluentd-rbac.yaml
fluentd-configmap.yaml
fluentd-daemonset.yaml
kubectl apply -f fluentd-rbac.yaml
kubectl apply -f fluentd-configmap.yaml
kubectl apply -f fluentd-daemonset.yaml
部署 Kibana
还是直接上部署清单:
kibana-svc.yaml
kibana-deploy.yaml
需要注意的是,Kibana 和 Elasticsearch 的版本要完全一致,因此修改想要更新版本,要同时更新他们俩的版本。
kubectl apply -f kibana-svc.yaml
kubectl apply -f kibana-deploy.yaml
由于使用 Kibana 使用 NodePort 暴露端口,且没有固定 NodePort 的端口号。因此我们查询系统分配给其的对外端口:
kubectl -n logging get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
elasticsearch ClusterIP None <none> 9200/TCP,9300/TCP 86m
kibana NodePort 10.106.138.219 <none> 5601:30778/TCP 21m
通过查看,可以看到系统为 Kibana 分配的 NodePort 为 30778,通过浏览器访问集群任意节点IP:30778 即可访问 Kibana。
点击左侧最下面的 management 图标,然后点击 Kibana 下面的 Index Patterns 开始导入索引数据。在 Index pattern
处填入 k8s-*
,点击 Next step
,在 Time Filter field name
处选择 @timestamp
,即可完成索引建立。
点击左侧导航菜单中的 Discover
,就可以看到一些直方图和最近采集到的日志数据了。
日志分析
要想使用日志分析功能,应用程序输出的日志格式必须是 JSON 格式的。
为了模拟程序产生日志,我这里运行几个 Pod:
kubectl apply -f << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: dummylogs-processor
spec:
replicas: 3
selector:
matchLabels:
app: dummylogs-processor
template:
metadata:
labels:
app: dummylogs-processor
spec:
containers:
- name: dummy
image: cnych/dummylogs:latest
args:
- msg-processor
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dummylogs-receiver
spec:
replicas: 3
selector:
matchLabels:
app: dummylogs-receiver
template:
metadata:
labels:
app: dummylogs-receiver
spec:
containers:
- name: dummy
image: cnych/dummylogs:latest
args:
- msg-receiver-api
EOF
dummylogs 输出的日志是 JSON 格式,如下:
{"LOGLEVEL":"INFO","serviceName":"msg-processor","serviceEnvironment":"staging","message":"Information event from service msg-processor staging - events received and processed.","eventsNumber":0}
{"LOGLEVEL":"INFO","serviceName":"msg-processor","serviceEnvironment":"staging","message":"Special important event from msg-processor staging received, processing completed.","special_value":28}
{"LOGLEVEL":"WARNING","serviceName":"msg-processor","serviceEnvironment":"staging","message":"WARNING client connection terminated unexpectedly."}
Kibana 支持十几种可视化图表,我们可以创建一些图表以方便直观的表示出我们想要看的内容。下面以一些示例说明一下图表的使用。
使用饼图展示 ERROR、WARNING、INFO 这三种日志级别的比例
- 点击
Visualize
->Create visualization
,选择饼图 - 新建
Split slices
类型的Buckets
Aggregation
选择Filters
- 新建三个
Filters
,分别是LOGLEVEL:INFO
,LOGLEVEL:WARNING
,LOGLEVEL:ERROR
这样效果就出来了,如下图:
使用柱状图展示日志级别的数量
- 点击
Visualize
->Create visualization
,选择柱状图(Vertical Bar) - 新建
Split slices
类型的Buckets
Aggregation
选择Terms
,Field
选择LOGLEVEL.keyworld
- 新建
X-axis
类型的Buckets
,Aggregation
选择Date Histogram
,Field
选择@timestamp
这样效果就出来了,如下图:
使用柱状图展示服务的错误日志数量
- 点击
Visualize
->Create visualization
,选择柱状图(Vertical Bar) - 新建
Split slices
类型的Buckets
Aggregation
选择Terms
,Field
选择serviceName.keyword
- 新建
X-axis
类型的Buckets
,Aggregation
选择Date Histogram
,Field
选择@timestamp
- 在搜索栏中输入
LOGLEVEL:ERROR
这样效果就出来了,如下图:
使用面积图展示dummylogs
消息生产与消费的直观图
- 点击
Visualize
->Create visualization
,选择面积图(Area) - 在
Metrics
下Y-axis
Aggregation
选择sum
,Field
选择eventNumber
- 新建一个
Y-axis
类型的Metric
,Aggregation
选择sum
,Field
选择volumn
- 新建一个
X-axis
类型的Buckets
,Aggregation
选择Date Histogram
,Field
选择@timestamp
- 还可以根据喜好,选择图表的样式,在
Metrics & axes
TAB 进行配置
这样效果就出来了,如下图:
完了之后,还可以将你感兴趣的图表添加到 Dashboard 中统一展示:
基于日志的告警
规则配置解析:
- es_host、es_port:应该指向我们要查询的Elasticsearch集群
- name:是这个规则的唯一名称。如果两个规则共享相同的名称,ElastAlert将不会启动
- type:每个规则都有不同的类型,可能会采用不同的参数。该frequency类型表示“在timeframe时间内匹配成功次数超过-
- num_events发出警报”。有关其他类型的信息,请参阅规则类型
- index:要查询的索引的名称。配置,从某类索引里读取数据,目前已经支持Ymd格式,需要先设置use_strftime_index:true,然后匹配索引,配置形如:index: logstash-es-test%Y.%m.%d,表示匹配logstash-es-test名称开头,以年月日作为索引后缀的index
- num_events:此参数特定于frequency类型,是触发警报时的阈值
- timeframe:timeframe是num_events必须发生的时间段
- filter:是用于过滤结果的Elasticsearch过滤器列表。有关详细信息,请参阅编写过滤规则
- email:是要发送警报的地址列表
alert:配置,设置触发报警时执行哪些报警手段。不同的type还有自己独特的配置选项。目前ElastAlert 有以下几种自带ruletype:
- any:只要有匹配就报警;
- blacklist:compare_key字段的内容匹配上 blacklist数组里任意内容;
- whitelist:compare_key字段的内容一个都没能匹配上whitelist数组里内容;
- change:在相同query_key条件下,compare_key字段的内容,在 timeframe范围内 发送变化;
- frequency:在相同 query_key条件下,timeframe 范围内有num_events个被过滤出 来的异常;
- spike:在相同query_key条件下,前后两个timeframe范围内数据量相差比例超过spike_height。其中可以通过spike_type设置具体涨跌方向是- up,down,both 。还可以通过threshold_ref设置要求上一个周期数据量的下限,threshold_cur设置要求当前周期数据量的下限,如果数据量不到下限,也不触发;
- flatline:timeframe 范围内,数据量小于threshold 阈值;
- new_term:fields字段新出现之前terms_window_size(默认30天)范围内最多的terms_size (默认50)个结果以外的数据;
- cardinality:在相同 query_key条件下,timeframe范围内cardinality_field的值超过 max_cardinality 或者低于min_cardinality