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

642 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 详解