第15章 命名空间回收机制失效

阿里云技术服务团队每天都在处理各式各样千奇百怪的线上问题,常见的有网络连接失败、服务器“宕机”、性能不达标、请求响应慢等。

但如果要评选什么问题看起来微不足道事实上却足以让人绞尽脑汁,我相信答案中肯定有“删不掉”一类的问题,比如文件删不掉、进程结束不了、驱动卸载不了等。

这样的问题就像冰山,隐藏在它们背后的复杂逻辑,往往超过我们的预想。本章我们一起来看一个这样的案例。

15.1 问题背景介绍

我们讨论的这个问题,跟 Kubernetes 集群命名空间有关。命名空间是 Kubernetes 集群资源的“收纳”机制,我们可以把相关的资源“收纳”到同一个命名空间里,以避免不相关资源之间不必要的影响。

命名空间本身也是一种资源。通过集群 API Server 入口,我们可以新建命名空间,如图 15-1 所示,而对于不再使用的命名空间,我们需要清理掉。命名空间的 Controller 会通过 API Server 监视集群中命名空间的变化,然后根据变化来执行预先定义的动作。

图 15-1 集群命名空间控制器

有时候我们会遇到下面的问题,即命名空间的状态被标记成了 Terminating,却没有办法被完全删除。

15.2 集群管控入口

因为删除操作是通过集群 API Server 来执行的,所以我们要分析 API Server 的行为。跟大多数集群组件类似,API Server 提供了不同级别的日志输出。为了理解 API Server 的行为,我们将日志级别调整到最高级。然后,通过创建、删除 tobedeletedb 这个命名空间来重现问题。

但可惜的是,API Server 并没有输出多少和这个问题有关的日志。

相关的日志可以分为两部分。一部分是命名空间被删除的记录,记录显示客户端工具是 kubectl,发起操作的源 IP 地址是 192.168.0.41,这符合预期;另外一部分是 Kube Controller Manager 在重复获取这个命名空间的信息。

  • 命名空间被删除。
  • kube-controller-manager 重复获取命名空间。

Kube Controller Manager 实现了集群中大多数的 Controller,它在重复获取 tobedeletedb 的信息,基本上可以判断,是命名空间的 Controller 在获取这个命名空间的信息。

15.3 命名空间控制器的行为

和上一节类似,我们通过开启 Kube Controller Manager 最高级别日志,来研究这个组件的行为。在 Kube Controller Manager 的日志里,可以看到命名空间控制器在不断地尝试一个失败的操作,就是清理 tobedeletedb 这个命名空间里“收纳”的资源。

  • 删除命名空间和其收纳的资源。
  • 因为获取 server APIs 列表失败,所以不能删除命名空间。

15.3.1 删除收纳盒里的资源

这里我们需要理解一点,就是命名空间作为资源的“收纳盒”,其实是逻辑意义上的概念。它并不像现实中的收纳工具,可以把小的物件收纳其中。命名空间的“收纳”实际上是一种映射关系,如图 15-2 所示。

图 15-2 命名空间与资源的关系

这一点之所以重要,是因为它直接决定了删除命名空间内部资源的方法。如果是物理意义上的“收纳”,那我们只需要删除“收纳盒”,里边的资源就一并被删除了。而对于逻辑意义上的关系,我们则需要罗列所有资源,并删除那些指向需要删除的命名空间的资源。

15.3.2 API、Group、Version

怎么样罗列集群中的所有资源呢,这个问题需要从集群 API 的组织方式说起。Kubernetes 集群的 API 不是铁板一块,它是用分组/版本来组织的。这样做的好处显而易见,就是不同分组的 API 可以独立迭代,互不影响。常见的分组如 apps,它有 v1v1beta1v1beta2 这三个版本。完整的分组/版本列表,可以使用 kubectl api-versions 命令看到。

我们创建的每一个资源,都必然属于某一个 API 分组/版本。以下面的 Ingress 为例,我们指定 Ingress 资源的分组/版本为 networking.Kubernetes.io/v1beta1

我们用一个简单的图来总结 API 分组和版本的关系,如图 15-3 所示。

图 15-3 API 分组和版本

实际上,集群有很多 API 分组/版本,每个 API 分组/版本支持特定的资源类型。我们通过 YAML 编排资源时,需要指定资源类型(kind)以及 API 分组/版本(apiVersion)。而要列出资源,我们需要获取 API 分组/版本的列表。

15.3.3 控制器不能删除命名空间里的资源

理解了 API 分组/版本的概念之后,再回头看 Kube Controller Manager 的日志,就会豁然开朗。显然命名空间的 Controller 在尝试获取 API 分组/版本列表,当遇到 metrics.Kubernetes.io/v1beta1 的时候,查询失败了,并且查询失败的原因是 the server is currently unable to handle the request

15.4 回到集群管控入口

在上一节中,我们发现 Kube Controller Manager 在获取 metrics.Kubernetes.io/v1beta1 这个 API 分组/版本的时候失败了。而这个查询请求,显然是发给 API Server 的。所以我们回到 API Server 日志,分析 metrics.Kubernetes.io/v1beta1 相关的记录。在相同的时间点,我们看到 API Server 也报了同样的错误 the server is currently unable to handle the request

  • API Server 报了和命名空间控制器一样的错误。

显然这里有一个矛盾,就是 API Server 明显在正常工作,为什么在获取 metrics.Kubernetes.io/v1beta1 这个 API 分组版本的时候会返回 Server 不可用呢?为了回答这个问题,我们需要理解一下 API Server 的“外挂”机制。

集群 API Server 有扩展自己的机制,开发者可以利用这个机制,来实现 API Server 的“外挂”。这个“外挂”的主要功能,就是实现新的 API 分组/版本。API Server 作为代理,会把相应的 API 调用,转发给自己的“外挂”,如图 15-4 所示。

图 15-4 API Server 的扩展机制

Metrics Server 为例,它实现了 metrics.Kubernetes.io/v1beta1 这个 API 分组/版本。所有针对这个分组/版本的调用,都会被转发到 Metrics Server。如下面命令行输出所示,Metrics Server 的实现,主要用到一个服务和一个 Pod。

# 示例命令行输出

以上命令行输出的 apiservice,则是把“外挂”和 API Server 联系起来的机制。通过下面的 YAML 代码可以看到这个 apiservice 的详细定义。它包括 API 分组/版本,以及实现了 Metrics Server 的服务名。

# 示例 apiservice YAML 定义
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io
spec:
  service:
    name: metrics-server
    namespace: kube-system
  group: metrics.k8s.io
  version: v1beta1
  versionPriority: 100
  groupPriorityMin: 100

有了这些信息,API Server 就能把针对 metrics.Kubernetes.io/v1beta1 的调用转发给 Metrics Server。

15.5 节点与 Pod 的通信

经过简单的测试,我们发现,这个问题实际上是 API Server 和 Metrics Server Pod 之间的通信问题。在阿里云 Kubernetes 集群 环境里,API Server 使用的是主机网络,即 ECS 的网络,而 Metrics Server 使用的是 Pod 网络。这两者之间的通信,依赖于 VPC 路由表 的转发,如图 15-5 所示。

图 15-5 集群网络概览

以图 15-5 为例,如果 API Server 运行在 Node A 上,那它的 IP 地址就是 192.168.0.193。假设 Metrics Server 的 IP 是地址 172.16.8.249,那么从 API Server 到 Metrics Server 的网络连接,必须要通过 VPC 路由表第一条路由规则转发。

检查集群 VPC 路由表,发现指向 Metrics Server 所在节点的路由表项缺失,所以 API Server 和 Metrics Server 之间的通信出了问题。

为了维持集群 VPC 路由表项的正确性,阿里云在 Cloud Controller Manager 内部实现了 Route Controller。这个 Controller 在时刻监听着集群节点状态,以及 VPC 路由表状态。当发现路由表项缺失的时候,它会自动把缺失的路由表项填写回去。

现在的情况和预期不一致,Route Controller 显然没有正常工作,这可以通过查看 Cloud Controller Manager 日志来确认。在日志中我们发现,Route Controller 在使用集群 VPC ID 去查找 VPC 实例的时候,没有办法获取到这个实例的信息。

  • 路由控制器无法获取 VPC 实例。

但是集群还在,ECS 还在,所以 VPC 不可能不在了。这一点我们可以通过 VPC ID 在 VPC 控制台确认。那下面的问题,就是为什么 Cloud Controller Manager 没有办法获取到这个 VPC 的信息。

15.6 集群节点访问云资源

Cloud Controller Manager 获取 VPC 信息,是通过阿里云开放 API 来实现的。这基本上等于从云上一台 ECS 内部获取一个 VPC 实例的信息,而这需要 ECS 有足够的权限。目前的常规做法是,为 ECS 服务器授予 RAM 角色,同时给 RAM 角色绑定相应的角色授权,如图 15-6 所示。

图 15-6 云服务器与角色授权

如果集群组件以其所在节点的身份不能获取云资源的信息,那基本上有两种可能性:一是 ECS 没有绑定正确的 RAM 角色;二是 RAM 角色绑定的 RAM 角色授权没有定义正确的授权规则。检查节点的 RAM 角色,以及 RAM 角色所管理的授权,我们发现,针对 VPC 的授权策略被改掉了,如图 15-7 所示。

图 15-7 授权策略更改

当我们把 Effect 修改成 Allow 之后,没多久,所有的 Terminating 状态的 namespace 全部消失了。

15.7 问题回顾

总体来说,这个问题与 Kubernetes 集群的六个组件有关系,分别是 API Server 及其扩展 Metrics ServerNamespace ControllerRoute Controller,以及 VPC 路由表RAM 角色授权,如图 15-8 所示。

图 15-8 问题相关组件关系

通过分析前三个组件的行为,我们发现是集群网络问题导致了 API Server 无法连接到 Metrics Server;通过排查后三个组件,我们发现导致问题的根本原因是 VPC 路由表被删除且 RAM 角色授权策略被改动。

15.8 总结

Kubernetes 集群命名空间删除不掉的问题,是线上比较常见的一个问题。这个问题看起来无关痛痒,但实际上不仅复杂,而且意味着集群重要功能的缺失。本章全面分析了一个这样的问题,希望其中的排查方法和原理对大家排查类似问题有一定的帮助。


图像说明

本章包含以下图像(原始位置已通过图编号标注):

  • 图15-1:集群命名空间控制器
  • 图15-2:命名空间与资源的关系(逻辑映射)
  • 图15-3:API分组和版本关系图
  • 图15-4:API Server的扩展机制(外挂)
  • 图15-5:集群网络概览(VPC路由表转发)
  • 图15-6:云服务器与角色授权
  • 图15-7:授权策略更改(Effect字段)
  • 图15-8:问题相关组件关系图(六个组件)

排查要点

  1. 命名空间 Terminating 删除失败时,优先检查 API Server 和 Kube Controller Manager 日志中的错误信息。
  2. 关注 metrics.Kubernetes.io/v1beta1 等扩展 API 是否可用,这通常指向 Metrics Server 或 API 聚合层的连通性。
  3. 若涉及云环境,需验证 VPC 路由表、节点 RAM 角色及授权策略的正确性。