Files
Qiniu/technical/k8s/k8s-05-service-and-networking.md
T

642 lines
19 KiB
Markdown
Raw Normal View History

2026-07-07 15:55:47 +08:00
---
tags: [k8s, service, networking, devops]
create time: 2026-07-07 15:48
---
# Kubernetes Service 与网络
## 概述
Kubernetes 中 Pod 的生命周期是短暂的——它们随时可能被销毁重建,每次重建都会获得新的 IP 地址。如果服务之间直接通过 Pod IP 通信,任何一次 Pod 重启都会导致调用方断连。**Service** 正是为了解决这个问题而生:它为一组动态变化的 Pod 提供一个稳定的访问入口(虚拟 IP),配合 DNS 和负载均衡,让服务发现变得透明且可靠。
## K8s 网络模型
Kubernetes 对集群网络有三个核心要求,任何 CNI(Container Network Interface)插件都必须满足:
| 原则 | 说明 |
|------|------|
| **Pod-to-Pod** | 集群内任意两个 Pod 可以直接通过 IP 通信,无需 NAT |
| **Pod-to-Service** | Pod 通过 Service 的虚拟 IP(ClusterIP)访问后端 Pod,由 kube-proxy 实现负载均衡 |
| **External-to-Service** | 集群外部流量可以通过 NodePort、LoadBalancer 或 Ingress 进入集群 |
### CNI 插件简介
CNI 插件负责为 Pod 分配 IP 地址并配置网络连通性。常见实现:
- **Calico**:基于 BGP 路由,支持 NetworkPolicy,性能优秀
- **Flannel**:简单轻量,使用 VXLAN overlay,适合小规模集群
- **Cilium**:基于 eBPF,高性能,支持 L7 网络策略
- **Weave Net**:自带加密,配置简单
> [!tip] 如何选择
> 小规模或学习环境用 Flannel 足够;生产环境推荐 Calico 或 Cilium,它们对 NetworkPolicy 的支持更完善。
## Service 详解
### ClusterIP
**默认类型**,为 Service 分配一个集群内部虚拟 IP,仅集群内可访问。
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
namespace: default
spec:
type: ClusterIP # 默认值,可省略
selector:
app: my-app
ports:
- protocol: TCP
port: 80 # Service 端口
targetPort: 8080 # Pod 端口
```
流量路径:`Client Pod → ClusterIP(虚拟 IP)→ kube-proxy → 后端 Pod:8080`
ClusterIP 是一个"虚拟 IP"——它不存在于任何网卡上,只存在于 iptables/IPVS 规则中。
### NodePort
在 ClusterIP 的基础上,在**每个节点**上开一个端口(默认范围 30000-32767),外部流量可通过 `<NodeIP>:<NodePort>` 访问。
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-nodeport
spec:
type: NodePort
selector:
app: my-app
ports:
- protocol: TCP
port: 80 # Service 端口(集群内部访问)
targetPort: 8080 # Pod 端口
nodePort: 30080 # 节点端口(可选,不指定则自动分配)
```
> [!info] iptables 规则链路
> 当外部请求到达 `<NodeIP>:30080` 时,kube-proxy 在每个节点上创建的 iptables 规则会将流量 DNAT 到后端 Pod 的 IP:Port。这意味着即使请求落在没有运行目标 Pod 的节点上,流量也会被正确转发。
```mermaid
flowchart LR
A[External Client] -->|NodeIP:30080| B[Node]
B -->|iptables DNAT| C[Pod on Node A]
B -->|iptables DNAT| D[Pod on Node B]
B -->|iptables DNAT| E[Pod on Node C]
```
### LoadBalancer
在 NodePort 的基础上,向云厂商申请一个外部负载均衡器(如 AWS ELB、GCP CLB),自动将外部流量分发到各节点的 NodePort。
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-lb
annotations:
# 云厂商特定注解,例如 AWS:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
```
> [!warning] 继承关系
> `LoadBalancer` = `ClusterIP` + `NodePort` + 云厂商 LB。如果没有云厂商支持(如裸金属环境),LoadBalancer 类型的 Service 会一直停留在 `<pending>` 状态。此时可以考虑 MetalLB 作为替代方案。
### ExternalName
将 Service 映射到一个外部 DNS 名称(CNAME 记录),不涉及任何代理和负载均衡。
```yaml
apiVersion: v1
kind: Service
metadata:
name: external-db
namespace: default
spec:
type: ExternalName
externalName: db.example.com # 映射到外部域名
```
**适用场景**:
- 集群内应用需要访问外部数据库或 API
- 跨命名空间/跨集群服务映射
- 迁移过程中临时将内部 Service 指向外部服务
当 Pod 查询 `external-db.default.svc.cluster.local` 时,CoreDNS 会返回 `db.example.com` 的 CNAME 记录,Pod 再对 `db.example.com` 发起真正的 DNS 查询。
### Service 类型对比
| 类型 | ClusterIP | NodePort | LoadBalancer | ExternalName |
|------|-----------|----------|--------------|--------------|
| **默认** | 是 | 否 | 否 | 否 |
| **集群内访问** | 支持 | 支持 | 支持 | 通过 CNAME |
| **集群外访问** | 不支持 | 支持(NodeIP:Port) | 支持(外部 IP) | 不适用 |
| **端口范围** | 任意 | 30000-32767 | 任意(LB 端口) | 无端口 |
| **典型场景** | 内部微服务互调 | 开发测试暴露 | 生产环境对外服务 | 访问外部服务 |
| **云厂商依赖** | 无 | 无 | 需要 | 无 |
## Endpoints 与 EndpointSlices
### Endpoints
每个 Service 背后都有一个同名的 **Endpoints** 对象,记录了当前所有健康 Pod 的 IP:Port 列表。当 Pod 就绪(Readiness Probe 通过)时自动加入,Pod 删除时自动移除。
```bash
# 查看 Service 对应的 Endpoints
kubectl get endpoints my-app-svc
# NAME ENDPOINTS AGE
# my-app-svc 10.244.1.5:8080,10.244.2.8:8080 5m
```
### EndpointSlices
Kubernetes 1.21 引入 EndpointSlices 作为 Endpoints 的替代方案。
> [!info] 为什么需要 EndpointSlices?
> 在大规模集群中,一个 Service 可能关联成百上千个 Pod。Endpoints 对象会将所有 Pod IP 存储在单个对象中,导致:
> 1. **更新风暴**:任何一个 Pod 变化都要更新整个 Endpoints 对象
> 2. **内存压力**:kube-proxy 和所有节点的 kubelet 都要 watch 这个大对象
> 3. **API Server 压力**:每次更新都要全量推送
>
> EndpointSlices 将后端列表拆分成多个小切片(默认每个切片最多 100 个端点),增量更新,大幅降低开销。
```bash
# 查看 EndpointSlices
kubectl get endpointslices -l kubernetes.io/service-name=my-app-svc
```
```mermaid
flowchart TB
subgraph Endpoints 旧方式
S1[Service] --> E1[Endpoints<br/>全部 Pod IP 在一个对象中]
end
subgraph EndpointSlices 新方式
S2[Service] --> ES1[EndpointSlice-0<br/>Pod 1-100]
S2 --> ES2[EndpointSlice-1<br/>Pod 101-200]
S2 --> ES3[EndpointSlice-2<br/>Pod 201-300]
end
```
## DNS 解析机制
### CoreDNS
Kubernetes 使用 **CoreDNS** 作为集群内置 DNS 服务(1.12 之前使用 kube-dns)。每个 Service 创建后,CoreDNS 会自动为其注册 DNS 记录。
### Service DNS 格式
| 资源类型 | DNS 格式 | 示例 |
|----------|----------|------|
| 普通 Service | `<svc>.<ns>.svc.cluster.local` | `my-app.default.svc.cluster.local` |
| Headless Service | `<svc>.<ns>.svc.cluster.local` → 解析到所有 Pod IP | 直接返回 Pod IP 列表 |
| Pod | `<pod-ip-dash>.<ns>.pod.cluster.local` | `10-244-1-5.default.pod.cluster.local` |
> [!tip] 同命名空间内可以省略后缀
> 在 `default` 命名空间内的 Pod 访问同命名空间的 Service,可以直接用 `my-app-svc`,无需写全限定域名。
### Headless Service
当 Service 的 `clusterIP` 设置为 `None` 时,它成为一个 **Headless Service**。此时 DNS 查询不会返回一个虚拟 IP,而是直接返回所有后端 Pod 的 IP 列表。
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-headless
spec:
clusterIP: None # Headless
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
```
**典型应用场景**:
- **StatefulSet**:有状态应用(如数据库集群)需要知道每个 Pod 的独立地址
- **自定义负载均衡**:客户端自己实现负载均衡逻辑
- **服务发现**:客户端需要获取所有后端地址
```bash
# 查询 Headless Service 的 DNS
nslookup my-app-headless.default.svc.cluster.local
# Name: my-app-headless.default.svc.cluster.local
# Address: 10.244.1.5
# Address: 10.244.2.8
# Address: 10.244.3.12
```
## kube-proxy 深入
kube-proxy 是运行在每个节点上的网络代理,负责实现 Service 的负载均衡。它有三种工作模式:
### iptables 模式(默认)
kube-proxy 监听 Service 和 Endpoints 的变化,在每个节点上维护 iptables 规则链。
```mermaid
flowchart TB
A[请求到达 ClusterIP] --> B[KUBE-SERVICES 链]
B --> C{匹配 Service?}
C -->|是| D[KUBE-SVC-XXX 链]
D --> E[概率匹配 -m statistic --mode random]
E -->|概率 50%| F[KUBE-SEP-AAA 链 → Pod A]
E -->|概率 50%| G[KUBE-SEP-BBB 链 → Pod B]
C -->|否| H[REJECT]
```
**规则链路详解**:
1. `KUBE-SERVICES`:总入口,匹配目标 IP:Port 确定 Service
2. `KUBE-SVC-XXX`:Service 级别链,使用 `statistic` 模块做随机负载均衡
3. `KUBE-SEP-XXX`:Endpoint 级别链,执行 DNAT 将目标地址转换为 Pod IP:Port
> [!warning] iptables 模式的局限
> - 规则数量与 Service × Pod 数量成正比,大规模集群下规则链很长
> - 线性遍历匹配,时间复杂度 O(n)
> - 不支持连接追踪以外的负载均衡算法
### IPVS 模式
IPVS(IP Virtual Server)是 Linux 内核中的四层负载均衡器,性能远优于 iptables。
```yaml
# kube-proxy 配置启用 IPVS
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
scheduler: "rr" # 负载均衡算法:rr / lc / sh / sed / nq
```
**支持的负载均衡算法**:
| 算法 | 全称 | 说明 |
|------|------|------|
| `rr` | Round Robin | 轮询(默认) |
| `lc` | Least Connections | 最少连接数 |
| `sh` | Source Hashing | 源地址哈希(会话保持) |
| `sed` | Shortest Expected Delay | 最短预期延迟 |
| `nq` | Never Queue | 永不排队 |
### iptables vs IPVS 对比
| 维度 | iptables | IPVS |
|------|----------|------|
| **性能** | 规则多时下降明显 | 哈希表查找,O(1) |
| **最大 Service 数** | 约 5000 开始明显退化 | 可支撑数万 |
| **负载均衡算法** | 仅随机 | rr / lc / sh / sed / nq |
| **连接保持** | 有限支持 | 原生支持 |
| **依赖** | iptables(用户态工具) | ipvs 内核模块 |
| **调试** | `iptables -L -n -t nat` | `ipvsadm -Ln` |
> [!tip] 生产建议
> 节点 Service 数超过 1000 时,强烈建议切换到 IPVS 模式。
## Ingress 基础
Service(NodePort/LoadBalancer)工作在四层(TCP/UDP),而 **Ingress** 工作在七层(HTTP/HTTPS),提供基于域名和路径的路由能力。
### Ingress Controller
Ingress 资源本身只是一份声明式配置,真正执行路由逻辑的是 **Ingress Controller**。常见的 Ingress Controller:
- **Nginx Ingress Controller**:最广泛使用,社区版 + F5 版
- **Traefik**:自动服务发现,配置简单
- **HAProxy**:高性能,企业级特性丰富
- **Istio Gateway**:Service Mesh 生态中的网关
### Path-based 路由
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-based-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- path: /web
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
```
### Host-based 路由
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-based-ingress
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- host: web.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
```
### TLS 终止
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls-secret # 存放证书的 Secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
```
证书需要提前创建为 Kubernetes Secret:
```bash
kubectl create secret tls app-tls-secret \
--cert=tls.crt \
--key=tls.key
```
```mermaid
flowchart LR
A[Client] -->|HTTPS| B[Ingress Controller<br/>TLS 终止]
B -->|HTTP| C[Service: my-app-svc]
C --> D[Pod A]
C --> E[Pod B]
```
## NetworkPolicy
默认情况下,Kubernetes 集群中所有 Pod 之间可以互相通信。**NetworkPolicy** 允许你定义精细的网络访问控制规则。
> [!warning] 前提条件
> NetworkPolicy 需要 CNI 插件支持。Flannel 不支持 NetworkPolicy,Calico 和 Cilium 支持。
### 默认全通策略
如果不创建任何 NetworkPolicy,所有流量都是放行的:
```mermaid
flowchart LR
A[Pod A] -->|允许| B[Pod B]
B -->|允许| A
A -->|允许| C[Pod C]
C -->|允许| B
```
### 限制 Pod 间通信
**示例:只允许前端 Pod 访问后端 Pod 的 8080 端口**
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: default
spec:
podSelector:
matchLabels:
app: backend # 策略作用于 backend Pod
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # 只允许来自 frontend Pod
ports:
- protocol: TCP
port: 8080 # 只允许访问 8080 端口
```
**示例:限制出站流量,只允许访问指定 DNS**
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: restrict-egress
namespace: default
spec:
podSelector:
matchLabels:
app: my-app
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: kube-system # 允许访问 kube-system 命名空间
ports:
- protocol: UDP
port: 53 # 允许 DNS 查询
- to:
- ipBlock:
cidr: 10.0.0.0/8 # 允许访问内网
ports:
- protocol: TCP
port: 443
```
> [!info] 策略叠加规则
> - 一旦某个 Pod 被任何 NetworkPolicy 选中,未被策略明确允许的流量将被**拒绝**
> - 多个 NetworkPolicy 取**并集**(任一策略允许即放行)
> - 只定义 `policyTypes: [Ingress]` 不影响出站,反之亦然
```mermaid
flowchart TB
subgraph 策略生效后
A[frontend Pod] -->|允许: TCP 8080| B[backend Pod]
C[other Pod] -->|拒绝| B
B -->|未定义 Egress 策略<br/>默认放行| D[任意目标]
end
```
## 常见陷阱与最佳实践
### Service 无法访问排查清单
当 Service 访问不通时,按以下顺序排查:
1. **确认 Pod 运行正常**
```bash
kubectl get pods -l app=my-app -o wide
kubectl logs <pod-name>
kubectl describe pod <pod-name>
```
2. **确认 Endpoints 已注册**
```bash
kubectl get endpoints my-app-svc
# 如果 ENDPOINTS 为 <none>,说明没有健康的 Pod 匹配 selector
```
3. **确认 selector 匹配**
```bash
# 对比 Service selector 和 Pod labels
kubectl get svc my-app-svc -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels
```
4. **确认端口正确**
```bash
# 检查 targetPort 是否与 Pod 实际监听端口一致
kubectl get svc my-app-svc -o yaml | grep -A5 ports
kubectl exec <pod-name> -- ss -tlnp
```
5. **确认 DNS 解析**
```bash
kubectl run test --rm -it --image=busybox -- nslookup my-app-svc
```
6. **确认网络策略未阻断**
```bash
kubectl get networkpolicy -n <namespace>
```
7. **确认 kube-proxy 正常**
```bash
kubectl get pods -n kube-system -l k8s-app=kube-proxy
# 检查 iptables 规则是否存在
iptables -t nat -L KUBE-SERVICES | grep my-app-svc
```
### Session Affinity
默认情况下 Service 使用轮询负载均衡。如果需要会话保持(同一客户端始终访问同一 Pod),可以启用 Session Affinity:
```yaml
apiVersion: v1
kind: Service
metadata:
name: sticky-svc
spec:
selector:
app: my-app
sessionAffinity: ClientIP # 基于客户端 IP 保持会话
sessionAffinityConfig:
clientIP:
timeoutSeconds: 1800 # 会话超时时间(默认 10800 秒)
ports:
- port: 80
targetPort: 8080
```
> [!warning] Session Affinity 注意事项
> - 在 iptables 模式下,基于源 IP 的会话保持可能在 NAT 环境下失效
> - IPVS 模式支持更精细的会话保持(`sh` 算法)
> - 对于 HTTP 应用,更推荐使用 Cookie-based Affinity(在 Ingress 层实现)
### External Traffic Policy
控制外部流量如何路由到 Pod,只对 NodePort 和 LoadBalancer 类型 Service 生效:
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 或 Cluster(默认)
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
```
| 策略 | 行为 | 优点 | 缺点 |
|------|------|------|------|
| **Cluster(默认)** | 流量可能被转发到其他节点上的 Pod | 负载均衡更均匀 | 多一跳转发,丢失源 IP |
| **Local** | 流量只转发到本节点上的 Pod | 保留源 IP,减少转发延迟 | Pod 分布不均时负载不均衡 |
```mermaid
flowchart TB
subgraph "externalTrafficPolicy: Cluster"
A1[Client] -->|请求| B1[Node 1]
B1 -->|可能转发| C1[Node 2 的 Pod]
B1 -->|本地| D1[Node 1 的 Pod]
end
subgraph "externalTrafficPolicy: Local"
A2[Client] -->|请求| B2[Node 1]
B2 -->|仅本地| D2[Node 1 的 Pod]
B2 -.->|不转发| C2[Node 2 的 Pod]
end
```
### 其他最佳实践
- **使用命名空间隔离 Service**:避免命名冲突,便于权限管理
- **合理设置 readinessProbe**:Pod 未就绪时不会被加入 Endpoints,避免流量打到未准备好的实例
- **避免使用 ExternalName 映射到集群内部 Service**:可能导致 DNS 解析循环
- **大规模集群启用 EndpointSlices**:减少 API Server 和 kube-proxy 的压力
- **生产环境使用 IPVS 模式**:在 Service 数量较多时性能显著优于 iptables
- **为关键 Service 配置 PodDisruptionBudget**:确保滚动更新和节点维护时服务可用性
## 延伸阅读
- [[k8s-01-overview]] — Kubernetes 基础概念总览
- [[k8s-04-deployment-and-replicaset]] — Deployment 与 ReplicaSet 详解