说起日志,我相信每个从业人员都不陌生。从分析问题、观测软件运行状态等都离不开日志,程序日志记录了程序的运行轨迹,往往还包含了一些关键数据、错误信息等。

因此在使用 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 容器来收集应用日志

下面分别分析这三种方案的使用场景和优缺点。

应用程序自身将日志推送到日志系统

logging-from-application

这个方案是依靠应用程序自身的日志输出逻辑,通常是使用日志系统的SDK,将程序产生的日志直接推送到日志系统中。在实际应用中,这个方案使用的很少。因为应用中包含了日志推送的代码,耦合太强。如果后期想要更换日志系统,应用程序还得重新修改日志推送代码以适配新的日志系统。

在每个节点上运行一个 Agent 来采集节点级别的日志

logging-with-node-agent

这个方案被广泛使用,通常是使用 DaemonSet 控制器将日志采集工具(通常是 Flunetd、Filebeat 等)部署到每一个节点中,采集相应的日志。应用程序无需关心日志是如何被收集的,只需要将日志输出到标准输出(STDOUT、STDERR)即可。如果你的应用程序不支持输出日志到标准输出,则使用这个方案前,需要将你的应用改造成支持将日志打到标准输出。

在一个 Pod 内使用一个 Sidecar 容器来收集应用日志

logging-with-sidecar-agent

当你的应用程序输出日志到文件,那么可以采用这个方案。在 Pod 内运行一个日志采集工具,采集应用程序所输出的日志文件,将内容直接推送到日志系统中。

这个方案可能会在应用容器化初期使用的较多,初期应用可能还没有改造支持将日志打到标准输出中,因此只能采用这中形式。

因为在每个 Pod 中都运行一个日志采集工具,造成资源的浪费,因此这个方案是不太建议采用的。

这个方案还有一个变种,配合在每个节点上运行一个 Agent 来采集节点级别的日志使用,如下所示

logging-with-streaming-sidecar

在此方案中,需要将日志目录作为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 hubKubeapps 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+1N为 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,就可以看到一些直方图和最近采集到的日志数据了。

discover.png

日志分析

要想使用日志分析功能,应用程序输出的日志格式必须是 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:INFOLOGLEVEL:WARNINGLOGLEVEL:ERROR

这样效果就出来了,如下图:

Kibana饼图.png

使用柱状图展示日志级别的数量

  • 点击 Visualize -> Create visualization,选择柱状图(Vertical Bar)
  • 新建 Split slices 类型的 Buckets
  • Aggregation 选择 TermsField 选择 LOGLEVEL.keyworld
  • 新建 X-axis 类型的 BucketsAggregation 选择 Date HistogramField 选择 @timestamp

这样效果就出来了,如下图:

Kibana柱状图.png

使用柱状图展示服务的错误日志数量

  • 点击 Visualize -> Create visualization,选择柱状图(Vertical Bar)
  • 新建 Split slices 类型的 Buckets
  • Aggregation 选择 TermsField 选择 serviceName.keyword
  • 新建 X-axis 类型的 BucketsAggregation 选择 Date HistogramField 选择 @timestamp
  • 在搜索栏中输入 LOGLEVEL:ERROR

这样效果就出来了,如下图:

Kibana展示服务错误日志数量.png

使用面积图展示dummylogs消息生产与消费的直观图

  • 点击 Visualize -> Create visualization,选择面积图(Area)
  • MetricsY-axis Aggregation 选择 sumField 选择 eventNumber
  • 新建一个 Y-axis 类型的 MetricAggregation 选择 sumField 选择 volumn
  • 新建一个 X-axis 类型的 BucketsAggregation 选择 Date HistogramField 选择 @timestamp
  • 还可以根据喜好,选择图表的样式,在 Metrics & axes TAB 进行配置

这样效果就出来了,如下图:

Kibana面积图.png

完了之后,还可以将你感兴趣的图表添加到 Dashboard 中统一展示:

Kibana Dashboard.png

基于日志的告警

规则配置解析:

  • 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

标签: none

添加新评论