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。

$ 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 最小版本的清单文件如下:

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 列表

$ 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
$ 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 地址。

/ # 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 清单文件:

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 命令启用本地端口转发:

$ 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

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

$ 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)。

$ 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 可以使用。

$ 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 类型的扩展。其基本配置如下:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容