KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
02 · Service — keel 龙骨
类型:主题概念 · 应用课对应:02 章 · 清单解剖
类型:主题概念 · 应用课对应:02 章 · 清单解剖
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。
一、它为什么会出现
一个具体的错位。你的服务跑得好好的,把其中一个 Pod 删掉——它按上一页说的方式回来了,
但地址变了:
被删掉的那个 web-565bc6668-t8bp4 10.244.0.6
回来的那个 web-565bc6668-c9g8d 10.244.0.8
这件事在集群里不是意外,是常态:滚动更新、自愈、扩容都在换 Pod,每次都会带来新地址、淘汰旧地址。
所以"用地址去连接"在集群里根本不成立——你写下去的那一刻就开始失效了。
它面对的需求是:在底下一层不断换的情况下,往上提供一层不换的。
二、它是什么
一句话:Service 是一个不随 Pod 生灭而变的固定入口,加一份"哪些 Pod 收我的流量"的名单。
这两半要分开记,因为它们一个不动、一个一直在动:
- 固定的那一半:一个虚拟地址(
CLUSTER-IP)和一个能被解析的名字。它不会因为 Pod 被换掉而变。 - 会变的那一半:成员名单。它由 Pod 上的标签决定,所以 Pod 换了它就跟着换。
它不是这三样东西——混淆会带来误判:
- 不是一个程序。 它没有镜像、没有容器、没有进程。它就是"一个地址 + 一条选择规则",
所以get svc里那一行不是一个在运行的东西的状态。 - 不是一台负载均衡器。 "把请求分给名单里的某一个"这件事,不是靠 Service 自己跑一个进程完成的;
它是每个节点上的一套转发规则,服务是声明,不是机器。 - 不是"给 Pod 换个固定名字"。 Pod 的名字和地址照旧随它生灭。Service 做的是站在它们前面:
变的它不给你,给你的是那个不动的。
三、它长什么样
它自己写出来的样子,只有两处是它独有的:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector: # ← 哪些 Pod 收我的流量(跟 Pod 上的标签对上)
app: web
ports: # ← 对外的端口 / 转发到容器里的哪个端口
- port: 80
targetPort: 80
交上去之后,两条命令分别读出它那两半(Kubernetes v1.37.0):
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.96.94.104 <none> 80/TCP 1s
$ kubectl get endpoints web
NAME ENDPOINTS AGE
web 10.244.0.7:80,10.244.0.8:80 2s
第一行是不动的那一半(CLUSTER-IP),第二行是会变的那一半(ENDPOINTS)。
把 kubectl get pods -o wide 里那两个 IP 拿来一比,会发现它们和 ENDPOINTS 逐个对得上——
名单不是凭空来的,是从 Pod 身上收来的。
它还有一件只在集群内部看得见的事:那个名字能直接被解析,你不需要知道任何地址:
$ kubectl exec <集群里的某个 Pod> -- wget -qO- http://web
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
$ kubectl exec <集群里的某个 Pod> -- nslookup web
Name: web.default.svc.cluster.local
四、它怎么工作
从声明到能用,五步:
① 声明 你写 selector("带这些标签的 Pod 收我的流量")
② 找齐 有一段循环持续找出所有符合这个条件的 Pod
③ 记名单 把它们的地址写进这个 Service 的成员名单——这就是 get endpoints 读到的东西
④ 建入口 把"这个名字 / 这个虚拟地址"接到那份名单上(在集群内部生效)
⑤ 重来 名单里的任何变化(换掉一个、加一个、减一个)都只是让 ②③ 再跑一遍
第 ④ 步是"虚拟"这个词的来处:CLUSTER-IP 不是一个真接在哪块网卡上的地址,
它是每个节点上的一套转发规则。所以它才能做到"换成员不影响入口"——
入口这一侧从来就没打算指向某个具体的东西。
第 ⑤ 步解释了那个最反直觉的现象:名单可以是空的,而且空着不算错。
"符合这个条件的 Pod 现在有 0 个"是一个合法状态——可能只是还没有 Pod 起来。
所以这件事永远不会在提交时报错,只会在名单上表现出来。
五、它会带出哪些概念
- Pod:名单里的地址是 Pod 的地址,标签也写在 Pod 上——所以换的是它,不是 Service。→ 00 Pod
- 控制器:第 ②③ 步那段"持续找出并记名单"的循环,和上一页说的是同一类东西。→ 01 控制器
- 节点:转发规则落在每个节点上,所以 Service 的效果和"你有几台机器"是绑在一起的。→ 03 节点
- 两种 selector 的可变性:Service 的 selector 能随时改、Pod 模板里的不能——这是同一段代码在不同对象里的两个语义。→ 应用课 02 章
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
| 02 章:"Service 不创建 Pod,只『选择』 Pod" | 它只有"选择 + 记名单 + 建入口"三个动作,没有任何一步会造出 Pod;名单的来源是别人造好的东西 |
| 02 章:"Service 的 selector 匹配不到任何 Pod 时,K8s 不会报错" | 因为第 ⑤ 步里"名单为空"是一个合法的中间状态,不是错误。失败被推到了"访问时",而不是"提交时" |
02 章:"Service.spec.selector ……能,随时改,立刻生效" |
它改的是第 ① 步的输入。改完 ②③ 立刻重跑,名单就换了——入口那一半完全没动,所以不会有任何东西需要重建 |
| 02 章:"为什么加前缀之后服务还能正常工作(selector 和标签都没动,匹配关系不变)" | 因为它连接两边的依据是标签,不是名字。名字变了不影响匹配关系,所以这一半本来就不该被碰 |
这一页改变了你的哪个判断:以后看到"服务访问不通",第一件事不是去查 Pod 起没起来,
而是先去比对那份名单——get endpoints 是不是空的、名单里的地址是不是你预期的那些 Pod。
入口和成员是两半,症状一样、位置完全不同。
自测题
- 为什么
CLUSTER-IP可以在 Pod 被换掉之后保持不变,而 Pod 自己的地址不行? - "名单是空的"为什么不算一个错误状态?这个性质如何解释那类"apply 成功但服务不可用"的故障?