服务发现:微服务如何"找到彼此"
你有没有想过,在一个拥有上百个服务的系统中,服务A是怎么知道服务B在哪台机器上的?
更关键的是,当服务B从10台扩容到100台时,服务A怎么自动感知?
从一个"糟糕"的例子说起 🔥
想象这样一个场景:
你的游戏平台有一个登录服务,它需要调用用户服务来验证玩家身份。
新手工程师小王的方案很简单——直接把用户服务的 IP 地址写进配置文件:
# 糟糕的做法!
USER_SERVICE_URL = "192.168.1.100:8080"
def verify_user(token):
response = requests.get(f"{USER_SERVICE_URL}/verify?token={token}")
return response.json()
看起来没问题?等扩容的时候就傻眼了。
问题一:扩容是场噩梦 😱
双11来了,用户服务需要从1台扩到10台。
小王需要:
- 登录服务改配置,写进10个IP
- 支付服务改配置,写进10个IP
- 活动服务改配置,写进10个IP
- ...还有20多个服务要改
一个不小心,漏改了某个服务的配置,线上事故预定。
问题二:机器宕机怎么办?💀
192.168.1.100 这台机器突然挂了。
所有依赖它的服务瞬间报错。
虽然你还有其他9台机器在跑,但配置文件里第一个IP就是坏的,请求全炸了。
# 即使改成列表也没用
USER_SERVICE_URLS = [
"192.168.1.100:8080", # 这台挂了!
"192.168.1.101:8080",
"192.168.1.102:8080",
]
def verify_user(token):
# 轮询?随机?第一个挂了怎么办?
# 需要自己实现健康检查、重试、负载均衡...
for url in USER_SERVICE_URLS:
try:
return requests.get(f"{url}/verify?token={token}").json()
except:
continue
raise Exception("所有服务都挂了!")
你看,即使改成列表,你还是需要自己实现:
- 健康检查逻辑
- 失败重试机制
- 负载均衡策略
- 动态更新IP列表
这不就是重复造轮子吗?
问题三:动态扩缩容不可能 🚫
凌晨3点流量低谷,想缩容省点钱?
对不起,你得手动改配置、重启服务。
这哪是微服务,这比单体应用还痛苦。
[配图建议:左侧是硬编码IP的混乱连线图,右侧是K8s服务发现后的清晰架构对比]
Kubernetes:天生自带服务发现 🚀
在Kubernetes中,服务发现不是额外组件,而是平台自带的基础能力。
你不需要部署Consul、Nacos、Eureka这些独立组件,Kubernetes帮你搞定了所有事情。
Kubernetes如何实现服务发现?
Kubernetes通过三个核心组件提供完整的服务发现能力:
┌─────────────────────────────────────────────────────────┐
│ Kubernetes服务发现架构 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Pod(服务实例) │ │
│ │ user-service-7d8f9c6k5 (192.168.1.101) │ │
│ │ user-service-7d8f9c6k6 (192.168.1.102) │ │
│ │ user-service-7d8f9c6k7 (192.168.1.103) │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 自动关联 │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Service(抽象服务) │ │
│ │ Name: user-service │ │
│ │ ClusterIP: 10.96.100.50 │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 通过Service访问 │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Kube-DNS(CoreDNS) │ │
│ │ user-service.default.svc.cluster.local │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
- Service:为一组Pod提供稳定的网络标识
- kube-proxy:在每个节点上配置转发规则
- CoreDNS:提供域名到Service的解析
我们逐一深入了解。
核心组件一:Service(服务) 🎯
Service是Kubernetes中最核心的概念,它定义了一组Pod的逻辑集合和访问这些Pod的策略。
Service解决了什么问题?
- IP不固定:Pod重启后IP会变化
- 动态增减:扩缩容时Pod数量在变
- 健康状态:Pod可能不健康但仍被访问
Service提供一个虚拟IP(ClusterIP),作为这组Pod的统一入口。
无论Pod怎么变,Service的ClusterIP永远不变。
Service的三种类型 🏗️
1. ClusterIP(集群内部访问)
默认类型,只能在集群内部访问。
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
type: ClusterIP
selector:
app: user-service # 选择标签为app=user-service的Pod
ports:
- port: 80 # Service端口
targetPort: 8080 # Pod端口
protocol: TCP
# 集群内其他服务可以直接访问
response = requests.get("http://user-service/api/users")
# 或
response = requests.get("http://user-service.default.svc.cluster.local/api/users")
2. NodePort(节点端口访问)
在每个Node上开放一个端口,可以从集群外部访问。
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
type: NodePort
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # 30000-32767范围
# 从任意Node的30080端口访问
curl http://任意NodeIP:30080/api/users
3. LoadBalancer(云负载均衡器)
云服务商自动创建外部负载均衡器。
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
type: LoadBalancer
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
在腾讯云TKE上,会自动创建一个CLB(负载均衡器)。
Service的Label Selector机制 🏷️
Service通过Label Selector来选择对应的Pod。
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service # Pod标签
spec:
containers:
- name: user-service
image: user-service:v1.0
ports:
- containerPort: 8080
---
# Service
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service # 选择标签为app=user-service的Pod
ports:
- port: 80
targetPort: 8080
- Deployment创建3个Pod,每个Pod都有标签
app=user-service - Service通过
selector.app==user-service找到这3个Pod - Service的ClusterIP作为这3个Pod的统一入口
- 请求到达Service后,自动转发到其中一个Pod
# 扩容到10个副本
kubectl scale deployment user-service --replicas=10
# Service会自动发现新的7个Pod
# 不需要修改Service配置!
Pod从3个变10个,Service配置完全不用改,因为它是通过Label Selector动态选择的。
核心组件二:kube-proxy(网络代理) 🛣️
kube-proxy运行在每个Node上,负责维护网络规则,实现Service的负载均衡。
kube-proxy的三种模式 🔄
1. Userspace模式(已废弃)
最早的实现方式,性能较差,现在基本不用。
2. iptables模式(常用)
使用iptables规则实现负载均衡。
# 查看iptables规则
sudo iptables -t nat -L KUBE-SERVICES | grep user-service
# 输出示例:
# KUBE-SVC-xxx tcp -- anywhere user-service /* user-service */
- kube-proxy监听API Server,Service和Pod变化时更新iptables规则
- 请求到达Service的ClusterIP时,iptables规则随机选择一个Pod IP
- 使用DNAT(目标地址转换)转发到Pod
3. IPVS模式(推荐,性能更好)
使用IPVS(Linux内核负载均衡模块),性能更高。
# 查看IPVS规则
sudo ipvsadm -Ln | grep user-service
# 输出示例:
# TCP 10.96.100.50:80 rr
# -> 192.168.1.101:8080 Masq 1 0 0
# -> 192.168.1.102:8080 Masq 1 0 0
# -> 192.168.1.103:8080 Masq 1 0 0
# 在kube-proxy配置中启用
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
| 特性 | iptables | IPVS |
|---|---|---|
| 性能 | 一般(线性规则匹配) | 高(哈希表) |
| 负载均衡算法 | 随机 | 轮询、最少连接、源IP哈希等 |
| Service数量上限 | 约5000个 | 无上限 |
| 复杂度 | 低 | 中 |
数据包流转过程 📦
以登录服务调用用户服务为例:
┌─────────────────────────────────────────────────────────┐
│ 登录服务Pod │
│ Pod IP: 192.168.1.50 │
│ 请求: http://user-service/api/users │
└─────────────────────────────────────────────────────────┘
│
│ 1. DNS解析:user-service -> 10.96.100.50
▼
┌─────────────────────────────────────────────────────────┐
│ Service (user-service) │
│ ClusterIP: 10.96.100.50 │
│ 后端Pod: 192.168.1.101, .102, .103 │
└─────────────────────────────────────────────────────────┘
│
│ 2. kube-proxy通过IPVS选择Pod(假设选到.101)
▼
┌─────────────────────────────────────────────────────────┐
│ 用户服务Pod │
│ Pod IP: 192.168.1.101 │
│ 实际处理请求 │
└─────────────────────────────────────────────────────────┘
- DNS解析:CoreDNS将
user-service解析为Service的ClusterIP(10.96.100.50) - 请求发送:登录服务向10.96.100.50发送请求
- 负载均衡:kube-proxy(IPVS)选择一个健康Pod(192.168.1.101)
- DNAT转发:目标IP改为192.168.1.101,端口改为8080
- 请求到达:请求到达用户服务Pod,处理业务逻辑
核心组件三:CoreDNS(域名解析) 🌐
CoreDNS是Kubernetes的DNS服务器,负责服务名到ClusterIP的解析。
DNS记录类型 📝
Kubernetes为每个Service创建DNS记录。
1. 普通Service的DNS记录
# Service定义
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: default
spec:
ports:
- port: 80
user-service.default.svc.cluster.local -> 10.96.100.50
user-service.default.svc -> 10.96.100.50
user-service.default -> 10.96.100.50
user-service -> 10.96.100.50(同namespace)
<service-name>.<namespace>.svc.cluster.local
2. Headless Service的DNS记录
Headless Service不分配ClusterIP,DNS直接返回Pod IP列表。
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
clusterIP: None # Headless Service
selector:
app: user-service
ports:
- port: 80
user-service.default.svc.cluster.local ->
192.168.1.101
192.168.1.102
192.168.1.103
返回的是所有Pod的IP,客户端自己做负载均衡。
- 需要自己控制负载均衡策略
- StatefulSet(如数据库集群)
- 需要直连Pod的场景
DNS搜索域配置 🔍
Kubernetes自动配置Pod的/etc/resolv.conf:
# Pod内的/etc/resolv.conf
nameserver 10.96.0.10 # CoreDNS的Service IP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
当你访问user-service时,DNS会依次尝试:
1. user-service.default.svc.cluster.local
2. user-service.svc.cluster.local
3. user-service.cluster.local
4. user-service.(根据ndots配置)
# 在prod namespace访问default namespace的服务
response = requests.get("http://user-service.default.svc.cluster.local/api/users")
调试DNS问题 🐛
# 1. 查看CoreDNS Pod状态
kubectl get pods -n kube-system -l k8s-app=kube-dns
# 2. 查看CoreDNS配置
kubectl get configmap coredns -n kube-system -o yaml
# 3. 测试DNS解析(启动一个测试Pod)
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup user-service
# 4. 查看Pod的DNS配置
kubectl exec -it <pod-name> -- cat /etc/resolv.conf
健康检查:确保流量只打到健康Pod 🏥
Kubernetes的健康检查机制确保只有健康的Pod才接收流量。
三种探针(Probe) 🔬
1. Liveness Probe(存活探针)
检测Pod是否还活着。如果探针失败,K8s会重启Pod。
apiVersion: v1
kind: Pod
metadata:
name: user-service
spec:
containers:
- name: user-service
image: user-service:v1.0
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30 # 容器启动后30秒才开始检测
periodSeconds: 10 # 每10秒检测一次
timeoutSeconds: 5 # 超时时间
failureThreshold: 3 # 连续3次失败才判定为失败
- 检测死锁(进程活着但不处理请求)
- 检测资源耗尽(如内存泄漏)
2. Readiness Probe(就绪探针)
检测Pod是否准备好接收流量。如果探针失败,Pod会从Service中移除。
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
- 应用启动需要加载大量数据(启动慢)
- 依赖其他服务(如数据库连接未就绪)
| 探针 | 失败后的行为 | 用途 |
|---|---|---|
| Liveness | 重启Pod | 检测Pod是否死亡 |
| Readiness | 从Service移除 | 检测Pod是否能接收流量 |
3. Startup Probe(启动探针,K8s 1.18+)
用于慢启动应用,防止应用还没启动就被Liveness探针误杀。
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30 # 最多允许30次失败,即150秒
容器启动
│
▼
Startup Probe(如果配置了)
│(成功后转为其他探针)
▼
Readiness Probe(决定是否加入Service)
│
▼
Liveness Probe(持续监控,失败则重启)
三种检测方式 🔍
1. HTTP GET检测
livenessProbe:
httpGet:
path: /health/live
port: 8080
scheme: HTTP
httpHeaders:
- name: X-Custom-Header
value: health-check
@app.get("/health/live")
def liveness():
"""存活检查:进程是否活着"""
return {"status": "alive"}
@app.get("/health/ready")
def readiness():
"""就绪检查:是否能接收流量"""
# 检查数据库连接
if not db.is_connected():
return {"status": "not_ready"}, 503
# 检查Redis连接
if not redis.ping():
return {"status": "not_ready"}, 503
# 检查缓存是否预热
if not cache.is_warmed_up():
return {"status": "not_ready"}, 503
return {"status": "ready"}
2. TCP Socket检测
livenessProbe:
tcpSocket:
port: 8080
3. Exec命令检测
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "ps aux | grep user-service | grep -v grep"
探针最佳实践 ✨
| 实践 | 说明 |
|---|---|
| 分离Readiness和Liveness | Readiness检查依赖,Liveness检查进程 |
| initialDelaySeconds要合理 | 等应用真正启动后再开始检测 |
| timeoutSeconds不要太长 | 2-5秒足够,超时说明有问题 |
| failureThreshold不要太小 | 3次失败再判定,避免网络抖动误判 |
| 健康检查接口要轻量 | 不要在健康检查里执行耗时操作 |
containers:
- name: user-service
image: user-service:v1.0
ports:
- containerPort: 8080
# 启动探针:慢启动应用
startupProbe:
httpGet:
path: /health/startup
port: 8080
failureThreshold: 30
periodSeconds: 5
# 就绪探针:依赖检查
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# 存活探针:进程检查
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
服务发现实战:游戏平台案例 🎮
让我们通过一个游戏平台的真实案例,看看Kubernetes服务发现如何工作。
场景:登录服务调用用户服务
┌─────────────────────────────────────────────────────────┐
│ Kubernetes集群(腾讯云TKE) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 登录服务 │ │ 用户服务 │ │
│ │ Deployment │ │ Deployment │ │
│ │ │ │ │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ Pod A │ │ │ │ Pod A │ │ │
│ │ │ (登录验证) │ │ │ │ (用户信息) │ │ │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ Pod B │ │ │ │ Pod B │ │ │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ │ │ │ │ │ │ │
│ └───────┼───────────┘ └───────┼───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ login-service │ │ user-service │ │
│ │ Service │ │ Service │ │
│ │ ClusterIP: │ │ ClusterIP: │ │
│ │ 10.96.100.20 │ │ 10.96.100.50 │ │
│ └───────────────┘ └───────────────┘ │
│ │ ▲ │
│ │ 调用 │ DNS解析 │
│ └───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ CoreDNS │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
完整配置示例 📝
1. 用户服务(User Service)
# deployment-user-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
spec:
replicas: 3 # 运行3个副本
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: ccr.ccs.tencentyun.com/game/user-service:v1.0.0
ports:
- containerPort: 8080
name: http
# 资源限制
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# 健康检查
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
# service-user-service.yaml
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
type: ClusterIP
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
2. 登录服务(Login Service)
# deployment-login-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: login-service
labels:
app: login-service
spec:
replicas: 2
selector:
matchLabels:
app: login-service
template:
metadata:
labels:
app: login-service
spec:
containers:
- name: login-service
image: ccr.ccs.tencentyun.com/game/login-service:v1.0.0
ports:
- containerPort: 8080
env:
# 配置用户服务的地址(直接用Service名)
- name: USER_SERVICE_URL
value: "http://user-service/api"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
---
# service-login-service.yaml
apiVersion: v1
kind: Service
metadata:
name: login-service
spec:
type: ClusterIP
selector:
app: login-service
ports:
- port: 80
targetPort: 8080
3. 代码中如何调用
# login-service中的调用代码
import os
import requests
USER_SERVICE_URL = os.getenv("USER_SERVICE_URL", "http://user-service/api")
def verify_user(token):
"""验证用户token"""
try:
# 直接使用Service名调用
response = requests.get(
f"{USER_SERVICE_URL}/users/verify",
params={"token": token},
timeout=5
)
if response.status_code == 200:
return response.json()
else:
return None
except requests.Timeout:
# 超时重试
return verify_user(token) # 简化版,实际应有重试次数限制
except requests.RequestException as e:
logger.error(f"调用用户服务失败: {e}")
return None
def login(username, password):
"""用户登录"""
# 1. 验证用户名密码(调用用户服务)
user = verify_user_password(username, password)
if not user:
return {"error": "invalid_credentials"}
# 2. 获取用户详细信息(调用用户服务)
user_info = requests.get(f"{USER_SERVICE_URL}/users/{user['id']}").json()
# 3. 生成token
token = generate_token(user)
return {
"token": token,
"user": user_info
}
部署和验证 🚀
# 1. 部署用户服务
kubectl apply -f deployment-user-service.yaml
kubectl apply -f service-user-service.yaml
# 2. 部署登录服务
kubectl apply -f deployment-login-service.yaml
kubectl apply -f service-login-service.yaml
# 3. 查看服务
kubectl get services
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# user-service ClusterIP 10.96.100.50 <none> 80/TCP 5m
# login-service ClusterIP 10.96.100.20 <none> 80/TCP 2m
# 4. 查看Pod
kubectl get pods -l app=user-service
# NAME READY STATUS RESTARTS AGE
# user-service-7d8f9c6k5-abc12 1/1 Running 0 5m
# user-service-7d8f9c6k5-abc13 1/1 Running 0 5m
# user-service-7d8f9c6k5-abc14 1/1 Running 0 5m
# 5. 测试DNS解析(启动一个测试Pod)
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup user-service
# Server: 10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
# Name: user-service
# Address 1: 10.96.100.50 user-service.default.svc.cluster.local
# 6. 测试服务调用
kubectl run -it --rm debug --image=curlimages/curl --restart=Never -- \
curl http://user-service/api/users/1
# {"id": 1, "username": "player1", ...}
扩容演示 📈
# 将用户服务从3个副本扩容到10个
kubectl scale deployment user-service --replicas=10
# 等待几秒,查看Pod状态
kubectl get pods -l app=user-service
# NAME READY STATUS RESTARTS AGE
# user-service-7d8f9c6k5-abc12 1/1 Running 0 10m
# user-service-7d8f9c6k5-abc13 1/1 Running 0 10m
# ...
# user-service-7d8f9c6k5-abc21 1/1 Running 0 30s # 新Pod
# ...
# 登录服务不需要任何修改!
# Service会自动发现新的Pod
# 在登录服务的Pod中查看访问日志
kubectl logs -l app=login-service --tail=50 | grep "user-service"
# 你会看到请求被分发到不同的用户服务Pod
# 因为kube-proxy做了负载均衡
高级特性:会话保持 🎯
有些场景需要同一个用户的请求始终打到同一个Pod(如游戏服务器的session管理)。
Session Affinity配置
apiVersion: v1
kind: Service
metadata:
name: game-server
spec:
sessionAffinity: ClientIP # 基于客户端IP的会话保持
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3小时
selector:
app: game-server
ports:
- port: 80
targetPort: 8080
- 客户端第一次请求时,kube-proxy选择一个Pod
- 记录
客户端IP -> Pod IP的映射(缓存3小时) - 后续请求来自同一IP时,始终转发到同一个Pod
- 有状态服务(如游戏服务器)
- 需要保持连接的场景(WebSocket)
- 本地缓存场景
游戏行业的实战经验 🎮
经验一:合理设置资源限制 📊
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
- requests:K8s调度Pod时的参考,确保Node有足够资源
- limits:防止Pod占用过多资源,影响其他Pod
- CPU requests: 250-500m(根据实际压测)
- Memory limits: requests的2倍(防止突发流量OOM)
- 不要设置CPU limits(避免CPU节流影响性能)
经验二:优雅终止 ⏳
spec:
terminationGracePeriodSeconds: 60 # 给Pod 60秒优雅关闭时间
containers:
- name: game-service
image: game-service:v1.0
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 15 && /app/scripts/stop.sh" # 等待15秒再停止
1. K8s发送SIGTERM信号
2. Pod从Service中移除(不再接收新请求)
3. preStop钩子执行(sleep 15秒,让kube-proxy更新规则)
4. 处理现有请求(最多60秒)
5. 进程退出
6. 如果60秒后还没退出,K8s发送SIGKILL强制杀死
IPVS规则更新需要时间,如果不等待,可能仍有请求转发到正在关闭的Pod。
经验三:使用Pod反亲和性 🌍
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
template:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: user-service
topologyKey: kubernetes.io/hostname
- 提高可用性(单Node故障不影响整个服务)
- 分散负载(避免某个Node压力过大)
经验四:监控服务发现指标 📊
# Prometheus监控示例
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
data:
prometheus.yml: |
scrape_configs:
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: user-service
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| Endpoint数量 | Service后端Pod数量 | < 预期副本数的50% |
| DNS解析延迟 | CoreDNS性能 | > 100ms (P99) |
| Service连接失败率 | 网络问题 | > 1% |
| Pod重启次数 | 健康检查问题 | > 3次/小时 |
最佳实践总结 ✨
1. 永远使用Service名,不要硬编码Pod IP
# ❌ 错误做法
USER_SERVICE_URL = "http://192.168.1.101:8080"
# ✅ 正确做法
USER_SERVICE_URL = "http://user-service/api"
Pod IP会变,Service名永远不变。
2. 合理配置健康检查
健康检查太严格 → Pod频繁重启 健康检查太宽松 → 不健康Pod接收流量
readinessProbe:
initialDelaySeconds: 10 # 应用启动需要时间
periodSeconds: 5 # 每5秒检查一次
timeoutSeconds: 3 # 3秒超时
failureThreshold: 3 # 连续3次失败才判定为不健康
livenessProbe:
initialDelaySeconds: 30 # 给应用充分的启动时间
periodSeconds: 10 # 不用太频繁
failureThreshold: 3 # 给应用恢复的机会
3. 设置合理的资源限制
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi" # memory limits应该是requests的2倍
cpu: "500m"
- Java/Golang等语言的GC(垃圾回收)可能需要2倍内存
- 突发流量时需要额外内存
- 避免OOM(内存溢出)导致Pod被杀
4. 使用Pod反亲和性提高可用性
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: user-service
topologyKey: kubernetes.io/hostname
5. 配置优雅终止
terminationGracePeriodSeconds: 60
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 15 && /app/shutdown.sh"
6. 监控关键指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| Service Endpoint数量 | 后端Pod数量 | < 期望副本的50% |
| DNS解析延迟 | CoreDNS性能 | P99 > 100ms |
| Pod重启次数 | 健康检查问题 | > 3次/小时 |
| Service连接失败率 | 网络问题 | > 1% |
7. 跨namespace访问服务
# 同namespace(简写)
response = requests.get("http://user-service/api/users")
# 不同namespace(完整FQDN)
response = requests.get("http://user-service.default.svc.cluster.local/api/users")
8. 使用Headless Service的场景
apiVersion: v1
kind: Service
metadata:
name: game-server
spec:
clusterIP: None # Headless Service
selector:
app: game-server
ports:
- port: 8080
- 需要自己控制负载均衡
- StatefulSet(如MongoDB、Redis集群)
- 需要直连每个Pod(游戏服务器)
常见问题FAQ ❓
Q1: 为什么有时访问Service会超时?
- Pod还未就绪:ReadinessProbe还没通过
initialDelaySeconds
- 资源不足:Node上资源耗尽,Pod处于Pending状态
- 网络策略限制:NetworkPolicy阻止了访问
# 排查命令
kubectl describe service <service-name>
kubectl get endpoints <service-name> # 查看后端Pod
kubectl logs -l app=<app-name> # 查看Pod日志
Q2: 如何实现灰度发布(金丝雀部署)?
方式1:使用两个Deployment
# 正式版本(90%流量)
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service-v1
spec:
replicas: 9
selector:
matchLabels:
app: user-service
version: v1
template:
metadata:
labels:
app: user-service
version: v1
spec:
containers:
- name: user-service
image: user-service:v1.0.0
---
# 灰度版本(10%流量)
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service-v2
spec:
replicas: 1
selector:
matchLabels:
app: user-service
version: v2
template:
metadata:
labels:
app: user-service
version: v2
spec:
containers:
- name: user-service
image: user-service:v2.0.0
---
# Service同时选择两个版本
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service # 同时选中v1和v2
ports:
- port: 80
targetPort: 8080
方式2:使用Istio等Service Mesh(更精确)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: user-service
spec:
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: user-service
subset: v2
- route:
- destination:
host: user-service
subset: v1
weight: 90
- destination:
host: user-service
subset: v2
weight: 10
Q3: DNS解析慢怎么办?
- 使用NodeLocal DNSCache:在每个Node上部署DNS缓存
# 安装NodeLocal DNSCache
kubectl apply -f https://k8s.io/examples/admin/dns/nodelocaldns.yaml
- 调整ndots参数:减少DNS搜索次数
apiVersion: v1
kind: Pod
spec:
dnsConfig:
options:
- name: ndots
value: "2" # 默认是5,改小可以减少搜索次数
- 使用短域名:同namespace内直接用Service名
# 快(只需1次DNS查询)
requests.get("http://user-service/api")
# 慢(需要5次DNS尝试)
requests.get("http://user-service.default.svc.cluster.local/api")
Q4: Service的ClusterIP可以 ping 通吗?
ClusterIP是一个虚拟IP,只配合Service的端口使用。
# ❌ 不工作
ping 10.96.100.50
# ✅ 正确方式
curl http://10.96.100.50/api/users
Q5: 如何查看Service到Pod的映射关系?
# 查看Service的Endpoints
kubectl get endpoints user-service
# 输出示例:
# NAME ENDPOINTS AGE
# user-service 192.168.1.101:8080,192.168.1.102:8080,... 1d
# 详细信息
kubectl describe endpoints user-service
Service(ClusterIP: 10.96.100.50)
│
│ selector: app=user-service
▼
Endpoints(Pod IP列表: 192.168.1.101, .102, .103)
│
▼
Pod(实际处理后端请求)
要点总结 📝
核心概念
- Kubernetes原生支持服务发现,不需要Consul、Nacos、Eureka等独立组件。
- 三大核心组件:
- kube-proxy:在每个Node上配置负载均衡规则 - CoreDNS:提供域名到Service的解析
- Service通过Label Selector选择Pod,Pod增减时Service自动发现。
工作原理
- 服务发现流程:
- Service通过Label Selector自动关联Pod - CoreDNS创建Service的DNS记录 - kube-proxy配置负载均衡规则 - 客户端通过Service名或ClusterIP访问
- 负载均衡在Node层完成,使用iptables或IPVS,不需要客户端SDK。
健康检查
- 三种探针:
- Readiness:检测Pod是否就绪,失败则从Service移除 - Startup:慢启动应用,防止被Liveness误杀
- 健康检查要合理配置,太严频繁重启,太松影响可用性。
实战经验
- 永远用Service名调用,不要硬编码Pod IP。
- 资源限制要合理:memory limits应该是requests的2倍。
- Pod反亲和性:尽量让Pod分布在不同Node,提高可用性。
- 优雅终止很重要:terminationGracePeriodSeconds + preStop sleep 15秒。
- 监控关键指标:Endpoint数量、DNS延迟、Pod重启次数、连接失败率。
高级特性
- Headless Service:需要直连Pod或自己控制负载均衡时使用。
- Session Affinity:需要会话保持时配置
sessionAffinity: ClientIP。
- 灰度发布:用两个Deployment + 同一个Service实现流量分配。
与传统注册中心的对比 🆚
| 特性 | Kubernetes | Consul/Nacos/Eureka |
|---|---|---|
| 部署复杂度 | ✅ 平台自带,零部署 | ❌ 需要独立部署集群 |
| 服务注册 | ✅ Pod自动注册 | ❌ 需要集成SDK |
| 健康检查 | ✅ 三种探针 | ✅ 支持 |
| 负载均衡 | ✅ kube-proxy自动处理 | ⚠️ 客户端或LB处理 |
| DNS集成 | ✅ CoreDNS原生支持 | ⚠️ 部分支持 |
| 多语言支持 | ✅ 完全无关 | ⚠️ 需要对应语言SDK |
| 运维成本 | ✅ 低(云平台托管) | ❌ 高(自建集群) |
| 功能丰富度 | ⚠️ 专注服务发现 | ✅ 包含配置中心等 |
在Kubernetes环境中,优先使用平台自带的服务发现。
只有当需要跨集群、跨云平台的服务发现时,才考虑Consul等工具。
💬 评论 (0)