# coding: utf8

# 使用说明:
# 1. 默认参数启动
#    python loggenerator.py
# 2. 自定义参数启动
#    KEY1=VALUE1 KEY2=VALUE2 python loggenerator.py
#    支持参数:
#       TYPE: 日志类型,默认json,可选txt
#       MAX: 日志总量,默认10000000
#       SPEED: 每秒生成日志量,默认15000
#       OUTPUT: 日志文件,默认logs/sum.log
#       MAXSIZE: 日志文件大小,超过此大小将会进行轮转
import os
import gzip
import shutil
from random import random
import time
import json


def logCompressor(filepath):
    offset = 0
    maxsize = os.getenv('MAXSIZE', 50 * 1024 * 1024)
    r = ''
    while True:
        n = yield r
        if not n:
            return
        size = os.path.getsize(filepath)
        if size >= maxsize:
            tmpfile = "%s%d_tmp" % (filepath, int(random() * 1e17))
            shutil.move(filepath, tmpfile)
            gzip.GzipFile(filename="", mode='wb', compresslevel=9, fileobj=open(
                "%s-%s.%d.log.gz" % (filepath.split('.')[0], time.strftime("%Y.%m.%d", time.localtime()), offset),
                'wb')).write(open(tmpfile, 'rb').read())
            os.remove(tmpfile)
            offset += 1
            r = '200'
        else:
            r = '0'


def logGenerator(c, maxline, speed, filepath, logtype):
    if not os.path.exists(os.path.dirname(filepath)):
        os.mkdir(os.path.dirname(filepath))
    fb = open(filepath, 'a+')
    c.send(None)
    n = 0
    while n < maxline:
        start = time.time()
        s = 0 # 控制速率
        while s < speed:
            if logtype == 'json':
                m = {
                    "level": "INFO",
                    "date": time.strftime("%Y.%m.%d %H:%M:%S", time.localtime()),
                    "message": "time:%s, nothing to do!" % time.time(),
                    "business": "logGenerator:19",
                    "service": "loggenerator",
                    "hostname": "fluentd1"
                }
                m = json.dumps(m)
            else:
                m = '%s [INFO] [logGenerator:19] - time:%s, nothing to do!' % (time.strftime("%Y.%m.%d %H:%M:%S", time.localtime()), time.time())
            fb.write(m + "\n")
            n += 1
            s += 1
            r = c.send(n)
            if r == '200':
                fb.close()
                fb = open(filepath, 'w+')
        end = time.time()
        if end - start < 1:
            # 写入耗时小于1秒,控制写入速度
            time.sleep(1 - (end - start))
    c.close()


if __name__ == "__main__":
    maxline = os.getenv('MAX', 10000000)
    speed = os.getenv('SPEED', 15000)
    logfile = os.getenv('OUTPUT', 'logs/sum.log')
    logtype = os.getenv('TYPE', 'json')
    c = logCompressor(logfile)
    logGenerator(c, maxline, speed, logfile, logtype)

nf_conntrack 调优

netfilter

netfilter是linux内在的一个软件框架,用来管理网络数据包。

netfilter提供了5个hook来进行管理网络包。如下图:

netfilter-hooks

  • PREROUTING, 所有包都会经过这个hook
  • LOCAL INPUT, 进入本机的包会经过这个hook
  • FORWARD, 不进入本机的包,做转发的包会经过这个hook
  • LOCAL OUTPUT, 从本机出去的包会经过这个hook
  • POSTROUTING, 所有出去的包都会经过这个hook

netfilter进行包的管理,则需要记录每个连接的状态信息。这就是nf_conntrack的工作了。

nf_conntrack

nf_conntrack是netfilter的一个子系统。它记录了每个连接的状态信息。

nf_conntrack记录的信息包括,源ip、端口,目标ip、端口,连接状态,协议等。

连接状态包含以下几种:

  • NEW, 新创建的连接,发起连接方发出包后,还没收到回包,都处理这种状态。
  • ESTABLISHED, 已建立的连接,发起连接后,收到回包,这时处理已连接状态。
  • RELATED, 与其他连接相关联,其他的连接与此连接有关联。如ftp的控制连接和数据连接。
  • INVALID, 非法的连接,比如包的行为不合法。

nf_conntrack需要保存这些信息在它自己的数据结构中。其数据结构如下:

connection-tracking-structure

它是一个开链的哈希表,链表是一个双向表。每个哈希节点称为一个bucket,计算出同样哈希值的连接放到链表里连起来。 每个节点记录了请求方向、响应方向的消息。

哈希表的大小,也就是说哈希表的节点数,由nf_conntrack_buckets配置。

nf_conntrack能跟踪的最大连接数由nf_conntrack_max配置。

nf_conntrack默认值

  • CentOS6.10 x86_64

    • 系统内存小于1GiB时

      • nf_conntrack_buckets=RAMSIZE (in bytes) / 16384 / (ARCH / 32)
      • nf_conntrack_max=nf_conntrack_buckets * 4
    • 系统内存大于1GiB时

      • nf_conntrack_buckets=16384
      • nf_conntrack_max=65536
  • CentOS7.9 x86_64

    • 系统内存小于1GiB时

      • nf_conntrack_buckets=RAMSIZE (in bytes) / 16384 / (ARCH / 32)
      • nf_conntrack_max=nf_conntrack_buckets * 4
    • 系统内存大于1GiB且小于4GiB

      • nf_conntrack_buckets=16384
      • nf_conntrack_max=65536
    • 系统内存大于4GiB

      • nf_conntrack_buckets=65536
      • nf_conntrack_max=262144

备注:

  • ARCH是指操作系统位数,取值32或64
  • 这里的系统内存不是指物理机或虚拟机所分配的内存,而是系统能识别的内存总量,也就是通过free命令查看到的总内存。

nf_conntrack表满的表现

默认值在大并发场景时可能会引起nf_conntrack被占满而导致服务不可用。当nf_conntrack满的时候,新连接过来就会直接被netfilter丢掉,导致连接不上。 在监控上看,可以看到脉冲式的请求。另外我们可以用dmesg命令或查看/var/log/messages看到系统的日志显示nf_conntrack表满的提示。

监控图一般会有以下的表现:

qps

dmesg可以看到以下的输出:

dmesg

什么情况下要调优

当出现以下一种或几种情形,应考虑调整nf_conntrack的相关参数

  • 系统启用了iptables或firewalld等使用了netfilter模块的软件,并且系统需要承载大量并发请求
  • nf_conntrack_count / nf_conntrack_buckets > 0.7,这时哈希表大部分桶不为空,哈希冲突的概率会增大,性能从 O(1) 退化为读链表的 O(n),建议及时扩容
  • 明确发现nf_conntrack满时,应及时扩容

调优方向:

  • 禁用netfilter模块
  • 哈希表扩容(nf_conntrack_buckets、nf_conntrack_max)
  • 让里面的元素尽快释放(超时相关参数)
  • 不追踪连接(iptables规则设置NOTRACK选项)

nf_conntrack设置

每个bucket的平均链表长度为bucket_len = nf_conntrack_max / nf_conntrack_buckets。系统默认是4。 每个数据包的链表查询时间复杂度为bucket_len。当bucket_len太大时,则每个数据包要花太多时间在链接的查找上。 所以,我们不能把bucket_len配得太长。按照linux的默认配置,nf_conntrack_buckets 与 nf_conntrack_max 的值比为 1:4 即可。

过大的nf_conntrack会消耗一定量的内存,其计算公式在下面一节中给出。

对于内存还算充裕的服务器而言,通常建议设置为:

nf_conntrack_buckets = 262144

nf_conntrack_max = 1048576

nf_conntrack_buckets

立即生效:

echo 262144 > /sys/module/nf_conntrack/parameters/hashsize
该方法已在CentOS6/CentOS7上测试通过,重启失效

永久生效:

方法1:

cat > /etc/sysconfig/modules/nf_conntrack_hashsize.modules << "EOF"
#!/bin/sh
exec /sbin/modprobe nf_conntrack hashsize=262144
EOF

chmod +x /etc/sysconfig/modules/nf_conntrack_hashsize.modules
该方法已在CentOS6/CentOS7上测试通过,重启生效

方法2:

cat > /etc/modprobe.d/nf_conntrack_hashsize.conf << "EOF"
options nf_conntrack hashsize = 262144
EOF
该方法在CentOS6/CentOS7上测试不通过,nf_conntrack模块无法加载

nf_conntrack_max

立即生效:

sysctl -w net.netfilter.nf_conntrack_max=1048576

永久生效:

net.netfilter.nf_conntrack_max=1048576写入/etc/sysctl.conf 或/etc/sysctl.d/xxx.conf

nf_conntrack内存占用计算

计算公式:

total_mem_used(bytes) = conntrack_max * sizeof(struct ip_conntrack) + conntrack_buckets * sizeof(struct list_head)

其中,sizeof(struct ip_conntrack),sizeof(struct list_head)的值在不同的发行版可能会不一样,可以使用以下python命令获得。

import ctypes
LIBNETFILTER_CONNTRACK = '/usr/lib64/libnetfilter_conntrack.so.3.6.0' #这个是nf_conntrack的动态库所在路径
nfct = ctypes.CDLL(LIBNETFILTER_CONNTRACK)
print 'sizeof(struct nf_conntrack):', nfct.nfct_maxsize()
print 'sizeof(struct list_head):', ctypes.sizeof(ctypes.c_void_p) * 2

以CentOS6和CentOS7为例,计算当设置了nf_conntrack_buckets = 262144nf_conntrack_max = 1048576时最多会占用多少内存。

执行上面的python命令得到:

  • CentOS6 x86_64

    • sizeof(struct nf_conntrack): 296
    • sizeof(struct list_head): 16
  • CentOS7 x86_64

    • sizeof(struct nf_conntrack): 376
    • sizeof(struct list_head): 16

计算结果:

  • CentOS6 x86_64

    • 1048576 * 296 + 262144 * 16 = 314572800 (bytes) 即 300MiB
  • CentOS7 x86_64

    • 1048576 * 376 + 262144 * 16 = 314572800 (bytes) 即 380MiB

在使用 Kubernetes 中,经常会遇到一个奇怪的现象,当 Pod 与 CoreDNS 在同一个 Node 节点时,此 Pod 无法解析 DNS 记录。

我清楚的知道 Service 是使用 iptables NAT DNAT + SNAT 规则实现的,因此我排查的方向是抓包查看数据包的流向。

经过抓包分析,发现同一个 Node 节点访问 Service 只是对访问的流量进行了 DNAT,没有做 SNAT。但当时并没有找到解决方法,今天又遇到了一样的问题,通过爬文找到了这篇文章,所描述的问题现象一模一样,也附带了解决方法,并有详细的排查过程,读者可以去拜读一下。

我的 Kubernetes 集群是使用 kubeadm 部署的,因此解决方法是修改 kube-proxy 的 ConfigMap

[root@k8s-master01 ~]# kubectl -n kube-system get cm
NAME                                 DATA   AGE
coredns                              1      20d
extension-apiserver-authentication   6      20d
kube-flannel-cfg                     2      20d
kube-proxy                           2      20d
kubeadm-config                       2      20d
kubelet-config-1.18                  1      20d
metrics-server-config                1      19d
kubectl -n kube-system edit cm kube-proxy

# 修改 masqueradeAll: true

重启所有节点的 kube-proxy:

[root@k8s-master01 monitoring]# kubectl -n kube-system get pods
NAME                                      READY   STATUS    RESTARTS   AGE
coredns-66bff467f8-5v2j7                  1/1     Running   1          20d
coredns-66bff467f8-gtrdb                  1/1     Running   0          26m
etcd-k8s-master01                         1/1     Running   1          20d
kube-apiserver-k8s-master01               1/1     Running   1          20d
kube-controller-manager-k8s-master01      1/1     Running   2          20d
kube-flannel-ds-94n7x                     1/1     Running   1          20d
kube-flannel-ds-nf26k                     1/1     Running   1          20d
kube-flannel-ds-nl2dl                     1/1     Running   1          20d
kube-flannel-ds-thl9f                     1/1     Running   2          20d
kube-proxy-gpxhb                          1/1     Running   1          20d
kube-proxy-k2gjh                          1/1     Running   1          20d
kube-proxy-lp5jw                          1/1     Running   1          20d
kube-proxy-z67qm                          1/1     Running   1          20d
kube-scheduler-k8s-master01               1/1     Running   3          20d
metrics-server-v0.3.6-5d9877d6b6-bqq8d    2/2     Running   2          19d
nfs-client-provisioner-5fbf5f4476-fpmdq   1/1     Running   2          10d
# 删掉Pod,让其重建
kubectl -n kube-system delete pod kube-proxy-gpxhb kube-proxy-k2gjh kube-proxy-lp5jw kube-proxy-z67qm

至此,问题解决。

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

因此在使用 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

说起Kubernetes监控,绕不开的话题就是Prometheus。随着Prometheus的发展,已然成为Kubernetes监控体系中的标准。

本文主要介绍通过yaml部署清单来安装Prometheus。

部署Prometheus

包含组件:

  • prometheus
  • kube-state-metrics
  • node-exporter
  • pushgateway
  • alertmanager
  • grafana

prometheus组件.png

- 阅读剩余部分 -