Kubernetes Label、LabelSelector 与 Service/Endpoints/DNS 深度解析:服务发现全链路剖析

一、Label 机制:Kubernetes 的元数据基石

1.1 Label 的设计哲学

在传统基础设施管理中,资源通过固定的层级结构组织(如目录树、固定分类)。这种方式在面对动态、多维度的资源管理需求时显得捉襟见肘。Kubernetes 选择了扁平化 + 标签的方案:

  • 所有资源处于同一层级(Namespace 内扁平)
  • 通过 Label(标签) 赋予资源任意多个维度的元数据
  • 通过 LabelSelector(标签选择器) 进行多维度的资源查询和关联

这种设计灵感来源于 Google 内部 Borg 系统的经验:在大规模集群中,松耦合的标签系统紧耦合的层级结构更适合动态环境。

1.2 Label 的数据模型

Label 是附加在 Kubernetes 对象 metadata.labels 上的 key-value 对:

metadata:
  labels:
    app: nginx                           # 应用名称
    version: v1.25.3                     # 版本号
    environment: production              # 环境
    tier: frontend                       # 层级
    team: platform-engineering           # 所属团队
    release: stable                      # 发布通道
    kubernetes.io/os: linux              # 系统前缀标签
    app.kubernetes.io/name: nginx        # 推荐标签
    app.kubernetes.io/instance: nginx-1  # 实例标识
    app.kubernetes.io/component: proxy   # 组件角色

Label Key 的命名规范(源码级校验 pkg/apis/core/validation/validation.go):

Key 格式: [prefix/]name

prefix(可选):
  - 必须是合法 DNS 子域名
  - 最长 253 字符
  - kubernetes.io/ 和 k8s.io/ 前缀保留给 Kubernetes 核心组件

name(必须):
  - 最长 63 字符
  - 必须以字母或数字开头和结尾
  - 中间可包含 字母、数字、'-'、'_'、'.'
  - 正则:[a-z0-9A-Z]([a-z0-9A-Z\-_.]*[a-z0-9A-Z])?

Value:
  - 最长 63 字符
  - 可以为空字符串
  - 非空时必须以字母或数字开头和结尾
  - 中间可包含 字母、数字、'-'、'_'、'.'

校验源码

// staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go
const (
    LabelValueMaxLength = 63
    DNS1123SubdomainMaxLength = 253
)

func IsValidLabelValue(value string) []string {
    var errs []string
    if len(value) > LabelValueMaxLength {
        errs = append(errs, fmt.Sprintf("must be no more than %d characters", LabelValueMaxLength))
    }
    if !labelValueRegexp.MatchString(value) {
        errs = append(errs, "a valid label must be an empty string or consist of ...")
    }
    return errs
}

1.3 Label vs Annotation:何时用哪个?

维度 Label Annotation
用途 标识和选择资源 附加非标识性元数据
可被选择器使用
值大小限制 63 字符 256KB
索引 在 etcd 中可被索引加速查询 不被索引
典型场景 app=nginx, env=prod 构建信息、Git commit SHA、工具配置

工程决策:如果一个元数据需要被 Service、ReplicaSet、NetworkPolicy 等资源通过选择器引用,它必须是 Label。否则应使用 Annotation,以避免 Label 索引膨胀影响 etcd 性能。

1.4 Label 在 etcd 中的存储与索引

Label 作为对象 metadata 的一部分,序列化为 Protobuf 存储在 etcd 中。API Server 提供了基于 Label 的服务端过滤能力:

GET /api/v1/namespaces/default/pods?labelSelector=app%3Dnginx%2Cenv%3Dprod

API Server 的过滤实现staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go):

func (s *store) GetList(ctx context.Context, key string, opts storage.ListOptions,
    listObj runtime.Object) error {
    // 从 etcd 获取所有对象
    // ...
    // 在内存中应用 Label 过滤
    if opts.Predicate.Label != nil {
        for _, item := range items {
            if opts.Predicate.Matches(item) {
                filtered = append(filtered, item)
            }
        }
    }
}

性能影响:Label 过滤在 API Server 内存中执行(非 etcd 层面)。在大规模集群(>5000 Pod)中,频繁的全量 List + Label 过滤可能导致 API Server 内存压力。Informer 的本地缓存 + 自定义 Indexer 是生产级解决方案。

二、LabelSelector 深度解析

2.1 两种选择器语法

2.1.1 等式选择器(Equality-based)
# 精确匹配
kubectl get pods -l app=nginx
kubectl get pods -l app=nginx,env=prod    # AND 语义

# 不等于
kubectl get pods -l app!=nginx

# 对应 API 请求
GET /api/v1/pods?labelSelector=app%3Dnginx%2Cenv%3Dprod
2.1.2 集合选择器(Set-based)
# In:值在集合中
kubectl get pods -l 'app in (nginx, apache)'

# NotIn:值不在集合中
kubectl get pods -l 'app notin (nginx, apache)'

# Exists:键存在(不关心值)
kubectl get pods -l 'app'
kubectl get pods -l '!app'   # 键不存在

# 组合使用
kubectl get pods -l 'app in (nginx),env=prod,tier'

2.2 LabelSelector 在不同资源中的使用

不同的 Kubernetes 资源对 LabelSelector 的支持程度不同:

# 1. Service(仅支持等式选择器)
apiVersion: v1
kind: Service
spec:
  selector:          # 简化格式,仅等式匹配
    app: nginx
    version: v1

# 2. ReplicaSet/Deployment(支持集合选择器)
apiVersion: apps/v1
kind: Deployment
spec:
  selector:
    matchLabels:     # 等式匹配
      app: nginx
    matchExpressions:  # 集合匹配
    - key: environment
      operator: In
      values: ["production", "staging"]
    - key: tier
      operator: Exists

# 3. Node Affinity(支持集合选择器)
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/arch
            operator: In
            values: ["amd64", "arm64"]

# 4. NetworkPolicy(支持集合选择器)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  podSelector:
    matchLabels:
      role: db
    matchExpressions:
    - key: environment
      operator: In
      values: ["production"]

2.3 LabelSelector 的匹配算法

源码位于 staging/src/k8s.io/apimachinery/pkg/labels/selector.go

// Selector 接口定义
type Selector interface {
    Matches(Labels) bool              // 判断标签集是否匹配
    Add(r ...Requirement) Selector    // 添加匹配条件
    Requirements() (Requirements, bool)
    String() string
}

// 单个匹配条件
type Requirement struct {
    key       string
    operator  selection.Operator  // In, NotIn, Exists, DoesNotExist
    strValues sets.String
}

// 匹配逻辑
func (r *Requirement) Matches(ls Labels) bool {
    switch r.operator {
    case selection.In, selection.Equals, selection.DoubleEquals:
        // 值必须在集合中
        if !ls.Has(r.key) {
            return false
        }
        return r.strValues.Has(ls.Get(r.key))

    case selection.NotIn, selection.NotEquals:
        // 值不在集合中(键不存在也算匹配)
        if !ls.Has(r.key) {
            return true
        }
        return !r.strValues.Has(ls.Get(r.key))

    case selection.Exists:
        // 键存在即可
        return ls.Has(r.key)

    case selection.DoesNotExist:
        // 键不存在
        return !ls.Has(r.key)

    case selection.GreaterThan:
        // 值大于指定值(整数比较)
        if !ls.Has(r.key) {
            return false
        }
        lsValue, _ := strconv.ParseInt(ls.Get(r.key), 10, 64)
        rValue, _ := strconv.ParseInt(r.strValues.List()[0], 10, 64)
        return lsValue > rValue

    case selection.LessThan:
        // 值小于指定值(整数比较)
        if !ls.Has(r.key) {
            return false
        }
        lsValue, _ := strconv.ParseInt(ls.Get(r.key), 10, 64)
        rValue, _ := strconv.ParseInt(r.strValues.List()[0], 10, 64)
        return lsValue < rValue
    }
    return false
}

// 多个条件之间是 AND 关系
func (s internalSelector) Matches(l Labels) bool {
    for ix := range s {
        if matches := s[ix].Matches(l); !matches {
            return false  // 任一条件不满足即失败
        }
    }
    return true
}

关键语义:所有 Requirement 之间是**逻辑与(AND)**关系,不支持 OR。如需 OR 语义,需要在应用层组合多个查询结果。

2.4 Informer 自定义索引加速 Label 查询

// 为 Informer 添加自定义索引
const AppIndexName = "byApp"

func AppIndexFunc(obj interface{}) ([]string, error) {
    pod, ok := obj.(*v1.Pod)
    if !ok {
        return nil, fmt.Errorf("not a pod")
    }
    app, exists := pod.Labels["app"]
    if !exists {
        return []string{""}, nil
    }
    return []string{app}, nil
}

// 注册索引
informer.AddIndexers(cache.Indexers{
    AppIndexName: AppIndexFunc,
})

// 使用索引快速查询(O(1) vs 全量遍历 O(n))
pods, _ := informer.GetIndexer().ByIndex(AppIndexName, "nginx")

三、Service:服务抽象层

3.1 Service 的核心问题

在 Kubernetes 中,Pod 是临时性实体——随时可能被销毁和重建,IP 地址不断变化。Service 解决的核心问题是:为一组动态变化的 Pod 提供稳定的访问入口

3.2 Service 的四种类型

┌──────────────────────────────────────────────────────────────────────┐
│                          Service Types                                │
│                                                                       │
│  ┌─── ClusterIP ────────────────────────────────────────────────┐    │
│  │  默认类型,仅集群内部可访问                                    │    │
│  │  分配虚拟 IP(VIP),kube-proxy 负责流量转发                  │    │
│  │  适用场景:微服务间通信                                       │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                       │
│  ┌─── NodePort ─────────────────────────────────────────────────┐    │
│  │  在 ClusterIP 基础上,在每个节点开放静态端口                   │    │
│  │  端口范围:30000-32767(通过 --service-node-port-range 配置) │    │
│  │  流量路径:NodeIP:NodePort → ClusterIP:Port → Pod:TargetPort │    │
│  │  适用场景:开发测试、简单外部访问                              │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                       │
│  ┌─── LoadBalancer ─────────────────────────────────────────────┐    │
│  │  在 NodePort 基础上,自动配置云厂商负载均衡器                  │    │
│  │  流量路径:LB → NodeIP:NodePort → ClusterIP:Port → Pod      │    │
│  │  适用场景:生产环境外部流量入口                                │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                       │
│  ┌─── ExternalName ─────────────────────────────────────────────┐    │
│  │  不分配 ClusterIP,返回 CNAME 记录                            │    │
│  │  用于将集群内 Service 名映射到外部 DNS 域名                   │    │
│  │  适用场景:访问集群外部服务(RDS、SaaS API)                  │    │
│  └──────────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────────┘

3.3 ClusterIP 分配机制

ClusterIP 从 --service-cluster-ip-range(默认 10.96.0.0/12)中分配:

// pkg/registry/core/service/ipallocator/bitmap.go
type Range struct {
    net  *net.IPNet
    base net.IP     // 网段基地址
    max  int        // 最大可分配 IP 数
    used bit.Alloc  // 位图标记已分配 IP
}

func (r *Range) Allocate(ip net.IP) error {
    offset := calculateIPOffset(r.base, ip)
    if r.used.Has(offset) {
        return ErrAllocated  // 已被分配
    }
    r.used.Set(offset)
    return nil
}

func (r *Range) AllocateNext() (net.IP, error) {
    // 从位图中找到下一个未分配的位
    offset, ok := r.used.AllocateNext()
    if !ok {
        return nil, ErrFull  // IP 池耗尽
    }
    return addIPOffset(r.base, offset), nil
}

特殊 IP10.96.0.1 通常保留给 kubernetes 默认 Service(API Server)。10.96.0.10 通常保留给 kube-dns Service。

3.4 Service 完整配置示例

apiVersion: v1
kind: Service
metadata:
  name: web-service
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: web
    version: v2
  ports:
  - name: http
    protocol: TCP
    port: 80          # Service 暴露的端口
    targetPort: 8080   # Pod 上的实际端口
  - name: https
    protocol: TCP
    port: 443
    targetPort: 8443
  sessionAffinity: ClientIP          # 会话保持
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800          # 会话保持时间 3 小时
  ipFamilyPolicy: PreferDualStack    # 双栈偏好
  ipFamilies:
  - IPv4
  - IPv6
  internalTrafficPolicy: Local       # 内部流量策略:优先本节点 Pod
  externalTrafficPolicy: Local       # 外部流量策略(NodePort/LB 类型)

3.5 externalTrafficPolicy 的工程权衡

externalTrafficPolicy: Cluster(默认)
┌─────────────────────────────────────────────────┐
│  优点:负载均匀分布到所有 Pod                    │
│  缺点:额外一跳 SNAT,源 IP 丢失                │
│                                                   │
│  Client → Node1:30080 ──SNAT──► Node2 Pod        │
│                           │                       │
│              源 IP 变为 Node1 IP                  │
└─────────────────────────────────────────────────┘

externalTrafficPolicy: Local
┌─────────────────────────────────────────────────┐
│  优点:保留客户端源 IP,减少跨节点跳转           │
│  缺点:如果本节点无 Pod,流量被丢弃              │
│        负载可能不均匀(取决于 Pod 分布)          │
│                                                   │
│  Client → Node1:30080 ──直达──► Node1 Pod        │
│  Client → Node2:30080 ──丢弃──► (无本地 Pod)     │
│                                                   │
│  解决方案:配合 Pod Anti-Affinity 确保每个节点    │
│  都有 Pod,或使用 LB 健康检查自动摘除无 Pod 节点 │
└─────────────────────────────────────────────────┘

四、Endpoints 与 EndpointSlice

4.1 Endpoints 对象

每个 Service 都有一个同名的 Endpoints 对象,记录了所有匹配 Pod 的 IP 和端口:

apiVersion: v1
kind: Endpoints
metadata:
  name: web-service       # 与 Service 同名
  namespace: default
subsets:
- addresses:              # Ready 的 Pod
  - ip: 172.16.1.5
    nodeName: node-1
    targetRef:
      kind: Pod
      name: web-abc123
      namespace: default
  - ip: 172.16.2.8
    nodeName: node-2
    targetRef:
      kind: Pod
      name: web-def456
      namespace: default
  notReadyAddresses:      # 未 Ready 的 Pod
  - ip: 172.16.3.2
    nodeName: node-3
    targetRef:
      kind: Pod
      name: web-ghi789
      namespace: default
  ports:
  - name: http
    port: 8080
    protocol: TCP

4.2 Endpoints Controller 工作原理

// pkg/controller/endpoint/endpoints_controller.go
func (e *Controller) syncService(ctx context.Context, key string) error {
    service, _ := e.serviceLister.Services(namespace).Get(name)

    // 获取 Service 的 Label Selector
    selector := labels.Set(service.Spec.Selector).AsSelectorPreValidated()

    // 列出匹配的 Pod
    pods, _ := e.podLister.Pods(service.Namespace).List(selector)

    subsets := []v1.EndpointSubset{}
    for _, pod := range pods {
        ep := podToEndpointAddress(pod)

        if podutil.IsPodReady(pod) {
            // Pod Ready → 加入 addresses
            epa = append(readyEps, ep)
        } else {
            // Pod Not Ready → 加入 notReadyAddresses
            // 除非 Service 设置了 publishNotReadyAddresses: true
            if service.Spec.PublishNotReadyAddresses {
                epa = append(readyEps, ep)
            } else {
                epa = append(notReadyEps, ep)
            }
        }
    }

    // 创建或更新 Endpoints 对象
    currentEndpoints, _ := e.client.CoreV1().Endpoints(namespace).Get(ctx, name, metav1.GetOptions{})
    newEndpoints := currentEndpoints.DeepCopy()
    newEndpoints.Subsets = subsets
    e.client.CoreV1().Endpoints(namespace).Update(ctx, newEndpoints, metav1.UpdateOptions{})
}

4.3 EndpointSlice:大规模集群的解决方案

Endpoints 的瓶颈:一个 Endpoints 对象包含 Service 的所有后端 Pod。当 Service 有数千个 Pod 时,每次 Pod 变化都需要更新整个 Endpoints 对象——巨大的序列化开销 + Watch 事件风暴。

EndpointSlice(v1.21 GA)将 Endpoints 分片:

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: web-service-abc12     # 自动生成的分片名
  labels:
    kubernetes.io/service-name: web-service
    endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
addressType: IPv4
ports:
- name: http
  port: 8080
  protocol: TCP
endpoints:
- addresses: ["172.16.1.5"]
  conditions:
    ready: true
    serving: true
    terminating: false
  nodeName: node-1
  zone: us-east-1a           # 拓扑信息
  targetRef:
    kind: Pod
    name: web-abc123
- addresses: ["172.16.2.8"]
  conditions:
    ready: true
  nodeName: node-2
  zone: us-east-1b
# ... 每个 EndpointSlice 最多 100 个 endpoint(可配置)

性能对比(1000 Pod 的 Service,1 个 Pod 变化):

指标 Endpoints EndpointSlice
更新对象大小 ~100KB(全量) ~1KB(单个分片)
Watch 事件大小 ~100KB ~1KB
kube-proxy 处理时间 ~50ms ~1ms
etcd 写入压力 极低

五、kube-proxy:Service 流量转发引擎

5.1 三种代理模式

5.1.1 iptables 模式(默认)
# 查看 kube-proxy 生成的 iptables 规则
iptables -t nat -L KUBE-SERVICES -n

# Service ClusterIP 入口规则
-A KUBE-SERVICES -d 10.96.100.50/32 -p tcp -m tcp --dport 80 \
   -j KUBE-SVC-XXXXXX

# 负载均衡:等概率随机选择后端
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.33333 \
   -j KUBE-SEP-AAAAAA
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.50000 \
   -j KUBE-SEP-BBBBBB
-A KUBE-SVC-XXXXXX \
   -j KUBE-SEP-CCCCCC

# 单个后端 Pod 的 DNAT 规则
-A KUBE-SEP-AAAAAA -p tcp -j DNAT --to-destination 172.16.1.5:8080
-A KUBE-SEP-BBBBBB -p tcp -j DNAT --to-destination 172.16.2.8:8080
-A KUBE-SEP-CCCCCC -p tcp -j DNAT --to-destination 172.16.3.2:8080

概率计算逻辑:3 个后端时,第一条规则概率 1/3,第二条概率 1/2(在未匹配第一条的 2/3 中选,2/3 × 1/2 = 1/3),第三条兜底(概率 1/3)。

iptables 模式的局限性

问题 影响
规则数量 O(n) 增长 1000 Service × 10 Pod = 数万条规则,iptables-restore 耗时数秒
规则更新非增量 每次变化需全量刷新所有规则
无连接级负载均衡 概率随机,无法感知后端负载
无健康检查 后端不健康时仍可能被选中(依赖 readinessProbe 摘除 Endpoint)
5.1.2 IPVS 模式(推荐大规模集群)
# 启用 IPVS 模式
# kube-proxy 配置:mode: "ipvs"

# 查看 IPVS 规则
ipvsadm -Ln

# 输出示例
TCP  10.96.100.50:80 rr                    # rr = Round Robin
  -> 172.16.1.5:8080    Masq    1    0    0
  -> 172.16.2.8:8080    Masq    1    0    0
  -> 172.16.3.2:8080    Masq    1    0    0

IPVS 的核心优势

┌─────────────────────────────────────────────────────────────────┐
│  IPVS vs iptables 性能对比                                      │
│                                                                   │
│  查找算法:                                                      │
│    iptables: O(n) 线性遍历规则链                                │
│    IPVS:     O(1) 哈希表查找                                    │
│                                                                   │
│  规则更新:                                                      │
│    iptables: 全量替换(iptables-restore)                       │
│    IPVS:     增量更新(ipvsadm --add / --delete)               │
│                                                                   │
│  负载均衡算法(IPVS 支持多种):                                │
│    - rr:  Round Robin                                            │
│    - lc:  Least Connection                                       │
│    - dh:  Destination Hashing                                    │
│    - sh:  Source Hashing                                         │
│    - sed: Shortest Expected Delay                                │
│    - nq:  Never Queue                                            │
│                                                                   │
│  10000 Service 场景下的延迟对比:                                │
│    iptables: 规则刷新 ~11s,首包延迟 ~5ms                       │
│    IPVS:     规则刷新 ~0.1s,首包延迟 ~0.2ms                    │
└─────────────────────────────────────────────────────────────────┘

IPVS 模式的实现细节

kube-proxy 在 IPVS 模式下仍然需要少量 iptables 规则处理 SNAT 和 MASQUERADE:

// pkg/proxy/ipvs/proxier.go
func (proxier *Proxier) syncProxyRules() {
    // 1. 确保 dummy 接口存在(绑定 ClusterIP)
    proxier.netlinkHandle.EnsureDummyDevice(defaultDummyDevice)

    // 2. 将 ClusterIP 绑定到 dummy 接口
    for _, svc := range proxier.serviceMap {
        proxier.netlinkHandle.EnsureAddressBind(svc.ClusterIP(), defaultDummyDevice)
    }

    // 3. 创建/更新 IPVS Virtual Server
    for _, svc := range proxier.serviceMap {
        vs := &utilipvs.VirtualServer{
            Address:  svc.ClusterIP(),
            Port:     uint16(svc.Port()),
            Protocol: string(svc.Protocol()),
            Scheduler: proxier.ipvsScheduler,
        }
        proxier.ipvs.AddVirtualServer(vs)

        // 4. 添加 Real Server(后端 Pod)
        for _, ep := range proxier.endpointsMap[svcName] {
            rs := &utilipvs.RealServer{
                Address: ep.IP(),
                Port:    uint16(ep.Port()),
                Weight:  1,
            }
            proxier.ipvs.AddRealServer(vs, rs)
        }
    }
}

5.2 拓扑感知路由(Topology Aware Routing)

apiVersion: v1
kind: Service
metadata:
  name: web-service
  annotations:
    service.kubernetes.io/topology-mode: Auto  # v1.27+ 替代 topologyKeys
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 8080

Auto 模式的路由逻辑

1. 优先将流量路由到同可用区(Zone)的 Pod
2. 如果同 Zone 的 Pod 不足以承担流量,自动溢出到其他 Zone
3. EndpointSlice Controller 为每个 endpoint 分配 hints(zone 信息)
4. kube-proxy 根据 hints 只将本 Zone 的 endpoint 加入负载均衡规则

六、CoreDNS:集群 DNS 解析引擎

6.1 DNS 记录生成规则

Kubernetes 中的 DNS 记录遵循严格的命名规范:

┌─────────────────────────────────────────────────────────────────────────┐
│  Service DNS 记录                                                       │
│                                                                          │
│  普通 Service (ClusterIP):                                              │
│    A/AAAA:  <service>.<namespace>.svc.cluster.local → ClusterIP         │
│    SRV:     _<port>._<protocol>.<service>.<ns>.svc.cluster.local        │
│                                                                          │
│  Headless Service (clusterIP: None):                                    │
│    A/AAAA:  <service>.<namespace>.svc.cluster.local → [Pod IPs]         │
│    每个 Pod: <pod-name>.<service>.<namespace>.svc.cluster.local → PodIP │
│                                                                          │
│  ExternalName Service:                                                   │
│    CNAME:   <service>.<namespace>.svc.cluster.local → spec.externalName │
│                                                                          │
├─────────────────────────────────────────────────────────────────────────┤
│  Pod DNS 记录                                                            │
│                                                                          │
│  默认:  <pod-ip-dashed>.<namespace>.pod.cluster.local                   │
│  例如:  172-16-1-5.default.pod.cluster.local                            │
│                                                                          │
│  StatefulSet Pod:                                                        │
│    <pod-name>.<service>.<namespace>.svc.cluster.local                   │
│    例如: mysql-0.mysql.default.svc.cluster.local                        │
└─────────────────────────────────────────────────────────────────────────┘

6.2 CoreDNS 配置解析

CoreDNS 的配置通过 ConfigMap kube-system/coredns 管理:

.:53 {
    errors                          # 错误日志
    health {                        # 健康检查端点 :8080/health
        lameduck 5s                 # 优雅关闭等待 5s
    }
    ready                           # 就绪检查端点 :8181/ready
    
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        # 监听 Kubernetes API 获取 Service/Pod 信息
        pods insecure               # 启用 Pod DNS 记录(insecure 模式不验证 Pod 存在性)
        fallthrough in-addr.arpa ip6.arpa  # 未匹配时传递给下一个插件
        ttl 30                      # DNS 记录 TTL 30 秒
    }
    
    prometheus :9153                # Prometheus 指标端点
    
    forward . /etc/resolv.conf {   # 非集群域名转发到上游 DNS
        max_concurrent 1000        # 最大并发转发数
    }
    
    cache 30                        # DNS 缓存 30 秒
    loop                            # 检测死循环
    reload                          # 自动重载配置
    loadbalance                     # DNS 响应中的 A 记录随机排序
}

6.3 Pod 的 DNS 配置

# Pod 的 /etc/resolv.conf 内容(kubelet 自动生成)
nameserver 10.96.0.10    # kube-dns Service 的 ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5           # 关键参数!

# DNS 查询流程示例:
# 应用请求 "mysql" → ndots:5 表示如果域名中 '.' 的数量 < 5,先尝试补全搜索域
# 1. mysql.default.svc.cluster.local    ← 命中
# 2. mysql.svc.cluster.local            ← 如果 1 未命中
# 3. mysql.cluster.local                ← 如果 2 未命中
# 4. mysql                              ← 最后尝试绝对域名

ndots:5 的性能影响

查询外部域名(如 api.github.com,有 2 个点 < 5)时:
1. api.github.com.default.svc.cluster.local  → NXDOMAIN
2. api.github.com.svc.cluster.local          → NXDOMAIN
3. api.github.com.cluster.local              → NXDOMAIN
4. api.github.com                            → 成功

每次外部域名查询产生 4 次 DNS 请求!且 A + AAAA 双查询,实际为 8 次。

优化方案

spec:
  dnsConfig:
    options:
    - name: ndots
      value: "2"              # 降低 ndots 减少无效查询
    - name: single-request-reopen
      value: ""               # A 和 AAAA 查询使用不同源端口,避免 conntrack 竞争
  # 或者在代码中使用 FQDN:
  # 用 mysql.default.svc.cluster.local. 代替 mysql(注意末尾的点)

6.4 CoreDNS 缓存与性能调优

# CoreDNS 缓存配置
cache {
    success 9984 30    # 成功响应缓存 30s,最多 9984 条
    denial 9984 5      # NXDOMAIN 缓存 5s,最多 9984 条
    prefetch 10 60s 10%  # 当某条目在 60s 内被查询 10 次,提前刷新
}

大规模集群 DNS 调优策略

┌─────────────────────────────────────────────────────────────────┐
│  CoreDNS 性能优化矩阵                                          │
│                                                                   │
│  1. NodeLocal DNSCache(推荐)                                   │
│     在每个节点运行 DNS 缓存(DaemonSet)                        │
│     Pod → NodeLocal DNSCache → CoreDNS                          │
│     优势:减少 CoreDNS 负载、降低 DNS 延迟、避免 conntrack 竞争 │
│                                                                   │
│  2. AutoPath 插件                                                │
│     autopath @kubernetes                                         │
│     服务端自动展开搜索域,一次查询返回结果                       │
│     减少客户端多次 DNS 查询                                      │
│                                                                   │
│  3. 水平扩展                                                     │
│     kubectl scale deployment coredns -n kube-system --replicas=5│
│     或配合 cluster-proportional-autoscaler 自动扩缩              │
│     建议比例:每 256 个 Pod 一个 CoreDNS 副本                   │
│                                                                   │
│  4. 协议优化                                                     │
│     DNS-over-TCP(避免 UDP conntrack 问题)                      │
│     Pod dnsConfig 中设置 use-vc 选项                            │
└─────────────────────────────────────────────────────────────────┘

七、Service 发现全链路流程

7.1 从创建到可访问的完整流程

1. kubectl create service → API Server 写入 etcd
       │
2.     ├─► ClusterIP Allocator 分配虚拟 IP(10.96.x.x)
       │
3.     ├─► Endpoints Controller Watch 到新 Service
       │     → 查找匹配 selector 的 Pod
       │     → 创建/更新 Endpoints 对象
       │
4.     ├─► EndpointSlice Controller Watch 到新 Service
       │     → 创建 EndpointSlice 分片对象
       │
5.     ├─► kube-proxy Watch 到 Service + Endpoints/EndpointSlice
       │     → 在每个节点上更新 iptables/IPVS 规则
       │     → ClusterIP 到 Pod IP 的 DNAT 规则生效
       │
6.     └─► CoreDNS Watch 到新 Service
             → 生成 DNS A/AAAA/SRV 记录
             → <service>.<namespace>.svc.cluster.local 可解析

7. Pod 内应用:
   curl http://web-service  
   → DNS 解析: web-service → web-service.default.svc.cluster.local → 10.96.100.50
   → kube-proxy DNAT: 10.96.100.50:80 → 172.16.1.5:8080
   → 网络转发: CNI 路由到目标 Pod
   → 应用响应

7.2 环境变量 vs DNS 发现

Kubernetes 提供两种服务发现机制:

# 1. 环境变量(kubelet 注入,仅限先于 Pod 创建的 Service)
WEB_SERVICE_SERVICE_HOST=10.96.100.50
WEB_SERVICE_SERVICE_PORT=80
WEB_SERVICE_PORT=tcp://10.96.100.50:80
WEB_SERVICE_PORT_80_TCP=tcp://10.96.100.50:80
WEB_SERVICE_PORT_80_TCP_PROTO=tcp
WEB_SERVICE_PORT_80_TCP_PORT=80
WEB_SERVICE_PORT_80_TCP_ADDR=10.96.100.50

# 2. DNS(推荐,动态发现)
# 在 Pod 内直接使用 Service 名称
curl http://web-service          # 同 Namespace
curl http://web-service.prod     # 跨 Namespace
curl http://web-service.prod.svc.cluster.local  # 完全限定域名

工程建议:始终使用 DNS 发现而非环境变量。环境变量有创建顺序依赖(Service 必须先于 Pod 创建),且无法动态更新。在大规模集群中,大量环境变量还会影响 Pod 启动性能。

八、Headless Service 深度解析

8.1 Headless Service 的工作原理

apiVersion: v1
kind: Service
metadata:
  name: cassandra
spec:
  clusterIP: None        # Headless:不分配 ClusterIP
  selector:
    app: cassandra
  ports:
  - port: 9042

与普通 Service 的核心区别

特性 ClusterIP Service Headless Service
ClusterIP 分配虚拟 IP None
DNS A 记录 返回 ClusterIP 返回所有 Pod IP
kube-proxy 规则 创建 DNAT 规则 不创建任何规则
负载均衡 kube-proxy 层面 客户端决定(DNS 轮询或自定义)
适用场景 无状态服务 有状态服务(数据库集群、消息队列)
# DNS 查询 Headless Service
nslookup cassandra.default.svc.cluster.local

# 返回所有 Pod IP(而非 ClusterIP)
Name:    cassandra.default.svc.cluster.local
Address: 172.16.1.5
Address: 172.16.2.8
Address: 172.16.3.2

8.2 Headless Service 的典型应用

# Cassandra 集群发现
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
spec:
  serviceName: cassandra    # 关联 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      containers:
      - name: cassandra
        image: cassandra:4.1
        env:
        - name: CASSANDRA_SEEDS
          # 利用稳定 DNS 名称发现种子节点
          value: "cassandra-0.cassandra.default.svc.cluster.local"
        ports:
        - containerPort: 9042
          name: cql
        - containerPort: 7000
          name: intra-node
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 100Gi

九、生产环境工程实践总结

9.1 Label 命名规范

# 推荐使用 Kubernetes 官方推荐标签
labels:
  app.kubernetes.io/name: mysql          # 应用名称
  app.kubernetes.io/instance: mysql-prod # 实例名
  app.kubernetes.io/version: "8.0.35"    # 版本
  app.kubernetes.io/component: database  # 组件
  app.kubernetes.io/part-of: ecommerce   # 所属系统
  app.kubernetes.io/managed-by: helm     # 管理工具

9.2 Service 配置检查清单

检查项 建议 原因
selector 是否精确 同时匹配 appversion 避免跨版本流量混乱
externalTrafficPolicy 需要源 IP 时设为 Local Cluster 模式会丢失源 IP
internalTrafficPolicy 延迟敏感服务设为 Local 优先本节点转发,降低网络延迟
DNS ndots 外部调用多时降低到 2 减少无效 DNS 查询
publishNotReadyAddresses StatefulSet 的 Headless Service 设为 true 集群发现场景需要在启动阶段互相发现
Session Affinity 有状态连接使用 ClientIP 确保同一客户端访问同一 Pod
EndpointSlice 确认 kube-proxy 使用 EndpointSlice v1.21+ 默认启用,大规模集群必须确认
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐