View a markdown version of this page

控制平面监控 - Amazon EKS

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

控制平面监控

API 服务器

在查看我们的 API 服务器时,请务必记住它的功能之一是限制入站请求以防止控制平面过载。在 API 服务器级别看似瓶颈,实际上可能是保护它免受更严重问题的影响。我们需要考虑增加通过系统传输的请求量的利弊。为了确定是否应该增加 API 服务器的值,以下是我们需要注意的一些小例子:

  1. 请求在系统中传输的延迟是多少?

  2. 延迟是 API 服务器本身,还是像 etcd 这样的 “下游”?

  3. API 服务器队列深度是造成这种延迟的一个因素吗?

  4. 对于我们想要的 API 调用模式,API 优先级和公平性 (APF) 队列的设置是否正确?

问题出在哪里?

首先,我们可以使用 API 延迟指标来深入了解 API 服务器为请求提供服务所需的时间。让我们使用下面的 PromQL 和 Grafana 热图来显示这些数据。

max(increase(apiserver_request_duration_seconds_bucket{subresource!="status",subresource!="token",subresource!="scale",subresource!="/healthz",subresource!="binding",subresource!="proxy",verb!="WATCH"}[$__rate_interval])) by (le)
注意

有关如何使用本文中使用的 API 仪表板监控 API 服务器的深入文章,请参阅以下博客

API 请求持续时间热图

这些请求都在一秒钟以下,这很好地表明控制平面正在及时处理请求。但是,如果不是这样呢?

我们在上述 API 请求持续时间中使用的格式是热图。热图格式的好处在于,它会告诉我们 API 的默认超时值(60 秒)。但是,我们真正需要知道的是,在达到超时阈值之前,这个值应该在什么阈值下引起关注。要大致了解可接受的阈值是多少,我们可以使用上游 Kubernetes SLO,可以在这里找到 https://github.com/kubernetes/community/blob/master/sig-scalability/slos/slos.md#steady-state-slisslos

注意

注意到这个语句上的 max 函数了吗? 当使用聚合多个服务器(默认情况下,EKS 上的两个 API 服务器)的指标时,不要将这些服务器求平均值。

不对称的交通模式

如果一个 API 服务器 [pod] 负载较轻,而另一个 API 服务器负载过重会怎样? 如果我们将这两个数字求平均值,我们可能会误解正在发生的事情。例如,这里我们有三个 API 服务器,但所有的负载都在其中一个 API 服务器上。通常,在投资规模和性能问题时,任何具有多台服务器(例如etcd和API服务器)的东西都应该被细分出来。

机上请求总数

随着向 API 优先级和公平性过渡,系统上的请求总数只是检查 API 服务器是否超额订阅的一个因素。由于系统现在可以处理一系列队列,因此我们必须查看这些队列中是否有任何队列已满,以及该队列的流量是否丢失。

让我们用以下查询来看看这些队列:

max without(instance)(apiserver_flowcontrol_nominal_limit_seats{})
注意

有关 API A&F 工作原理的更多信息,请参阅以下最佳实践指南

在这里,我们可以看到集群上默认出现的七个不同的优先级组

共享并发

接下来,我们想看看该优先组的使用百分比是多少,这样我们就可以了解某个优先级是否已饱和。将请求限制在低工作负载级别可能是可取的,但是降低领导者选举级别却不是。

API 优先级和公平性 (APF) 系统有许多复杂的选项,其中一些选项可能会产生意想不到的后果。我们在现场看到的一个常见问题是队列深度增加到开始增加不必要的延迟的程度。我们可以使用apiserver_flowcontrol_current_inqueue_request指标来监控这个问题。我们可以使用... 检查是否有掉落apiserver_flowcontrol_rejected_requests_total。如果任何存储桶超过其并发量,则这些指标将为非零值。

正在使用的请求

增加队列深度会使 API 服务器成为重要的延迟来源,应谨慎操作。我们建议谨慎对待创建的队列数量。例如,EKS 系统上的共享数量为 600,如果我们创建的队列过多,这会减少需要吞吐量的重要队列中的份额,例如领导者选举队列或系统队列。创建过多的额外队列会使正确调整这些队列的大小变得更加困难。

为了专注于您可以在 APF 中做出的简单而有影响力的更改,我们只需从未充分利用的存储桶中提取份额,然后增加达到最大使用量的存储桶的大小。通过智能地在这些区间重新分配股票,您可以降低下跌的可能性。

有关更多信息,请访问 EKS 最佳实践指南中的 API 优先级和公平性设置。

API 与 etcd 延迟

我们如何使用 API 服务器来确定 API 服务器是否存在问题,还是 API 服务器的问题,或者两者兼而有之。 metrics/logs upstream/downstream 为了更好地理解这一点,让我们来看看 API Server 和 etcd 是如何关联的,以及对错误的系统进行故障排除有多容易。

在下图中,我们看到了 API 服务器延迟,但我们也看到其中大部分延迟与 etcd 服务器相关,因为图表中的条形显示了 etcd 级别的大部分延迟。如果有 15 秒的 etcd 延迟,同时有 20 秒的 API 服务器延迟,那么大部分延迟实际上在 etcd 级别。

通过查看整个流程,我们发现明智的做法是不要只关注 API 服务器,还要寻找表明 etcd 受到压力(即应用计数器缓慢增加)的信号。仪表板之所以强大,是因为只需看一眼就能快速移至正确的问题区域。

注意

分区中的仪表板可在以下网址找到 https://github.com/RiskyAdventure/Troubleshooting-Dashboards/blob/main/api-troubleshooter.json

ETCD 胁迫

控制平面与客户端的问题

在此图表中,我们正在寻找该期间完成时间最长的 API 调用。在本例中,我们看到自定义资源 (CRD) 正在调用一个 APPLY 函数,这是 05:40 时间范围内最潜在的调用。

最慢的请求

有了这些数据,我们就可以使用 Ad-Hoc PromQL或 CloudWatch Insights查询在该时间段内从审计日志中提取列表请求,以查看这可能是哪个应用程序。

使用以下方法寻找来源 CloudWatch

最好使用指标来找到我们要研究的问题区域,并缩小问题的时间范围和搜索参数。获得这些数据后,我们希望转换为日志,以获取更详细的时间和错误。为此,我们将使用日志见解将CloudWatch 日志转换为指标

例如,为了调查上述问题,我们将使用以下 L CloudWatch ogs Insights 查询来提取 userAgent 和 requestURI,这样我们就可以确定哪个应用程序导致了这种延迟。

注意

需要使用适当的计数,以免在手表上出现正常 List/Resync 行为。

fields @timestamp, @message
| filter @logStream like "kube-apiserver-audit"
| filter ispresent(requestURI)
| filter verb = "list"
| parse requestReceivedTimestamp /\d+-\d+-(?<StartDay>\d+)T(?<StartHour>\d+):(?<StartMinute>\d+):(?<StartSec>\d+).(?<StartMsec>\d+)Z/
| parse stageTimestamp /\d+-\d+-(?<EndDay>\d+)T(?<EndHour>\d+):(?<EndMinute>\d+):(?<EndSec>\d+).(?<EndMsec>\d+)Z/
| fields (StartHour * 3600 + StartMinute * 60 + StartSec + StartMsec / 1000000) as StartTime, (EndHour * 3600 + EndMinute * 60 + EndSec + EndMsec / 1000000) as EndTime, (EndTime - StartTime) as DeltaTime
| stats avg(DeltaTime) as AverageDeltaTime, count(*) as CountTime by requestURI, userAgent
| filter CountTime >=50
| sort AverageDeltaTime desc

使用此查询,我们发现两个不同的代理在运行大量高延迟列表操作。Splunk 和 CloudWatch 代理。有了这些数据,我们就可以决定移除、更新此控制器或将其替换为其他项目。

查询结果
注意

有关此主题的更多详细信息,请参阅以下博客

调度器

由于 EKS 控制平面实例在单独的 AWS 账户中运行,我们将无法抓取这些组件以获取指标(API 服务器例外)。但是,由于我们可以访问这些组件的审计日志,因此我们可以将这些日志转换为指标,以查看是否有任何子系统造成了扩展瓶颈。让我们使用 CloudWatch 日志见解来查看调度器队列中有多少未调度的 pod。

调度器日志中的未调度 pod

如果我们可以直接在自管理的 Kubernetes(例如 Kops)上抓取调度程序指标,我们将使用以下 PromQL 来了解调度程序的待办事项。

max without(instance)(scheduler_pending_pods)

由于我们无法在 EKS 中访问上述指标,因此我们将使用下面的 CloudWatch 日志见解查询来查看待办事项,检查在特定时间段内有多少 Pod 无法调度。然后,我们可以在高峰时段进一步研究消息,以了解瓶颈的性质。例如,节点的旋转速度不够快,或者调度器本身的速率限制器。

fields timestamp, pod, err, @message
| filter @logStream like "scheduler"
| filter @message like "Unable to schedule pod"
| parse @message  /^.(?<date>\d{4})\s+(?<timestamp>\d+:\d+:\d+\.\d+)\s+\S*\s+\S+\]\s\"(.*?)\"\s+pod=(?<pod>\"(.*?)\")\s+err=(?<err>\"(.*?)\")/
| count(*) as count by pod, err
| sort count desc

在这里,我们看到调度器错误地说 pod 没有部署是因为存储 PVC 不可用。

CloudWatch 日志查询
注意

必须在控制平面上打开审核日志记录才能启用此功能。限制日志保留也是一种最佳做法,以免随着时间的推移不必要地增加成本。下面是使用 EKSCTL 工具打开所有日志功能的示例。

cloudWatch: clusterLogging: enableTypes: ["*"] logRetentionInDays: 10

Kube 控制器管理器

与所有其他控制器一样,Kube 控制器管理器对一次可以执行的操作数量有限制。让我们通过查看可以在其中设置这些参数的 KOPS 配置来回顾其中的一些标志。

kubeControllerManager: concurrentEndpointSyncs: 5 concurrentReplicasetSyncs: 5 concurrentNamespaceSyncs: 10 concurrentServiceaccountTokenSyncs: 5 concurrentServiceSyncs: 5 concurrentResourceQuotaSyncs: 5 concurrentGcSyncs: 20 kubeAPIBurst: 30 kubeAPIQPS: "20"

这些控制器的队列在集群流失率高时段会被填满。在这种情况下,我们看到副本集控制器的队列中有大量待办事项。

队列

我们有两种不同的方法可以解决这种情况。如果运行自管理,我们可以简单地增加并发 goroutine,但是,这将通过在 KCM 中处理更多数据来对 etcd 产生影响。另一种选择是减少部署时使用的.spec.revisionHistoryLimit副本集对象的数量,以减少我们可以回滚的副本集对象的数量,从而减轻该控制器的压力。

spec: revisionHistoryLimit: 2

可以调整或关闭其他 Kubernetes 功能,以减轻高流失率系统的压力。例如,如果我们 pod 中的应用程序不需要直接与 k8s API 通信,那么关闭这些 pod 中的预计密钥将减少负载。 ServiceaccountTokenSyncs如果可能的话,这是解决此类问题的更可取的方式。

kind: Pod spec: automountServiceAccountToken: false

在我们无法访问指标的系统中,我们可以再次查看日志以检测竞争情况。如果我们想查看每个控制器上正在处理的请求数量或聚合层面,我们将使用以下 CloudWatch 日志见解查询。

KCM 处理的总容量

# Query to count API qps coming from kube-controller-manager, split by controller type.
# If you're seeing values close to 20/sec for any particular controller, it's most likely seeing client-side API throttling.
fields @timestamp, @logStream, @message
| filter @logStream like /kube-apiserver-audit/
| filter userAgent like /kube-controller-manager/
# Exclude lease-related calls (not counted under kcm qps)
| filter requestURI not like "apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-manager"
# Exclude API discovery calls (not counted under kcm qps)
| filter requestURI not like "?timeout=32s"
# Exclude watch calls (not counted under kcm qps)
| filter verb != "watch"
# If you want to get counts of API calls coming from a specific controller, uncomment the appropriate line below:
# | filter user.username like "system:serviceaccount:kube-system:job-controller"
# | filter user.username like "system:serviceaccount:kube-system:cronjob-controller"
# | filter user.username like "system:serviceaccount:kube-system:deployment-controller"
# | filter user.username like "system:serviceaccount:kube-system:replicaset-controller"
# | filter user.username like "system:serviceaccount:kube-system:horizontal-pod-autoscaler"
# | filter user.username like "system:serviceaccount:kube-system:persistent-volume-binder"
# | filter user.username like "system:serviceaccount:kube-system:endpointslice-controller"
# | filter user.username like "system:serviceaccount:kube-system:endpoint-controller"
# | filter user.username like "system:serviceaccount:kube-system:generic-garbage-controller"
| stats count(*) as count by user.username
| sort count desc

这里的关键要点是,在研究可扩展性问题时,在进入详细的故障排除阶段之前,先查看路径中的每一个步骤(API、调度程序、KCM 等)。通常,在生产环境中,您会发现需要对 Kubernetes 的多个部分进行调整才能让系统以最佳性能运行。很容易无意中对瓶颈变大的症状(例如节点超时)进行故障排除。

ETCD

etcd 使用内存映射文件来高效存储键值对。有一种保护机制可以将此可用内存空间的大小设置为通常设置为 2、4 和 8 GB 的限制。数据库中更少的对象意味着在更新对象和需要清理旧版本时 etcd 需要做的清理工作更少。这种清理对象旧版本的过程称为压缩。经过多次压缩操作后,随后会有一个称为碎片整理的可用空间空间的过程,该过程将在特定阈值以上或按固定的时间表进行。

我们可以采取一些与用户相关的措施来限制 Kubernetes 中的对象数量,从而减少压缩和碎片整理过程的影响。例如,Helm 保持高位revisionHistoryLimit。这样可以使系统 ReplicaSets 上的旧对象(例如系统上的对象)能够进行回滚。通过将历史限制设置为 2,我们可以将对象(比如 ReplicaSets)的数量从十个减少到两个,这反过来又会减少系统的负载。

apiVersion: apps/v1 kind: Deployment spec: revisionHistoryLimit: 2

从监控的角度来看,如果系统延迟峰值以设定的模式出现,则检查此碎片整理过程是否是源代码可能会有所帮助。我们可以使用 CloudWatch 日志来看到这一点。

如果你想查看碎片整理 start/end 次数,请使用以下查询:

fields @timestamp, @message
| filter @logStream like /etcd-manager/
| filter @message like /defraging|defraged/
| sort @timestamp asc
对查询进行碎片整理