我们为什么会删除不了集群的 Namespace?
作者 | 声东 阿里云售后技术专家
导读:阿里云售后技术团队的同学,每天都在处理各式各样千奇百怪的线上问题。常见的有网络连接失败、服务器宕机、性能不达标及请求响应慢等。但如果要评选的话,什么问题看起来微不足道事实上却让人绞尽脑汁,我相信肯定是“删不掉”的问题,比如文件删不掉、进程结束不掉、驱动卸载不了等。这样的问题就像冰山,隐藏在它们背后的复杂逻辑,往往超过我们的预想。
背景
今天我们讨论的这个问题,跟 K8s 集群的 Namespace 有关。Namespace 是 K8s 集群资源的“收纳”机制。我们可以把相关的资源“收纳”到同一个 Namespace 里,以避免不相关资源之间不必要的影响。
Namespace 本身也是一种资源。通过集群 API Server 入口,我们可以新建 Namespace,而对于不再使用的 Namespace,我们需要清理掉。Namespace 的 Controller 会通过 API Server,监视集群中 Namespace 的变化,然后根据变化来执行预先定义的动作。
有时候,我们会遇到下图中的问题,即 Namespace 的状态被标记成了 "Terminating",但却没有办法被完全删除。
从集群入口开始
因为删除操作是通过集群 API Server 来执行的,所以我们要分析 API Server 的行为。跟大多数集群组件类似,API Server 提供了不同级别的日志输出。为了理解 API Server 的行为,我们将日志级别调整到最高级。然后,通过创建删除 tobedeletedb 这个 Namespace 来重现问题。
但可惜的是,API Server 并没有输出太多和这个问题有关的日志。
相关的日志,可以分为两部分:
- 一部分是 Namespace 被删除的记录,记录显示客户端工具是 kubectl,以及发起操作的源 IP 地址是 192.168.0.41,这符合预期;
- 另外一部分是 Kube Controller Manager 在重复地获取这个 Namespace 的信息。
Kube Controller Manager 实现了集群中大多数的 Controller,它在重复获取 tobedeletedb 的信息,基本上可以判断,是 Namespace 的 Controller 在获取这个 Namespace 的信息。
Controller 在做什么?
和上一节类似,我们通过开启 Kube Controller Manager 最高级别日志,来研究这个组件的行为。在 Kube Controller Manager 的日志里,可以看到 Namespace 的 Controller 在不断地尝试一个失败了的操作,就是清理 tobedeletedb 这个 Namespace 里“收纳”的资源。
怎么样删除“收纳盒”里的资源?
这里我们需要理解一点,就是 Namespace 作为资源的“收纳盒”,其实是逻辑意义上的概念。它并不像现实中的收纳工具,可以把小的物件收纳其中。Namespace 的“收纳”实际上是一种映射关系。
这一点之所以重要,是因为它直接决定了,删除 Namespace 内部资源的方法。如果是物理意义上的“收纳”,那我们只需要删除“收纳盒”,里边的资源就一并被删除了。而对于逻辑意义上的关系,我们则需要罗列所有资源,并删除那些指向需要删除的 Namespace 的资源。
API、Group、Version
怎么样罗列集群中的所有资源呢?这个问题需要从集群 API 的组织方式说起。K8s 集群的 API 不是铁板一块的,它是用分组和版本来组织的。这样做的好处显而易见,就是不同分组的 API 可以独立迭代,互不影响。常见的分组如 apps,它有 v1、v1beta1 和 v1beta2 三个版本。完整的分组/版本列表,可以使用 kubectl api-versions 命令看到。
我们创建的每一个资源,都必然属于某一个 API 分组/版本。以下边 Ingress 为例,我们指定 Ingress 资源的分组/版本为 networking.k8s.io/v1beta1。
kind: Ingress metadata: name: test-ingress spec: rules: - http: paths: - path: /testpath backend: serviceName: test servicePort: 80
用一个简单的示意图来总结 API 分组和版本。
实际上,集群有很多 API 分组/版本,每个 API 分组/版本支持特定的资源类型。我们通过 yaml 编排资源时,需要指定资源类型 kind,以及 API 分组/版本 apiVersion。而要列出资源,我们需要获取 API 分组/版本的列表。
Controller 为什么不能删除 Namespace 里的资源
理解了 API 分组/版本的概念之后,再回头看 Kube Controller Manager 的日志,就会豁然开朗。显然 Namespace 的 Controller 在尝试获取 API 分组/版本列表,当遇到 metrics.k8s.io/v1beta1 的时候,查询失败了。并且查询失败的原因是 "the server is currently unable to handle the request"。
再次回到集群入口
在上一节中,我们发现 Kube Controller Manager 在获取 metrics.k8s.io/v1beta1 这个 API 分组/版本的时候失败了。而这个查询请求,显然是发给 API Server 的。所以我们回到 API Server 日志,分析 metrics.k8s.io/v1beta1 相关的记录。在相同的时间点,我们看到 API Server 也报了同样的错误 "the server is currently unable to handle the request"。
显然这里有一个矛盾,就是 API Server 明显在正常工作,为什么在获取 metrics.k8s.io/v1beta1 这个 API 分组版本的时候,会返回 Server 不可用呢?为了回答这个问题,我们需要理解一下 API Server 的“外挂”机制。
集群 API Server 有扩展自己的机制,开发者可以利用这个机制,来实现 API Server 的“外挂”。这个“外挂”的主要功能,就是实现新的 API 分组/版本。API Server 作为代理,会把相应的 API 调用,转发给自己的“外挂”。
以 Metrics Server 为例,它实现了 metrics.k8s.io/v1beta1 这个 API 分组/版本。所有针对这个分组/版本的调用,都会被转发到 Metrics Server。如下图,Metrics Server 的实现,主要用到一个服务和一个 pod。
而上图中最后的 apiservice,则是把“外挂”和 API Server 联系起来的机制。下图可以看到这个 apiservice 详细定义。它包括 API 分组/版本,以及实现了 Metrics Server 的服务名。有了这些信息,API Server 就能把针对 metrics.k8s.io/v1beta1 的调用,转发给 Metrics Server。
节点与Pod之间的通信