从公有云供应商那里设置或使用 Kubernetes 集群,这一切都很好。但如果没有正确的策略来监控该集群的指标和日志,并在出现问题时发出适当的警报,那么你创建的集群就是一场等待发生的灾难。虽然 Kubernetes 让开发人员可以轻松地构建和部署他们的应用程序,但它也创建了依赖于 Kubernetes 成功运行的应用程序。这意味着,当一个集群发生故障时,你的用户的应用往往也会失败。而且,如果一个集群失败的频率太高,用户就会失去对系统的信任,并开始质疑集群及其操作人员的价值。本章讨论了为您的 Kubernetes 集群开发和部署监控和警报的方法,以防止这种情况发生。此外,我们还介绍了如何在集群上添加监控,以便应用开发人员可以自动为自己的应用利用它。
监控的目的
在我们进入如何监控集群的细节之前,重要的是要先了解一下这个监控的目标。所有关于如何部署和管理监控的具体细节都是为这些目标服务的,因此,对为什么的清晰认识将有助于理解什么。
显然,监控的首要目标是可靠性。在这种情况下,可靠性既是 Kubernetes 集群的可靠性,也是运行在集群之上的应用的可靠性。作为这种关系的一个例子,考虑一个二进制,比如控制器管理器。如果它停止正常运行,服务发现将开始慢慢过时。现有的 Service 将已经正确地传播到集群中的 DNS 服务器上,但是新的 Service,或者由于推出或扩展操作而改变的 Service,其 DNS 将不会被更新。
:::info 这次失败表明了理解 Kubernetes 底层架构的重要性。如果你不能清晰地解释控制器管理器在整个 Kubernetes 集群中的作用,这可能是退一步回顾第 3 章的好时机,该章涉及 Kubernetes 组件和架构。 :::
这种故障其实不会体现在 Kubernetes 集群本身的正确运行上。在集群初始化之后,其所有的 Service 发现基本上都是静态的。但是,它反映在 Kubernetes 集群上面运行的应用的正确性上,因为它们的 Service 发现和故障转移本身就会开始失效。
这个例子展示了关于 Kubernetes 监控的两个重要观点。第一个是,在很多情况下,集群本身看起来运行正常,但它实际是失败的。这说明不仅要监控集群件,还要监控用户所需的集群功能。在这种情况下,最好的监控类型是一个黑匣子监控器,它可以持续部署一个新的 Pod 和 Service,并验证 Service 发现是否按照预期工作。
:::info 在本章中,我们提到了两种不同类型的监控。白盒监控关注应用程序产生的信号,并利用这些信号来发现问题。黑盒或探针监控使用公共接口(如 Kubernetes)采取已知预期结果的行动(如 “创建一个大小为 3 的 ReplicaSet 会生成三个 Pod”),如果预期结果没有发生,则发送警报。 :::
这个 DNS 例子的另一个重要教训是,建立适当的警报是多么重要。如果你只有在有用户抱怨他们的应用出现故障时才注意到你的 Kubernetes 集群出现了问题,那么你就存在监控漏洞。虽然实时站点事件是运行 Service 不可避免的一部分,但在一个监控良好的系统中,客户报告的事件应该是不存在的。
除了可靠性,监控系统的另一个重要功能是为您的 Kubernetes 集群提供可观察性。观察你的集群很重要,有很多原因。
能够发出监控警报,表明你的集群有问题是一回事。另一回事是能够确定到底是什么问题导致了警报,另一回事是能够在问题成为终端用户面临的问题之前看到并纠正它们。观察、可视化和查询你的监控数据的能力,是确定正在发生的问题和在问题成为事件之前发现问题的关键工具。
除了为可靠性服务的洞察力,集群监控数据的另一个重要用例是为用户提供对集群运行的洞察力。例如,用户可能很想知道,平均而言,拉取并开始运行他们的图像需要多长时间。用户可能想知道,在实际操作中,创建一个 Kubernetes DNS 记录的速度有多快,或者一个来自财务部门的人可能想跟踪用户是否真的使用了他们所请求的所有计算资源。所有这些信息都可以从集群监控系统中获得。
日志和监控的区别
在我们深入研究监控 Kubernetes 的细节之前,有一个重要的话题是日志和监控之间的区别。虽然密切相关,但它们实际上是完全不同的,用于解决不同的问题,并且通常存储在不同的基础设施中。
:::tips 日志记录的是事件(例如,一个 Pod 被创建或 API 调用失败),而监控记录的是统计数据(例如,特定请求的延迟、进程使用的 CPU 或对特定端点的请求数量)。日志记录的性质是离散的,而监控数据则是一些连续值的采样。 :::
日志系统一般用于搜索相关信息。(“为什么创建那个 Pod 失败了?” “为什么那个 Service 没有正常工作?”)由于这个原因,日志存储系统是围绕着存储和查询大量的数据,而监控系统一般是围绕着可视化。(“给我看看过去一个小时的 CPU 使用情况。”)因此,它们被存储在能够有效存储时间序列数据的系统中。
值得注意的是,无论是日志记录还是监控,都不足以了解你的集群。监控数据可以让你很好地了解集群的整体健康状况,并可以帮助你识别可能发生的异常事件。另一方面,日志记录对于深入了解究竟发生了什么,可能是在许多机器上发生的,从而导致这种异常行为至关重要。
构建一个监控栈
现在你已经对为什么以及你可能需要监控你的 Kubernetes 集群有了一些了解,让我们来看看你如何实现它。
从你的集群和应用中获取数据
监控从向监控系统暴露数据开始。其中一些数据是从内核中获取的,关于构成集群中容器的 cgroups 和命名空间的通用数据,但是大部分对监控有用的信息是由开发者添加到应用程序中的。有许多不同的方法可以将指标集成到你的应用程序中,但其中最受欢迎的 — 也是 Kuberentes 暴露指标的选择 — 是 Prometheus 监控接口。
Kubernetes 中的每一台服务器都会通过 HTTP(S) 端点暴露监控数据,该端点使用 Prometheus 协议来服务监控数据。如果你有一台 Kubernetes kubelet 服务器已经启动并运行,你可以通过标准的 HTTP 客户端(如 curl)访问这些数据,网址如下:http://localhost:9093。
要将新的应用指标集成到你的代码中,你需要链接到相关的 Prometheus 库。这不仅为您的应用程序添加了正确的 HTTP 服务器,而且还暴露了从该服务器中刮取的特定指标。Prometheus 有 Go、Java、Python 和 Ruby 的官方库,此外还有许多其他语言的非官方库。
下面是一个如何对 Go 应用进行仪表化的例子。首先,你将 Prometheus 服务器添加到你的代码中:
import "github.com/prometheus/client_golang/prometheus/promhttp"...func main() {...http.Handle("/metrics", promhttp.Handler())...}
一旦你有服务器运行,你需要定义一个度量并观察值:
"github.com/prometheus/client_golang/prometheus"
...
histogram := prometheus.NewHistogram(...)
...
histogram.Observe(someLatency)
...
除了 Kubernetes 提供的监控信息外,每个 Kubernetes 二进制文件都会将大量信息记录到 stdout 文件流中。通常情况下,这些输出会被捕获并重定向到一个日志旋转文件,如 /var/lib/kubernetes-apiserver.log。如果你 SSH 进入运行 Kubernetes API Server 的主节点,你可以在 /var/lib/kube-apiserver.log 中找到 API Server 的日志文件,你可以使用 tail -f ... 命令观看运行中的日志行。Kubernetes 组件使用 github.com/google/glog 库将不同严重程度的数据记录到文件中。在查看文件时,你可以通过查看被记录的第一个字母来检测这些严重性级别。例如,一个错误级别的日志看起来是这样的:
E0610 03:39:40.323732 1753 reflector.go:205] ...
你可以看到错误日志级别的 E,日志的时间,以及记录的文件。
Kubernetes 组件也会以不同的信息级别(Verbosity)来记录数据。大多数 Kubernetes 的安装都将信息级别设置为 2,这也是默认值。这在信息级别和垃圾邮件之间产生了良好的平衡。如果你需要增加或减少日志的信息级别,你可以使用 --v 标志,并将其设置在 0 和 10 之间,其中 10 表示最大信息级别,这可能是相当垃圾的。你也可以使用 -vmodule 标志来设置一个或一组特定文件的信息级别。
:::info 将日志的可读性设置为更高的水平,可以提高可视性,但这是有代价的 — 无论是在经济上还是在性能上。因为日志的绝对数量较多,会增加存储和保留成本,并会使你的查询速度变慢。当增加日志级别时,一般只在短时间内进行,并确保尽快将日志恢复到标准级别。 :::
从多数据源聚合指标和日志数据
一旦你的组件产生了数据,你就需要一个地方把数据归纳在一起,或者把数据聚合在一起,然后在聚合之后,把数据存储起来,以便查询和探究。本节讲的是日志和监控数据的聚合,后面的章节讲的是存储方面的选择。
:::info 说到聚合,有两种不同的风格。在拉式聚合中,聚合系统主动联系每个被监控系统,并将监控信息拉到聚合存储中。另一种方式是基于推式的监控系统,即被监控的系统负责将其指标发送给聚合系统。这两种不同的监控设计各有优缺点,根据系统的细节不同,实施起来也是孰易孰难。当你看了 Prometheus 和 Fluentd 这两个我们详细研究的日志和监控聚合系统,你可以看到这两个系统有不同的设计。了解它们各自的情况,以及它们为什么选择它们的设计,有助于你理解它们的权衡。 :::
Prometheus 是一个基于拉的监控指标聚合器。正如我们所看到的,当你暴露一个 Prometheus 指标时,它是以网页的形式暴露出来的,可以被抓取并聚合到 Prometheus 服务器中。Prometheus 选择这样的设计是因为它使得增加更多的系统来监控变得相当琐碎。只要系统实现了预期的接口,就像在 Prometheus 配置中添加一个额外的 URL 一样简单,该服务器的数据就会开始被监控。因为 Prometheus 负责按照自己的节奏来搜刮数据和汇总数据,所以 Prometheus 系统不需要担心突发或滞后的客户端以不同的时间间隔向它发送数据。相反,Prometheus 总是能准确地知道它请求监控数据的时间,而且它还能控制数据采样的速度,确保采样率一致,并均匀地分布在所有被监控的系统中。
相比之下,fluentd 守护进程是一个基于推送的日志聚合器。正如 Prometheus 出于各种实用的原因选择了拉式模式一样,Fluentd 选择推式模式也是基于一些实际的设计考虑。Fluentd 选择基于推送的模式的主要原因是,几乎所有的系统都会将日志记录到本地机器上的文件或流中。因此,为了集成到日志堆栈中,Fluentd 需要从磁盘上的大量文件中读取最新的日志。一般来说,在被监控的系统中注入自定义代码是不太可能的(例如,打印到日志系统而不是 stdout)。因此,Fluentd 选择了控制日志信息,从每一个不同的文件中读取信息,并将其推送到一个汇总的日志系统中,这意味着将 fluentd 添加到现有的二进制中是非常直接的。你根本不需要改变二进制文件。相反,你配置 fluentd 从特定路径的文件中加载数据,并将这些日志转发到一个日志存储系统。为了说明这一点,下面是一个 Fluentd 配置的例子,用于监控和推送 Kubernetes API Server 审计日志:
<source>
@type tail
format json
path /var/log/audit
pos_file /var/log/audit.pos
tag audit
time_key time
time_format %Y-%m-%dT%H:%M:%S.%N%z
</source>
….
从这个例子可以看出,Fluentd 的配置性很强,采取文件位置、文件格式和表达式,既可以从日志中解析日期,也可以为数据添加标签。由于Kubernetes 服务器使用 glog 包,日志行遵循一致的格式,因此你可以从 Kubernetes 记录的每一行中期待(并提取)结构化数据。
:::info Prometheus 通常被链接到一个应用程序中,但您仍然可以用现成的软件来使用它。有各种各样的 Prometheus 适配器,可以作为侧车运行在您的应用程序旁边。这些可以成为应用程序和预期的 Prometheus 接口之间的大使。在许多情况下(例如 Redis 或 Java),适配器知道如何直接与应用程序对话,以 Prometheus 可以理解的格式公开其数据。此外,还有来自常见监控协议(如 StatsD)的适配器,这样 Prometheus 也可以抓取原本用于其他系统的指标。 :::
存储数据以便检索和查询
在监控和记录数据被 Prometheus 或 Fluentd 汇总后,数据仍然需要存储在某个地方保留一段时间。在数据的存储空间方面,你保留监控数据的时间取决于你的系统需求和你愿意支付的成本。然而,根据我们的经验,你最少应该拥有 30-45 天的数据。起初,这可能看起来很多,但事实是,许多问题都是慢慢开始的,需要很长时间才能显现出来。有了历史的视角,你就可以在差异成为重大问题之前看到它们,并更容易确定问题的来源。
例如,四周前的一个版本可能会在请求处理中引入一些额外的延迟。这可能还没有显著到足以引起问题,但当与最近增加的请求流量相结合时,请求的处理速度太慢了,你的警报正在发射。如果没有历史数据来确定四周前延迟的最初增加,你就无法确定导致问题的版本(从而确定变化)。你将被困在代码中寻找问题,这可能会花费大量时间。
与聚合器一样,有许多选择来存储监控和日志数据。许多存储选项作为云服务运行。这些可以是很好的选择,因为它们消除了你的集群的存储部分的操作,但也有很好的理由运行你自己的存储,如次能够精确控制数据的位置和保留。即使在用于日志和监控的开源存储空间,你也有多种选择。为了时间和空间的利益,我们在这里讨论其中的两个。用于维护时间序列数据的 InfluxDB 和用于存储日志结构数据的 Elasticsearch。
InfluxDB
InfluxDB 是一个时间序列数据库,能够以紧凑和可搜索的格式存储大量数据。它是一个开放源码项目,可以在各种操作系统上免费使用。InfluxDB 以二进制包的形式发布,可以很容易地安装。
:::info 时间序列是一个数据对的集合,其中一个成员是一个值,另一个是时间的一个瞬间。例如,您可能有一个时间序列,表示一个进程在一段时间内的 CPU 使用情况。每个数据对都会结合 CPU 的使用情况和观察到 CPU 使用情况的瞬间。 :::
在运行 InfluxDB 时,有一个重要的问题是,是否将其作为容器在 Kubernetes 集群本身上运行。一般来说,这不是一个推荐的设置。你正在使用 InfluxDB 来监控集群,所以你希望监控数据能够继续被访问,即使集群本身出现问题。
Elasticsearch
Elasticsearch 是一个用于摄取和搜索基于日志的数据的系统。与 InfluxDB 不同的是,InfluxDB 是面向存储时间序列数据的,而 Elasticsearch 则是为了摄取大量的非结构化或半结构化的日志文件,并通过搜索界面来提供这些数据。Elasticsearch 可以从二进制包中安装。
数据可视化及交互
当然,如果你不能以有趣的方式访问这些信息来分析和理解系统中发生的事情,那么存储信息就不是很有用。为此,在一个完整的监控栈中,可视化是一个关键的组成部分。可视化对于日志数据和度量数据是不同的。度量监控数据通常被可视化为图表,可以是一个时间序列,显示一段时间内的几个指标,也可以是一个直方图,总结一个时间窗口内某一数值的统计数据。有时,它被可视化为一个跨时间窗口的总和(例如,一周内每小时所有错误的总和)。可视化指标的最流行的界面之一是开源的 Grafana 仪表盘,它可以与 Prometheus 和其他指标源对接,可以让你建立自己的仪表盘或导入其他用户创建的仪表盘。
对于记录的数据,搜索界面更多的是围绕着对已记录数据的临时查询和探索。查看日志数据的流行界面之一是 Kibana Web 前端,它可以让你搜索、浏览和反省已经被记录到 Elasticsearch 的数据。
监控什么?
现在,您已经组装好了您的监控堆栈,但仍有两个重要的问题没有解决:监控什么,以及相应地,对什么进行警报?
在组装监控信息时,和几乎所有的软件一样,采取分层的方法是很有价值的。要监控的层级是机器、集群基础、集群附加组件,最后是用户应用。这样,从基础知识开始,监控堆栈中的每一层都建立在它下面的一层之上。当这样构建时,识别问题就是在各层中向下潜行,直到找出原因。然而,相应地,如果达到了一个健康的层(例如,集群中的所有机器似乎都在正常运行),就会发现,问题显然出在上面的层(例如,集群基础设施)。
前一段所描述的监控都是白盒监控,我们的意思是,监控是基于对系统的详细了解以及系统的组装方式。系统的每个部分都会被监测到是否与预期有偏差,并报告这种偏差。
与白盒监控相对应的是黑盒或基于探针的监控。在黑盒监控中,你不假设或知道系统是如何构造的任何细节。相反,您只需像客户或用户一样消费外部接口,并观察您的操作是否具有预期的结果。例如,一个简单的 Kubernetes 集群的探针可能会将一个 Pod 调度到集群上,并验证 Pod 是否被成功创建,以及 Pod 中运行的应用程序(例如,nginx)是否可以通过 Kubernetes 服务到达。如果黑盒监控成功,一般可以认为系统是健康的。如果黑盒失败,则说明系统不健康。
黑盒监控的价值在于它能给你一个非常明确的信号,告诉你系统的健康状况。缺点是,它给你的系统故障原因的可见性非常小。因此,必须将白盒和黑盒监控结合起来,才能拥有一个强大、有用的监控系统。
节点监控
组成你的集群的机器(物理或虚拟)是你的 Kubernetes 集群的基础。如果您的集群中的机器超载或行为不当,集群内的所有其他操作都是可疑的。监控机器对于了解您的基本基础设施是否正常运行至关重要。
幸运的是,用 Prometheus 监控机器指标是非常直接的。Prometheus 项目有一个 node-exporter 守护进程,它可以在每台机器上运行,它公开了从内核和其他系统来源收集的基本信息,以便 Prometheus 可以抓取。这些数据包括:
- CPU使用率
- 网络使用情况
- 磁盘使用量和可用空间
- 内存使用情况
- …还有更多
你可以从 GitHub 上下载 Node Exporter,或者自己构建。当你的系统中有了 Node Exporter 二进制文件后,你可以使用这个简单的 systemd 单元文件将它设置为守护进程自动运行。
[Unit]
Description=Node Exporter
[Service]
User=node_exporter
EnvironmentFile=/etc/sysconfig/node_exporter
ExecStart=/usr/sbin/node_exporter $OPTIONS
[Install]
WantedBy=multi-user.target
在您启动并运行节点导出器后,您可以在 Prometheus 中使用以下抓取配置配置 Prometheus 从集群中的每台机器上抓取指标:
- job_name: "node"
scrape_interval: "60s"
static_configs:
- targets:
- 'server.1:9100'
- 'server.2:9100'
- ...
- 'server.N:9100'
系统监控
幸运的是,Kubernetes 基础架构的所有部分都使用 Prometheus API 暴露了指标,还有一个 Kubernetes 服务发现,你可以用它来自动发现和监控集群中的 Kubernetes 组件。
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
….
你可以重用这个服务发现的实现来为集群中的多个不同组件添加抓取,比如 API Server 和 kubelet。
应用监控
最后,你还可以使用 Kubernetes 服务发现从 Pod 本身寻找和刮取度量。这意味着,你会自动从 Kubernetes 集群中被设计成作为 Pod 运行的部分(例如,kube-dns 服务器)中搜刮度量,你也会自动从用户运行的 Pod 中搜刮所有度量,假设,也就是说,用户集成并暴露了与 Prometheus 兼容的度量。然而,当你的用户看到通过 Prometheus 获得自动度量监控是多么容易之后,他们不太可能使用任何其他监控。
黑盒监控
如前所述,黑盒监控会探测系统的外部 API,确保其正确响应。在本例中,系统的外部 API 是 Kubernetes API。对系统的探测可以由一个针对 Kubernetes API 进行调用的代理来完成。确定这个代理是否运行在 Kubernetes 集群内部是一个挑战。在集群内部运行它可以使其大大简化管理,但也使其容易受到集群故障的影响。如果你选择从集群内部监控集群,那么还必须有一个 “看门狗警报”,如果一个探针在之前的 N 分钟内没有运行完成,那么这个警报就会启动。你可以为 Kubernetes API 设计许多不同的黑盒测试。
作为一个简单的例子,你可以想象写一个小脚本:
#!/bin/bash
# exit on all failures
set -e
NAMESPACE=blackbox
# tear down the namespace no matter what
function teardown {
kubectl delete namespace ${NAMESPACE}
}
trap teardown ERR
# Create a probe namespace
kubectl create namespace ${NAMESPACE}
kubectl create -f my-deployment-spec.yaml
# Test connectivity to your app here, wget etc.
teardown
你可以每隔五分钟(或其他时间间隔)运行这个脚本来验证集群是否正常工作。一个更完整的例子可能是编写一个持续测试 Kubernetes API 的应用程序,类似于之前的脚本,但也可以导出 Prometheus 指标,这样你就可以将黑盒监控数据抓取进 Prometheus。
归根结底,你黑盒测试的极限其实就是你的想象力和你设计和构建测试的意愿。截至目前,Kubernetes API 还没有现成的好的黑盒探针。设计和构建这样的测试取决于你。
日志流
除了所有的指标监控数据,从你的集群中获取日志也很重要。这包括像来自每个节点的 kubelet 日志,以及来自主站的 API Server、调度器和控制器管理器日志。这些日志通常位于 /var/log/kube-*.log 中。你可以通过一个简单的 Fluentd 配置来设置它们的导出,比如:
<source>
@type tail
path /var/log/kube-apiserver.log
pos_file /var/log/fluentd-kube-apiserver.log.pos
tag kube-apiserver
...
</source>
将集群中运行的容器写到 stdout 的任何东西记录下来也是很有用的。默认情况下,Docker 会将容器的所有日志写入 /var/log/containers/*.log,因此你可以在类似的 Fluentd 配置中使用该表达式,也可以导出集群中运行的所有容器的日志数据。
告警
在您让监控正常工作后,就可以添加警报了。在 Prometheus 或其他系统中定义和实现警报超出了本书的范围。如果你以前从来没有做过监控,我们强烈建议你获得一本专门针对这个主题的书。
然而,当涉及到要定义哪些警报时,有两种哲学可以考虑。第一种,类似于白盒监控,当信号不再是标称时,就会发出警报。例如,了解一个 API Server 平时消耗了多少 CPU,如果 API Server 的 CPU 使用率超出了这个范围,就会发出警报。
这种监控方式的好处是,你经常会在问题对用户造成影响之前就注意到这些问题。系统在出现灾难性故障之前,往往就开始表现得很奇怪或很糟糕。
这种警报策略的缺点是,它可能会相当嘈杂。像 CPU 使用情况这样的信号可能会相当多,而且当事情发生变化时发出警报 — 即使不一定有真正的问题 — 可能会导致疲惫、沮丧的操作员,当有真正的警报时,他们会忽略页面。
另一种监控策略,更类似于黑盒监控,是对用户看到的信号进行提醒。例如,向 API Server 请求的延迟或你的 API Server 返回的403(Unauthorized)响应的数量。这种警报的好处是,根据定义,不能有嘈杂的警报。每当这样的警报响起时,都是一个真正的问题。这种告警的缺点是,你不会注意到问题,直到它们面向客户。
就像所有的事情一样,预警的最佳路径在于各方面的平衡。对于你非常了解的信号,这些信号具有稳定的数值,白盒预警在重大问题发生之前提供了关键的提醒。另一方面,黑匣子警报为您提供由真实的、面向用户的问题引起的高质量警报。一个成功的警报策略结合了这两种风格的警报(而且,也许更关键的是,随着你对你的特定集群的理解的增长,调整这些警报)。
总结
日志和监控是了解您的集群和您的应用程序如何执行,和/或它们在哪里出现问题的关键组成部分。构建一个高质量的警报和监控栈应该是成功建立集群后的首要任务之一。如果做得正确,一个自动提供给 Kubernetes 集群用户的日志记录和监控堆栈是一个关键的差异化因素,它使得开发人员可以大规模地部署和管理可靠的应用程序。
