嘿,朋友,坐下来聊聊天。
如果你刚接触Kubernetes,可能会被它的网络模型搞得有点晕头转向。为什么Pod A能直接ping通Pod B?为什么Service能让后端Pod透明访问?CNI插件又是在哪里起作用的?
别急,我用最通俗的方式,带你一步步拆解Kubernetes的网络世界。咱们就像在搭积木一样,从最底层开始,一层一层往上 build。
一、先搞懂Kubernetes网络模型的核心原则
在深入技术细节之前,咱们得先记住Kubernetes设计者定下的几条”铁律”。这些原则贯穿了整个Kubernetes网络设计,理解了它们,后面的一切都会顺理成章。
原则1:每个Pod都有独立的IP地址
想象一下,每个Pod就像是一个独立的小房子,每栋房子都有自己的门牌号(IP地址)。不管这个房子里住了多少容器,对外看来,整栋房子只有一个门牌号。
Pod 1: 10.244.1.5 ──── 包含容器A + 容器B
Pod 2: 10.244.1.6 ──── 包含容器C
Pod 3: 10.244.2.10 ──── 包含容器D
这意味着什么?意味着Pod间的通信就像两个朋友互相写信,只需要知道对方的”门牌号”就能找到对方。
原则2:所有Pod都能无阻碍地与其他Pod通信
不需要NAT,不需要复杂的映射规则。Pod A可以直接访问Pod B,就像在同一个局域网里一样简单。这是Kubernetes网络模型的基石。
原则3:每个Pod都能与Node上的所有其他Pod通信
无论Pod在哪个节点上,只要在同一集群内,就能互相通信。跨节点、跨机房都没问题。
原则4:Pod IP与容器IP相同
每个Pod里的容器共享同一个网络命名空间,所以它们看到IP是一样的。这就是为什么同一个Pod里的多个容器可以用localhost互相通信。
一个小例子:假设你的Pod里运行了两个容器:一个Web服务器(Nginx)和一个日志收集器(Fluentd)。因为它们在同一个Pod里,共享网络栈,所以Fluentd可以直接通过
localhost:80访问Nginx,而不需要知道Pod的IP地址。
二、这些IP是从哪里来的?CNI插件的魔法
好,现在问题来了:这些IP是谁分配的?怎么分配的?这就轮到我们的主角——CNI插件登场了。
什么是CNI?
CNI全称是Container Network Interface(容器网络接口),它是CNCF(云原生计算基金会)定义的一个标准,用来给容器配置网络。简单来说,CNI就是Kubernetes网络世界的”装修工人”。
当Kubernetes创建一个Pod时,它会调用CNI插件来:
- 给Pod分配IP地址
- 在节点上创建虚拟网络接口
- 配置网络路由
- 设置网络隔离(可选)
CNI的工作流程
让我用一个具体的例子来说明。假设你有一个Kubernetes集群,节点是Node1和Node2,你部署了一个Pod。
用户执行:kubectl run nginx --image=nginx
Kubernetes会做以下几件事:
- 调度Pod:调度器决定把这个Pod放在Node1上
- 创建网络命名空间:在Node1上创建一个Linux network namespace,这就是Pod的网络环境
- 调用CNI插件:Kubernetes告诉CNI插件:”嘿,给这个Pod配置网络”
- CNI插件执行:
- 创建一个虚拟以太网对(veth pair)
- 一端放入Pod的网络命名空间
- 另一端放入节点的network namespace
- 从IP池中分配一个IP地址给Pod
- 配置路由,让Pod能访问其他Pod和外部网络
常见的CNI插件有哪些?
市面上有很多CNI插件,它们就像是不同风格的”装修工人”,各有各的特点:
| 插件名称 | 特点 | 适用场景 |
|---|---|---|
| Calico | 基于BGP路由,性能优秀,支持网络策略 | 大规模集群,需要网络隔离 |
| Flannel | 简单易用,支持VXLAN和host-gw模式 | 小型集群,快速部署 |
| Cilium | 基于eBPF,高性能,支持L7策略 | 需要高级网络策略和可观测性 |
| Weave Net | 支持加密,跨机房部署 | 多机房、混合云场景 |
| Canal | Flannel + Calico的组合 | 需要Flannel的简单性和Calico的策略功能 |
你知道吗? 在AWS EKS中,默认使用的是VPC CNI插件,它直接将Pod的IP地址分配自你的VPC子网,这让Pod可以直接与AWS其他服务通信,而无需NAT。
深入看一个例子:Calico的工作原理
让我用代码和图解的方式,带你看看Calico是怎么工作的。
# 查看当前集群使用的CNI插件
kubectl get pod -n kube-system | grep calico
# 查看Pod的网络配置
kubectl describe pod <pod-name> | grep -A 10 "Network"
当一个Pod被调度到Node1后,Calico会执行以下步骤:
第一步:创建网络命名空间
Kubernetes在Node1上创建一个网络命名空间:
/var/run/netns/pod-12345
第二步:创建veth pair
Calico创建一个虚拟以太网对(veth pair),就像一根虚拟网线:
veth1 (在Pod网络命名空间中) ────── veth2 (在节点网络命名空间中)
第三步:分配IP地址
从IP池中取出一个IP,比如10.244.1.5,分配给Pod。
第四步:配置路由
在节点上添加路由规则,让流量知道如何到达这个Pod:
# 在Node1上
ip route add 10.244.1.5/32 dev cali123
第五步:配置iptables规则(如果启用网络策略)
如果需要网络隔离,Calico会添加iptables规则:
# 示例:只允许来自10.244.0.0/16的流量
iptables -A INPUT -s 10.244.1.5 -j ACCEPT
iptables -A INPUT -s 10.244.1.5 -j DROP
三、跨节点通信:网络是怎么”连”起来的?
好,现在假设你的集群有两个节点:Node1和Node2。
- Node1上的Pod IP是
10.244.1.5 - Node2上的Pod IP是
10.244.2.10
问题来了:Pod1怎么找到Pod2?
这两个Pod不在同一个物理网络里,它们之间是怎么通信的?这就涉及到跨节点网络的设计了。
方案一:VXLAN overlay网络
这是Flannel默认使用的方式。让我用一个比喻来解释:
想象你有两个办公室(Node1和Node2),每个办公室都有自己的局域网。现在你想让两个办公室的员工互相通信,但你不想重新布线。
VXLAN的做法是:在两个办公室之间架一座”空中走廊”。所有的数据包都会被封装在一个VXLAN包里,通过物理网络传输,到达目标节点后再解封,取出原始数据包。
Pod1 (10.244.1.5) ──► Node1 ──► [VXLAN封装] ──► 物理网络 ──► [VXLAN解封] ──► Node2 ──► Pod2 (10.244.2.10)
在Linux内核中,这由vxlan内核模块处理。你可以用以下命令查看:
# 查看VXLAN设备
ip link show vxlan.calico
# 查看VTEP(VXLAN Tunnel End Point)信息
bridge fdb show | grep vxlan
方案二:Host-gw直接路由
这是Flannel的另一种模式,也是Calico默认使用的方式。它更简单、性能更好。
原理是:在每个节点上添加路由规则,告诉系统如何到达其他Pod的网段。
Node1的路由表:
10.244.1.0/24 via 10.0.0.1 dev eth0 (本节点的Pod网段)
10.244.2.0/24 via 10.0.0.2 dev eth0 (Node2的Pod网段)
Node2的路由表:
10.244.2.0/24 via 10.0.0.2 dev eth0 (本节点的Pod网段)
10.244.1.0/24 via 10.0.0.1 dev eth0 (Node1的Pod网段)
这样,当Pod1发送数据包到Pod2时:
- Pod1检查目标IP
10.244.2.10,发现不在本地网段 - 数据包发送到Node1的默认网关
- Node1的路由表告诉它:
10.244.2.0/24可以通过10.0.0.2(Node2的IP)到达 - 数据包通过物理网络发送到Node2
- Node2收到后,解包,发现目标Pod是本地的,直接交给Pod2
这种方式不需要封装,性能更好,但要求底层网络支持路由(即所有节点必须在同一个二层或三层网络中)。
方案三:BGP路由(Calico的进阶用法)
Calico还支持BGP(边界网关协议),让每个节点都参与路由学习。这种方式最适合大规模集群。
Node1 ──BGP──► Node2
Node1 ──BGP──► Node3
Node2 ──BGP──► Node3
每个节点都通过BGP协议交换路由信息,形成一个完整的路由表。这样,Pod之间的通信就像在大型数据中心里一样高效。
# 查看BGP连接状态(如果使用的是Calico)
kubectl get pod -n calico-system
kubectl logs -n calico-system <calico-node-pod> | grep BGP
四、Service:给Pod加一个”固定门牌号”
好,现在假设你的应用跑起来了,有10个Pod副本,每个都有动态变化的IP。问题来了:
客户端怎么知道该连接哪个Pod?
如果Pod IP经常变化(因为故障重启、扩容缩容),客户端配置的地址就会失效。这就是Service存在的意义。
Service是什么?
Service就像是一个负载均衡器,它有一个固定的IP地址(ClusterIP),将流量分发到后端的多个Pod。
┌─────────────┐
│ Service │
│ 10.96.0.10 │
└──────┬──────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Pod 1 │ │ Pod 2 │ │ Pod 3 │
│10.244.1.5│ │10.244.2.10│ │10.244.3.15│
└─────────┘ └─────────┘ └─────────┘
Service的类型
Kubernetes提供了三种主要的Service类型:
1. ClusterIP(默认)
只在集群内部可访问,使用集群内部的虚拟IP。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
2. NodePort
在每个节点上开放一个端口,通过NodeIP:NodePort可以从集群外部访问。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
3. LoadBalancer
在NodePort的基础上,自动创建一个云服务商的负载均衡器(如AWS ALB、GCP LB)。
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
Service背后的机制:kube-proxy
Service能工作,全靠kube-proxy这个幕后英雄。它在每个节点上运行,负责维护网络规则和负载均衡。
kube-proxy有三种工作模式:
1. userspace模式(老模式)
流量经过kube-proxy进程,性能较差,现已不推荐使用。
2. iptables模式
通过iptables规则实现负载均衡:
# 示例:kube-proxy创建的iptables规则
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "default/my-service cluster IP" -m tcp --dport 80 -j KUBE-SVC-XXXX
-A KUBE-SVC-XXXX -m statistic --mode random --probability 0.33333 -j KUBE-SEP-AAA
-A KUBE-SVC-XXXX -m statistic --mode random --probability 0.50000 -j KUBE-SEP-BBB
-A KUBE-SVC-XXXX -j KUBE-SEP-CCC
3. ipvs模式(推荐)
基于Linux内核的IPVS(IP Virtual Server),性能更好,支持更多负载均衡算法:
# 查看IPVS规则
ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.10:80 rr
-> 10.244.1.5:80 Masq 1 0 0
-> 10.244.2.10:80 Masq 1 0 0
-> 10.244.3.15:80 Masq 1 0 0
Service DNS解析:CoreDNS
在Kubernetes中,你不需要记住Service的IP地址。Kubernetes自带DNS服务(CoreDNS或kube-dns),你可以通过Service名称访问。
# 假设你创建了以下Service
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
spec:
selector:
app: nginx
ports:
- port: 80
在集群内,你可以这样访问:
# 方式1:使用短名称
curl http://my-service
# 方式2:使用完整DNS名称
curl http://my-service.default.svc.cluster.local
CoreDNS会自动将my-service解析为Service的ClusterIP(如10.96.0.10)。
# 查看CoreDNS Pod
kubectl get pod -n kube-system | grep coredns
# 在Pod内测试DNS解析
kubectl run -it --rm dns-test --image=busybox --restart=Never -- nslookup my-service
五、端到端通信全景图
好,现在让我们把一切串联起来,看一个完整的通信场景。
场景:Pod A(在Node1上)访问Pod B(在Node2上),通过Service。
┌─────────────────────────────────────────────┐
│ Node1 │
│ │
Pod A │ ┌──────────────────┐ │
10.244.1.5 ──────┼─────►│ kube-proxy │ │
│ │ (iptables/ipvs) │ │
│ └────────┬──────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
│ │ CoreDNS │ │
│ │ 10.96.0.10 │ │
│ └────────┬──────────┘ │
│ │ │
│ ┌────────▼──────────┐ │
└──────┼─── Service ───────┼───────────────────┘
│ 10.96.0.10:80 │
└────────┬──────────┘
│
┌────────▼──────────┐
│ Node2 │
│ │
│ ┌────────────┴────────────┐
│ │ kube-proxy │
│ │ (iptables/ipvs) │
│ └────────────┬────────────┘
│ │
│ ┌────────────▼────────────┐
Pod B ─┼─────►│ Pod B │
10.244.2.10 │ 10.244.2.10 │
└─────────────────────────┘
通信过程详解
