Kubernetes in Action 笔记 —— 通过 Services 对象暴露 Pod 中的服务

不同于只运行某个提供特定服务的单一 Pod,现在人们通常会以副本的形式部署多个 Pod 实例,以便工作负载能够均匀地分发到不同的集群节点上。
这也意味着同一个 Pod 的所有副本都提供相同的服务,且能够通过一个单一的地址访问。Kubernetes 中的 Services 对象就负责实现这部分功能。

Pods 间如何通信

Pods communicate via their own computer network

每个 Pod 都拥有自己的网络接口和 IP 地址。集群中的所有 Pod 通过一个私有的 Flat network 相互通信,该 Flat network 实际上是一个定义在实体网络之上的虚拟网络层。
Pod 中的容器可以通过这个虚拟网络层传输数据,无需进行 NAT 转换,就像是局域网中接入到同一个交换机上的计算机一样。
对于应用来说,Node 之间实际的网络拓扑是不重要的。

为什么需要 Service

如果某个 Pod 中的应用需要连接其他 Pod 中的另一个应用,则它需要知道目标 Pod 的访问地址,这是显而易见的。实际上实现起来要复杂的多:

  • Pods 是有生命周期的。一个 Pod 可以在任意时间被销毁和替代(IP 地址会变)
  • Pod 只有在分配给某个 Node 后才获取到 IP 地址,无法提前知道
  • 在水平扩展中,多个 Pod 副本提供同样的服务,每个副本都有自己的 IP 地址。当另一个 Pod 访问所有这些副本时,就需要能够用一个单一的 IP 或 DNS 名称连接到负载均衡器,再通过负载均衡器在所有的副本间分担工作负载

Service 介绍

Kubernetes Service 对象可以为一系列提供同一服务的 Pod 集合,绑定一个单一、稳定的访问点。在 Service 的生命周期里,其 IP 地址稳定不变。客户端通过该 IP 地址创建网络连接,这些请求之后再被转发给后端提供服务的 Pod。
简单来说,Service 就是放置在 Pods 前面的负载均衡器。

Exposing pods with Service objects

Pod 和 Service 如何组合在一起

Services 通过 labellabel selector 机制找到对应的 Pods。
Label selectors determine which pods are part of the Service

创建和更新 Service

PS:作者的示例代码可以从其 Github kubernetes-in-action-2nd-edition 处下载,Service 部分的代码位于 Chapter11
在创建 Service 之前,可以先进入到 Chapter11 路径下,运行 kubectl apply -f SETUP/ --recursive 命令,创建需要的 Pods。

1
2
3
4
5
6
7
$ kubectl get po
NAME READY STATUS RESTARTS AGE
quiz 2/2 Running 0 33m
quote-001 2/2 Running 0 33m
quote-002 2/2 Running 0 33m
quote-003 2/2 Running 0 33m
quote-canary 2/2 Running 0 33m

Kubernetes 支持如下几种 Service 类型:ClusterIPNodePortLoadBalancerExternalName
ClusterIP 是默认的类型,仅用于集群内部通信。

通过 YAML 清单文件创建 Service
quote Service 最小版本的清单文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: quote
spec:
type: ClusterIP
selector:
app: quote
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP

The quote service and the pods that it forwards traffic to

通过 kubectl expose 命令创建 Service
kubectl expose pod quiz --name quiz

获取 Services 列表

1
2
3
4
$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
quiz ClusterIP 10.106.164.155 <none> 8080/TCP 45s app=quiz,rel=stable
quote ClusterIP 10.102.134.27 <none> 80/TCP 62s app=quote

修改 Service 的 label selector
kubectl set selector service quiz app=quiz

修改 Service 暴露的端口
可以运行 kubectl edit svc quiz 命令编辑清单文件,将 port 字段修改为 80,保存退出即可。

访问集群内部的 Services

前面创建的 ClusterIP 类型的 Service 只支持集群内部访问,可以 ssh 到任意一个 Node 或者 Pod 上来测试其连通性。

从 Pods 连接 Services
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ kubectl get po
NAME READY STATUS RESTARTS AGE
quiz 2/2 Running 2 (26h ago) 27h
quote-001 2/2 Running 2 (26h ago) 27h
quote-002 2/2 Running 2 (26h ago) 27h
quote-003 2/2 Running 2 (26h ago) 27h
quote-canary 2/2 Running 2 (26h ago) 27h

$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
quiz ClusterIP 10.106.164.155 <none> 80/TCP 26h
quote ClusterIP 10.102.134.27 <none> 80/TCP 26h

$ kubectl exec -it quote-001 -c nginx -- sh
/ # curl 10.106.164.155
This is the quiz service running in pod quiz
/ # curl 10.102.134.27
This is the quote service running in pod quote-002 on node minikube
/ # curl 10.102.134.27
This is the quote service running in pod quote-003 on node minikube
/ # curl 10.102.134.27
This is the quote service running in pod quote-002 on node minikube
/ # curl 10.102.134.27
This is the quote service running in pod quote-canary on node minikube
Services 的 DNS 解析

Kubernetes 有一个内部的 DNS 服务器组件,供集群中所有的 Pods 使用。允许通过 Service 的名称解析其 ClusterIP 地址。

1
2
3
4
5
6
7
8
9
10
/ # curl quiz
This is the quiz service running in pod quiz
/ # curl quote
This is the quote service running in pod quote-003 on node minikube
/ # curl quote
This is the quote service running in pod quote-canary on node minikube
/ # curl quote
This is the quote service running in pod quote-003 on node minikube
/ # curl quote
This is the quote service running in pod quote-003 on node minikube

在 Pod 中使用 Services

可以参考如下 YAML 清单文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: kiada-003
labels:
app: kiada
rel: stable
spec:
containers:
- name: kiada
image: luksa/kiada:0.5
imagePullPolicy: Always
env:
- name: QUOTE_URL
value: http://quote/quote
- name: QUIZ_URL
value: http://quiz
ports:
- name: http
containerPort: 8080

完整的源代码参考 Chapter11 路径下的 kiada-stable-and-canary.yaml 文件。
运行 kubectl apply -f kiada-stable-and-canary.yaml 命令应用该清单文件。
所有容器成功运行后,运行 kubectl port-forward 命令启用本地端口转发:

1
2
3
4
5
$ kubectl port-forward kiada-001 8080 8443
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Forwarding from 127.0.0.1:8443 -> 8443
Forwarding from [::1]:8443 -> 8443

此时打开浏览器访问 http://localhost:8080https://localhost:8443 即可进入应用页面:
Kiada App

向集群外部暴露服务

为了令某个 Service 能够被外部世界访问,可以采取如下几种措施:

  • 为 Node 分配一个额外的 IP,并将其设置为 Service 的 externalIP
  • 将 Service 的类型配置为 NodePort,通过 Node 端口访问该服务
  • 创建 LoadBalancer 类型的 Service 对象
  • Ingress 对象

其中第一种方式会为 Service 对象的 spec.externalIPs 字段指定一个额外的 IP,这种方式并不常用。
更常见的方式是将 Service 类型设置为 NodePort。Kubernetes 会令该 Service 能够通过所有 Node 节点上的特定端口访问。通常还需要用户配置一个外部的负载均衡器负责将客户端流量转发到这些 Node 端口。
不同于 NodePort 一般需要手动配置负载均衡,Kubernetes 还支持自动完成类似搭建过程,只需要用户指定 Service 的类型为 LoadBalancer。但并不是所有环境下的集群都支持这样做,因为负载均衡的创建依赖特定的云服务供应商。
最后一种方式则是通过 Ingress 对象实现服务对外部的开放,其具体实现机制依赖于底层的 ingress 控制器。

NodePort Service

Exposing pods through a NodePort service

ClusterIP 类似,NodePort Service 支持通过内部的 cluster IP 访问。除此之外,它还可以通过任意一个 Node 的特定端口来访问。
最终由哪一个 Node 为客户端提供连接是不重要的,因为每一个 Node 都总是会将客户端请求转发给 Service 背后的任意 Pod,不管这个 Pod 是否运行在同一个 Node 上。
即 Node A 的端口接收到客户端请求,它可能会将该请求转发给 Node A 上运行的 Pod,也可能转发给 Node B 上运行的 Pod。

创建 NodePort Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
name: kiada
spec:
type: NodePort
selector:
app: kiada
ports:
- name: http
port: 80
nodePort: 30080
targetPort: 8080
- name: https
port: 443
nodePort: 30443
targetPort: 8443

上面的清单文件中共有 6 个 port,可以参考如下截图理解各个 port 的不同含义:
Exposing multiple ports through with a NodePort service

查看 NodePort Service

1
2
3
4
5
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kiada NodePort 10.103.96.75 <none> 80:30080/TCP,443:30443/TCP 6s
quiz ClusterIP 10.106.164.155 <none> 80/TCP 28h
quote ClusterIP 10.102.134.27 <none> 80/TCP 28h

访问 NodePort Service
访问 NodePort Service 不仅仅需要知道端口号,还必须先获取到 Node 的 IP 地址。
可以使用 kubectl get nodes -o wide 命令查看 Node 的 IP 地址(INTERNAL-IPEXTERNAL-IP)。

1
2
3
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
minikube Ready control-plane,master 144d v1.22.3 192.168.49.2 <none> Ubuntu 20.04.2 LTS 5.4.72-microsoft-standard-WSL2 docker://20.10.8

此时在集群内部,则可以使用以下几种 IP 端口组合来访问 Kiada 应用:

  • 10.103.96.75:80:cluster IP 和内部端口
  • 192.168.49.2:30080:Node IP 和 Node 端口

因为是 Minikube 单机模拟的集群环境,只有一个 Node 可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl 192.168.49.2:30080
KUBERNETES IN ACTION DEMO APPLICATION v0.5

==== TIP OF THE MINUTE
You can use the `jq` tool to print out the value of a pod’s `phase` field like this: `kubectl get po kiada -o json | jq .status.phase`.


==== POP QUIZ
Which of the following statements is correct?
0) When the readiness probe fails, the container is restarted.
1) When the liveness probe fails, the container is restarted.
2) Containers without a readiness probe are never restarted.
3) Containers without a liveness probe are never restarted.

Submit your answer to /question/6/answers/<index of answer> using the POST method.

LoadBalancer Service

LoadBalancer 类型的 Service 实际上是 NodePort 类型的扩展。其基本配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
name: kiada
spec:
type: LoadBalancer
selector:
app: kiada
ports:
- name: http
port: 80
nodePort: 30080
targetPort: 8080
- name: https
port: 443
nodePort: 30443
targetPort: 8443

与前面 NodePort Service 的配置几乎完全一致,只是服务类型由 NodePort 改为了 LoadBalancer

Exposing a LoadBalancer service

external traffic policy

任何通过 NodePort 的外部客户端连接,不管是直接访问 Node 端口还是通过 LoadBalancer 间接访问 Node 端口,客户端连接都有可能会被转发给另一个 Node 上的 Pod。即接收客户端连接的 Node 和执行任务的 Pod 所在的 Node 可能不是同一个。
在这种情况下,就意味着网络路径上多了一次跳转。

此外,在上述情况下,转发连接时还需要将 source IP 替换成一开始接收客户端连接的 Node 的 IP。这会导致 Pod 中运行的应用无法看到此网络连接的初始来源,即无法在其 access log 中记录真实的客户端地址。

Local external traffic policy 的优劣

为了解决上述问题,可以选择阻止 Node 将客户端连接转发给运行在其他 Node 上的 Pod。即访问 Node 的外部连接最终只会被同一个 Node 上的 Pod 接收到。具体方法是将 Service 对象 spec 字段下的 externalTrafficPolicy 字段改为 Local

但上述配置同时会引发其他问题。
第一,如果接收到外部连接的 Node 上并没有 Pods 在运行,则该连接会卡住。因此必须确保负载均衡器只会将外部连接转发给有 Pod 运行的 Node,可以通过令负载均衡器持续检测 healthCheckNodePort 来实现。
第二,external traffic policy 设置为 Local 会导致 Pods 间的负载不够均衡。LoadBalancer 均匀地分发外部连接给 Nodes,Node 再将连接转发到自身运行的 Pods 上。但是每个 Node 实际上运行着不同数量的 Pods,不能跨 Node 转发就意味着,在每个 Node 接收等量连接的前提下,有些 Node 上的 Pods 较少,则这些 Pods 平均要承担的负载就更多。

externalTrafficPolicy 设置为 ClusterLocal 的区别可以参考下图:
Understanding the two external traffic policies for NodePort and LoadBalancer services

参考资料

Kubernetes in Action, Second Edition