服务发现:微服务如何"找到彼此"

你有没有想过,在一个拥有上百个服务的系统中,服务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台。

小王需要:

一个不小心,漏改了某个服务的配置,线上事故预定。

问题二:机器宕机怎么办?💀

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("所有服务都挂了!")

你看,即使改成列表,你还是需要自己实现:

这不就是重复造轮子吗?

问题三:动态扩缩容不可能 🚫

凌晨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       │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
  1. Service:为一组Pod提供稳定的网络标识
  2. kube-proxy:在每个节点上配置转发规则
  3. CoreDNS:提供域名到Service的解析

我们逐一深入了解。


核心组件一:Service(服务) 🎯

Service是Kubernetes中最核心的概念,它定义了一组Pod的逻辑集合和访问这些Pod的策略。

Service解决了什么问题?

  1. IP不固定:Pod重启后IP会变化
  2. 动态增减:扩缩容时Pod数量在变
  3. 健康状态: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
  1. Deployment创建3个Pod,每个Pod都有标签app=user-service
  2. Service通过selector.app==user-service找到这3个Pod
  3. Service的ClusterIP作为这3个Pod的统一入口
  4. 请求到达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 */ 
  1. kube-proxy监听API Server,Service和Pod变化时更新iptables规则
  2. 请求到达Service的ClusterIP时,iptables规则随机选择一个Pod IP
  3. 使用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                                  │
│  实际处理请求                                            │
└─────────────────────────────────────────────────────────┘
  1. DNS解析:CoreDNS将user-service解析为Service的ClusterIP(10.96.100.50)
  2. 请求发送:登录服务向10.96.100.50发送请求
  3. 负载均衡:kube-proxy(IPVS)选择一个健康Pod(192.168.1.101)
  4. DNAT转发:目标IP改为192.168.1.101,端口改为8080
  5. 请求到达:请求到达用户服务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,客户端自己做负载均衡。

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
  1. 客户端第一次请求时,kube-proxy选择一个Pod
  2. 记录客户端IP -> Pod IP的映射(缓存3小时)
  3. 后续请求来自同一IP时,始终转发到同一个Pod

游戏行业的实战经验 🎮

经验一:合理设置资源限制 📊

resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"
  1. requests:K8s调度Pod时的参考,确保Node有足够资源
  2. limits:防止Pod占用过多资源,影响其他Pod

经验二:优雅终止 ⏳

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

经验四:监控服务发现指标 📊

# 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"

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

常见问题FAQ ❓

Q1: 为什么有时访问Service会超时?

  1. Pod还未就绪:ReadinessProbe还没通过
- 解决:增加initialDelaySeconds
  1. 资源不足:Node上资源耗尽,Pod处于Pending状态
- 解决:检查Node资源,扩容或调整资源限制
  1. 网络策略限制:NetworkPolicy阻止了访问
- 解决:检查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解析慢怎么办?

  1. 使用NodeLocal DNSCache:在每个Node上部署DNS缓存
# 安装NodeLocal DNSCache
kubectl apply -f https://k8s.io/examples/admin/dns/nodelocaldns.yaml
  1. 调整ndots参数:减少DNS搜索次数
apiVersion: v1
kind: Pod
spec:
  dnsConfig:
    options:
    - name: ndots
      value: "2"  # 默认是5,改小可以减少搜索次数
  1. 使用短域名:同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(实际处理后端请求)

要点总结 📝

核心概念

  1. Kubernetes原生支持服务发现,不需要Consul、Nacos、Eureka等独立组件。
  1. 三大核心组件
- Service:提供稳定的虚拟IP(ClusterIP)

- kube-proxy:在每个Node上配置负载均衡规则 - CoreDNS:提供域名到Service的解析

  1. Service通过Label Selector选择Pod,Pod增减时Service自动发现。

工作原理

  1. 服务发现流程
- Pod启动时向API Server注册

- Service通过Label Selector自动关联Pod - CoreDNS创建Service的DNS记录 - kube-proxy配置负载均衡规则 - 客户端通过Service名或ClusterIP访问

  1. 负载均衡在Node层完成,使用iptables或IPVS,不需要客户端SDK。

健康检查

  1. 三种探针
- Liveness:检测Pod是否存活,失败则重启

- Readiness:检测Pod是否就绪,失败则从Service移除 - Startup:慢启动应用,防止被Liveness误杀

  1. 健康检查要合理配置,太严频繁重启,太松影响可用性。

实战经验

  1. 永远用Service名调用,不要硬编码Pod IP。
  1. 资源限制要合理:memory limits应该是requests的2倍。
  1. Pod反亲和性:尽量让Pod分布在不同Node,提高可用性。
  1. 优雅终止很重要:terminationGracePeriodSeconds + preStop sleep 15秒。
  1. 监控关键指标:Endpoint数量、DNS延迟、Pod重启次数、连接失败率。

高级特性

  1. Headless Service:需要直连Pod或自己控制负载均衡时使用。
  1. Session Affinity:需要会话保持时配置sessionAffinity: ClientIP
  1. 灰度发布:用两个Deployment + 同一个Service实现流量分配。

与传统注册中心的对比 🆚

特性 Kubernetes Consul/Nacos/Eureka
部署复杂度 ✅ 平台自带,零部署 ❌ 需要独立部署集群
服务注册 ✅ Pod自动注册 ❌ 需要集成SDK
健康检查 ✅ 三种探针 ✅ 支持
负载均衡 ✅ kube-proxy自动处理 ⚠️ 客户端或LB处理
DNS集成 ✅ CoreDNS原生支持 ⚠️ 部分支持
多语言支持 ✅ 完全无关 ⚠️ 需要对应语言SDK
运维成本 ✅ 低(云平台托管) ❌ 高(自建集群)
功能丰富度 ⚠️ 专注服务发现 ✅ 包含配置中心等

在Kubernetes环境中,优先使用平台自带的服务发现

只有当需要跨集群、跨云平台的服务发现时,才考虑Consul等工具。



💬 评论 (0)

0/500
排序: