11
使用Services暴露pods
本章涵盖
• pods之间的通信
• 将客户端连接分布在一组提供相同服务的pod上
• 通过DNS和环境变量发现集群中的服务
• 向集群外部的客户端暴露服务
• 使用字段选择器根据对象的属性进行筛选
• 使用就绪探测从服务中添加或删除单个pod
如今,人们通常不会只运行一个 pod 来提供某个特定服务,而是运行多个 pod 副本,以便将负载分布到多个集群节点上。但这意味着所有提供相同服务的 pod 副本应该通过一个单一地址可达,这样客户端就可以使用这个地址,而无需跟踪并直接连接到各个 pod 实例。在 Kubernetes 中,你可以通过 Service 对象来实现这一点。你在本书中构建的 Kiada 套件包括三个服务——Kiada 服务、Quiz 服务和 Quote 服务。到目前为止,这三个服务是相互独立的,你可以单独与它们交互,但计划是将它们连接起来,如下图所示。
图 11.1 Kiada 套件的架构和操作。

Kiada服务将调用其他两个服务,并将它们返回的信息集成到它发送给客户机的响应中。多个pod副本将提供每个服务,因此你需要使用service对象来公开它们。
注意 本章的代码文件请参见https://github.com/luksa/kubernetes-in-action-2ndedition/tree/master/Chapter11.
在创建Service对象之前,通过应用Chapter11/SETUP/directory中的清单来部署pod和其他对象,如下所示:
$ kubectl apply -f SETUP/ --recursive
你可能还记得在上一章中,这个命令会应用指定目录及其子目录中的所有清单文件。在应用这些清单文件之后,你的当前 Kubernetes 命名空间中应该会有多个 Pod。
理解 Pod 是如何通信的
在第 5 章中,你学到了什么是 Pod,什么时候将多个容器组合到一个 Pod 以及这些容器如何相互通信。但是,不同 Pod 中的容器如何通信呢?每个 Pod 都有自己的网络接口和 IP 地址。集群中的所有 Pod 都通过一个具有平面地址空间的私有网络连接。如下面的图所示,即使承载 Pod 的节点在地理上分散,中间有许多网络路由器,Pod 仍然可以通过它们自己的平面网络进行通信,而无需 NAT(网络地址转换)。这种 Pod 网络通常是一个软件定义网络,它位于连接节点的实际网络之上。
图11.2 pod通过自己的计算机网络进行通信

当一个 Pod 向另一个 Pod 发送网络数据包时,不会对数据包执行 SNAT(源 NAT)或 DNAT(目标 NAT)。这意味着直接在 Pod 之间交换的数据包的源 IP 和端口,以及目标 IP 和端口,永远不会被更改。如果发送 Pod 知道接收 Pod 的 IP 地址,它就可以向其发送数据包。接收 Pod 可以将发送方的 IP 视为数据包的源 IP 地址。尽管存在许多 Kubernetes 网络插件,但它们都必须按上述方式行为。因此,无论 Pod 是运行在同一个节点上,还是位于不同地理区域的节点上,两个 Pod 之间的通信总是相同的。Pod 中的容器可以通过无 NAT 扁平网络相互通信,就像连接到单个网络交换机的局域网(LAN)上的计算机一样。从应用程序的角度来看,节点之间的实际网络拓扑并不重要。
11.1 通过服务暴露 Pod
如果一个运行在某个 Pod 中的应用程序需要连接到运行在不同 Pod 中的另一个应用程序,它需要知道另一个 Pod 的地址。然而,这比说起来容易,原因如下:
- Pod 是短暂存在的。Pod 可能随时被删除并替换为新的 Pod。这种情况发生在 Pod 被驱逐出节点以腾出空间给其他 Pod、节点发生故障、Pod 不再需要因为较少数量的 Pod 副本可以处理负载,以及许多其他原因。
- Pod 在被分配到节点时会获得其 IP 地址。你事先并不知道 Pod 的 IP 地址,所以无法将其提供给将要连接它的 Pod。
- 在水平扩展中,多个 Pod 副本提供相同的服务。每个副本都有自己的 IP 地址。如果其他 Pod 需要连接到这些副本,它应该能够使用一个指向负载均衡器的单一 IP 或 DNS 名称来连接,该负载均衡器将负载分配到所有副本上。
此外,一些 Pod 需要对集群外部的客户端开放。到目前为止,每当你想连接运行在 Pod 中的应用时,你使用的是端口转发,这仅适用于开发环境。正确的方式是使用 Kubernetes 服务,使一组 Pod 对外可访问。
11.1.1 引入服务
Kubernetes 服务是你创建的一个对象,用于为提供相同服务的一组 Pod 提供单一、稳定的访问点。每个服务都有一个稳定的 IP 地址,只要服务存在,该地址就不会改变。客户端通过暴露的网络端口之一连接到该 IP 地址,然后这些连接会被转发到支持该服务的某一个 Pod。通过这种方式,客户端无需知道提供服务的各个 Pod 的地址,因此这些 Pod 可以随意进行水平扩展或收缩,并在集群节点之间移动。服务在这些 Pod 前充当负载均衡器。
理解为什么需要服务
Kiada 套件是解释服务的一个极佳示例。它包含三组提供三种不同服务的 Pod。Kiada 服务调用 Quote 服务以从书中获取引用,并调用 Quiz 服务以获取测验题目。
我已经在 Kiada 应用程序的 0.5 版本中进行了必要的更改。你可以在本书代码仓库的 Chapter11/ 目录下找到更新后的源代码。本章中你将使用这个新版本。你将学习如何配置 Kiada 应用程序以连接到另外两个服务,并让它对外可见。由于每个服务中的 Pod 数量及其 IP 地址可能会变化,你将通过 Service 对象来暴露它们,如下图所示。
图11.3使用Service对象公开pod

通过为 Kiada pods 创建一个服务并配置它以便从集群外部访问,你可以创建一个单一的、固定的 IP 地址,外部客户端可以通过该地址连接到 pods。每个连接都会转发到其中一个 Kiada pod。通过为 Quote pods 创建服务,你可以创建一个稳定的 IP 地址,使 Kiada pods 无论服务背后有多少个 pod 实例及其位置如何,都能访问到 Quote pods。尽管 Quiz pod 只有一个实例,它也必须通过服务暴露出来,因为每次 pod 被删除并重新创建时,其 IP 地址都会发生变化。如果没有服务,你每次都必须重新配置 Kiada pods,或者让 pods 从 Kubernetes API 获取 Quiz pod 的 IP 地址。如果使用服务,则不必这样做,因为其 IP 地址永远不会改变。
了解 Pod 如何成为服务的一部分
一个服务可以由多个 Pod 支持。当你连接到一个服务时,连接会被传递到其中的一个支持 Pod。但是,你如何定义哪些 Pod 是服务的一部分,哪些不是呢?在前一章中,你学习了标签和标签选择器,以及它们如何用于将一组对象组织成子集。服务使用相同的机制。如下面的图所示,你可以向 Pod 对象添加标签,并在服务对象中指定标签选择器。标签与选择器匹配的 Pod 就是服务的一部分。
图 11.4 标签选择器决定哪些 Pod 是服务的一部分。

在quote服务中定义的标签选择器是 app=quote,这意味着它会选择所有报价 Pod,包括稳定版和金丝雀版本实例,因为它们都包含键为 app、值为 quote 的标签。Pod 上的其他标签无关紧要。
11.1.2 创建和更新服务’
Kubernetes 支持几种类型的服务:ClusterIP、NodePort、LoadBalancer 和 ExternalName。首先你将了解的 ClusterIP 类型仅在集群内部使用。如果你创建一个 Service 对象而没有指定类型,那么得到的就是这种类型的服务。Quiz 和 Quote Pods 的服务属于这种类型,因为它们是由集群内的 Kiada Pods 使用的。另一方面,Kiada Pods 的服务必须能够被外部访问,因此 ClusterIP 类型是不够的。
创建服务的 YAML 清单
下面的清单展示了 quote Service 对象的最小 YAML 清单。列表 11.1 quote 服务的 YAML 清单:

注意 由于quote服务对象是构成报价应用程序的对象之一,你也可以为该对象添加 app: quote 标签。然而,因为此标签对于服务的功能不是必需的,所以在此示例中省略了该标签。
注意 如果你创建一个有多个端口的服务,必须为每个端口指定一个名称。对于只有单个端口的服务,也最好采用同样的做法。
注意 你可以在 targetPort 字段中指定端口名称,而不是端口号,该名称应在 Pod 定义的容器端口列表中定义。这样即使服务背后的 Pod 使用不同的端口号,服务仍然可以使用正确的目标端口号。
清单定义了一个名为 quote 的 ClusterIP 服务。该服务接收 80 端口的连接,并将每个连接转发到符合 app=quote 标签选择器的随机 Pod 的 80 端口,如下图所示。
图 11.5 quote 服务及其转发流量的 Pod

要创建服务,请使用 kubectl apply 将清单文件应用到 Kubernetes API。
使用 kubectl expose 创建服务
通常,你像创建其他对象一样,通过使用 kubectl apply 应用对象清单来创建服务。然而,你也可以像本书第 3 章中所做的那样,使用 kubectl expose 命令来创建服务。按如下方式为 Quiz pod 创建服务:

此命令创建一个名为 quiz 的服务,该服务用于暴露 quiz pod。为此,它会检查 pod 的标签,并创建一个具有与该 pod 所有标签匹配的标签选择器的 Service 对象。
注意 在第 3 章中,你使用了 kubectl expose 命令来暴露一个 Deployment 对象。在这种情况下,命令从 Deployment 获取选择器,并在 Service 对象中使用它来暴露所有的 pod。你将在第 13 章中学习关于 Deployments 的内容。
你现在已经创建了两个服务。你将在第 11.1.3 节中学习如何连接它们,但首先让我们看看它们是否配置正确。
列出服务
当你创建一个服务时,它会被分配一个内部 IP 地址,集群中运行的任何工作负载都可以使用该地址连接到属于该服务的 Pod。这就是服务的集群 IP 地址。你可以通过使用 kubectl get services 命令来查看它。如果你想查看每个服务的标签选择器,可以使用 -o wide 选项,如下所示:

注意 服务的简写是 svc。
命令的输出显示了你创建的两个服务。对于每个服务,都会打印其类型、IP 地址、暴露的端口和标签选择器。
注意 你也可以使用 kubectl describe svc 命令查看每个服务的详细信息。
你会注意到 quiz 服务使用了一个标签选择器,选择具有标签 app: quiz 和 rel: stable 的 Pod。这是因为这些是通过 kubectl expose 命令从 quiz Pod 创建服务时使用的标签。让我们来思考一下。你希望 quiz 服务只包括 stable Pod 吗?可能不希望。也许你稍后决定在稳定版的同时部署 quiz 服务的金丝雀版本。在这种情况下,你希望流量能够被导向两个 Pod。我不喜欢 quiz 服务的另一个原因是端口号。由于服务使用 HTTP,我更希望它使用端口 80,而不是 8080。幸运的是,你可以在创建服务后修改它。
更改服务的标签选择器要更改服务的标签选择器,可以使用 kubectl set selector 命令。要修复 quiz 服务的选择器,请运行以下命令:

再次使用 -o wide 选项列出服务,以确认选择器的更改。如果你正在部署应用程序的多个版本,并希望将客户端从一个版本重定向到另一个版本,这种更改选择器的方法非常有用。
更改服务暴露的端口
要更改服务转发到 Pod 的端口,可以使用 kubectl edit 命令编辑 Service 对象,或者更新清单文件然后应用到集群。在继续之前,运行 kubectl edit svc quiz 并将端口从 8080 改为 80,确保只更改 port 字段,并保持 targetPort 设置为 8080,因为这是 quiz Pod 监听的端口。配置基本服务属性下表列出了可以在 Service 对象中设置的基本字段。
表 11.1 配置服务基本属性的 Service 对象 spec 中的字段
| 字段 | 字段类型 | 描述 |
| type | string | 指定此服务对象的类型。允许的值有 ClusterIP、NodePort、LoadBalancer 和 ExternalName。默认值为 ClusterIP。本章后续部分将解释这些类型之间的差异。 |
| clusterIP | sring | 集群内提供服务的内部 IP 地址。通常情况下,你可以将此字段留空,让 Kubernetes 分配 IP。如果将其设置为 None,则该服务为无主服务。这些内容在第 11.4 节中有说明。 |
| selector | map[string] string | 指定pod必须具有的标签键和值,以便此服务将流量转发给它。如果你不设置此字段,你将负责管理服务端点。第11.3节对此进行了解释。 |
| ports | []Object | 此服务公开的端口列表。每个表项可以指定名称、协议、appProtocol、port、nodePort和targetPort。 |
其余各字段将在本章的其余部分中进行说明。IPv4/IPv6 双栈支持Kubernetes
支持 IPv4 和 IPv6。
你的集群是否支持双栈网络,取决于集群组件是否启用了 IPv6DualStack 功能门控。当你创建一个 Service 对象时,可以通过 ipFamilyPolicy 字段指定希望服务是单栈还是双栈。默认值为 SingleStack,这意味着无论集群配置为单栈还是双栈网络,服务都只会分配一个 IP 类型。如果希望在集群支持双栈时服务能够同时接收两个 IP 类型,而在仅支持单栈时接收一个 IP 类型,请将该值设置为 PreferDualStack。如果你的服务需要同时拥有 IPv4 和 IPv6 地址,请将该值设置为 RequireDualStack。服务仅在双栈集群上创建才会成功。
在创建 Service 对象之后,其 spec.ipFamilies 数组会指示已分配给它的 IP 类型。两个有效值是 IPv4 和 IPv6。你也可以自行设置此字段,以指定在提供双栈网络的集群中要分配给服务的 IP 类型。ipFamilyPolicy 必须相应设置,否则创建将失败。对于双栈服务,spec.clusterIP 字段仅包含一个 IP 地址,而 spec.clusterIPs 字段包含 IPv4 和 IPv6 地址。clusterIPs 字段中 IP 的顺序与 ipFamilies 字段中的顺序对应。
11.1.3 访问集群内部服务
你在上一节创建的 ClusterIP 服务只能在集群内部访问,即从其他 Pod 或集群节点访问。你无法从自己的机器访问它们。要检查服务是否实际可用,你必须要么通过 SSH 登录到其中一个节点并从那里连接服务,要么使用 kubectl exec 命令在现有 Pod 中运行类似 curl 的命令以连接该服务。
注意 你还可以使用 kubectl port-forward svc/my-service 命令连接到支持该服务的某个 Pod。然而,此命令并不连接到服务本身,它只是使用 Service 对象来找到一个 Pod 进行连接。连接会直接建立到 Pod,从而绕过服务。
从 Pod 连接服务要从 Pod
使用该服务,请按照以下步骤在 quote-001 Pod 中运行一个 shell:
![]()
现在检查是否可以访问这两个服务。使用 kubectl get services 显示的服务的集群 IP 地址。在我的例子中,quiz 服务使用的集群 IP 是 10.96.136.190,而 quote 服务使用的 IP 是 10.96.74.151。从 quote-001 pod,我可以按如下方式连接到这两个服务:

注意在 curl 命令中你不需要指定端口,因为你将服务端口设置为 80,这是 HTTP 的默认端口。如果你重复执行上一个命令多次,你会看到服务每次都会将请求转发到不同的 Pod:

该服务充当负载均衡器。它将请求分发到其后面的所有 Pod。在服务上配置会话亲和性你可以配置服务是否应将每个连接转发到不同的 Pod,或者是否应将来自同一客户端的所有连接转发到同一个 Pod。通过 Service 对象中的 spec.sessionAffinity 字段即可进行此操作。服务会话亲和性只支持两种类型:None 和 ClientIP。默认类型是 None,这意味着无法保证每个连接会被转发到哪个 Pod。但是,如果你将该值设置为 ClientIP,则所有来自同一 IP 的连接将被转发到同一个 Pod.在spec.sessionAffinityConfig.clientIP.timeoutSeconds 字段中,你可以指定会话持续的时间。默认值为 3 小时。
你可能会感到惊讶,Kubernetes 并不提供基于 cookie 的会话亲和性。然而,考虑到 Kubernetes 服务在 OSI 网络模型的传输层(UDP 和 TCP)而不是应用层(HTTP)上运行,它们根本不理解 HTTP cookie。
通过 DNS 解析服务
Kubernetes 集群通常运行一个内部 DNS 服务器,集群中的所有 Pod 都配置为使用该服务器。在大多数集群中,这个内部 DNS 服务由 CoreDNS 提供,而一些集群使用 kube-dns。你可以通过列出 kube-system 命名空间中的 Pod 来查看你的集群中部署的是哪一个。无论你的集群中运行的是哪种实现,它都允许 Pod 通过名称解析服务的集群 IP 地址。因此,使用集群 DNS,Pod 可以像这样连接到 quiz 服务:

Pod 可以解析与其在同一命名空间中定义的任何服务,只需在 URL 中指向服务的名称即可。如果 Pod 需要连接到不同命名空间中的服务,则必须在 URL 中附加服务对象的命名空间。例如,要连接到 kiada 命名空间中的 quiz 服务,Pod 可以使用 URL http://quiz.kiada/,无论它处于哪个命名空间。从运行 shell 命令的 quote-001 Pod,也可以按如下方式连接到该服务:

一个服务可以通过以下 DNS 名称进行解析:<service-name>,如果服务与执行 DNS 查找的 Pod 在同一个命名空间中,<service-name>.<service-namespace>,可以从任何命名空间访问,但也可以通过<service-name>.<service-namespace>.svc,以及<service-name>.<service-namespace>.svc.cluster.local。注意默认的域名后缀是 cluster.local,但可以在集群级别进行更改。当通过 DNS 解析服务时,你不需要指定完全限定域名(FQDN)的原因是 Pod 的 /etc/resolv.conf 文件中的搜索行。例如,对于 quote-001 Pod,该文件内容如下:

当你尝试解析一个服务时,搜索字段中指定的域名会被附加到名称后,直到找到匹配项。如果你想知道 nameserver 行中的 IP 地址,可以列出集群中的所有服务来查找:

Pod 的 resolv.conf 文件中的名称服务器指向 kube-system 命名空间中的 kube-dns 服务。这是 Pods 使用的集群 DNS 服务。作为一个练习,尝试找出该服务将流量转发到哪些 Pod。
配置 Pod 的 DNS 策略
Pod 是否使用内部 DNS 服务器可以通过 Pod 的 spec 中的 dnsPolicy 字段进行配置。默认值是 ClusterFirst,这意味着 Pod 先使用内部 DNS,然后使用为集群节点配置的 DNS。其他有效值包括 Default(使用节点配置的 DNS)、None(Kubernetes 不提供 DNS 配置;你必须使用下一段中解释的 dnsConfig 字段配置 Pod 的 DNS 设置)以及 ClusterFirstWithHostNet(用于使用主机网络而非自身网络的特殊 Pod——本书后面会解释)。
设置 dnsPolicy 字段会影响 Kubernetes 如何配置 Pod 的 resolv.conf 文件。你还可以通过 Pod 的 dnsConfig 字段进一步自定义此文件。本书代码仓库中的 pod-with-dns-options.yaml 文件演示了此字段的使用。
通过环境变量发现服务
如今,几乎每个 Kubernetes 集群都提供了集群 DNS 服务。在早期情况并非如此。那时,Pod 是通过环境变量找到服务的 IP 地址的。这些变量现在仍然存在。当容器启动时,Kubernetes 会为 Pod 命名空间内存在的每个服务初始化一组环境变量。让我们通过查看一个运行中 Pod 的环境来了解这些环境变量的样子。
由于你在创建服务之前就创建了你的 Pod,所以除了默认命名空间中存在的 kubernetes 服务相关的环境变量外,你不会看到与其他服务相关的任何环境变量。
注意 kubernetes 服务会将流量转发到 API 服务器。你将在第 16 章中使用它。
要查看你创建的两个服务的环境变量,你必须按如下方式重新启动容器:

当容器重新启动时,其环境变量包含测验和报价服务的条目。用下面的命令显示它们:

相当多的环境变量,你不觉得吗?对于具有多个端口的服务,变量的数量甚至更多。运行在容器中的应用程序可以使用这些变量来查找特定服务的 IP 地址和端口。
注意 在环境变量名称中,服务名中的连字符会被转换为下划线,并且所有字母都会大写。
如今,应用程序通常通过 DNS 获取这些信息,因此这些环境变量不像早期那样有用。它们甚至可能引发问题。如果命名空间中的服务数量过多,你在该命名空间中创建的任何 Pod 都会启动失败。容器以退出代码 1 退出,并且你会在容器日志中看到以下错误信息:
![]()
要防止这种情况,你可以通过将 pod 规格中的 enableServiceLinks 字段设置为 false 来禁用服务信息注入到环境中。
理解为什么你无法 ping 服务 IP
你已经了解了如何验证服务是否将流量转发到你的 pods。但如果没有转发呢?在这种情况下,你可能想尝试 ping 服务的 IP。为什么不现在就试试呢?从 quote-001 pod ping 测试 quiz 服务,方法如下:

等待几秒钟,然后通过按 Control-C 中断该进程。如你所见,IP 地址已被正确解析,但没有任何数据包传输成功。这是因为服务的 IP 地址是虚拟的,只有与服务中定义的某个端口结合使用时才有意义。这将在第18章中解释,该章讲解了服务的内部工作原理。现在,请记住,你不能 ping 服务。
在 Pod 中使用服务
既然你知道 Quiz 和 Quote 服务可以从 Pod 访问,那么你可以部署 Kiada Pod 并配置它们使用这两个服务。该应用程序期望在环境变量 QUIZ_URL 和 QUOTE_URL 中获取这些服务的 URL。这些并不是 Kubernetes 自动添加的环境变量,而是你需要手动设置的变量,以便应用程序知道去哪里找到这两个服务。因此,kiada 容器的 env 字段必须按以下清单进行配置。
清单11.2配置kiada pod中的服务url

环境变量 QUOTE_URL 设置为 http://quote/quote。主机名与你在上一部分创建的服务名称相同。同样,QUIZ_URL 设置为 http://quiz,其中 quiz 是你创建的另一个服务的名称。通过使用 kubectl apply 将清单文件 kiada-stable-andcanary.yaml 部署到集群中,从而部署 Kiada pod。然后运行以下命令以打开到刚创建的某个 pod 的隧道:
![]()
你现在可以在 http://localhost:8080 或 https://localhost:8443 测试该应用程序。如果你使用 curl,你应该会看到如下响应:

图11.6使用web浏览器访问Kiada应用程序时

如果你能看到引用和测验问题,这意味着 kiada-001 Pod 能够与引用和测验服务进行通信。如果你检查支撑这些服务的 Pod 的日志,你会看到它们正在接收请求。对于由多个 Pod 支撑的引用服务,你会看到每个请求都会被发送到不同的 Pod。
11.2 将服务暴露到外部
像上一节中创建的 ClusterIP 服务只能在集群内部访问。由于客户端必须能够从集群外部访问 Kiada 服务,如下图所示,创建 ClusterIP 服务是不够的。图 11.7 将服务暴露到外部

如果你需要将服务对外部世界开放,可以执行以下操作之一:
- 为节点分配一个额外的 IP,并将其设置为服务的 externalIPs 之一;
- 将服务的类型设置为 NodePort,并通过节点的端口访问服务;
- 通过将类型设置为 LoadBalancer,请求 Kubernetes 提供负载均衡器;或-
- 通过 Ingress 对象暴露服务。
一种很少使用的方法是,在 Service 对象的 spec.externalIPs 字段中指定一个额外的 IP。通过这样做,你是在告诉 Kubernetes 将针对该 IP 地址的任何流量视为需要由该服务处理的流量。当你确保这些流量到达以服务的外部 IP 作为目的地的节点时,Kubernetes 会将其转发到支持该服务的某个 Pod。
一种更常见的方法是将服务的类型设置为 NodePort,从而使其在外部可用。Kubernetes 会在所有集群节点上为该服务提供一个网络端口(即所谓的节点端口,这也是这种服务类型名称的由来)。与 ClusterIP 服务类似,该服务会获得一个集群内部 IP,但也可以通过每个集群节点上的节点端口进行访问。通常,你会随后配置一个外部负载均衡器,将流量重定向到这些节点端口。客户端可以通过负载均衡器的 IP 地址连接到你的服务。
与其使用 NodePort 服务并手动设置负载均衡器,不如直接将服务类型设置为 LoadBalancer,让 Kubernetes 为你处理。然而,并非所有集群都支持这种服务类型,因为负载均衡器的配置取决于集群运行的基础设施。大多数云提供商在其集群中支持 LoadBalancer 服务,而在本地部署的集群则需要像 MetalLB 这样的附加组件,这是一个针对裸机 Kubernetes 集群的负载均衡实现。最后一种将一组 Pod 对外暴露的方法则完全不同。你可以使用 Ingress 对象,而不是通过节点端口和负载均衡器对服务进行外部暴露。这种对象如何暴露服务取决于底层的 ingress 控制器,但它允许你通过一个可外部访问的 IP 地址暴露多个服务。在下一章中,你将学习更多相关内容。
11.2.1 通过 NodePort 服务暴露 Pod
让外部客户端访问 Pod 的一种方式是通过 NodePort 服务暴露它们。当你创建这样的服务时,匹配其选择器的 Pod 可以通过集群中所有节点上的特定端口访问,如下图所示。由于此端口在节点上是开放的,因此称为节点端口。
图 11.8 通过 NodePort 服务暴露 Pod

像 ClusterIP 服务一样,NodePort 服务可以通过其内部集群 IP 访问,同时也可以通过每个集群节点的节点端口访问。在图中所示的示例中,Pod 可以通过端口 30080 访问。如你所见,该端口在两个集群节点上都是开放的。无论客户端连接到哪个节点,都没有关系,因为所有节点都会将连接转发到属于该服务的 Pod,无论该 Pod 运行在哪个节点上。当客户端连接到节点 A 时,节点 A 或节点 B 上的 Pod 都可以接收该连接。当客户端连接到节点 B 的端口时,情况也是一样的。
创建 NodePort 服务
要通过 NodePort 服务暴露 kiada pods,你可以根据下列清单创建该服务。清单 11.3 使用 NodePort 服务在两个端口上暴露 kiada pods

与你之前创建的 ClusterIP 服务相比,列表中的服务类型是 NodePort。与之前的服务不同,该服务公开了两个端口,并为每个端口定义了 nodePort 端口号。
注意 你可以省略 nodePort 字段,由 Kubernetes 分配端口号。这可以避免不同 NodePort 服务之间的端口冲突。
该服务指定了六个不同的端口号,这可能会让人难以理解,但下图应该能帮助你理解。
图 11.9 通过 NodePort 服务公开多个端口

检查NodePort服务
创建服务后,使用kubectl get命令检查它,如下所示:

比较你到目前为止创建的服务的 TYPE 和 PORT(S) 列。与两个 ClusterIP 服务不同,kiada 服务是一个 NodePort 服务,除了在服务的集群 IP 上可用的端口 80 和 443 外,还暴露了节点端口 30080 和 30443。
访问 NodePort 服务
要找出服务可用的所有 IP:端口 组合,你不仅需要节点端口号,还需要节点的 IP。你可以通过运行 kubectl get nodes -o wide 并查看 INTERNAL-IP 和 EXTERNAL-IP 列来获取这些信息。在云中运行的集群通常为节点设置外部 IP,而在裸机上运行的集群可能只设置节点的内部 IP。如果没有防火墙阻挡,你应该能够使用这些 IP 访问节点端口。
注意 在使用 GKE 时,如果要允许流量访问节点端口,请运行 `gcloud compute firewall-rules create gke-allow-nodeports --allow=tcp:30000-32767`。
如果你的集群运行在不同的云提供商上,请查阅该提供商的文档,了解如何配置防火墙以允许访问节点端口。在我使用 kind 工具创建的集群中,节点的内部 IP 如下:

Kiada 服务在所有这些 IP 上都是可用的,即使是运行 Kubernetes 控制平面的节点的 IP。我可以通过以下任意 URL 访问该服务:
- 集群内部: 10.96.226.212:80(这是集群 IP 和内部端口),
- 从可以访问 kind-control-plane 节点的任何地方: 172.18.0.3:30080,这是该节点的 IP 地址;端口是 Kiada 服务的某个节点端口,
- 第二个节点的 IP 和节点端口: 172.18.0.4:30080,
- 第三个节点的 IP 和节点端口: 172.18.0.2:30080。
该服务也可以通过集群内的 HTTPS 443 端口以及节点端口 30443 访问。如果我的节点还有外部 IP,该服务也可以通过这些 IP 的两个节点端口访问。如果你使用的是 Minikube 或其他单节点集群,则应使用该节点的 IP。
提示 如果你正在使用 Minikube,你可以通过运行 minikube service <service-name> [-n <namespace>] 来轻松通过浏览器访问你的 NodePort 服务。
使用 curl 或你的网页浏览器访问该服务。选择一个节点并找到其 IP 地址。向该 IP 的 30080 端口发送 HTTP 请求。检查响应的末尾以了解哪个 pod 处理了请求以及该 pod 运行在哪个节点上。例如,下面是我收到的其中一个请求的响应:

注意,我将请求发送到 172.18.0.4,这是 kindworker 节点的 IP,但处理请求的 Pod 实际上运行在节点 kind-worker2 上。正如在 NodePort 服务的介绍中所解释的,第一个节点将连接转发到了第二个节点。你是否也注意到 Pod 认为请求来自哪里?看看响应末尾的客户端 IP。这不是我发送请求的计算机的 IP。你可能已经注意到,它是我发送请求的节点的 IP。我将在第 11.2.3 节中解释这是为什么,以及如何防止这种情况。
尝试将请求发送到其他节点。你会发现它们都会将请求转发到一个随机的 kiada pod。如果你的节点可以从互联网访问,那么该应用现在可以被全球用户访问。你可以使用轮询 DNS 来分配到各节点的传入连接,或者在节点前面放置一个合适的四层负载均衡器,并让客户端指向它。或者你也可以让 Kubernetes 来处理,如下一节所示。
11.2.2 通过外部负载均衡器暴露服务
在上一节中,你创建了一种类型为 NodePort 的服务。另一种服务类型是 LoadBalancer。顾名思义,这种服务类型使你的应用程序可以通过负载均衡器访问。虽然所有服务都可以充当负载均衡器,但创建 LoadBalancer 服务会实际配置一个负载均衡器。
如以下图所示,该负载均衡器位于节点前方,处理来自客户端的连接。它通过将每个连接转发到其中一个节点的节点端口,将连接路由到服务。这是可行的,因为 LoadBalancer 服务类型是 NodePort 类型的扩展,使服务可以通过这些节点端口访问。通过将客户端指向负载均衡器,而不是直接指向某个特定节点的节点端口,客户端就不会尝试连接不可用的节点,因为负载均衡器只将流量转发到健康的节点。此外,负载均衡器还确保连接均匀分布在集群的所有节点上。
图11.10公开LoadBalancer服务

并非所有 Kubernetes 集群都支持这种类型的服务,但如果你的集群运行在云端,几乎可以肯定支持。如果你的集群运行在本地,只要安装了附加组件,也能支持 LoadBalancer 服务。如果集群不支持这种类型的服务,你仍然可以创建此类服务,但该服务只能通过其节点端口访问。
创建 LoadBalancer 服务
下列清单中的清单包含了一个 LoadBalancer 服务的定义。
清单 11.4 一个 LoadBalancer 类型的服务

这个清单与你之前部署的 NodePort 服务的清单只有一行的不同——指定服务类型的那一行。选择器和端口与之前相同。节点端口只是为了避免被 Kubernetes 随机选择。如果你不在意节点端口号,可以省略 nodePort 字段。使用 kubectl apply 应用清单。你无需先删除现有的 kiada 服务。这可以确保服务的集群内部 IP 保持不变。
通过负载均衡器连接服务
创建服务后,云基础设施可能需要几分钟时间来创建负载均衡器并在 Service 对象中更新其 IP 地址。这个 IP 地址随后将作为你服务的外部 IP 地址显示。

在我的情况下,负载均衡器的 IP 地址是 172.18.255.200,我可以通过该 IP 的 80 和 443 端口访问服务。在负载均衡器创建之前,EXTERNAL-IP 列中会显示 <pending> 而不是 IP 地址。这可能是因为配置过程尚未完成,或者集群不支持 LoadBalancer 服务。
为 MetalLB 添加 LoadBalancer 服务支持
如果你的集群运行在裸机环境中,你可以安装 MetalLB 来支持 LoadBalancer 服务。你可以在 metallb.universe.tf 找到相关信息。如果你使用 kind 工具创建了集群,可以使用本书代码库中的 install-metallb-kind.sh 脚本来安装 MetalLB。如果你使用其他工具创建集群,可以查阅 MetalLB 文档以了解安装方法。
添加对 LoadBalancer 服务的支持是可选的。你始终可以直接使用节点端口。负载均衡器只是一个额外的层。调整 LoadBalancer 服务LoadBalancer 服务很容易创建。你只需将类型设置为 LoadBalancer。然而,如果你需要对负载均衡器有更多控制,你可以使用 Service 对象 spec 中的附加字段进行配置,这些字段在下表中进行了说明。
表 11.2 可用于配置 LoadBalancer 服务的 service spec 字段
| 字段 | 字段类型 | 描述 |
| loadBalancerClass | string | 如果集群支持多种类别的负载均衡器,你可以为此服务指定使用哪一种。可用值取决于集群中安装的负载均衡器控制器。 |
| loadBanlanceIP | string | 如果云提供商支持,此字段可用于指定负载均衡器的目标 IP。 |
| loadBalance- SourceRanges | []string | 限制允许通过负载均衡器访问服务的客户端 IP。并非所有负载均衡器控制器都支持此功能。 |
| Allocate- LoadBanlancerNodePorts | Boolean | 指定是否为此负载均衡器类型的服务分配节点端口。一些负载均衡器实现可以在不依赖节点端口的情况下将流量转发到 Pod。 |
11.2.3 配置服务的外部流量策略
你已经了解到,当外部客户端通过节点端口直接或通过负载均衡器连接到服务时,连接可能会被转发到位于与接收连接的节点不同的节点上的 Pod。在这种情况下,必须进行额外的网络跳转才能到达 Pod,这会导致延迟增加。同时,如前所述,当以这种方式将连接从一个节点转发到另一个节点时,源 IP 必须被替换为最初接收连接的节点的 IP。这会掩盖客户端的 IP 地址。因此,Pod 中运行的应用程序无法看到连接的来源。例如,运行在 Pod 中的 Web 服务器无法在其访问日志中记录真实的客户端 IP。
节点需要更改源 IP 的原因是确保返回的数据包发送回最初接收连接的节点,以便该节点可以将其返回给客户端。
本地外部流量策略的优缺点
通过防止节点将流量转发到未在同一节点上运行的 Pod,可以解决额外网络跳数问题和源 IP 混淆问题。这可以通过将 Service 对象的 spec 字段中的 externalTrafficPolicy 字段设置为 Local 来实现。这样,节点只将外部流量转发到在接收连接的节点上运行的 Pod。
然而,将外部流量策略设置为本地(Local)会引发其他问题。首先,如果接收连接的节点上没有本地 Pod,连接将挂起。因此,必须确保负载均衡器仅将连接转发到至少有一个此类 Pod 的节点。这可以通过使用 healthCheckNodePort 字段来实现。外部负载均衡器使用此节点端口检查节点是否包含该服务的端点。这样可以使负载均衡器仅将流量转发到拥有此类 Pod 的节点。当外部流量策略设置为本地(Local)时,你会遇到的第二个问题是 Pod 之间的流量分布不均。如果负载均衡器在各节点之间均匀分配流量,但每个节点运行的 Pod 数量不同,那么运行 Pod 较少的节点上的 Pod 将接收到更多的流量。
比较 Cluster 和 Local 外部流量策略考虑下图所示的情况。节点 A 上运行一个 Pod,节点 B 上运行两个 Pod。负载均衡器将一半的流量路由到节点 A,另一半路由到节点 B。
图 11.11 理解 NodePort 和 LoadBalancer 服务的两种外部流量策略

当 externalTrafficPolicy 设置为 Cluster 时,每个节点将流量转发到系统中的所有 Pod。流量在 Pod 之间平均分配。此时需要额外的网络跳转,并且客户端 IP 将被隐藏。当 externalTrafficPolicy 设置为 Local 时,所有到达节点 A 的流量将被转发到该节点上的单个 Pod。这意味着该 Pod 将接收到所有流量的 50%。到达节点 B 的流量会在两个 Pod 之间分配。每个 Pod 接收到负载均衡器处理的总流量的 25%。没有不必要的网络跳转,并且源 IP 是客户端的 IP。就像作为工程师做出的多数决策一样,每个服务选择使用哪种外部流量策略取决于你愿意做出的权衡。
11.3 管理服务
端点到目前为止,你已经了解到服务是由 Pod 支撑的,但情况并非总是如此。服务转发流量的端点可以是任何具有 IP 地址的对象。
11.3.1 介绍 Endpoints 对象
服务通常由一组 Pod 支撑,这些 Pod 的标签与 Service 对象中定义的标签选择器匹配。除了标签选择器之外,Service 对象的 spec 或 status 部分并不包含属于该服务的 Pod 列表。然而,如果你使用 kubectl describe 检查服务,你会在 Endpoints 下看到 Pod 的 IP,如下所示:

kubectl describe 命令收集这些数据不是来自 Service 对象,而是来自与该服务同名的 Endpoints 对象。kiada 服务的端点在 kiada Endpoints 对象中指定。列出 Endpoints 对象你可以按如下方式获取当前命名空间中的 Endpoints 对象:

注意 endpoints 的简写是 ep。
另外,对象类型是 Endpoints(复数形式),而不是 Endpoint。运行 kubectl get endpoint 会报错。如你所见,该命名空间中有三个 Endpoints 对象,每个服务一个。每个 Endpoints 对象包含一个 IP 和端口组合列表,这些组合代表了服务的端点。
更详细地检查 Endpoints
对象要查看哪些 pods 代表这些端点,可以使用 kubectl get -o yaml 来获取 Endpoints 对象的完整清单,如下所示:

如你所见,每个 Pod 都作为 addresses 数组的一个元素列出。在 Kiada Endpoints 对象中,所有端点都位于同一个端点子集中,因为它们都使用相同的端口号。然而,如果一组 Pod 使用端口 8080,而另一组使用端口 8088,则 Endpoints 对象将包含两个子集,每个子集都有自己的端口。
了解谁管理 Endpoints 对象
你没有创建这三个 Endpoints 对象。它们是在你创建相关 Service 对象时由 Kubernetes 创建的。这些对象完全由 Kubernetes 管理。每次出现或消失一个与 Service 标签选择器匹配的新 Pod 时,Kubernetes 都会更新 Endpoints 对象以添加或删除与该 Pod 关联的端点。你也可以手动管理服务的端点。稍后你将学习如何进行操作。
11.3.2 介绍 EndpointSlice 对象
正如你可以想象的,当一个服务包含大量的端点时,Endpoints 对象的大小会成为一个问题。Kubernetes 控制平面组件每次更改时都需要将整个对象发送到所有集群节点。在大型集群中,这会导致明显的性能问题。为了解决这个问题,引入了 EndpointSlice 对象,它将单个服务的端点拆分为多个片段。
虽然一个 Endpoints 对象包含多个 endpoint 子集,但每个 EndpointSlice 只包含一个。如果两组 Pod 在不同端口上暴露服务,它们会出现在两个不同的 EndpointSlice 对象中。此外,一个 EndpointSlice 对象最多支持 1000 个 endpoints,但默认情况下,Kubernetes 仅向每个 slice 添加最多 100 个 endpoints。一个 slice 中的端口数量也限制为 100。因此,对于具有数百个 endpoints 或多个端口的服务,可能会有多个 EndpointSlice 对象与之关联。与 Endpoints 类似,EndpointSlices 也是自动创建和管理的。
列出 EndpointSlice 对象
除了 Endpoints 对象,Kubernetes 还会为你的三个服务创建 EndpointSlice 对象。你可以使用 kubectl get endpointslices 命令查看它们。

注意 截至本文撰写时,还没有针对 EndpointSlices 的简写方式。
你会注意到,与 Endpoints 对象不同,Endpoints 对象的名称与其各自的 Service 对象名称相匹配,每个 EndpointSlice 对象在服务名称后包含一个随机生成的后缀。这样,每个服务可以存在多个 EndpointSlice 对象。列出特定服务的 EndpointSlices要仅查看与特定服务关联的 EndpointSlice 对象,可以在 kubectl get 命令中指定标签选择器。要列出与 kiada 服务关联的 EndpointSlice 对象,可以使用标签选择器 kubernetes.io/service-name=kiada,如下所示:

检查 EndpointSlice要更详细地检查一个 EndpointSlice 对象,可以使用 kubectl describe。由于 describe 命令不需要完整的对象名称,并且与某个服务关联的所有 EndpointSlice 对象都以该服务名称开头,因此只需指定服务名称即可查看它们,如下所示:

注意 如果有多个 EndpointSlice 与你提供给 kubectl describe 的名称匹配,该命令将打印它们所有的信息。
kubectl describe 命令输出的信息与你之前看到的 Endpoint 对象中的信息没有太大不同。EndpointSlice 对象包含端口和端点地址的列表,以及表示这些端点的 Pod 的信息。这包括 Pod 的拓扑信息,用于拓扑感知的流量路由。你将在本章后面学习相关内容。
11.3.3 手动管理服务端点
当你使用标签选择器创建 Service 对象时,Kubernetes 会自动创建并管理 Endpoints 和 EndpointSlice 对象,并使用选择器来确定服务端点。不过,你也可以通过创建没有标签选择器的 Service 对象来手动管理端点。在这种情况下,你必须自己创建 Endpoints 对象。你不需要创建 EndpointSlice 对象,因为 Kubernetes 会镜像 Endpoints 对象来创建相应的 EndpointSlice。通常,当你希望以不同的名称使现有的外部服务对集群中的 pod 可访问时,会以这种方式管理服务端点。这样,服务可以通过集群 DNS 和环境变量被发现。
创建没有标签选择器的服务以下列表显示了一个不定义标签选择器的 Service 对象清单示例。你需要手动配置该服务的端点。
清单 11.5 一个没有 Pod 选择器的服务

清单中定义的清单文件声明了一个名为 external-service 的服务,该服务接受端口 80 的传入连接。如本章第一部分所述,集群中的 Pod 可以通过服务的集群 IP 地址(在创建服务时分配)或其 DNS 名称来使用该服务。
创建 Endpoints 对象
如果服务未定义 Pod 选择器,则不会自动为其创建 Endpoints 对象。你必须手动创建。下面的清单显示了在上一节中创建的服务的 Endpoints 对象的清单。
清单 11.6 手动创建的 Endpoints 对象
Endpoints 对象必须与服务同名,并包含目标地址和端口的列表。在列表中,IP 地址 1.1.1.1 和 2.2.2.2 代表该服务的端点。注意你不必创建 EndpointSlice 对象。Kubernetes 会根据 Endpoints 对象自动创建它。创建 Service 及其关联的 Endpoints 对象后,Pods 就可以像使用集群中定义的其他服务一样使用该服务。如下图所示,发送到服务的集群 IP 的流量会分发到该服务的端点。这些端点位于集群外,但也可以是内部的。图 11.12 Pods 使用具有两个外部端点的服务。

Endpoints 对象必须与服务同名,并包含目标地址和端口的列表。在列表中,IP 地址 1.1.1.1 和 2.2.2.2 代表该服务的端点。
注意 你不必创建 EndpointSlice 对象。Kubernetes 会根据 Endpoints 对象自动创建它。
创建 Service 及其关联的 Endpoints 对象后,Pods 就可以像使用集群中定义的其他服务一样使用该服务。如下图所示,发送到服务的集群 IP 的流量会分发到该服务的端点。这些端点位于集群外,但也可以是内部的。
图 11.12 Pods 使用具有两个外部端点的服务。

如果你以后决定将外部服务迁移到在 Kubernetes 集群中运行的 Pods,你可以向该服务添加一个选择器,将流量重定向到这些 Pods,而不是手动配置的 Endpoints。这是因为在你为服务添加选择器后,Kubernetes 会立即开始管理 Endpoints 对象。你也可以做相反的操作:如果你想将现有服务从集群迁移到外部位置,删除 Service 对象中的选择器,这样 Kubernetes 就不再更新关联的 Endpoints 对象。从那时起,你可以手动管理服务的端点。你不需要删除该服务来执行此操作。通过更改现有的 Service 对象,服务的集群 IP 地址保持不变。使用该服务的客户端甚至不会注意到你已经迁移了服务。
11.4 了解服务对象的 DNS 记录
Kubernetes 服务的一个重要方面是能够通过 DNS 查找它们。这是一个值得深入研究的内容。你知道服务会被分配一个内部集群 IP 地址,Pod 可以通过集群 DNS 解析该地址。这是因为每个服务在 DNS 中都有一个 A 记录(或 IPv6 的 AAAA 记录)。然而,服务还会为它提供的每个端口接收一个 SRV 记录。让我们仔细看看这些 DNS 记录。首先,运行一个一次性的 Pod,如下所示:

该命令运行一个名为 dns-test 的 pod,其容器基于容器镜像 giantswarm/tiny-tools。该镜像包含 host、nslookup 和 dig 工具,你可以使用它们来检查 DNS 记录。当你运行 kubectl run 命令时,你的终端将附加到容器中运行的 shell 进程上(-it 选项实现此功能)。当你退出 shell 时,pod 将被删除(--rm 选项实现此功能)。
11.4.1 检查服务的 A 记录和 SRV 记录
你首先需要检查与你的服务关联的 A 记录和 SRV 记录。
查找服务的 A 记录
要确定 quote 服务的 IP 地址,你需要在 dns-test pod 的容器中运行 shell,然后执行 nslookup 命令,如下所示:

注意 你可以使用 dig 来代替 nslookup,但你必须使用 search 选项或指定服务的完全限定域名(FQDN)才能使 DNS 查询成功(运行 dig search quote 或 dig quote.kiada.svc.cluster.local)。
现在查找 kiada 服务的 IP 地址。虽然该服务类型为 LoadBalancer,因此既有集群内部 IP,也有外部 IP(负载均衡器的 IP),但 DNS 只返回集群 IP。这是预期的行为,因为 DNS 服务器是内部的,仅在集群内部使用。
查找 SRV 记录
一个服务可以提供一个或多个端口。每个端口在 DNS 中都有一个 SRV 记录。使用以下命令检索 kiada 服务的 SRV 记录:

注意截至本文撰写时,GKE 仍然运行 kube-dns 而不是 CoreDNS。kube-dns 并不支持本节中展示的所有 DNS 查询。运行在 Pod 中的智能客户端可以查找某个服务的 SRV 记录,以了解该服务提供了哪些端口。如果你在 Service 对象中定义了这些端口的名称,它们甚至可以通过名称进行查找。SRV 记录的格式如下:
![]()
kiada 服务中的两个端口名称分别是 http 和 https,它们都将 TCP 定义为协议。要获取 http 端口的 SRV 记录,请运行以下命令:

提示 要列出 kiada 命名空间中所有的服务及其开放的端口,可以运行命令 nslookup -query=SRV any.kiada.svc.cluster.local。要列出集群中的所有服务,请使用名称 any.any.svc.cluster.local。
你可能几乎不需要查找 SRV 记录,但一些互联网协议,如 SIP 和 XMPP,依赖它们来正常工作。
注意 请保持 dns-test Pod 中的 shell 运行,因为在下一节学习无主服务的练习中,你将需要使用它。
11.4.2 使用无主服务直接连接到 Pod
服务在一个稳定的 IP 地址上暴露一组 Pod。每个对该 IP 地址的连接都会被转发到随机的 Pod 或其他支撑该服务的端点。对服务的连接会自动在其端点之间进行分配。但如果你希望客户端进行负载均衡呢?如果客户端需要决定连接到哪个 Pod 呢?或者如果它需要连接到支持该服务的所有 Pod 呢?如果作为服务一部分的 Pod 都需要直接互相连接呢?通过服务的集群 IP 连接显然不是实现这些需求的方案。那么该怎么办呢?与其连接到服务 IP,客户端可以从 Kubernetes API 获取 Pod IP,但最好让它们对 Kubernetes 保持无感,并使用标准机制如 DNS。幸运的是,你可以通过创建无主服务来配置内部 DNS 返回 Pod 的 IP,而不是服务的集群 IP。
对于无主服务,集群 DNS 返回的不仅仅是一个指向服务集群 IP 的 A 记录,而是多个 A 记录,每个记录对应服务中的一个 Pod。因此,客户端可以查询 DNS 来获取服务中所有 Pod 的 IP。有了这些信息,客户端就可以直接连接到 Pod,如下图所示。
图 11.13 使用无主服务时,客户端直接连接到 Pod

创建无主服务要创建无主服务,需要将 clusterIP 字段设置为 None。为 quote Pod 创建另一个服务,但使其成为无主服务。以下清单显示了其清单内容:
清单11.7 一个无主服务

在使用 kubectl apply 创建服务后,可以使用 kubectl get 来检查。你会看到它没有集群 IP:

因为该服务没有集群IP,所以当你尝试解析服务名称时,DNS 服务器无法返回它。取而代之的是,它会返回 Pod 的 IP 地址。在继续之前,请按照以下方式列出与服务的标签选择器匹配的 Pod 的 IP 地址:

请注意这些 Pod 的 IP 地址。
了解为无主服务返回的 DNS A 记录
要查看解析服务时 DNS 返回的内容,请在上一节中创建的 dns-test Pod 中运行以下命令:

DNS 服务器返回与服务标签选择器匹配的四个 Pod 的 IP 地址。这与 DNS 对常规(非无主)服务(如 quote 服务)的返回不同,后者名称解析为服务的集群 IP:

理解客户端如何使用无主服务希望直接连接到属于某个服务的 pod 的客户端,可以通过从 DNS 获取 A(或 AAAA)记录来实现。客户端随后可以连接到返回的一个、多个或所有 IP 地址。不自己执行 DNS 查询的客户端,可以像使用普通非无主服务一样使用该服务。由于 DNS 服务器会轮换返回的 IP 地址列表,因此仅在连接 URL 中使用服务的 FQDN 的客户端,每次都会获得不同的 pod IP。因此,客户端请求会分布到所有 pod 上。你可以通过从 dns-test pod 使用 curl 向 quote-headless 服务发送多次请求来尝试如下操作:

每个请求由不同的 Pod 处理,就像使用常规服务时一样。不同之处在于,使用无主服务时,你直接连接到 Pod 的 IP,而使用常规服务时,你连接到服务的集群 IP,你的连接会被转发到其中一个 Pod。你可以通过运行带有 --verbose 选项的 curl 并检查它连接的 IP 来看到这一点:

无标签选择器的无主服务
为了结束本节关于无主服务的内容,我想提到,使用手动配置端点的服务(没有标签选择器的服务)也可以是无主服务。如果省略标签选择器并将 clusterIP 设置为 None,DNS 将为每个端点返回一个 A/AAAA 记录,就像服务端点是 Pod 时一样。要自己测试此功能,请应用 svc.external-service-headless.yaml 文件中的清单,并在 dns-test Pod 中运行以下命令:
![]()
11.4.3 为现有服务创建 CNAME
别名在前面的章节中,你已经学习了如何在集群 DNS 中创建 A 和 AAAA 记录。为此,你可以创建服务对象,这些对象要么指定标签选择器以查找服务端点,要么使用 Endpoints 和 EndpointSlice 对象手动定义它们。还有一种方法可以向集群 DNS 添加 CNAME 记录。在 Kubernetes 中,你可以通过创建服务对象来向 DNS 添加 CNAME 记录,就像添加 A 和 AAAA 记录一样。
注意 CNAME 记录是一种 DNS 记录,它将别名映射到现有的 DNS 名称,而不是 IP 地址。
创建 ExternalName 服务要创建一个作为现有服务别名的服务,无论该服务是在集群内还是集群外,都可以创建一个类型字段设置为 ExternalName 的 Service 对象。以下清单展示了这种类型服务的示例。
清单 11.8 一个 ExternalName 类型的服务

除了将类型设置为 ExternalName 外,服务清单还必须在 externalName 字段中指定该服务所解析的外部名称。ExternalName 服务不需要 Endpoints 或 EndpointSlice 对象。
从 Pod 连接到 ExternalName 服务
服务创建后,Pod 可以使用域名 time-api.<namespace>.svc.cluster.local(如果它们与服务在同一个命名空间中,则可以使用 time-api)连接到外部服务,而不必使用外部服务的实际 FQDN,如下例所示:
![]()
在 DNS 中解析 ExternalName 服务由于 ExternalName 服务是在 DNS 层实现的(仅为该服务创建 CNAME 记录),客户端不会像使用非无头 ClusterIP 服务那样通过集群 IP 连接到服务。它们直接连接到外部服务。像无头服务一样,ExternalName 服务没有集群 IP,如以下输出所示:

作为本节关于 DNS 的最后一个练习,你可以尝试如下在 dns-test Pod 中解析 timeapi 服务:

你可以看到 time-api.kiada.svc.cluster.local 指向 worldtimeapi.org。至此,本节关于 Kubernetes 服务的 DNS 记录内容就结束了。你现在可以通过输入 exit 或按 Control-D 键退出 dns-test pod 中的 shell。该 pod 将会被自动删除。
11.5 配置服务以将流量路由到附近的端点
当你部署 pod 时,它们会分布在集群中的各个节点上。如果集群节点跨越不同的可用区或区域,并且部署在这些节点上的 pod 之间进行流量交换,网络性能和流量成本可能会成为问题。在这种情况下,让服务将流量转发到离流量源 pod 较近的 pod 是有意义的。
在其他情况下,Pod 可能只需要与同一节点上的服务端点进行通信。这并不是出于性能或成本原因,而是因为只有节点本地的端点才能在适当的上下文中提供服务。让我解释一下我的意思。
11.5.1 使用 internalTrafficPolicy 仅在同一节点内转发流量
如果 Pod 提供的服务以某种方式绑定到 Pod 所在的节点,则必须确保运行在特定节点上的客户端 Pod 仅连接到同一节点上的端点。你可以通过创建一个将 internalTrafficPolicy 设置为 Local 的 Service 来实现这一点。
注意 你之前学习过 externalTrafficPolicy 字段,该字段用于在外部流量进入集群时防止节点之间不必要的网络跳转。服务的 internalTrafficPolicy 字段类似,但用途不同。
如下面的图所示,如果服务配置了 Local 内部流量策略,从某个节点上的 Pod 发出的流量只会转发到同一节点上的 Pod。如果没有本节点的服务端点,连接将失败。
图 11.14 设置 internalTrafficPolicy 为 Local 的服务行为

想象一个系统 Pod 在每个集群节点上运行,负责管理与连接到该节点的设备的通信。Pod 本身不直接使用设备,而是与系统 Pod 进行通信。由于 Pod 的 IP 是可替换的,而服务的 IP 是稳定的,Pod 通过服务连接到系统 Pod。为了确保 Pod 仅连接到本地的系统 Pod,而不是其他节点上的系统 Pod,服务被配置为只将流量转发到本地端点。你的集群中可能没有这样的 Pod,但你可以使用 quote Pod 来尝试此功能。
创建具有本地内部流量策略的服务
以下清单显示了名为 quote-local 的服务清单,该服务仅将流量转发到与客户端 Pod 运行在同一节点上的 Pod。
清单 11.9 一个只将流量转发到本地端点的服务

正如你在清单中看到的,服务会将流量转发到所有带有标签 app: quote 的 Pod,但由于 internalTrafficPolicy 设置为 Local,流量不会转发到集群中所有的 quote Pod,而只会转发到与客户端 Pod 位于同一节点的 Pod。通过使用 kubectl apply 应用清单来创建该服务。观察节点本地的流量路由在查看服务如何路由流量之前,你需要先弄清客户端 Pod 和作为服务端点的 Pod 位于哪个节点。使用 -o wide 选项列出 Pod,以查看每个 Pod 运行在哪个节点上。选择其中一个 kiada Pod 并记下它所在的集群节点。从该 Pod 使用 curl 连接到 quote-local 服务。例如,我的 kiada-001 Pod 运行在 kind-worker 节点。如果我在该 Pod 中多次运行 curl,所有请求都会被同一节点上的 quote Pod 处理。

没有请求被转发到其他节点上的pod。如果我删除kind-worker节点上的两个pod,下一次连接尝试将失败:

在本节中,你学习了如何仅在服务语义需要时将流量转发到节点本地的端点。在其他情况下,你可能希望流量优先转发到靠近客户端 Pod 的端点,只有在必要时才转发到更远的 Pod。你将在下一节中学习如何实现这一点。
11.5.2 拓扑感知提示
想象 Kiada 套件运行在一个集群中,该集群的节点分布在不同区域和地区的多个数据中心,如下图所示。你不希望在一个区域中运行的 Kiada Pod 连接到另一个区域的 Quote Pod,除非本地区域没有 Quote Pod。理想情况下,你希望连接在同一区域内建立,以减少网络流量和相关成本。
图 11.15 跨可用区路由服务流量

注意 截至本文撰写时,拓扑感知提示(topology-aware hints)仍处于 alpha 级功能阶段,因此未来可能会更改或被移除。
由于该功能仍处于 alpha 阶段,因此默认情况下未启用。我不会解释如何尝试它,而是直接说明它的工作方式。
理解拓扑感知提示的计算方式
首先,你的所有集群节点必须包含 kubernetes.io/zone 标签,以指示每个节点所在的可用区。要表示某个服务应使用拓扑感知提示,你必须将 service.kubernetes.io/topology-aware-hints 注释设置为 Auto。如果服务拥有足够数量的端点(endpoints),Kubernetes 会将这些提示添加到 EndpointSlice 对象中的每个端点。正如你在以下列表中所看到的,hints 字段指定了该端点可被消费的可用区。
列表 11.10 带有拓扑感知提示的 EndpointSlice

列表中仅显示一个端点。该端点表示在 node kind-worker 上运行的 pod quote-002,该节点位于 zoneA。因此,该端点的提示表明它应由 zoneA 中的 pods 使用。在这个特定情况下,只有 zoneA 应该使用这个端点,但 forZones 数组中可能包含多个区域。 这些提示由 EndpointSlice 控制器计算,EndpointSlice 控制器是 Kubernetes 控制平面的一部分。它根据每个区域可分配的 CPU 核心数将端点分配到各区域。如果一个区域的 CPU 核心数较多,它将被分配比 CPU 核心数较少的区域更多的端点。在大多数情况下,这些提示确保流量保持在一个区域内,但为了确保更均衡的分布,这种情况并不总是发生。
了解拓扑感知提示的使用位置每个节点确保发送到服务的集群 IP 的流量被转发到服务的某个端点。如果 EndpointSlice 对象中没有拓扑感知提示,则所有端点都会收到来自特定节点的流量,而不管它们所在的节点。但是,如果 EndpointSlice 对象中的所有端点都包含提示,每个节点只处理包含该节点所在区域提示的端点,其余的端点会被忽略。因此,来自节点上 Pod 的流量仅会被转发到部分端点。目前,你无法影响拓扑感知路由,除了开启或关闭它,但这在未来可能会有所变化。
11.6 管理服务端点中包含的pod
关于服务和端点,还有一件事尚未讨论。你已经了解,如果 Pod 的标签与服务的标签选择器匹配,则该 Pod 会被包含为服务的一个端点。一旦出现带有匹配标签的新 Pod,它就会成为服务的一部分,并且连接会被转发到该 Pod。但如果 Pod 中的应用程序还没有立即准备好接受连接呢?可能是应用程序需要时间加载配置或数据,或者需要预热,以便第一个客户端连接可以尽可能快速地处理,而不会因为应用程序刚刚启动而产生不必要的延迟。在这种情况下,你不希望 Pod 立即接收流量,特别是如果现有的 Pod 实例能够处理流量的话。在 Pod 刚启动而还未准备好之前,不将请求转发给它是有意义的。
11.6.1 引入就绪探针
在第6章中,你学习了如何通过让 Kubernetes 重新启动未通过存活探针的容器来保持应用程序的健康状态。一个类似的机制称为就绪探针,它允许应用程序发出信号,表示它已准备好接受连接。像存活探针一样,Kubelet 也会定期调用就绪探针以确定 Pod 的就绪状态。如果探针成功,Pod 被认为是就绪的。如果探针失败,则情况相反。与存活探针不同,如果容器的就绪探针失败,它不会被重新启动;它只是从所属服务的端点中移除。如下面的图所示,如果 Pod 的就绪探针失败,即使它的标签与服务中定义的标签选择器匹配,服务也不会将连接转发到该 Pod。
图 11.16 就绪探针失败的 Pod 将从服务中移除

“就绪”这一概念因应用而异。应用开发者决定在其应用的上下文中“就绪”意味着什么。为此,他们会公开一个端点,通过该端点 Kubernetes 可以询问应用是否已准备好。根据端点的类型,必须使用正确的就绪探针类型。
理解就绪探针类型
与存活探针一样,Kubernetes 支持三种类型的就绪探针:
- exec 探针会在容器中执行一个进程。用于终止进程的退出代码决定容器是否已就绪。
- httpGet 探针通过 HTTP 或 HTTPS 向容器发送 GET 请求。响应状态码决定容器的就绪状态。
- tcpSocket 探针会在容器上的指定端口打开一个 TCP 连接。如果连接建立,容器被视为已就绪。
配置探针执行的频率
你可能还记得,可以使用以下属性配置给定容器的存活探针运行的时间和频率:initialDelaySeconds、periodSeconds、failureThreshold 和 timeoutSeconds。这些属性也适用于就绪探针,但就绪探针还支持额外的 successThreshold 属性,该属性指定探针必须成功的次数,才能将容器视为就绪。这些设置最好通过图示来解释。下图显示了各个属性如何影响就绪探针的执行以及容器的最终就绪状态。
图 11.17 就绪探针执行及容器最终就绪状态

注意 如果容器定义了启动探针,则就绪探针的初始延迟从启动探针成功时开始。第6章中解释了启动探针。
当容器准备就绪时,Pod 会成为其标签选择器匹配的服务的一个端点。当它不再准备就绪时,它将从这些服务中移除。
11.6.2 向Pod添加准备就绪探针
要查看准备就绪探针的实际效果,可以创建一个新的 Pod,并添加一个可以随意从成功切换到失败的探针。这并不是配置准备就绪探针的实际示例,但它可以让你查看探针的结果如何影响 Pod 是否被包含在服务中。下面的清单显示了 Pod 清单文件 pod.kiada-mock-readiness.yaml 的相关部分,你可以在本书的代码仓库中找到该文件。
清单 11.11 Pod 中准备就绪探针的定义

就绪探针会定期在 kiada 容器中运行 ls /var/ready 命令。如果文件存在,ls 命令会返回退出码为零,否则返回非零值。由于零被视为成功,因此如果文件存在,则就绪探针成功。定义如此特殊的就绪探针的原因是,你可以通过创建或删除相关文件来改变其结果。当你创建 Pod 时,该文件尚不存在,因此 Pod 还未就绪。在创建 Pod 之前,删除除 kiada-001 之外的所有其他 kiada Pod。这样可以更方便地观察服务端点的变化。
观察 Pod 的准备状态在你从清单文件创建 Pod 后,请按如下方式检查其状态:

READY 列显示该 Pod 的容器中只有一个处于就绪状态。这个容器是 envoy 容器,它没有定义就绪探针。没有就绪探针的容器在启动后会被视为就绪。由于 Pod 的容器并非全部就绪,Pod 不应接收发送到该服务的流量。你可以通过向 kiada 服务发送多个请求来检查这一点。你会注意到所有请求都由 kiada-001 Pod 处理,它是该服务的唯一活动端点。这可以从与服务关联的 Endpoints 和 EndpointSlice 对象中看出。例如,kiada-mock-readiness Pod 显示在 Endpoints 对象的 notReadyAddresses 中,而不是 addresses 数组中:

在EndpointSlice对象中,端点的就绪条件为false:

注意 在某些情况下,你可能希望忽略 Pod 的就绪状态。如果你希望一个组中的所有 Pod 即使尚未就绪也能获取 A、AAAA 和 SRV 记录,就可能会出现这种情况。如果你在 Service 对象的 spec 中将 publishNotReadyAddresses 字段设置为 true,则未就绪的 Pod 会在 Endpoints 和 EndpointSlice 对象中标记为就绪。集群 DNS 等组件会将它们视为已就绪。
为了使就绪探针成功,请按照以下方式在容器中创建 /var/ready 文件:

kubectl exec 命令在 kiada-mock-readiness Pod 的 kiada 容器中运行 touch 命令。touch 命令会创建指定的文件。容器的就绪探针现在将成功。现在,所有 Pod 的容器都应该显示为就绪。可以通过以下方式验证这一点:

令人惊讶的是,Pod 仍未就绪。是出现了问题,还是这是预期的结果?使用 kubectl describe 仔细查看该 Pod。在输出中你会发现以下这一行:
![]()
Pod 中定义的就绪探针配置为每 5 秒检查一次容器的状态。但是,它还配置为需要连续两次探针尝试成功,才能将容器的状态设置为就绪。因此,在创建 /var/ready 文件后,大约需要 10 秒钟 Pod 才会变为就绪状态。当这种情况发生时,Pod 应该成为服务的一个活动端点。你可以通过检查与该服务关联的 Endpoints 或 EndpointSlice 对象来验证,或者只需访问服务几次,查看 kiada-mock-readiness Pod 是否接收到你发送的请求。如果你想再次将 Pod 从服务中移除,请运行以下命令从容器中删除 /var/ready 文件:

这个就绪探针的示意仅用于展示就绪探针的工作原理。 在实际情况下,就绪探针不应采用这种方式实现。 如果你想手动将 Pod 从服务中移除,你可以通过删除 Pod 或更改 Pod 的标签,而不是操作就绪探针的结果来实现。
提示 如果你想手动控制一个 Pod 是否包含在服务中,可以给 Pod 添加一个诸如 enabled 的标签键,并将其值设置为 true。然后在你的服务中添加标签选择器 enabled=true。要从服务中移除该 Pod,只需移除 Pod 上的该标签即可。
11.6.3 实现真实场景下的就绪检查
如果你在 Pod 中没有定义就绪探针,它在创建后会立即成为服务端点。这意味着每次你创建一个新的 Pod 实例时,由服务转发到该新实例的连接将会失败,直到 Pod 中的应用程序准备好接受它们。为防止这种情况,你应该始终为 Pod 定义一个就绪探针。在前一节中,你已经学会了如何向容器添加一个模拟就绪探针,以手动控制 Pod 是否成为服务端点。在实际场景中,就绪探针的结果应该反映容器中运行的应用程序接受连接的能力。
定义一个最小化的就绪探针
对于运行 HTTP 服务器的容器,定义一个简单的准备就绪探针以检查服务器是否响应简单的 GET / 请求(如以下代码片段所示)要比完全没有准备就绪探针好得多。

当 Kubernetes 调用此就绪探针时,它会向容器的 8080 端口发送 GET / 请求,并检查返回的 HTTP 响应码。如果响应码大于等于 200 且小于 400,则探针成功,Pod 被视为已就绪。如果响应码为其他值(例如 404 或 500)或连接尝试失败,则就绪探针被视为失败,Pod 会被标记为未就绪。此简单探针确保 Pod 只有在实际能够处理 HTTP 请求时才成为服务的一部分,而不是在 Pod 启动时立即加入。
定义更好的就绪探针
像上一节中所示的简单就绪探针并不总是足够的。以 Quote Pod 为例。你可能还记得,它运行两个容器。quote-writer 容器从这本书中选择一个随机引用,并将其写入两个容器共享卷中的一个名为 quote 的文件。nginx 容器则从这个共享卷中提供文件。因此,该引用本身可以通过 URL 路径 /quote 访问。Quote Pod 的目的是显而易见的:提供书中的随机引用。因此,只有在它能够提供该引用时,它才应被标记为就绪。如果你将就绪探针定向到 URL 路径 /,即使 quote-writer 容器尚未创建 quote 文件,它也会返回成功。因此,Quote Pod 中的就绪探针应按以下 pod.quote-readiness.yaml 文件片段进行配置:

如果你将此就绪探针添加到你的 Quote Pod,你会发现只有当 quote 文件存在时,该 Pod 才显示为就绪。尝试按如下方式从 Pod 删除该文件:

现在使用 kubectl get pod 检查 Pod 的就绪状态,你会看到其中一个容器不再就绪。当 quote-writer 重新创建文件时,容器会再次变为就绪。你还可以使用 kubectl get endpoints quote 检查 quote 服务的端点,你会看到 Pod 被移除然后重新添加。
实现专用的就绪端点
正如你在前面的示例中看到的,将就绪探针指向 HTTP 服务器提供的现有路径可能就足够了,但应用程序通常也会提供一个专用端点,例如 /healthz/ready 或 /readyz,用于报告其就绪状态。当应用程序收到该端点的请求时,它可以执行一系列内部检查以确定其就绪状态。
以 Quiz 服务为例。Quiz pod 同时运行一个 HTTP 服务器和一个 MongoDB 容器。如以下清单所示,quiz-api 服务器实现了 /healthz/ready 端点。当它接收到请求时,会检查是否能成功连接到另一个容器中的 MongoDB。如果可以,它会返回 200 OK;如果不行,则返回 500 内部服务器错误。清单 11.12:quiz-api 应用程序中的就绪端点

Quiz pod 中定义的就绪探针确保 Pod 提供服务所需的一切资源都已就绪并正常工作。随着更多组件被添加到 quiz-api 应用中,可以在就绪检查代码中添加更多检查。例如,添加内部缓存就是一个例子。就绪端点可以检查缓存是否已经预热,只有在缓存预热完成后,Pod 才会向客户端开放。
在就绪探针中检查依赖项
在 Quiz Pod 中,MongoDB 数据库是 quiz-api 容器的内部依赖项。另一方面,Kiada Pod 依赖于 Quiz 和 Quote 服务,这些都是外部依赖项。那么就绪探针应该在 Kiada Pod 中检查什么?它是否应该检查是否能访问 Quote 和 Quiz 服务?这个问题的答案存在争议,但每当在就绪探针中检查依赖项时,必须考虑如果出现临时问题(例如网络延迟暂时升高)导致探针失败,会发生什么。请注意,就绪探针定义中的 timeoutSeconds 字段限制了探针响应的时间。默认超时时间仅为一秒。容器必须在此时间内对就绪探针作出响应。
如果 Kiada Pod 在就绪检查中调用其他两个服务,但它们的响应由于短暂的网络中断而稍有延迟,则其就绪探针会失败,Pod 会被从服务端点中移除。如果这种情况同时发生在所有 Kiada Pod 上,将没有 Pod 可以处理客户端请求。中断可能只持续一秒钟,但 Pod 可能要数十秒后才会根据配置的 periodSeconds 和 successThreshold 属性重新加入服务。当在就绪探针中检查外部依赖时,你应考虑这种瞬时网络问题发生时会带来什么影响。然后应相应地设置周期、超时和阈值。
提示 试图过于“聪明”的就绪探针可能会带来更多问题而不是解决问题。一般来说,就绪探针不应测试外部依赖,但可以测试同一个 Pod 内的依赖。
Kiada 应用程序还实现了 /healthz/ready 端点,而不是让就绪探针使用 / 端点来检查其状态。该端点仅响应 HTTP 状态码 200 OK,并在响应体中返回 "Ready"。这确保了就绪探针只检查应用程序本身是否响应,而不会连接到 Quiz 或 Quote 服务。你可以在 pod.kiadareadiness.yaml 文件中找到 Pod 清单。
在 Pod 关闭的情况下理解就绪探针
在结束本章之前的最后一点说明。如你所知,就绪探针在 Pod 启动时最为重要,但它们也确保在正常操作过程中,当某些情况导致 Pod 不再就绪时,Pod 会被移出服务。那么当 Pod 正在终止时呢?一个正在关闭的 Pod 不应该成为任何服务的一部分。在实现就绪探针时,你是否需要考虑这一点?幸运的是,当你删除一个 Pod 时,Kubernetes 不仅会向 Pod 的容器发送终止信号,还会将 Pod 从所有服务中移除。这意味着你不必在就绪探针中为正在终止的 Pod 做任何特殊处理。你无需确保在应用程序接收到终止信号时探针会失败。
11.7 总结
在本章中,你终于将 Kiada 模块连接到了 Quiz 和 Service 模块。现在你可以使用 Kiada 套件来测试迄今为止获得的知识,并通过本书的引用来刷新记忆。在本章中,你学到了以下内容:
- Pods通过一个平面网络进行通信,该网络允许任何Pod访问集群中的任意其他Pod,而不管连接集群节点的实际网络拓扑如何。
- Kubernetes服务使一组Pods可以通过一个单一的IP地址访问。虽然Pods的IP可能会改变,但服务的IP保持不变。
- 服务的集群IP可以从集群内部访问,但NodePort和LoadBalancer类型的服务也可以从集群外部访问。
- 服务的端点要么由Service对象中指定的标签选择器确定,要么手动配置。这些端点会存储在Endpoints和EndpointSlice对象中。
- 客户端Pods可以使用集群DNS或环境变量来查找服务。根据服务类型,可能会创建以下DNS记录:A、AAAA、SRV与CNAME。
- 服务可以配置为仅将外部流量转发到接收到外部流量的同一节点上的 Pod,或者转发到集群中的任何 Pod。它们也可以配置为将内部流量仅路由到与流量源 Pod 位于同一节点的 Pod。拓扑感知路由确保在本地 Pod 可以提供所请求服务的情况下,流量不会跨可用区路由。
- Pod 只有在准备就绪后才会成为服务端点。通过在应用程序中实现就绪探针处理程序,你可以定义在特定应用程序的上下文中“就绪”意味着什么。
在下一章中,你将学习如何使用 Ingress 对象通过单个外部 IP 地址访问多个服务。
522

被折叠的 条评论
为什么被折叠?



