DevOps-2

DevOps 2.5 工具包(全)

原文:zh.annas-archive.org/md5/E695B8200F27D70136CB7C8920C8BCB0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Kubernetes 可能是我们所知道的最大的项目。它是庞大的,然而许多人认为经过几周或几个月的阅读和实践后,他们就知道了所有关于它的知识。它比这大得多,而且它的增长速度比我们大多数人能够跟上的要快。你在 Kubernetes 采用中走了多远?

根据我的经验,Kubernetes 采用有四个主要阶段。

在第一阶段,我们创建一个集群,并学习 Kube API 的复杂性以及不同类型的资源(例如 Pods,Ingress,Deployments,StatefulSets 等)。一旦我们熟悉了 Kubernetes 的工作方式,我们就开始部署和管理我们的应用程序。在这个阶段结束时,我们可以大喊“看,我的生产 Kubernetes 集群中有东西在运行,没有出现问题!”我在《DevOps 2.3 工具包:Kubernetes》中解释了这个阶段的大部分内容(amzn.to/2GvzDjy)。

第二阶段通常是自动化。一旦我们熟悉了 Kubernetes 的工作方式,并且我们正在运行生产负载,我们就可以转向自动化。我们经常采用某种形式的持续交付(CD)或持续部署(CDP)。我们使用我们需要的工具创建 Pods,构建我们的软件和容器映像,运行测试,并部署到生产环境。完成后,我们的大部分流程都是自动化的,我们不再手动部署到 Kubernetes。我们可以说事情正在运行,我甚至没有碰键盘。我尽力在《DevOps 2.4 工具包:持续部署到 Kubernetes》中提供了一些关于 Kubernetes 的 CD 和 CDP 的见解(amzn.to/2NkIiVi)。

第三阶段在许多情况下与监控、警报、日志记录和扩展有关。我们可以在 Kubernetes 中运行(几乎)任何东西,并且它会尽最大努力使其具有容错性和高可用性,但这并不意味着我们的应用程序和集群是防弹的。我们需要监视集群,并且我们需要警报来通知我们可能存在的问题。当我们发现有问题时,我们需要能够查询整个系统的指标和日志。只有当我们知道根本原因是什么时,我们才能解决问题。在像 Kubernetes 这样高度动态的分布式系统中,这并不像看起来那么容易。

此外,我们需要学习如何扩展(和缩减)一切。应用程序的 Pod 数量应随时间变化,以适应流量和需求的波动。节点也应该进行扩展,以满足我们应用程序的需求。

Kubernetes 已经有了提供指标和日志可见性的工具。它允许我们创建自动扩展规则。然而,我们可能会发现单单 Kubernetes 还不够,我们可能需要用额外的流程和工具来扩展我们的系统。这本书的主题就是这个阶段。当你读完它时,你将能够说你的集群和应用程序真正是动态和有弹性的,并且需要很少的手动干预。我们将努力使我们的系统自适应。

我提到了第四阶段。亲爱的读者,那就是其他一切。最后阶段主要是关于跟上 Kubernetes 提供的所有其他好东西。这是关于遵循其路线图并调整我们的流程以获得每个新版本的好处。

最终,你可能会遇到困难,需要帮助。或者你可能想对这本书的内容写一篇评论或评论。请加入DevOps20slack.devops20toolkit.com/)Slack 工作区,发表你的想法,提出问题,或参与讨论。如果你更喜欢一对一的沟通,你可以使用 Slack 给我发私信,或发送邮件至viktor@farcic.com。我写的所有书对我来说都很重要,我希望你在阅读它们时有一个很好的体验。其中一部分体验就是可以联系我。不要害羞。

请注意,这本书和之前的书一样,是我自行出版的。我相信作家和读者之间没有中间人是最好的方式。这样可以让我更快地写作,更频繁地更新书籍,并与你进行更直接的沟通。你的反馈是这个过程的一部分。无论你是在只有少数章节还是所有章节都写完时购买了这本书,我的想法是它永远不会真正完成。随着时间的推移,它将需要更新,以使其与技术或流程的变化保持一致。在可能的情况下,我会尽量保持更新,并在有意义的时候发布更新。最终,事情可能会发生如此大的变化,以至于更新不再是一个好选择,这将是需要一本全新书的迹象。只要我继续得到你的支持,我就会继续写作。

概述

我们将探讨操作 Kubernetes 集群所需的一些技能和知识。我们将处理一些通常不会在最初阶段学习的主题,而是在我们厌倦了 Kubernetes 的核心功能(如 Pod、ReplicaSets、Deployments、Ingress、PersistentVolumes 等)之后才会涉及。我们将掌握一些我们通常在学会基础知识并自动化所有流程之后才会深入研究的主题。我们将探讨监控警报日志记录自动扩展等旨在使我们的集群具有弹性自给自足自适应的主题。

受众

我假设你对 Kubernetes 很熟悉,不需要解释 Kube API 的工作原理,也不需要解释主节点和工作节点之间的区别,尤其不需要解释像 Pods、Ingress、Deployments、StatefulSets、ServiceAccounts 等资源和构造。如果你不熟悉,这个内容可能太高级了,我建议你先阅读《The DevOps 2.3 Toolkit: Kubernetes》(amzn.to/2GvzDjy)。我希望你已经是一个 Kubernetes 忍者学徒,你对如何使你的集群更具弹性、可扩展和自适应感兴趣。如果是这样,这本书就是为你准备的。继续阅读。

要求

这本书假设你已经知道如何操作 Kubernetes 集群,因此我们不会详细介绍如何创建一个,也不会探讨 Pods、Deployments、StatefulSets 等常用的 Kubernetes 资源。如果这个假设是不正确的,你可能需要先阅读《The DevOps 2.3 Toolkit: Kubernetes》。

除了基于知识的假设外,还有一些技术要求。如果您是Windows 用户,请从Git Bash中运行所有示例。这将允许您像 MacOS 和 Linux 用户一样通过他们的终端运行相同的命令。Git Bash 在Git安装过程中设置。如果您还没有它,请重新运行 Git 设置。

由于我们将使用 Kubernetes 集群,我们将需要kubectl (kubernetes.io/docs/tasks/tools/install-kubectl/)。我们将在集群内运行的大多数应用程序都将使用Helm (helm.sh/)进行安装,因此请确保您也安装了客户端。最后,也安装jq (stedolan.github.io/jq/)。这是一个帮助我们格式化和过滤 JSON 输出的工具。

最后,我们将需要一个 Kubernetes 集群。所有示例都是使用Docker for DesktopminikubeGoogle Kubernetes Engine (GKE)Amazon Elastic Container Service for Kubernetes (EKS)Azure Kubernetes Service (AKS)进行测试的。我将为每种 Kubernetes 版本提供要求(例如节点数、CPU、内存、Ingress 等)。

您可以将这些教训应用于任何经过测试的 Kubernetes 平台,或者您可以选择使用其他平台。这本书中的示例不应该在任何 Kubernetes 版本中无法运行。您可能需要在某些地方进行微调,但我相信这不会成为问题。

如果遇到任何问题,请通过DevOps20 (slack.devops20toolkit.com/) slack 工作区与我联系,或者通过发送电子邮件至viktor@farcic.com。我会尽力帮助解决。如果您使用的是我未测试过的 Kubernetes 集群,请帮助我扩展列表。

在选择 Kubernetes 版本之前,您应该知道并非所有功能都在所有地方都可用。在基于 Docker for Desktop 或 minikube 的本地集群中,由于两者都是单节点集群,将无法扩展节点。其他集群可能也无法使用更多特定功能。我将利用这个机会比较不同的平台,并为您提供额外的见解,如果您正在评估要使用哪个 Kubernetes 发行版以及在哪里托管它,您可能会想要使用。或者,您可以选择在本地集群中运行一些章节,并仅在本地无法运行的部分切换到多节点集群。这样,您可以通过在云中拥有一个短暂的集群来节省一些开支。

如果您不确定要选择哪种 Kubernetes 版本,请选择 GKE。它目前是市场上最先进和功能丰富的托管 Kubernetes。另一方面,如果您已经习惯了 EKS 或 AKS,它们也差不多可以。本书中的大多数,如果不是全部的功能都会起作用。最后,您可能更喜欢在本地运行集群,或者您正在使用不同的(可能是本地)Kubernetes 平台。在这种情况下,您将了解到您所缺少的内容,以及您需要在“标准提供”的基础上构建哪些内容来实现相同的结果。

下载示例代码文件

您可以从您在www.packt.com上的账户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support注册,将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名,按照屏幕上的指示操作。

文件下载后,请确保使用以下最新版本的解压缩或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/The-DevOps-2.5-Toolkit。如果代码有更新,将会在现有的 GitHub 存储库中更新。

我们还有来自我们丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781838647513_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“该定义使用HorizontalPodAutoscaler目标为api部署。”

代码块设置如下:

 1  sum(label_join(
 2      rate(
 3          container_cpu_usage_seconds_total{
 4              namespace!="kube-system",
 5              pod_name!=""
 6          }[5m]
 7      )

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将以粗体显示:

 1  sum(label_join(
 2      rate(
 3          container_cpu_usage_seconds_total{
 4              namespace!="kube-system",
 5              pod_name!=""
 6          }[5m]
 7      )

任何命令行输入或输出都以以下方式编写:

 1  cd k8s-specs
 2
 3  git pull

粗体:表示一个新术语,一个重要的词,或者你在屏幕上看到的词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“选择 Prometheus,并单击导入按钮。”

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一章:根据资源使用自动调整部署和有状态集

变化是所有存在的基本过程。

  • 斯波克

到目前为止,你可能已经了解到,基于 Kubernetes 的系统的一个关键方面是高度的动态性。几乎没有什么是静态的。我们定义部署或有状态集,Kubernetes 会在集群中分发 Pods。在大多数情况下,这些 Pods 很少在一个地方停留很长时间。滚动更新会导致 Pods 被重新创建并可能移动到其他节点。任何类型的故障都会引发受影响资源的重新调度。许多其他事件也会导致 Pods 移动。Kubernetes 集群就像一个蜂巢。它充满了生机,而且总是在运动中。

Kubernetes 集群的动态性不仅是由我们(人类)的行为或由故障引起的重新调度所致。自动缩放也应该受到责备。我们应该充分接受 Kubernetes 的动态性,并朝着能够满足我们应用程序需求的自主和自给的集群发展。为了实现这一点,我们需要提供足够的信息,让 Kubernetes 能够调整应用程序以及构成集群的节点。在本章中,我们将重点关注前一种情况。我们将探讨基于内存和 CPU 消耗的自动缩放 Pods 的常用和基本方法。我们将使用 HorizontalPodAutoscaler 来实现这一点。

HorizontalPodAutoscaler 的唯一功能是自动调整部署、有状态集或其他一些类型资源中 Pods 的数量。它通过观察 Pods 的 CPU 和内存消耗,并在达到预定义阈值时采取行动来实现这一点。

HorizontalPodAutoscaler 被实现为 Kubernetes API 资源和控制器。资源决定了控制器的行为。控制器定期调整有状态集或部署中的副本数量,以匹配用户指定的目标平均 CPU 利用率。

我们很快就会看到 HorizontalPodAutoscaler 的实际应用,并通过实际示例评论其特定功能。但在那之前,我们需要一个 Kubernetes 集群以及一个度量源。

创建集群

在创建集群之前(或开始使用您已经可用的集群),我们将克隆 vfarcic/k8s-specs (github.com/vfarcic/k8s-specs) 存储库,其中包含本书中大部分我们将使用的定义。

给 Windows 用户的说明:请从 Git Bash 中执行本书中的所有命令。这样,您就可以直接运行它们,而不需要修改其语法以适应 Windows 终端或 PowerShell。本章中的所有命令都可以在 01-hpa.sh (gist.github.com/vfarcic/b46ca2eababb98d967e3e25748740d0d) Gist 中找到。

 1  git clone https://github.com/vfarcic/k8s-specs.git
 2
 3  cd k8s-specs

如果您之前克隆过该存储库,请确保通过执行 git pull 来获取最新版本。

以下的代码片段和规范用于测试本章中的命令。请在创建自己的测试集群时以此为灵感,或者验证您计划用于练习的集群是否满足最低要求。

请注意,我们将使用 Helm 来安装必要的应用程序,但我们将切换到“纯粹”的 Kubernetes YAML 来尝试(可能是新的)本章中使用的资源,并部署演示应用程序。换句话说,我们将使用 Helm 进行一次性安装(例如,Metrics Server),并使用 YAML 来更详细地探索我们将要使用的内容(例如,HorizontalPodAutoscaler)。

现在,让我们来谈谈 Metrics Server。

观察 Metrics Server 数据

在扩展 Pods 的关键元素是 Kubernetes Metrics Server。你可能认为自己是 Kubernetes 的高手,但从未听说过 Metrics Server。如果是这种情况,不要感到羞愧。你并不是唯一一个。

如果你开始观察 Kubernetes 的指标,你可能已经使用过 Heapster。它已经存在很长时间了,你可能已经在你的集群中运行它,即使你不知道它是什么。两者都有相同的目的,其中一个已经被弃用了一段时间,所以让我们澄清一下事情。

早期,Kubernetes 引入了 Heapster 作为一种工具,用于为 Kubernetes 启用容器集群监控和性能分析。它从 Kubernetes 版本 1.0.6 开始存在。你可以说 Heapster 从 Kubernetes 的幼年时代就开始了。它收集和解释各种指标,如资源使用情况、事件等。Heapster 一直是 Kubernetes 的一个重要组成部分,并使其能够适当地调度 Pods。没有它,Kubernetes 将是盲目的。它不会知道哪个节点有可用内存,哪个 Pod 使用了太多的 CPU 等等。但是,就像大多数其他早期可用的工具一样,它的设计是一个“失败的实验”。

随着 Kubernetes 的持续增长,我们(Kubernetes 周围的社区)开始意识到需要一个新的、更好的、更重要的是更具可扩展性的设计。因此,Metrics Server 诞生了。现在,尽管 Heapster 仍在使用中,但它被视为已弃用,即使在今天(2018 年 9 月),Metrics Server 仍处于测试阶段。

那么,Metrics Server 是什么?一个简单的解释是,它收集有关节点和 Pod 使用的资源(内存和 CPU)的信息。它不存储指标,所以不要认为您可以使用它来检索历史值和预测趋势。有其他工具可以做到这一点,我们稍后会探讨它们。相反,Metrics Server 的目标是提供一个 API,可以用来检索当前的资源使用情况。我们可以通过kubectl或通过发送直接请求,比如curl来使用该 API。换句话说,Metrics Server 收集集群范围的指标,并允许我们通过其 API 检索这些指标。这本身就非常强大,但这只是故事的一部分。

我已经提到了可扩展性。我们可以扩展 Metrics Server 以从其他来源收集指标。我们会在适当的时候到达那里。现在,我们将探索它提供的开箱即用功能,以及它如何与一些其他 Kubernetes 资源交互,这些资源将帮助我们使我们的 Pods 可伸缩和更具弹性。

如果您读过我的其他书,您就会知道我不会过多涉及理论,而是更喜欢通过实际示例来演示功能和原则。这本书也不例外,我们将直接深入了解 Metrics Server 的实际练习。第一步是安装它。

Helm 使安装几乎任何公开可用的软件变得非常容易,如果有 Chart 可用的话。如果没有,您可能需要考虑另一种选择,因为这清楚地表明供应商或社区不相信 Kubernetes。或者,也许他们没有必要开发 Chart 的技能。无论哪种方式,最好的做法是远离它并采用另一种选择。如果这不是一个选择,那就自己开发一个 Helm Chart。在我们的情况下,不需要这样的措施。Metrics Server 确实有一个 Helm Chart,我们需要做的就是安装它。

GKE 和 AKS 用户请注意,Google 和 Microsoft 已经将 Metrics Server 作为其托管的 Kubernetes 集群(GKE 和 AKS)的一部分进行了打包。无需安装它,请跳过接下来的命令。对于 minikube 用户,请注意,Metrics Server 作为插件之一可用。请执行minikube addons enable metrics-serverkubectl -n kube-system rollout status deployment metrics-server命令,而不是接下来的命令。对于 Docker for Desktop 用户,请注意,Metrics Server 的最新更新默认情况下不适用于自签名证书。由于 Docker for Desktop 使用这样的证书,您需要允许不安全的 TLS。请在接下来的helm install命令中添加--set args={"--kubelet-insecure-tls=true"}参数。

 1  helm install stable/metrics-server \
 2      --name metrics-server \
 3      --version 2.0.2 \
 4      --namespace metrics
 5
 6  kubectl -n metrics \
 7      rollout status \
 8      deployment metrics-server

我们使用 Helm 安装了 Metrics Server,并等待直到它部署完成。

Metrics Server 将定期从运行在节点上的 Kubeletes 中获取指标。目前,这些指标包括 Pod 和节点的内存和 CPU 利用率。其他实体可以通过具有 Master Metrics API 的 API 服务器从 Metrics Server 请求数据。这些实体的一个例子是调度程序,一旦安装了 Metrics Server,就会使用其数据来做出决策。

很快您将会看到,Metrics Server 的使用超出了调度程序,但是目前,这个解释应该提供了一个基本数据流的图像。

图 1-1:数据流向和从 Metrics Server 获取数据的基本流程(箭头显示数据流向)

现在我们可以探索一种检索指标的方式。我们将从与节点相关的指标开始。

 1  kubectl top nodes

如果您很快,输出应该会声明“尚未提供指标”。这是正常的。在执行第一次迭代的指标检索之前需要几分钟时间。例外情况是 GKE 和 AKS,它们已经预先安装了 Metrics Server。

在重复命令之前先去冲杯咖啡。

 1  kubectl top nodes

这次,输出是不同的。

在本章中,我将展示来自 Docker for Desktop 的输出。根据您使用的 Kubernetes 版本不同,您的输出也会有所不同。但是,逻辑是相同的,您不应该有问题跟随操作。

我的输出如下。

NAME               CPU(cores) CPU% MEMORY(bytes) MEMORY%
docker-for-desktop 248m       12%  1208Mi        63%

我们可以看到我有一个名为docker-for-desktop的节点。它正在使用 248 CPU 毫秒。由于节点有两个核心,这占总可用 CPU 的 12%。同样,使用了 1.2GB 的 RAM,这占总可用内存 2GB 的 63%。

节点的资源使用情况很有用,但不是我们要寻找的内容。在本章中,我们专注于 Pod 的自动扩展。但是,在我们开始之前,我们应该观察一下我们的每个 Pod 使用了多少内存。我们将从在kube-system命名空间中运行的 Pod 开始。

 1  kubectl -n kube-system top pod

输出(在 Docker for Desktop 上)如下。

NAME                                       CPU(cores) MEMORY(bytes)
etcd-docker-for-desktop                    16m        74Mi
kube-apiserver-docker-for-desktop          33m        427Mi
kube-controller-manager-docker-for-desktop 44m        63Mi
kube-dns-86f4d74b45-c47nh                  1m         39Mi
kube-proxy-r56kd                           2m         22Mi
kube-scheduler-docker-for-desktop          13m        23Mi
tiller-deploy-5c688d5f9b-2pspz             0m         21Mi

我们可以看到kube-system中当前运行的每个 Pod 的资源使用情况(CPU 和内存)。如果我们找不到更好的工具,我们可以使用该信息来调整这些 Pod 的requests以使其更准确。但是,有更好的方法来获取这些信息,所以我们将暂时跳过调整。相反,让我们尝试获取所有 Pod 的当前资源使用情况,无论命名空间如何。

 1  kubectl top pods --all-namespaces

输出(在 Docker for Desktop 上)如下。

NAMESPACE   NAME                                       CPU(cores) MEMORY(bytes) 
docker      compose-7447646cf5-wqbwz                   0m         11Mi 
docker      compose-api-6fbc44c575-gwhxt               0m         14Mi 
kube-system etcd-docker-for-desktop                    16m        74Mi 
kube-system kube-apiserver-docker-for-desktop          33m        427Mi 
kube-system kube-controller-manager-docker-for-desktop 46m        63Mi 
kube-system kube-dns-86f4d74b45-c47nh                  1m         38Mi 
kube-system kube-proxy-r56kd                           3m         22Mi 
kube-system kube-scheduler-docker-for-desktop          14m        23Mi 
kube-system tiller-deploy-5c688d5f9b-2pspz             0m         21Mi 
metrics     metrics-server-5d78586d76-pbqj8            0m         10Mi 

该输出显示与上一个输出相同的信息,只是扩展到所有命名空间。不需要对其进行评论。

通常,Pod 的度量不够精细,我们需要观察构成 Pod 的每个容器的资源。要获取容器度量,我们只需要添加--containers参数。

 1  kubectl top pods \
 2    --all-namespaces \
 3    --containers

输出(在 Docker for Desktop 上)如下。

NAMESPACE   POD                                        NAME                 CPU(cores) MEMORY(bytes) 
docker      compose-7447646cf5-wqbwz                   compose                 0m         11Mi 
docker      compose-api-6fbc44c575-gwhxt               compose                 0m         14Mi 
kube-system etcd-docker-for-desktop                    etcd                    16m        74Mi 
kube-system kube-apiserver-docker-for-desktop          kube-apiserver          33m        427Mi 
kube-system kube-controller-manager-docker-for-desktop kube-controller-manager 46m        63Mi 
kube-system kube-dns-86f4d74b45-c47nh                  kubedns                 0m         13Mi 
kube-system kube-dns-86f4d74b45-c47nh                  dnsmasq                 0m         10Mi 
kube-system kube-dns-86f4d74b45-c47nh                  sidecar                 1m         14Mi 
kube-system kube-proxy-r56kd                           kube-proxy              3m         22Mi 
kube-system kube-scheduler-docker-for-desktop          kube-scheduler          14m        23Mi 
kube-system tiller-deploy-5c688d5f9b-2pspz             tiller                  0m         21Mi 
metrics     metrics-server-5d78586d76-pbqj8            metrics-server          0m         10Mi 

我们可以看到,这次输出显示了每个容器。例如,我们可以观察到kube-dns-* Pod 的度量分为三个容器(kubednsdnsmasqsidecar)。

当我们通过kubectl top请求指标时,数据流几乎与调度程序发出请求时的流程相同。请求被发送到 API 服务器(主度量 API),该服务器从度量服务器获取数据,而度量服务器又从集群节点上运行的 Kubeletes 收集信息。

图 1-2:数据流向和从度量服务器流向的方向(箭头显示数据流向)

虽然 kubectl top 命令对观察当前指标很有用,但如果我们想从其他工具访问它们,它就没什么用了。毕竟,我们的目标不是坐在终端前用 watch "kubectl top pods" 命令。那将是浪费我们(人类)的才能。相反,我们的目标应该是从其他工具中抓取这些指标,并根据实时和历史数据创建警报和(也许)仪表板。为此,我们需要以 JSON 或其他机器可解析的格式输出。幸运的是,kubectl 允许我们以原始格式直接调用其 API,并检索与工具查询相同的结果。

 1  kubectl get \
 2      --raw "/apis/metrics.k8s.io/v1beta1" \
 3      | jq '.'

输出如下。

{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "nodes",
      "singularName": "",
      "namespaced": false,
      "kind": "NodeMetrics",
      "verbs": [
        "get",
        "list"
      ]
    },
    {
      "name": "pods",
      "singularName": "",
      "namespaced": true,
      "kind": "PodMetrics",
      "verbs": [
        "get",
        "list"
      ]
    }
  ]
}

我们可以看到 /apis/metrics.k8s.io/v1beta1 端点是一个索引 API,有两个资源(nodespods)。

让我们更仔细地看一下度量 API 的 pods 资源。

 1  kubectl get \
 2      --raw "/apis/metrics.k8s.io/v1beta1/pods" \
 3      | jq '.'

输出太大,无法在一本书中呈现,所以我会留给你去探索。你会注意到输出是通过 kubectl top pods --all-namespaces --containers 命令观察到的 JSON 等效物。

这是度量服务器的快速概述。有两件重要的事情需要注意。首先,它提供了集群内运行的容器的当前(或短期)内存和 CPU 利用率。第二个更重要的注意事项是我们不会直接使用它。度量服务器不是为人类设计的,而是为机器设计的。我们以后会到那里。现在,记住有一个叫做度量服务器的东西,你不应该直接使用它(一旦你采用了一个会抓取其度量的工具)。

现在我们已经探索了度量服务器,我们将尝试充分利用它,并学习如何根据资源利用率自动扩展我们的 Pods。

根据资源利用率自动扩展 Pods

我们的目标是部署一个应用程序,根据其资源使用情况自动扩展(或缩小)。我们将首先部署一个应用程序,然后讨论如何实现自动扩展。

我已经警告过您,我假设您熟悉 Kubernetes,并且在本书中我们将探讨监控,警报,扩展和其他一些特定主题。我们不会讨论 Pods,StatefulSets,Deployments,Services,Ingress 和其他“基本”Kubernetes 资源。这是您承认您不了解 Kubernetes 基础知识的最后机会,退一步,并阅读The DevOps 2.3 Toolkit: Kubernetes (www.devopstoolkitseries.com/posts/devops-23/)和The DevOps 2.4 Toolkit: Continuous Deployment To Kubernetes (www.devopstoolkitseries.com/posts/devops-24/)

让我们看一下我们示例中将使用的应用程序的定义。

 1  cat scaling/go-demo-5-no-sidecar-mem.yml

如果您熟悉 Kubernetes,YAML 定义应该是不言自明的。我们只会评论与自动扩展相关的部分。

输出,仅限于相关部分,如下。

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
  namespace: go-demo-5
spec:
  ...
  template:
    ...
    spec:
      ...
      containers:
      - name: db
        ...
        resources:
          limits:
            memory: "150Mi"
            cpu: 0.2
          requests:
            memory: "100Mi"
            cpu: 0.1
        ...
      - name: db-sidecar
    ... 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: go-demo-5
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: api
        ...
        resources:
          limits:
            memory: 15Mi
            cpu: 0.1
          requests:
            memory: 10Mi
            cpu: 0.01
...

我们有两个形成应用程序的 Pod。 api部署是一个后端 API,使用db StatefulSet 来保存其状态。

定义的基本部分是“资源”。 apidb都为内存和 CPU 定义了“请求”和“限制”。数据库使用一个 sidecar 容器,将 MongoDB 副本加入到副本集中。请注意,与其他容器不同,sidecar 没有“资源”。这背后的重要性将在稍后揭示。现在,只需记住两个容器有定义的“请求”和“限制”,而另一个没有。

现在,让我们创建这些资源。

 1  kubectl apply \
 2      -f scaling/go-demo-5-no-sidecar-mem.yml \
 3      --record

输出应该显示已创建了相当多的资源,我们的下一步是等待api部署推出,从而确认应用程序正在运行。

 1  kubectl -n go-demo-5 \
 2      rollout status \
 3      deployment api

几分钟后,您应该会看到消息,指出“api”部署成功推出。

为了安全起见,我们将列出go-demo-5命名空间中的 Pod,并确认每个 Pod 都在运行一个副本。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        1m
db-0    2/2   Running 0        1m

到目前为止,我们还没有做任何超出 StatefulSet 和 Deployment 的普通创建。

他们又创建了 ReplicaSets,这导致了 Pod 的创建。

图 1-3:StatefulSet 和 Deployment 的创建

希望你知道,我们应该至少有每个 Pod 的两个副本,只要它们是可扩展的。然而,这两者都没有定义replicas。这是有意的。我们可以指定部署或 StatefulSet 的副本数量,并不意味着我们应该这样做。至少,不总是。

如果副本数量是静态的,并且你没有打算随时间扩展(或缩减)你的应用程序,那么将replicas作为部署或 StatefulSet 定义的一部分。另一方面,如果你计划根据内存、CPU 或其他指标更改副本数量,请改用 HorizontalPodAutoscaler 资源。

让我们来看一个 HorizontalPodAutoscaler 的简单示例。

 1  cat scaling/go-demo-5-api-hpa.yml

输出如下。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: api
  namespace: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 80
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 80

定义使用HorizontalPodAutoscaler来定位api部署。它的边界是最少两个和最多五个副本。这些限制是基本的。没有这些限制,我们会面临无限扩展或缩减到零副本的风险。minReplicasmaxReplicas字段是一个安全网。

定义的关键部分是metrics。它提供了 Kubernetes 应该使用的公式来决定是否应该扩展(或缩减)资源。在我们的例子中,我们使用Resource类型的条目。它们针对内存和 CPU 的平均利用率为 80%。如果两者中的任何一个实际使用情况偏离,Kubernetes 将扩展(或缩减)资源。

请注意,我们使用了 API 的v2beta1版本,你可能想知道为什么我们选择了这个版本,而不是稳定且适用于生产的v1。毕竟,beta1版本仍远未经过充分打磨以供一般使用。原因很简单。HorizontalPodAutoscaler v1太基础了。它只允许基于 CPU 进行扩展。即使我们的简单示例也超越了这一点,通过将内存加入其中。以后,我们将进一步扩展它。因此,虽然v1被认为是稳定的,但它并没有提供太多价值,我们可以等待v2发布,或者立即开始尝试v2beta版本。我们选择了后者。当你阅读这篇文章时,更稳定的版本可能已经存在并且在你的 Kubernetes 集群中得到支持。如果是这种情况,请随时在应用定义之前更改apiVersion

现在让我们应用它。

 1  kubectl apply \
 2      -f scaling/go-demo-5-api-hpa.yml \
 3      --record

我们应用了创建HorizontalPodAutoscalerHPA)的定义。接下来,我们将查看检索 HPA 资源时获得的信息。

 1  kubectl -n go-demo-5 get hpa

如果你很快,输出应该类似于以下内容。

NAME REFERENCE      TARGETS                      MINPODS MAXPODS REPLICAS AGE
api  Deployment/api <unknown>/80%, <unknown>/80% 2       5       0        20s

我们可以看到,Kubernetes 尚未具有实际的 CPU 和内存利用率,而是输出了<unknown>。在从 Metrics Server 收集下一次数据之前,我们需要再给它一些时间。在我们重复相同的查询之前,先喝杯咖啡。

 1  kubectl -n go-demo-5 get hpa

这次,输出中没有未知项。

NAME REFERENCE      TARGETS          MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 38%/80%, 10%/80% 2       5       2        1m

我们可以看到,CPU 和内存利用率远低于预期的80%利用率。尽管如此,Kubernetes 将副本数从一个增加到两个,因为这是我们定义的最小值。我们签订了合同,规定api Deployment 的副本数永远不得少于两个,即使资源利用率远低于预期的平均利用率,Kubernetes 也会遵守这一点进行扩展。我们可以通过 HorizontalPodAutoscaler 的事件来确认这种行为。

 1  kubectl -n go-demo-5 describe hpa api

输出,仅限于事件消息,如下所示。

...
Events:
... Message
... -------
... New size: 2; reason: Current number of replicas below Spec.MinReplicas

事件的消息应该是不言自明的。HorizontalPodAutoscaler 将副本数更改为2,因为当前数量(1)低于MinReplicas值。

最后,我们将列出 Pods,以确认所需数量的副本确实正在运行。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        2m
api-... 1/1   Running 0        6m
db-0    2/2   Running 0        6m

到目前为止,HPA 尚未根据资源使用情况执行自动缩放。相反,它只增加了 Pod 的数量以满足指定的最小值。它通过操纵 Deployment 来实现这一点。

图 1-4:根据 HPA 中指定的最小副本数进行部署的扩展

接下来,我们将尝试创建另一个 HorizontalPodAutoscaler,但这次,我们将以运行我们的 MongoDB 的 StatefulSet 为目标。因此,让我们再看一下另一个 YAML 定义。

 1  cat scaling/go-demo-5-db-hpa.yml

输出如下。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: db
  namespace: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: db
  minReplicas: 3
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 80
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 80

该定义几乎与我们之前使用的定义相同。唯一的区别是,这次我们的目标是名为dbStatefulSet,并且最小副本数应为3

让我们应用它。

 1  kubectl apply \
 2      -f scaling/go-demo-5-db-hpa.yml \
 3      --record

让我们再看一下 HorizontalPodAutoscaler 资源。

 1  kubectl -n go-demo-5 get hpa

输出如下。

NAME REFERENCE      TARGETS                      MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 41%/80%, 0%/80%              2       5       2        5m
db   StatefulSet/db <unknown>/80%, <unknown>/80% 3       5       0        20s

我们可以看到第二个 HPA 已经创建,并且当前利用率为“未知”。这一定是之前的类似情况。我们应该给它一些时间让数据开始流动吗?等待片刻,然后再次检索 HPA。目标仍然是“未知”吗?

资源利用持续未知可能有问题。让我们描述新创建的 HPA,看看是否能找到问题的原因。

 1  kubectl -n go-demo-5 describe hpa db

输出,仅限于事件消息,如下所示。

...
Events:
... Message
... -------
... New size: 3; reason: Current number of replicas below Spec.MinReplicas
... missing request for memory on container db-sidecar in pod go-demo-5/db-0
... failed to get memory utilization: missing request for memory on container db-sidecar in pod go-demo-5/db-0

请注意,您的输出可能只有一个事件,甚至没有这些事件。如果是这种情况,请等待几分钟,然后重复上一个命令。

如果我们关注第一条消息,我们可以看到它开始得很好。HPA 检测到当前副本数低于限制,并将它们增加到了三个。这是预期的行为,所以让我们转向其他两条消息。

HPA 无法计算百分比,因为我们没有指定db-sidecar容器请求多少内存。没有requests,HPA 无法计算实际内存使用的百分比。换句话说,我们忽略了为db-sidecar容器指定资源,HPA 无法完成其工作。我们将通过应用go-demo-5-no-hpa.yml来解决这个问题。

让我们快速看一下新定义。

 1  cat scaling/go-demo-5-no-hpa.yml

输出,仅限于相关部分,如下所示。

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
  namespace: go-demo-5
spec:
  ...
  template:
    ...
    spec:
      ...
      - name: db-sidecar
        ...
        resources:
          limits:
            memory: "100Mi"
            cpu: 0.2
          requests:
            memory: "50Mi"
            cpu: 0.1
...

与初始定义相比,唯一显着的区别是这次我们为db-sidecar容器定义了资源。让我们应用它。

 1  kubectl apply \
 2      -f scaling/go-demo-5-no-hpa.yml \
 3      --record

接下来,我们将等待片刻以使更改生效,然后再次检索 HPA。

 1  kubectl -n go-demo-5 get hpa

这一次,输出更有希望。

NAME REFERENCE      TARGETS          MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 66%/80%, 10%/80% 2       5       2        16m
db   StatefulSet/db 60%/80%, 4%/80%  3       5       3        10m

两个 HPA 都显示了当前和目标资源使用情况。都没有达到目标值,所以 HPA 保持了最小副本数。我们可以通过列出go-demo-5命名空间中的所有 Pod 来确认这一点。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        42m
api-... 1/1   Running 0        46m
db-0    2/2   Running 0        33m
db-1    2/2   Running 0        33m
db-2    2/2   Running 0        33m

我们可以看到api部署有两个 Pod,而db StatefulSet 有三个副本。这些数字等同于 HPA 定义中的spec.minReplicas条目。

让我们看看当实际内存使用量高于目标值时会发生什么。

我们将通过降低其中一个 HPA 的目标来修改其定义,以重现我们的 Pod 消耗资源超出预期的情况。

让我们看一下修改后的 HPA 定义。

 1  cat scaling/go-demo-5-api-hpa-low-mem.yml

输出,仅限于相关部分,如下所示。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: api
  namespace: go-demo-5
spec:
  ...
  metrics:
  ...
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 10

我们将targetAverageUtilization减少到10。这肯定低于当前的内存利用率,我们将能够见证 HPA 的工作。让我们应用新的定义。

 1  kubectl apply \
 2      -f scaling/go-demo-5-api-hpa-low-mem.yml \
 3      --record

请等待一段时间,以便进行下一次数据收集迭代,并检索 HPAs。

 1  kubectl -n go-demo-5 get hpa

输出如下。

NAME REFERENCE      TARGETS          MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 49%/10%, 10%/80% 2       5       2        44m
db   StatefulSet/db 64%/80%, 5%/80%  3       5       3        39m

我们可以看到api HPA 的实际内存(49%)远远超过了阈值(10%)。然而,副本的数量仍然是相同的(2)。我们需要等待几分钟,然后再次检索 HPAs。

 1  kubectl -n go-demo-5 get hpa

这次,输出略有不同。

NAME REFERENCE      TARGETS          MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 49%/10%, 10%/80% 2       5       4        44m
db   StatefulSet/db 64%/80%, 5%/80%  3       5       3        39m

我们可以看到副本数量增加到4。HPA 改变了部署,导致了级联效应,从而增加了 Pod 的数量。

让我们描述一下api HPA。

 1  kubectl -n go-demo-5 describe hpa api

输出,仅限于事件消息,如下所示。

...
Events:
... Message
... -------
... New size: 2; reason: Current number of replicas below Spec.MinReplicas
... New size: 4; reason: memory resource utilization (percentage of request) above target

我们可以看到 HPA 将大小更改为4,因为内存资源利用率(请求百分比)高于目标。

由于在这种情况下,增加副本数量并没有将内存消耗降低到 HPA 目标以下,我们应该期望 HPA 将继续扩展部署,直到达到5的限制。我们将通过等待几分钟并再次描述 HPA 来确认这一假设。

 1  kubectl -n go-demo-5 describe hpa api

输出,仅限于事件消息,如下所示。

...
Events:
... Message
... -------
... New size: 2; reason: Current number of replicas below Spec.MinReplicas
... New size: 4; reason: memory resource utilization (percentage of request) above target
... New size: 5; reason: memory resource utilization (percentage of request) above target

我们收到了消息,说明新的大小现在是5,从而证明 HPA 将继续扩展,直到资源低于目标,或者在我们的情况下,达到最大副本数量。

我们可以通过列出go-demo-5命名空间中的所有 Pod 来确认扩展确实起作用。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        47m
api-... 1/1   Running 0        51m
api-... 1/1   Running 0        4m
api-... 1/1   Running 0        4m
api-... 1/1   Running 0        24s
db-0    2/2   Running 0        38m
db-1    2/2   Running 0        38m
db-2    2/2   Running 0        38m

正如我们所看到的,api部署确实有五个副本。

HPA 从 Metrics Server 中检索数据,得出实际资源使用量高于阈值,并使用新的副本数量操纵了部署。

图 1-5:HPA 通过操纵部署进行扩展

接下来,我们将验证缩减副本数量也能正常工作。我们将重新应用初始定义,其中内存和 CPU 都设置为百分之八十。由于实际内存使用量低于该值,HPA 应该开始缩减,直到达到最小副本数量。

 1  kubectl apply \
 2      -f scaling/go-demo-5-api-hpa.yml \
 3      --record

与之前一样,我们将等待几分钟,然后再描述 HPA。

 1  kubectl -n go-demo-5 describe hpa api

输出,仅限于事件消息,如下所示。

...
Events:
... Message
... -------
... New size: 2; reason: Current number of replicas below Spec.MinReplicas
... New size: 4; reason: memory resource utilization (percentage of request) above target
... New size: 5; reason: memory resource utilization (percentage of request) above target
... New size: 3; reason: All metrics below target

正如我们所看到的,它将大小更改为3,因为所有的metricsbelow target

一段时间后,它会再次缩减到两个副本,并停止,因为这是我们在 HPA 定义中设置的限制。

在部署和有状态集中使用副本还是不使用副本?

知道 HorizontalPodAutoscaler(HPA)管理我们应用程序的自动扩展,可能会产生关于副本的问题。我们应该在我们的部署和有状态集中定义它们,还是应该完全依赖 HPA 来管理它们?我们不直接回答这个问题,而是探讨不同的组合,并根据结果定义策略。

首先,让我们看看我们集群中有多少个 Pods。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        27m
api-... 1/1   Running 2        31m
db-0    2/2   Running 0        20m
db-1    2/2   Running 0        20m
db-2    2/2   Running 0        21m

我们可以看到api部署有两个副本,db有三个有状态集的副本。

假设我们想要发布一个新版本的go-demo-5应用程序。我们将使用的定义如下。

 1  cat scaling/go-demo-5-replicas-10.yml

输出,仅限于相关部分,如下所示。

...
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: go-demo-5
spec:
  replicas: 10
... 
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: api
  namespace: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 2
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 80
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 80

需要注意的重要事情是我们的api部署有10个副本,并且我们有 HPA。其他一切都和以前一样。

如果我们应用了那个定义会发生什么?

 1  kubectl apply \
 2    -f scaling/go-demo-5-replicas-10.yml
 3
 4  kubectl -n go-demo-5 get pods

我们应用了新的定义,并从go-demo-5命名空间中检索了所有的 Pods。后一条命令的输出如下。

NAME    READY STATUS            RESTARTS AGE
api-... 1/1   Running           0        9s
api-... 0/1   ContainerCreating 0        9s
api-... 0/1   ContainerCreating 0        9s
api-... 1/1   Running           2        41m
api-... 1/1   Running           0        22s
api-... 0/1   ContainerCreating 0        9s
api-... 0/1   ContainerCreating 0        9s
api-... 1/1   Running           0        9s
api-... 1/1   Running           0        9s
api-... 1/1   Running           0        9s
db-0    2/2   Running           0        31m
db-1    2/2   Running           0        31m
db-2    2/2   Running           0        31m

Kubernetes 遵循我们希望有十个api副本的要求,并创建了八个 Pods(之前我们有两个)。乍一看,HPA 似乎没有任何效果。让我们再次检索 Pods。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        30s
api-... 1/1   Running 2        42m
api-... 1/1   Running 0        43s
api-... 1/1   Running 0        30s
api-... 1/1   Running 0        30s
db-0    2/2   Running 0        31m
db-1    2/2   Running 0        32m
db-2    2/2   Running 0        32m

我们的部署从十个缩减到了五个副本。HPA 检测到副本超过了最大阈值,并相应地采取了行动。但它做了什么?它只是简单地移除了五个副本吗?那不可能,因为那只会有暂时的效果。如果 HPA 移除或添加 Pods,部署也会移除或添加 Pods,两者将互相对抗。Pods 的数量将无限波动。相反,HPA 修改了部署。

让我们描述一下api

 1  kubectl -n go-demo-5 \
 2    describe deployment api

输出,仅限于相关部分,如下所示。

...
Replicas: 5 desired | 5 updated | 5 total | 5 available | 0 unavailable
...
Events:
... Message
... -------
...
... Scaled up replica set api-5bbfd85577 to 10
... Scaled down replica set api-5bbfd85577 to 5

副本的数量设置为5 desired。HPA 修改了我们的部署。我们可以通过事件消息更好地观察到这一点。倒数第二条消息表明副本的数量被扩展到10,而最后一条消息表明它被缩减到5。前者是我们通过应用新的部署来执行滚动更新的结果,而后者是由 HPA 修改部署并改变其副本数量产生的。

到目前为止,我们观察到 HPA 修改了我们的部署。无论我们在部署(或 StatefulSets)中定义了多少副本,HPA 都会更改它以适应自己的阈值和计算。换句话说,当我们更新部署时,副本的数量将暂时更改为我们定义的任何内容,然后在几分钟后再次被 HPA 修改。这种行为是不可接受的。

如果 HPA 更改了副本的数量,通常会有很好的理由。将该数字重置为部署(或 StatetifulSet)中设置的任何数字可能会产生严重的副作用。

假设我们在部署中定义了三个副本,并且 HPA 将其扩展到三十个,因为该应用程序的负载增加了。如果我们apply部署,因为我们想要推出一个新的版本,那么在短暂的时间内,将会有三个副本,而不是三十个。

因此,我们的用户可能会在我们的应用程序中经历较慢的响应时间,或者由于太少的副本提供了太多的流量而导致其他影响。我们必须努力避免这种情况。副本的数量应始终由 HPA 控制。这意味着我们需要改变我们的策略。

如果在部署中指定副本的数量没有产生我们想要的效果,我们可能会干脆将它们全部删除。让我们看看在这种情况下会发生什么。

我们将使用go-demo-5.yml的定义,让我们看看它与我们之前使用的go-demo-5-replicas-10.yml有何不同。

 1  diff \
 2    scaling/go-demo-5-replicas-10.yml \
 3    scaling/go-demo-5.yml

输出显示的唯一区别是,这一次,我们没有指定副本的数量。

让我们应用这个变化,看看会发生什么。

 1  kubectl apply \
 2    -f scaling/go-demo-5.yml
 3
 4  kubectl -n go-demo-5 \
 5    describe deployment api

后一条命令的输出,仅限于相关部分,如下所示。

...
Replicas: 1 desired | 5 updated | 5 total | 5 available | 0 unavailable
...
Events:
... Message
... -------
...
... Scaled down replica set api-5bbfd85577 to 5
... Scaled down replica set api-5bbfd85577 to 1

应用部署而没有副本导致1 desired。当然,HPA 很快会将其扩展到2(其最小值),但我们仍然未能实现我们的使命,即始终保持 HPA 定义的副本数量。

我们还能做什么?无论我们是使用副本定义还是不使用副本定义我们的部署,结果都是一样的。应用部署总是会取消 HPA 的效果,即使我们没有指定副本

实际上,这个说法是不正确的。如果我们知道整个过程是如何工作的,我们可以实现期望的行为而不需要副本

如果为部署定义了副本,那么每次我们应用一个定义时都会使用它。如果我们通过删除副本来更改定义,部署将认为我们想要一个副本,而不是之前的副本数量。但是,如果我们从未指定副本的数量,它们将完全由 HPA 控制。

让我们来测试一下。

 1  kubectl delete -f scaling/go-demo-5.yml

我们删除了与go-demo-5应用程序相关的所有内容。现在,让我们测试一下,如果从一开始就没有定义副本,部署会如何行为。

 1  kubectl apply \
 2    -f scaling/go-demo-5.yml
 3
 4  kubectl -n go-demo-5 \
 5    describe deployment api

后一条命令的输出,仅限于相关部分,如下所示。

...
Replicas: 1 desired | 1 updated | 1 total | 0 available | 1 unavailable
...

看起来我们失败了。部署确实将副本的数量设置为1。但是,您看不到的是副本在内部没有定义。

然而,几分钟后,我们的部署将被 HPA 扩展到两个副本。这是预期的行为,但我们将确认一下。

 1  kubectl -n go-demo-5 \
 2    describe deployment api

您应该从输出中看到副本的数量已经被(由 HPA)更改为2

现在是最终测试。如果我们发布一个新版本的部署,它会缩减到1个副本,还是会保持在2个副本?

我们将应用一个新的定义。与当前运行的定义相比,唯一的区别在于镜像的标签。这样我们将确保部署确实被更新。

 1  kubectl apply \
 2    -f scaling/go-demo-5-2-5.yml
 3
 4  kubectl -n go-demo-5 \
 5    describe deployment api

后一条命令的输出,仅限于相关部分,如下所示。

...
Replicas: 2 desired | 1 updated | 3 total | 2 available | 1 unavailable
...
Events:
... Message
... -------
... Scaled up replica set api-5bbfd85577 to 1
... Scaled up replica set api-5bbfd85577 to 2
... Scaled up replica set api-745bc9fc6d to 1

我们可以看到,由 HPA 设置的副本数量得到了保留。

如果您在“事件”中看到副本的数量被缩减为1,不要惊慌。那是部署启动的第二个 ReplicaSet。您可以通过观察 ReplicaSet 的名称来看到这一点。部署正在通过搅动两个 ReplicaSet 来进行滚动更新,以尝试在没有停机时间的情况下推出新版本。这与自动扩展无关,我假设您已经知道滚动更新是如何工作的。如果您不知道,您知道在哪里学习它。

现在出现了关键问题。在部署和有状态集中,我们应该如何定义副本?

如果您计划在部署或 StatefulSet 中使用 HPA,请不要声明副本。如果这样做,每次滚动更新都会暂时取消 HPA 的效果。仅为不与 HPA 一起使用的资源定义副本。

现在呢?

我们探讨了扩展部署和 StatefulSets 的最简单方法。这很简单,因为这个机制已经内置在 Kubernetes 中。我们所要做的就是定义一个具有目标内存和 CPU 的 HorizontalPodAutoscaler。虽然这种自动缩放的方法通常被使用,但通常是不够的。并非所有应用程序在压力下都会增加内存或 CPU 使用率。即使它们这样做了,这两个指标可能还不够。

在接下来的章节中,我们将探讨如何扩展 HorizontalPodAutoscaler 以使用自定义的指标来源。现在,我们将销毁我们创建的内容,并开始下一章。

如果您计划保持集群运行,请执行以下命令以删除我们创建的资源。

 1  # If NOT GKE or AKS
 2  helm delete metrics-server --purge
 3
 4  kubectl delete ns go-demo-5

否则,请删除整个集群,如果您只是为了本书的目的而创建它,并且不打算立即深入下一章。

在您离开之前,您可能希望复习本章的要点。

  • 水平 Pod 自动缩放器的唯一功能是自动调整部署、StatefulSet 或其他一些类型的资源中 Pod 的数量。它通过观察 Pod 的 CPU 和内存消耗,并在它们达到预定义的阈值时采取行动来实现这一点。

  • Metrics Server 收集有关节点和 Pod 使用的资源(内存和 CPU)的信息。

  • Metrics Server 定期从运行在节点上的 Kubeletes 获取指标。

  • 如果副本的数量是静态的,并且您没有打算随时间缩放(或反向缩放)您的应用程序,请将replicas作为部署或 StatefulSet 定义的一部分。另一方面,如果您计划根据内存、CPU 或其他指标更改副本的数量,请改用 HorizontalPodAutoscaler 资源。

  • 如果为部署定义了replicas,那么每次我们apply一个定义时都会使用它。如果我们通过删除replicas来更改定义,部署将认为我们想要一个,而不是我们之前拥有的副本数量。但是,如果我们从未指定replicas的数量,它们将完全由 HPA 控制。

  • 如果您计划在部署或 StatefulSet 中使用 HPA,请不要声明replicas。如果这样做,每次滚动更新都会暂时取消 HPA 的效果。仅为不与 HPA 一起使用的资源定义replicas

第二章:Kubernetes 集群的自动缩放节点

我可以说我并没有完全享受与人类一起工作吗?我发现他们的不合逻辑和愚蠢的情绪是一个不断的刺激。

  • 斯波克

使用HorizontalPodAutoscalerHPA)是使系统具有弹性、容错和高可用性的最关键方面之一。然而,如果没有可用资源的节点,它就没有用处。当 Kubernetes 无法调度新的 Pod 时,因为没有足够的可用内存或 CPU,新的 Pod 将无法调度并处于挂起状态。如果我们不增加集群的容量,挂起的 Pod 可能会无限期地保持在那种状态。更复杂的是,Kubernetes 可能会开始删除其他 Pod,以为那些处于挂起状态的 Pod 腾出空间。你可能已经猜到,这可能会导致比我们的应用程序没有足够的副本来满足需求的问题更严重的问题。

Kubernetes 通过 Cluster Autoscaler 解决了节点扩展的问题。

Cluster Autoscaler 只有一个目的,那就是通过添加或删除工作节点来调整集群的大小。当 Pod 由于资源不足而无法调度时,它会添加新节点。同样,当节点在一段时间内未被充分利用,并且在该节点上运行的 Pod 可以在其他地方重新调度时,它会删除节点。

Cluster Autoscaler 背后的逻辑很容易理解。我们还没有看到它是否也很容易使用。

让我们创建一个集群(除非您已经有一个),并为其准备自动缩放。

创建一个集群

我们将继续使用vfarcic/k8s-specsgithub.com/vfarcic/k8s-specs)存储库中的定义。为了安全起见,我们将首先拉取最新版本。

本章中的所有命令都可以在02-ca.shgist.github.com/vfarcic/a6b2a5132aad6ca05b8ff5033c61a88f)Gist 中找到。

 1  cd k8s-specs
 2
 3  git pull

接下来,我们需要一个集群。请使用下面的 Gists 作为灵感来创建一个新的集群,或者验证您已经满足所有要求。

AKS 用户注意:在撰写本文时(2018 年 10 月),Cluster Autoscaler 在Azure Kubernetes ServiceAKS)中并不总是有效。请参阅在 AKS 中设置 Cluster Autoscaler部分以获取更多信息和设置说明的链接。

当检查 Gists 时,你会注意到一些事情。首先,Docker for Desktop 和 minikube 都不在其中。它们都是无法扩展的单节点集群。我们需要在一个可以根据需求添加和删除节点的地方运行集群。我们将不得不使用云供应商之一(例如 AWS、Azure、GCP)。这并不意味着我们不能在本地集群上扩展。

我们可以,但这取决于我们使用的供应商。有些供应商有解决方案,而其他供应商没有。为简单起见,我们将坚持使用三大云供应商之一。请在Google Kubernetes EngineGKE)、亚马逊弹性容器服务EKS)或Azure Kubernetes 服务AKS)之间进行选择。如果你不确定选择哪一个,我建议选择 GKE,因为它是最稳定和功能丰富的托管 Kubernetes 集群。

你还会注意到,GKE 和 AKS 的 Gists 与上一章相同,而 EKS 发生了变化。正如你已经知道的那样,前者已经内置了 Metrics Server。EKS 没有,所以我复制了我们之前使用的 Gist,并添加了安装 Metrics Server 的说明。也许在这一章中我们不需要它,但以后会经常用到,我希望你习惯随时拥有它。

如果你更喜欢在本地运行示例,你可能会因为我们在本章中不使用本地集群而感到沮丧。不要绝望。成本将被保持在最低水平(总共可能只有几美元),我们将在下一章回到本地集群(除非你选择留在云端)。

现在我们在 GKE、EKS 或 AKS 中有了一个集群,我们的下一步是启用集群自动扩展。

设置集群自动扩展

在开始使用之前,我们可能需要安装集群自动缩放器。我说“可能”,而不是说“必须”,因为某些 Kubernetes 版本确实预先配置了集群自动缩放器,而其他版本则没有。我们将逐个讨论“三大”托管 Kubernetes 集群。您可以选择探索它们三个,或者直接跳转到您喜欢的一个。作为学习经验,我认为体验在所有三个提供商中运行 Kubernetes 是有益的。尽管如此,这可能不是您的观点,您可能更喜欢只使用一个。选择权在您手中。

在 GKE 中设置集群自动缩放器

这将是有史以来最短的部分。如果在创建集群时指定了--enable-autoscaling参数,则在 GKE 中无需进行任何操作。它已经预先配置并准备好了集群自动缩放器。

在 EKS 中设置集群自动缩放器

与 GKE 不同,EKS 不带有集群自动缩放器。我们将不得不自己配置它。我们需要向专用于工作节点的 Autoscaling Group 添加一些标签,为我们正在使用的角色添加额外的权限,并安装集群自动缩放器。

让我们开始吧。

我们将向专用于工作节点的 Autoscaling Group 添加一些标签。为此,我们需要发现组的名称。由于我们使用eksctl创建了集群,名称遵循一种模式,我们可以使用该模式来过滤结果。另一方面,如果您在没有使用 eksctl 的情况下创建了 EKS 集群,逻辑应该与接下来的逻辑相同,尽管命令可能略有不同。

首先,我们将检索 AWS Autoscaling Groups 的列表,并使用jq过滤结果,以便只返回匹配组的名称。

 1  export NAME=devops25
 2
 3  ASG_NAME=$(aws autoscaling \
 4      describe-auto-scaling-groups \
 5      | jq -r ".AutoScalingGroups[] \
 6      | select(.AutoScalingGroupName \
 7      | startswith(\"eksctl-$NAME-nodegroup\")) \
 8      .AutoScalingGroupName")
 9
10 echo $ASG_NAME

后一个命令的输出应该类似于接下来的输出。

eksctl-devops25-nodegroup-0-NodeGroup-1KWSL5SEH9L1Y

我们将集群的名称存储在环境变量NAME中。然后,我们检索了所有组的列表,并使用jq过滤输出,以便只返回名称以eksctl-$NAME-nodegroup开头的组。最后,相同的jq命令检索了AutoScalingGroupName字段,并将其存储在环境变量ASG_NAME中。最后一个命令输出了组名,以便我们可以确认(视觉上)它看起来是否正确。

接下来,我们将向组添加一些标记。Kubernetes Cluster Autoscaler 将与具有k8s.io/cluster-autoscaler/enabledkubernetes.io/cluster/[NAME_OF_THE_CLUSTER]标记的组一起工作。因此,我们只需添加这些标记,让 Kubernetes 知道要使用哪个组。

 1  aws autoscaling \
 2      create-or-update-tags \
 3      --tags \
 4      ResourceId=$ASG_NAME,ResourceType=auto-scaling-group,Key=k8s.io/
    clusterautoscaler/enabled,Value=true,PropagateAtLaunch=true \
 5      ResourceId=$ASG_NAME,ResourceType=auto-scaling-
    group,Key=kubernetes.io/cluster/$NAME,Value=true,PropagateAtLaunch=true

我们在 AWS 中需要做的最后一项更改是向通过 eksctl 创建的角色添加一些额外的权限。与自动缩放组一样,我们不知道角色的名称,但我们知道用于创建它的模式。因此,在添加新策略之前,我们将检索角色的名称。

 1  IAM_ROLE=$(aws iam list-roles \
 2      | jq -r ".Roles[] \
 3      | select(.RoleName \
 4      | startswith(\"eksctl-$NAME-nodegroup-0-NodeInstanceRole\")) \
 5      .RoleName")
 6  
 7  echo $IAM_ROLE

后一条命令的输出应该类似于接下来的输出。

eksctl-devops25-nodegroup-0-NodeInstanceRole-UU6CKXYESUES

我们列出了所有角色,并使用jq过滤输出,以便只返回名称以eksctl-$NAME-nodegroup-0-NodeInstanceRole开头的角色。过滤角色后,我们检索了RoleName并将其存储在环境变量IAM_ROLE中。

接下来,我们需要描述新策略的 JSON。我已经准备好了,让我们快速看一下。

 1  cat scaling/eks-autoscaling-policy.json

输出如下。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeAutoScalingInstances",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:DescribeTags",
        "autoscaling:SetDesiredCapacity",
        "autoscaling:TerminateInstanceInAutoScalingGroup"
      ],
      "Resource": "*"
    }
  ]
}

如果你熟悉 AWS(我希望你是),那个策略应该很简单。它允许与autoscaling相关的一些额外操作。

最后,我们可以将新策略put到角色中。

 1  aws iam put-role-policy \
 2      --role-name $IAM_ROLE \
 3      --policy-name $NAME-AutoScaling \
 4      --policy-document file://scaling/eks-autoscaling-policy.json

现在我们已经向自动缩放组添加了所需的标记,并创建了额外的权限,允许 Kubernetes 与该组进行交互,我们可以安装 Cluster Autoscaler Helm Chart。

 1  helm install stable/cluster-autoscaler \
 2      --name aws-cluster-autoscaler \
 3      --namespace kube-system \
 4      --set autoDiscovery.clusterName=$NAME \
 5      --set awsRegion=$AWS_DEFAULT_REGION \
 6      --set sslCertPath=/etc/kubernetes/pki/ca.crt \
 7      --set rbac.create=true
 8
9  kubectl -n kube-system \
10      rollout status \
11      deployment aws-cluster-autoscaler

一旦部署完成,自动缩放器应该完全可用。

在 AKS 中设置 Cluster Autoscaler

在撰写本文时(2018 年 10 月),Cluster Autoscaler 在 AKS 中无法正常工作。至少,不总是。它仍处于测试阶段,我暂时不能推荐。希望它很快就能完全运行并保持稳定。一旦发生这种情况,我将使用 AKS 特定的说明更新本章。如果你感到有冒险精神,或者你致力于 Azure,请按照Azure Kubernetes Service(AKS)上的 Cluster Autoscaler - 预览docs.microsoft.com/en-in/azure/aks/cluster-autoscaler)文章中的说明。如果它有效,你应该能够按照本章的其余部分进行操作。

扩大集群

我们的目标是扩展集群的节点,以满足 Pod 的需求。我们不仅希望在需要额外容量时增加工作节点的数量,而且在它们被闲置时也要删除它们。现在,我们将专注于前者,并在之后探索后者。

让我们首先看一下集群中有多少个节点。

 1  kubectl get nodes

来自 GKE 的输出如下。

NAME             STATUS ROLES  AGE   VERSION
gke-devops25-... Ready  <none> 5m27s v1.9.7-gke.6
gke-devops25-... Ready  <none> 5m28s v1.9.7-gke.6
gke-devops25-... Ready  <none> 5m24s v1.9.7-gke.6

在您的情况下,节点的数量可能会有所不同。这并不重要。重要的是要记住您现在有多少个节点,因为这个数字很快就会改变。

在我们推出go-demo-5应用程序之前,让我们先看一下它的定义。

 1  cat scaling/go-demo-5-many.yml

输出内容,仅限于相关部分,如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: go-demo-5
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: api
        ...
        resources:
          limits:
            memory: 1Gi
            cpu: 0.1
          requests:
            memory: 500Mi
            cpu: 0.01
...
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: api
  namespace: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 15
  maxReplicas: 30
  ...

在这种情况下,我们即将应用的定义中唯一重要的部分是与api部署连接的 HPA。它的最小副本数是15。假设每个api容器请求 500 MB RAM,那么十五个副本(7.5 GB RAM)应该超出了我们的集群可以承受的范围,假设它是使用其中一个 Gists 创建的。否则,您可能需要增加最小副本数。

让我们应用这个定义并看一下 HPA。

 1  kubectl apply \
 2      -f scaling/go-demo-5-many.yml \
 3      --record
 4
 5  kubectl -n go-demo-5 get hpa

后一条命令的输出如下。

NAME   REFERENCE        TARGETS                        MINPODS   MAXPODS   REPLICAS   AGE
api    Deployment/api   <unknown>/80%, <unknown>/80%   15        30        1          38s
db     StatefulSet/db   <unknown>/80%, <unknown>/80%   3         5         1          40s

无论目标是否仍然是未知,它们很快就会被计算出来,但我们现在不关心它们。重要的是api HPA 将会将部署扩展至至少15个副本。

接下来,我们需要等待几秒钟,然后再看一下go-demo-5命名空间中的 Pod。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS            RESTARTS AGE
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   Pending           0        2s
api-... 0/1   Pending           0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 1        32s
api-... 0/1   Pending           0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   Pending           0        2s
api-... 0/1   ContainerCreating 0        2s
api-... 0/1   ContainerCreating 0        2s
db-0    2/2   Running           0        34s
db-1    0/2   ContainerCreating 0        34s

我们可以看到一些api Pod 正在被创建,而其他一些则是挂起的。Pod 进入挂起状态可能有很多原因。

在我们的情况下,没有足够的可用资源来托管所有的 Pod。

图 2-1:无法调度(挂起)的 Pod 正在等待集群容量增加

让我们看看集群自动缩放器是否有助于解决我们的容量不足问题。我们将探索包含集群自动缩放器状态的 ConfigMap。

 1  kubectl -n kube-system get cm \
 2      cluster-autoscaler-status \
 3      -o yaml

输出内容太多,无法完整呈现,所以我们将专注于重要的部分。

apiVersion: v1
data:
  status: |+
    Cluster-autoscaler status at 2018-10-03 ...
    Cluster-wide:
      ...
      ScaleUp: InProgress (ready=3 registered=3)
    ... 
    NodeGroups:
      Name:    ...gke-devops25-default-pool-ce277413-grp
      ...
      ScaleUp: InProgress (ready=1 cloudProviderTarget=2)
               ...

状态分为两个部分:整个集群节点组。整个集群状态的ScaleUp部分显示缩放正在进行中。目前有3个就绪节点。

如果我们移动到NodeGroups,我们会注意到每个托管我们节点的组都有一个。在 AWS 中,这些组映射到自动缩放组,在谷歌的情况下映射到实例组,在 Azure 中映射到自动缩放。配置中的一个NodeGroups具有ScaleUp部分InProgress。在该组内,1个节点是readycloudProviderTarget值应设置为高于ready节点数量的数字,我们可以得出结论,集群自动缩放器已经增加了该组中所需的节点数量。

根据提供商的不同,您可能会看到三个组(GKE)或一个(EKS)节点组。这取决于每个提供商如何在内部组织其节点组。

现在我们知道集群自动缩放器正在进行节点扩展,我们可以探索是什么触发了该操作。

让我们描述api Pod 并检索它们的事件。由于我们只想要与cluster-autoscaler相关的事件,我们将使用grep来限制输出。

 1  kubectl -n go-demo-5 \
 2      describe pods \
 3      -l app=api \
 4      | grep cluster-autoscaler

在 GKE 上的输出如下。

  Normal TriggeredScaleUp 85s cluster-autoscaler pod triggered scale-up: [{... 1->2 (max: 3)}]
  Normal TriggeredScaleUp 86s cluster-autoscaler pod triggered scale-up: [{... 1->2 (max: 3)}]
  Normal TriggeredScaleUp 87s cluster-autoscaler pod triggered scale-up: [{... 1->2 (max: 3)}]
  Normal TriggeredScaleUp 88s cluster-autoscaler pod triggered scale-up: [{... 1->2 (max: 3)}]

我们可以看到几个 Pod 触发了scale-up事件。这些是处于挂起状态的 Pod。这并不意味着每个触发都创建了一个新节点。集群自动缩放器足够智能,知道不应该为每个触发创建新节点,但在这种情况下,一个或两个节点(取决于缺少的容量)应该足够。如果证明这是错误的,它将在一段时间后再次扩展。

让我们检索构成集群的节点,看看是否有任何变化。

 1  kubectl get nodes

输出如下。

NAME                                     STATUS     ROLES    AGE     VERSION
gke-devops25-default-pool-7d4b99ad-...   Ready      <none>   2m45s   v1.9.7-gke.6
gke-devops25-default-pool-cb207043-...   Ready      <none>   2m45s   v1.9.7-gke.6
gke-devops25-default-pool-ce277413-...   NotReady   <none>   12s     v1.9.7-gke.6
gke-devops25-default-pool-ce277413-...   Ready      <none>   2m48s   v1.9.7-gke.6

我们可以看到一个新的工作节点被添加到集群中。它还没有准备好,所以我们需要等待一段时间,直到它完全可操作。

请注意,新节点的数量取决于托管所有 Pod 所需的容量。您可能会看到一个、两个或更多新节点。

图 2-2:集群自动缩放器扩展节点的过程

现在,让我们看看我们的 Pod 发生了什么。记住,上次我们检查它们时,有相当多的 Pod 处于挂起状态。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 1        75s
api-... 1/1   Running 0        75s
api-... 1/1   Running 0        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 3        105s
api-... 1/1   Running 0        75s
api-... 1/1   Running 0        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 0        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 0        75s
api-... 1/1   Running 1        75s
api-... 1/1   Running 0        75s
db-0    2/2   Running 0        107s
db-1    2/2   Running 0        67s
db-2    2/2   Running 0        28s

集群自动缩放器增加了节点组(例如,AWS 中的自动缩放组)中所需的节点数量,从而创建了一个新节点。一旦调度程序注意到集群容量的增加,它就会将待定的 Pod 调度到新节点中。在几分钟内,我们的集群扩展了,所有缩放的 Pod 都在运行。

图 2-3:通过节点组创建新节点和挂起 Pod 的重新调度

那么,集群自动缩放器在何时决定扩大节点的规则是什么?

节点规模扩大的规则

集群自动缩放器通过对 Kube API 进行监视来监视 Pod。它每 10 秒检查一次是否有任何无法调度的 Pod(可通过--scan-interval标志进行配置)。在这种情况下,当 Kubernetes 调度程序无法找到可以容纳它的节点时,Pod 是无法调度的。例如,一个 Pod 可以请求比任何工作节点上可用的内存更多的内存。

集群自动缩放器假设集群运行在某种节点组之上。例如,在 AWS 的情况下,这些组是自动缩放组ASGs)。当需要额外的节点时,集群自动缩放器通过增加节点组的大小来创建一个新节点。

集群自动缩放器假设请求的节点将在 15 分钟内出现(可通过--max-node-provision-time标志进行配置)。如果该时间段到期,新节点未注册,它将尝试扩展不同的组,如果 Pod 仍处于挂起状态。它还将在 15 分钟后删除未注册的节点(可通过--unregistered-node-removal-time标志进行配置)。

接下来,我们将探讨如何缩小集群。

缩小集群

扩大集群以满足需求是必不可少的,因为它允许我们托管我们需要满足(部分)SLA 的所有副本。当需求下降,我们的节点变得未充分利用时,我们应该缩小规模。鉴于我们的用户不会因为集群中有太多硬件而遇到问题,这并非必要。然而,如果我们要减少开支,我们不应该有未充分利用的节点。未使用的节点会导致浪费。这在所有情况下都是正确的,特别是在云中运行并且只支付我们使用的资源的情况下。即使在本地,我们已经购买了硬件,缩小规模并释放资源以便其他集群使用是必不可少的。

我们将通过应用一个新的定义来模拟需求下降,这将重新定义 HPAs 的阈值为2(最小)和5(最大)。

 1  kubectl apply \
 2      -f scaling/go-demo-5.yml \
 3      --record
 4
 5  kubectl -n go-demo-5 get hpa

后一条命令的输出如下。

NAME REFERENCE      TARGETS          MINPODS MAXPODS REPLICAS AGE
api  Deployment/api 0%/80%, 0%/80%   2       5       15       2m56s
db   StatefulSet/db 56%/80%, 10%/80% 3       5       3        2m57s

我们可以看到api HPA 的最小和最大值已经改变为25。当前副本的数量仍然是15,但很快会降到5。HPA 已经改变了部署的副本,所以让我们等待它的部署完成,然后再看一下 Pods。

 1  kubectl -n go-demo-5 rollout status \
 2      deployment api
 3
 4  kubectl -n go-demo-5 get pods

后一个命令的输出如下。

NAME    READY STATUS  RESTARTS AGE
api-... 1/1   Running 0        104s
api-... 1/1   Running 0        104s
api-... 1/1   Running 0        104s
api-... 1/1   Running 0        94s
api-... 1/1   Running 0        104s
db-0    2/2   Running 0        4m37s
db-1    2/2   Running 0        3m57s
db-2    2/2   Running 0        3m18s

让我们看看nodes发生了什么。

 1  kubectl get nodes

输出显示我们仍然有四个节点(或者在我们缩减部署之前的数字)。

考虑到我们还没有达到只有三个节点的期望状态,我们可能需要再看一下cluster-autoscaler-status ConfigMap。

 1  kubectl -n kube-system \
 2      get configmap \
 3      cluster-autoscaler-status \
 4      -o yaml

输出,仅限于相关部分,如下所示。

apiVersion: v1
data:
  status: |+
    Cluster-autoscaler status at 2018-10-03 ...
    Cluster-wide:
      Health: Healthy (ready=4 ...)
      ...
      ScaleDown: CandidatesPresent (candidates=1)
                 ...
    NodeGroups:
      Name:      ...gke-devops25-default-pool-f4c233dd-grp
      ...
      ScaleDown: CandidatesPresent (candidates=1)
                 LastProbeTime:      2018-10-03 23:06:...
                 LastTransitionTime: 2018-10-03 23:05:...
      ...

如果您的输出不包含ScaleDown: CandidatesPresent,您可能需要等一会儿并重复上一个命令。

如果我们关注整个集群状态的Health部分,所有四个节点仍然是就绪的。

从状态的整个集群部分来看,我们可以看到有一个候选节点进行ScaleDown(在您的情况下可能有更多)。如果我们转到NodeGroups,我们可以观察到其中一个节点组在ScaleDown部分中的CandidatesPresent设置为1(或者在扩展之前的初始值)。

换句话说,其中一个节点是待删除的候选节点。如果它保持这样十分钟,节点将首先被排空,以允许其中运行的 Pods 优雅关闭。之后,通过操纵扩展组来物理移除它。

图 2-4:集群自动缩放的缩减过程

在继续之前,我们应该等待十分钟,所以这是一个很好的机会去喝杯咖啡(或茶)。

现在已经过了足够的时间,我们将再次查看cluster-autoscaler-status ConfigMap。

 1  kubectl -n kube-system \
 2      get configmap \
 3      cluster-autoscaler-status \
 4      -o yaml

输出,仅限于相关部分,如下所示。

apiVersion: v1
data:
  status: |+
    Cluster-autoscaler status at 2018-10-03 23:16:24...
    Cluster-wide:
      Health:    Healthy (ready=3 ... registered=4 ...)
                 ...
      ScaleDown: NoCandidates (candidates=0)
                 ...
    NodeGroups:
      Name:      ...gke-devops25-default-pool-f4c233dd-grp
      Health:    Healthy (ready=1 ... registered=2 ...)
                 ...
      ScaleDown: NoCandidates (candidates=0)
                 ...

从整个集群部分,我们可以看到现在有3个就绪节点,但仍然有4(或更多)已注册。这意味着其中一个节点已经被排空,但仍然没有被销毁。同样,其中一个节点组显示有1个就绪节点,尽管已注册2个(您的数字可能有所不同)。

从 Kubernetes 的角度来看,我们回到了三个操作节点,尽管第四个节点仍然存在。

现在我们需要再等一会儿,然后检索节点并确认只有三个可用。

 1  kubectl get nodes

来自 GKE 的输出如下。

NAME    STATUS ROLES  AGE VERSION
gke-... Ready  <none> 36m v1.9.7-gke.6
gke-... Ready  <none> 36m v1.9.7-gke.6
gke-... Ready  <none> 36m v1.9.7-gke.6

我们可以看到节点已被移除,我们已经从过去的经验中知道,Kube Scheduler 将那个节点中的 Pod 移动到仍在运行的节点中。现在您已经经历了节点的缩减,我们将探讨管理该过程的规则。

控制节点缩减的规则

集群自动缩放器每 10 秒迭代一次(可通过--scan-interval标志进行配置)。如果不满足扩展的条件,它会检查是否有不需要的节点。

当满足以下所有条件时,它将考虑将节点标记为可移除。

  • 在节点上运行的所有 Pod 的 CPU 和内存请求总和小于节点可分配资源的 50%(可通过--scale-down-utilization-threshold标志进行配置)。

  • 所有在节点上运行的 Pod 都可以移动到其他节点。例外情况是那些在所有节点上运行的 Pod,比如通过 DaemonSets 创建的 Pod。

当满足以下条件之一时,Pod 可能不符合重新调度到不同节点的条件。

  • 具有亲和性或反亲和性规则将其与特定节点绑定的 Pod。

  • 使用本地存储的 Pod。

  • 直接创建的 Pod,而不是通过部署、有状态集、作业或副本集等控制器创建的 Pod。

所有这些规则归结为一个简单的规则。如果一个节点包含一个不能安全驱逐的 Pod,那么它就不符合移除的条件。

接下来,我们应该谈谈集群扩展的边界。

我们是否可以扩展得太多或将节点缩减到零?

如果让集群自动缩放器在不定义任何阈值的情况下进行"魔术",我们的集群或钱包可能会面临风险。

例如,我们可能会错误配置 HPA,导致将部署或有状态集扩展到大量副本。结果,集群自动缩放器可能会向集群添加过多的节点。因此,我们可能会支付数百个节点的费用,尽管我们实际上需要的要少得多。幸运的是,AWS、Azure 和 GCP 限制了我们可以拥有的节点数量,因此我们无法无限扩展。尽管如此,我们也不应允许集群自动缩放器超出一些限制。

同样,集群自动缩放器可能会缩减到太少的节点。拥有零个节点几乎是不可能的,因为这意味着我们在集群中没有 Pod。尽管如此,我们应该保持健康的最小节点数量,即使有时会被低效利用。

节点的合理最小数量是三个。这样,我们在该地区的每个区域(数据中心)都有一个工作节点。正如您已经知道的,Kubernetes 需要三个带有主节点的区域来维持法定人数。在某些情况下,特别是在本地,我们可能只有一个地理上相邻的延迟较低的数据中心。在这种情况下,一个区域(数据中心)总比没有好。但是,在云服务提供商的情况下,三个区域是推荐的分布,并且在每个区域至少有一个工作节点是有意义的。如果我们使用块存储,这一点尤为重要。

根据其性质,块存储(例如 AWS 中的 EBS、GCP 中的持久磁盘和 Azure 中的块 Blob)无法从一个区域移动到另一个区域。这意味着我们必须在每个区域都有一个工作节点,以便(很可能)总是有一个与存储在同一区域的位置。当然,如果我们不使用块存储,那么这个论点就站不住脚了。

那么工作节点的最大数量呢?嗯,这取决于不同的用例。您不必永远坚持相同的最大值。它可以随着时间的推移而改变。

作为一个经验法则,我建议将最大值设为实际节点数量的两倍。但是,不要太认真对待这个规则。这确实取决于您的集群大小。如果您只有三个工作节点,那么最大尺寸可能是九个(三倍)。另一方面,如果您有数百甚至数千个节点,将该数字加倍作为最大值就没有意义。那将太多了。只需确保节点的最大数量反映了需求的潜在增长。

无论如何,我相信您会弄清楚您的工作节点的最小和最大数量应该是多少。如果您犯了错误,可以随后更正。更重要的是如何定义这些阈值。

幸运的是,在 EKS、GKE 和 AKS 中设置最小和最大值很容易。对于 EKS,如果您使用eksctl来创建集群,我们只需在eksctl create cluster命令中添加--nodes-min--nodes-max参数。GKE 遵循类似的逻辑,使用gcloud container clusters create命令的--min-nodes--max-nodes参数。如果其中一个是您的首选项,那么如果您遵循了 Gists,您已经使用了这些参数。即使您忘记指定它们,您也可以随时修改自动缩放组(AWS)或实例组(GCP),因为实际应用限制的地方就在那里。

Azure 采取了稍微不同的方法。我们直接在cluster-autoscaler部署中定义其限制,并且可以通过应用新的定义来更改它们。

在 GKE、EKS 和 AKS 中比较的集群自动缩放器

集群自动缩放器是不同托管 Kubernetes 服务提供商之间差异的一个主要例子。我们将使用它来比较三个主要的 Kubernetes 即服务提供商。

我将把供应商之间的比较限制在与集群自动缩放相关的主题上。

对于那些可以使用谷歌来托管他们的集群的人来说,GKE 是一个不言而喻的选择。它是最成熟和功能丰富的平台。他们比其他人早很久就开始了Google Kubernetes EngineGKE)。当我们将他们的领先优势与他们是 Kubernetes 的主要贡献者并且因此拥有最丰富经验这一事实结合起来时,他们的产品远远超过其他人并不足为奇。

在使用 GKE 时,一切都包含在集群中。这包括集群自动缩放器。我们不必执行任何额外的命令。只要我们在创建集群时指定--enable-autoscaling参数,它就可以直接使用。此外,GKE 比其他提供商更快地启动新节点并将它们加入集群。如果需要扩展集群,新节点将在一分钟内添加。

我会推荐 GKE 的许多其他原因,但现在不是讨论的主题。不过,单单集群自动缩放就足以证明 GKE 是其他人努力追随的解决方案。

亚马逊的弹性容器服务EKS)处于中间位置。集群自动缩放器可以工作,但它并不是内置的。就好像亚马逊认为扩展集群并不重要,所以将其作为一个可选的附加组件。

与 GKE 和 AKS 相比,EKS 的安装过于复杂,但多亏了来自 Weaveworks 的 eksctl(eksctl.io/),我们解决了这个问题。不过,eksctl 还有很多需要改进的地方。例如,我们无法使用它来升级我们的集群。

我提到 eksctl 是在自动缩放设置的上下文中。

我不能说在 EKS 中设置集群自动缩放器很难。并不是。然而,它并不像应该的那么简单。我们需要给自动缩放组打标签,为角色添加额外的权限,并安装集群自动缩放器。这并不多。然而,这些步骤比应该的复杂得多。我们可以拿 GKE 来比较。谷歌明白自动缩放 Kubernetes 集群是必须的,并提供了一个参数(或者如果你更喜欢 UI,可以选择一个复选框)。而 AWS 则认为自动缩放并不重要,没有给我们那么简单的设置。除了 EKS 中不必要的设置之外,事实上 AWS 最近才添加了扩展所需的内部组件。Metrics Server 只能在 2018 年 9 月之后使用。

我怀疑 AWS 并不急于让 EKS 变得更好,而是把改进留给了 Fargate。如果是这样的话(我们很快就会知道),我会把它称为“隐秘的商业行为”。Kubernetes 拥有所有扩展 Pod 和节点所需的工具,并且它们被设计为可扩展的。选择不将集群自动缩放器作为托管 Kubernetes 服务的一部分是一个很大的缺点。

AKS 有什么好说的呢?我钦佩微软在 Azure 上所做的改进,以及他们对 Kubernetes 的贡献。他们确实意识到了提供一个良好的托管 Kubernetes 的需求。然而,集群自动缩放器仍处于测试阶段。有时它能正常工作,但更多时候却不能。即使它正常工作,速度也很慢。等待新节点加入集群需要耐心等待。

在 AKS 中安装集群自动缩放器所需的步骤有些荒谬。我们需要定义大量参数,而这些参数本应该已经在集群内可用。它应该知道集群的名称,资源组的名称等等。然而,它并不知道。至少在撰写本文时是这样的(2018 年 10 月)。我希望随着时间的推移,这个过程和体验会得到改善。目前来看,就自动缩放的角度来看,AKS 处于队伍的最后。

你可能会说设置的复杂性并不重要。你说得对。重要的是集群自动缩放器的可靠性以及它添加新节点到集群的速度。然而,情况却是一样的。GKE 在可靠性和速度方面处于领先地位。EKS 紧随其后,而 AKS 则落后。

现在呢?

关于集群自动缩放器没有太多要说的了。

我们已经探索了自动缩放 Pod 和节点的基本方法。很快我们将深入探讨更复杂的主题,并探索那些没有“内置”到 Kubernetes 集群中的东西。我们将超越核心项目,并介绍一些新的工具和流程。

如果您不打算立即进入下一章,并且您的集群是可丢弃的(例如,不在裸机上),那么这就是您应该销毁集群的时刻。否则,请删除go-demo-5命名空间,以删除我们在本章中创建的资源。

 1  kubectl delete ns go-demo-5

在您离开之前,您可能希望复习本章的要点。

  • 集群自动缩放器有一个单一的目的,即通过添加或删除工作节点来调整集群的大小。当 Pod 由于资源不足而无法调度时,它会添加新节点。同样,当节点在一段时间内利用不足,并且运行在该节点上的 Pod 可以在其他地方重新调度时,它会消除节点。

  • 集群自动缩放器假设集群正在某种节点组之上运行。例如,在 AWS 的情况下,这些组是自动缩放组(ASG)。当需要额外的节点时,集群自动缩放器通过增加节点组的大小来创建新节点。

  • 当运行在节点上的所有 Pod 的 CPU 和内存请求总和小于节点可分配资源的 50%时,集群将被缩减,并且当运行在节点上的所有 Pod 可以移动到其他节点时(DamonSets 是例外情况)。

第三章:收集和查询指标并发送警报

不充分的事实总是会引发危险。

  • 斯波克

到目前为止,我们已经探讨了如何利用一些 Kubernetes 核心功能。我们使用了 HorizontalPodAutoscaler 和 Cluster Autoscaler。前者依赖于度量服务器,而后者不是基于指标,而是基于调度程序无法将 Pod 放置在现有集群容量内。尽管度量服务器确实提供了一些基本指标,但我们迫切需要更多。

我们必须能够监视我们的集群,而度量服务器并不足够。它包含有限数量的指标,它们保存的时间很短,而且它不允许我们执行除了最简单的查询之外的任何操作。如果我们只依赖度量服务器,我不能说我们是盲目的,但我们受到严重的影响。如果我们不增加收集的指标数量以及它们的保留时间,我们只能对我们的 Kubernetes 集群中发生的情况有一瞥。

能够获取和存储指标本身并不是目标。我们还需要能够查询它们以寻找问题的原因。为此,我们需要指标“丰富”的信息,以及强大的查询语言。

最后,能够找到问题的原因没有多大意义,如果不能首先被通知存在问题。这意味着我们需要一个系统,可以让我们定义警报,当达到一定阈值时,会向我们发送通知,或者在适当时将它们发送到系统的其他部分,可以自动执行解决问题的步骤。

如果我们做到了这一点,我们将更接近于拥有不仅自我修复(Kubernetes 已经做到了),而且还会对变化的条件做出反应的自适应系统。我们甚至可以进一步尝试预测未来会发生“坏事”,并在它们出现之前积极解决它们。

总而言之,我们需要一个工具,或一组工具,可以让我们获取和存储“丰富”的指标,可以让我们查询它们,并且在出现问题时通知我们,甚至更好的是,在问题即将发生时通知我们。

在本章中,我们可能无法构建一个自适应系统,但我们可以尝试创建一个基础。但首先,我们需要一个集群,让我们可以“玩”一些新的工具和概念。

创建一个集群

我们将继续使用vfarcic/k8s-specsgithub.com/vfarcic/k8s-specs)存储库中的定义。为了安全起见,我们将首先拉取最新版本。

本章中的所有命令都在03-monitor.shgist.github.com/vfarcic/718886797a247f2f9ad4002f17e9ebd9)Gist 中可用。

 1  cd k8s-specs
 2
 3  git pull

给 minikube 和 Docker for Desktop 用户的提示:我们需要将内存增加到 3GB。请记住这一点,以防您只是计划浏览与您的 Kubernetes 版本匹配的 Gist。在本章中,我们将需要一些以前不是要求的东西,尽管您可能已经使用过它们。

我们将开始使用 UI,因此我们将需要 NGINX Ingress Controller 来从集群外部路由流量。我们还需要环境变量LB_IP,其中包含我们可以访问工作节点的 IP。我们将用它来配置一些 Ingress 资源。

本章中用于测试示例的 Gists 如下。请按原样使用它们,或者作为创建自己的集群的灵感,或者确认您已有的集群是否符合要求。由于新的要求(Ingress 和LB_IP),所有集群设置的 Gists 都是新的。

给 Docker for Desktop 用户的提示:您会注意到 Gist 末尾的LB_IP=[...]命令。您需要用您集群的 IP 替换[...]。可能找到它最简单的方法是通过ifconfig命令。只需记住它不能是localhost,而是您笔记本电脑的 IP(例如,192.168.0.152)。

Gists 如下。

现在我们有了一个集群,我们需要选择我们将用来实现我们目标的工具。

选择存储和查询指标以及警报的工具

HorizontalPodAutoscaler (HPA)和Cluster Autoscaler (CA)提供了必要但非常基本的机制来扩展我们的 Pods 和集群。

虽然它们可以很好地进行扩展,但它们并不能解决我们在出现问题时需要接收警报的需求,也不能提供足够的信息来找到问题的原因。我们需要通过额外的工具来扩展我们的设置,这些工具将允许我们存储和查询指标,并在出现问题时接收通知。

如果我们专注于可以安装和管理的工具,那么我们对使用什么工具几乎没有疑问。如果我们看一下Cloud Native Computing Foundation (CNCF)项目列表(www.cncf.io/projects/),到目前为止只有两个项目已经毕业(2018 年 10 月)。它们分别是KubernetesPrometheus(prometheus.io/)。考虑到我们正在寻找一个可以存储和查询指标的工具,而 Prometheus 满足了这一需求,选择就很明显了。这并不是说没有其他值得考虑的类似工具。有,但它们都是基于服务的。我们以后可能会探索它们,但现在,我们专注于那些可以在我们的集群内运行的工具。因此,我们将把 Prometheus 加入到混合中,并尝试回答一个简单的问题。Prometheus 是什么?

Prometheus 是一个(某种程度上的)数据库,旨在获取(拉取)和存储高维时间序列数据。

时间序列由指标名称和一组键值对标识。数据既存储在内存中,也存储在磁盘上。前者可以快速检索信息,而后者存在是为了容错。

Prometheus 的查询语言使我们能够轻松找到可用于图表和更重要的警报的数据。它并不试图提供“出色”的可视化体验。为此,它与Grafanagrafana.com/)集成。

与大多数其他类似工具不同,我们不会将数据推送到 Prometheus。或者更准确地说,这不是获取指标的常见方式。相反,Prometheus 是一个基于拉取的系统,定期从导出器中获取指标。我们可以使用许多第三方导出器。但是,在我们的情况下,最关键的导出器已经内置到 Kubernetes 中。Prometheus 可以从一个将信息从 Kube API 转换的导出器中拉取数据。通过它,我们可以获取(几乎)我们可能需要的所有信息。或者至少,这就是大部分信息将来自的地方。

最后,如果我们在出现问题时没有得到通知,将在 Prometheus 中存储的指标没有太大用处。即使我们将 Prometheus 与 Grafana 集成,那也只会为我们提供仪表板。我假设你有更重要的事情要做,而不是盯着五颜六色的图表。因此,我们需要一种方式将来自 Prometheus 的警报发送到 Slack,比如说。幸运的是,Alertmanagerprometheus.io/docs/alerting/alertmanager/)允许我们做到这一点。这是一个由同一个社区维护的独立应用程序。

我们将通过实际操作来看看所有这些部分是如何组合在一起的。所以,让我们开始安装 Prometheus、Alertmanager 和其他一些应用程序。

对 Prometheus 和 Alertmanager 的快速介绍

我们将继续使用 Helm 作为安装机制。Prometheus 的 Helm Chart 是作为官方 Chart 之一进行维护的。您可以在项目的README中找到更多信息(github.com/helm/charts/tree/master/stable/prometheus)。如果您关注配置部分中的变量(github.com/helm/charts/tree/master/stable/prometheus#configuration),您会注意到有很多东西可以调整。我们不会遍历所有变量。您可以查看官方文档。相反,我们将从基本设置开始,并随着我们的需求增加而扩展。

让我们来看看我们将作为起点使用的变量。

 1  cat mon/prom-values-bare.yml

输出如下。

server:
  ingress:
    enabled: true
    annotations:
      ingress.kubernetes.io/ssl-redirect: "false"
      nginx.ingress.kubernetes.io/ssl-redirect: "false"
  resources:
    limits:
      cpu: 100m
      memory: 1000Mi
    requests:
      cpu: 10m
      memory: 500Mi
alertmanager:
  ingress:
    enabled: true
    annotations:
      ingress.kubernetes.io/ssl-redirect: "false"
      nginx.ingress.kubernetes.io/ssl-redirect: "false"
  resources:
    limits:
      cpu: 10m
      memory: 20Mi
    requests:
      cpu: 5m
      memory: 10Mi
kubeStateMetrics:
  resources:
    limits:
      cpu: 10m
      memory: 50Mi
    requests:
      cpu: 5m
      memory: 25Mi
nodeExporter:
  resources:
    limits:
      cpu: 10m
      memory: 20Mi
    requests:
      cpu: 5m
      memory: 10Mi
pushgateway:
  resources:
    limits:
      cpu: 10m
      memory: 20Mi
        requests:
      cpu: 5m
      memory: 10Mi

目前我们所做的一切都是为我们将安装的所有五个应用程序定义资源,以及使用一些注释启用 Ingress,这些注释将确保我们不会被重定向到 HTTPS 版本,因为我们没有我们的临时域的证书。我们将在稍后深入研究将要安装的应用程序。目前,我们将定义 Prometheus 和 Alertmanager UI 的地址。

 1  PROM_ADDR=mon.$LB_IP.nip.io
 2
 3  AM_ADDR=alertmanager.$LB_IP.nip.io

让我们安装图表。

 1  helm install stable/prometheus \
 2      --name prometheus \
 3      --namespace metrics \
 4      --version 7.1.3 \
 5      --set server.ingress.hosts={$PROM_ADDR} \
 6      --set alertmanager.ingress.hosts={$AM_ADDR} \
 7      -f mon/prom-values-bare.yml

我们刚刚执行的命令应该是不言自明的,所以我们将跳转到输出的相关部分。

...
RESOURCES:
==> v1beta1/DaemonSet
NAME                     DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
prometheus-node-exporter 3       3       0     3          0         <none>        3s 
==> v1beta1/Deployment
NAME                          DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
prometheus-alertmanager       1       1       1          0         3s
prometheus-kube-state-metrics 1       1       1          0         3s
prometheus-pushgateway        1       1       1          0         3s
prometheus-server             1       1       1          0         3s
...

我们可以看到,图表安装了一个 DeamonSet 和四个部署。

DeamonSet 是 Node Exporter,它将在集群的每个节点上运行一个 Pod。它提供特定于节点的指标,这些指标将被 Prometheus 拉取。第二个导出器(Kube State Metrics)作为单个副本部署运行。它从 Kube API 获取数据,并将其转换为 Prometheus 友好的格式。这两个将提供我们所需的大部分指标。稍后,我们可能选择使用其他导出器来扩展它们。目前,这两个连同直接从 Kube API 获取的指标应该提供比我们在单个章节中能吸收的更多的指标。

此外,我们有服务器,即 Prometheus 本身。Alertmanager 将警报转发到它们的目的地。最后,还有 Pushgateway,我们可能会在接下来的章节中探索它。

在等待所有这些应用程序变得可操作时,我们可以探索它们之间的流程。

Prometheus 服务器从出口商那里获取数据。在我们的情况下,这些是 Node Exporter 和 Kube State Metrics。这些出口商的工作是从源获取数据并将其转换为 Prometheus 友好的格式。Node Exporter 从节点上挂载的/proc/sys卷获取数据,而 Kube State Metrics 从 Kube API 获取数据。指标在 Prometheus 内部存储。

除了能够查询这些数据,我们还可以定义警报。当警报达到阈值时,它将被转发到充当十字路口的 Alertmanager。

根据其内部规则,它可以将这些警报进一步转发到各种目的地,如 Slack、电子邮件和 HipChat(仅举几例)。

图 3-1:数据流向和从 Prometheus 流向的流程(箭头表示方向)

到目前为止,Prometheus 服务器可能已经推出。我们会确认一下以防万一。

 1  kubectl -n metrics \
 2      rollout status \
 3      deploy prometheus-server

让我们来看看通过prometheus-server部署创建的 Pod 内部有什么。

 1  kubectl -n metrics \
 2      describe deployment \
 3      prometheus-server

输出,仅限于相关部分,如下所示。

  Containers:
   prometheus-server-configmap-reload:
    Image: jimmidyson/configmap-reload:v0.2.2
    ...
   prometheus-server:
    Image: prom/prometheus:v2.4.2
    ...

除了基于prom/prometheus镜像的容器外,我们还从jimmidyson/configmap-reload创建了另一个容器。后者的工作是在我们更改存储在 ConfigMap 中的配置时重新加载 Prometheus。

接下来,我们可能想看一下prometheus-server ConfigMap,因为它存储了 Prometheus 所需的所有配置。

 1  kubectl -n metrics \
 2      describe cm prometheus-server

输出,仅限于相关部分,如下所示。

...
Data
====
alerts:
----
{} 
prometheus.yml:
----
global:
  evaluation_interval: 1m
  scrape_interval: 1m
  scrape_timeout: 10s 
rule_files:
- /etc/config/rules
- /etc/config/alerts
scrape_configs:
- job_name: prometheus
  static_configs:
  - targets:
    - localhost:9090
- bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  job_name: kubernetes-apiservers
  kubernetes_sd_configs:
  - role: endpoints
  relabel_configs:
  - action: keep
    regex: default;kubernetes;https
    source_labels:
    - __meta_kubernetes_namespace
    - __meta_kubernetes_service_name
    - __meta_kubernetes_endpoint_port_name
  scheme: https
  tls_config:
    ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    insecure_skip_verify: true
...

我们可以看到alerts仍然是空的。我们很快会改变这一点。

更下面是prometheus.yml配置,其中scrape_configs占据了大部分空间。我们可以花一个章节的时间来解释当前的配置以及我们可以修改它的方式。我们不会这样做,因为你面前的配置接近疯狂。这是如何使事情变得比应该更复杂的最佳例子。在大多数情况下,您应该保持不变。如果您确实想要玩弄它,请咨询官方文档。

接下来,我们将快速查看 Prometheus 的屏幕。

对于 Windows 用户,Git Bash 可能无法使用open命令。如果是这种情况,请用echo替换open。结果,您将获得应直接在您选择的浏览器中打开的完整地址。

 1  open "http://$PROM_ADDR/config"

配置屏幕反映了我们已经在prometheus-server ConfigMap 中看到的相同信息,所以我们将继续。

接下来,让我们来看看这些目标。

 1  open "http://$PROM_ADDR/targets"

该屏幕包含七个目标,每个目标提供不同的指标。Prometheus 定期从这些目标中拉取数据。

本章中的所有输出和截图都来自 AKS。根据您的 Kubernetes 版本,可能会看到一些差异。您可能会注意到,本章包含的截图比其他章节多得多。尽管看起来可能有点多,但我想确保您可以将您的结果与我的进行比较,因为不可避免地会有一些差异,有时可能会让人感到困惑,如果没有参考(我的截图)的话。图 3-2:Prometheus 的目标屏幕 AKS 用户注意kubernetes-apiservers目标可能是红色的,表示 Prometheus 无法连接到它。这没关系,因为我们不会使用它的指标。minikube 用户注意kubernetes-service-endpoints目标可能有一些红色的来源。没有理由担心。这些是不可访问的,但这不会影响我们的练习。

我们无法从屏幕上找出每个目标提供什么。我们将尝试以与 Prometheus 拉取它们相同的方式查询导出器。

为了做到这一点,我们需要找出可以访问导出器的服务。

 1  kubectl -n metrics get svc

来自 AKS 的输出如下。

NAME                          TYPE      CLUSTER-IP    EXTERNAL-IP PORT(S)  AGE
prometheus-alertmanager       ClusterIP 10.23.245.165 <none>      80/TCP   41d
prometheus-kube-state-metrics ClusterIP None          <none>      80/TCP   41d
prometheus-node-exporter      ClusterIP None          <none>      9100/TCP 41d
prometheus-pushgateway        ClusterIP 10.23.244.47  <none>      9091/TCP 41d
prometheus-server             ClusterIP 10.23.241.182 <none>      80/TCP   41d

我们对prometheus-kube-state-metricsprometheus-node-exporter感兴趣,因为它们提供了访问本章中将使用的导出器的数据。

接下来,我们将创建一个临时 Pod,通过它我们将访问那些服务后面的导出器提供的数据。

 1  kubectl -n metrics run -it test \
 2      --image=appropriate/curl \
 3      --restart=Never \
 4      --rm \
 5      -- prometheus-node-exporter:9100/metrics

我们基于appropriate/curl创建了一个新的 Pod。该镜像只提供curl的单一目的。我们指定prometheus-node-exporter:9100/metrics作为命令,这相当于使用该地址运行curl。结果,输出了大量指标。它们都以相同的“键/值”格式呈现,可选标签用大括号({})括起来。在每个指标的顶部,都有一个HELP条目,解释了其功能以及TYPE(例如,gauge)。其中一个指标如下。

 1  # HELP node_memory_MemTotal_bytes Memory information field
    MemTotal_bytes.
 2  # TYPE node_memory_MemTotal_bytes gauge
 3  node_memory_MemTotal_bytes 3.878477824e+09

我们可以看到它提供了“内存信息字段 MemTotal_bytes”,类型为gauge。在TYPE下面是实际的指标,带有键(node_memory_MemTotal_bytes)和值3.878477824e+09

大多数 Node Exporter 指标都没有标签。因此,我们将不得不在prometheus-kube-state-metrics导出器中寻找一个示例。

 1  kubectl -n metrics run -it test \
 2      --image=appropriate/curl \
 3      --restart=Never \
 4      --rm \
 5      -- prometheus-kube-state-metrics:8080/metrics

正如您所看到的,Kube 状态指标遵循与节点导出器相同的模式。主要区别在于大多数指标都有标签。一个例子如下。

 1  kube_deployment_created{deployment="prometheus-
    server",namespace="metrics"} 1.535566512e+09

该指标表示在metrics命名空间内创建prometheus-server部署的时间。

我会让你更详细地探索这些指标。我们很快将使用其中的许多。

现在,只需记住,通过来自节点导出器、Kube 状态指标以及来自 Kubernetes 本身的指标的组合,我们可以满足大部分需求。或者更准确地说,这些数据提供了大部分基本和常见用例所需的数据。

接下来,我们将查看警报屏幕。

 1  open "http://$PROM_ADDR/alerts"

屏幕是空的。不要绝望。我们将会多次返回到那个屏幕。随着我们的进展,警报将会增加。现在,只需记住那里是您可以找到警报的地方。

最后,我们将打开图形屏幕。

 1  open "http://$PROM_ADDR/graph"

那里是您将花费时间调试通过警报发现的问题的地方。

作为我们的第一个任务,我们将尝试检索有关我们节点的信息。我们将使用kube_node_info,所以让我们看一下它的描述(帮助)和类型。

 1  kubectl -n metrics run -it test \
 2      --image=appropriate/curl \
 3      --restart=Never \
 4      --rm \
 5      -- prometheus-kube-state-metrics:8080/metrics \
 6      | grep "kube_node_info"

输出,仅限于HELPTYPE条目,如下所示。

 1  # HELP kube_node_info Information about a cluster node.
 2  # TYPE kube_node_info gauge
 3  ...

您可能会看到您的结果与我的结果之间的差异。这是正常的,因为我们的集群可能具有不同数量的资源,我的带宽可能不同,等等。在某些情况下,我的警报会触发,而您的不会,或者反之。我会尽力解释我的经验并提供伴随它们的截图。您将不得不将其与您在屏幕上看到的内容进行比较。

现在,让我们尝试在 Prometheus 中使用该指标。

请在表达式字段中输入以下查询。

 1  kube_node_info

点击“执行”按钮以检索kube_node_info指标的值。

与以往章节不同,这个章节的 Gist(03-monitor.sh (gist.github.com/vfarcic/718886797a247f2f9ad4002f17e9ebd9)不仅包含命令,还包含 Prometheus 表达式。它们都被注释了(使用#)。如果您打算从 Gist 中复制并粘贴表达式,请排除注释。每个表达式顶部都有# Prometheus expression的注释,以帮助您识别它。例如,您刚刚执行的表达式在 Gist 中的写法如下。# Prometheus expression # kube_node_info

如果您检查kube_node_infoHELP条目,您会看到它提供了有关集群节点的信息,并且它是一个仪表仪表(prometheus.io/docs/concepts/metric_types/#gauge)是表示单个数值的度量,可以任意上升或下降。

关于节点的信息是有道理的,因为它们的数量可能随时间增加或减少。

Prometheus 的仪表是表示单个数值的度量,可以任意上升或下降。

如果我们关注输出,您会注意到条目的数量与集群中的工作节点数量相同。在这种情况下,值(1)在这种情况下是无用的。另一方面,标签可以提供一些有用的信息。例如,在我的情况下,操作系统(os_image)是Ubuntu 16.04.5 LTS。通过这个例子,我们可以看到我们不仅可以使用度量来计算值(例如,可用内存),还可以一窥系统的具体情况。

图 3-3:Prometheus 的控制台输出 kube_node_info 度量

让我们看看是否可以通过将该度量与 Prometheus 的一个函数结合来获得更有意义的查询。我们将count集群中工作节点的数量。count是 Prometheus 的聚合运算符之一(prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators)。

请执行接下来的表达式。

 1  count(kube_node_info)

输出应该显示集群中工作节点的总数。在我的情况下(AKS),有3个。乍一看,这可能并不是非常有用。您可能认为,即使没有 Prometheus,您也应该知道集群中有多少个节点。但这可能并不正确。其中一个节点可能已经失败,并且没有恢复。如果您在本地运行集群而没有扩展组,这一点尤其正确。或者 Cluster Autoscaler 增加或减少了节点的数量。一切都会随时间而改变,无论是由于故障,人为行为,还是通过自适应的系统。无论波动的原因是什么,当某些情况达到阈值时,我们可能希望得到通知。我们将以节点作为第一个例子。

我们的任务是定义一个警报,如果集群中的节点超过三个或少于一个,将通知我们。我们假设这些是我们的限制,并且我们想知道是由于故障还是集群自动缩放而达到了下限或上限。

我们将看一下 Prometheus Chart 值的新定义。由于定义很大,并且会随着时间增长,所以从现在开始,我们只会关注其中的差异。

 1  diff mon/prom-values-bare.yml \
 2      mon/prom-values-nodes.yml

输出如下。

> serverFiles:
>   alerts:
>     groups:
>     - name: nodes
>       rules:
>       - alert: TooManyNodes
>         expr: count(kube_node_info) > 3
>         for: 15m
>         labels:
>           severity: notify
>         annotations:
>           summary: Cluster increased
>           description: The number of the nodes in the cluster increased
>       - alert: TooFewNodes
>         expr: count(kube_node_info) < 1
>         for: 15m
>         labels:
>           severity: notify
>         annotations:
>           summary: Cluster decreased
>           description: The number of the nodes in the cluster decreased

我们添加了一个新条目serverFiles.alerts。如果您查看 Prometheus 的 Helm 文档,您会发现它允许我们定义警报(因此得名)。在其中,我们使用了“标准”Prometheus 语法来定义警报。

请参阅Alerting Rules documentation (prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) 以获取有关语法的更多信息。

我们只定义了一个名为nodes的规则组。里面有两个rules。第一个规则(TooManyNodes)会在超过15分钟内有超过3个节点时通知我们。另一个规则(TooFewNodes)则相反,会在15分钟内没有节点(<1)时通知我们。这两个规则都有labelsannotations,目前仅用于信息目的。稍后我们会看到它们的真正用途。

让我们升级我们的 Prometheus Chart 并查看新警报的效果。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-nodes.yml

新配置被“发现”并重新加载 Prometheus 需要一些时间。过一会儿,我们可以打开 Prometheus 警报屏幕,检查是否有我们的第一个条目。

从现在开始,我不会(太多)评论需要等待一段时间直到下一个配置传播的需要。如果您在屏幕上看到的与您期望的不一致,请稍等片刻并刷新一下。

 1  open "http://$PROM_ADDR/alerts"

您应该会看到两个警报。

由于没有一个评估为true,所以这两个警报都是绿色的。根据您选择的 Kubernetes 版本,您可能只有一个节点(例如,Docker for Desktop 和 minikube),或者有三个节点(例如,GKE,EKS,AKS)。由于我们的警报检查了我们是否有少于一个或多于三个节点,无论您使用哪种 Kubernetes 版本,都不满足任何条件。

如果您的集群不是通过本章开头提供的 Gists 之一创建的,那么您的集群可能有超过三个节点,并且警报将触发。如果是这种情况,我建议您修改mon/prom-values-nodes.yml文件以调整警报的阈值。图 3-4:Prometheus 的警报屏幕

看到无效的警报很无聊,所以我想向您展示一个触发的警报(变为红色)。为了做到这一点,我们可以向集群添加更多节点(除非您正在使用像 Docker for Desktop 和 minikube 这样的单节点集群)。但是,修改一个警报的表达式会更容易,所以下面我们将这样做。

 1  diff mon/prom-values-nodes.yml \
 2      mon/prom-values-nodes-0.yml

输出如下。

57,58c57,58
< expr: count(kube_node_info) > 3
< for: 15m
---
> expr: count(kube_node_info) > 0
> for: 1m
66c66
< for: 15m
---
> for: 1m

新的定义将TooManyNodes警报的条件更改为如果节点数大于零则触发。我们还修改了for语句,这样在警报触发之前我们不需要等待15分钟。

让我们再次升级 Chart。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-nodes-0.yml

...然后我们将返回到警报屏幕。

 1  open "http://$PROM_ADDR/alerts"

几分钟后(不要忘记刷新屏幕),警报将转为挂起状态,颜色将变为黄色。这意味着警报的条件已经满足(我们确实有超过零个节点),但for时间段尚未到期。

等待一分钟(for时间段的持续时间)并刷新屏幕。警报状态已切换为触发,并且颜色变为红色。Prometheus 发送了我们的第一个警报。

图 3-5:Prometheus 的警报屏幕,其中一个警报触发

警报发送到了哪里?Prometheus Helm Chart 部署了 Alertmanager,并预先配置了 Prometheus 将其警报发送到那里。让我们来看看它的 UI。

 1  open "http://$AM_ADDR"

我们可以看到一个警报已经到达了 Alertmanager。如果我们点击TooManyNodes警报旁边的+信息按钮,我们将看到注释(摘要和描述)以及标签(严重程度)。

图 3-6:Alertmanager UI,其中一个警报已展开

我们可能不会坐在 Alertmanager 前等待问题出现。如果这是我们的目标,我们也可以在 Prometheus 中等待警报。

显示警报确实不是我们拥有 Alertmanager 的原因。它应该接收警报并进一步分发它们。它之所以没有做任何这样的事情,只是因为我们还没有定义它应该用来转发警报的规则。这是我们的下一个任务。

我们将看一下 Prometheus Chart 值的另一个更新。

 1  diff mon/prom-values-nodes-0.yml \
 2      mon/prom-values-nodes-am.yml

输出如下。

71a72,93
> alertmanagerFiles:
>   alertmanager.yml:
>     global: {}
>     route:
>       group_wait: 10s
>       group_interval: 5m
>       receiver: slack
>       repeat_interval: 3h
>       routes:
>       - receiver: slack
>         repeat_interval: 5d
>         match:
>           severity: notify
>           frequency: low
>     receivers:
>     - name: slack
>       slack_configs:
>       - api_url: "https://hooks.slack.com/services/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"
>         send_resolved: true
>         title: "{{ .CommonAnnotations.summary }}"
>         text: "{{ .CommonAnnotations.description }}"
>         title_link: http://my-prometheus.com/alerts

当我们应用该定义时,我们将向 Alertmanager 添加alertmanager.yml文件。如果包含了它应该用来分发警报的规则。route部分包含了将应用于所有不匹配任何一个routes的警报的一般规则。group_wait值使 Alertmanager 在同一组的其他警报到达时等待10秒。这样,我们将避免接收到相同类型的多个警报。

当一组中的第一个警报被发送时,它将在发送同一组的新警报的下一批之前使用group_interval字段(5m)的值。

route部分中的receiver字段定义了警报的默认目的地。这些目的地在下面的receivers部分中定义。在我们的情况下,我们默认将警报发送到slack接收器。

repeat_interval(设置为3h)定义了如果 Alertmanager 继续接收警报,警报将在之后的时间段内重新发送。

routes部分定义了具体的规则。只有当它们都不匹配时,才会使用上面route部分中的规则。routes部分继承自上面的属性,因此只有我们在这个部分中定义的规则会改变。我们将继续将匹配的routes发送到slack,唯一的变化是将repeat_interval3h增加到5d

routes的关键部分是match部分。它定义了用于决定警报是否匹配的过滤器。在我们的情况下,只有那些带有标签severity: notifyfrequency: low的警报才会被视为匹配。

总的来说,带有severity标签设置为notifyfrequency设置为low的警报将每五天重新发送一次。所有其他警报的频率为三小时。

我们 Alertmanager 配置的最后一部分是receivers。我们只有一个名为slack的接收器。在name下面是slack_config。它包含特定于 Slack 的配置。我们可以使用hipchat_configpagerduty_config或任何其他受支持的配置。即使我们的目的地不是其中之一,我们也可以始终退回到webhook_config并向我们选择的工具的 API 发送自定义请求。

有关所有受支持的receivers列表,请参阅Alertmanager 配置页面 (prometheus.io/docs/alerting/configuration/)。

slack_configs部分中,我们有包含来自devops20频道中一个房间令牌的 Slack 地址的api_url

有关如何为您的 Slack 频道生成传入 Webhook 地址的信息,请访问传入 Webhooks页面 (api.slack.com/incoming-webhooks)。

接下来是send_resolved标志。当设置为true时,Alertmanager 将在警报触发时发送通知,也会在导致问题解决时发送通知。

我们使用summary注释作为消息的title,并使用description注释作为text。两者都使用Go 模板 (golang.org/pkg/text/template/)。这些是我们在 Prometheus 警报中定义的相同注释。

最后,title_link设置为http://my-prometheus.com/alerts。这确实不是您的 Prometheus UI 的地址,但由于我事先无法知道您的域名是什么,我放了一个不存在的域名。请随意将my-prometheus.com更改为环境变量$PROM_ADDR的值。或者保持不变,知道如果您单击该链接,它将不会将您带到您的 Prometheus UI。

现在我们已经探索了 Alertmanager 配置,我们可以继续并升级图表。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-nodes-am.yml

几分钟后,Alertmanager 将被重新配置,下次它从 Prometheus 接收到警报时,它将将其发送到 Slack。我们可以通过访问devops20.slack.com工作区来确认。如果您尚未注册,请访问slack.devops20toolkit.com。一旦您成为会员,我们可以访问devops25-tests频道。

 1  open "https://devops20.slack.com/messages/CD8QJA8DS/"

你应该看到集群增加通知。如果你看到其他消息,不要感到困惑。你可能不是唯一一个在运行本书练习的人。

图 3-7:从 Alertmanager 接收到的 Slack 警报消息有时,由于我无法弄清楚的原因,Slack 会收到来自 Alertmanager 的空通知。目前,我因懒惰而忽略了这个问题。

现在我们已经了解了 Prometheus 和 Alertmanager 的基本用法,我们将暂停一下手动操作,并讨论我们可能想要使用的指标类型。

我们应该使用哪种指标类型?

如果这是你第一次使用连接到 Kube API 的 Prometheus 指标,可能会感到压力很大。除此之外,还要考虑到配置排除了 Kube API 提供的许多指标,并且我们可以通过额外的导出器进一步扩展范围。

虽然每种情况都是不同的,你可能需要一些特定于你的组织和架构的指标,但是有一些指导方针我们应该遵循。在本节中,我们将讨论关键指标。一旦你通过几个例子理解了它们,你应该能够将它们扩展到你特定的用例中。

每个人都应该利用的四个关键指标是延迟、流量、错误和饱和度。

这四个指标被谷歌的网站可靠性工程师SREs)推崇为跟踪系统性能和健康状况的最基本指标。

延迟代表服务响应请求所需的时间。重点不仅应该放在持续时间上,还应该区分成功请求的延迟和失败请求的延迟。

流量是对服务所承受的需求的衡量。一个例子是每秒的 HTTP 请求次数。

错误是由请求失败的速率来衡量的。大多数情况下,这些失败是显式的(例如,HTTP 500 错误),但它们也可以是隐式的(例如,一个 HTTP 200 响应,其中的内容描述了查询没有返回任何结果)。

饱和度可以用来描述服务或系统的“充实程度”。一个典型的例子是缺乏 CPU 导致节流,从而降低了应用程序的性能。

随着时间的推移,不同的监控方法被开发出来。例如,我们得到了USE方法,该方法规定对于每个资源,我们应该检查利用率饱和度错误。另一个是RED方法,它将速率错误持续时间定义为关键指标。这些和许多其他方法在本质上是相似的,并且与 SRE 对于测量延迟、流量、错误和饱和度的需求没有明显的区别。

我们将逐个讨论 SRE 描述的四种测量类型,并提供一些示例。我们甚至可能会扩展它们,加入一些不一定适合任何四类的指标。首先是延迟。

延迟相关问题的警报

我们将使用go-demo-5应用程序来测量延迟,所以我们的第一步是安装它。

 1  GD5_ADDR=go-demo-5.$LB_IP.nip.io
 2
 3  helm install \
 4      https://github.com/vfarcic/go-demo-5/releases/download/
    0.0.1/go-demo-5-0.0.1.tgz \
 5      --name go-demo-5 \
 6      --namespace go-demo-5 \
 7      --set ingress.host=$GD5_ADDR

我们生成了一个地址,我们将用作 Ingress 入口点,并使用 Helm 部署了应用程序。现在我们应该等待直到它完全部署。

 1  kubectl -n go-demo-5 \
 2      rollout status \
 3      deployment go-demo-5

在继续之前,我们将检查应用程序是否确实通过发送 HTTP 请求正确工作。

 1  curl "http://$GD5_ADDR/demo/hello"

输出应该是熟悉的hello, world!消息。

现在,让我们看看是否可以,例如,通过 Ingress 进入系统的请求的持续时间。

 1  open "http://$PROM_ADDR/graph"

如果您点击“在光标处插入指标”下拉列表,您将能够浏览所有可用的指标。我们正在寻找的是nginx_ingress_controller_request_duration_seconds_bucket。正如其名称所示,该指标来自 NGINX Ingress Controller,并提供以秒为单位分组的请求持续时间。

请键入以下表达式,然后单击“执行”按钮。

 1  nginx_ingress_controller_request_duration_seconds_bucket

在这种情况下,查看原始值可能并不是非常有用,所以请点击“图表”选项卡。

您应该看到图表,每个 Ingress 都有一个。每个图表都在增加,因为所讨论的指标是一个计数器 (prometheus.io/docs/concepts/metric_types/#counter)。它的值随着每个请求而增加。

Prometheus 计数器是一个累积指标,其值只能增加,或者在重新启动时重置为零。

我们需要计算一段时间内的请求速率。我们将通过结合sumrate (prometheus.io/docs/prometheus/latest/querying/functions/#rate()) 函数来实现这一点。前者应该是不言自明的。

Prometheus 的速率函数计算了范围向量中时间序列的每秒平均增长率。

请输入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_count[5m]
 3  )) 
 4  by (ingress)

结果图表向我们显示了通过 Ingress 进入系统的所有请求的每秒速率。速率是基于五分钟的间隔计算的。如果您将鼠标悬停在其中一条线上,您将看到额外的信息,如值和 Ingress。by语句允许我们按ingress对结果进行分组。

尽管如此,单独的结果并不是非常有用,因此让我们重新定义我们的需求。我们应该能够找出有多少请求比 0.25 秒慢。我们无法直接做到这一点。相反,我们可以检索所有那些 0.25 秒或更快的请求。

请输入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25"
 4    }[5m]
 5  )) 
 6  by (ingress)

我们真正想要的是找出落入 0.25 秒区间的请求的百分比。为了实现这一点,我们将获取快于或等于 0.25 秒的请求的速率,并将结果除以所有请求的速率。

请输入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25"
 4    }[5m]
 5  )) 
 6  by (ingress) / 
 7  sum(rate(
 8    nginx_ingress_controller_request_duration_seconds_count[5m]
 9  )) 
10  by (ingress)

由于我们尚未生成太多流量,您可能在图表中看不到太多内容,除了偶尔与 Prometheus 和 Alertmanager 的交互以及我们发送到go-demo-5的单个请求。尽管如此,您可以看到的几行显示了响应时间在 0.25 秒内的请求的百分比。

目前,我们只对go-demo-5的请求感兴趣,因此我们将进一步完善表达式,将结果限制为仅限于go-demo-5的 Ingress。

请输入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25", 
 4      ingress="go-demo-5"
 5    }[5m]
 6  )) 
 7  by (ingress) / 
 8  sum(rate(
 9    nginx_ingress_controller_request_duration_seconds_count{
10      ingress="go-demo-5"
11    }[5m]
12  )) 
13  by (ingress)

由于我们只发送了一个请求,图表应该几乎是空的。或者,您可能收到了“未找到数据点”的消息。现在是时候生成一些流量了。

 1  for i in {1..30}; do
 2    DELAY=$[ $RANDOM % 1000 ]
 3    curl "http://$GD5_ADDR/demo/hello?delay=$DELAY"
 4  done

我们向go-demo-5发送了 30 个请求。该应用程序具有延迟响应请求的“隐藏”功能。鉴于我们希望生成具有随机响应时间的流量,我们使用了DELAY变量,其随机值最多为 1000 毫秒。现在我们可以重新运行相同的查询,看看是否可以获得一些更有意义的数据。

请等一会儿,直到收集到新请求的数据,然后在 Prometheus 中输入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25", 
 4      ingress="go-demo-5"
 5    }[5m]
 6  )) 
 7  by (ingress) / 
 8  sum(rate(
 9    nginx_ingress_controller_request_duration_seconds_count{
10      ingress="go-demo-5"
11    }[5m]
12  )) 
13  by (ingress)

这次,我们可以看到新行的出现。在我的情况下(随后的屏幕截图),大约百分之二十五的请求持续时间在 0.25 秒内。换句话说,大约四分之一的请求比预期慢。

图 3-8:带有 0.25 秒持续时间请求百分比的 Prometheus 图表屏幕

过滤特定应用程序(Ingress)的指标在我们知道存在问题并希望进一步挖掘时非常有用。但是,我们仍然需要一个警报,告诉我们存在问题。因此,我们将执行类似的查询,但这次不限制结果为特定应用程序(Ingress)。我们还必须定义一个条件来触发警报,因此我们将将阈值设置为百分之九十五(0.95)。如果没有这样的阈值,每次单个请求变慢时我们都会收到通知。结果,我们会被警报淹没,并很快开始忽视它们。毕竟,如果单个请求变慢,系统并不会有危险,只有当有相当数量的请求变慢时才会有危险。在我们的情况下,这是百分之五的慢请求,或者更准确地说,少于百分之九十五的快速请求。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25"
 4    }[5m]
 5  ))
 6  by (ingress) /
 7  sum(rate(
 8    nginx_ingress_controller_request_duration_seconds_count[5m]
 9  ))
10  by (ingress) < 0.95

我们可以偶尔看到少于百分之九十五的请求在 0.25 秒内。在我的情况下(随后的屏幕截图),我们可以看到 Prometheus、Alertmanager 和go-demo-5偶尔变慢。

图 3-9:带有 0.25 秒持续时间请求百分比的 Prometheus 图表屏幕,仅限于高于百分之九十五的结果

唯一缺少的是基于先前表达式定义警报。因此,每当少于百分之九十五的请求持续时间少于 0.25 秒时,我们应该收到通知。

我准备了一组更新后的 Prometheus 图表数值,让我们看看与我们当前使用的图表的差异。

 1  diff mon/prom-values-nodes-am.yml \
 2      mon/prom-values-latency.yml

输出如下。

53a54,62
> - name: latency
>   rules:
>   - alert: AppTooSlow
>     expr: sum(rate(nginx_ingress_controller_request_duration_seconds_bucket{le= "0.25"}[5m])) by (ingress) / sum(rate(nginx_ingress_controller_request_duration_seconds_count[5m])) by (ingress) < 0.95
>     labels:
>       severity: notify
>     annotations:
>       summary: Application is too slow
>       description: More then 5% of requests are slower than 0.25s
57c66
<     expr: count(kube_node_info) > 0
---
>     expr: count(kube_node_info) > 3

我们添加了一个新的警报AppTooSlow。如果持续时间为 0.25 秒或更短的请求的百分比小于百分之九十五(0.95),它将触发。

我们还将TooManyNodes的阈值恢复为其原始值3

接下来,我们将使用新值更新prometheus图表,并打开警报屏幕以确认是否确实添加了新警报。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-latency.yml
 8
 9  open "http://$PROM_ADDR/alerts"

如果AppTooSlow警报仍然不可用,请稍等片刻并刷新屏幕。

图 3-10:Prometheus 警报屏幕

新增的警报可能是绿色的(不会触发)。我们需要生成一些慢请求来看它的作用。

请执行以下命令,发送 30 个具有随机响应时间的请求,最长为 10000 毫秒(10 秒)。

 1  for i in {1..30}; do
 2    DELAY=$[ $RANDOM % 10000 ]
 3    curl "http://$GD5_ADDR/demo/hello?delay=$DELAY"
 4  done

直到 Prometheus 抓取新的指标并且警报检测到阈值已达到,需要一些时间。过一会儿,我们可以再次打开警报屏幕,检查警报是否确实触发。

 1  open "http://$PROM_ADDR/alerts"

我们可以看到警报的状态是触发。如果这不是您的情况,请再等一会儿并刷新屏幕。在我的情况下(随后的截图),该值为 0.125,意味着只有 12.5%的请求持续时间为 0.25 秒或更短。

如果prometheus-serverprometheus-alertmanager或其他一些应用程序响应缓慢,AppTooSlow内可能会有两个或更多活动警报。图 3-11:带有一个触发警报的 Prometheus 警报屏幕

警报是红色的,意味着 Prometheus 将其发送到 Alertmanager,后者又将其转发到 Slack。让我们确认一下。

 1  open "https://devops20.slack.com/messages/CD8QJA8DS/"

正如您所看到的(随后的截图),我们收到了两个通知。由于我们将TooManyNodes警报的阈值恢复为大于三个节点,并且我们的集群节点较少,因此 Prometheus 向 Alertmanager 发送了问题已解决的通知。结果,我们在 Slack 中收到了新的通知。这次,消息的颜色是绿色的。

接着,出现了一个新的红色消息,指示“应用程序太慢”。

图 3-12:Slack 显示触发(红色)和解决(绿色)消息

我们经常不能依赖单一规则来适用于所有应用程序。例如,Prometheus 和 Jenkins 可能是内部应用程序的良好候选者,我们不能期望其响应时间低于 0.25 秒的百分之五。因此,我们可能需要进一步过滤警报。我们可以使用任意数量的标签来实现这一点。为了简单起见,我们将继续利用ingress标签,但这次我们将使用正则表达式来排除一些应用程序(Ingress)的警报。

让我们再次打开图表屏幕。

 1  open "http://$PROM_ADDR/graph"

请键入以下表达式,点击“执行”按钮,然后切换到图表选项卡。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.25", 
 4      ingress!~"prometheus-server|jenkins"
 5    }[5m]
 6  )) 
 7  by (ingress) / 
 8  sum(rate(
 9    nginx_ingress_controller_request_duration_seconds_count{
10      ingress!~"prometheus-server|jenkins"
11    }[5m]
12  )) 
13  by (ingress)

与之前的查询相比,新增的是ingress!~"prometheus-server|jenkins"过滤器。!~用于选择具有不与prometheus-server|jenkins字符串匹配的标签的指标。由于|等同于or语句,我们可以将该过滤器翻译为“所有不是prometheus-server或不是jenkins的内容”。我们的集群中没有 Jenkins。我只是想向您展示一种排除多个值的方法。

图 3-13:Prometheus 图表屏幕,显示了持续时间为 0.25 秒的请求百分比,结果不包括 prometheus-server 和 jenkins

我们可以再复杂一点,指定ingress!~"prometheus.+|jenkins.+作为过滤器。在这种情况下,它将排除所有名称以prometheusjenkins开头的 Ingress。关键在于.+的添加,在正则表达式中,它匹配一个或多个任何字符的条目(+)。

我们不会详细解释正则表达式的语法。我希望您已经熟悉它。如果您不熟悉,您可能需要搜索一下或访问正则表达式维基页面(en.wikipedia.org/wiki/Regular_expression)。

之前的表达式只检索不是prometheus-serverjenkins的结果。我们可能需要创建另一个表达式,只包括这两个。

请键入以下表达式,然后点击“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_request_duration_seconds_bucket{
 3      le="0.5",
 4      ingress=~"prometheus-server|jenkins"
 5    }[5m]
 6  )) 
 7  by (ingress) /
 8  sum(rate(
 9    nginx_ingress_controller_request_duration_seconds_count{
10      ingress=~"prometheus-server|jenkins"
11    }[5m]
12  ))
13  by (ingress)

与之前的表达式相比,唯一的区别是这次我们使用了=~运算符。它选择与提供的字符串匹配的标签。此外,桶(le)现在设置为0.5秒,因为这两个应用程序可能需要更多时间来响应,我们可以接受这一点。

在我的情况下,图表显示prometheus-server的请求百分比为百分之百,持续时间在 0.5 秒内(在您的情况下可能不是真的)。

图 3-14:Prometheus 图表屏幕,显示了持续 0.5 秒的请求百分比,以及仅包括 prometheus-server 和 jenkins 的结果

少数延迟示例应该足以让您了解这种类型的指标,所以我们将转向流量。

流量相关问题的警报

到目前为止,我们测量了应用程序的延迟,并创建了在达到基于请求持续时间的特定阈值时触发的警报。这些警报不是基于进入的请求数量(流量),而是基于慢请求的百分比。即使只有一个请求进入应用程序,只要持续时间超过阈值,AppTooSlow也会触发。为了完整起见,我们需要开始测量流量,或者更准确地说,是发送到每个应用程序和整个系统的请求数。通过这样做,我们可以知道我们的系统是否承受了很大的压力,并决定是否要扩展我们的应用程序,增加更多的工作人员,或者采取其他解决方案来缓解问题。如果请求的数量达到异常数字,清楚地表明我们正在遭受拒绝服务DoS)攻击(en.wikipedia.org/wiki/Denial-of-service_attack),我们甚至可以选择阻止部分传入流量。

我们将开始创建一些流量,以便我们可以用来可视化请求。

 1  for i in {1..100}; do
 2      curl "http://$GD5_ADDR/demo/hello"
 3  done
 4
 5  open "http://$PROM_ADDR/graph"

我们向go-demo-5应用程序发送了一百个请求,并打开了 Prometheus 的图表屏幕。

我们可以通过nginx_ingress_controller_requests获取进入 Ingress 控制器的请求数。由于它是一个计数器,我们可以继续使用rate函数结合sum。最后,我们可能想要按ingress标签对请求的速率进行分组。

请键入下面的表达式,按“执行”按钮,然后切换到图表选项卡。

 1  sum(rate(
 2    nginx_ingress_controller_requests[5m]
 3  ))
 4  by (ingress)

图表的右侧显示了一个峰值。它显示了通过具有相同名称的 Ingress 发送到go-demo-5应用程序的请求。

在我的情况下(随后的屏幕截图),峰值接近每秒一个请求(您的情况将不同)。

图 3-15:Prometheus 的图形屏幕显示请求数量的速率

我们可能更感兴趣的是每个应用程序每秒每个副本的请求数,因此我们的下一个任务是找到一种检索该数据的方法。由于go-demo-5是一个部署,我们可以使用kube_deployment_status_replicas

请键入以下表达式,然后按“执行”按钮。

 1  kube_deployment_status_replicas

我们可以看到系统中每个部署的副本数量。在我的情况下,go-demo-5 应用程序以红色(后续截图)显示,有三个副本。

图 3-16:Prometheus 的图形屏幕显示部署的副本数量

接下来,我们应该组合这两个表达式,以获得每个副本每秒的请求数。然而,我们面临一个问题。要使两个指标结合,它们需要具有匹配的标签。go-demo-5的部署和入口都具有相同的名称,因此我们可以利用这一点,假设我们可以重命名其中一个标签。我们将借助label_joinprometheus.io/docs/prometheus/latest/querying/functions/#label_join())函数来实现这一点。

对于 v 中的每个时间序列,label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...)使用分隔符连接所有src_labels的值,并返回包含连接值的标签dst_label的时间序列。

如果之前对label_join函数的解释让你感到困惑,你并不孤单。相反,让我们通过一个示例来了解,该示例将通过添加ingress标签来转换kube_deployment_status_replicas,该标签将包含来自deployment标签的值。如果我们成功了,我们将能够将结果与nginx_ingress_controller_requests组合,因为两者都具有相同的匹配标签(ingress)。

请键入以下表达式,然后按“执行”按钮。

 1  label_join(
 2    kube_deployment_status_replicas,
 3    "ingress", 
 4    ",", 
 5    "deployment"
 6  )

由于这次我们主要关注标签的值,请通过点击选项卡切换到控制台视图。

从输出中可以看出,每个指标现在都包含一个额外的标签ingress,其值与deployment相同。

图 3-17:Prometheus 的控制台视图显示部署副本状态和从部署标签创建的新标签 ingress

现在我们可以结合这两个指标。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2    nginx_ingress_controller_requests[5m]
 3  ))
 4  by (ingress) /
 5  sum(label_join(
 6    kube_deployment_status_replicas,
 7    "ingress",
 8    ",",
 9    "deployment"
10  ))
11  by (ingress)

切换回图表视图。

我们计算了每个应用程序(ingress)的请求数量的速率,并将其除以每个应用程序(ingress)的副本总数。最终结果是每个应用程序(ingress)每个副本的请求数量的速率。

值得注意的是,我们无法检索每个特定副本的请求数量,而是每个副本的平均请求数量。在大多数情况下,这种方法应该有效,因为 Kubernetes 网络通常执行轮询,导致向每个副本发送的请求数量多少相同。

总的来说,现在我们知道我们的副本每秒收到多少请求。

图 3-18:普罗米修斯图表屏幕,显示请求数量除以部署副本数量的速率

现在我们已经学会了如何编写一个表达式来检索每秒每个副本的请求数量的速率,我们应该将其转换为警报。

因此,让我们来看看普罗米修斯图表值的旧定义和新定义之间的区别。

 1  diff mon/prom-values-latency.yml \
 2      mon/prom-values-latency2.yml

输出如下。

62a63,69
> - alert: TooManyRequests
>   expr: sum(rate(nginx_ingress_controller_requests[5m])) by (ingress) / sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment")) by (ingress) > 0.1
>   labels:
>     severity: notify
>   annotations:
>     summary: Too many requests
>     description: There is more than average of 1 requests per second per replica for at least one application

我们可以看到表达式几乎与我们在普罗米修斯图表屏幕中使用的表达式相同。唯一的区别是我们将阈值设置为0.1。因此,该警报应在副本每秒收到的请求数超过五分钟内计算的速率0.1时通知我们。你可能已经猜到,每秒0.1个请求是一个太低的数字,不能在生产中使用。然而,它将使我们能够轻松触发警报并看到它的作用。

现在,让我们升级我们的图表,并打开普罗米修斯的警报屏幕。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-latency2.yml
 8
 9  open "http://$PROM_ADDR/alerts"

请刷新屏幕,直到TooManyRequests警报出现。

图 3-19:普罗米修斯警报屏幕

接下来,我们将生成一些流量,以便我们可以看到警报是如何生成并通过 Alertmanager 发送到 Slack 的。

 1  for i in {1..200}; do
 2      curl "http://$GD5_ADDR/demo/hello"
 3  done
 4
 5  open "http://$PROM_ADDR/alerts"

我们发送了两百个请求,并重新打开了普罗米修斯的警报屏幕。现在我们应该刷新屏幕,直到TooManyRequests警报变为红色。

普罗米修斯一旦触发了警报,就会被发送到 Alertmanager,然后转发到 Slack。让我们确认一下。

 1  open "https://devops20.slack.com/messages/CD8QJA8DS/"

我们可以看到“请求过多”的通知,从而证明了这个警报的流程是有效的。

图 3-20:Slack 与警报消息

接下来,我们将转向与错误相关的指标。

关于错误相关问题的警报

我们应该时刻注意我们的应用程序或系统是否产生错误。然而,我们不能在第一次出现错误时就开始惊慌,因为那样会产生太多通知,我们很可能会忽略它们。

错误经常发生,许多错误是由自动修复的问题或由我们无法控制的情况引起的。如果我们要对每个错误执行操作,我们就需要一支全天候工作的人员团队,专门解决通常不需要解决的问题。举个例子,因为有一个单独的响应代码在 500 范围内而进入“恐慌”模式几乎肯定会产生永久性危机。相反,我们应该监视错误的比率与总请求数量的比较,并且只有在超过一定阈值时才做出反应。毕竟,如果一个错误持续存在,那么错误率肯定会增加。另一方面,如果错误率持续很低,这意味着问题已经被系统自动修复(例如,Kubernetes 重新安排了从失败节点中的 Pod)或者这是一个不重复的孤立案例。

我们的下一个任务是检索请求并根据它们的状态进行区分。如果我们能做到这一点,我们应该能够计算出错误的比率。

我们将从生成一些流量开始。

 1  for i in {1..100}; do
 2      curl "http://$GD5_ADDR/demo/hello"
 3  done
 4
 5  open "http://$PROM_ADDR/graph"

我们发送了一百个请求并打开了 Prometheus 的图形屏幕。

让我们看看我们之前使用的nginx_ingress_controller_requests指标是否提供了请求的状态。

请键入以下表达式,然后点击执行按钮。

 1  nginx_ingress_controller_requests

我们可以看到 Prometheus 最近抓取的所有数据。如果我们更加关注标签,我们会发现,其中包括status。我们可以使用它来根据请求的总数计算出错误的百分比(例如,500 范围内的错误)。

我们已经看到我们可以使用ingress标签来按应用程序分别计算,假设我们只对那些面向公众的应用程序感兴趣。

图 3-21:通过 Ingress 进入的 Prometheus 控制台视图的请求

go-demo-5应用程序有一个特殊的端点/demo/random-error,它将生成随机的错误响应。大约每十个对该地址的请求中就会产生一个错误。我们可以用这个来测试我们的表达式。

 1  for i in {1..100}; do
 2    curl "http://$GD5_ADDR/demo/random-error"
 3  done

我们向/demo/random-error端点发送了一百个请求,大约有 10%的请求产生了错误(HTTP 状态码500)。

接下来,我们将不得不等待一段时间,让 Prometheus 抓取新的一批指标。之后,我们可以打开图表屏幕,尝试编写一个表达式,以检索我们应用程序的错误率。

 1  open "http://$PROM_ADDR/graph"

请键入以下表达式,然后按执行按钮。

 1  sum(rate(
 2    nginx_ingress_controller_requests{
 3      status=~"5.."
 4    }[5m]
 5  ))
 6  by (ingress) /
 7  sum(rate(
 8    nginx_ingress_controller_requests[5m]
 9  ))
10  by (ingress)

我们使用了5..正则表达式来计算按ingress分组的带有错误的请求的比率,并将结果除以所有请求的比率。结果按ingress分组。在我的情况下(随后的截图),结果大约为 4%(0.04)。Prometheus 尚未抓取所有指标,我预计在下一次抓取迭代中这个数字会接近 10%。

图 3-22:带有错误响应请求百分比的 Prometheus 图表屏幕

让我们比较图表值文件的更新版本与我们之前使用的版本。

 1  diff mon/prom-values-cpu-memory.yml \
 2      mon/prom-values-errors.yml

输出如下。

127a128,136
> - name: errors
>   rules:
>   - alert: TooManyErrors
>     expr: sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m])) by (ingress) / sum(rate(nginx_ingress_controller_requests[5m])) by (ingress) > 0.025
>     labels:
>       severity: error
>     annotations:
>       summary: Too many errors
>       description: At least one application produced more then 5% of error responses

如果错误率超过总请求率的 2.5%,警报将触发。

现在我们可以升级我们的 Prometheus 图表。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-errors.yml

我们可能不需要确认警报是否有效。我们已经看到 Prometheus 将所有警报发送到 Alertmanager,然后从那里转发到 Slack。

接下来,我们将转移到饱和度指标和警报。

饱和度相关问题的警报

饱和度衡量了我们的服务和系统的充实程度。如果我们的服务副本处理了太多的请求并被迫排队处理其中一些,我们应该意识到这一点。我们还应该监视我们的 CPU、内存、磁盘和其他资源的使用是否达到了临界限制。

现在,我们将专注于 CPU 使用率。我们将首先打开 Prometheus 的图表屏幕。

 1  open "http://$PROM_ADDR/graph"

让我们看看是否可以获得节点(instance)的 CPU 使用率。我们可以使用node_cpu_seconds_total指标来实现。但是,它被分成不同的模式,我们将不得不排除其中的一些模式,以获得“真实”的 CPU 使用率。这些将是idleiowait,和任何类型的guest周期。

请键入以下表达式,然后按执行按钮。

 1  sum(rate(
 2    node_cpu_seconds_total{
 3      mode!="idle", 
 4      mode!="iowait", 
 5      mode!~"^(?:guest.*)$"
 6   }[5m]
 7  ))
 8  by (instance)

切换到图表视图。

输出代表了系统中 CPU 的实际使用情况。在我的情况下(以下是屏幕截图),除了临时的峰值,所有节点的 CPU 使用量都低于一百毫秒。

系统远未处于压力之下。

图 3-23:Prometheus 的图表屏幕,显示了按节点实例分组的 CPU 使用率

正如您已经注意到的,绝对数字很少有用。我们应该尝试发现使用的 CPU 百分比。我们需要找出我们的节点有多少 CPU。我们可以通过计算指标的数量来做到这一点。每个 CPU 都有自己的数据条目,每种模式都有一个。如果我们将结果限制在单个模式(例如system)上,我们应该能够获得 CPU 的总数。

请输入以下表达式,然后点击执行按钮。

 1  count(
 2    node_cpu_seconds_total{
 3      mode="system"
 4    }
 5  )

在我的情况下(以下是屏幕截图),总共有六个核心。如果您使用的是 GKE、EKS 或来自 Gists 的 AKS,您的情况可能也是六个。另一方面,如果您在 Docker for Desktop 或 minikube 中运行集群,结果应该是一个节点。

现在我们可以结合这两个查询来获取使用的 CPU 百分比

请输入以下表达式,然后点击执行按钮。

 1  sum(rate(
 2    node_cpu_seconds_total{
 3      mode!="idle", 
 4      mode!="iowait",
 5      mode!~"^(?:guest.*)$"
 6    }[5m]
 7  )) /
 8  count(
 9    node_cpu_seconds_total{
10      mode="system"
11    }
12  )

我们总结了使用的 CPU 速率,并将其除以 CPU 的总数。在我的情况下(以下是屏幕截图),当前仅使用了三到四个百分比的 CPU。

这并不奇怪,因为大部分系统都处于休眠状态。我们的集群现在并没有太多活动。

图 3-24:Prometheus 的图表屏幕,显示了可用 CPU 的百分比

现在我们知道如何获取整个集群使用的 CPU 百分比,我们将把注意力转向应用程序。

我们将尝试发现我们有多少可分配的核心。从应用程序的角度来看,至少当它们在 Kubernetes 中运行时,可分配的 CPU 显示了可以为 Pods 请求多少。可分配的 CPU 始终低于总 CPU。

请输入以下表达式,然后点击执行按钮。

 1  kube_node_status_allocatable_cpu_cores

输出应该低于我们的虚拟机使用的核心数。可分配的核心显示了可以分配给容器的 CPU 数量。更准确地说,可分配的核心是分配给节点的 CPU 数量减去系统级进程保留的数量。在我的情况下(以下是屏幕截图),几乎有两个完整的可分配 CPU。

图 3-25:Prometheus 的图表屏幕,显示集群中每个节点的可分配 CPU

然而,在这种情况下,我们对可分配的 CPU 总量感兴趣,因为我们试图发现我们的 Pods 在整个集群中使用了多少。因此,我们将对可分配的核心求和。

请键入以下表达式,然后按“执行”按钮。

 1  sum(
 2    kube_node_status_allocatable_cpu_cores
 3  )

在我的情况下,可分配的 CPU 总数大约为 5.8 个核心。要获取确切的数字,请将鼠标悬停在图表线上。

现在我们知道了有多少可分配的 CPU,我们应该尝试发现 Pods 请求了多少。

请注意,请求的资源与已使用的资源不同。我们稍后会讨论这种情况。现在,我们想知道我们从系统中请求了多少。

请键入以下表达式,然后按“执行”按钮。

 1  kube_pod_container_resource_requests_cpu_cores

我们可以看到请求的 CPU 相对较低。在我的情况下,所有请求 CPU 的容器值都低于 0.15(一百五十毫秒)。您的结果可能有所不同。

与可分配的 CPU 一样,我们对请求的 CPU 总和感兴趣。稍后,我们将能够结合这两个结果,并推断集群中还有多少未保留的资源。

请键入以下表达式,然后按“执行”按钮。

 1  sum(
 2    kube_pod_container_resource_requests_cpu_cores
 3  )

我们对所有 CPU 资源请求求和。结果是,在我的情况下(随后的屏幕截图),所有请求的 CPU 略低于 1.5。

图 3-26:Prometheus 的图表屏幕,显示请求的 CPU 总和

现在,让我们结合这两个表达式,看看请求的 CPU 百分比。

请键入以下表达式,然后按“执行”按钮。

 1  sum(
 2    kube_pod_container_resource_requests_cpu_cores
 3  ) /
 4  sum(
 5    kube_node_status_allocatable_cpu_cores
 6  )

在我的情况下,输出显示大约四分之一(0.25)的可分配 CPU 被保留。这意味着在我们达到扩展集群的需要之前,我们可以有四倍的 CPU 请求。当然,您已经知道,如果存在的话,集群自动扩展器会在此之前添加节点。但是,知道我们接近达到 CPU 限制是很重要的。集群自动扩展器可能无法正常工作,或者甚至可能根本没有激活。如果有的话,后一种情况对于大多数本地集群来说是真实的。

让我们看看我们是否可以将我们探索的表达式转换为警报。

我们将探讨一组新的图表值与之前使用的值之间的另一个差异。

 1  diff mon/prom-values-latency2.yml \
 2      mon/prom-values-cpu.yml

输出如下。

64c64
<   expr: sum(rate(nginx_ingress_controller_requests[5m])) by (ingress) / sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment")) by (ingress) > 0.1
---
>   expr: sum(rate(nginx_ingress_controller_requests[5m])) by (ingress) / sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment")) by (ingress) > 1
87a88,103
> - alert: NotEnoughCPU
>   expr: sum(rate(node_cpu_seconds_total{mode!="idle", mode!="iowait", mode!~"^(?:guest.*)$"}[5m])) / count(node_cpu_seconds_total{mode="system"}) > 0.9
>   for: 30m
>   labels:
>     severity: notify
>   annotations:
>     summary: There's not enough CPU
>     description: CPU usage of the cluster is above 90%
> - alert: TooMuchCPURequested
>   expr: sum(kube_pod_container_resource_requests_cpu_cores) / sum(kube_node_status_allocatable_cpu_cores) > 0.9
>   for: 30m
>   labels:
>     severity: notify
>   annotations:
>     summary: There's not enough allocatable CPU
>     description: More than 90% of allocatable CPU is requested

从差异中我们可以看到,我们将TooManyRequests的原始阈值恢复为1,并添加了两个名为NotEnoughCPUTooMuchCPURequested的新警报。

如果整个集群的 CPU 使用率超过百分之九十并持续超过三十分钟,NotEnoughCPU警报将会触发。这样我们就可以避免在 CPU 使用率暂时飙升时设置警报。

TooMuchCPURequested也有百分之九十的阈值,如果持续超过三十分钟,将会触发警报。该表达式计算请求的总量与可分配 CPU 的总量之间的比率。

这两个警报都是我们之前执行的 Prometheus 表达式的反映,所以您应该已经熟悉它们的用途。

让我们使用新的值升级 Prometheus 的图表并打开警报屏幕。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-cpu.yml
 8
 9  open "http://$PROM_ADDR/alerts"

现在剩下的就是等待两个新的警报出现。如果它们还没有出现,请刷新您的屏幕。

现在可能没有必要看新的警报如何起作用。到现在为止,您应该相信这个流程,没有理由认为它们不会触发。

图 3-27:Prometheus 的警报屏幕

在“现实世界”场景中,接收到这两个警报中的一个可能会引发不同的反应,这取决于我们使用的 Kubernetes 版本。

如果我们有集群自动缩放器(CA),我们可能不需要NotEnoughCPUTooMuchCPURequested警报。节点 CPU 使用率达到百分之九十并不会影响集群的正常运行,只要我们的 CPU 请求设置正确。同样,百分之九十的可分配 CPU 被保留也不是问题。如果 Kubernetes 无法安排新的 Pod,因为所有 CPU 都被保留,它将扩展集群。实际上,接近满负荷的 CPU 使用率或几乎所有可分配的 CPU 被保留是件好事。这意味着我们拥有我们所需的 CPU,并且我们不必为未使用的资源付费。然而,这种逻辑主要适用于云服务提供商,甚至并非所有云服务提供商都适用。今天(2018 年 10 月),集群自动缩放器只在 AWS、GCE 和 Azure 中运行。

所有这些并不意味着我们应该只依赖于集群自动缩放器。它也可能出现故障,就像其他任何东西一样。然而,由于集群自动缩放器是基于观察无法调度的 Pods,如果它无法工作,我们应该通过观察 Pod 状态而不是 CPU 使用率来检测到这一点。不过,当 CPU 使用率过高时接收警报也许并不是一个坏主意,但在这种情况下,我们可能希望将阈值增加到接近百分之百的值。

如果我们的集群是本地的,更准确地说,如果它没有集群自动缩放器,那么我们探讨的警报对于我们的集群扩展流程是否没有自动化或者速度较慢是至关重要的。逻辑很简单。如果我们需要超过几分钟来向集群添加新节点,我们就不能等到 Pods 无法调度。那将为时已晚。相反,我们需要知道在集群变满(饱和)之前我们已经没有可用容量,这样我们就有足够的时间通过向集群添加新节点来做出反应。

然而,拥有一个因为集群自动缩放器不工作而不自动缩放的集群并不是一个足够好的借口。我们有很多其他工具可以用来自动化我们的基础设施。当我们设法到达这样一个地步,我们可以自动向集群添加新节点时,警报的目的地应该改变。我们可能不再希望收到 Slack 通知,而是希望向一个服务发送请求,该服务将执行脚本,从而向集群添加新节点。如果我们的集群是在虚拟机上运行的,我们总是可以通过脚本(或某个工具)添加更多节点。

接收 Slack 通知的唯一真正理由是如果我们的集群是运行在裸机上的。在这种情况下,我们不能指望脚本会神奇地创建新的服务器。对于其他人来说,当 CPU 使用过高或者所有分配的 CPU 都被保留时接收 Slack 通知应该只是一个临时解决方案,直到适当的自动化到位为止。

现在,让我们尝试通过测量内存使用和保留来实现类似的目标。

测量内存消耗与 CPU 类似,但也有一些我们应该考虑的不同之处。但在我们到达那里之前,让我们回到 Prometheus 的图表界面,探索我们的第一个与内存相关的指标。

 1  open "http://$PROM_ADDR/graph"

就像 CPU 一样,首先我们需要找出每个节点有多少内存。

请键入以下表达式,点击“执行”按钮,然后切换到图表选项卡。

 1  node_memory_MemTotal_bytes

你的结果可能与我的不同。在我的情况下,每个节点大约有 4GB 的 RAM。

知道每个节点有多少 RAM 是没有用的,如果不知道当前有多少 RAM 可用。我们可以通过node_memory_MemAvailable_bytes指标获得这些信息。

请在下面输入表达式,然后按“执行”按钮。

 1  node_memory_MemAvailable_bytes

我们可以看到集群中每个节点的可用内存。在我的情况下(如下截图所示),每个节点大约有 3GB 的可用 RAM。

图 3-28:Prometheus 的图表屏幕,显示集群中每个节点的可用内存

现在我们知道如何从每个节点获取总内存和可用内存,我们应该结合查询来获得整个集群已使用内存的百分比。

请在下面输入表达式,然后按“执行”按钮。

 1  1 -
 2  sum(
 3    node_memory_MemAvailable_bytes
 4  ) /
 5  sum(
 6    node_memory_MemTotal_bytes
 7  )

由于我们正在寻找已使用内存的百分比,并且我们有可用内存的指标,我们从1 -开始表达式,这将颠倒结果。表达式的其余部分是可用和总内存的简单除法。在我的情况下(如下截图所示),每个节点上使用的内存不到 30%。

图 3-29:Prometheus 的图表屏幕,显示可用内存的百分比

就像 CPU 一样,可用和总内存并不能完全反映实际情况。虽然这是有用的信息,也是潜在警报的基础,但我们还需要知道有多少内存可分配,以及有多少内存被 Pod 使用。我们可以通过kube_node_status_allocatable_memory_bytes指标获得第一个数字。

请在下面输入表达式,然后按“执行”按钮。

 1  kube_node_status_allocatable_memory_bytes

根据 Kubernetes 的版本和您使用的托管提供商,总内存和可分配内存之间可能存在很小或很大的差异。我在 AKS 中运行集群,可分配内存比总内存少了整整 1GB。前者大约为 3GB RAM,而后者大约为 4GB RAM。这是一个很大的差异。我的 Pod 并没有完整的 4GB 内存,而是少了大约四分之一。其余的大约 1GB RAM 花费在系统级服务上。更糟糕的是,这 1GB RAM 花费在每个节点上,而在我的情况下,这导致总共少了 3GB,因为我的集群有三个节点。鉴于总内存和可分配内存之间的巨大差异,拥有更少但更大的节点是有明显好处的。然而,并不是每个人都需要大节点,如果我们希望节点分布在所有区域,将节点数量减少到少于三个可能不是一个好主意。

现在我们知道如何检索可分配内存量,让我们看看如何获取每个应用程序所请求的内存量。

请键入以下表达式,然后按“执行”按钮。

 1  kube_pod_container_resource_requests_memory_bytes

我们可以看到 Prometheus(服务器)具有最多的请求内存(500MB),而其他所有的请求内存都远低于这个数值。请记住,我们只看到了具有预留的 Pod,那些没有预留的 Pod 不会出现在该查询的结果中。正如您已经知道的那样,在特殊情况下,例如用于 CI/CD 流程中的短暂 Pod,不定义预留和限制是可以的。

图 3-30:Prometheus 的图形屏幕显示了每个 Pod 所请求的内存

前面的表达式返回了每个 Pod 使用的内存量。然而,我们的任务是发现系统中总共有多少请求的内存。

请键入以下表达式,然后按“执行”按钮。

 1  sum(
 2    kube_pod_container_resource_requests_memory_bytes
 3  )

在我的情况下,所请求的内存总量大约为 1.6GB RAM。

现在剩下的就是将总请求的内存量除以集群中所有可分配的内存量。

请键入以下表达式,然后按“执行”按钮。

 1  sum(
 2    kube_pod_container_resource_requests_memory_bytes
 3  ) / 
 4  sum(
 5    kube_node_status_allocatable_memory_bytes
 6  )

在我的情况下(以下是屏幕截图),请求内存的总量约为集群可分配 RAM 的百分之二十(0.2)。我远非处于任何危险之中,也没有必要扩展集群。如果有什么,我有太多未使用的内存,可能想要缩减规模。然而,目前我们只关注扩展规模。稍后我们将探讨可能导致缩减规模的警报。

图 3-31:Prometheus 的图表屏幕,显示集群中请求内存占总可分配内存的百分比

让我们看看旧图表数值和我们即将使用的数值之间的差异。

 1  diff mon/prom-values-cpu.yml \
 2      mon/prom-values-memory.yml

输出如下。

103a104,119
> - alert: NotEnoughMemory
>   expr: 1 - sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes) > 0.9
>   for: 30m
>   labels:
>     severity: notify
>   annotations:
>     summary: There's not enough memory
>     description: Memory usage of the cluster is above 90%
> - alert: TooMuchMemoryRequested
>   expr: sum(kube_pod_container_resource_requests_memory_bytes) / sum(kube_node_status_allocatable_memory_bytes) > 0.9
>   for: 30m
>   labels:
>     severity: notify
>   annotations:
>     summary: There's not enough allocatable memory
>     description: More than 90% of allocatable memory is requested

我们添加了两个新的警报(“内存不足”和“请求内存过多”)。定义本身应该很简单,因为我们已经创建了相当多的警报。表达式与我们在 Prometheus 图表屏幕中使用的表达式相同,只是增加了大于百分之九十(> 0.9)的阈值。因此,我们将跳过进一步的解释。

我们将使用新值升级我们的 Prometheus 图表,并打开警报屏幕以确认它们。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-memory.yml
 8
 9  open "http://$PROM_ADDR/alerts"

如果警报“内存不足”和“请求内存过多”尚不可用,请稍等片刻,然后刷新屏幕。

图 3-32:Prometheus 的警报屏幕

到目前为止,基于内存的警报所采取的操作应该与我们讨论 CPU 时类似。我们可以使用它们来决定是否以及何时扩展我们的集群,无论是通过手动操作还是通过自动化脚本。就像以前一样,如果我们的集群托管在 Cluster Autoscaler(CA)支持的供应商之一,这些警报应该纯粹是信息性的,而在本地或不受支持的云提供商那里,它们不仅仅是简单的通知。它们表明我们即将耗尽容量,至少在涉及内存时是这样。

CPU 和内存的示例都集中在需要知道何时扩展我们的集群。我们可能会创建类似的警报,通知我们 CPU 或内存的使用率过低。这将清楚地告诉我们,我们的集群中有太多节点,我们可能需要移除一些。同样,这假设我们没有集群自动缩放器正在运行。但是,仅考虑 CPU 或内存中的一个来缩减规模太冒险,可能会导致意想不到的结果。

假设只有百分之十二的可分配 CPU 被保留,并且我们的集群中有三个工作节点。由于平均每个节点的保留 CPU 量相对较小,这么低的 CPU 使用率当然不需要那么多节点。因此,我们可以选择缩减规模,并移除一个节点,从而允许其他集群重用它。这样做是一个好主意吗?嗯,这取决于其他资源。如果内存预留的百分比也很低,移除一个节点是一个好主意。另一方面,如果保留的内存超过百分之六十六,移除一个节点将导致资源不足。当我们移除三个节点中的一个时,三个节点中超过百分之六十六的内存预留变成了两个节点中超过百分之一百。

总的来说,如果我们要收到通知,我们的集群需要缩减规模(而且我们没有集群自动缩放器),我们需要将内存和 CPU 以及可能还有其他一些指标作为警报阈值的组合。幸运的是,这些表达式与我们之前使用的非常相似。我们只需要将它们组合成一个单一的警报并更改阈值。

作为提醒,我们之前使用的表达式如下(无需重新运行)。

 1  sum(rate(
 2    node_cpu_seconds_total{
 3      mode!="idle",
 4      mode!="iowait",
 5      mode!~"^(?:guest.*)$"
 6    }[5m]
 7  ))
 8  by (instance) /
 9  count(
10    node_cpu_seconds_total{
11      mode="system"
12    }
13  )
14  by (instance)
15
16  1 -
17  sum(
18    node_memory_MemAvailable_bytes
19  ) 
20  by (instance) /
21  sum(
22    node_memory_MemTotal_bytes
23  )
24  by (instance)

现在,让我们将图表的值的另一个更新与我们现在使用的进行比较。

 1  diff mon/prom-values-memory.yml \
 2      mon/prom-values-cpu-memory.yml

输出如下。

119a120,127
> - alert: TooMuchCPUAndMemory
>   expr: (sum(rate(node_cpu_seconds_total{mode!="idle", mode!="iowait", mode!~"^(?:guest.*)$"}[5m])) by (instance) / count(node_cpu_seconds_total{mode="system"}) by (instance)) < 0.5 and (1 - sum(node_memory_MemAvailable_bytes) by (instance) / sum(node_memory_MemTotal_bytes) by (instance)) < 0.5
>   for: 30m
>   labels:
>     severity: notify
>   annotations:
>     summary: Too much unused CPU and memory
>     description: Less than 50% of CPU and 50% of memory is used on at least one node

我们正在添加一个名为TooMuchCPUAndMemory的新警报。它是以前两个警报的组合。只有当 CPU 和内存使用率都低于百分之五十时,它才会触发。这样我们就可以避免发送错误的警报,并且不会因为资源预留(CPU 或内存)之一太低而诱发缩减集群的决定,而另一个可能很高。

在我们进入下一个主题(或指标类型)之前,剩下的就是升级 Prometheus 的图表并确认新的警报确实是可操作的。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-cpu-memory.yml
 8
 9  open "http://$PROM_ADDR/alerts"

如果警报仍然不存在,请刷新警报屏幕。在我的情况下(以下是屏幕截图),保留内存和 CPU 的总量低于百分之五十,并且警报处于挂起状态。在您的情况下,这可能并不是真的,警报可能还没有达到其阈值。尽管如此,我将继续解释我的情况,在这种情况下,CPU 和内存使用量都低于总可用量的百分之五十。

三十分钟后(for: 30m),警报触发了。它等了一会儿(30m)以确认内存和 CPU 使用率的下降不是暂时的。鉴于我在 AKS 中运行我的集群,集群自动缩放器会在三十分钟之前删除一个节点。但是,由于它配置为最少三个节点,CA 将不执行该操作。因此,我可能需要重新考虑是否支付三个节点是值得的投资。另一方面,如果我的集群没有集群自动缩放器,并且假设我不想在其他集群可能需要更多资源的情况下浪费资源,我需要删除一个节点(手动或自动)。如果那个移除是自动的,目的地将不是 Slack,而是负责删除节点的工具的 API。

图 3-33:Prometheus 的警报屏幕,其中一个警报处于挂起状态

现在我们已经有了一些饱和的例子,我们涵盖了谷歌网站可靠性工程师和几乎任何其他监控方法所推崇的每个指标。但我们还没有完成。还有一些其他指标和警报我想探索。它们可能不属于讨论的任何类别,但它们可能证明非常有用。

对无法调度或失败的 Pod 进行警报

知道我们的应用程序是否在快速响应请求方面出现问题,是否受到了比其处理能力更多的请求,是否产生了太多错误,以及是否饱和,如果它们甚至没有运行,这就没有用了。即使我们的警报通过通知我们出现了太多错误或由于副本数量不足而导致响应时间变慢,我们仍然应该被告知,例如,一个或甚至所有副本无法运行。在最好的情况下,这样的通知将提供有关问题原因的额外信息。在更糟糕的情况下,我们可能会发现数据库的一个副本没有运行。这不一定会减慢速度,也不会产生任何错误,但会使我们处于数据无法复制的状态(额外的副本没有运行),如果最后一个剩下的副本也失败,我们可能会面临其状态的完全丢失。

应用程序无法运行的原因有很多。集群中可能没有足够的未保留资源。如果出现这种情况,集群自动缩放器将处理该问题。但是,还有许多其他潜在问题。也许新版本的镜像在注册表中不可用。或者,Pod 可能正在请求无法索赔的持久卷。正如你可能已经猜到的那样,可能导致我们的 Pod 失败、无法调度或处于未知状态的原因几乎是无限的。

我们无法单独解决 Pod 问题的所有原因。但是,如果一个或多个 Pod 的阶段是“失败”、“未知”或“挂起”,我们可以收到通知。随着时间的推移,我们可能会扩展我们的自愈脚本,以解决其中一些状态的特定原因。目前,我们最好的第一步是在 Pod 长时间处于这些阶段之一时收到通知(例如,十五分钟)。如果在 Pod 的状态指示出现问题后立即发出警报,那将是愚蠢的,因为那样会产生太多的误报。只有在等待一段时间后,我们才应该收到警报并选择如何行动,从而给 Kubernetes 时间来解决问题。只有在 Kubernetes 未能解决问题时,我们才应该执行一些反应性操作。

随着时间的推移,我们会注意到我们收到的警报中存在一些模式。当我们发现时,警报应该被转换为自动响应,可以在不需要我们参与的情况下解决选定的问题。我们已经通过 HorizontalPodAutoscaler 和 Cluster Autoscaler 探索了一些低 hanging fruits。目前,我们将专注于接收所有其他情况的警报,而失败和无法调度的 Pod 是其中的一部分。稍后,我们可能会探索如何自动化响应。但是,现在还不是时候,所以我们将继续进行另一个警报,这将导致 Slack 收到通知。

让我们打开 Prometheus 的图形屏幕。

 1  open "http://$PROM_ADDR/graph"

请键入以下表达式,然后单击“执行”按钮。

 1  kube_pod_status_phase

输出显示了集群中每个 Pod 的情况。如果你仔细观察,你会注意到每个 Pod 都有五个结果,分别对应五种可能的阶段。如果你关注phase字段,你会发现有一个条目是FailedPendingRunningSucceededUnknown。因此,每个 Pod 有五个结果,但只有一个值为1,而其他四个的值都设置为0

图 3-34:Prometheus 的控制台视图,显示了 Pod 的阶段

目前,我们主要关注警报,它们在大多数情况下应该是通用的,与特定节点、应用程序、副本或其他类型的资源无关。只有当我们收到有问题的警报时,我们才应该开始深入挖掘并寻找更详细的数据。考虑到这一点,我们将重新编写我们的表达式,以检索每个阶段的 Pod 数量。

请键入以下表达式,然后单击“执行”按钮。

 1  sum(
 2    kube_pod_status_phase
 3  ) 
 4  by (phase)

输出应该显示所有的 Pod 都处于Running阶段。在我的情况下,有二十七个正在运行的 Pod,没有一个处于其他任何阶段。

现在,我们实际上不应该关心健康的 Pod。它们正在运行,我们不需要做任何事情。相反,我们应该关注那些有问题的 Pod。因此,我们可能会重新编写先前的表达式,只检索那些处于FailedUnknownPending阶段的总和。

请键入以下表达式,然后单击“执行”按钮。

 1  sum(
 2    kube_pod_status_phase{
 3      phase=~"Failed|Unknown|Pending"
 4    }
 5  ) 
 6  by (phase)

如预期的那样,除非您搞砸了什么,输出的值都设置为0

图 3-35:Prometheus 的控制台视图,显示了处于失败、未知或挂起阶段的 Pod 的总数

到目前为止,没有我们需要担心的 Pod。我们将通过创建一个故意失败的 Pod 来改变这种情况,使用一个显然不存在的镜像。

 1  kubectl run problem \
 2      --image i-do-not-exist \
 3      --restart=Never

从输出中可以看出,pod/problem已经被created。如果我们通过脚本(例如,CI/CD 流水线)创建它,我们可能会认为一切都很好。即使我们跟着使用kubectl rollout status,我们只能确保它开始工作,而不能确保它继续工作。

但是,由于我们没有通过 CI/CD 流水线创建该 Pod,而是手动创建的,我们可以列出default命名空间中的所有 Pod。

 1  kubectl get pods

输出如下。

NAME    READY STATUS       RESTARTS AGE
problem 0/1   ErrImagePull 0        27s

我们假设我们只有短期记忆,并且已经忘记了image设置为i-do-not-exist。问题可能是什么?嗯,第一步是描述 Pod。

 1  kubectl describe pod problem

输出,仅限于Events部分的消息,如下。

...
Events:
...  Message
...  -------
...  Successfully assigned default/problem to aks-nodepool1-29770171-2
...  Back-off pulling image "i-do-not-exist"
...  Error: ImagePullBackOff
...  pulling image "i-do-not-exist"
...  Failed to pull image "i-do-not-exist": rpc error: code = Unknown desc = Error response from daemon: repository i-do-not-exist not found: does not exist or no pull access
 Warning  Failed     8s (x3 over 46s)   kubelet, aks-nodepool1-29770171-2  Error: ErrImagePull

问题显然通过Back-off pulling image "i-do-not-exist"消息表现出来。在更下面,我们可以看到来自容器服务器的消息,说明它未能拉取图像"i-do-not-exist"

当然,我们事先知道这将是结果,但类似的事情可能发生而我们没有注意到存在问题。原因可能是未能拉取镜像,或者其他无数的原因之一。然而,我们不应该坐在终端前,列出和描述 Pod 和其他类型的资源。相反,我们应该收到一个警报,告诉我们 Kubernetes 未能运行一个 Pod,只有在那之后,我们才应该开始查找问题的原因。所以,让我们创建一个新的警报,当 Pod 失败且无法恢复时通知我们。

像以前许多次一样,我们将查看 Prometheus 的图表值的旧定义和新定义之间的差异。

 1  diff mon/prom-values-errors.yml \
 2      mon/prom-values-phase.yml

输出如下。

136a137,146
> - name: pods
>   rules:
>   - alert: ProblematicPods
>     expr: sum(kube_pod_status_phase{phase=~"Failed|Unknown|Pending"}) by (phase) > 0
>     for: 1m
>     labels:
>       severity: notify
>     annotations:
>       summary: At least one Pod could not run
>       description: At least one Pod is in a problematic phase

我们定义了一个名为pod的新警报组。在其中,我们有一个名为ProblematicPodsalert,如果有一个或多个 Pod 的FailedUnknownPending阶段持续超过一分钟(1m)将触发警报。我故意将它设置为非常短的for持续时间,以便我们可以轻松测试它。后来,我们将切换到十五分钟的间隔,这将足够让 Kubernetes 在我们收到通知之前解决问题,而不会让我们陷入恐慌模式。

让我们用更新后的值更新 Prometheus 的图表。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-phase.yml

由于我们尚未解决problem Pod 的问题,我们很快应该在 Slack 上收到新的通知。让我们确认一下。

 1  open "https://devops20.slack.com/messages/CD8QJA8DS/"

如果您还没有收到通知,请稍等片刻。

我们收到了一条消息,说明“至少有一个 Pod 无法运行”。

图 3-36:Slack 上的警报消息

现在我们收到了一个通知,说其中一个 Pod 出了问题,我们应该去 Prometheus,挖掘数据,直到找到问题的原因,并解决它。但是,由于我们已经知道问题是什么(我们是故意创建的),我们将跳过所有这些,然后移除有问题的 Pod,然后继续下一个主题。

 1  kubectl delete pod problem

升级旧的 Pod

我们的主要目标应该是通过积极主动的方式防止问题发生。在我们无法预测问题即将出现的情况下,我们必须至少在问题发生后迅速采取反应措施来减轻问题。然而,还有第三种情况,可以宽泛地归类为积极主动。我们应该保持系统清洁和及时更新。

在许多可以保持系统最新的事项中,是确保我们的软件相对较新(已打补丁、已更新等)。一个合理的规则可能是在九十天后尝试更新软件,如果不是更早。这并不意味着我们在集群中运行的所有东西都应该比九十天更新,但这可能是一个很好的起点。此外,我们可能会创建更精细的策略,允许某些类型的应用程序(通常是第三方)在不升级的情况下存活,比如说半年。其他的,特别是我们正在积极开发的软件,可能会更频繁地升级。尽管如此,我们的起点是检测所有在九十天或更长时间内没有升级的应用程序。

就像本章中几乎所有其他练习一样,我们将从打开 Prometheus 的图形屏幕开始,探索可能帮助我们实现目标的指标。

 1  open "http://$PROM_ADDR/graph"

如果我们检查可用的指标,我们会看到有kube_pod_start_time。它的名称清楚地表明了它的目的。它以一个仪表的形式提供了每个 Pod 的启动时间的 Unix 时间戳。让我们看看它的作用。

请键入以下表达式,然后单击“执行”按钮。

 1  kube_pod_start_time

这些值本身没有用,教你如何从这些值计算出人类日期也没有意义。重要的是现在和那些时间戳之间的差异。

图 3-37:Prometheus 控制台视图,显示了 Pod 的启动时间

我们可以使用 Prometheus 的time()函数来返回自 1970 年 1 月 1 日 UTC(或 Unix 时间戳)以来的秒数。

请键入以下表达式,然后点击执行按钮。

 1  time()

就像kube_pod_start_time一样,我们得到了一个代表自 1970 年以来的秒数的长数字。除了值之外,唯一显着的区别是只有一个条目,而对于kube_pod_start_time,我们得到了集群中每个 Pod 的结果。

现在,让我们尝试结合这两个指标,以尝试检索每个 Pod 的年龄。

请键入以下表达式,然后点击执行按钮。

 1  time() -
 2  kube_pod_start_time

这次的结果是表示现在与每个 Pod 创建之间的秒数的更小的数字。在我的情况下(以下是屏幕截图),第一个 Pod(go-demo-5的一个副本)已经超过六千秒。那将是大约一百分钟(6096/60),或不到两个小时(100 分钟/60 分钟=1.666 小时)。

图 3-38:Prometheus 控制台视图,显示了 Pod 创建以来经过的时间

由于可能没有 Pod 比我们的目标九十天更老,我们将临时将其降低到一分钟(六十秒)。

请键入以下表达式,然后点击执行按钮。

 1  (
 2    time() -
 3    kube_pod_start_time{
 4      namespace!="kube-system"
 5    }
 6  ) > 60

在我的情况下,所有的 Pod 都比一分钟大(你的情况可能也是如此)。我们确认它可以工作,所以我们可以将阈值增加到九十天。要达到九十天,我们应该将阈值乘以六十得到分钟,再乘以六十得到小时,再乘以二十四得到天,最后再乘以九十。公式将是60 * 60 * 24 * 90。我们可以使用最终值7776000,但那会使查询更难解读。我更喜欢使用公式。

请键入以下表达式,然后点击执行按钮。

 1  (
 2    time() -
 3    kube_pod_start_time{
 4      namespace!="kube-system"
 5    }
 6  ) >
 7  (60 * 60 * 24 * 90)

毫无疑问,可能没有结果。如果您为本章创建了一个新的集群,如果您花了九十天才到这里,那您可能是地球上最慢的读者。这可能是我迄今为止写过的最长的一章,但仍不值得花九十天的时间来阅读。

现在我们知道要使用哪个表达式,我们可以在我们的设置中添加一个警报。

 1  diff mon/prom-values-phase.yml \
 2      mon/prom-values-old-pods.yml

输出如下。

146a147,154
> - alert: OldPods
>   expr: (time() - kube_pod_start_time{namespace!="kube-system"}) > 60
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Old Pods
>     description: At least one Pod has not been updated to more than 90 days

我们可以看到旧值和新值之间的差异在OldPods警报中。它包含了我们几分钟前使用的相同表达式。

我们保持了60秒的低阈值,以便我们可以看到警报的作用。以后,我们将把该值增加到 90 天。

没有必要指定for持续时间。一旦其中一个 Pod 的年龄达到三个月(加减),警报就会触发。

让我们使用更新后的值升级我们的 Prometheus 图表,并打开 Slack 频道,我们应该能看到新消息。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-old-pods.yml
 8
 9  open "https://devops20.slack.com/messages/CD8QJA8DS/"

现在只需等待片刻,直到新消息到达。它应该包含标题旧的 Pod和文本说明至少有一个 Pod 未更新超过 90 天

图 3-39:Slack 显示多个触发和解决的警报消息

这样一个通用的警报可能不适用于您所有的用例。但是,我相信您可以根据命名空间、名称或类似的内容将其拆分为多个警报。

现在我们有了一个机制,可以在我们的 Pod 过旧并且可能需要升级时接收通知,我们将进入下一个主题,探讨如何检索容器使用的内存和 CPU。

测量容器的内存和 CPU 使用

如果您熟悉 Kubernetes,您就会理解定义资源请求和限制的重要性。由于我们已经探讨了kubectl top pods命令,您可能已经设置了请求的资源以匹配当前的使用情况,并且可能已经定义了限制高于请求。这种方法可能在第一天有效。但是,随着时间的推移,这些数字将发生变化,我们将无法通过kubectl top pods获得全面的图片。我们需要知道容器在峰值负载时使用多少内存和 CPU,以及在压力较小时使用多少。我们应该随时间观察这些指标,并定期进行调整。

即使我们设法猜出容器需要多少内存和 CPU,这些数字也可能会从一个版本到另一个版本发生变化。也许我们引入了一个需要更多内存或 CPU 的功能?

我们需要观察资源使用情况随时间的变化,并确保它不会随着新版本的发布或用户数量的增加(或减少)而改变。现在,我们将专注于前一种情况,并探讨如何查看容器随时间使用了多少内存和 CPU。

像往常一样,我们将首先打开 Prometheus 的图表屏幕。

 1  open "http://$PROM_ADDR/graph"

我们可以通过container_memory_usage_bytes来检索容器的内存使用情况。

请键入下面的表达式,点击执行按钮,然后切换到图表屏幕。

 1  container_memory_usage_bytes

如果你仔细观察顶部的使用情况,你可能会感到困惑。似乎有些容器使用的内存远远超出预期的数量。

事实上,一些container_memory_usage_bytes记录包含累积值,我们应该排除它们,以便只检索单个容器的内存使用情况。我们可以通过仅检索在container_name字段中具有值的记录来实现这一点。

请键入下面的表达式,然后点击执行按钮。

 1  container_memory_usage_bytes{
 2    container_name!=""
 3  }

现在结果更有意义了。它反映了我们集群内运行的容器的内存使用情况。

稍后我们将基于容器资源设置警报。现在,我们假设我们想要检查特定容器的内存使用情况(例如prometheus-server)。由于我们已经知道可用标签之一是container_name,检索我们需要的数据应该是直截了当的。

请键入下面的表达式,然后点击执行按钮。

 1  container_memory_usage_bytes{
 2    container_name="prometheus-server"
 3  }

我们可以看到容器在过去一小时内内存使用情况的波动。通常,我们会对一天或一周这样更长的时间段感兴趣。我们可以通过点击图表上方的-和+按钮,或者直接在它们之间的字段中输入值(例如1w)来实现这一点。然而,改变持续时间可能不会有太大帮助,因为我们运行集群的时间不长。除非你阅读速度很慢,否则我们可能无法获得比几个小时更多的数据。

图 3-40:限制为 prometheus-server 的 Prometheus 图表屏幕上的容器内存使用情况

同样,我们应该能够检索容器的 CPU 使用情况。在这种情况下,我们正在寻找的指标可能是container_cpu_usage_seconds_total。然而,与container_memory_usage_bytes不同,它是一个计数器,我们将不得不结合sumrate来获得随时间变化的值。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2    container_cpu_usage_seconds_total{
 3      container_name="prometheus-server"
 4    }[5m]
 5  ))
 6  by (pod_name)

查询显示了五分钟间隔内的 CPU 秒速总和。我们添加了by (pod_name)到混合中,以便我们可以区分不同的 Pod,并查看一个是何时创建的,另一个是何时销毁的。

图 3-41:Prometheus 的图形屏幕,限制为 prometheus-server 的容器 CPU 使用率

如果这是一个“现实世界”的情况,我们下一步将是将实际资源使用与我们在 Prometheus资源中定义的进行比较。如果我们定义的与实际情况相比差异很大,我们可能应该更新我们的 Pod 定义(资源部分)。

问题在于使用“真实”资源使用情况来定义 Kubernetes 的资源将只会暂时提供有效值。随着时间的推移,我们的资源使用情况将会发生变化。负载可能会增加,新功能可能会更加耗费资源,等等。无论原因是什么,需要注意的关键一点是一切都是动态的,对于资源来说没有理由认为会有其他情况。在这种精神下,我们下一个挑战是找出当实际资源使用与我们在容器资源中定义的差异太大时如何获得通知。

将实际资源使用与定义的请求进行比较

如果我们在 Pod 中定义容器的资源而不依赖于实际使用情况,我们只是在猜测容器将使用多少内存和 CPU。我相信你已经知道为什么在软件行业中猜测是一个糟糕的主意,所以我将只关注 Kubernetes 方面。

Kubernetes 将没有指定资源的容器的 Pod 视为BestEffort Quality of ServiceQoS)。因此,如果它的内存或 CPU 不足以为所有的 Pod 提供服务,那么这些 Pod 将被强制删除,为其他 Pod 腾出空间。如果这样的 Pod 是短暂的,例如用作持续交付流程的一次性代理,BestEffort QoS 并不是一个坏主意。但是,当我们的应用是长期运行时,BestEffort QoS 应该是不可接受的。这意味着在大多数情况下,我们必须定义容器的resources

如果容器的resources(几乎总是)是必须的,我们需要知道要设置哪些值。我经常看到团队仅仅是猜测。“这是一个数据库,因此它需要大量的 RAM”,“它只是一个 API,不应该需要太多”是我经常听到的一些句子。这些猜测往往是由于无法测量实际使用情况而产生的。当出现问题时,这些团队会简单地将分配的内存和 CPU 加倍。问题解决了!

我从来不明白为什么会有人发明应用程序需要多少内存和 CPU。即使没有任何“花哨”的工具,我们在 Linux 中总是有top命令。我们可以知道我们的应用程序使用了多少。随着时间的推移,出现了更好的工具,我们所要做的就是谷歌“如何测量我的应用程序的内存和 CPU”。

当你需要当前数据时,你已经看到了kubectl top pods的操作,并且你已经开始熟悉 Prometheus 的强大功能。你没有理由去猜测。

但是,为什么我们关心资源使用情况与请求的资源相比呢?除了可能揭示潜在问题(例如内存泄漏)之外,不准确的资源请求和限制会阻止 Kubernetes 有效地完成其工作。例如,如果我们将内存请求定义为 1GB RAM,那么 Kubernetes 将从可分配的内存中删除这么多。如果一个节点有 2GB 的可分配 RAM,即使每个节点只使用 50MB RAM,也只能运行两个这样的容器。我们的节点将只使用可分配内存的一小部分,如果我们有集群自动缩放器,即使旧节点仍有大量未使用的内存,新节点也会被添加。

即使现在我们知道如何获取实际内存使用情况,每天都通过比较 YAML 文件和 Prometheus 中的结果来开始工作将是浪费时间。相反,我们将创建另一个警报,当请求的内存和 CPU 与实际使用情况相差太大时,它将发送通知给我们。这是我们的下一个任务。

首先,我们将重新打开 Prometheus 的图表屏幕。

 1 open "http://$PROM_ADDR/graph"

我们已经知道如何通过container_memory_usage_bytes获取内存使用情况,所以我们将直接开始检索请求的内存。如果我们可以将这两者结合起来,我们将得到请求的内存和实际内存使用之间的差异。

我们正在寻找的指标是kube_pod_container_resource_requests_memory_bytes,所以让我们以prometheus-server Pod 为例来试一试。

请键入以下表达式,然后点击“执行”按钮,切换到图表选项卡。

 1  kube_pod_container_resource_requests_memory_bytes{
 2    container="prometheus-server"
 3  }

从结果中我们可以看到,我们为prometheus-server容器请求了 500MB 的 RAM。

图 3-42:Prometheus 的图表屏幕,容器请求的内存限制为 prometheus-server

问题在于kube_pod_container_resource_requests_memory_bytes指标中,除了pod标签外,还有container_memory_usage_bytes使用pod_name。如果我们要将两者结合起来,我们需要将标签pod转换为pod_name。幸运的是,这不是我们第一次面临这个问题,我们已经知道解决方案是使用label_join函数,它将基于一个或多个现有标签创建一个新标签。

请键入以下表达式,然后点击“执行”按钮。

 1  sum(label_join(
 2    container_memory_usage_bytes{
 3      container_name="prometheus-server"
 4    },
 5    "pod",
 6    ",",
 7    "pod_name"
 8  ))
 9  by (pod)

这一次,我们不仅为指标添加了一个新标签,而且还通过这个新标签(by (pod))对结果进行了分组。

图 3-43:Prometheus 的图表屏幕,容器内存使用限制为 prometheus-server,并按从 pod_name 提取的 pod 标签进行分组。

现在我们可以将这两个指标结合起来,找出请求的内存和实际内存使用之间的差异。

请键入以下表达式,然后点击“执行”按钮。

 1  sum(label_join(
 2    container_memory_usage_bytes{
 3      container_name="prometheus-server"
 4    },
 5    "pod",
 6    ",",
 7    "pod_name"
 8  ))
 9  by (pod) /
10  sum(
11    kube_pod_container_resource_requests_memory_bytes{
12      container="prometheus-server"
13    }
14  )
15  by (pod)

在我的情况下(以下是屏幕截图),差异逐渐变小。它开始时大约为百分之六十,现在大约为百分之七十五。这样的差异对我们来说不足以采取任何纠正措施。

图 3-44:Prometheus 的图形屏幕,显示基于请求内存的容器内存使用百分比

现在我们已经看到了如何获取单个容器的保留和实际内存使用之间的差异,我们可能应该使表达更加普遍,并获取集群中的所有容器。但是,获取所有可能有点太多了。我们可能不想干扰运行在kube-system命名空间中的 Pod。它们可能是预先安装在集群中的,至少目前我们可能希望将它们保持原样。因此,我们将在查询中排除它们。

请键入以下表达式,然后按“执行”按钮。

 1  sum(label_join(
 2    container_memory_usage_bytes{
 3      namespace!="kube-system"
 4    },
 5    "pod",
 6    ",",
 7    "pod_name"
 8  ))
 9  by (pod) /
10  sum(
11    kube_pod_container_resource_requests_memory_bytes{
12      namespace!="kube-system"
13    }
14  )
15  by (pod)

结果应该是请求和实际内存之间差异的百分比列表,排除了kube-system中的 Pod。

在我的情况下,有相当多的容器使用的内存比我们请求的要多得多。主要问题是prometheus-alertmanager,它使用的内存比我们请求的要多三倍以上。这可能由于几个原因。也许我们请求的内存太少,或者它包含的容器没有指定requests。无论哪种情况,我们可能应该重新定义请求,不仅针对 Alertmanager,还针对所有使用的内存比请求的多 50%以上的其他 Pod。

图 3-45:Prometheus 的图形屏幕,显示基于请求内存的容器内存使用百分比,排除了来自 kube-system 命名空间的容器

我们即将定义一个新的警报,用于处理请求的内存远高于或远低于实际使用情况的情况。但在这样做之前,我们应该讨论应该使用的条件。一个警报可以在实际内存使用超过请求内存的 150%以上并持续一个小时以上时触发。这将消除由内存使用暂时激增引起的误报(这就是为什么我们也有limits)。另一个警报可以处理内存使用量低于请求量 50%以上的情况。但是,在这种警报情况下,我们可能会添加另一个条件。

有些应用程序太小,我们可能永远无法调整它们的请求。我们可以通过添加另一个条件来排除这些情况,该条件将忽略仅保留了 5MB 或更少内存的 Pod。

最后,这个警报可能不需要像之前的那样频繁地触发。我们应该相对快速地知道我们的应用程序是否使用了比我们打算给予的更多内存,因为这可能是内存泄漏、显著增加的流量或其他潜在危险情况的迹象。但是,如果内存使用远低于预期,问题就不那么紧急了。我们应该纠正它,但没有必要紧急采取行动。因此,我们将后一个警报的持续时间设置为六小时。

现在我们已经制定了一些规则,我们可以看一下旧值和新值之间的另一个差异。

 1  diff mon/prom-values-old-pods.yml \
 2      mon/prom-values-req-mem.yml

输出如下。

148c148
<   expr: (time() - kube_pod_start_time{namespace!="kube-system"}) > 60
---
>   expr: (time() - kube_pod_start_time{namespace!="kube-system"}) > (60 * 60 * 24 * 90)
154a155,172
> - alert: ReservedMemTooLow
>   expr: sum(label_join(container_memory_usage_bytes{namespace!="kube-system", namespace!="ingress-nginx"}, "pod", ",", "pod_name")) by (pod) /
 sum(kube_pod_container_resource_requests_memory_bytes{namespace!="kube-system"}) by (pod) > 1.5
>   for: 1m
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Reserved memory is too low
>     description: At least one Pod uses much more memory than it reserved
> - alert: ReservedMemTooHigh
>   expr: sum(label_join(container_memory_usage_bytes{namespace!="kube-system", namespace!="ingress-nginx"}, "pod", ",", "pod_name")) by (pod) / sum(kube_pod_container_resource_requests_memory_bytes{namespace!="kube-system"}) by (pod) < 0.5 and sum(kube_pod_container_resource_requests_memory_bytes{namespace!="kube-system"}) by (pod) > 5.25e+06
>   for: 6m
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Reserved memory is too high
>     description: At least one Pod uses much less memory than it reserved

首先,我们将OldPods警报的阈值重新设置为其预期值九十天(60 * 60 * 24 * 90)。这样我们就可以阻止它仅用于测试目的触发警报。

接下来,我们定义了一个名为ReservedMemTooLow的新警报。如果使用的内存比请求的内存大1.5倍,它将触发。警报的挂起状态持续时间设置为1m,只是为了我们可以在不等待整个小时的情况下看到结果。稍后,我们将把它恢复为1h

ReservedMemTooHigh警报与之前的部分类似,不同之处在于,如果实际内存和请求内存之间的差异小于0.5,并且如果这种情况持续超过6m(我们稍后将其更改为6h),则会触发警报。表达式的第二部分是新的。它要求 Pod 中的所有容器都具有超过 5MB 的请求内存(5.25e+06)。通过第二个语句(用and分隔),我们可以避免处理太小的应用程序。如果需要的内存小于 5MB,我们应该忽略它,并且可能要祝贺背后的团队使其如此高效。

现在,让我们使用更新后的值升级我们的 Prometheus 图表,并打开图表屏幕。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-req-mem.yml

我们不会等到警报开始触发。相反,我们将尝试实现类似的目标,但使用 CPU。

可能没有必要解释我们将使用的表达式的过程。我们将直接跳入基于 CPU 的警报,探索旧值和新值之间的差异。

 1  diff mon/prom-values-req-mem.yml \
 2      mon/prom-values-req-cpu.yml

输出如下。

157c157
<   for: 1m
---
>   for: 1h
166c166
<   for: 6m
---
>   for: 6h
172a173,190
> - alert: ReservedCPUTooLow
>   expr: sum(label_join(rate(container_cpu_usage_seconds_total{namespace!="kube-system", namespace!="ingress-nginx", pod_name!=""}[5m]), "pod", ",", "pod_name")) by (pod) / sum(kube_pod_container_resource_requests_cpu_cores{namespace!="kube-system"}) by (pod) > 1.5
>   for: 1m
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Reserved CPU is too low
>     description: At least one Pod uses much more CPU than it reserved
> - alert: ReservedCPUTooHigh
>   expr: sum(label_join(rate(container_cpu_usage_seconds_total{namespace!="kube-system", pod_name!=""}[5m]), "pod", ",", "pod_name")) by (pod) / sum(kube_pod_container_resource_requests_cpu_cores{namespace!="kube-system"}) by (pod) < 0.5 and 
sum(kube_pod_container_resource_requests_cpu_cores{namespace!="kube-system"}) by (pod) > 0.005
>   for: 6m
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Reserved CPU is too high
>     description: At least one Pod uses much less CPU than it reserved

前两组差异是为我们之前探讨的ReservedMemTooLowReservedMemTooHigh警报定义更明智的阈值。在更下面,我们可以看到两个新的警报。

如果 CPU 使用量超过请求量的 1.5 倍,将触发ReservedCPUTooLow警报。同样,只有当 CPU 使用量少于请求量的一半,并且我们请求的 CPU 毫秒数超过 5 时,才会触发ReservedCPUTooHigh警报。因为 5MB RAM 太多而收到通知将是浪费时间。

这两个警报都设置为在短时间内持续存在(1m6m),这样我们就可以看到它们的作用,而不必等待太长时间。

现在,让我们使用更新后的值升级我们的 Prometheus 图表。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-req-cpu.yml

我会让你去检查是否有任何警报触发,以及它们是否从 Alertmanager 转发到 Slack。你现在应该知道如何做了。

接下来,我们将转移到本章的最后一个警报。

比较实际资源使用与定义的限制

了解容器使用资源与请求相比使用过多或过少的情况有助于我们更精确地定义资源,并最终帮助 Kubernetes 更好地决定在哪里调度我们的 Pods。在大多数情况下,请求和实际资源使用之间存在较大的差异通常不会导致故障。相反,更有可能导致 Pods 的分布不平衡或节点过多。另一方面,限制则是另一回事。

如果我们容器作为 Pods 的资源使用达到指定的limits,Kubernetes 可能会杀死这些容器,如果没有足够的内存。它这样做是为了保护系统的完整性。被杀死的 Pod 并不是一个永久性的问题,因为 Kubernetes 几乎会立即重新调度它们,如果有足够的容量。

如果我们使用集群自动缩放,即使容量不足,一旦检测到一些 Pod 处于挂起状态(无法调度),新节点将被添加。因此,如果资源使用超过限制,世界不太可能会结束。

然而,杀死和重新安排 Pod 可能会导致停机时间。显然,可能会发生更糟糕的情况。但我们不会深入讨论。相反,我们将假设我们应该意识到一个 Pod 即将达到其极限,我们可能需要调查发生了什么,并且可能需要采取一些纠正措施。也许最新的发布引入了内存泄漏?或者负载增加超出了我们预期和测试的范围,导致内存使用增加。目前不关注接近极限的内存使用的原因,而是关注是否达到了极限。

首先,我们将返回 Prometheus 的图表屏幕。

 1  open "http://$PROM_ADDR/graph"

我们已经知道可以通过container_memory_usage_bytes指标获取实际内存使用情况。由于我们已经探讨了如何获取请求的内存,我们可以猜测极限是类似的。它们确实是,可以通过kube_pod_container_resource_limits_memory_bytes获取。由于其中一个指标与以前相同,另一个非常相似,我们将直接执行完整查询。

请键入以下表达式,按“执行”按钮,然后切换到图表选项卡。

 1  sum(label_join(
 2    container_memory_usage_bytes{
 3      namespace!="kube-system"
 4    }, 
 5    "pod", 
 6    ",", 
 7    "pod_name"
 8  ))
 9  by (pod) /
10  sum(
11    kube_pod_container_resource_limits_memory_bytes{
12      namespace!="kube-system"
13    }
14  )
15  by (pod)

在我的情况下(以下是屏幕截图),我们可以看到相当多的 Pod 使用的内存超过了定义的极限。

幸运的是,我的集群中有多余的容量,Kubernetes 没有迫切需要杀死任何 Pod。此外,问题可能不在于 Pod 使用的超出其极限的情况,而是这些 Pod 中并非所有容器都设置了极限。无论哪种情况,我可能应该更新这些 Pod/容器的定义,并确保它们的极限高于几天甚至几周的平均使用量。

图 3-46:基于内存限制的容器内存使用百分比的 Prometheus 图表屏幕,排除了 kube-system 命名空间中的内存使用情况

接下来,我们将详细探讨旧值和新值之间的差异。

 1  diff mon/prom-values-req-cpu.yml \
 2      mon/prom-values-limit-mem.yml

输出如下。

175c175
<   for: 1m
---
>   for: 1h
184c184
<   for: 6m
---
>   for: 6h
190a191,199
> - alert: MemoryAtTheLimit
>   expr: sum(label_join(container_memory_usage_bytes{namespace!="kube-system"}, "pod", ",", "pod_name")) by (pod) / sum(kube_pod_container_resource_limits_memory_bytes{namespace!="kube-system"}) by (pod) > 0.8
>   for: 1h
>   labels:
>     severity: notify
>     frequency: low
>   annotations:
>     summary: Memory usage is almost at the limit
>     description: At least one Pod uses memory that is close it its limit

除了恢复以前使用的警报的合理阈值之外,我们定义了一个名为MemoryAtTheLimit的新警报。如果实际使用超过极限的百分之八十(0.8)超过一小时(1h),它将触发。

接下来是升级我们的 Prometheus 图表。

 1  helm upgrade -i prometheus \
 2    stable/prometheus \
 3    --namespace metrics \
 4    --version 7.1.3 \
 5    --set server.ingress.hosts={$PROM_ADDR} \
 6    --set alertmanager.ingress.hosts={$AM_ADDR} \
 7    -f mon/prom-values-limit-mem.yml

最后,我们可以打开 Prometheus 的警报屏幕,并确认新的警报确实被添加到了其中。

 1  open "http://$PROM_ADDR/alerts"

我们不会重复为 CPU 创建类似的警报的步骤。你应该知道如何自己做。

现在呢?

我们探索了相当多的 Prometheus 指标、表达式和警报。我们看到了如何将 Prometheus 警报与 Alertmanager 连接,并从那里将它们转发到一个应用程序到另一个应用程序。

到目前为止,我们所做的只是冰山一角。要探索所有我们可能使用的指标和表达式将需要太多的时间(和空间)。尽管如此,我相信现在你知道了一些更有用的指标,而且你将能够用你自己特定的指标来扩展它们。

我敦促你发送给我你发现有用的表达式和警报。你知道在哪里找到我(DevOps20 (slack.devops20toolkit.com/) Slack,viktor@farcic 邮件,@vfarcic 推特,等等)。

目前,我会让你决定是直接进入下一章,销毁整个集群,还是只移除我们安装的资源。如果你选择后者,请使用接下来的命令。

 1  helm delete prometheus --purge
 2
 3  helm delete go-demo-5 --purge
 4
 5  kubectl delete ns go-demo-5 metrics

在你离开之前,你可能想回顾一下本章的要点。

  • Prometheus 是一个设计用于获取(拉取)和存储高维时间序列数据的数据库(某种程度上)。

  • 每个人都应该利用的四个关键指标是延迟、流量、错误和饱和度。

第四章:通过指标和警报发现的问题调试

当你排除了不可能的,无论剩下什么,无论多么不可能,都必须是真相。

  • 斯波克

到目前为止,我们已经探讨了如何收集指标以及如何创建警报,以便在出现问题时通知我们。我们还学会了如何查询指标并搜索我们在尝试找到问题原因时可能需要的信息。我们将在此基础上继续,并尝试调试一个模拟的问题。

仅仅说一个应用程序工作不正常是不够的。我们应该更加精确。我们的目标是不仅能够准确定位哪个应用程序出现故障,还能够确定是其中的哪个部分出了问题。我们应该能够指责特定的功能、方法、请求路径等等。我们在检测应用程序的哪个部分导致问题时越精确,我们就越快地找到问题的原因。因此,通过新版本的发布(热修复)、扩展或其他手段修复问题应该更容易更快。

让我们开始吧。在模拟需要解决的问题之前,我们需要一个集群(除非您已经有一个)。

创建一个集群

vfarcic/k8s-specs (github.com/vfarcic/k8s-specs) 仓库将继续是我们用于示例的 Kubernetes 定义的来源。我们将确保通过拉取最新版本使其保持最新。

本章中的所有命令都可以在04-instrument.sh (gist.github.com/vfarcic/851b37be06bb7652e55529fcb28d2c16) Gist 中找到。就像上一章一样,它不仅包含命令,还包括 Prometheus 的表达式。它们都被注释了(用#)。如果您打算从 Gist 中复制和粘贴表达式,请排除注释。每个表达式顶部都有# Prometheus expression的注释,以帮助您识别它。

 1  cd k8s-specs
 2
 3  git pull

鉴于我们已经学会了如何安装一个完全可操作的 Prometheus 和其图表中的其他工具,并且我们将继续使用它们,我将其移至 Gists。接下来的内容是我们在上一章中使用的内容的副本,还增加了环境变量 PROM_ADDRAM_ADDR,以及安装 Prometheus Chart 的步骤。请创建一个符合(或超出)下面 Gists 中指定要求的集群,除非您已经有一个满足这些要求的集群。

现在我们已经准备好面对可能需要调试的第一个模拟问题。

面对灾难

让我们探索一个灾难场景。坦率地说,这不会是一个真正的灾难,但它将需要我们找到解决问题的方法。

我们将从安装已经熟悉的go-demo-5应用程序开始。

 1  GD5_ADDR=go-demo-5.$LB_IP.nip.io
 2
 3  helm install \
 4      https://github.com/vfarcic/go-demo-5/releases/download/
    0.0.1/go-demo-5-0.0.1.tgz \
 5      --name go-demo-5 \
 6      --namespace go-demo-5 \
 7      --set ingress.host=$GD5_ADDR
 8
 9  kubectl -n go-demo-5 \
10      rollout status \
11      deployment go-demo-5

我们使用GD5_ADDR声明了地址,通过该地址我们将能够访问应用程序。我们在安装go-demo-5图表时将其用作ingress.host变量。为了安全起见,我们等到应用程序部署完成,从部署的角度来看,唯一剩下的就是通过发送 HTTP 请求来确认它正在运行。

 1  curl http://$GD5_ADDR/demo/hello

输出是开发人员最喜欢的消息hello, world!

接下来,我们将通过发送二十个持续时间长达十秒的慢请求来模拟问题。这将是我们模拟可能需要修复的问题。

 1  for i in {1..20}; do
 2      DELAY=$[ $RANDOM % 10000 ]
 3      curl "http://$GD5_ADDR/demo/hello?delay=$DELAY"
 4  done

由于我们已经有了 Prometheus 的警报,我们应该在 Slack 上收到通知,说明应用程序太慢了。然而,许多读者可能会在同一个频道进行这些练习,并且可能不清楚消息是来自我们。相反,我们将打开 Prometheus 的警报屏幕以确认存在问题。在“真实”环境中,您不会检查 Prometheus 警报,而是等待在 Slack 上收到通知,或者您选择的其他通知工具。

 1  open "http://$PROM_ADDR/alerts"

几分钟后(不要忘记刷新屏幕),AppTooSlow警报应该触发,让我们知道我们的一个应用程序运行缓慢,我们应该采取措施解决问题。

忠于每章将展示不同 Kubernetes 版本的输出和截图的承诺,这次轮到 minikube 了。

图 4-1:Prometheus 中一个警报处于触发状态

我们假设我们没有故意生成慢请求,所以我们将尝试找出问题所在。哪个应用程序太慢了?我们可以传递什么有用的信息给团队,以便他们尽快解决问题?

第一个逻辑调试步骤是执行与警报使用的相同表达式。请展开AppTooSlow警报,并单击表达式的链接。您将被重定向到已经预填充的图形屏幕。单击“执行”按钮,切换到图形选项卡。

从图表中我们可以看到,慢请求数量激增。警报被触发是因为不到 95%的响应在 0.25 秒内完成。根据我的图表(随后的截图),零百分比的响应在 0.25 秒内完成,换句话说,所有响应都比那慢。片刻之后,情况略有改善,只有 6%的请求很快。

总的来说,我们面临着太多请求得到缓慢响应的情况,我们应该解决这个问题。主要问题是如何找出缓慢的原因是什么?

图 4-2:百分比请求快速响应的图表

尝试执行不同的表达式。例如,我们可以输出该ingress(应用程序)的请求持续时间的速率。

请键入以下表达式,然后点击执行按钮。

 1  sum(rate(
 2      nginx_ingress_controller_request_duration_seconds_sum{
 3          ingress="go-demo-5"
 4      }[5m]
 5  )) /
 6  sum(rate(
 7      nginx_ingress_controller_request_duration_seconds_count{
 8          ingress="go-demo-5"
 9      }[5m]
10  ))

该图表显示了请求持续时间的历史记录,但它并没有让我们更接近揭示问题的原因,或者更准确地说,是应用程序的哪一部分慢。我们可以尝试使用其他指标,但它们或多或少同样泛泛,并且可能不会让我们有所收获。我们需要更详细的特定于应用程序的指标。我们需要来自go-demo-5应用程序内部的数据。

使用仪器提供更详细的指标

我们不应该只是说go-demo-5应用程序很慢。这不会为我们提供足够的信息,让我们快速检查代码以找出缓慢的确切原因。我们应该能做得更好,并推断出应用程序的哪一部分表现不佳。我们能否找出产生缓慢响应的特定路径?所有方法都一样慢吗,还是问题只限于一个?我们知道哪个函数产生缓慢吗?在这种情况下,我们应该能够回答许多类似的问题。但是,根据当前的指标,我们无法做到。它们太泛泛,通常只能告诉我们特定的 Kubernetes 资源表现不佳。我们收集的指标太广泛,无法回答特定于应用程序的问题。

到目前为止,我们探讨的指标是出口和仪器化的组合。出口负责获取现有的指标并将其转换为 Prometheus 友好格式。一个例子是 Node Exporter(github.com/prometheus/node_exporter),它获取“标准”Linux 指标并将其转换为 Prometheus 的时间序列格式。另一个例子是 kube-state-metrics(github.com/kubernetes/kube-state-metrics),它监听 Kube API 服务器并生成资源状态的指标。

仪器化指标已经内置到应用程序中。它们是我们应用程序代码的一个组成部分,通常通过/metrics端点公开。

将指标添加到应用程序的最简单方法是通过 Prometheus 客户端库之一。在撰写本文时,Go(github.com/prometheus/client_golang)、Java 和 Scala(github.com/prometheus/client_java)、Python(github.com/prometheus/client_python)和 Ruby(github.com/prometheus/client_ruby)库是官方提供的。

除此之外,社区还支持 Bash (github.com/aecolley/client_bash),C++ (github.com/jupp0r/prometheus-cpp),Common Lisp (github.com/deadtrickster/prometheus.cl),Elixir (github.com/deadtrickster/prometheus.ex),Erlang (github.com/deadtrickster/prometheus.erl),Haskell (github.com/fimad/prometheus-haskell),Lua for Nginx (github.com/knyar/nginx-lua-prometheus),Lua for Tarantool (github.com/tarantool/prometheus),.NET / C# (github.com/andrasm/prometheus-net),Node.js (github.com/siimon/prom-client),Perl (metacpan.org/pod/Net::Prometheus),PHP (github.com/Jimdo/prometheus_client_php),和 Rust (github.com/pingcap/rust-prometheus)。即使您使用不同的语言编写代码,也可以通过以文本为基础的输出格式(prometheus.io/docs/instrumenting/exposition_formats/)轻松提供符合 Prometheus 的指标。

收集指标的开销应该可以忽略不计,而且由于 Prometheus 定期获取它们,输出它们的开销也应该很小。即使您选择不使用 Prometheus,或者切换到其他工具,该格式也正在成为标准,您的下一个指标收集工具很可能也会期望相同的数据。

总之,没有理由不将指标集成到您的应用程序中,正如您很快将看到的那样,它们提供了我们无法从外部获得的宝贵信息。

让我们来看一个go-demo-5中已经标记的指标的例子。

 1  open "https://github.com/vfarcic/go-demo-5/blob/master/main.go"

该应用程序是用 Go 语言编写的。如果这不是您选择的语言,不要担心。我们只是快速看一下一些例子,以了解仪表化背后的逻辑,而不是确切的实现。

第一个有趣的部分如下。

 1  ...
 2  var (
 3    histogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{
 4      Subsystem: "http_server",
 5      Name:      "resp_time",
 6      Help:      "Request response time",
 7    }, []string{
 8      "service",
 9      "code",
10      "method",
11      "path",
12    })
13  )
14  ...

我们定义了一个包含一些选项的 Prometheus 直方图向量的变量。SybsystemName形成了基本指标http_server_resp_time。由于它是一个直方图,最终的指标将通过添加_bucket_sum_count后缀来创建。

请参考histogram (prometheus.io/docs/concepts/metric_types/#histogram) 文档,了解有关 Prometheus 指标类型的更多信息。

最后一部分是一个字符串数组([]string),定义了我们想要添加到指标中的所有标签。在我们的情况下,这些标签是servicecodemethodpath。标签可以是我们需要的任何东西,只要它们提供了我们在查询这些指标时可能需要的足够信息。

兴趣点是recordMetrics函数。

 1  ...
 2  func recordMetrics(start time.Time, req *http.Request, code int) {
 3    duration := time.Since(start)
 4    histogram.With(
 5      prometheus.Labels{
 6        "service": serviceName,
 7        "code":    fmt.Sprintf("%d", code),
 8        "method":  req.Method,
 9        "path":    req.URL.Path,
10      },
11    ).Observe(duration.Seconds())
12  }
13  ...

我创建了一个辅助函数,可以从代码的不同位置调用。它接受start时间、Request和返回的code作为参数。函数本身通过将当前时间与start时间相减来计算durationdurationObserve函数中使用,并提供指标的值。还有标签,将帮助我们在以后微调我们的表达式。

最后,我们将看一个示例,其中调用了recordMetrics函数。

 1  ...
 2  func HelloServer(w http.ResponseWriter, req *http.Request) {
 3    start := time.Now()
 4    defer func() { recordMetrics(start, req, http.StatusOK) }()
 5    ...
 6  }
 7  ...

HelloServer函数是返回您已经看到多次的hello, world!响应的函数。该函数的细节并不重要。在这种情况下,唯一重要的部分是defer func() { recordMetrics(start, req, http.StatusOK) }()这一行。在 Go 中,defer允许我们在它所在的函数结束时执行某些操作。在我们的情况下,这个操作是调用recordMetrics函数,记录请求的持续时间。换句话说,在执行离开HelloServer函数之前,它将通过调用recordMetrics函数记录持续时间。

我不会深入探讨包含仪表的代码,因为那将意味着您对 Go 背后的复杂性感兴趣,而我试图让这本书与语言无关。我会让您参考您喜欢的语言的文档和示例。相反,我们将看一下go-demo-5中的仪表指标的实际应用。

 1  kubectl -n metrics \
 2      run -it test \
 3      --image=appropriate/curl \
 4      --restart=Never \
 5      --rm \
 6      -- go-demo-5.go-demo-5:8080/metrics

我们创建了一个基于appropriate/curl镜像的 Pod,并通过使用地址go-demo-5.go-demo-5:8080/metrics向服务发送了一个请求。第一个go-demo-5是服务的名称,第二个是它所在的命名空间。结果,我们得到了该应用程序中所有可用的受监控指标的输出。我们不会逐个讨论所有这些指标,而只会讨论由http_server_resp_time直方图创建的指标。

输出的相关部分如下。

...
# HELP http_server_resp_time Request response time
# TYPE http_server_resp_time histogram
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.005"} 931
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.01"} 931
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.025"} 931
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.05"} 931
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.1"} 934
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.25"} 935
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="0.5"} 935
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="1"} 936
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="2.5"} 936
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="5"} 937
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="10"} 942
http_server_resp_time_bucket{code="200",method="GET",path="/demo/hello",service="go-demo",le="+Inf"} 942
http_server_resp_time_sum{code="200",method="GET",path="/demo/hello",service="go-demo"} 38.87928942600006
http_server_resp_time_count{code="200",method="GET",path="/demo/hello",service="go-demo"} 942
...

我们可以看到,在应用程序代码中使用的 Go 库从http_server_resp_time直方图中创建了相当多的指标。我们得到了每个十二个桶的指标(http_server_resp_time_bucket),一个持续时间的总和指标(http_server_resp_time_sum),以及一个计数指标(http_server_resp_time_count)。如果我们发出具有不同标签的请求,我们将得到更多指标。目前,这十四个指标都来自于响应 HTTP 代码200的请求,使用GET方法,发送到/demo/hello路径,并来自go-demo服务(应用程序)。如果我们创建具有不同方法(例如POST)或不同路径的请求,指标数量将增加。同样,如果我们在其他应用程序中实现相同的受监控指标(但具有不同的service标签),我们将拥有具有相同键(http_server_resp_time)的指标,这将提供有关多个应用程序的见解。这引发了一个问题,即我们是否应该统一所有应用程序中的指标名称,还是不统一。

我更喜欢在所有应用程序中具有相同名称的相同类型的受监控指标。例如,所有收集响应时间的指标都可以称为http_server_resp_time。这简化了在 Prometheus 中查询数据。与其从每个单独的应用程序中了解受监控指标,不如从一个应用程序中了解所有应用程序的知识。另一方面,我赞成让每个团队完全控制他们的应用程序。这包括决定要实现哪些指标以及如何调用它们。

总的来说,这取决于团队的结构和职责。如果一个团队完全负责他们的应用程序,并且调试特定于他们应用程序的问题,那么标准化已经被仪表化指标的名称是没有必要的。另一方面,如果监控是集中的,并且其他团队可能期望从该领域的专家那里获得帮助,那么创建命名约定是必不可少的。否则,我们可能会轻易地得到成千上万个具有不同名称和类型的指标,尽管它们大多提供相同的信息。

在本章的其余部分,我将假设我们同意在所有适用的应用程序中都有http_server_resp_time直方图。

现在,让我们看看如何告诉 Prometheus 它应该从go-demo-5应用程序中拉取指标。如果我们能告诉 Prometheus 从所有有仪表化指标的应用程序中拉取数据,那将更好。实际上,现在我想起来了,我们在上一章中还没有讨论 Prometheus 是如何发现 Node Exporter 和 Kube State Metrics 的。所以,让我们简要地通过发现过程。

一个很好的起点是 Prometheus 的目标屏幕。

 1  open "http://$PROM_ADDR/targets"

最有趣的目标组是kubernetes-service-endpoints。如果我们仔细看标签,我们会发现每个标签都有kubernetes_name,其中三个目标将其设置为go-demo-5。Prometheus 不知何故发现我们有该应用程序的三个副本,并且指标可以通过端口8080获得。如果我们进一步观察,我们会注意到prometheus-node-exporter也在其中,每个节点在集群中都有一个。

对于prometheus-kube-state-metrics也是一样的。在该组中可能还有其他应用程序。

图 4-3:kubernetes-service-endpoints Prometheus 的目标

Prometheus 通过 Kubernetes 服务发现了所有目标。它从每个服务中提取了端口,并假定数据可以通过/metrics端点获得。因此,我们在集群中拥有的每个应用程序,只要可以通过 Kubernetes 服务访问,就会自动添加到 Prometheus 的目标kubernetes-service-endpoints组中。我们无需摆弄 Prometheus 的配置来将go-demo-5添加到其中。它只是被发现了。相当不错,不是吗?

在某些情况下,一些指标将无法访问,并且该目标将标记为红色。例如,在 minikube 中的kube-dns无法从 Prometheus 访问。这很常见,只要这不是我们确实需要的指标来源之一,就不必惊慌。

接下来,我们将快速查看一下我们可以使用来自go-demo-5的仪表化指标编写的一些表达式。

 1  open "http://$PROM_ADDR/graph"

请键入接下来的表达式,按“执行”按钮,然后切换到图表选项卡。

 1  http_server_resp_time_count

我们可以看到三条线对应于go-demo-5的三个副本。这应该不会让人感到惊讶,因为每个副本都是从应用程序的每个副本的仪表化指标中提取的。由于这些指标是只能增加的计数器,图表的线条不断上升。

图 4-4:http_server_resp_time_count 计数器的图表

这并不是很有用。如果我们对请求计数的速率感兴趣,我们会将先前的表达式包含在rate()函数中。我们以后会这样做。现在,我们将编写最简单的表达式,以便得到每个请求的平均响应时间。

请键入接下来的表达式,然后按“执行”按钮。

 1  http_server_resp_time_sum{
 2      kubernetes_name="go-demo-5"
 3  } /
 4  http_server_resp_time_count{
 5      kubernetes_name="go-demo-5"
 6  }

表达式本身应该很容易理解。我们将所有请求的总和除以计数。由于我们已经发现问题出现在go-demo-5应用程序中,我们使用kubernetes_name标签来限制结果。尽管这是我们集群中当前唯一运行该指标的应用程序,但习惯于这样做是个好主意,因为在将来我们将扩展到其他应用程序时,可能会有其他应用程序。

我们可以看到,平均请求持续时间在一段时间内增加,只是在稍后又接近初始值。这个峰值与我们之前发送的二十个慢请求相吻合。在我的情况下(以下是屏幕截图),峰值接近平均响应时间的 0.1 秒,然后在稍后降至大约 0.02 秒。

图 4-5:累积平均响应时间的图表

请注意,我们刚刚执行的表达式存在严重缺陷。它显示的是累积平均响应时间,而不是显示rate。但是,你已经知道了。那只是对仪表度量的一个预演,而不是它的“真正”用法(很快就会出现)。

您可能会注意到,即使是峰值也非常低。它肯定比我们只通过curl发送二十个慢请求所期望的要低。原因在于我们不是唯一一个发出这些请求的人。readinessProbelivenessProbe也在发送请求,并且非常快。与上一章不同的是,我们只测量通过 Ingress 进入的请求,这一次我们捕获了进入应用程序的所有请求,包括健康检查。

现在我们已经看到了在我们的go-demo-5应用程序内部生成的http_server_resp_time度量标准的一些示例,我们可以利用这些知识来尝试调试导致我们走向仪表化的模拟问题。

使用内部度量标准来调试潜在问题

我们将重新发送慢响应的请求,以便我们回到开始本章的同一点。

 1  for i in {1..20}; do
 2      DELAY=$[ $RANDOM % 10000 ]
 3      curl "http://$GD5_ADDR/demo/hello?delay=$DELAY"
 4  done
 5
 6  open "http://$PROM_ADDR/alerts"

我们发送了二十个请求,这些请求将产生随机持续时间的响应(最长十秒)。随后,我们打开了 Prometheus 的警报屏幕。

一段时间后,AppTooSlow警报应该会触发(记得刷新你的屏幕),我们有一个(模拟的)需要解决的问题。在我们开始惊慌和匆忙行事之前,我们将尝试找出问题的原因。

请点击AppTooSlow警报的表达式。

我们被重定向到具有警报预填表达式的图形屏幕。请随意点击表达式按钮,即使它不会提供任何额外的信息,除了应用程序一开始很快,然后因某种莫名其妙的原因变慢。

您将无法从该表达式中收集更多详细信息。您将不知道所有方法是否都很慢,是否只有特定路径响应缓慢,也不会知道任何其他特定于应用程序的细节。简而言之,nginx_ingress_controller_request_duration_seconds度量标准太泛化了。它作为通知我们应用程序响应时间增加的一种方式服务得很好,但它并不提供足够关于问题原因的信息。为此,我们将切换到 Prometheus 直接从go-demo-5副本中检索的http_server_resp_time度量标准。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2      http_server_resp_time_bucket{
 3          le="0.1",
 4          kubernetes_name="go-demo-5"
 5      }[5m]
 6  )) /
 7  sum(rate(
 8      http_server_resp_time_count{
 9          kubernetes_name="go-demo-5"
10      }[5m]
11  ))

如果你还没有切换到图表选项卡,请切换到那里。

该表达式与我们以前使用nginx_ingress_controller_request_duration_seconds_sum指标时编写的查询非常相似。我们正在将 0.1 秒桶中的请求速率与所有请求的速率进行比较。

在我的案例中(随后的屏幕截图),我们可以看到快速响应的百分比下降了两次。这与我们之前发送的模拟慢请求相吻合。

图 4-6:使用仪器化指标测量的快速请求百分比的图表

到目前为止,使用仪器化指标http_server_resp_time_countnginx_ingress_controller_request_duration_seconds_sum相比,并没有提供任何实质性的好处。如果仅此而已,我们可以得出结论,添加仪器化是一种浪费。然而,我们还没有将标签包含在我们的表达式中。

假设我们想按methodpath对请求进行分组。这可能会让我们更好地了解慢速是全局性的,还是仅限于特定类型的请求。如果是后者,我们将知道问题出在哪里,并希望能够快速找到罪魁祸首。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2      http_server_resp_time_bucket{
 3          le="0.1",
 4          kubernetes_name="go-demo-5"
 5      }[5m]
 6  ))
 7  by (method, path) /
 8  sum(rate(
 9      http_server_resp_time_count{
10          kubernetes_name="go-demo-5"
11      }[5m]
12  ))
13  by (method, path)

该表达式几乎与之前的表达式相同。唯一的区别是添加了by (method, path)语句。因此,我们得到了按methodpath分组的快速响应百分比。

输出并不代表“真实”的使用情况。通常,我们会看到许多不同的线条,每条线代表被请求的每种方法和路径。但是,由于我们只对/demo/hello使用 HTTP GET 进行了请求,我们的图表有点无聊。你得想象还有许多其他线条。

通过研究图表,我们发现除了一条线(我们仍在想象许多条线)之外,其他所有线都接近于百分之百的快速响应。那条急剧下降的线应该是具有/demo/hello路径和GET方法的那条线。然而,如果这确实是一个真实的情景,我们的图表可能会有太多线条,我们可能无法轻松地加以区分。我们的表达式可能会受益于添加一个阈值。

请键入以下表达式,然后按“执行”按钮。

 1  sum(rate(
 2      http_server_resp_time_bucket{
 3          le="0.1",
 4          kubernetes_name="go-demo-5"
 5      }[5m]
 6  ))
 7  by (method, path) /
 8  sum(rate(
 9      http_server_resp_time_count{
10          kubernetes_name="go-demo-5"
11      }[5m]
12  ))
13  by (method, path) < 0.99

唯一的添加是 <0.99 的阈值。因此,我们的图表排除了所有结果(所有路径和方法),只留下低于百分之九十九(0.99)的结果。我们去除了所有噪音,只关注超过百分之一的所有请求缓慢的情况(或者少于百分之九十九的请求快速)。结果现在很明确。问题出在处理 /demo/hello 路径上的 GET 请求的函数中。我们通过图表下方提供的标签知道了这一点。

图 4-7:使用仪器测量的快速请求百分比的图表,限制在百分之九十九以下的结果。

现在我们几乎知道问题的确切位置,剩下的就是修复问题,将更改推送到我们的 Git 存储库,并等待我们的持续部署流程升级软件以使用新版本。

在相对较短的时间内,我们设法找到(调试)了问题,或者更准确地说,将问题缩小到代码的特定部分。

或者,也许我们发现问题不在代码中,而是我们的应用程序需要扩展。无论哪种情况,没有仪器测量的指标,我们只会知道应用程序运行缓慢,这可能意味着应用程序的任何部分都在表现不佳。仪器测量为我们提供了更详细的指标,我们用这些指标更准确地缩小范围,并减少我们通常需要找到问题并相应采取行动的时间。

通常,我们会有许多其他仪器测量指标,我们的“调试”过程会更加复杂。我们会执行其他表达式并查看不同的指标。然而,关键是我们应该将通用指标与直接来自我们应用程序的更详细的指标相结合。前一组通常用于检测是否存在问题,而后一种类型在寻找问题的原因时非常有用。这两种类型的指标在监控、警报和调试我们的集群和应用程序时都有其作用。有了仪器测量指标,我们可以获得更多特定于应用程序的细节。这使我们能够缩小问题的位置和原因。我们对问题的确切原因越有信心,我们就越能够做出反应。

现在呢?

我不认为我们需要很多其他的仪表化指标示例。它们与我们通过导出器收集的指标没有任何不同。我会让你开始为你的应用程序进行仪表化。从小处开始,看看哪些效果好,改进和扩展。

又一个章节完成了。销毁你的集群,开始下一个新的,或者保留它。如果你选择后者,请执行接下来的命令来移除go-demo-5应用程序。

 1  helm delete go-demo-5 --purge
 2
 3  kubectl delete ns go-demo-5

在你离开之前,记住接下来的要点。它总结了仪表化。

  • 仪表化指标被嵌入到应用程序中。它们是我们应用程序代码的一个组成部分,通常通过/metrics端点公开。

第五章:使用自定义指标扩展 HorizontalPodAutoscaler

计算机是出色且高效的仆人,但我不愿意在它们的服务下工作。

  • 斯波克

HorizontalPodAutoscalerHPA)的采用通常经历三个阶段。

第一阶段是发现。第一次我们发现它的功能时,通常会感到非常惊讶。"看这个。它可以自动扩展我们的应用程序。我不再需要担心副本的数量了。"

第二阶段是使用。一旦我们开始使用 HPA,我们很快意识到基于内存和 CPU 的应用程序扩展不足够。一些应用程序随着负载的增加而增加其内存和 CPU 使用率,而许多其他应用程序则没有。或者更准确地说,不成比例。对于一些应用程序,HPA 运作良好。对于许多其他应用程序,它根本不起作用,或者不够。迟早,我们需要将 HPA 的阈值扩展到不仅仅是基于内存和 CPU 的阈值。这个阶段的特点是失望。"这似乎是个好主意,但我们不能用它来处理我们大多数的应用程序。我们需要退回到基于指标和手动更改副本数量的警报。"

第三阶段是重新发现。一旦我们阅读了 HPA v2 文档(在撰写本文时仍处于测试阶段),我们就会发现它允许我们将其扩展到几乎任何类型的指标和表达式。我们可以通过适配器将 HPAs 连接到 Prometheus,或几乎任何其他工具。一旦我们掌握了这一点,我们几乎没有限制条件可以设置为自动扩展我们的应用程序的触发器。唯一的限制是我们将数据转换为 Kubernetes 自定义指标的能力。

我们的下一个目标是扩展 HorizontalPodAutoscaler 定义,以包括基于 Prometheus 中存储的数据的条件。

创建一个集群

vfarcic/k8s-specsgithub.com/vfarcic/k8s-specs)存储库将继续作为我们的 Kubernetes 定义的来源。我们将确保通过拉取最新版本使其保持最新。

本章中的所有命令都可以在05-hpa-custom-metrics.shgist.github.com/vfarcic/cc546f81e060e4f5fc5661e4fa003af7)Gist 中找到。

 1  cd k8s-specs
 2
 3  git pull

要求与上一章相同。唯一的例外是EKS。我们将继续为所有其他 Kubernetes 版本使用与之前相同的 Gists。

对于 EKS 用户的注意事项,尽管我们迄今为止使用的三个 t2.small 节点具有足够的内存和 CPU,但它们可能无法承载我们将创建的所有 Pod。EKS(默认情况下)使用 AWS 网络。t2.small 实例最多可以有三个网络接口,每个接口最多有四个 IPv4 地址。这意味着每个 t2.small 节点上最多可以有十二个 IPv4 地址。鉴于每个 Pod 需要有自己的地址,这意味着每个节点最多可以有十二个 Pod。在本章中,我们可能需要在整个集群中超过三十六个 Pod。我们将添加 Cluster Autoscaler(CA)到集群中,让集群在需要时扩展,而不是创建超过三个节点的集群。我们已经在之前的章节中探讨了 CA,并且设置说明现在已添加到了 Gist eks-hpa-custom.sh (gist.github.com/vfarcic/868bf70ac2946458f5485edea1f6fc4c))。

请使用以下 Gist 之一创建新的集群。如果您已经有一个要用于练习的集群,请使用 Gist 来验证它是否满足所有要求。

现在我们准备扩展我们对 HPA 的使用。但在我们这样做之前,让我们简要地探索(再次)HPA 如何开箱即用。

在没有度量适配器的情况下使用 HorizontalPodAutoscaler

如果我们不创建度量适配器,度量聚合器只知道与容器和节点相关的 CPU 和内存使用情况。更复杂的是,这些信息仅限于最近几分钟。由于 HPA 只关心 Pod 和其中的容器,我们只能使用两个指标。当我们创建 HPA 时,如果构成这些 Pod 的容器的内存或 CPU 消耗超过或低于预定义的阈值,它将扩展或缩减我们的 Pods。

Metrics Server 定期从运行在工作节点内的 Kubelet 获取信息(CPU 和内存)。

这些指标被传递给度量聚合器,但在这种情况下,度量聚合器并没有增加任何额外的价值。从那里开始,HPA 定期查询度量聚合器中的数据(通过其 API 端点)。当 HPA 中定义的目标值与实际值存在差异时,HPA 将操纵部署或 StatefulSet 的副本数量。正如我们已经知道的那样,对这些控制器的任何更改都会导致通过创建和操作 ReplicaSets 执行滚动更新,从而创建和删除 Pods,这些 Pods 由运行在调度了 Pod 的节点上的 Kubelet 转换为容器。

图 5-1:开箱即用设置的 HPA(箭头显示数据流向)

从功能上讲,我们刚刚描述的流程运行良好。唯一的问题是指标聚合器中可用的数据。它仅限于内存和 CPU。往往这是不够的。因此,我们不需要改变流程,而是要扩展可用于 HPA 的数据。我们可以通过指标适配器来实现这一点。

探索 Prometheus 适配器

鉴于我们希望通过指标 API 扩展可用的指标,并且 Kubernetes 允许我们通过其自定义指标 APIgithub.com/kubernetes/metrics/tree/master/pkg/apis/custom_metrics)来实现这一点,实现我们目标的一个选项可能是创建我们自己的适配器。

根据我们存储指标的应用程序(DB),这可能是一个不错的选择。但是,鉴于重新发明轮子是毫无意义的,我们的第一步应该是寻找解决方案。如果有人已经创建了一个适合我们需求的适配器,那么采用它而不是自己创建一个新的适配器是有意义的。即使我们选择了只提供部分我们寻找的功能的东西,也比从头开始更容易(并向项目做出贡献),而不是从零开始。

鉴于我们的指标存储在 Prometheus 中,我们需要一个指标适配器,它将能够从中获取数据。由于 Prometheus 非常受欢迎并被社区采用,已经有一个项目在等待我们使用。它被称为用于 Prometheus 的 Kubernetes 自定义指标适配器。它是使用 Prometheus 作为数据源的 Kubernetes 自定义指标 API 的实现。

鉴于我们已经采用 Helm 进行所有安装,我们将使用它来安装适配器。

 1  helm install \
 2      stable/prometheus-adapter \
 3      --name prometheus-adapter \
 4      --version v0.5.0 \
 5      --namespace metrics \
 6      --set image.tag=v0.5.0 \
 7      --set metricsRelistInterval=90s \
 8      --set prometheus.url=http://prometheus-server.metrics.svc \
 9      --set prometheus.port=80
10
11  kubectl -n metrics \
12      rollout status \
13      deployment prometheus-adapter

我们从stable存储库安装了prometheus-adapter Helm Chart。资源被创建在metrics命名空间中,image.tag设置为v0.3.0

我们将metricsRelistInterval从默认值30s更改为90s。这是适配器用来从 Prometheus 获取指标的间隔。由于我们的 Prometheus 设置每 60 秒从其目标获取指标,我们必须将适配器的间隔设置为高于该值。否则,适配器的频率将高于 Prometheus 的拉取频率,我们将有一些迭代没有新数据。

最后两个参数指定了适配器可以访问 Prometheus API 的 URL 和端口。在我们的情况下,URL 设置为通过 Prometheus 的服务。

请访问Prometheus Adapter Chart READMEgithub.com/helm/charts/tree/master/stable/prometheus-adapter)获取有关所有可以设置以自定义安装的值的更多信息。

最后,我们等待prometheus-adapter部署完成。

如果一切按预期运行,我们应该能够查询 Kubernetes 的自定义指标 API,并检索通过适配器提供的一些 Prometheus 数据。

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1" \
 3      | jq "."

鉴于每个章节都将呈现不同的 Kubernetes 版本的特点,并且 AWS 还没有轮到,所有输出都来自 EKS。根据您使用的平台不同,您的输出可能略有不同。

查询自定义指标的输出的前几个条目如下。

{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "custom.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "namespaces/memory_max_usage_bytes",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "jobs.batch/kube_deployment_spec_strategy_rollingupdate_max_unavailable",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    ...

透过适配器可用的自定义指标列表很长,我们可能会被迫认为它包含了 Prometheus 中存储的所有指标。我们将在以后发现这是否属实。现在,我们将专注于可能需要的与go-demo-5部署绑定的 HPA 的指标。毕竟,为自动扩展提供指标是适配器的主要功能,如果不是唯一功能的话。

从现在开始,Metrics Aggregator 不仅包含来自度量服务器的数据,还包括来自 Prometheus Adapter 的数据,后者又从 Prometheus 服务器获取度量。我们还需要确认通过适配器获取的数据是否足够,以及 HPA 是否能够使用自定义指标。

图 5-2:使用 Prometheus Adapter 的自定义指标(箭头显示数据流向)

在我们深入了解适配器之前,我们将为go-demo-5应用程序定义我们的目标。

我们应该能够根据内存和 CPU 使用率以及通过 Ingress 进入或通过仪表化指标观察到的请求数量来扩展 Pods。我们可以添加许多其他标准,但作为学习经验,这些应该足够了。我们已经知道如何配置 HPA 以根据 CPU 和内存进行扩展,所以我们的任务是通过请求计数器来扩展它。这样,我们将能够设置规则,当应用程序接收到太多请求时增加副本的数量,以及在流量减少时减少副本的数量。

由于我们想要扩展与go-demo-5相关的 HPA,我们的下一步是安装应用程序。

 1  GD5_ADDR=go-demo-5.$LB_IP.nip.io
 2
 3  helm install \
 4    https://github.com/vfarcic/go-demo-5/releases/download/
    0.0.1/go-demo-5-0.0.1.tgz \
 5      --name go-demo-5 \
 6      --namespace go-demo-5 \
 7      --set ingress.host=$GD5_ADDR
 8
 9  kubectl -n go-demo-5 \
10      rollout status \
11      deployment go-demo-5

我们定义了应用程序的地址,安装了图表,并等待部署完成。

EKS 用户注意:如果收到了error: deployment "go-demo-5" exceeded its progress deadline的消息,集群可能正在自动扩展以适应所有 Pod 和 PersistentVolumes 的区域。这可能比progress deadline需要更长的时间。在这种情况下,请等待片刻并重复rollout命令。

接下来,我们将通过其 Ingress 资源向应用程序发送一百个请求,以生成一些流量。

 1  for i in {1..100}; do
 2      curl "http://$GD5_ADDR/demo/hello"
 3  done

现在我们已经生成了一些流量,我们可以尝试找到一个指标,帮助我们计算通过 Ingress 传递了多少请求。由于我们已经知道(从之前的章节中)nginx_ingress_controller_requests提供了通过 Ingress 进入的请求数量,我们应该检查它是否现在作为自定义指标可用。

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1" \
 3      | jq '.resources[]
 4      | select(.name
 5      | contains("nginx_ingress_controller_requests"))'

我们向/apis/custom.metrics.k8s.io/v1beta1发送了一个请求。但是,正如你已经看到的,单独这样做会返回所有的指标,而我们只对其中一个感兴趣。这就是为什么我们将输出导入到jq并使用它的过滤器来检索只包含nginx_ingress_controller_requests作为name的条目。

如果你收到了空的输出,请等待片刻,直到适配器从 Prometheus 中拉取指标(它每九十秒执行一次),然后重新执行命令。

输出如下。

{
  "name": "ingresses.extensions/nginx_ingress_controller_requests",
  "singularName": "",
  "namespaced": true,
  "kind": "MetricValueList",
  "verbs": [
    "get"
  ]
}
{
  "name": "jobs.batch/nginx_ingress_controller_requests",
  "singularName": "",
  "namespaced": true,
  "kind": "MetricValueList",
  "verbs": [
    "get"
  ]
}
{
  "name": "namespaces/nginx_ingress_controller_requests",
  "singularName": "",
  "namespaced": false,
  "kind": "MetricValueList",
  "verbs": [
    "get"
  ]
}

我们得到了三个结果。每个的名称由资源类型和指标名称组成。我们将丢弃与jobs.batchnamespaces相关的内容,并集中在与ingresses.extensions相关的指标上,因为它提供了我们需要的信息。我们可以看到它是namespaced,这意味着指标在其他方面是由其来源的命名空间分隔的。kindverbs(几乎)总是相同的,浏览它们并没有太大的价值。

ingresses.extensions/nginx_ingress_controller_requests的主要问题是它提供了 Ingress 资源的请求数量。我们无法将其作为 HPA 标准的当前形式使用。相反,我们应该将请求数量除以副本数量。这将给我们每个副本的平均请求数量,这应该是一个更好的 HPA 阈值。我们将探讨如何在后面使用表达式而不是简单的指标。了解通过 Ingress 进入的请求数量是有用的,但可能还不够。

由于go-demo-5已经提供了有仪器的指标,看看我们是否可以检索http_server_resp_time_count将会很有帮助。提醒一下,这是我们在第四章中使用的相同指标,通过指标和警报发现的故障调试

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1" \
 3      | jq '.resources[]
 4      | select(.name
 5      | contains("http_server_resp_time_count"))'

我们使用jq来过滤结果,以便只检索http_server_resp_time_count。看到空输出不要感到惊讶。这是正常的,因为 Prometheus Adapter 没有配置为处理来自 Prometheus 的所有指标,而只处理符合其内部规则的指标。因此,现在可能是时候看一下包含其配置的prometheus-adapter ConfigMap 了。

 1  kubectl -n metrics \
 2      describe cm prometheus-adapter

输出太大,无法在书中呈现,所以我们只会讨论第一个规则。它如下所示。

...
rules:
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
  seriesFilters: []
  resources:
    overrides:
      namespace:
        resource: namespace
      pod_name:
        resource: pod
  name:
    matches: ^container_(.*)_seconds_total$
    as: ""
  metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[5m]))
    by (<<.GroupBy>>)
...

第一个规则仅检索以container开头的指标(__name__=~"^container_.*"),标签container_name不是POD,并且namespacepod_name不为空。

每个规则都必须指定一些资源覆盖。在这种情况下,namespace标签包含namespace资源。类似地,pod资源是从标签pod_name中检索的。此外,我们可以看到name部分使用正则表达式来命名新的指标。最后,metricsQuery告诉适配器在检索数据时应执行哪个 Prometheus 查询。

如果这个设置看起来令人困惑,您应该知道您并不是唯一一开始看起来感到困惑的人。就像 Prometheus 服务器配置一样,Prometheus Adapter 一开始很难理解。尽管如此,它们非常强大,可以让我们定义服务发现规则,而不是指定单个指标(对于适配器的情况)或目标(对于 Prometheus 服务器的情况)。很快我们将更详细地介绍适配器规则的设置。目前,重要的一点是默认配置告诉适配器获取与几个规则匹配的所有指标。

到目前为止,我们看到nginx_ingress_controller_requests指标可以通过适配器获得,但由于我们需要将请求数除以副本数,因此它没有用处。我们还看到go-demo-5 Pods 中的http_server_resp_time_count指标不可用。总而言之,我们没有所有需要的指标,而适配器当前获取的大多数指标都没有用。它通过无意义的查询浪费时间和资源。

我们的下一个任务是重新配置适配器,以便仅从 Prometheus 中检索我们需要的指标。我们将尝试编写自己的表达式,只获取我们需要的数据。如果我们能做到这一点,我们应该能够创建 HPA。

使用自定义指标创建 HorizontalPodAutoscaler

正如您已经看到的,Prometheus Adapter 带有一组默认规则,提供了许多我们不需要的指标,而我们需要的并不是所有的指标。它通过做太多事情而浪费 CPU 和内存,但又不够。我们将探讨如何使用我们自己的规则自定义适配器。我们的下一个目标是使适配器仅检索nginx_ingress_controller_requests指标,因为这是我们唯一需要的指标。除此之外,它还应该以两种形式提供该指标。首先,它应该按资源分组检索速率。

第二种形式应该与第一种形式相同,但应该除以托管 Ingress 转发资源的 Pod 的部署的副本数。

这将为我们提供每个副本的平均请求数,并将成为基于自定义指标的第一个 HPA 定义的良好候选。

我已经准备了一个包含可能实现我们当前目标的 Chart 值的文件,让我们来看一下。

 1  cat mon/prom-adapter-values-ing.yml

输出如下。

image:
  tag: v0.5.0
metricsRelistInterval: 90s
prometheus:
  url: http://prometheus-server.metrics.svc
  port: 80
rules:
  default: false
  custom:
  - seriesQuery: 'nginx_ingress_controller_requests'
    resources:
      overrides:
        namespace: {resource: "namespace"}
        ingress: {resource: "ingress"}
    name:
      as: "http_req_per_second"
    metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>)'
  - seriesQuery: 'nginx_ingress_controller_requests'
    resources:
      overrides:
        namespace: {resource: "namespace"}
        ingress: {resource: "ingress"}
    name:
      as: "http_req_per_second_per_replica"
    metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) / sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment")) by (<<.GroupBy>>)'

在定义中的前几个条目与我们先前通过--set参数使用的数值相同。我们将跳过这些条目,直接进入rules部分。

rules部分,我们将default条目设置为false。这将摆脱我们先前探索的默认规则,并使我们能够从干净的状态开始。此外,还有两个custom规则。

第一个规则基于seriesQuery,其值为nginx_ingress_controller_requestsresources部分内的overrides条目帮助适配器找出与该指标相关联的 Kubernetes 资源。我们将namespace标签的值设置为namespace资源。对于ingress也有类似的条目。换句话说,我们将 Prometheus 标签与 Kubernetes 资源namespaceingress相关联。

很快你会看到,该指标本身将成为完整查询的一部分,并由 HPA 视为单一指标。由于我们正在创建新内容,我们需要一个名称。因此,我们在name部分指定了一个名为http_req_per_second的单一as条目。这将成为我们 HPA 定义中的参考。

你已经知道nginx_ingress_controller_requests本身并不是很有用。当我们在 Prometheus 中使用它时,我们必须将其放入rate函数中,我们必须sum所有内容,并且我们必须按资源对结果进行分组。我们通过metricsQuery条目正在做类似的事情。将其视为我们在 Prometheus 中编写的表达式的等价物。唯一的区别是我们使用了像<<.Series>>这样的“特殊”语法。这是适配器的模板机制。我们没有硬编码指标的名称、标签和分组语句,而是使用<<.Series>><<.LabelMatchers>><<.GroupBy>>子句,这些子句将根据我们在 API 调用中放入的内容填充正确的值。

第二个规则几乎与第一个相同。不同之处在于名称(现在是http_req_per_second_per_replica)和metricsQuery。后者现在将结果除以相关部署的副本数,就像我们在第三章中练习的那样,收集和查询指标并发送警报

接下来,我们将使用新数值更新图表。

 1  helm upgrade prometheus-adapter \
 2      stable/prometheus-adapter \
 3      --version v0.5.0 \
 4      --namespace metrics \
 5      --values mon/prom-adapter-values-ing.yml
 6
 7  kubectl -n metrics \
 8      rollout status \
 9      deployment prometheus-adapter

现在部署已成功推出,我们可以再次确认 ConfigMap 中存储的配置确实是正确的。

 1  kubectl -n metrics \
 2      describe cm prometheus-adapter

输出,限于Data部分,如下。

...
Data
====
config.yaml:
----
rules:
- metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>)
  name:
    as: http_req_per_second
  resources:
    overrides:
      ingress:
        resource: ingress
      namespace:
        resource: namespace
  seriesQuery: nginx_ingress_controller_requests
- metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) /
    sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment"))
    by (<<.GroupBy>>)
  name:
    as: http_req_per_second_per_replica
  resources:
    overrides:
      ingress:
        resource: ingress
      namespace:
        resource: namespace
  seriesQuery: nginx_ingress_controller_requests
...

我们可以看到我们之前探索的默认rules现在被我们在 Chart 值文件的rules.custom部分中定义的两个规则所替换。

配置看起来正确并不一定意味着适配器现在提供数据作为 Kubernetes 自定义指标。我们也要检查一下。

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1" \
 3      | jq "."

输出如下。

{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "custom.metrics.k8s.io/v1beta1",
  "resources": [
    {
      "name": "namespaces/http_req_per_second_per_replica",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "ingresses.extensions/http_req_per_second_per_replica",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "ingresses.extensions/http_req_per_second",
      "singularName": "",
      "namespaced": true,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    },
    {
      "name": "namespaces/http_req_per_second",
      "singularName": "",
      "namespaced": false,
      "kind": "MetricValueList",
      "verbs": [
        "get"
      ]
    }
  ]
}

我们可以看到有四个可用的指标,其中两个是http_req_per_second,另外两个是http_req_per_second_per_replica。我们定义的两个指标都可以作为namespacesingresses使用。现在,我们不关心namespaces,我们将集中在ingresses上。

我假设至少过去了五分钟(或更长时间),自从我们发送了一百个请求。如果没有,你是一个快速的读者,你将不得不等一会儿,然后我们再发送一百个请求。我们即将创建我们的第一个基于自定义指标的 HPA,并且我想确保你在激活之前和之后都能看到它的行为。

现在,让我们来看一个 HPA 定义。

 1  cat mon/go-demo-5-hpa-ing.yml

输出如下。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-demo-5
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Object
    object:
      metricName: http_req_per_second_per_replica
      target:
        kind: Namespace
        name: go-demo-5
      targetValue: 50m

定义的前半部分应该是熟悉的,因为它与我们以前使用的内容没有区别。它将维护go-demo-5部署的310个副本。新的内容在metrics部分。

过去,我们使用spec.metrics.type设置为Resource。通过该类型,我们定义了 CPU 和内存目标。然而,这一次,我们的类型是Object。它指的是描述单个 Kubernetes 对象的指标,而在我们的情况下,恰好是来自 Prometheus Adapter 的自定义指标。

如果我们浏览ObjectMetricSource v2beta1 autoscaling (kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#objectmetricsource-v2beta1-autoscaling) 文档,我们可以看到Object类型的字段与我们以前使用的Resources类型不同。我们将metricName设置为我们在 Prometheus Adapter 中定义的指标(http_req_per_second_per_replica)。

请记住,这不是一个指标,而是我们定义的一个表达式,适配器用来从 Prometheus 获取数据并将其转换为自定义指标。在这种情况下,我们得到了进入 Ingress 资源的请求数,然后除以部署的副本数。

最后,targetValue设置为50m或每秒 0.05 个请求。我故意将其设置为一个非常低的值,以便我们可以轻松达到目标并观察发生了什么。

让我们apply定义。

 1  kubectl -n go-demo-5 \
 2      apply -f mon/go-demo-5-hpa-ing.yml

接下来,我们将描述新创建的 HPA,并看看是否能观察到一些有趣的东西。

 1  kubectl -n go-demo-5 \
 2      describe hpa go-demo-5

输出,仅限于相关部分,如下所示。

...
Metrics:         ( current / target )
  "http_req_per_second_per_replica" on Namespace/go-demo-5: 0 / 50m
Min replicas:    3
Max replicas:    10
Deployment pods: 3 current / 3 desired
...

我们可以看到Metrics部分只有一个条目。HPA 正在使用基于Namespace/go-demo-5的自定义指标http_req_per_second_per_replica。目前,当前值为0target设置为50m(每秒 0.05 个请求)。如果在您的情况下,current值为unknown,请等待片刻,然后重新运行命令。

进一步向下,我们可以看到Deployment Podscurrentdesired数量均设置为3

总的来说,目标没有达到(有0个请求),因此 HPA 无需做任何事情。它保持最小数量的副本。

让我们增加一些流量。

 1  for i in {1..100}; do
 2      curl "http://$GD5_ADDR/demo/hello"
 3  done

我们向go-demo-5 Ingress 发送了一百个请求。

让我们再次describe HPA,并看看是否有一些变化。

 1  kubectl -n go-demo-5 \
 2      describe hpa go-demo-5

输出,仅限于相关部分,如下所示。

...
Metrics:                                                   ( current / target )
  "http_req_per_second_per_replica" on Ingress/go-demo-5:  138m / 50m
Min replicas:                                              3
Max replicas:                                              10
Deployment pods:                                           3 current / 6 desired
...
Events:
  ... Message
  ... -------
  ... New size: 6; reason: Ingress metric http_req_per_second_per_replica above target

我们可以看到指标的current值增加了。在我的情况下,它是138m(每秒 0.138 个请求)。如果您的输出仍然显示0,您必须等待直到 Prometheus 拉取指标,直到适配器获取它们,直到 HPA 刷新其状态。换句话说,请等待片刻,然后重新运行上一个命令。

鉴于current值高于target,在我的情况下,HPA 将desiredDeployment pods数量更改为6(根据指标值的不同,您的数字可能会有所不同)。因此,HPA 通过更改副本的数量来修改 Deployment,并且我们应该看到额外的 Pod 正在运行。这在Events部分中更加明显。应该有一条新消息,说明New size: 6; reason: Ingress metric http_req_per_second_per_replica above target

为了安全起见,我们将列出go-demo-5 Namespace 中的 Pods,并确认新的 Pod 确实正在运行。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME           READY STATUS  RESTARTS AGE
go-demo-5-db-0 2/2   Running 0        19m
go-demo-5-db-1 2/2   Running 0        19m
go-demo-5-db-2 2/2   Running 0        10m
go-demo-5-...  1/1   Running 2        19m
go-demo-5-...  1/1   Running 0        16s
go-demo-5-...  1/1   Running 2        19m
go-demo-5-...  1/1   Running 0        16s
go-demo-5-...  1/1   Running 2        19m
go-demo-5-...  1/1   Running 0        16s

我们现在可以看到有六个go-demo-5-* Pods,其中有三个比其余的年轻得多。

接下来,我们将探讨当流量低于 HPA 的“目标”时会发生什么。我们将通过一段时间不做任何事情来实现这一点。由于我们是唯一向应用程序发送请求的人,我们所要做的就是静静地站在那里五分钟,或者更好的是利用这段时间去取一杯咖啡。

我们需要等待至少五分钟的原因在于 HPA 用于扩展和缩小的频率。默认情况下,只要“当前”值高于“目标”,HPA 就会每三分钟扩展一次。缩小需要五分钟。只有在自上次扩展以来“当前”值低于目标至少三分钟时,HPA 才会缩小。

总的来说,我们需要等待五分钟或更长时间,然后才能看到相反方向的扩展效果。

 1  kubectl -n go-demo-5 \
 2      describe hpa go-demo-5

输出,仅限相关部分,如下所示。

...
Metrics:         ( current / target )
  "http_req_per_second_per_replica" on Ingress/go-demo-5:  0 / 50m
Min replicas:    3
Max replicas:    10
Deployment pods: 3 current / 3 desired
...
Events:
... Age   ... Message
... ----  ... -------
... 10m   ... New size: 6; reason: Ingress metric http_req_per_second_per_replica above target
... 7m10s ... New size: 9; reason: Ingress metric http_req_per_second_per_replica above target
... 2m9s  ... New size: 3; reason: All metrics below target

输出中最有趣的部分是事件部分。我们将专注于“年龄”和“消息”字段。请记住,只要当前值高于目标,扩展事件就会每三分钟执行一次,而缩小迭代则是每五分钟一次。

在我的情况下,HPA 在三分钟后再次扩展了部署。副本的数量从六个跳到了九个。由于适配器使用的表达式使用了五分钟的速率,一些请求进入了第二次 HPA 迭代。即使在我们停止发送请求后仍然扩展可能看起来不是一个好主意(确实不是),但在“现实世界”的场景中不应该发生,因为流量比我们生成的要多得多,我们不会将50m(每秒 0.2 个请求)作为目标。

在最后一次扩展事件后的五分钟内,“当前”值为0,HPA 将部署缩小到最小副本数(3)。再也没有流量了,我们又回到了起点。

我们确认了 Prometheus 的指标,通过 Prometheus Adapter 获取,并转换为 Kuberentes 的自定义指标,可以在 HPA 中使用。到目前为止,我们使用了通过出口商(nginx_ingress_controller_requests)从 Prometheus 获取的指标。鉴于适配器从 Prometheus 获取指标,它不应该关心它们是如何到达那里的。尽管如此,我们将确认仪表化指标也可以使用。这将为我们提供一个巩固到目前为止学到的知识的机会,同时,也许学到一些新的技巧。

 1  cat mon/prom-adapter-values-svc.yml

输出还是另一组 Prometheus Adapter 图表值。

image:
  tag: v0.5.0
metricsRelistInterval: 90s
prometheus:
  url: http://prometheus-server.metrics.svc
  port: 80
rules:
  default: false
  custom:
  - seriesQuery: 'http_server_resp_time_count{kubernetes_namespace!="",kubernetes_name!=""}'
    resources:
      overrides:
        kubernetes_namespace: {resource: "namespace"}
        kubernetes_name: {resource: "service"}
    name:
      matches: "^(.*)server_resp_time_count"
      as: "${1}req_per_second_per_replica"
    metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) / count(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
  - seriesQuery: 'nginx_ingress_controller_requests'
    resources:
      overrides:
        namespace: {resource: "namespace"}
        ingress: {resource: "ingress"}
    name:
      as: "http_req_per_second_per_replica"
    metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) / sum(label_join(kube_deployment_status_replicas, "ingress", ",", "deployment")) by (<<.GroupBy>>)'

这一次,我们将合并包含不同指标系列的规则。第一条规则基于go-demo-5中源自http_server_resp_time_count的仪表指标。我们在第四章中使用过它,通过指标和警报调试问题,在其定义中并没有什么特别之处。它遵循与我们之前使用的规则相同的逻辑。第二条规则是我们之前使用过的规则的副本。

有趣的是这些规则的是,有两个完全不同的查询产生了不同的结果。然而,在这两种情况下,名称是相同的(http_req_per_second_per_replica)。

“等一下”,你可能会说。这两个名称并不相同。一个被称为${1}req_per_second_per_replica,而另一个是http_req_per_second_per_replica。虽然这是真的,但最终名称,不包括资源类型,确实是相同的。我想向你展示你可以使用正则表达式来形成一个名称。在第一条规则中,名称由matchesas条目组成。matches条目的(.*)部分成为第一个变量(还可以有其他变量),稍后用作as值(${1})。由于指标是http_server_resp_time_count,它将从^(.*)server_resp_time_count中提取http_,然后在下一行中,用于替换${1}。最终结果是http_req_per_second_per_replica,这与第二条规则的名称相同。

现在我们已经确定了两条规则都将提供相同名称的自定义指标,我们可能会认为这将导致冲突。如果两者都被称为相同的话,HPA 将如何知道使用哪个指标?适配器是否必须丢弃一个并保留另一个?答案在“资源”部分。

指标的真正标识符是其名称和其关联的资源的组合。第一条规则生成两个自定义指标,一个用于服务,另一个用于命名空间。第二条规则还为命名空间生成自定义指标,但也为 Ingresses 生成自定义指标。

总共有多少个指标?在我们检查结果之前,我会让你考虑一下答案。为了做到这一点,我们将不得不“升级”图表,以使新值生效。

 1  helm upgrade -i prometheus-adapter \
 2      stable/prometheus-adapter \
 3      --version v0.5.0 \
 4      --namespace metrics \
 5      --values mon/prom-adapter-values-svc.yml
 6
 7  kubectl -n metrics \
 8      rollout status \
 9      deployment prometheus-adapter

我们用新值升级了图表,并等待部署完成。

现在我们可以回到我们未决问题“我们有多少个自定义指标?”让我们看看…

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1" \
 3      | jq "."

输出,仅限于相关部分,如下所示。

{
  ...
    {
      "name": "services/http_req_per_second_per_replica",
      ...
    },
    {
      "name": "namespaces/http_req_per_second_per_replica",
      ...
    },
    {
      "name": "ingresses.extensions/http_req_per_second_per_replica",
      ...

现在我们有三个自定义度量标准,而不是四个。我已经解释过,唯一的标识符是度量标准的名称与其绑定的 Kubernetes 资源。所有度量标准都被称为 http_req_per_second_per_replica。但是,由于两个规则都覆盖了两个资源,并且在两者中都设置了 namespace,因此必须丢弃一个。我们不知道哪一个被移除了,哪一个留下了。或者,它们可能已经合并了。这并不重要,因为我们不应该用相同名称的度量标准覆盖相同的资源。对于我在适配器规则中包含 namespace 的实际原因,除了向您展示可以有多个覆盖以及它们相同时会发生什么之外,没有其他实际原因。

除了那个愚蠢的原因,你可以在脑海中忽略 namespaces/http_req_per_second_per_replica 度量标准。

我们使用了两个不同的 Prometheus 表达式来创建两个不同的自定义度量标准,它们具有相同的名称,但与其他资源相关。一个(基于 nginx_ingress_controller_requests 表达式)来自 Ingress 资源,而另一个(基于 http_server_resp_time_count)来自 Services。尽管后者起源于 go-demo-5 Pods,但 Prometheus 是通过 Services 发现它的(正如在前一章中讨论的那样)。

我们不仅可以使用 /apis/custom.metrics.k8s.io 端点来发现我们拥有哪些自定义度量标准,还可以检查细节,包括数值。例如,我们可以通过以下命令检索 services/http_req_per_second_per_replica 度量标准。

 1  kubectl get --raw \
 2      "/apis/custom.metrics.k8s.io/v1beta1/namespaces/go-demo5
    /services/*/http_req_per_second_per_replica" \
 3       | jq .

输出如下。

{
  "kind": "MetricValueList",
  "apiVersion": "custom.metrics.k8s.io/v1beta1",
  "metadata": {
    "selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/go-demo-5/services/%2A/http_req_per_second_per_replica"
  },
  "items": [
    {
      "describedObject": {
        "kind": "Service",
        "namespace": "go-demo-5",
        "name": "go-demo-5",
        "apiVersion": "/v1"
      },
      "metricName": "http_req_per_second_per_replica",
      "timestamp": "2018-10-27T23:49:58Z",
      "value": "1130m"
    }
  ]
}

describedObject 部分向我们展示了项目的细节。现在,我们只有一个具有该度量标准的 Service。

我们可以看到该 Service 位于 go-demo-5 Namespace 中,它的名称是 go-demo-5,并且它正在使用 v1 API 版本。

在更下面,我们可以看到度量标准的当前值。在我的情况下,它是 1130m,或者略高于每秒一个请求。由于没有人向 go-demo-5 Service 发送请求,考虑到每秒执行一次健康检查,这个值是预期的。

接下来,我们将探讨更新后的 HPA 定义,将使用基于服务的度量标准。

 1  cat mon/go-demo-5-hpa-svc.yml

输出如下。

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: go-demo-5
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-demo-5
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Object
    object:
      metricName: http_req_per_second_per_replica
      target:
        kind: Service
        name: go-demo-5
       targetValue: 1500m

与先前的定义相比,唯一的变化在于targettargetValue字段。请记住,完整的标识符是metricNametarget的组合。因此,这次我们将kind更改为Service。我们还必须更改targetValue,因为我们的应用程序不仅接收来自 Ingress 的外部请求,还接收内部请求。它们可能来自其他可能与go-demo-5通信的应用程序,或者像我们的情况一样,来自 Kubernetes 的健康检查。由于它们的频率是一秒,我们将targetValue设置为1500m,即每秒 1.5 个请求。这样,如果我们不向应用程序发送任何请求,就不会触发扩展。通常,您会设置一个更大的值。但是,目前,我们只是尝试观察在扩展之前和之后它的行为。

接下来,我们将应用对 HPA 的更改,并进行描述。

 1  kubectl -n go-demo-5 \
 2      apply -f mon/go-demo-5-hpa-svc.yml
 3
 4  kubectl -n go-demo-5 \
 5      describe hpa go-demo-5

后一条命令的输出,仅限于相关部分,如下所示。

...
Metrics:                                                  ( current / target )
  "http_req_per_second_per_replica" on Service/go-demo-5: 1100m / 1500m
...
Deployment pods:                                           3 current / 3 desired
...
Events:
  Type    Reason             Age    From                       Message
  ----    ------             ----   ----                       -------
  Normal  SuccessfulRescale  12m    horizontal-pod-autoscaler  New size: 6; reason: Ingress metric http_req_per_second_per_replica above target
  Normal  SuccessfulRescale  9m20s  horizontal-pod-autoscaler  New size: 9; reason: Ingress metric http_req_per_second_per_replica above target
  Normal  SuccessfulRescale  4m20s  horizontal-pod-autoscaler  New size: 3; reason: All metrics below target

目前,没有理由让 HPA 扩展部署。当前值低于阈值。在我的情况下,它是1100m

现在我们可以测试基于来自仪器的自定义指标的自动缩放是否按预期工作。通过 Ingress 发送请求可能会很慢,特别是如果我们的集群在云中运行。从我们的笔记本到服务的往返可能太慢了。因此,我们将从集群内部发送请求,通过启动一个 Pod 并从其中执行请求循环。

 1  kubectl -n go-demo-5 \
 2      run -it test \
 3      --image=debian \
 4      --restart=Never \
 5      --rm \
 6      -- bash

通常,我更喜欢alpine镜像,因为它们更小更高效。但是,for循环在alpine中无法工作(或者我不知道如何编写),所以我们改用debian。不过debian中没有curl,所以我们需要安装它。

 1  apt update
 2
 3  apt install -y curl

现在我们可以发送请求,这些请求将产生足够的流量,以便 HPA 触发扩展过程。

 1  for i in {1..500}; do
 2      curl "http://go-demo-5:8080/demo/hello"
 3  done
 4  
 5  exit

我们向/demo/hello端点发送了五百个请求,然后退出了容器。由于我们在创建 Pod 时使用了--rm参数,它将自动从系统中删除,因此我们不需要执行任何清理操作。

让我们描述一下 HPA 并看看发生了什么。

 1  kubectl -n go-demo-5 \
 2      describe hpa go-demo-5

输出结果,仅限于相关部分,如下所示。

...
Reference:                                                Deployment/go-demo-5
Metrics:                                                  ( current / target )
  "http_req_per_second_per_replica" on Service/go-demo-5: 1794m / 1500m
Min replicas:                                             3
Max replicas:                                             10
Deployment pods:                                          3 current / 4 desired
...
Events:
... Message
... -------
... New size: 6; reason: Ingress metric http_req_per_second_per_replica above target
... New size: 9; reason: Ingress metric http_req_per_second_per_replica above target
... New size: 3; reason: All metrics below target
... New size: 4; reason: Service metric http_req_per_second_per_replica above target

HPA 检测到current值高于目标值(在我的情况下是1794m),并将所需的副本数量从3更改为4。我们也可以从最后一个事件中观察到这一点。如果在您的情况下,desired副本数量仍然是3,请等待一段时间进行 HPA 评估的下一次迭代,并重复describe命令。

如果我们需要额外确认扩展确实按预期工作,我们可以检索go-demo-5命名空间中的 Pods。

 1  kubectl -n go-demo-5 get pods

输出如下。

NAME           READY STATUS  RESTARTS AGE
go-demo-5-db-0 2/2   Running 0        33m
go-demo-5-db-1 2/2   Running 0        32m
go-demo-5-db-2 2/2   Running 0        32m
go-demo-5-...  1/1   Running 2        33m
go-demo-5-...  1/1   Running 0        53s
go-demo-5-...  1/1   Running 2        33m
go-demo-5-...  1/1   Running 2        33m

毋庸置疑,当我们停止发送请求后,HPA 很快会缩减go-demo-5部署。相反,我们将进入下一个主题。

结合 Metric Server 数据和自定义指标

到目前为止,少数 HPA 示例使用单个自定义指标来决定是否扩展部署。您已经从第一章中了解到,基于资源使用情况自动扩展部署和 StatefulSets,我们可以在 HPA 中结合多个指标。然而,该章节中的所有示例都使用了来自 Metrics Server 的数据。我们了解到,在许多情况下,来自 Metrics Server 的内存和 CPU 指标是不够的,因此我们引入了 Prometheus Adapter,它将自定义指标提供给 Metrics Aggregator。我们成功地配置了 HPA 来使用这些自定义指标。然而,通常情况下,我们需要在 HPA 定义中结合这两种类型的指标。虽然内存和 CPU 指标本身是不够的,但它们仍然是必不可少的。我们能否将两者结合起来呢?

让我们再看看另一个 HPA 定义。

 1  cat mon/go-demo-5-hpa.yml

输出,仅限于相关部分,如下所示。

...
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 80
  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 80
  - type: Object
    object:
      metricName: http_req_per_second_per_replica
      target:
        kind: Service
        name: go-demo-5
      targetValue: 1500m

这次,HPA 在metrics部分有三个条目。前两个是基于Resource类型的“标准”cpumemory条目。最后一个条目是我们之前使用过的Object类型之一。通过结合这些,我们告诉 HPA 如果满足三个标准中的任何一个,就进行扩展。同样,它也会进行缩减,但为了发生这种情况,所有三个标准都需要低于目标值。

让我们apply这个定义。

 1  kubectl -n go-demo-5 \
 2      apply -f mon/go-demo-5-hpa.yml

接下来,我们将描述 HPA。但在此之前,我们需要等待一段时间,直到更新后的 HPA 经过下一次迭代。

 1  kubectl -n go-demo-5 \
 2      describe hpa go-demo-5

输出,仅限于相关部分,如下所示。

...
Metrics:                                                  ( current / target )
  resource memory on pods  (as a percentage of request):  110% (5768533333m) / 80%
  "http_req_per_second_per_replica" on Service/go-demo-5: 825m / 1500m
  resource cpu on pods  (as a percentage of request):     20% (1m) / 80%
...
Deployment pods:                                          5 current / 5 desired
...
Events:
... Message
... -------
... New size: 6; reason: Ingress metric http_req_per_second_per_replica above target
... New size: 9; reason: Ingress metric http_req_per_second_per_replica above target
... New size: 4; reason: Service metric http_req_per_second_per_replica above target
... New size: 3; reason: All metrics below target
... New size: 5; reason: memory resource utilization (percentage of request) above target

我们可以看到基于内存的度量从一开始就超过了阈值。在我的情况下,它是110%,而目标是80%。因此,HPA 扩展了部署。在我的情况下,它将新大小设置为5个副本。

不需要确认新的 Pod 是否正在运行。到现在为止,我们应该相信 HPA 会做正确的事情。相反,我们将简要评论整个流程。

完整的 HorizontalPodAutoscaler 事件流

Metrics Server 从运行在工作节点上的 Kubelets 获取内存和 CPU 数据。与此同时,Prometheus Adapter 从 Prometheus Server 获取数据,而你已经知道,Prometheus Server 从不同的来源获取数据。Metrics Server 和 Prometheus Adapter 的数据都合并在 Metrics Aggregator 中。

HPA 定期评估定义为缩放标准的度量。它从 Metrics Aggregator 获取数据,实际上并不在乎它们是来自 Metrics Server、Prometheus Adapter 还是我们可能使用的任何其他工具。

一旦满足缩放标准,HPA 通过改变其副本数量来操作部署和 StatefulSets。

因此,通过创建和更新 ReplicaSets 执行滚动更新,ReplicaSets 又会创建或删除 Pods。

图 5-3:HPA 使用 Metrics Server 和 Prometheus Adapter 提供的度量指标的组合(箭头显示数据流)

达到涅磐

现在我们知道如何将几乎任何指标添加到 HPA 中,它们比在第一章中看起来要有用得多,基于资源使用情况自动扩展部署和有状态集。最初,HPA 并不是非常实用,因为在许多情况下,内存和 CPU 是不足以决定是否扩展我们的 Pods 的。我们必须学会如何收集指标(我们使用 Prometheus Server 进行了这项工作),以及如何为我们的应用程序提供更详细的可见性。自定义指标是这个难题的缺失部分。如果我们用额外的我们需要的指标(例如,Prometheus Adapter)扩展了“标准”指标(CPU 和内存),我们就获得了一个强大的流程,它将使我们应用程序的副本数量与内部和外部需求保持同步。假设我们的应用程序是可扩展的,我们可以保证它们(几乎)总是能够按需执行。至少在涉及到扩展时,不再需要手动干预。具有“标准”和自定义指标的 HPA 将保证 Pod 的数量满足需求,而集群自动扩展器(在适用时)将确保我们有足够的容量来运行这些 Pods。

我们的系统离自给自足又近了一步。它将自适应于变化的条件,而我们(人类)可以把注意力转向比维持系统满足需求状态所需的更有创意和不那么重复的任务。我们离涅槃又近了一步。

现在呢?

请注意,我们使用了autoscaling/v2beta1版本的 HorizontalPodAutoscaler。在撰写本文时(2018 年 11 月),只有v1是稳定且可用于生产环境的。然而,v1非常有限(只能使用 CPU 指标),几乎没有什么用。Kubernetes 社区已经在新的(v2)HPA 上工作了一段时间,并且根据我的经验,它运行得相当不错。主要问题不是稳定性,而是 API 可能发生的不向后兼容的变化。不久前,autoscaling/v2beta2发布了,它使用了不同的 API。我没有在书中包含它,因为(在撰写本文时)大多数 Kubernetes 集群尚不支持它。如果你正在运行 Kubernetes 1.11+,你可能想切换到v2beta2。如果这样做,记住你需要对我们探讨的 HPA 定义进行一些更改。逻辑仍然是一样的,它的行为也是一样的。唯一可见的区别在于 API。

请参考HorizontalPodAutoscaler v2beta2 autoscaling (kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#horizontalpodautoscaler-v2beta2-autoscaling),了解从v2beta1v2beta2的变化,这些变化在 Kubernetes 1.11+中可用。

就是这样。如果集群专门用于本书,请销毁它;如果不是,或者您计划立即跳转到下一章节,请保留它。如果您要保留它,请通过执行以下命令删除go-demo-5资源。

 1  helm delete go-demo-5 --purge
 2
 3  kubectl delete ns go-demo-5

在您离开之前,您可能希望复习本章的要点。

  • HPA 定期评估定义为扩展标准的指标。

  • HPA 从 Metrics Aggregator 获取数据,它并不真的在乎这些数据是来自 Metrics Server、Prometheus Adapter 还是我们可能使用的任何其他工具。

第六章:可视化指标和警报

你们人类经常设法获得你们不想要的东西,这是很有趣的。

  • 斯波克

仪表板是无用的!它们是浪费时间。如果你想看点东西,就去看 Netflix 吧。比任何其他选择都便宜。

我在许多公开场合重复了这些话。我认为公司夸大了对仪表板的需求。他们花费了大量精力创建了一堆图表,并让很多人负责盯着它们。好像那样会帮助任何人一样。仪表板的主要优势在于它们色彩丰富,充满线条、方框和标签。这些特性总是很容易卖给像 CTO 和部门负责人这样的决策者。当一个软件供应商与有权签发支票的决策者开会时,他知道没有“漂亮的颜色”就没有销售。软件做什么并不重要,重要的是它的外观。这就是为什么每家软件公司都专注于仪表板。

想想看。仪表板有什么好处?我们会盯着图表看,直到柱状图达到红线,表示达到了临界阈值吗?如果是这样,为什么不创建一个在相同条件下触发的警报,而不是浪费时间盯着屏幕等待发生什么。相反,我们可以做一些更有用的事情(比如看 Netflix)。

我们的“恐慌标准”是否比警报所能表达的更复杂?我认为它更复杂。然而,这种复杂性无法通过预定义的图表来反映。当然,意外事件会发生,我们需要深入挖掘数据。然而,“意外”这个词违背了仪表板所提供的内容。它们都是关于预期结果的。否则,我们如何在不知道期望结果的情况下定义一个图表呢?“它可以是任何东西”无法转化为图表。带有图表的仪表板是我们假设可能出错的方式,并将这些假设放在屏幕上,或者更常见的是放在很多屏幕上。

然而,意外只能通过查询指标来探索,不断深入直到找到问题的原因。这是一项调查工作,无法很好地转化为仪表板。我们使用 Prometheus 查询来进行这项工作。

然而,我在这里把一整章都献给了仪表板。

我承认仪表板并不是(完全)无用的。它们有时是有用的。我真正想传达的是它们的用处被夸大了,我们可能需要以不同于许多人习惯的方式构建和使用仪表板。

但是,我有点跳到了结论。我们稍后会讨论仪表板的细节。现在,我们需要创建一个集群,这将使我们能够进行实验,并将这个对话提升到更实际的水平。

创建一个集群

vfarcic/k8s-specs (github.com/vfarcic/k8s-specs) 仓库将继续作为我们的 Kubernetes 定义的来源。我们将确保通过拉取最新版本使其保持最新。

本章中的所有命令都可以在 06-grafana.sh (gist.github.com/vfarcic/b94b3b220aab815946d34af1655733cb) Gist 中找到。

 1  cd k8s-specs
 2
 3  git pull

要求与上一章相同。为了方便起见,Gists 在这里也是可用的。请随意使用它们来创建一个新的集群,或者验证您计划使用的集群是否符合要求。

我们应该使用哪些工具来创建仪表板?

使用 Prometheus 只需几分钟就会发现它并不是设计用来作为仪表板的。当然,你可以在 Prometheus 中创建图表,但它们并不是永久的,也没有提供很多关于数据呈现的功能。Prometheus 的图表设计用于可视化临时查询。这正是我们大部分时间所需要的。当我们收到来自警报的通知表明有问题时,通常会通过执行警报的查询来开始寻找问题的罪魁祸首,然后根据结果深入数据。也就是说,如果警报没有立即显示问题,那么就没有必要接收通知,因为这类明显的问题通常可以自动修复。

但是,正如我已经提到的,Prometheus 并没有仪表板功能,所以我们需要寻找其他工具。

如今,选择仪表板很容易。Grafana (grafana.com/)是该领域无可争议的统治者。其他解决方案太老旧,不值得费心,或者它们不支持 Prometheus。这并不是说 Grafana 是市场上最好的工具。但价格合适(免费),并且可以与许多不同的数据源一起使用。例如,我们可以争论Kibana (www.elastic.co/products/kibana)和 Grafana 一样好,甚至更好。但是,它仅限于来自 ElasticSearch 的数据。而 Grafana 除了可以使用来自 ElasticSearch 的数据外,还支持许多其他数据源。有人可能会说DataDog (www.datadoghq.com/)是一个更好的选择。但是,它遇到了与 Kibana 相同的问题。它与特定的指标来源绑定。

没有灵活性,也没有组合来自其他数据源的选项。更重要的是,这两者都不支持 Prometheus。

我将不再与其他工具进行比较。你可以自己尝试。目前,你需要相信我,Grafana 是一个不错的选择,如果不是最好的选择。如果我们在这一点上不同意,那么你继续阅读本章将毫无意义。

既然我已经强制选择了 Grafana,我们将继续安装它。

安装和设置 Grafana

接下来你可能知道发生了什么。我们谷歌搜索“Grafana Helm”,希望社区已经创建了一个我们可以使用的图表。我将为您揭示,Helm 的稳定频道中有 Grafana。我们所要做的就是检查数值并选择我们将使用的数值。

 1  helm inspect values stable/grafana

我不会列举我们可以使用的所有数值。我假设到现在为止,你已经是一个 Helm 忍者,可以自己探索它们。相反,我们将使用我已经定义的数值。

 1  cat mon/grafana-values-bare.yml

输出如下。

ingress:
  enabled: true
persistence:
  enabled: true
  accessModes:
  - ReadWriteOnce
  size: 1Gi
resources:
  limits:
    cpu: 20m
    memory: 50Mi
  requests:
    cpu: 5m
    memory: 25Mi

这些数值没有什么特别之处。我们启用了 Ingress,设置了persistence,并定义了resources。正如文件名所示,这是一个非常简单的设置,没有任何多余的东西。

现在剩下的就是安装图表。

 1  GRAFANA_ADDR="grafana.$LB_IP.nip.io"
 2    
 3  helm install stable/grafana \
 4      --name grafana \
 5      --namespace metrics \
 6      --version 1.17.5 \
 7      --set ingress.hosts="{$GRAFANA_ADDR}" \
 8      --values mon/grafana-values-bare.yml
 9
10  kubectl -n metrics \
11      rollout status deployment grafana

现在我们可以在您喜欢的浏览器中打开 Grafana。

 1  open "http://$GRAFANA_ADDR"

你将看到登录界面。就像许多其他 Helm 图表一样,安装包括admin用户和密码存储为一个秘密。

 1  kubectl -n metrics \
 2      get secret grafana \
 3      -o jsonpath="{.data.admin-password}" \
 4      | base64 --decode; echo

请返回到 Grafana 登录界面,输入admin作为用户名,并将上一个命令的输出粘贴为密码。

Grafana 不收集指标。相反,它使用其他数据源,因此我们的第一个操作是将 Prometheus 设置为数据源。

请点击“添加数据源”图标。

Prometheus作为名称,并选择它作为类型。我们将让 Grafana 通过 Kubernetes 服务prometheus-server连接到它。由于两者都在同一个命名空间中,URL 应设置为http://prometheus-server。剩下的就是保存并测试。

本章的输出和截图来自 Docker for Desktop。您在这里看到的内容可能与您在屏幕上观察到的内容略有不同。图 6-1:Grafana 的新数据源屏幕本章的截图比通常多。我相信它们将帮助您复制我们将讨论的步骤。

导入和自定义预制仪表板

数据源本身是无用的。我们需要以某种方式将它们可视化。我们可以通过创建自己的仪表板来实现这一点,但这可能不是 Grafana 的最佳(也不是最简单)介绍。相反,我们将导入一个现有的社区维护的仪表板。我们只需要选择一个适合我们需求的仪表板。

 1  open "https://grafana.com/dashboards"

随意花一点时间探索可用的仪表板。

我认为Kubernetes 集群监控grafana.com/dashboards/3119)仪表板是一个很好的起点。让我们导入它。

请点击左侧菜单中的“+”图标,然后点击“导入”链接,您将看到一个屏幕,允许我们导入 Grafana.com 的仪表板之一,或者粘贴定义它的 JSON。

我们将选择前一种选项。

图 6-2:Grafana 的导入仪表板选项

请在Grafana.com 仪表板字段中输入3119,然后点击“加载”按钮。您将看到一些字段。在这种情况下,唯一重要的是prometheus下拉列表。我们必须使用它来设置数据源。选择 Prometheus,并点击“导入”按钮。

您看到的是一个带有一些基本 Kubernetes 指标的仪表板。

图 6-3:Kubernetes 集群监控仪表板

但是,一些图表可能无法正常工作。这是否意味着我们导入了错误的仪表板?一个简单的答案恰恰相反。在所有可用的仪表板中,这个可能是最有效的。至少,如果我们只计算那些更多或少有用的图表。这样的结果很常见。这些仪表板由社区维护,但其中大多数是为个人使用而制作的。它们被配置为在特定集群中工作并使用特定的指标。您将很难找到许多不经任何更改就能正常工作并且同时显示您真正需要的内容的仪表板。相反,我认为这些仪表板是一个很好的起点。

我只是导入它们以获得一个我可以修改以满足我的特定需求的基础。这就是我们接下来要做的,至少部分地。

目前,我们只关注旨在使其完全运行的更改。我们将使一些当前没有数据的图表运行,并删除对我们无用的图表。

如果我们仔细看一下总使用量行,我们会发现集群文件系统使用情况N/A。它使用的指标可能有问题。让我们仔细看一下。

在某些集群中(例如,EKS),此仪表板中的硬编码文件系统是正确的。如果是这种情况(如果集群文件系统使用情况不是N/A),则您无需进行任何更改。但是,我建议您在想象您的集群使用不同文件系统的同时进行练习。这样你就可以学到一些技巧,可以应用到其他仪表板上。

请按下集群文件系统使用情况标题旁边的箭头,并单击编辑链接。

图 6-4:Grafana 编辑面板的选项

该图表使用的查询(为了可读性而格式化)如下。

 1  sum (
 2      container_fs_usage_bytes{
 3          device=~"^/dev/xvda.$",
 4          id="/",
 5          kubernetes_io_hostname=~"^$Node$"
 6      }
 7  ) / 
 8  sum (
 9      container_fs_limit_bytes{
10          device=~"^/dev/xvda.$",
11          id="/",
12          kubernetes_io_hostname=~"^$Node$"
13      }
14  ) * 100

我们不会深入讨论该查询的细节。你现在应该熟悉 Prometheus 表达式。相反,我们将专注于问题的可能原因。我们可能没有名为/dev/xvda的文件系统设备(除非您使用 EKS 或在某些情况下使用 GKE)。如果这是问题,我们可以通过简单地将值更改为我们的设备来修复图表。但是,在我们继续之前,我们可能会探索 Grafana 变量。毕竟,如果我们甚至不知道我们的设备是什么,将一个硬编码值更改为另一个值对我们毫无好处。

我们可以转到 Prometheus 并检索所有设备的列表,或者让 Grafana 为我们执行此操作。我们将选择后者。

仔细观察kubernetes_io_hostname。它设置为^$Node$。这是使用 Grafana 变量的示例。接下来,我们将探讨它们,试图替换硬编码的设备。

请单击位于屏幕右上角的返回仪表板按钮。

单击位于屏幕顶部的设置图标。您将看到我们可以更改的整个仪表板范围配置。随意在左侧菜单中探索选项。

由于我们有兴趣创建一个新变量,该变量将动态填充查询的device标签,我们接下来要做的是单击设置部分中的变量链接,然后点击+新按钮。

请将device键入为变量名称,将IO 设备键入为标签。我们将从 Prometheus(数据源)检索值,因此我们将将类型保留为查询。

接下来,我们需要指定数据源。选择$datasource。这告诉 Grafana 我们要从我们在导入仪表板时选择的任何数据源中查询数据。

到目前为止,一切可能都是不言自明的。接下来的内容不是。我们需要查阅文档,了解如何编写用作变量值的 Grafana 查询。

 1  open
    "http://docs.grafana.org/features/datasources/prometheus/#query-variable"

让这成为一个练习。通过文档找出如何编写一个查询,以检索container_fs_usage_bytes指标中可用的device标签的所有不同值。

Grafana 仅支持四种类型的变量查询,因此我想您不会很难找出我们应该添加到查询字段的表达式是label_values(container_fs_usage_bytes, device)

有了查询,剩下的就是单击添加按钮。

图 6-5:创建新仪表板变量的 Grafana 屏幕

现在我们应该返回仪表板并确认新变量是否可用。

您应该在屏幕左上部看到一个带有标签IO 设备的新下拉列表。如果您展开它,您将看到我们集群中使用的所有设备。确保选择正确的设备。这可能是/dev/sda1/dev/xvda1

接下来,我们需要更改图表以使用我们刚刚创建的变量。

请点击Cluster 文件系统使用情况图表旁边的箭头,并选择编辑。指标(查询)包含两个硬编码的^/dev/xvda.$值。将它们更改为$device,然后点击屏幕右上角的返回仪表板按钮。

就是这样。现在图表通过显示集群文件系统使用情况(/dev/sda1)的百分比来正确工作。

然而,下面的已使用总计数字仍然是N/A。我相信你知道该怎么做来修复它们。编辑这些图形,并用$device替换^/dev/xvda.$

该仪表板仍然有两个问题要解决。更准确地说,我们有两个图表对我们没有用。系统服务 CPU 使用情况系统服务内存使用情况图表的目的应该可以从它们的标题中推断出来。然而,大多数 Kubernetes 集群不提供对系统级服务的访问(例如 GKE)。即使提供,我们的 Prometheus 也没有配置来获取数据。如果你不相信我,复制其中一个图表的查询并在 Prometheus 中执行它。就目前而言,这些图表只是在浪费空间,所以我们将删除它们。

请点击系统服务 CPU 使用情况行标题旁边的垃圾桶图标。点击是以删除该行和面板。对系统服务内存使用情况行执行相同的操作。

现在我们已经完成了对仪表板的更改。它已经完全可操作,我们应该通过点击屏幕右上角的保存仪表板图标或按CTRL+S来保存更改。

我们不会详细介绍 Grafana 的所有选项和我们可以做的操作。我相信你可以自己弄清楚。这是一个非常直观的应用程序。相反,我们将尝试创建自己的仪表板。或者,至少,探索一些东西,让你可以继续自己的工作。

创建自定义仪表板

如果我们所有的需求都可以由现有的仪表板满足,那将是很好的。但是,这可能并不是事实。每个组织都是“特殊”的,我们的需求必须反映在我们的仪表板中。有时我们可以使用他人制作的仪表板,有时我们需要对其进行更改。在其他情况下,我们需要创建自己的仪表板。这就是我们接下来要探索的内容。

请点击左侧菜单中的+图标,并选择创建仪表板。您将被提供选择几种类型的面板。选择图形

在定义我们的第一个图表之前,我们将更改一些仪表板设置。请点击屏幕右上角的设置图标。

General部分,输入仪表板的名称。如果你今天没有灵感,你可以称它为我的仪表板。将标签设置为PrometheusKubernetes。在输入每个标签后,你需要按下回车键。最后,将时区更改为本地浏览器时间。

图 6-6:Grafana 的仪表板常规设置屏幕

那是无聊的部分。现在让我们转到更有趣的事情。我们将把我们在 Prometheus 中创建的警报之一转换成图表。我们将使用告诉我们实际 CPU 使用量与保留 CPU 的百分比的那个。为此,我们需要一些变量。更准确地说,我们并不真的需要它们,因为我们可以硬编码这些值。但是,如果以后决定更改它们,那将会引起问题。修改变量比更改查询要容易得多。

具体来说,我们需要变量来告诉我们最小的 CPU 是多少,这样我们就可以忽略那些设置为使用非常低保留的应用程序的阈值。此外,我们将定义作为下限和上限的变量。我们的目标是在与实际使用相比时,如果保留的 CPU 太低或太高,我们会收到通知,就像我们在 Prometheus 警报中所做的那样。

请从左侧菜单中选择变量部分,然后点击“添加变量”按钮。

当我们为导入的仪表板创建新的变量时,你已经看到了 Grafana 变量的屏幕。然而,这次我们将使用略有不同的设置。

将名称设置为minCpu,并选择常量作为类型。与之前创建的device变量不同,这次我们不需要 Grafana 来查询数值。通过使用这种类型,我们将定义一个常量值。请将值设置为0.005(五个 CPU 毫秒)。最后,我们不需要在仪表板中看到该变量,因为该值不太可能经常更改。如果将来需要更改它,我们总是可以回到这个屏幕并更新它。因此,将隐藏值更改为变量。

现在只需点击两次“添加”按钮。

图 6-7:Grafana 的仪表板新变量屏幕

我们还需要两个变量。可能没有必要重复相同的说明,所以请使用以下信息来创建它们。

 1  Name:  cpuReqPercentMin
 2  Type:  Constant
 3  Label: Min % of requested CPU
 4  Hide:  Variable
 5  Value: 50
 6
 7  Name:  cpuReqPercentMax
 8  Type:  Constant
 9  Label: Max % of requested CPU
10  Hide:  Variable
11  Value: 150

现在我们可以返回并定义我们的图表。请点击屏幕右上角的返回仪表板图标。

你已经知道如何编辑面板。点击面板标题旁边的箭头,然后选择编辑。

我们将从常规部分开始。请选择它。

接下来,将%实际 CPU 与保留 CPU作为标题,以及后面的文本作为描述。

 1  The percentage of actual CPU usage compared to reserved. The
    calculation excludes Pods with reserved CPU equal to or smaller than
    $minCpu. Those with less than $minCpu of requested CPU are ignored.

请注意描述中$minCpu变量的使用。当我们回到仪表板时,它将展开为其值。

接下来,请切换到指标选项卡。那里才是真正的操作发生的地方。

我们可以定义多个查询,但对于我们的用例,一个应该就足够了。请在A右侧的字段中输入以下查询。

为了方便起见,查询可在grafana-actual-vs-reserved-cpu (gist.github.com/vfarcic/1b027a1e2b2415e1d156687c1cf14012) Gist 中找到。

 1  sum(label_join(
 2      rate(
 3          container_cpu_usage_seconds_total{
 4              namespace!="kube-system",
 5              pod_name!=""
 6          }[5m]
 7      ),
 8      "pod",
 9      ",",
10      "pod_name"
11  )) by (pod) /
12  sum(
13      kube_pod_container_resource_requests_cpu_cores{
14          namespace!="kube-system",
15          namespace!="ingress-nginx"
16      }
17  ) by (pod) and 
18  sum(
19      kube_pod_container_resource_requests_cpu_cores{
20          namespace!="kube-system",
21          namespace!="ingress-nginx"
22      }
23  ) by (pod) > $minCpu

该查询几乎与我们在第三章中使用的查询之一几乎相同,收集和查询指标并发送警报。唯一的区别是使用了$minCpu变量。

输入查询后的几分钟后,我们应该看到图表活跃起来。可能只包括一个 Pod,因为我们的许多应用程序被定义为使用五个 CPU 毫秒($minCpu的值)或更少。

图 6-8:基于图表的 Grafana 面板

接下来,我们将调整图表左侧的单位。请点击选项卡。

展开左 Y 单位,选择无,然后选择百分比(0.0-1.0)。由于我们不使用右 Y轴,请取消选中显示复选框。

接下来是图例部分。请选择它。

勾选作为表格显示选项在右侧值>当前复选框。更改将立即应用到图表上,你应该不难推断出每个选项的作用。

只剩下一件事。我们应该定义上限和下限阈值,以清楚地指示结果是否超出预期范围。

请点击警报选项卡。

单击“创建警报”按钮,并将“高于”条件更改为“范围之外”。将下两个字段的值设置为0,51,5。这样,当实际 CPU 使用率低于 50%或高于 150%时,与保留值相比,应该会通知我们。

图 6-9:带有警报的 Grafana 图表

我们已经完成了图表,所以请返回到仪表板并享受“漂亮的颜色”。您可能希望拖动图表的右下角以调整其大小。

我们可以看到请求的 CPU 使用率与实际 CPU 使用率之间的差异。我们还有阈值(用红色标记),它将告诉我们使用情况是否超出了设定的边界。

现在出现了一个重要问题。这样的图表有用吗?答案取决于我们打算用它做什么。

如果目标是盯着它,等待其中一个 Pod 开始使用过多或过少的 CPU,我只能说您正在浪费可以用于更有生产力任务的才能。毕竟,我们已经在 Prometheus 中有类似的警报,当满足条件时会向我们发送 Slack 通知。它比我们在图表中拥有的更先进,因为它只会在 CPU 使用率在一定时间内激增时通知我们,从而避免可能在几秒或几分钟后解决的临时问题。我们应该将这些情况视为误报。

该图的另一个用途可能更为被动。我们可以忽略它(关闭 Grafana),只有在上述 Prometheus 警报触发时才回来。这可能更有意义。尽管我们可以在 Prometheus 中运行类似的查询并获得相同的结果,但具有预定义图表可以使我们免于编写这样的查询。您可以将其视为具有相应图形表示的查询注册表。这是更有意义的事情。与盯着仪表板(选择 Netflix)相比,我们可以在需要时回来。虽然在某些情况下这可能是一个合理的策略,但它只在非常简单的情况下起作用。当出现问题时,一个单独的预定义图表解决问题或更准确地说是提供问题原因的明确指示时,图表确实提供了重要价值。然而,往往情况并不那么简单,我们将不得不求助于 Prometheus 来深入挖掘指标。

盯着有图表的仪表板是浪费时间。在收到有关问题的通知后访问仪表板可能更有些意义。但是,除了琐碎的问题,其他问题都需要通过 Prometheus 指标进行更深入的挖掘。

尽管如此,我们刚刚制作的图表可能会证明自己是有用的,所以我们会保留它。在这种情况下,我们可能想要做的是更改 Prometheus 警报的链接(我们目前在 Slack 上收到的警报),以便它直接带我们到图表(而不是仪表板)。我们可以通过单击面板名称旁边的箭头,并选择“查看”选项来获取该链接。

我相信,如果我们将面板类型从图表更改为更少颜色、更少线条、更少坐标轴和没有其他漂亮东西的类型,我们的仪表板可以变得更有用。

创建信号量仪表板

如果我声称仪表板为我们带来的价值比我们想象的要低,你可能会问自己这一章开头的同样问题。为什么我们要谈论仪表板?嗯,我已经改变了我的说法,从“仪表板是无用的”变成了“仪表板中有一些价值”。它们可以作为查询的注册表。通过仪表板,我们不需要记住我们需要在 Prometheus 中编写的表达式。在我们深入挖掘指标之前,它们可能是我们寻找问题原因的一个很好的起点。但是,我包括仪表板在解决方案中还有另一个原因。

我喜欢大屏幕。走进一个有大屏幕显示重要内容的房间是非常令人满意的。通常有一个房间,操作员坐在四面墙上都是显示器的环境中。那通常是一个令人印象深刻的景象。然而,许多这样的情况存在问题。一堆显示很多图表的监视器可能并不比漂亮的景象更有意义。最初的几天过后,没有人会盯着图表看。如果这不是真的,你也可以解雇那个人,因为他是在假装工作。

让我再重复一遍。

仪表板并不是为了我们盯着它们而设计的,尤其是当它们在所有人都能看到的大屏幕上时。

因此,如果拥有大屏幕是一个好主意,但图形不是一个好的装饰候选,那么我们应该做什么呢?答案就在于信号量。它们类似于警报,应该清晰地指示系统的状态。如果屏幕上的一切都是绿色的,我们就没有理由做任何事情。其中一个变成红色是一个提示,我们应该做一些事情来纠正问题。因此,我们必须尽量避免误报。如果某事变成红色,而不需要任何行动,我们很可能在将来开始忽视它。当这种情况发生时,我们冒着忽视一个真正问题的风险,认为它只是另一个误报。因此,每次出现警报都应该跟随着一个行动。

这可以是纠正系统的修复措施,也可以是改变导致其中一个信号量变红的条件。无论哪种情况,我们都不应该忽视它。

信号量的主要问题在于它们对 CTO 和其他决策者来说并不那么吸引人。它们既不丰富多彩,也不显示很多框、线和数字。人们经常将有用性与外观上的吸引力混淆。尽管如此,我们并不是在建造应该出售给 CTO 的东西,而是在建造可以帮助我们日常工作的东西。

与图形相比,信号量作为查看系统状态的一种方式要更有用得多,尽管它们看起来不像图形那样丰富多彩和令人愉悦。

让我们创建我们的第一个信号量。

请点击屏幕右上角的添加面板图标,并选择 Singlestat。点击面板标题旁边的箭头图标,然后选择编辑。

在大多数情况下,创建一个单一的状态(信号量)与创建一个图形并没有太大的不同。显著的区别在于应该产生一个单一值的度量(查询)。我们很快就会到达那里。现在,我们将改变面板的一些一般信息。

请选中“常规”选项卡。

Pods with <$cpuReqPercentMin%||>$cpuReqPercentMax% actual compared to reserved CPU输入为标题,然后输入后面的文本为描述。

 1  The number of Pods with less than $cpuReqPercentMin% or more than
    $cpuReqPercentMax% actual compared to reserved CPU

这个单一统计将使用与我们之前制作的图表类似的查询。然而,虽然图表显示当前使用量与保留 CPU 相比,但该面板应该显示有多少个 Pod 的实际 CPU 使用量超出了基于保留 CPU 的边界。这反映在我们刚刚输入的标题和描述中。正如您所看到的,这一次我们依赖更多的变量来表达我们的意图。

现在,让我们把注意力转向查询。请单击“指标”选项卡,并将以下表达式输入到* A *旁边的字段中。

为了您的方便,该查询可在grafana-single-stat-actual-vs-reserved-cpu (gist.github.com/vfarcic/078674efd3b379c211c4da2c9844f5bd) Gist 中找到。

 1  sum(
 2      (
 3          sum(
 4              label_join(
 5                  rate(container_cpu_usage_seconds_total{
 6                      namespace!="kube-system",
 7                      pod_name!=""}[5m]),
 8                      "pod",
 9                      ",",
10                      "pod_name"
11              )
12          ) by (pod) /
13          sum(
14              kube_pod_container_resource_requests_cpu_cores{
15                  namespace!="kube-system",
16                  namespace!="ingress-nginx"
17              }
18          ) by (pod) and
19          sum(
20              kube_pod_container_resource_requests_cpu_cores{
21                  namespace!="kube-system",
22                  namespace!="ingress-nginx"
23              }
24          ) by (pod) > $minCpu
25      ) < bool ($cpuReqPercentMin / 100)
26  ) +
27  sum(
28      (
29          sum(
30              label_join(
31                  rate(
32                      container_cpu_usage_seconds_total{
33                          namespace!="kube-system",
34                          pod_name!=""
35                      }[5m]
36                  ),
37                  "pod",
38                  ",",
39                  "pod_name"
40              )
41          ) by (pod) /
42          sum(
43              kube_pod_container_resource_requests_cpu_cores{
44                  namespace!="kube-system",
45                  namespace!="ingress-nginx"
46              }
47          ) by (pod) and
48          sum(
49              kube_pod_container_resource_requests_cpu_cores{
50                  namespace!="kube-system",
51                  namespace!="ingress-nginx"
52              }
53          ) by (pod) > $minCpu
54      ) > bool ($cpuReqPercentMax / 100)
55  )

该查询类似于我们用作 Prometheus 警报的查询之一。更准确地说,它是两个 Prometheus 警报的组合。前半部分返回具有超过$minCpu(5 CPU 毫秒)的保留 CPU 和实际 CPU 使用低于$cpuReqPercentMin(50%)的 Pod 数量。后半部分与第一部分几乎相同,只是返回 CPU 使用高于$cpuReqPercentMax(150%)的 Pod。

由于我们的目标是返回一个单一的统计数据,即 Pod 的数量,在这种情况下,您可能会感到惊讶,我们使用了sum而不是count。统计 Pod 的数量确实更有意义,但如果没有结果,将返回N/A。为了避免这种情况,我们使用了bool的技巧。通过将其放在表达式前面,如果有匹配,则返回1,如果没有,则返回0。这样,如果没有 Pod 符合条件,我们不会得到空结果,而是得到0,这更好地表示有问题的 Pod 的数量。

总的来说,我们正在检索所有实际 CPU 低于保留 CPU 的$cpuReqPercentMin(50%)的 Pod 的总和,以及所有实际 CPU 高于保留 CPU 的$cpuReqPercentMax(150%)的 Pod 的总和。在这两种情况下,只有超过$minCpu(五个 CPU 毫秒)的 Pod 被包括在内。查询本身并不是我们可以编写的最简单的查询,但考虑到我们已经花了很多时间处理 Prometheus 查询,我认为我不应该用一些琐碎的东西“侮辱”您。

接下来,请单击“选项”选项卡。这是我们将定义应触发颜色变化的条件的地方。

我们不想要在指定周期内的平均值,而是有问题的 Pods 的当前数量。我们将通过将 Stat 下拉列表的值更改为 Current 来实现这一点。

我们希望这个面板非常显眼,所以我们将将 Stat 字体大小更改为200%。我更喜欢更大的字体,但是 Grafana 不允许我们超过这个值。

接下来,我们想要更改面板的背景颜色,请勾选 Coloring Background 复选框。

我们最多可以使用三种颜色,但我相信我们只需要两种。要么一个或多个 Pod 符合条件,要么没有一个符合条件。

一旦查询返回1或更高的数字,我们应该立即收到通知。请将1键入为 Coloring Thresholds。如果我们有更多,我们将用逗号分隔它们。

最后,由于我们只有两个条件,绿色和红色,我们需要将第二个颜色从橙色更改为红色。请点击 Coloring Colors 中的red图标,并将值替换为单词red。第三种颜色没有使用,所以我们将保持不变。

图 6-10:Grafana 的单个统计面板

我们已经完成了我们的面板,所以返回仪表板

在继续之前,请点击保存仪表板图标,然后点击保存按钮。

到目前为止,我们创建了一个带有图形和单个统计(信号量)的仪表板。前者显示了 CPU 使用率与保留 CPU 随时间的偏差。它有警报(红色区域),告诉我们一个向量是否超出了预定义的边界。单个统计(信号量)显示一个数字,具有绿色或红色的背景,具体取决于该数字是否达到了阈值,而在我们的情况下,阈值设置为1

我们刚刚开始,还需要在此仪表板变得有用之前添加许多其他面板。我会免去您定义其他面板的重复指令。我觉得您已经掌握了 Grafana 的工作原理。至少,您应该具备可以自行扩展的基础知识。

我们将快进。我们将导入我准备好的仪表板并讨论设计选择。

更适合大屏幕的仪表板

我们探讨了如何创建一个带有图形和单个统计(信号量)的仪表板。两者都基于类似的查询,显著的区别在于它们显示结果的方式。我们将假设我们开始构建的仪表板的主要目的是在大屏幕上可用,对许多人可见,并不是作为我们在笔记本电脑上持续保持打开的东西。至少,不是持续的。

这样的仪表板的主要目的应该是什么?在我回答这个问题之前,我们将导入我为本章创建的一个仪表板。

请从左侧菜单中单击+按钮,然后选择导入。在Grafana.com 仪表板中键入9132,然后按加载按钮。选择Prometheus 数据源。随时更改任何值以满足您的需求。尽管如此,您可能希望在更熟悉仪表板之后再进行更改。

无论如何,完成后点击导入按钮。

图 6-11:基于信号的 Grafana 仪表板

您可能会看到一个或多个红色的信号。这是正常的,因为我们集群中的一些资源配置不正确。例如,Prometheus 可能请求的内存比它实际需要的要少。这没关系,因为它可以让我们看到仪表板的运行情况。Gists 中使用的定义不应该是生产就绪的,您已经知道您需要调整它们的资源,以及可能还有其他一些东西。

您会注意到我们导入的仪表板只包含信号。至少在第一次看时是这样。尽管它们可能不像图形和其他类型的面板那样吸引人,但它们作为系统健康的指标要更有效得多。我们不需要一直看着那个仪表板。只要它显示在大屏幕上,我们就可以在做其他事情的同时工作。如果其中一个方框变成红色,我们会注意到。这将是一个行动的呼唤。或者更准确地说,如果一个红色的方框持续保持红色,排除了它是一个自行解决的错误警报的可能性,我们将需要采取一些行动。

您可以将此仪表板视为 Prometheus 警报的补充。它并不取代它们,因为我们将在稍后讨论一些微妙但重要的区别。

我不会描述每个面板,因为它们是我们之前创建的 Prometheus 警报的反映。您现在应该对它们很熟悉。如果有疑问,请单击面板左上角的 i 图标。如果描述不够,请进入面板的编辑模式,检查查询和着色选项。

请注意,仪表板可能不是完美的。您可能需要更改一些变量值或着色阈值。例如,节点 面板的阈值设置为 4,5。从颜色来看,我们可以看到如果节点数跳到四个,它会变成橙色(警告),如果变成五个,就会变成红色(恐慌)。您的值可能会有所不同。理想情况下,我们应该使用变量而不是硬编码的阈值,但目前在 Grafana 中还不可能。变量并非在所有地方都受支持。作为开源项目的支持者,您应该提交 PR。如果您这样做了,请告诉我。

这是否意味着我们所有的仪表板都应该是绿色和红色的框,里面只有一个数字?我确实认为信号灯应该是“默认”显示。当它们是绿色时,就不需要其他任何东西。如果不是这种情况,我们应该增加信号灯的数量,而不是用随机图表来混乱我们的监视器。然而,这就引出了一个问题。当一些框变成红色甚至橙色时,我们该怎么办?

在框下面,您会找到图表 行,带有额外的面板。它们默认情况下是不可见的,有其原因。

在正常情况下,看到它们是没有理由的。但是,如果其中一个信号灯发出警报,我们可以展开图表,看到有关问题的更多细节。

图 6-12:基于表格和图表的 Grafana 仪表板

图表 行内的面板是对警报 行内的面板(信号灯)的反映。每个图表显示了与相同位置的单个统计数据相关的更详细的数据(但是不同的行)。这样,我们就不需要浪费时间来弄清哪个图表对应于“红色框”。

相反,我们可以直接跳转到相应的图表。如果右侧第二行的信号灯变成红色,就看右侧第二行的图表。如果多个框变成红色,我们可以快速查看相关的图表,并尝试找到关联(如果有的话)。往往情况下,我们将不得不从 Grafana 切换到 Prometheus,并深入挖掘指标。

像你面前的这个仪表板应该让我们快速启动解决问题。顶部的信号灯提供了警报机制,应该导致下面的图表快速指示出问题的可能原因。从那里开始,如果原因很明显,我们可以转到 Prometheus 并开始调试(如果这个词用得对的话)。

带有信号灯的仪表板应该显示在办公室周围的大屏幕上。它们应该提供问题的指示。相应的图表(和其他面板)提供了对问题的第一印象。Prometheus 作为我们用来挖掘指标直到找到罪魁祸首的调试工具。

我们探讨了一些提供类似功能的东西。但是,Prometheus 警报、信号灯、图表警报和 Grafana 通知之间的区别可能并不清楚?为什么我们没有创建任何 Grafana 通知?我们将在接下来探讨这些问题以及其他一些问题。

Prometheus 警报 vs. Grafana 通知 vs. 信号灯 vs. 图表警报

标题本身可能会让人感到困惑,所以让我们简要描述一下其中提到的每个元素。

Prometheus 警报和 Grafana 通知具有相同的目的,尽管我们没有探讨后者。我会让你自己学习 Grafana 通知的工作原理。谁知道呢?在接下来的讨论之后,你可能甚至不想花时间去了解它们。

Grafana 通知可以以与 Prometheus 的警报转发方式类似的方式转发给不同的接收者。然而,有一些事情使 Grafana 通知变得不那么吸引人。

如果我们可以通过 Prometheus 警报实现与 Grafana 警报相同的结果,那么前者就具有明显的优势。如果从 Prometheus 触发了警报,那意味着导致触发警报的规则也在 Prometheus 中定义。

因此,评估是在数据源处进行的,我们避免了 Grafana 和 Prometheus 之间不必要的延迟。我们离数据源越近越好。在警报/通知的情况下,更近意味着在 Prometheus 内部。

在 Prometheus 中定义警报的另一个优势是它允许我们做更多的事情。例如,在 Grafana 中没有与 Prometheus 的for语句相当的东西。我们无法定义一个只有在条件持续一段时间后才触发的通知。我们需要通过对查询进行非平凡的添加来实现相同的功能。另一方面,Alertmanager 提供了更复杂的方法来过滤警报,对其进行分组,并仅转发符合特定条件的警报。在 Prometheus 和 Alertmanager 中定义警报而不是在 Grafana 中定义通知还有许多其他优点。但我们不会详细讨论所有这些优点。我会留给你去发现所有的区别,除非你已经被说服放弃 Grafana 通知,转而使用 Prometheus 警报和 Alertmanager。

有一个重要的原因你不应该完全忽视 Grafana 通知。你使用的数据源可能没有警报/通知机制,或者它可能是你没有拥有的企业许可证的一部分。由于 Grafana 支持许多不同的数据源,其中 Prometheus 只是其中之一,Grafana 通知允许我们使用任何这些数据源,甚至将它们组合在一起。

基于存储在那里的指标,坚持使用 Prometheus 进行警报/通知。对于其他数据源,Grafana 警报可能是更好的选择,甚至是唯一的选择。

现在我们简要探讨了 Prometheus 警报和 Grafana 通知之间的区别,我们将进入信号量。

信号量(基于单个状态面板的 Grafana 仪表板)不能取代 Prometheus 警报。首先,很难,甚至不可能,创建只有在值达到某个阈值一段时间后才变红的信号量(例如,就像 Prometheus 警报中的for语句)。这意味着信号量可能会变红,只是在几分钟后又变回绿色。这并不是一个需要采取行动的原因,因为问题在短时间内会自动解决。如果我们每次在 Grafana 中看到红色就跳起来,我们可能会身体非常健康,但我们不会做太多有用的工作。

信号量是可能存在问题的指示,可能不需要任何干预。虽然应该避免这种错误的积极性,但要完全摆脱它们几乎是不可能的。这意味着我们应该盯着屏幕看,看看红色的方框是否在我们采取行动之前至少持续几分钟。信号量的主要目的不是向个人或团队提供通知,告诉他们应该解决问题。Slack、电子邮件和其他目的地的通知会做到这一点。信号量提供了对系统状态的认识。

最后,我们探讨了在图表上定义的警报。这些是图表中的红线和区域。它们并不是表明出现问题的良好指标。它们并不容易被发现,因此不能引起注意,而且绝对不能取代通知。相反,它们在我们发现存在问题后帮助我们。如果通知或信号量警告我们存在可能需要解决的问题,图表警报将帮助我们确定罪魁祸首。哪个 Pod 处于红色区域?哪个入口收到的请求超出了预期?这些只是我们可以通过图表警报回答的一些问题。

现在呢?

Grafana 相对简单易用。如果您知道如何为连接到 Grafana 的数据源(例如 Prometheus)编写查询,那么您已经学会了最具挑战性的部分。其余部分大多是勾选框、选择面板类型和在屏幕上排列事物。主要困难在于避免被创建一堆没有太多价值的花哨仪表板所带走。一个常见的错误是为我们能想象到的一切创建图表。这只会降低那些真正重要的价值。少即是多。

就这样。如果集群专门用于本书,请销毁它;如果不是,或者您打算立即跳到下一章,那就保留它。如果要保留它,请通过执行以下命令删除grafana图表。如果我们在接下来的章节中需要它,我会确保它包含在 Gists 中。

 1  helm delete grafana --purge

在离开之前,您可能希望复习本章的要点。

  • 查看带有图形的仪表板是浪费时间。在收到有关问题的通知后访问仪表板会更有意义一些。但是,除了琐碎的问题,其他问题都需要通过 Prometheus 指标进行更深入的挖掘。

  • 仪表板并不是为了我们盯着它们看而设计的,尤其是当它们显示在大屏幕上,所有人都能看到时。

  • 信号灯比图表更有用,作为查看系统状态的一种方式,尽管它们看起来没有图表那么丰富多彩和令人愉悦。

  • 带有信号灯的仪表板应该显示在办公室周围的大屏幕上。它们应该提供问题的指示。相应的图表(和其他面板)提供了对问题的第一印象。Prometheus 作为我们用来深入挖掘指标直到找到罪魁祸首的调试工具。

  • 对于基于存储在那里的指标的警报/通知,坚持使用 Prometheus。对于其他数据源,Grafana 警报可能是更好甚至是唯一的选择。

第七章:收集和查询日志

在关键时刻,人们有时会看到他们希望看到的东西。

  • Spock

到目前为止,我们的主要重点是指标。我们以不同的形式和不同的目的使用它们。在某些情况下,我们使用指标来扩展 Pod 和节点。在其他情况下,指标被用来创建警报,以便在出现无法自动修复的问题时通知我们。我们还创建了一些仪表板。

然而,指标通常是不够的。特别是在处理需要手动干预的问题时。当仅仅依靠指标是不够的时,我们通常需要查看日志,希望它们能揭示问题的原因。

日志经常被误解,或者更准确地说,与指标混淆。对许多人来说,日志和指标之间的界限变得模糊。有些人从日志中提取指标。其他人则将指标和日志视为相同的信息来源。这两种方法都是错误的。指标和日志是独立的实体,它们有不同的用途,它们之间有明显的区别。我们将它们分开存储,并且我们使用它们来解决不同类型的问题。我们将暂时搁置这些讨论和其他一些讨论。我们不会基于理论细节进行探讨,而是通过实际示例来探索它们。为此,我们需要一个集群。

创建一个集群

你知道该怎么做。我们将进入vfarcic/k8s-specs (github.com/vfarcic/k8s-specs)存储库的目录,我们将拉取最新版本的代码,以防我最近推送了一些东西,然后我们将创建一个新的集群,除非您已经有一个可用。

本章中的所有命令都可以在07-logging.sh (gist.github.com/vfarcic/74774240545e638b6cf0e01460894f34) Gist 中找到。

 1  cd k8s-specs
 2
 3  git pull

这一次,集群的要求发生了变化。我们需要比以前更多的内存。主要问题是 ElasticSearch 非常耗资源。

如果您使用Docker for Desktopminikube,您需要将集群的内存增加到10 GB。如果这对您的笔记本来说太多,您可以选择阅读通过 Elasticsearch、Fluentd 和 Kibana 探索集中式日志记录,而不运行示例,或者您可能需要切换到云提供商(AWS、GCP 或 Azure)之一。

对于 EKSAKS,我们将需要更大的节点。对于 EKS,我们将使用 t2.large,对于 AKS,我们将使用 Standard_B2ms。两者都基于 2 个 CPU8 GB RAM

GKE 的要求与以前相同。

除了新的要求之外,还应该注意到在本章中我们不需要 Prometheus,所以我从 Gists 中删除了它。

请随意使用以下其中一个 Gist 来创建一个新的集群,或者验证您计划使用的集群是否符合要求。

现在我们有一个可用的集群,我们将探索如何通过 kubectl 使用日志。这将为后续更全面的解决方案提供一个基础。

通过 kubectl 探索日志

大多数人在 Kubernetes 中接触日志的第一次是通过 kubectl。几乎不可避免地会使用它。

当我们学习如何驯服 Kubernetes 时,我们必须在遇到困难时检查日志。在 Kubernetes 中,“日志”一词是指集群内运行的我们和第三方应用程序产生的输出。然而,这些不包括不同 Kubernetes 资源生成的事件。尽管许多人也称它们为日志,但 Kubernetes 将它们与日志分开,并称其为事件。我相信你已经知道如何从应用程序中检索日志以及如何查看 Kubernetes 事件。尽管如此,我们也会在这里简要探讨它们,因为这将使我们后面的讨论更有关联。我保证会简短地介绍,如果你认为 Kubernetes 中日志和事件的简要概述对你来说太基础,你可以跳过这一部分。

我们将安装已经熟悉的go-demo-5应用程序。它应该会产生足够的日志供我们探索。由于它由几个资源组成,我们也必然会创建一些 Kubernetes 事件。

我们出发了。

 1  GD5_ADDR=go-demo-5.$LB_IP.nip.io
 2
 3  echo $GD5_ADDR
 4
 5  helm upgrade -i go-demo-5 \
 6      https://github.com/vfarcic/go-demo-5/releases/download/
    0.0.1/go-demo-5-0.0.1.tgz \
 7      --namespace go-demo-5 \
 8      --set ingress.host=$GD5_ADDR
 9
10  kubectl -n go-demo-5 \
11    rollout status deployment go-demo-5
12
13  curl "http://$GD5_ADDR/demo/hello"

我们部署了go-demo-5并发送了一个curl请求来确认它确实在运行。

本章中的输出和截图来自 minikube,除了专门用于 GKE、EKS 和 AKS 的部分。你在这里看到的内容可能与你在屏幕上观察到的有轻微差异。

要查看由 Kubernetes 生成并限于特定资源的“日志”,我们需要检索事件。

 1  kubectl -n go-demo-5 \
 2    describe sts go-demo-5-db

输出仅限于“事件”部分的消息如下。

...
Events:
... Message
... -------
... create Claim go-demo-5-db-go-demo-5-db-0 Pod go-demo-5-db-0 in StatefulSet go-demo-5-db success
... create Pod go-demo-5-db-0 in StatefulSet go-demo-5-db successful
... create Claim go-demo-5-db-go-demo-5-db-1 Pod go-demo-5-db-1 in StatefulSet go-demo-5-db success
... create Pod go-demo-5-db-1 in StatefulSet go-demo-5-db successful
... create Claim go-demo-5-db-go-demo-5-db-2 Pod go-demo-5-db-2 in StatefulSet go-demo-5-db success
... create Pod go-demo-5-db-2 in StatefulSet go-demo-5-db successful

你在面前看到的事件,在某种程度上,是由 Kubernetes 生成的日志,这里是go-demo-5-db StatefulSet。

尽管这些事件很有用,但通常还不够。往往我们事先不知道问题出在哪里。如果我们的 Pod 之一表现不佳,问题可能在于该 Pod,但也可能在创建它的 ReplicaSet 中,或者可能在创建 ReplicaSet 的 Deployment 中,或者可能节点从集群中分离了,或者可能是完全不同的原因。

对于除了最小的系统之外的任何系统来说,从一个资源到另一个资源,从一个节点到另一个节点去找到问题的原因绝非实际、可靠和快速。

简而言之,通过描述资源来查看事件并不是解决问题的方法,我们需要找到替代方案。

但在此之前,让我们看看应用程序的日志发生了什么。

我们部署了几个go-demo-5 API 的副本和几个 MongoDB 的副本。如果我们怀疑其中一个存在问题,我们如何探索它们的日志?我们可以执行像下面这样的kubectl logs命令。

 1  kubectl -n go-demo-5 \
 2      logs go-demo-5-db-0 -c db

输出显示了go-demo-5-db-0 Pod 内db容器的日志。

虽然先前的输出仅限于单个容器和单个 Pod,但我们可以使用标签从多个 Pod 中检索日志。

 1  kubectl -n go-demo-5 \
 2      logs -l app=go-demo-5

这一次,输出来自所有标签为app设置为go-demo-5的 Pod。我们扩大了我们的结果,这通常是我们所需要的。如果我们知道go-demo-5的 Pod 存在问题,我们需要弄清楚问题是存在于多个 Pod 中还是仅限于一个 Pod。虽然先前的命令允许我们扩大搜索范围,但如果日志中有可疑的内容,我们将不知道其来源。从多个 Pod 中检索日志并不能让我们更接近知道哪些 Pod 的行为不端。

使用标签仍然非常有限。它们绝对不能替代更复杂的查询。我们可能需要根据时间戳、节点、关键字等来过滤结果。虽然我们可以通过额外的kubectl logs参数和对grepsed和其他 Linux 命令的创造性使用来实现其中一些功能,但这种检索、过滤和输出日志的方法远非最佳。

往往kubectl logs命令并不能为我们提供足够的选项来执行除了最简单的日志检索之外的任何操作。

我们需要一些东西来提高我们的调试能力。我们需要一个强大的查询语言,可以让我们过滤日志条目,我们需要足够的关于这些日志来源的信息,我们需要查询速度快,我们需要访问集群中任何部分创建的日志。我们将尝试通过设置集中式日志解决方案来实现这一点,以及其他一些事情。

选择集中式日志解决方案

我们需要做的第一件事是找到一个存储日志的地方。考虑到我们希望能够过滤日志条目,将它们存储在文件中应该从一开始就被排除。我们需要的是一种数据库。它更重要的是快速而不是事务性,所以我们很可能在寻找一种内存数据库解决方案。但在我们看选择之前,我们应该讨论一下我们的数据库位置。我们应该在集群内运行它,还是应该使用一个服务?在立即做出决定之前,我们将探讨两种选择,然后再做出选择。

日志服务有两个主要类型。如果我们正在使用云提供商之一的集群,一个明显的选择可能是使用他们提供的日志解决方案。EKS 有 AWS CloudWatch,GKE 有 GCP Stackdriver,AKS 有 Azure Log Analytics。如果您选择使用其中一个云供应商,这可能是很有意义的。如果一切都已经设置好并等待您,为什么要费心设置自己的解决方案或寻找第三方服务呢?我们很快将探索它们。

由于我的使命是为(几乎)任何人提供有效的指导,我们还将探讨一些在托管供应商之外找到的日志服务解决方案。但是,我们应该选择哪一个?市场上有太多的解决方案。例如,我们可以选择Splunk (www.splunk.com/) 或 DataDog (www.datadoghq.com/)。两者都是很好的选择,而且都不仅仅是日志解决方案。我们可以使用它们来收集指标(就像使用 Prometheus 一样)。它们提供仪表板(就像 Grafana 一样),还有其他一些功能。稍后,我们将讨论是否应该将日志和指标合并到一个工具中。目前,我们的重点只是日志记录,这也是我们跳过 Splunk、DataDog 和类似综合工具的主要原因,因为它们提供的远远超出我们所需的。这并不意味着你应该放弃它们,而是本章试图保持对日志记录的关注。

有许多日志记录服务可用,包括Scalyr (www.scalyr.com/pricing), logdna (logdna.com/), sumo logic (www.sumologic.com/) 等等。我们不会逐一介绍它们,因为这将花费比我认为有用的时间和空间更多。鉴于大多数服务在涉及日志记录时非常相似,我将跳过详细比较,直接介绍Papertrail (papertrailapp.com/),这是我最喜欢的日志记录服务。请记住,我们只会将其用作示例。我假设您至少会检查其他一些服务,并根据自己的需求做出选择。

作为服务的日志记录可能并不适合所有人。有些人可能更喜欢自托管的解决方案,而其他人甚至可能不被允许将数据发送到集群外部。在这些情况下,自托管的日志记录解决方案很可能是唯一的选择。

即使您不受限于自己的集群,也可能有其他原因将其保留在内部,延迟只是其中之一。我们也将探讨自托管的解决方案,因此让我们选择一个。它将是哪一个?

鉴于我们需要一个存储日志的地方,我们可能会研究传统数据库。然而,大多数传统数据库都不符合我们的需求。像 MySQL 这样的事务性数据库需要固定的模式,因此我们可以立即将其排除。NoSQL 更适合,因此我们可能会选择类似 MongoDB 的东西。但是,这将是一个糟糕的选择,因为我们需要能够执行非常快速的自由文本搜索。为此,我们可能需要一个内存数据库。MongoDB 不是其中之一。我们可以使用 Splunk Enterprise,但本书致力于免费(大多数是开源)的解决方案。到目前为止,我们唯一的例外是云提供商,我打算保持这种方式。

我们提到的少数要求(快速、自由文本、内存中、免费解决方案)将我们的潜在候选者限制在很少的几个。Solrlucene.apache.org/solr/)是其中之一,但它的使用量一直在下降,如今很少被使用(有充分的理由)。从一小群人中脱颖而出的解决方案是Elasticsearchwww.elastic.co/products/elasticsearch)。如果你对本地解决方案有偏好,可以将我们将要介绍的示例视为一套实践,你应该能够将其应用到其他集中式日志记录解决方案上。

总的来说,我们将探索一个独立的日志服务提供示例(Papertrail),我们将探索云托管供应商提供的解决方案(AWS CloudWatch、GCP Stackdriver 和 Azure Log Analytics),并尝试与一些朋友设置 ElasticSearch。这些示例应该足够让你选择哪种类型的解决方案最适合你的用例。

但是,在我们探索存储日志的工具之前,我们需要弄清楚如何收集它们并将它们发送到最终目的地。

探索日志收集和发送

长期以来,有两个主要竞争者争夺“日志收集和发送”王位。它们分别是Logstashwww.elastic.co/products/logstash)和Fluentdwww.fluentd.org/)。两者都是开源的,都得到了广泛的接受和积极的维护。虽然两者都有各自的优缺点,但 Fluentd 在云原生分布式系统中表现出了优势。它消耗更少的资源,更重要的是,它不受限于单一目的地(Elasticsearch)。虽然 Logstash 可以将日志推送到许多不同的目标,但它主要设计用于与 Elasticsearch 一起工作。因此,其他日志解决方案采用了 Fluentd。

截至目前,无论你采用哪种日志产品,它都很可能支持 Fluentd。这种采用的高潮可以从 Fluentd 进入Cloud Native Computing Foundationwww.cncf.io/)项目列表中看出。甚至 Elasticsearch 用户也在采用 Fluentd 而不是 Logstash。以前通常被称为ELKElasticsearchLogstashKibana)堆栈的东西,现在被称为EFKElasticsearchFluentdKibana)。

我们将跟随潮流,采用 Fluentd 作为收集和传送日志的解决方案,无论目的地是 Papertrail、Elasticsearch 还是其他什么。

我们很快将安装 Fluentd。但是,由于 Papertrail 是我们的第一个目标,我们需要创建和设置一个账户。现在,记住我们需要从集群的所有节点收集日志,正如您已经知道的那样,Kubernetes 的 DaemonSet 将确保在我们的每个服务器上运行一个 Fluentd Pod。

通过 Papertrail 探索集中式日志记录

我们将探索的第一个集中式日志记录解决方案是Papertrail (papertrailapp.com/)。我们将把它作为一种日志即服务解决方案的代表,它可以使我们摆脱安装和更重要的是维护自托管的替代方案。

Papertrail 具有实时跟踪、按时间戳过滤、强大的搜索查询、漂亮的颜色,以及在浏览我们集群内产生的日志时可能(或可能不)必不可少的其他一些功能。

我们需要做的第一件事是注册,或者,如果这不是您第一次尝试 Papertrail,那就登录。

 1  open "https://papertrailapp.com/"

请按照说明注册或登录,如果您已经在他们的系统中有用户。

您会高兴地发现,Papertrail 提供了一个免费计划,允许存储 50MB 的日志,可搜索一天,以及一整年的可下载存档。这应该足够运行我们即将探索的示例。如果您有一个相对较小的集群,那应该可以无限期地继续下去。即使您的集群更大,每月的日志超过 50MB,他们的价格也是合理的。

可以说,他们的价格如此便宜,以至于我们可以说它提供了比在我们自己的集群内运行替代解决方案更好的投资回报。毕竟,没有什么是免费的。即使基于开源的自托管解决方案也会在维护时间和计算能力方面产生成本。

目前,重要的是我们将使用 Papertrail 运行的示例将完全在他们的免费计划范围内。

如果您有一个小型操作,Papertrail 将运作良好。但是,如果您有许多应用程序和一个更大的集群,您可能会想知道 Papertrail 是否能够满足您的需求。不用担心。他们的一个客户是 GitHub,他们可能比您更大。Papertrail 可以处理(几乎)任何负载。它是否对您是一个好的解决方案还有待发现。继续阅读。

让我们去到起始屏幕,除非您已经在那里。

 1  open "https://papertrailapp.com/start"

如果您被重定向到欢迎屏幕,则表示您未经过身份验证(您的会话可能已过期)。登录并重复上一个命令以返回到起始屏幕。

点击“添加系统”按钮。

如果您阅读了说明,您可能会认为设置相对容易。的确如此。然而,Kubernetes 不作为选项之一。如果您将from下拉列表的值更改为something else...,您将看到一个相当大的日志来源列表,可以连接到 Papertrail。但是,没有 Kubernetes 的迹象。列表中最接近的是Docker。即使那个也不行。不用担心。我已经为您准备好了说明,更准确地说,我从 Papertrail 网站的文档中提取了它们。

请注意屏幕顶部的Your logs will go to logsN.papertrailapp.com:NNNNN and appear in Events消息。我们很快就会需要那个地址,所以最好将这些值存储在环境变量中。

 1  PT_HOST=[...]
 2
 3  PT_PORT=[...]

请用主机替换第一个[...]。它应该类似于logsN.papertrailapp.com,其中N是 Papertrail 分配给您的数字。第二个[...]应该用前面提到的消息中的端口替换。

现在我们已经将主机和端口存储在环境变量中,我们可以探索我们将用来收集和发送日志到 Papertrail 的机制。

由于我已经声称大多数供应商都采用了 Fluentd 来收集和发送日志到他们的解决方案,因此 Papertrail 也推荐使用它。SolarWinds(Papertrail 的母公司)的人员创建了一个带有定制 Fluentd 的镜像,我们可以使用。反过来,我创建了一个 YAML 文件,其中包含我们运行其镜像所需的所有资源。

 1  cat logging/fluentd-papertrail.yml

正如您所看到的,YAML 定义了一个带有 ServiceAccount、SolarWind 的 Fluentd 和使用一些环境变量来指定日志应该发送到的主机和端口的 ConfigMap 的 DaemonSet。

在我们应用之前,我们需要更改 YAML 中的logsN.papertrailapp.comNNNNN条目。此外,我更喜欢在logging命名空间中运行所有与日志相关的资源,所以我们也需要更改那个。

 1  cat logging/fluentd-papertrail.yml \
 2      | sed -e \
 3      "s@logsN.papertrailapp.com@$PT_HOST@g" \
 4      | sed -e \
 5      "s@NNNNN@$PT_PORT@g" \
 6      | kubectl apply -f - --record
 7
 8  kubectl -n logging \
 9    rollout status ds fluentd-papertrail

现在我们在集群中运行 Fluentd,并且它配置为将日志转发到我们的 Papertrail 帐户,我们应该转回到它的 UI。

请在浏览器中切换回 Papertrail 控制台。您应该看到一个绿色框,指出已接收到日志。点击事件链接。

图 7-1:Papertrail 的设置日志屏幕

接下来,我们将生成一些日志,并探索它们在 Papertrail 中的显示方式。

 1  cat logging/logger.yml
 2  apiVersion: v1
 3  kind: Pod
 4  metadata:
 5    name: random-logger
 6  spec:
 7    containers:
 8    - name: random-logger
 9      image: chentex/random-logger

该 Pod 使用chentex/random-logger图像,其目的是单一的。它定期输出随机日志条目。

让我们创建random-logger

 1  kubectl create -f logging/logger.yml

请等待一两分钟积累一些日志条目。

 1  kubectl logs random-logger

输出应该类似于接下来的内容。

...
2018-12-06T17:21:15+0000 ERROR something happened in this execution.
2018-12-06T17:21:20+0000 DEBUG first loop completed.
2018-12-06T17:21:24+0000 ERROR something happened in this execution.
2018-12-06T17:21:27+0000 ERROR something happened in this execution.
2018-12-06T17:21:29+0000 WARN variable not in use.
2018-12-06T17:21:31+0000 ERROR something happened in this execution.
2018-12-06T17:21:33+0000 DEBUG first loop completed.
2018-12-06T17:21:35+0000 WARN variable not in use.
2018-12-06T17:21:40+0000 WARN variable not in use.
2018-12-06T17:21:43+0000 INFO takes the value and converts it to string.
2018-12-06T17:21:44+0000 INFO takes the value and converts it to string.
2018-12-06T17:21:47+0000 DEBUG first loop completed.

正如你所看到的,容器正在输出随机条目,其中一些是ERROR,其他的是DEBUGWARNINFO。消息也是随机的。毕竟,这不是一个真正的应用程序,而是一个产生日志条目的简单图像,我们可以用来探索我们的日志解决方案。

请返回到 Papertrail UI。您应该注意到我们系统中的所有日志都可用。一些来自 Kubernetes,而其他来自系统级服务。

go-demo-5的日志也在那里,与我们刚刚安装的random-logger一起。我们将专注于后者。

假设我们通过警报发现了问题,并将范围限定在random-logger应用程序上。警报帮助我们检测到问题,并通过挖掘指标将其缩小到单个应用程序。我们仍然需要查看日志以找出原因。根据我们所知道的(或者虚构的),逻辑上的下一步将是仅检索与random-logger相关的日志条目。

请在屏幕底部的搜索字段中键入random-logger,然后按回车键。

图 7-2:Papertrail 的事件屏幕

从现在开始,我们将只看到包含单词random-logger的日志条目。这并不一定意味着只显示来自该应用程序的日志条目。相反,屏幕上显示了该单词的任何提及。我们所做的是指示 Papertrail 在所有日志条目中执行自由文本搜索,并仅检索包含上述单词的日志条目。

尽管跨所有记录的自由文本搜索可能是最常用的查询方式,但我们还有一些其他过滤日志的方法。我们不会逐一介绍所有这些方法。相反,点击搜索字段右侧的“搜索提示”按钮,自行探索语法。如果这些示例不够用,点击“完整语法指南”链接。

图 7-3:Papertrail 的语法和示例屏幕

可能没有必要更详细地探索 Papertrail。它是直观的,易于使用,并且有很好的文档服务。我相信您如果选择使用它,会弄清楚细节。现在,我们将在进入探索替代方案之前删除 DaemonSet 和 ConfigMap。

 1  kubectl delete \
 2    -f logging/fluentd-papertrail.yml

接下来,我们将探讨云提供商中可用的日志记录解决方案。随时可以直接跳转到GCP StackdriverAWS CloudWatchAzure Log Analytics。如果您不使用这三个提供商中的任何一个,可以完全跳过它们,直接转到通过 Elasticsearch、Fluentd 和 Kibana 探索集中式日志记录子章节。

将 GCP Stackdriver 与 GKE 集群结合使用

如果您正在使用 GKE 集群,日志记录已经设置好了,尽管您可能不知道。默认情况下,每个 GKE 集群都默认配备了一个配置为将日志转发到 GCP Stackdriver 的 Fluentd DaemonSet。它正在kube-system命名空间中运行。

让我们描述一下 GKE 的 Fluentd DaemonSet,并看看我们可能会找到的一些有用信息。

 1  kubectl -n kube-system \
 2    describe ds -l k8s-app=fluentd-gcp

输出,仅限相关部分,如下所示。

...
Pod Template:
  Labels:     k8s-app=fluentd-gcp
              kubernetes.io/cluster-service=true
              version=v3.1.0
...
  Containers:
   fluentd-gcp:
    Image: gcr.io/stackdriver-agents/stackdriver-logging-agent:0.3-1.5.34-1-k8s-1
    ...

我们可以看到,除其他外,DaemonSet 的 Pod 模板具有标签k8s-app=fluentd-gcp。我们很快就会需要它。此外,我们可以看到其中一个容器是基于stackdriver-logging-agent镜像的。就像 Papertrail 扩展了 Fluentd 一样,Google 也做了同样的事情。

现在我们知道了在我们的集群中运行的特定于 Stackdriver 的 Fluentd 是作为 DaemonSet 运行的,逻辑结论将是已经有一个我们可以用来探索日志的 UI。

UI 确实可用,但在我们看到它实际运行之前,我们将输出 Fluentd 容器的日志,并验证一切是否按预期工作。

 1  kubectl -n kube-system \
 2    logs -l k8s-app=fluentd-gcp \
 3    -c fluentd-gcp

除非您已经启用了 Stackdriver Logging API,否则输出应该至少包含一个类似于以下内容的消息。

...
18-12-12 21:36:41 +0000 [warn]: Dropping 1 log message(s) error="7:Stackdriver Logging API has not been used in project 152824630010 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/logging.googleapis.com/overview?project=152824630010 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." error_code="7"

幸运的是,警告已经告诉我们不仅问题是什么,还告诉了我们该怎么做。在您喜欢的浏览器中打开日志条目中的链接,然后单击“启用”按钮。

现在我们已经启用了 Stackdriver Logging API,Fluentd 将能够将日志条目传送到那里。我们所要做的就是等待一两分钟,直到操作传播。

让我们看看 Stackdriver 用户界面。

 1  open "https://console.cloud.google.com/logs/viewer"

请在标签或文本搜索字段中键入random-logger,然后从下拉列表中选择 GKE 容器。

输出应显示包含random-logger文本的所有日志。

图 7-4:GCP Stackdriver 日志屏幕

我们不会详细介绍如何使用 Stackdriver。这很容易,希望也很直观。所以,我会留给你更详细地探索它。重要的是它与我们在 Papertrail 中体验到的非常相似。大部分差异是表面的。

如果您正在使用 GCP,Stackdriver 已经准备好等待您。因此,使用它可能是有道理的,而不是使用任何其他第三方解决方案。Stackdriver 不仅包含来自集群的日志,还包括所有 GCP 服务的日志(例如负载均衡器)。这可能是两种解决方案之间的重大区别。这是 Stackdriver 的巨大优势。但是,在做出决定之前,请检查定价。

将 AWS CloudWatch 与 EKS 集群结合使用

与 GKE 相比,EKS 需要我们设置日志解决方案,而不是在集群中内置日志解决方案。它确实提供了 CloudWatch 服务,但我们需要确保日志从我们的集群中传送到那里。

与以前一样,我们将使用 Fluentd 来收集日志并将其传送到 CloudWatch。更准确地说,我们将使用专门为 CloudWatch 构建的 Fluentd 标签。您可能已经知道,我们还需要一个 IAM 策略,允许 Fluentd 与 CloudWatch 通信。

总的来说,我们即将进行的设置将与我们在 Papertrail 中进行的设置非常相似,只是我们将把日志存储在 CloudWatch 中,并且我们将不得不花一些精力创建 AWS 权限。

在我们继续之前,我假设您仍然拥有eks-logging.shgist.github.com/vfarcic/a783351fc9a3637a291346dd4bc346e7)Gist 中使用的环境变量AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION。如果没有,请创建它们。

我们开始吧。

我们需要创建一个新的 AWS 身份和访问管理IAM)(aws.amazon.com/iam/)政策。为此,我们需要找出 IAM 角色,而这需要 IAM 配置文件。如果你对此感到困惑,知道你并不是唯一一个会这样的人可能会有所帮助。AWS 权限绝非简单。尽管如此,这并不是本章(也不是本书)的主题,所以我会假设至少有基本的 IAM 工作原理的理解。

如果我们逆向工程 IAM 政策的创建路径,我们首先需要的是配置文件。

 1  PROFILE=$(aws iam \
 2    list-instance-profiles \
 3    | jq -r \
 4    ".InstanceProfiles[]\
 5    .InstanceProfileName" \
 6    | grep eksctl-$NAME-nodegroup-0)
 7
 8  echo $PROFILE

输出应该与接下来的内容类似。

eksctl-devops25-nodegroup-0-NodeInstanceProfile-SBTFOBLRAKJF

现在我们知道了配置文件,我们可以使用它来检索角色。

 1  ROLE=$(aws iam get-instance-profile \
 2    --instance-profile-name $PROFILE \
 3    | jq -r ".InstanceProfile.Roles[] \
 4    | .RoleName")
 5
 6  echo $ROLE

有了角色,我们终于可以创建政策了。我已经创建了一个我们可以使用的,让我们快速看一下。

 1  cat logging/eks-logs-policy.json

输出如下。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*",
      "Effect": "Allow"
    }
  ]
}

正如你所看到的,这个政策并没有什么特别之处。它定义了在我们集群内部与logs(CloudWatch)交互所需的权限。

所以,让我们继续并创建它。

 1  aws iam put-role-policy \
 2    --role-name $ROLE \
 3    --policy-name eks-logs \
 4    --policy-document file://logging/eks-logs-policy.json

最后,为了安全起见,我们将检索eks-logs政策并确认它确实被正确创建了。

 1  aws iam get-role-policy \
 2    --role-name $ROLE \
 3    --policy-name eks-logs

输出的PolicyDocument部分应该与我们用来创建政策的 JSON 文件相同。

现在我们已经制定了政策,我们可以把注意力转向 Fluentd。

不幸的是,目前(2018 年 12 月),还没有适用于 CloudWatch 的 Fluentd Helm Chart。因此,我们将退而求其次,使用老式的 YAML。我已经准备好了一个,让我们快速看一下。

 1  cat logging/fluentd-eks.yml

我不会详细介绍 YAML。你应该能够通过自己探索来理解它的作用。关键资源是包含配置的fluentd-cloudwatch ConfigMap 和具有相同名称的 DaemonSet,它将在集群的每个节点上运行 Fluentd Pod。你可能会在理解 Fluentd 配置方面遇到困难,特别是如果这是你第一次使用它。尽管如此,我们不会深入细节,我会让你自己去探索 Fluentd 的文档。相反,我们将apply这个 YAML,希望一切都能如预期般工作。

 1  kubectl apply \
 2    -f logging/fluentd-eks.yml

在我们进入 Cloudwatch UI 之前,我们将检索 Fluentd Pods 并确认集群中每个节点都有一个。

 1  kubectl -n logging get pods

在我的情况下,输出显示了三个与我的 EKS 集群中节点数量相匹配的fluentd-cloudwatch Pods。

NAME                       READY   STATUS    RESTARTS   AGE
fluentd-cloudwatch-7dp5b   1/1     Running   0          19s
fluentd-cloudwatch-zq98z   1/1     Running   0          19s
fluentd-cloudwatch-zrrk7   1/1     Running   0          19s

现在,一切似乎都在我们的集群内正常运行,是时候进入 CloudWatch UI 了。

 1  open "https://$AWS_DEFAULT_REGION.console.aws.amazon.com/
    cloudwatch/home?#logStream:group=/eks/$NAME/containers"

请在 Log Stream Name Prefix 字段中输入random-logger并按下回车键。结果,只会有一个流可用。点击它。

一旦进入random-logger屏幕,你应该能看到该 Pod 生成的所有日志。我会留给你去探索可用的选项(并不多)。

图 7-5:AWS CloudWatch 事件屏幕

一旦你探索完 CloudWatch,我们将继续删除 Fluentd 资源以及策略和日志组。我们还有更多日志解决方案要探索。如果你选择在 Fluentd 中使用 CloudWatch,你应该能够在你的“真实”集群中复制相同的安装步骤。

 1  kubectl delete \
 2    -f logging/fluentd-eks.yml
 3
 4  aws iam delete-role-policy \
 5      --role-name $ROLE \
 6      --policy-name eks-logs
 7
 8  aws logs delete-log-group \
 9    --log-group-name \
10    "/eks/devops25/containers"

将 Azure Log Analytics 与 AKS 集群结合使用

就像 GKE(而不像 EKS)一样,AKS 带有集成的日志解决方案。我们所要做的就是启用其中一个 AKS 插件。更准确地说,我们将启用monitoring插件。正如其名称所示,该插件不仅满足了收集日志的需求,还处理指标。然而,我们只对日志感兴趣。我相信在指标方面没有什么能比得上 Prometheus,特别是因为它与 HorizontalPodAutoscaler 集成。不过,你也应该探索一下 AKS 的指标,并得出自己的结论。目前,我们只会探索插件的日志部分。

 1  az aks enable-addons \
 2    -a monitoring \
 3    -n devops25-cluster \
 4    -g devops25-group

输出是一个相当庞大的 JSON,包含了关于新启用的monitoring插件的所有信息。里面没有什么令人兴奋的东西。

重要的是,我们本可以在创建集群时通过在az aks create命令中添加-a monitoring参数来启用插件。

如果你好奇我们得到了什么,我们可以列出kube-system命名空间中的部署。

 1  kubectl -n kube-system get deployments

输出如下。

NAME                 DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
heapster             1       1       1          1         1m
kube-dns-v20         2       2       2          2         1h
kubernetes-dashboard 1       1       1          1         1h
metrics-server       1       1       1          1         1h
omsagent-rs          1       1       1          1         1m
tiller-deploy        1       1       1          1         59m
tunnelfront          1       1       1          1         1h

新增的是omsagent-rs部署,它将日志(和指标)发送到 Azure Log Analytics。如果你describe它,你会发现它是基于microsoft/oms镜像的。这使得它成为我们从 Fluentd 切换到不同日志发送解决方案的第一次,也是唯一一次。我们将使用它,只是因为 Azure 推荐它。

接下来,我们需要等待几分钟,直到日志传播到 Log Analytics。这是你休息片刻的绝佳时机。去冲杯咖啡吧。

让我们打开 Azure 门户并看看 Log Analytics 的运行情况。

 1  open "https://portal.azure.com"

请从左侧菜单中单击“所有服务”项目,在过滤字段中键入“日志分析”,然后单击“日志分析”项目。

图 7-6:Azure 门户所有服务屏幕,带有日志分析过滤器

除非您已经在使用 Log Analytics,否则应该只有一个活动的工作区。如果是这种情况,请单击它。否则,如果有多个工作区,请选择与az aks enable-addons输出的id条目匹配的工作区。

单击常规部分中的菜单项日志。

接下来,我们将尝试将输出条目限制为仅包含random-logger的条目。请在“在此处键入查询”字段中输入以下查询。

 1  ContainerLog | where Name contains "random-logger"

单击运行按钮,您将看到所有random-logger条目。

默认情况下,表中显示所有字段,其中许多字段要么未被使用,要么没有多大用处。额外的列可能会分散我们吸收日志的注意力,因此我们将更改输出。

指定我们需要哪些列比指定我们不需要哪些列更容易。请展开“列”列表,并单击“选择无”。然后,选择LogEntryNameTimeGenerated字段,完成后,收缩列表。

您面前看到的是限定为random-logger的日志,并且仅通过我们选择的三列呈现。

图 7-7:带有过滤条目的 Azure 日志分析屏幕

我会让您自己探索日志分析功能。尽管 Azure 门户的用户界面可能不够直观,但我相信您能够熟悉它。如果您选择采用 AKS 与 Log Analytics 集成,您可能需要探索Log Analytics 查询语言docs.microsoft.com/en-us/azure/azure-monitor/log-query/query-language)文档,该文档将帮助您编写比我们使用的更复杂的查询。

考虑到在选择最适合您需求的解决方案之前,我们应该探索至少还有一个解决方案,我们将禁用插件。稍后,如果您确实更喜欢 Log Analytics 而不是其他替代方案,您只需再次启用它即可。

 1  az aks disable-addons \
 2    -a monitoring \
 3    -n devops25-cluster \
 4    -g devops25-group

通过 Elasticsearch、Fluentd 和 Kibana 探索集中式日志记录

Elasticsearch 可能是最常用的内存数据库。至少,如果我们将范围缩小到自托管数据库。它设计用于许多其他场景,并且可以用于存储(几乎)任何类型的数据。因此,它几乎完美地用于存储可能以许多不同格式出现的日志。鉴于其灵活性,一些人也将其用于指标,并且因此,Elasticsearch 与 Prometheus 竞争。我们暂时将指标放在一边,只关注日志。

EFKElasticsearchFluentdKibana)堆栈由三个组件组成。数据存储在 Elasticsearch 中,日志由 Fluentd 收集,转换并推送到数据库,并且 Kibana 用作 UI,通过它我们可以探索存储在 Elasticsearch 中的数据。如果您习惯于 ELK(Logstash 而不是 Fluentd),那么接下来的设置应该很熟悉。

我们将安装的第一个组件是 Elasticsearch。没有它,Fluentd 将没有日志的目的地,Kibana 也将没有数据来源。

正如您可能已经猜到的,我们将继续使用 Helm,幸运的是,Elasticsearch Chartgithub.com/helm/charts/tree/master/stable/elasticsearch)已经在稳定通道中可用。我相信您知道如何找到图表并探索您可以使用的所有值。因此,我们将直接跳转到我准备的值。它们是最低限度的,只包含资源

 1  cat logging/es-values.yml

输出如下。

client:
  resources:
    limits:
      cpu: 1
      memory: 1500Mi
    requests:
      cpu: 25m
      memory: 750Mi
master:
  resources:
    limits:
      cpu: 1
      memory: 1500Mi
    requests:
      cpu: 25m
      memory: 750Mi
data:
  resources:
    limits:
      cpu: 1
      memory: 3Gi
    requests:
      cpu: 100m
      memory: 1500Mi

正如您所看到的,有三个部分(clientmasterdata),对应于将要安装的 ElasticSearch 组件。我们所做的就是设置资源请求和限制,然后将其余部分留给图表的默认值。

在我们继续之前,请注意您不应该在生产中使用这些值。您现在应该知道它们在不同情况下有所不同,您应该根据实际使用情况调整资源,您可以从kubectl top,Prometheus 和其他工具中获取。

让我们安装 Elasticsearch。

 1  helm upgrade -i elasticsearch \
 2      stable/elasticsearch \
 3      --version 1.14.1 \
 4      --namespace logging \
 5      --values logging/es-values.yml
 6 
 7  kubectl -n logging \
 8    rollout status \
 9    deployment elasticsearch-client

可能需要一段时间才能创建所有资源。此外,如果您正在使用 GKE,可能需要创建新节点来容纳所请求的资源。请耐心等待。

现在 Elasticsearch 已经推出,我们可以把注意力转向 EFK 堆栈中的第二个组件。我们将安装 Fluentd。就像 Elasticsearch 一样,Fluentd 也可以在 Helm 的稳定通道中找到。

 1  helm upgrade -i fluentd \
 2      stable/fluentd-elasticsearch \
 3      --version 1.4.0 \
 4      --namespace logging \
 5      --values logging/fluentd-values.yml
 6
 7  kubectl -n logging \
 8      rollout status \
 9     ds fluentd-fluentd-elasticsearch

关于 Fluentd 没有太多可说的。它作为 DaemonSet 运行,并且正如图表的名称所暗示的那样,它已经预先配置好以与 Elasticsearch 一起工作。我甚至都没有打扰向你展示logging/fluentd-values.yml值文件的内容,因为它只包含资源。

为了安全起见,我们将检查 Fluentd 的日志,以确认它是否成功连接到 Elasticsearch。

 1  kubectl -n logging logs \
 2      -l app=fluentd-fluentd-elasticsearch

输出,仅限于消息,如下所示。

... Connection opened to Elasticsearch cluster => {:host=>"elasticsearch-client", :port=>9200, :scheme=>"http"}
... Detected ES 6.x: ES 7.x will only accept `_doc` in type_name.

给 Docker for Desktop 用户的一条注释:您可能会看到比上面呈现的少得多的日志条目。由于 Docker for Desktop API 与其他 Kubernetes 版本的差异,会有很多警告。请随意忽略这些警告,因为它们不会影响我们即将探索的示例,并且您不会在生产中使用 Docker for Desktop,而只会用于练习和本地开发。

这很简单也很美丽。唯一剩下的就是安装 EFK 中的 K。

让我们来看看我们将用于 Kibana 图表的值文件。

 1  cat logging/kibana-values.yml

输出如下。

ingress:
  enabled: true
  hosts:
  - acme.com
env:
  ELASTICSEARCH_URL: http://elasticsearch-client:9200
resources:
  limits:
    cpu: 50m
    memory: 300Mi
  requests:
    cpu: 5m
    memory: 150Mi

再次强调,这是一组相对简单的值。这一次,我们不仅指定了资源,还指定了 Ingress 主机,以及环境变量ELASTICSEARCH_URL,告诉 Kibana 在哪里找到 Elasticsearch。正如你可能已经猜到的,我事先不知道你的主机是什么,所以我们需要在运行时覆盖hosts。但在我们这样做之前,我们需要定义它。

 1  KIBANA_ADDR=kibana.$LB_IP.nip.io

我们继续安装 EFK 堆栈中的最后一个组件。

 1  helm upgrade -i kibana \
 2      stable/kibana \
 3      --version 0.20.0 \
 4      --namespace logging \
 5      --set ingress.hosts="{$KIBANA_ADDR}" \
 6     --values logging/kibana-values.yml
 7
 8  kubectl -n logging \
 9      rollout status \
10      deployment kibana

现在我们终于可以打开 Kibana 并确认所有三个 EFK 组件确实一起工作,并且它们正在实现我们的集中日志记录目标。

 1  open "http://$KIBANA_ADDR"

如果您还没有看到 Kibana,请等待片刻并刷新屏幕。

您应该会看到欢迎屏幕。忽略尝试使用他们的示例数据的建议,点击链接自己探索。您将看到允许您添加数据的屏幕。

图 7-8:Kibana 的主屏幕

我们需要做的第一件事是创建一个新的 Elasticsearch 索引,以匹配 Fluentd 创建的索引。我们正在运行的版本已经将数据推送到 Elasticsearch,并且它是通过使用 LogStash 索引模式来简化事情的,因为这是 Kibana 期望看到的。

从左侧菜单中点击管理项目,然后点击索引模式链接。

Fluentd 发送到 Elasticsearch 的所有日志都是以logstash前缀和日期为后缀的索引。由于我们希望 Kibana 检索所有日志,所以在索引模式字段中键入logstash-*,然后单击“> 下一步”按钮。

接下来,我们需要指定包含时间戳的字段。这是一个简单的选择。从时间过滤器字段名称中选择@timestamp,并单击“创建索引模式”按钮。

图 7-9:Kibana 的创建索引模式屏幕

就是这样。现在我们所要做的就是等待一段时间,直到索引创建完成,并探索从整个集群收集的日志。

请从左侧菜单中点击“发现”项。

您在面前看到的是过去十五分钟内生成的所有日志(可以延长到任何时期)。字段列表位于左侧。

顶部有一个愚蠢(且无用)的图表,日志本身位于屏幕主体中。

图 7-10:Kibana 的发现屏幕

就像 Papertrail 一样,我们不会涉及 Kibana 中的所有可用选项。我相信你可以自己弄清楚它们。我们只会介绍一些基本操作,以防这是您第一次接触 Kibana。

我们的情景与以前相同。我们将尝试找到从random-logger应用程序生成的所有日志条目。

请在搜索字段中键入kubernetes.pod_name: "random-logger",然后单击右侧的刷新(或更新)按钮。

往往我们希望自定义默认显示的字段。例如,仅查看日志条目会更有用,而不是完整的源。

点击日志字段旁边的“添加”按钮,它将替换默认的_source列。

如果您想看到带有所有字段的条目,请通过单击行左侧的箭头来展开。

图 7-11:Kibana 的带有过滤条目的发现屏幕

我会让你自己去探索 Kibana 的其余部分。但在你这样做之前,有一个警告。不要被所有花哨的选项所迷惑。如果我们只有日志,可能没有必要创建可视化、仪表板、时间轴和其他看起来不错但无用的东西。这些可能对指标有用,但我们现在没有。目前,它们在 Prometheus 中。以后,我们将讨论将指标推送到 Elasticsearch 而不是从 Prometheus 中拉取的选项。

现在,花点时间看看你在 Kibana 中还能做些什么,至少在Discover屏幕中。

我们已经完成了 EFK 堆栈,并且鉴于我们尚未做出使用哪种解决方案的决定,我们将从系统中清除它。以后,如果你选择了 EFK,你在你的“真实”集群中创建它应该不会有任何麻烦。

 1  helm delete kibana --purge
 2
 3  helm delete fluentd --purge
 4
 5  helm delete elasticsearch --purge
 6
 7  kubectl -n logging \
 8      delete pvc \
 9      -l release=elasticsearch,component=data
10
11  kubectl -n logging \
12      delete pvc \
13      -l release=elasticsearch,component=master

切换到 Elasticsearch 存储指标。

既然我们的集群中已经运行了 Elasticsearch,并且知道它几乎可以处理任何数据类型,一个合乎逻辑的问题可能是我们是否可以用它来存储除日志之外的指标。如果你探索elastic.cowww.elastic.co/),你会发现指标确实是他们宣传的东西。如果它可以取代 Prometheus,那么拥有一个既可以处理日志又可以处理指标的单一工具无疑是有益的。除此之外,我们可以放弃 Grafana,保留 Kibana 作为两种数据类型的单一 UI。

然而,我强烈建议不要使用 Elasticsearch 来存储指标。它是一个通用的自由文本非 SQL 数据库。这意味着它几乎可以处理任何数据,但与此同时,它并不擅长处理任何特定格式。另一方面,Prometheus 被设计用于存储时间序列数据,这是暴露指标的首选方式。因此,它在所做的事情上更受限制。但是,它比 Elasticsearch 更擅长处理指标。我相信使用合适的工具比拥有一个可以做太多事情的单一工具更好,如果你也持相同观点,那么 Prometheus 是处理指标的更好选择。

与 Elasticsearch 相比,仅专注于指标的 Prometheus 需要更少的资源(正如您已经注意到的),它更快,而且具有更好的查询语言。这并不奇怪,因为这两个工具都很棒,但只有 Prometheus 被设计为专门处理指标。额外工具的维护成本增加得很值得,因为它提供了更好(更专注)的解决方案。

我是否提到通过 Prometheus 和 Alertmanager 生成的通知比通过 Elasticsearch 生成的更好?

还有一件重要的事情需要注意。Prometheus 与 Kubernetes 的集成要比 Elasticsearch 提供的要好得多。这并不奇怪,因为 Prometheus 基于与 Kubernetes 相同的云原生原则,并且两者都属于Cloud Native Computing Foundationwww.cncf.io/)。另一方面,Elasticsearch 来自更传统的背景。

Elasticsearch 很棒,但它做得太多了。它缺乏专注性,使其在存储和查询指标以及基于这些数据发送警报方面不如 Prometheus。

如果用 Elasticsearch 替换 Prometheus 不是一个好主意,那我们是否可以反过来提问?我们可以使用 Prometheus 来处理日志吗?答案是绝对不行。正如前面所述,Prometheus 只专注于指标。如果您采用它,您需要另一个工具来存储日志。这可以是 Elasticsearch、Papertrail 或任何其他符合您需求的解决方案。

那么 Kibana 呢?我们可以放弃它,转而使用 Grafana 吗?答案是可以,但不要这样做。虽然我们可以在 Grafana 中创建一个表,并将其附加到 Elasticsearch 作为数据源,但它显示和过滤日志的能力不如 Kibana。另一方面,相对于 Kibana 来说,Grafana 在显示基于指标的图形方面更加灵活。因此,答案与 Elasticsearch 与 Prometheus 的困境类似。保留 Grafana 用于指标,并在选择将日志存储在 Elasticsearch 中时使用 Kibana。

您是否应该将 Elasticsearch 作为 Grafana 中的另一个数据源?如果您采纳了之前的建议,答案很可能是否定的。将日志呈现为图形并没有太大价值。即使在 Kibana 的Explore部分中提供的预定义图表,在我看来也是浪费空间。显示我们总共有多少日志条目,甚至有多少错误条目是没有意义的。我们使用指标来做这些。

日志本身解析起来太昂贵,而且大多数时候它们并不能提供足够的数据作为指标。

我们看到了几种工具的运行情况,但我们还没有讨论我们真正需要从集中式日志记录解决方案中得到什么。我们将在接下来更详细地探讨这个问题。

我们应该从集中式日志记录中期待什么?

我们探索了几种可用于集中日志记录的产品。正如你所看到的,它们都非常相似,我们可以假设大多数其他解决方案都遵循相同的原则。我们需要跨集群收集日志。我们使用 Fluentd 来做到这一点,这是最广泛接受的解决方案,无论哪个数据库接收这些日志,你都很可能会使用它(Azure 是一个例外)。

使用 Fluentd 收集的日志条目被传送到一个数据库,我们的情况下是 Papertrail、Elasticsearch 或者由托管供应商提供的解决方案之一。最后,所有解决方案都提供了一个允许我们探索日志的用户界面。

我通常为问题提供一个解决方案,但在这种情况下,有很多候选项可以满足你对集中式日志记录的需求。你应该选择哪一个?是 Papertrail,Elasticsearch-Fluentd-Kibana 堆栈(EFK),AWS CloudWatch,GCP Stackdriver,Azure Log Analytics,还是其他什么?

在可能和实际的情况下,我更喜欢作为服务提供的集中式日志记录解决方案,而不是在我的集群内部运行它。许多事情在其他人确保一切正常时会更容易。如果我们使用 Helm 来安装 EFK,这可能看起来是一个简单的设置。然而,维护远非微不足道。Elasticsearch 需要大量资源。对于较小的集群,仅运行 Elasticsearch 所需的计算量可能高于 Papertrail 或类似解决方案的价格。如果我可以以与在自己的集群内运行替代方案相同的价格获得由他人管理的服务,大多数情况下服务会获胜。但也有一些例外。

我不想将我的业务锁定在一个服务提供商上。更准确地说,我认为核心组件由我控制,而尽可能多的其他部分交给他人是至关重要的。一个很好的例子是虚拟机。我并不真的在乎谁创建它们,只要价格有竞争力,服务可靠。我可以很容易地将我的虚拟机从本地转移到 AWS,然后再从那里转移到,比如说,Azure。我甚至可以回到本地。虚拟机的创建和维护并没有太多的逻辑。或者,至少,不应该有。

我真正关心的是我的应用程序。只要它们在运行,它们是容错的,它们是高可用的,它们的维护成本不高,它们在哪里运行并不重要。但是,我需要确保系统以一种允许我在不花费数月进行重构的情况下从一个提供商切换到另一个提供商的方式完成。这就是为什么 Kubernetes 如此广泛被采用的一个重要原因。它将其下方的所有内容抽象化,从而使我们能够在(几乎)任何 Kubernetes 集群中运行我们的应用程序。我相信同样的方法也可以应用于日志。我们需要明确我们的期望,任何满足我们要求的解决方案都和其他任何解决方案一样好。那么,我们需要从日志解决方案中得到什么?

我们需要将日志集中在一个位置,以便我们可以从系统的任何部分探索日志。我们需要一种查询语言,可以让我们过滤结果。我们需要解决方案快速。

我们探索的所有解决方案都满足这些要求。Papertrail、EFK、AWS CloudWatch、GCP Stackdriver 和 Azure Log Analytics 都满足这些要求。Kibana 可能更漂亮一些,Elasticsearch 的查询语言可能比其他解决方案提供的更丰富一些。漂亮的重要性取决于您的建立。至于 Elasticsearch 的查询语言更强大……这并不重要。大多数时候,我们只需要对日志进行简单的操作。找到所有包含特定关键字的条目。返回该应用程序的所有日志。将结果限制在最近三十分钟内。

在可能和实际的情况下,由 Papertrail、AWS、GCP 或 Azure 等第三方提供的日志即服务比在我们的集群内部托管更好。

通过服务,我们实现了相同的目标,同时摆脱了我们需要担心的一件事。这种说法背后的推理类似于使我相信托管的 Kubernetes 服务(例如 EKS、AKS、GKE)比我们自己维护的 Kubernetes 更好的逻辑。然而,使用第三方即服务解决方案可能有很多原因。法规可能不允许我们走出内部网络。延迟可能太大。决策者固执。无论原因如何,当我们无法使用即服务时,我们必须自己托管该服务。在这种情况下,EFK 可能是最佳解决方案,不包括超出本书范围的企业提供的解决方案。

如果 EFK 很可能是自托管集中日志的最佳解决方案之一,那么当我们可以使用日志作为服务时,我们应该选择哪一个?Papertrail 是一个好选择吗?

如果我们的集群在云提供商之一内运行,很可能已经有一个很好的解决方案。例如,EKS 有 AWS CloudWatch,GKE 有 GCP Stackdriver,AKS 有 Azure Log Analytics。使用其中之一是完全合理的。它已经存在,很可能已经与您运行的集群集成在一起,您只需要说是。当集群与云提供商之一一起运行时,选择其他解决方案的唯一原因可能是价格。

使用云提供商提供的服务,除非它比其他选择更昂贵。如果您的集群在本地,使用像 Papertrail 这样的第三方服务,除非有规则阻止您将日志发送到内部网络之外。如果其他一切都失败了,使用 EFK。

此时,您可能会想知道为什么我建议使用日志服务,而我提出我们的指标应该托管在我们的集群内。这不是矛盾的吗?按照这种逻辑,我们难道不应该也使用指标作为服务吗?

我们的系统不需要与日志存储进行交互。系统需要发送日志,但不需要检索它们。例如,HorizontalPodAutoscaler 无需连接到 Elasticsearch 并使用日志来决定是否扩展 Pod 的数量。如果系统不需要日志来做决定,我们是否也可以对人类说同样的话?我们需要日志来做什么?我们需要日志来调试。我们需要它们来找出问题的原因。我们不需要基于日志的警报。日志不能帮助我们发现问题,但可以帮助我们找到基于指标警报检测到的问题的原因。

等一下!当包含ERROR这个词的日志条目数量超过一定阈值时,我们不应该创建警报吗?答案是否定的。我们可以(而且应该)通过指标实现相同的目标。我们已经探讨了如何从出口商和仪器中获取错误。

当我们通过基于指标的通知检测到存在问题时,会发生什么?这是我们应该开始探索日志的时刻吗?大多数情况下,寻找问题原因的第一步并不在于探索日志,而是在于查询指标。应用程序是否宕机?它是否有内存泄漏?网络是否有问题?错误响应的数量是否很高?这些以及无数其他问题都可以通过指标得到答案。有时指标会揭示问题的原因,而在其他情况下,它们会帮助我们将问题范围缩小到系统的特定部分。只有在后一种情况下,日志才变得有用。

只有当指标揭示了罪魁祸首,而不是问题的原因时,我们才应该开始探索日志。

如果我们有全面的指标,并且它们确实揭示了我们解决问题所需的大部分(如果不是全部)信息,我们对日志解决方案的需求就不是很大。我们需要将日志集中起来,这样我们就可以在一个地方找到它们,我们需要能够按应用程序或特定副本进行过滤,我们需要能够将范围缩小到特定的时间范围,我们需要能够搜索特定的关键词。

这就是我们需要的全部。恰巧的是,几乎所有的解决方案都提供了这些功能。因此,选择应该基于简单性和所有权成本。

无论你选择什么,都不要陷入对闪亮功能的印象,而你并不打算使用。我更喜欢简单易用且易管理的解决方案。Papertrail 满足所有要求,而且价格便宜。它是本地和云集群的完美选择。CloudWatch(AWS)、Stackdriver(GCP)和 Log Analytics(Azure)也可以这样说。尽管我对 Papertrail 有点偏爱,但这三个基本上做着同样的工作,并且它们已经是服务的一部分。

如果你不允许将数据存储在集群外,或者对其中一个解决方案有其他障碍,EFK 是一个不错的选择。只是要注意它会吃掉你的资源,而且还会抱怨自己饿了。单单 Elasticsearch 就需要几 GB 的 RAM 作为最低配置,而你可能需要更多。当然,如果你已经在其他用途上使用 Elasticsearch,那就不那么重要了。如果是这种情况,EFK 是一个不需要动脑筋的选择。它已经在那里,所以就用它吧。

现在怎么办?

你知道该怎么做。如果你专门为这一章创建了集群,就销毁它。

在你离开之前,你可能想要复习一下本章的要点。

  • 对于除了最小系统之外的任何系统来说,从一个资源到另一个资源,从一个节点到另一个节点去找到问题的原因是不切实际、可靠和快速的。

  • 很多时候,kubectl logs命令并不能提供足够的选项来执行除了最简单的日志检索之外的任何操作。

  • Elasticsearch 很棒,但它做得太多了。它缺乏专注使得它在存储和查询指标以及基于这些数据发送警报方面不如 Prometheus。

  • 日志本身解析起来太昂贵,而且大多数时候它们并不能提供足够的数据来充当指标。

  • 我们需要将日志集中在一个地方,这样我们就可以从系统的任何部分探索日志。

  • 我们需要一种查询语言,可以让我们过滤结果。

  • 我们需要解决方案要快。

  • 使用云提供商提供的服务,除非它比其他选择更昂贵。如果你的集群在本地,使用像 Papertrail 这样的第三方服务,除非有规定阻止你将日志发送到内部网络之外。如果其他方法都失败了,就使用 EFK。

  • 当指标显示出罪魁祸首时,我们应该开始探索日志,而不是问题的原因。

第八章:我们做了什么?

我们探讨了一些超出“正常”Kubernetes 使用范围的主题。我们学会了如何使用 HorizontalPodAutoscaler 来扩展 Pods。我们发现,如果不能扩展集群节点,扩展 Pods 并不能提供足够的好处。我们探讨了如何使用 Cluster Autoscaler 来做到这一点。不幸的是,目前它只适用于 AWS、GKE 和 AKS。

虽然扩展 Pods 和节点是必不可少的,但我们也需要收集指标。它们让我们了解集群和其中运行的应用程序的行为。为此,我们采用了 Prometheus。更重要的是,我们看到了如何利用 Alertmanager 创建通知,当出现问题时会提醒我们,而不是盯着屏幕等待图表达到“红线”。

我们了解到,仅从出口商那里收集指标可能不够,因此我们将我们的应用程序作为提供更低级别指标的一种方式,让我们深入了解我们应用程序的状态。

我们还探讨了 HorizontalPodAutoscaler 使用自定义指标的能力。我们将其连接到了 Prometheus,从而将扩展阈值扩展到几乎我们能想象的任何公式。

鉴于我们在 Prometheus 中收集指标,并且它不提供仪表板功能,我们将其连接到了 Grafana。在这个过程中,我们探讨了如何使仪表板比许多人本能地被吸引的传统“漂亮颜色”更有用。

最后,我们讨论了集中式日志记录的需求,以及一些可能帮助我们通过警报发现问题的工具。为此,我们评估了 Papertrail、AWS CloudWatch、GCP Stackdriver、Azure Log Analytics 和 Elasticsearch-Fluentd-Kibana(EFK)堆栈。

我们超越了常规的 Kubernetes 操作,成功使我们的集群更加健壮和可靠。我们迈出了一步,朝着大部分自适应系统迈进,这些系统只在无法自动解决的特殊情况下需要人工干预。当我们需要干预时,我们将装备所有必要的信息,以快速推断问题的原因并执行纠正措施,将我们的集群恢复到期望的状态。

贡献

像以前的书一样,这本书也是一次合作努力。许多人通过讨论、笔记和错误报告帮助塑造了这本书。我被DevOps20 (slack.devops20toolkit.com/) Slack(通常是私人的)消息和电子邮件淹没在评论中。我与早期版本读者的交谈对最终结果产生了重大影响。我很感激有这样一个伟大的社区支持我。谢谢你们帮助我打造这本伟大的书。

有些人站在人群之上。

Vadim Gusev 帮助校对和讨论书籍结构,从初学者的角度出发。

用他自己的话说...

Vadim 是一位年轻的 IT 专家,他从事网络工程师的职业生涯,但对云和容器的概念如此着迷,以至于决定转变自己的职业道路走向 DevOps。他在一家小型初创公司工作,并带领它走向光明的容器化未来,主要受到 Viktor 的书籍的指导。在业余时间,他喜欢锻炼身体,玩鼓,故意拖延。

Prageeth Warnak 不断提交修正和建议的拉取请求。他使这本书比我依赖于我经常错误的假设读者期望的情况下更加清晰。

用他自己的话说...

Prageeth 是一位经验丰富的 IT 专业人士,目前担任澳大利亚电信巨头 Telstra 的首席软件架构师。他喜欢使用新技术,并喜欢在闲暇时间阅读书籍(尤其是 Viktor 写的书),举重(他可以举起 180 公斤的重物),观看 Netflix 和 Fox 新闻(是的,他是一个原始主义者和保守派)。他与家人一起住在墨尔本。他着迷于正确实施微服务和 DevOps。

posted @ 2024-05-06 18:32  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报