3.2.4 PodSpec
值得花一点时间来理解 Deployment 对象是如何组成的,因为它实际上封装了一个 Pod 对象,而 Pod 对象有其自己的规范。您会在 Kubernetes 中看到这种模式在其他更高阶的工作负载类型中重复,例如 Job。这也很重要,因为我们在 Service 中暴露 Deployment 的方式实际上是通过引用 Pods,而不是 Deployment。
当您创建三个副本的部署时,实际上是在指示 Kubernetes 部署控制器创建和管理三个 Pod。部署控制器管理这些 Pod 的生命周期,包括在您使用新容器更新部署时用新版本替换它们,以及重新调度因计划或非计划维护事件而被驱逐的 Pod。图 3.10 对该对象组成进行了可视化分解。
Pod 对象模板在 Kubernetes API 文档中被称为 PodSpec。您实际上可以将其提取出来并单独运行。为此,您需要提供一个头部,指定该对象是 Pod 类型而不是 Deployment;然后,您可以将 template 下的整个 YAML 复制到配置的根目录,如下所示。
列表 3.2 Chapter03/3.2.4_ThePodSpec/pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: timeserver
labels:
pod: timeserver-pod
spec:
containers:
- name: timeserver-container
image: docker.io/wdenniss/timeserver:1
您可以直接创建这个 Pod。这种 Pod 不受任何 Kubernetes 控制器管理。如果它们崩溃,将会重启,但如果由于升级事件或节点故障等原因被驱逐,则不会重新调度。因此,通常您不会直接调度 Pod,而是使用更高层次的对象,如 Deployment,或者在后面的章节中看到的 StatefulSet、Job 等。
注意 在 Kubernetes 中对象组合的一个关键要点是,每当您在像 Deployment 这样的对象中看到 PodSpec 时,请知道它携带了 Pod 的所有功能。这意味着您可以查看 Pod 的文档,并在托管对象的 Pod 模板中使用任何值。
PodSpec 包含有关您的应用程序的关键信息,包括构成它的一个或多个容器。每个容器都有自己的名称(因此您可以在多容器 Pod 中引用各个容器),以及最重要的字段:容器镜像路径。还有许多可选字段,包括一些重要字段,用于指定健康检查和资源要求,这些将在接下来的章节中介绍。
在 Deployment 及其嵌入的 PodSpec 中,还有一些看似重复的标签。Deployment 的 spec 中有一个 selector → matchLabels 部分,而 PodSpec 中有一个 metadata → labels 部分,两者都包含相同的键值对 pod: timeserver-pod 。那么,这里发生了什么呢?
好吧,由于 Pod 对象在创建后实际上是相对独立存在的(它作为一个由 Deployment 控制器管理的独立对象创建),我们需要一种方法来引用它。Kubernetes 通过要求 Pod 具有一个标签(这是一个任意的键值对)来解决这个问题,并且我们从 Deployment 中引用(选择)相同的标签。这本质上是将这两个对象绑定在一起的粘合剂。在图 3.11 中更容易可视化。
这个过程可能看起来没有必要,毕竟:Kubernetes 不能为我们做这个对象链接吗,因为 PodSpec 嵌入在 Deployment 中?您需要手动指定这些标签的原因是,它们在直接引用其他对象中的 Pods 时起着重要作用。例如,在下一节中,我们配置一个网络服务,它直接引用 Deployment 的 Pods,而不是 Deployment 本身。书中后面涉及的其他概念也是如此,例如 Pod 中断预算(PDB)。通过为您的 Pods 指定标签,您将知道在这些其他对象中引用哪个标签。Pod 是 Kubernetes 中的基本执行和调度单元,而 Deployment 只是创建、管理和与 Pods 交互的众多方式之一。
至于键值标签本身,它是完全任意的。对于 Kubernetes 来说,你可以使用 foo: bar 。我使用了 pod: timeserver-pod ,因为我发现它在选择其他对象中的 Pods 时读起来很好。很多文档使用类似 app: timeserver 的格式。我避免将 Deployment 的名称 ( timeserver ) 作为此标签的值,以避免误解认为 Deployment 的名称与 Pod 标签有任何关系(实际上没有)。
所以,这就是如何构建带有嵌入式 PodSpec 的 Deployment 对象。我希望理解这个对象组合以及如何引用 Pod 是有用的。在下一节中,我们将向世界公开这个 Deployment,它将通过标签引用 Pod。
3.2.5 发布您的服务
随着您的容器成功部署,您无疑会想与之互动!每个 Pod 都被分配了一个集群本地(内部)IP 地址,可用于集群内 Pod 之间的通信。可以将 Pod 直接暴露在互联网上以及节点的 IP 上(使用字段 hostPort ),但除非您正在编写实时游戏服务器,否则这通常不是您会做的。通常,尤其是在使用 Deployment 时,您会将 Pod 聚合到一个 Service 中,该 Service 提供一个具有内部(可选外部)IP 的单一访问点,并在您的 Pod 之间负载均衡请求。即使您只有一个 Pod 的 Deployment,您仍然会想创建一个 Service 以提供一个稳定的地址。
除了负载均衡,服务还跟踪哪些 Pod 正在运行并能够接收流量。例如,尽管您可能在部署中指定了三个副本,但这并不意味着在任何时候都会有三个副本可用。如果一个节点正在升级,可能只有两个副本,或者在您推出新版本的部署时,可能会有超过三个副本。服务只会将流量路由到正在运行的 Pod(在下一章中,我们将介绍一些您需要提供的关键信息,以确保这一过程顺利进行)。
服务在集群内部使用,以实现多个应用程序之间的通信(所谓的微服务架构),并为此提供方便的功能,例如服务发现。此主题在第 7 章中详细介绍。现在,让我们专注于使用服务,并通过指定一个 LoadBalancer 类型的服务将您的新应用程序暴露到互联网,以便最终用户使用。与部署一样,我们将从 YAML 配置开始。
清单 3.3 Chapter03/3.2_部署到 Kubernetes/service.yaml
apiVersion: v1
kind: Service
metadata:
name: timeserver
spec:
selector: ?
pod: timeserver-pod ?
ports:
- port: 80 ?
targetPort: 80 ?
protocol: TCP ?
type: LoadBalancer ?
? 流量将被路由到具有此标签的 Pods。
服务将暴露的端口
? 货物将被转发到的目的港
? 网络协议
? 服务类型;在这种情况下,是一个外部负载均衡器
端口列表允许您配置要为服务 ( port ) 的用户公开的端口,以及此流量将发送到的 Pod 的端口 ( targetPort )。这使您可以,例如,在端口 80(默认的 HTTP 端口)上公开服务,并将其连接到在端口 8080 上运行的容器中的应用程序。
在 Kubernetes 中,每个 Pod 和 Service 都有自己的内部集群 IP,因此您无需担心 Pod 之间的端口冲突。因此,您可以在任何您喜欢的端口上运行应用程序(例如,HTTP 服务的 80 端口),并为简便起见,像之前的示例一样对 port 和 targetPort 使用相同的数字。如果您这样做,您可以完全省略 targetPort ,因为默认值是使用 port 值。
所有服务(除了第 9 章中讨论的无头服务)都分配一个内部的、集群本地的 IP 地址,集群中的 Pod 可以使用。如果您像前面的示例中那样指定 type: LoadBalancer ,将额外分配一个外部 IP 地址。
请注意,此服务有一个名为 selector 的部分,类似于我们的部署。该服务并不引用部署,实际上对部署没有任何了解。相反,它引用了具有给定标签的一组 Pods(在这种情况下,将是由我们的部署创建的 Pods)。再次强调,正如图 3.12 所示,更容易可视化。
与 Deployment 对象不同, selector 部分没有 matchLabels 子部分。然而,它们是等效的。Deployment 只是使用了 Kubernetes 中更新、更具表现力的语法。Deployment 和 Service 中的选择器实现了相同的结果:指定对象所引用的 Pod 集合。
在您的集群上创建服务对象,使用
cd Chapter03/3.2_DeployingToKubernetes
kubectl create -f service.yaml
注意创建命令( kubectl create )对于部署和服务是相同的。所有 Kubernetes 对象都可以通过四个 kubectl 命令进行创建、读取、更新和删除(即 CRUD 操作): kubectl create 、 kubectl get 、 kubectl apply 和 kubectl delete 。
要查看您的服务状态,您可以在对象类型上拨打 kubectl get ,如下所示:
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.22.128.1 443/TCP 1h
timeserver LoadBalancer 10.22.129.13 203.0.113.16 80:30701/TCP 26m
请注意,您的服务在此列出(在此示例中为 timeserver ),以及另一个名为 kubernetes 的服务。如果显示了 kubernetes 服务,您可以忽略它,因为那是运行在您集群中的 Kubernetes API 服务。您还可以仅使用 kubectl get service $SERVICE_NAME 指定您感兴趣的服务。
如果输出中的 External IP 表示 Pending ,这只是意味着外部 IP 正在等待负载均衡器上线。这通常需要一两分钟,因此不需要急于调试为什么它处于待处理状态,除非这种情况持续了一段时间。与其重复之前的 get 命令,不如通过添加 --watch/-w 标志来流式传输状态的任何更改(即 kubectl get service -w )。运行该命令后,几分钟内,您应该会看到输出,指示您的服务现在具有外部 IP。
注意 要拥有一个外部 IP,您必须在云服务提供商上运行 Kubernetes,因为提供商在后台配置了一个可外部路由的网络负载均衡器。如果您在本地开发,请参见 3.4.3 节,了解如何使用工具如 kubectl port-forward 进行连接。
一旦 IP 上线,尝试通过访问 URL 来访问服务。根据前面的示例输出,这意味着访问 http://203.0.113.16 (但请用您自己的外部 IP 替换 kubectl get service! )。 curl 工具非常适合从命令行测试 HTTP 请求( curl http://203.0.113.16 );在浏览器中查看也同样有效:
$ curl http://203.0.113.16
The time is 7:01 PM, UTC.
故障排除:无法连接
导致 Unable to Connect 错误的两个常见原因是 (1) 选择器不正确和 (2) 端口错误。三次检查选择器是否与您部署的 Pod 模板中的标签匹配。验证目标端口确实是您的容器正在监听的端口(容器中打印端口的启动时调试消息可以很好地帮助验证这一点),并确保您从浏览器连接到正确的端口。
查看您是否可以通过 kubectl 的端口转发功能直接连接到 targetPort 上的某个 Pod。如果您无法直接连接到 Pod,那么问题可能出在 Pod 上。如果可以连接,则问题可能是服务定义不正确。您可以通过以下方式设置到 Deployment 中某个 Pod 的端口转发:
kubectl port-forward deploy/$DEPLOYMENT_NAME $FROM_PORT:$TO_PORT
其中 $FROM_PORT 是您将在本地使用的端口,而 $TO_PORT 是您在服务中定义的 targetPort 。使用我们之前的示例,这将是
kubectl port-forward deploy/timeserver 8080:80
然后浏览到 http://localhost:8080。这将自动选择 Deployment 中的一个 Pod(绕过 Service)。您还可以指定一个特定的 Pod 直接连接。
kubectl port-forward pod/$POD_NAME $FROM_PORT:$TO_PORT
故障排除:外部 IP 卡在待处理状态
获取外部 IP 可能需要一些时间,因此请等待几分钟。验证您的云服务提供商是否会为类型为 LoadBalanacer 的服务配置外部 IP。查看提供商的文档以获取有关在 Kubernetes 中设置负载均衡器的任何其他信息。
如果您在本地运行或只是想在等待外部 IP 的同时尝试该服务,您可以将机器上的一个端口转发到该服务,如下所示:
kubectl port-forward service/$SERVICE_NAME $FROM_PORT:$TO_PORT
3.2.6 与部署交互
在开发过程中,能够与容器交互以运行命令或来回复制文件是很方便的。幸运的是,Kubernetes 使这与 Docker 一样简单。
运行一次性命令
正如我们可以使用 docker exec 命令(在第 2 章中介绍)在 Docker 镜像上运行一次性命令,我们也可以使用 kubectl exec 在我们的 Pods 上运行一次性命令。用于诊断容器中问题的常用命令是 sh ,它将为您提供容器的交互式 shell(前提是容器中可用 sh )。从那里,您可以在容器内执行所需的其他调试步骤。
从技术上讲, exec 是在一个 Pod 上运行的,但我们可以指定 Deployment 而不是特定的 Pod, kubectl 将随机选择一个 Pod 来运行命令:
$ kubectl exec -it deploy/timeserver -- sh
# echo "Testing exec"
Testing exec
您可以通过这种方式在容器上运行任何命令,例如:
$ kubectl exec -it deploy/timeserver -- echo "Testing exec"
Testing exec
将文件复制到/从容器中
再次,与 Docker 类似, kubectl 具有一个 cp 命令,允许您在系统和容器之间复制文件。此命令要求 tar 二进制文件存在于您的容器镜像中。当您想下载应用程序日志或其他诊断信息时,这可能会很有用。默认路径是容器的工作目录,因此如果您在容器中有一个名为“example.txt”的文件,您可以像这样将其复制到您的机器:
kubectl cp $POD_NAME:example.txt example.txt
您还可以反向复制文件:
kubectl cp example.txt $POD_NAME:.
3.2.7 更新您的应用程序
现在您的应用程序已经部署并发布到世界上,您无疑希望能够更新它。对示例应用程序进行代码更改,然后将容器镜像构建并推送到容器库,并使用新的版本标签。例如,如果您之前使用的是 us-docker.pkg.dev/wdenniss/ts/timeserver:1 ,那么您的新镜像可以是 us-docker.pkg.dev/wdenniss/ts/timeserver:2 。您可以将此标签命名为您喜欢的任何内容,但使用版本号是一种良好的约定。
一旦容器镜像被推送到仓库(如我们在第 3.2.2 节中所做的),请用新镜像名称更新列表 3.1 中的 deploy.yaml 文件——例如(强调已添加):
清单 3.4 Chapter03/3.2.7_更新/deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: timeserver
spec:
replicas: 3
selector:
matchLabels:
pod: timeserver-pod
template:
metadata:
labels:
pod: timeserver-pod
spec:
containers:
- name: timeserver-container
image: docker.io/wdenniss/timeserver:2 ?
? 新图像版本
保存文件并将更改应用到您的集群中
$ kubectl apply -f deploy.yaml
deployment.apps/timeserver configured
当您应用此更改时,会发生一件有趣的事情。还记得 Kubernetes 如何不断寻求实现您的要求,将它在系统中观察到的状态驱动到您所需的状态吗?好吧,由于您刚刚声明该 Deployment 现在使用带有版本标签 2 的镜像,并且所有 Pods 当前都标记为 1 ,Kubernetes 将寻求更新实时状态,以便所有 Pods 都是当前版本。
我们可以通过运行 kubectl get deploy 来看到这一点。以下是一些示例输出:
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
timeserver 3/3 1 3 10m
READY 列显示了有多少个 Pod 正在处理流量以及我们请求了多少个。在这种情况下,所有三个 Pod 都已准备就绪。然而, UP-TO-DATE 列表明这些 Pod 中只有一个是当前版本。这是因为,默认情况下,Pod 是通过所谓的滚动更新策略进行更新的,即一次更新一个或几个,而不是一次性替换所有 Pod,从而导致应用程序出现一些停机时间。
滚动更新和其他发布策略将在下一章中详细介绍,以及需要配置的重要健康检查,以避免在发布过程中出现故障。现在,了解 Kubernetes 将执行您的更改,并用新的 v2 Pods 替换旧的 v1 Pods 就足够了。
一旦 UP-TO-DATE 计数等于 READY 计数,发布就完成了。您还可以观察到正在创建和替换的单个 Pod,使用 kubectl get pods ,这将显示 Deployment 中所有 Pod 的列表,包括新 Pod 和旧 Pod。
监控推广情况
由于 kubectl get 命令的输出显示的是瞬时信息,但部署是持续变化的,大多数操作员会以自动化的方式监控部署,避免不断重新运行相同的命令。Kubernetes 包括一个这样的选项,即 --watch/-w 标志,可以添加到大多数 kubectl 命令中,例如 kubectl get pods -w 和 kubectl get deploy -w. 。当指定 watch 时,状态的任何变化将被流式传输到控制台输出。
watch 标志的缺点是它会使输出变得混乱。如果有许多 Pods 在变化,您会看到一行接一行的打印,容易失去对系统当前状态的关注。我更倾向于使用 Linux watch 命令。与 watch 标志不同, watch 命令会刷新整个输出,选择性地显示当前更新与上次更新之间的变化。该命令在大多数 Linux 发行版、macOS 和 Windows 子系统 for Linux (WSL) 中可用,并可以在您获取软件包的地方找到。
当安装了 watch 时,您可以简单地将其添加到任何 kubectl 命令前,例如
watch kubectl get deploy
我最喜欢的 watch 标志是 -d ,它将突出显示任何更改:
watch -d kubectl get deploy
通过打开一个终端窗口(或 tmux 会话窗口)来观察每个命令,您可以仅使用 watch 和 kubectl 来组建一个实时状态仪表板。
观看部署
之前讨论的 kubectl get deploy 和 kubectl get pods 命令分别返回当前命名空间中的所有部署和 Pod。当您创建更多部署时,您可能希望仅指定您感兴趣的资源:
kubectl get deploy $DEPLOYMENT_NAME
对象的名称可以在文件顶部的元数据部分的 name 字段中找到。从单个部署查看所有 Pod 有点棘手;但是,您可以使用标签选择器来获取一组 Pod 的状态。
kubectl get pods --selector=pod=timeserver-pod
其中 pod=timeserver-pod 是在部署中指定的标签选择器。
3.2.8 清理
有多种方法可以清理我们创建的对象。您可以按对象类型和名称删除:
kubectl delete deploy timeserver
deployment.apps "timeserver" deleted
$ kubectl delete service timeserver
service "timeserver" deleted
$ kubectl delete pod timeserver
pod "timeserver" deleted
注意:您不需要删除由其他对象(如 Deployment)管理的 Pods,只需删除您手动创建的 Pods。删除 Deployment 将自动删除它管理的所有 Pods。
或者,您可以通过引用单个配置文件或配置文件目录来删除对象:
$ cd Chapter03
$ kubectl delete -f 3.2_DeployingToKubernetes
deployment.apps "timeserver" deleted
service "timeserver" deleted
$ kubectl delete -f 3.2.4_ThePodSpec/pod.yaml
pod "timeserver" deleted
如果在删除后你改变主意,你可以简单地重新创建它们(例如, kubectl create -f 3.2_DeployingToKubernetes )。这就是将配置保存在文件中的美妙之处:你不需要记住对实时状态所做的任何调整,因为一切都是首先在配置中更新的。
集群本身通常会产生费用,因此在一天结束时,您可以考虑删除它。这可以通过大多数云服务提供商的用户界面控制台完成。如果您使用命令行操作 GKE,可以运行 gcloud container clusters delete $CLUSTER_NAME --region $REGION 。即使集群中没有任何 Pods 或 Services 在运行,节点本身通常也会产生费用(除非您使用像 GKE Autopilot 这样的平台),但删除集群应该也会清理它们。如果您保留集群并使用按节点计费的平台,请注意您的节点资源以及 Kubernetes 对象,以确保您只保留所需的资源。
提示 本书的其余部分将假设您知道如何删除不想保留的资源。在您尝试本书(及其他地方)中的示例时,请记住这些步骤,并确保删除您创建但不再需要的任何对象,以释放资源并减少账单!