二十、Kubernetes基础-73-kubernetes-label-service-endpoints-dns-deep-dive
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
}
特殊 IP:10.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 是否精确 |
同时匹配 app 和 version |
避免跨版本流量混乱 |
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+ 默认启用,大规模集群必须确认 |
更多推荐




所有评论(0)