最近在 K8s 集群里部署多服务时,最让人意外的是服务之间居然可以不写 IP、不改代码地互相找到对方。这篇笔记拆解这套机制是怎么实现的:Service 如何当稳定的”门牌号”、CoreDNS 怎么当集群内的”黄页”、以及这套设计与单机 localhost 部署的本质区别。
1. 先理清 K8s 的几个基本对象
讲服务发现之前,先把几个会反复出现的对象过一遍,避免后面混在一起。
| 对象 | 一句话解释 |
|---|---|
| Pod | K8s 调度的最小单位,里面跑着一个或多个容器。每个 Pod 有自己的 IP,但这个 IP 不稳定——重启就换。 |
| Deployment | 用来声明”我要跑 N 个相同的 Pod”,K8s 会自动维护这个数量(挂了会重启)。 |
| Service | 一组 Pod 的稳定访问入口。给它一个名字 + ClusterIP,后面所有 Pod 都通过这个名字访问它。 |
| NodePort / Ingress | 把 Service 暴露到集群外部的两种方式(前者用节点端口,后者用 HTTP 路由)。 |
记住一句话:Pod 是临时的,Service 是稳定的。 后面所有机制都围绕这个差异展开。
2. Service:一个虚拟的、稳定的”门牌号”
创建 Service 时,Kubernetes 会在集群内部为它分配一个固定的虚拟 IP(ClusterIP)。这个 IP 永远不会变——只要 Service 存在,它就在那里。
假设后端被部署成 3 份 Pod:
apiVersion: apps/v1kind: Deploymentmetadata: name: backendspec: replicas: 3 # 3 个相同的 Pod template: spec: containers: - name: backend image: my-backend:1.0每个 Pod 启动后会被分到自己的随机 IP,比如 10.244.1.5、10.244.2.3、10.244.3.8。如果前端直接写这些 IP,任何一个 Pod 重启,IP 就变了,前端就找不到了。
解决办法是再创建一个 Service:
apiVersion: v1kind: Servicemetadata: name: backendspec: selector: app: backend # 选中上面那 3 个 Pod ports: - port: 80 targetPort: 8080K8s 会做两件事:
- 给这个 Service 分一个固定 ClusterIP(比如
10.96.100.1)。 - 在背后维护一个负载均衡器,把发往
10.96.100.1:80的请求,均匀转发到那 3 个 Pod 上。
于是前端只需要记住 backend:80(或者 10.96.100.1:80)这一个地址,就能访问到后端的任意副本。后端 Pod 增减、重启、IP 变化,前端完全不感知。
3. CoreDNS:一个内置的”黄页”
让前端去记 10.96.100.1 这种纯数字地址不现实。Kubernetes 内建了一个 DNS 服务(CoreDNS),相当于集群内部的电话本:
- 创建一个叫
backend的 Service 后,CoreDNS 会自动注册一条记录:backend→10.96.100.1。 - 前端 Pod 里的应用发起
http://backend:80请求时,容器里的 DNS 解析器会自动把backend翻译成10.96.100.1,流量就精准抵达了后端的 Service。
前端 Pod 里的应用:GET http://backend:80/api ↓ DNS 解析CoreDNS:backend → 10.96.100.1 ↓ 流量转发backend Service (10.96.100.1) → 负载均衡到某个后端 Pod这就是”服务发现”的本质:调用方只需要知道服务名,不需要知道它有几个副本、IP 是什么、跑在哪台机器上。
4. 一个完整的调用链路
假设集群里有 3 个服务:数据库、后端、前端。完整通信过程是这样的:
# 1. 数据库apiVersion: v1kind: Servicemetadata: name: dbspec: selector: app: mysql ports: - port: 3306
---# 2. 后端(3 副本)apiVersion: v1kind: Servicemetadata: name: backendspec: selector: app: backend ports: - port: 80 targetPort: 8080
---# 3. 前端(NodePort 暴露给外部)apiVersion: v1kind: Servicemetadata: name: frontendspec: type: NodePort selector: app: frontend ports: - port: 80 nodePort: 30080后端代码里,数据库连接地址直接写成 db:3306,不用关心 MySQL 的 Pod IP:
# 后端代码示例import mysql.connectorconn = mysql.connector.connect( host="db", # 直接用 Service 名 port=3306, user="app", password="xxx", database="app_db")前端代码里,API 请求地址直接写成 backend:80,也不用关心后端有几个副本:
// 前端代码示例fetch("http://backend:80/api/users") .then(res => res.json()) .then(data => console.log(data));用户从浏览器访问 http://Master节点IP:30080 时,完整的链路是:
用户浏览器 ↓ http://MasterIP:30080前端 Service (NodePort) ↓ 负载均衡到某个前端 Pod前端 Pod 发起 GET http://backend:80/api ↓ CoreDNS 解析 backend → 10.96.100.1后端 Service ↓ 负载均衡到某个后端 Pod后端 Pod 连接 mysql -h db ↓ CoreDNS 解析 db → 10.96.x.x数据库 Service ↓ 连上 MySQL Pod整个过程中,没有一个地方出现了具体的 Pod IP 或节点 IP。 服务之间完全通过 Service 名称通信,K8s 自动处理负载均衡、高可用和故障恢复。
5. 和单机 localhost 部署的本质区别
单机上跑多服务时,连接地址通常是这样写的:
# 单机部署:写死 localhostconn = mysql.connector.connect( host="localhost", # 必须是本机 port=3306, ...)要把后端拆到另一台机器上,要改的东西很多:IP 地址、端口、可能还有防火墙、配置中心……
| 维度 | 单机 localhost 方式 | Kubernetes Service 方式 |
|---|---|---|
| 服务地址 | 固定写死 IP/端口 | 通过虚拟 IP + DNS 动态发现 |
| 扩展性 | 无法横向扩展(端口冲突) | 随时增减副本,前端无感知 |
| 故障恢复 | 进程挂了就断了 | 副本自动重新调度,Service 自动转发到健康实例 |
| 部署灵活性 | 所有服务绑死在同一台机器 | 服务可以分布在集群的任何节点上 |
| 代码改动 | 迁移机器要改 IP/配置 | 一行不用改,Service 名不变即可 |
用一句话总结这个差异:
单机部署是”我告诉你地址”,Kubernetes 是”告诉我你叫什么,我帮你找到地址”。
后者让服务之间的通信变得松耦合、高可用,也更符合微服务架构的理念。
6. 写给自己的小结
这次在集群里做多服务部署实验,最关键的认知升级不是”我会写 yaml 了”,而是理解了”为什么 Pod IP 不重要”。
只要 Service 还在、名字没变,Pod 怎么重建、IP 怎么换、节点怎么调度,对调用方来说都是透明的。这种”调用方不关心被调用方物理位置”的设计,是分布式系统能横向扩展的基石。
K8s 把这套机制内化到了平台层——调用方一行代码不用改,平台就把寻址、负载均衡、故障恢复全做了。这在单台服务器上根本无法做到:单机部署时,进程地址是写死的、端口是冲突的、机器是绑死的,这套”靠名字找服务”的能力在单台服务器上根本无从发挥。