正如在 Kubernetes 组件概述中提到的,API Server 是 Kubernetes 集群的网关。它是 Kubernetes 集群中所有用户、自动化和组件访问的中心接触点。API Server 通过 HTTP 实现 RESTful API,执行所有 API 操作,并负责将 API 对象存储到持久化存储后端。本章介绍了该操作的细节。

可管理性的基本特征

虽然复杂,但从管理的角度来看,Kubernetes API Server 的管理其实是比较简单的。因为 API Server 的所有持久化状态都存储在 API Server 外部的数据库中,所以服务器本身是无状态的,可以通过复制来处理请求负载和进行容错。通常,在一个高可用的集群中,API Server 会有 3 个副本。

API Server 在输出的日志方面可以很聊得来。它对收到的每一个请求都会输出至少一行。正因为如此,在API服务器上添加某种形式的日志滚动是至关重要的,这样它就不会消耗所有可用的磁盘空间。然而,由于 API Server 的日志对于了解 API Server 的运行情况至关重要,我们强烈建议将日志从 API Server 运到日志聚合服务,以便后续的反省和查询,以调试用户或组件对 API 的请求。

API Server 的部件

操作 Kubernetes API Server 涉及三个核心功能。

  • API 管理:服务器暴露和管理 API 的过程
  • 请求处理:处理来自客户端的单个 API 请求的最大功能集
  • 内部控制环路:负责 API 服务器成功运行所需的后台操作的内部结构

以下各节分别介绍这些大类。

API 管理

虽然 API 的主要用途是服务于单个客户的请求,但在处理 API 请求之前,客户必须知道如何提出 API 请求。最终,API Server 是一个 HTTP 服务器 — 因此,每个 API 请求都是一个 HTTP 请求。但必须描述这些 HTTP 请求的特性,以便客户端和服务器知道如何通信。为了探索的目的,有一个实际运行的 API Server 是很好的,这样你就可以探索它。你可以使用现有的 Kubernetes 集群,你可以访问它,或者你可以使用 minikube 工具在本地构建 Kubernetes 集群服务。为了方便使用 curl 工具来探索 API Server,请使用以下命令在代理模式下运行 kubectl 工具,在 localhost:8001 上暴露一个未经认证的 API Server。

  1. $ kubectl proxy

API 路径

对 API Server 的每个请求都遵循 RESTful API 模式,其中请求由请求的 HTTP 路径定义。所有的 Kubernetes 请求都以前缀 /api/(核心API)或 /apis/(按API分组的API)开头。这两组不同的路径主要是历史原因。Kubernetes API 中最初并不存在 API 组,所以最初的或 “核心” 的对象,如 Pod 和 Service,都维护在 /api/ 前缀下,没有 API 组。后来的 API 一般都是在 API 组下添加的,所以它们遵循 /apis/<api-group>/ 路径。例如,Job 对象是批处理 API 组的一部分,因此可以在 /apis/batch/v1/...... 下找到。

对于资源路径来说,还有一个疑问就是资源是否有命名空间。Kubernetes 中的命名空间为对象增加了一层分组,命名空间资源只能在一个命名空间内创建,并且命名空间的名称包含在命名空间资源的 HTTP 路径中。当然,也有一些资源并不生活在命名空间中(最明显的例子是 Namespace API 对象本身),在这种情况下,它们的 HTTP 路径中没有命名空间组件。

下面是命名空间资源类型的两个不同路径的组成部分:

  • /api/v1/namespaces/<namespace-name>/<resource-type-name>/<resource-name>
  • /apis/<api-group>/<api-version>/namespaces/<namespace-name>/<resource-type-name>/<resource-name>

这里是两个不同路径的非命名空间资源类型的组件:

  • /api/v1/<resource-type-name>/<resource-name>
  • /apis/<api-group>/<api-version>/<resource-type-name>/<resource-name>

API 发现

当然,为了能够向 API 提出请求,有必要了解哪些 API 对象对客户端可用。这个过程是通过客户端的 API 发现来实现的。为了看到这个过程的实际情况,并以更亲身的方式探索 API Server,我们可以自己执行这个 API 发现。

首先,为了简化事情,我们使用 kubectl 命令行工具的内置代理来为我们的集群提供认证。运行 kubectl proxy 可以在您的本地机器上创建一个运行在 8001 端口上的简单服务器。

我们可以使用这个服务器来开始 API 发现的过程。我们从检查 /api 前缀开始。

  1. $ curl localhost:8001/api
  2. {
  3. "kind": "APIVersions",
  4. "versions": [
  5. "v1"
  6. ],
  7. "serverAddressByClientCIDRs": [
  8. {
  9. "clientCIDR": "0.0.0.0/0",
  10. "serverAddress": "10.0.0.1:6443"
  11. }
  12. ]
  13. }

你可以看到服务器返回了一个 APIVersions 类型的 API 对象。这个对象为我们提供了一个版本字段,其中列出了可用的版本。

在这种情况下,只有一个,但对于 /apis 前缀,有很多。我们可以用这个版本来继续调查。

$ curl localhost:8001/api/v1

{
  "kind": "APIResourceList",
  "groupVersion": "v1",
  "resources": [
    {
        ….
    {
      "name": "namespaces",
      "singularName": "",
      "namespaced": false,
      "kind": "Namespace",
      "verbs": [
        "create",
        "delete",
        "get",
        "list",
        "patch",
        "update",
        "watch"
      ],
      "shortNames": [
        "ns"
      ]
    },
    …
    {
      "name": "pods",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": [
        "create",
        "delete",
        "deletecollection",
        "get",
        "list",
        "patch",
        "proxy",
        "update",
        "watch"
      ],
      "shortNames": [
        "po"
      ],
      "categories": [
        "all"
      ]
    },
    {
      "name": "pods/attach",
      "singularName": "",
      "namespaced": true,
      "kind": "Pod",
      "verbs": []
    },
    {
      "name": "pods/binding",
      "singularName": "",
      "namespaced": true,
      "kind": "Binding",
      "verbs": [
        "create"
      ]
    },
   ….
  ]
}

现在,我们有了收获。我们可以看到,特定路径上的特定资源会被 API Server 打印出来。在这种情况下,返回的对象包含了 /api/v1/ 路径下暴露的资源列表。

除了资源类型之外,描述 API 的 OpenAPI/Swagger JSON 规范(元 API 对象)还包含各种有趣的信息。我们看一下 Pod 对象的 OpenAPI 规范:

{
  "name": "pods",
  "singularName": "",
  "namespaced": true,
  "kind": "Pod",
  "verbs": [
    "create",
    "delete",
    "deletecollection",
    "get",
    "list",
    "patch",
    "proxy",
    "update",
    "watch"
  ],
  "shortNames": [
    "po"
  ],
  "categories": [
    "all"
  ]
}

从这个对象来看,名称字段提供了这个资源的名称。它还指示了这些资源的子路径。因为推断一个英文单词的复数是很有挑战性的,所以 API 资源还包含一个 singularName 字段,它表示这个资源的单数实例应该使用的名称。我们之前讨论过命名空间。对象描述中的 namespaced 字段表示该对象是否有命名空间。kind 字段提供了存在于 API 对象的 JSON 表示中的字符串,用来表示它是什么类型的对象。动词字段是 API 对象中最重要的一个字段,因为它表示可以对该对象采取什么样的操作。pods 对象包含了所有可能的动词。大多数动词的效果从它们的名字中就可以看出。watch 表示可以为资源建立一个监视。watch 是一个长期运行的操作,它提供关于对象变化的通知。watch 会在后面的章节中详细介绍,proxy 是一个专门的操作,通过 API Server 与网络端口建立代理网络连接。目前只有两个资源(Pod 和 Service)支持代理。

除了可以在对象上进行的操作(描述为动词),还有其他的操作被建模为资源类型的子资源。例如,attach 命令被建模为子资源。

{
  "name": "pods/attach",
  "singularName": "",
  "namespaced": true,
  "kind": "Pod",
  "verbs": []
}

attach 为你提供了将终端连接到 Pod 中正在运行的容器的能力。允许你在 Pod 中执行命令的 exec 功能也是类似的模型。

OpenAPI 规范服务

当然,知道你可以用来访问 API Server 的资源和路径只是你访问 Kubernetes API 所需信息的一部分。除了 HTTP 路径之外,你还需要知道要发送和接收的 JSON 有效载荷。API Server 还提供路径,为您提供有关 Kubernetes 资源的模式的信息。这些模式使用 OpenAPI(以前是 Swagger)语法来表示。你可以在以下路径下拉 OpenAPI 规范。

  • /swaggerapi:在 Kubernetes 1.10 之前,服务于 Swagger 1.2。
  • /openapi/v2:Kubernetes 1.10 及以后,服务于 OpenAPI(Swagger 2.0)。

OpenAPI 规范本身就是一个完整的主题,超出了本书的范围。无论如何,你在 Kubernetes 的日常操作中不太可能需要访问它。然而,各种客户端编程语言库都是使用这些 OpenAPI 规范生成的(值得注意的例外是 Go 客户端库,它目前是手工编码的)。因此,如果你或用户在通过客户端库访问 Kubernetes API 的部分内容时遇到困难,第一站应该是 OpenAPI 规范,了解 API 对象是如何建模的。

API 翻译

在 Kubernetes 中,一个 API 开始时是 Alpha API(例如 v1alpha1)。Alpha 称号表示该 API 不稳定,不适合生产用例。采用 Alpha API 的用户既要预料到 API 表面积可能会在 Kubernetes 的不同版本之间发生变化,也要预料到 API 本身的实现可能不稳定,甚至可能会破坏整个 Kubernetes 集群的稳定。因此,在生产型 Kubernetes 集群中禁用 Alpha API。

一旦一个 API 成熟,它就会成为一个 Beta API(例如 v1beta1)。Beta 称号表示该 API 总体上是稳定的,但可能存在问题或最终的 API 表面完善。一般来说,Beta API 被认为在 Kubernetes 发布之间是稳定的,向后兼容是一个目标。然而,在特殊情况下,测试版 API 可能在 Kubernetes 发布之间仍然不兼容。同样,测试版 API 旨在保持稳定,但仍可能存在问题。Beta API 一般在生产型 Kubernetes 集群中启用,但应谨慎使用。

最后,一个 API 变得普遍可用(例如,v1)。一般可用性(GA)表示该 API 是稳定的。这些 API 既有向后兼容性的保证,也有废弃保证。在一个 API 被标记为计划移除后,Kubernetes 会保留该 API 至少三个版本或一年,以先到者为准。废弃的可能性也相当小。只有在开发出更优秀的替代方案后,API 才会被废弃。同样,GA API 也是稳定的,适合所有生产使用。

Kubernetes 的一个特定版本可以支持多个版本(Alpha、Beta 和 GA)。为了实现这一点,API Server 在任何时候都有三个不同的 API 表示:外部表示,即通过 API 请求进来的表示;内部表示,即 API 服务器内部用于处理对象的内存表示;存储表示,即记录到存储层中以持久化 API 对象。API Server 内部有代码,知道如何在所有这些表示之间进行各种翻译。一个 API 对象可以作为 v1alpha1 版本提交,作为 v1 对象存储,随后作为v1beta1 对象或任何其他任意支持的版本检索。这些转换是通过使用机器生成的深度拷贝库以合理的性能实现的,这些库会执行适当的翻译。

请求管理

Kubernetes 中 API Server 的主要目的是以 HTTP 请求的形式接收和处理 API 调用。这些请求要么来自 Kubernetes 系统中的其他组件,要么是终端用户的请求。无论是哪种情况,它们都会被 Kubernetes API Server 以同样的方式处理。

请求类型

Kubernetes API Server 执行的请求有几大类。

  • GET:最简单的请求是针对特定资源的 GET 请求。这些请求检索与特定资源相关的数据。例如,对路径 /api/v1/namespaces/default/pods/foo 的 HTTP GET 请求可以检索名为 foo 的 Pod 的数据。
  • LIST:稍微复杂一点但仍然相当直接的请求是集合 GET,或 LIST。这些请求是列出一些不同的请求。例如,对路径 /api/v1/namespaces/default/pods 的 HTTP GET 请求可以检索出默认命名空间中所有 Pod 的集合。LIST 请求也可以选择指定一个标签查询,在这种情况下,只返回匹配该标签查询的资源。
  • POST:要创建一个资源,需要使用 POST 请求。请求的主体是应该创建的新资源。在 POST 请求的情况下,路径是资源类型(例如,/api/v1/namespaces/default/pods)。要更新一个现有的资源,需要向特定的资源路径(例如,/api/v1/namespaces/default/pods/foo)提出 PUT 请求。
  • DELETE:当到了删除请求的时候,会使用 HTTP DELETE 请求到资源的路径(例如,/api/v1/namespaces/default/pods/foo)。需要注意的是,这种改变是永久性的 — 在 HTTP 请求发出后,资源就会被删除。

:::info 所有这些请求的内容类型通常是基于文本的 JSON(application/json),但最近发布的 Kubernetes 也支持协议 Protocal Buffer 二进制编码。一般来说,JSON 对于客户端和服务器之间网络上的人类可读性和可调试性更好,但它明显更啰嗦,解析成本更高。Protocal Buffer 较难使用常见工具(如 curl)进行探查,但可以使 API 请求的性能和吞吐量更高。 ::: 除了这些标准请求之外,许多请求还使用 WebSocket 协议来实现客户端和服务器之间的流式会话。这类协议的例子有 execattach 命令。这些请求将在下面的章节中描述。

请求的生命周期

为了更好地理解 API Server 对这些不同请求的处理,我们将拆解并描述对 API Server 的单个请求的处理。

认证

请求处理的第一阶段是认证,它建立了与请求相关的身份。API Server 支持几种不同的建立身份的模式,包括客户端证书、不记名令牌(Bearer Token)和 HTTP 基本认证。一般来说,客户端证书或不记名令牌,应该用于认证;不鼓励使用 HTTP 基本认证。

除了这些建立身份的本地方法外,身份验证是可插拔的,有几个使用远程身份提供者的插件实现。这些插件包括对 OpenID Connect (OIDC) 协议以及 Azure Active Directory 的支持。这些身份验证插件会被编译到 API Server 和客户端库中。这意味着您可能需要确保命令行工具和 API Server 都是大致相同的版本,或者支持相同的身份验证方法。

:::info API Server 还支持基于 Webhook 的远程身份验证配置,在这种情况下,身份验证决定权通过承载令牌转发委托给外部服务器。外部服务器验证来自终端用户的承载令牌,并将认证信息返回给 API Server。 :::

考虑到这一点在保障服务器安全方面的重要性,将在后面的章节中深入介绍。

基于角色的授权

在 API Server 确定了一个请求的身份后,就会继续对其进行授权。对 Kubernetes 的每个请求都遵循传统的 RBAC(基于角色的访问控制)模型。要访问一个请求,该身份必须具有与该请求相关联的相应角色。Kubernetes RBAC 是一个丰富而复杂的话题,因此,我们专门用了一整章的时间来介绍它的操作细节。在本篇 API Server 总结中,当处理一个请求时,API Server 会判断与请求相关联的身份是否可以访问请求中的动词和 HTTP 路径的组合。如果请求的身份具有相应的作用,则允许继续处理。否则,将返回一个 HTTP 403 响应。

这一点在后面的一章中会有更详细的介绍。

准入控制

一个请求经过认证和授权后,就进入到准入控制。认证和 RBAC 决定是否允许请求发生,这是基于请求的 HTTP 属性(头、方法和路径)。准人控制决定请求是否形成良好,并有可能在请求被处理之前对其进行修改。准入控制定义了一个可插拔的接口:apply(request): (transformedRequest, error)

:::info 如果任何准入控制器发现错误,则拒绝该请求。如果请求被接受,则用转换后的请求代替初始请求。准入控制器是串行调用的,每个控制器都接收前一个控制器的输出。 :::

:::tips 由于准入控制是这样一种通用的、可插拔的机制,所以它被用于 API Server 中的各种不同功能。例如,它被用来给对象添加默认值。它还可以用来强制执行策略(例如,要求所有对象都有某个标签)。此外,它还可以用来做一些事情,比如给每个 Pod 注入一个额外的容器。服务网格 Istio 使用这种方法透明地注入其 Sidecar 容器。 :::

准入控制器是相当通用的,可以通过基于 Webhook 的准入控制动态地添加到 API Server 上。

请求验证

请求验证发生在准入控制之后,尽管它也可以作为准入控制的一部分来实现,特别是基于外部 Webhook 的验证。此外,验证仅在单个对象上执行。如果它需要更广泛的集群状态知识,则必须作为准入控制器来实现。

请求验证确保请求中包含的特定资源是有效的。例如,它确保服务对象的名称符合围绕 DNS 名称的规则,因为最终服务的名称将被编入 Kubernetes 服务发现 DNS 服务器。一般来说,验证是以自定义代码的形式实现的,每个资源类型都会被定义。

特殊请求

除了标准的 RESTful 请求外,API Server 还有一些专门的请求模式,为客户提供扩展功能。比如,/proxy, /exec, /attach, /logs 等。

:::info 第一类重要的操作是与 API Server 的开放式、长期运行的连接。这些请求提供的是流式数据,而不是即时响应。 :::

logs 操作是我们描述的第一个流式请求,因为它是最容易理解的。事实上,默认情况下,logs 根本不是一个流请求。客户端通过在特定 Pod 的路径末尾追加 /logs 来请求获取 Pod 的日志(例如,/api/v1/namespaces/default/pods/some-pod/logs),然后指定容器名称作为 HTTP 查询参数和 HTTP GET 请求。给定一个默认请求,API Server 会以纯文本的形式返回截至当前时间的所有日志,然后关闭 HTTP 请求。但是,如果客户端要求对日志进行尾随(通过指定 follow 请求参数),API Server 就会保持 HTTP 响应的打开状态,并且当新的日志通过 API Server 从 kubelet 接收到时,就会写入 HTTP 响应中。这种连接如图 4-1 所示。
image.png
图 4-1 容器日志的 HTTP 请求的基本流程

logs 是最容易理解的流式请求,因为它只是将请求打开并流式传输更多数据。其余的操作都是利用 WebSocket 协议来实现双向流数据。它们实际上还在这些数据流中复用数据,以在 HTTP 上实现任意数量的双向数据流。如果这一切听起来有点复杂,那确实如此,但它也是 API Server 表面面积的宝贵部分。

:::info API Server 实际上支持两种不同的流媒体协议。它支持 SPDY 协议,以及 HTTP2/WebSocket。SPDY 正在被 HTTP2/WebSocket 所取代,因此我们将注意力集中在 WebSocket 协议上。 :::

完整的 WebSocket 协议超出了本书的范围,但它在许多其他地方都有记载。为了理解 API Server,您可以简单地将 WebSocket 视为将 HTTP 转变为双向字节流协议的协议。

:::tips 然而,在这些流之上,Kubernetes API Server 实际上引入了一个额外的多路流协议。原因是,对于很多用例来说,API Server 能够服务多个独立的字节流是相当有用的。例如,考虑在一个容器内执行一个命令。在这种情况下,实际上有三个流需要维护(stdinstderrstdout)。 :::

这个流的基本协议如下:每个流都被分配一个从 0255 的号码。这个流号同时用于输入和输出,它在概念上模拟了一个单一的双向字节流。

对于通过 WebSocket 协议发送的每一帧,第一个字节是流号(如 0),帧的其余部分是在该流上行进的数据(图 4-2)。
image.png
图 4-2 Kubernetes WebSocket 多通道框架的示例

使用该协议和 WebSockets,API Server 可以在一个 WebSocket 会话中同时复用 256 字节的流。

这个基本协议用于 execattach 会话,其通道如下:

  • 0:用于向进程写入的 stdin 流。不从该流读取数据。
  • 1stdout 输出流,用于从进程中读取 stdout。不应将数据写入此流。
  • 2stderr 输出流,用于从进程中读取 stderr。数据不应写入此流。

/proxy 端点用于转发客户端与容器和集群内部运行的服务之间的网络流量,而不将这些端点暴露在外部。为了流转这些 TCP 会话,协议稍微复杂一些。除了对各种流进行复用之外,流的前两个字节(在流号之后,所以实际上是 WebSocket 帧中的第二和第三个字节)是被转发的端口号,因此,/proxy 的单个 WebSocket 帧看起来像图 4-3。
image.png
图 4-3 基于 WebSocket 的端口转发的数据帧示例

监视操作

除了流式数据外,API Server 还支持一个监视(Watch)API。Watch 会监视路径的变化。因此,使用 Watch 可以让用户通过一个单一的连接获得低延迟的更新,而不是以一定的时间间隔来轮询可能的更新,因为轮询会带来额外的负载(由于快速轮询)或额外的延迟(由于缓慢轮询)。当用户通过在某个 API Server 请求中添加查询参数 ?watch=true 来建立与 API Server 的 watch 连接时,API Server 就会切换到 Watch 模式,它让客户端和服务器之间的连接处于开放状态。同样,API Server 返回的数据也不再是单纯的 API 对象 — 而是一个 Watch 对象,它既包含了变化的类型(创建、更新、删除),也包含了 API 对象本身。通过这种方式,客户端可以观察和观察该对象或对象集的所有变化。

乐观的并发更新

API Server 支持的另外一个高级操作是对 Kubernetes API 进行乐观并发更新的能力。乐观并发背后的想法是能够在不使用锁的情况下执行大多数操作(悲观并发),而是在发生并发写入时进行检测,拒绝两个并发写入中较晚的一个。被拒绝的写不会被重试(由客户端自己检测冲突并重试写)。

要理解为什么需要这种乐观的并发和冲突检测,就必须了解读/更新/写竞赛条件的结构。许多 API Server 客户端的操作包括三个操作:

  1. 从 API Server 读取一些数据
  2. 在内存中更新该数据
  3. 将其写回 API Server

现在想象一下,当这两种读/更新/写模式同时发生时会发生什么:

  1. 服务器 A 读取对象 O
  2. 服务器 B 读取对象 O
  3. 服务器 A 更新客户端内存中的对象 O
  4. 服务器 B 更新客户端内存中的对象 O
  5. 服务器 A 写入对象 O
  6. 服务器 B 写入对象 O

最后,服务器 A 所做的改动会丢失,因为它们被服务器 B 的更新覆盖了。

解决这种竞赛有两种方案。第一种是悲观锁,它可以防止服务器A在对对象进行操作时发生其他读操作。这样做的问题是,它将所有的操作序列化,这将导致性能和吞吐量问题。

:::tips Kubernetes API Server 实现的另一个选项是乐观并发,它假设一切都会好起来,只有在尝试冲突写入时才会检测到问题。为了实现这一点,一个对象的每个实例都会同时返回它的数据和资源版本。这个资源版本表示对象的当前迭代。当发生写入时,如果对象的资源版本被设置,只有当当前版本与对象的版本相匹配时,写入才会成功。如果不匹配,就会返回一个 HTTP 错误 409 Conflict,客户端必须重试。 :::

要想知道这样做是如何解决刚才所说的读/更新/写竞赛的,我们再来看看这些操作:

  1. 服务器 A 在版本 v1 时读取对象 O
  2. 服务器 B 在版本 v1 时读取对象 O
  3. 服务器 A 在客户端的内存中更新版本为 v1 的对象 O
  4. 服务器 B 更新客户端内存中版本为 v1 的对象 O
  5. 服务器 A 在版本 v1 处写入对象 O,成功
  6. 服务器 B 在版本 v1 下写入对象 O,但对象在 v2 下,返回 409 冲突

备用编码

除了支持 JSON 编码的请求对象外,API Server 还支持另外两种格式的请求。请求的编码由请求上的 Content-Type HTTP头表示。如果这个头缺失,则假设内容为 application/json,表示为 JSON 编码。第一种备用编码是 YAML,它由 application/yaml Content Type 表示。YAML 是一种基于文本的格式,通常被认为比 JSON 更易于人类阅读。使用 YAML 编码与服务器进行通信的理由不多,但在一些情况下(例如,通过 curl 手动向服务器发送文件)可以很方便。

另一种用于请求和响应的替代编码是 Protocal Buffers 编码格式。协议缓冲区是一种相当高效的二进制对象协议。使用 Protocal Buffers 可以向 API Server 发出更高效、更高吞吐量的请求。事实上,许多 Kubernetes 内部工具都使用 Protocol Buffers 作为其传输方式。Protocal Buffers 的主要问题是,由于它们的二进制性质,它们的线格式明显难以可视化/调试。此外,并非所有的客户端库目前都支持 Protocal Buffers 的请求或响应。Protocal Buffers 的格式由 application/vnd.kubernetes.protobuf Content-Type 头表示。

通用响应码

因为 API Server 是作为 RESTful 服务器实现的,所以服务器的所有响应都与 HTTP 响应代码一致。除了典型的 200 表示成功响应和 500 表示内部服务器错误之外,这里还有一些常见的响应代码及其含义。

  • 202:已接受。已收到创建或删除对象的异步请求。结果用一个状态对象响应,直到异步请求完成,此时将返回实际对象。
  • 400:坏的请求。服务器无法解析或理解该请求。
  • 401:未经授权。收到的请求没有已知的认证方案。
  • 403:被禁止。请求已收到并理解,但禁止访问。
  • 409:冲突。该请求已收到,但它是一个更新对象旧版本的请求。
  • 422:无法处理的实体。该请求解析正确,但未能通过某种验证。

API Server 内部服务

除了操作 HTTP RESTful 服务的基础知识外,API Server 还有一些内部服务来实现 Kubernetes API 的部分功能。一般来说,这些种类的控制循环是在一个单独的二进制中运行的,称为控制器管理器。但也有一些控制循环必须在 API Server 内部运行。在每种情况下,我们都会描述其功能以及在 API Server 中存在的原因。

CRD 控制循环

自定义资源定义(CRD)是动态的 API 对象,可以添加到正在运行的 API Server 中。因为创建 CRD 的行为本身就会创建新的 HTTP 路径,而 API Server 必须知道如何提供服务,所以负责添加这些路径的控制器被安置在 API Server 内部。随着委托的 API Server 的增加(在后面的章节中描述),这个控制器实际上已经大部分从 API Server 中抽象出来了。目前默认情况下,它仍然在进程中运行,但也可以在进程外运行。

CRD 控制循环的操作如下:

for crd in AllCustomResourceDefinitions:
    if !RegisteredPath(crd):
       registerPath

for path in AllRegisteredPaths:
    if !CustomResourceExists(path):
       markPathInvalid(path)
       delete custom resource data
       delete path

:::tips 自定义资源路径的创建是相当简单的,但删除自定义资源就比较复杂了。这是因为删除一个自定义资源意味着删除与该类型资源相关的所有数据。这样,如果一个 CRD 被删除,然后在以后的某个日期被读取,旧的数据就不会以某种方式被复活。因此,在删除 HTTP 服务路径之前,首先要将该路径标记为无效,这样就不能创建新的资源。然后,删除与 CRD 相关的所有数据,最后,删除该路径。 :::

调试 API Server

当然,了解 API Server 的实现是很好的,但更多的时候,你真正需要的是能够调试 API Server(以及调用 API Server 的客户端)的实际情况。实现这一目的的主要方式是通过 API Server 写入的日志。API Server 输出的日志流有两种 — 标准(或基本)日志,以及更有针对性的审计日志,这些日志试图捕获请求的原因和方式,以及改变的 API Server 状态。此外,还可以开启更多的详细日志,用于调试特定问题。

基本日志

默认情况下,API Server 会记录发送到 API Server 的每个请求。这个日志包括客户端的 IP 地址,请求的路径,以及服务器返回的代码。如果一个意外的错误导致服务器恐慌,服务器也会捕捉到这个恐慌(Panic),返回一个500,并记录这个错误。

I0803 19:59:19.929302       1 trace.go:76] Trace[1449222206]:
 "Create /api/v1/namespaces/default/events" (started: 2018-08-03
 19:59:19.001777279 +0000 UTC m=+25.386403121) (total time: 927.484579ms):
Trace[1449222206]: [927.401927ms] [927.279642ms] Object stored in database
I0803 19:59:20.402215       1 controller.go:537] quota admission added
 evaluator for: { namespaces}

在这份日志中,你可以看到它的开头是日志行发出的时间戳 I0803 19:59:...,后面是发出它的行号,trace.go:76,最后是日志消息本身。

审计日志

审计日志是为了让服务器管理员能够取证恢复服务器的状态,以及导致 Kubernetes API 中数据当前状态的一系列客户端交互。例如,它可以让用户回答 “为什么那个 ReplicaSet 被放大到 100?”、“谁删除了那个 Pod?” 等问题。

:::info 审计日志的写法有一个可插拔的后台。一般情况下,审计日志会被写入文件,但也有可能被写入 Webhook。无论哪种情况,记录的数据都是 audit.k8s.io API 组中类型为 event 的结构化 JSON 对象。 :::

审计本身可以通过同一 API 组中的策略对象进行配置。该策略允许您指定将审计事件发送到审计日志中的规则。

激活附加日志

Kubernetes 使用 github.com/golang/glog 分层日志包进行日志记录。使用 API Server 上的 --v 标志,你可以调整日志信息的级别。一般来说,Kubernetes 项目将日志信息级别 2(--v=2)设置为一个合理的默认值,用于记录相关但不太垃圾的消息。如果你正在研究特定的问题,你可以提高日志级别来查看更多的(可能是垃圾的)消息。由于过度的日志记录对性能的影响,我们建议不要在生产中使用冗长的日志级别运行。如果你正在寻找更有针对性的日志记录,--vmodule 标志可以增加单个源文件的日志级别。这对于限制在一组小文件上的目标明确的详细日志记录是很有用的。

调试 kubectl 请求

除了通过日志调试 API Server,还可以通过 kubectl 命令行工具调试与 API Server 的交互。和 API Server 一样,kubectl 命令行工具通过 github.com/golang/glog 包进行日志记录,并支持 --v 信息级别标志。将信息级别设置为 10 级(--v=10)可以开启最大程度的详细的日志记录。在这个模式下,kubectl 会记录它向服务器发出的所有请求,以及试图打印你可以用来复制这些请求的 curl 命令。请注意,这些 curl 命令有时是不完整的。

此外,如果你想直接戳穿 API Server,我们之前用来探索 API 发现的方法也很好用。运行 kubectl proxy 会在 localhost 上创建一个代理服务器,根据本地的 $HOME/.kube/config 文件,自动提供你的认证和授权凭证。当你运行代理服务器后,使用 curl 命令调用各种 API 请求是相当直接的。

总结

作为运维人员,你向用户提供的核心服务是 Kubernetes API。为了有效地提供这项服务,了解构成 Kubernetes 的核心组件,以及你的用户如何将这些 API 组合在一起构建应用程序,对于实现一个有用和可靠的 Kubernetes 集群至关重要。在阅读完本章后,你应该对 Kubernetes API 及其使用方式有一个基本的了解。