嘿,朋友。如果你刚接触 Kubernetes (K8s),看到那一堆 NetworkPolicy、CNI、Service、Ingress 还有满屏的 iptables 规则时,是不是感觉脑子都要炸了?别担心,这种迷茫太正常了。毕竟,K8s 的网络模型设计初衷是为了在成千上万个动态变化的容器中,维持一种“看似简单实则复杂”的一致性。
今天,我们不讲枯燥的教科书定义,而是像剥洋葱一样,一层层揭开 K8s 网络的神秘面纱。我会用大白话,配合真实的排错案例,带你彻底搞懂 Pod 是怎么聊天的,Service 是怎么指路的,以及当网络不通时,我们该如何像个老练的医生一样去“诊断”病情。
第一层洋葱:Pod 之间的“局域网”通信
首先,我们要确立一个核心概念:在 Kubernetes 的世界里,每个 Pod 都拥有一个独立的 IP 地址。
这听起来很简单,但背后的逻辑很强大。K8s 的设计哲学是:“Pod 之间可以像物理机或虚拟机一样直接通信”。这意味着,Pod A 访问 Pod B,不需要知道 Pod B 跑了哪个节点,也不需要关心它是不是重启过,只要知道它的 IP 就行。
1. 为什么 Pod 有自己的 IP?
在 Docker 时代,容器共享宿主机的网络命名空间,或者通过 NAT 映射端口。但在 K8s 中,为了实现上述的“直连”能力,每个 Pod 都被分配了一个唯一的、全局可达的 IP。这个 IP 不是凭空产生的,它是通过 CNI (Container Network Interface) 插件来实现的。
常见的 CNI 插件有 Flannel, Calico, Cilium 等。它们的工作流程大致如下:
- 创建网桥:当节点启动时,CNI 插件会在宿主机上创建一个虚拟网桥(比如
cni0或br0)。 - 分配 IP:当一个新的 Pod 创建时,插件会从预定义的 IP 池中分配一个 IP 给它。
- 连接 veth pair:插件会在宿主机和 Pod 内部各创建一个虚拟网卡(veth pair),并将它们连接起来。Pod 内部的网卡连接到网桥,宿主机的网卡也连接到同一个网桥。
- 配置路由:确保其他节点上的 Pod 可以通过路由找到这个 Pod 的 IP。
2. 跨节点的 Pod 通信:隧道 vs. BGP
如果两个 Pod 在同一个节点上,通信非常简单,数据包直接在网桥内转发。但如果 Pod A 在 Node 1,Pod B 在 Node 2,数据怎么过去?这就涉及到了不同的 CNI 实现策略:
- Flannel (VXLAN 模式):这是最简单的方式。Flannel 会在每个节点上运行一个
flanneld守护进程。当 Node 1 的 Pod 要访问 Node 2 的 Pod 时,数据包会被封装在 VXLAN 包中,通过宿主机的物理网卡发送出去,经过物理网络到达 Node 2,然后被解封装。这种方式对底层网络要求低,但性能稍差,因为多了封装和解封装的开销。 - Calico (BGP 模式):Calico 更高级一些。它利用 BGP (Border Gateway Protocol) 协议,将每个 Pod 的路由信息发布给集群中的其他节点。这样,数据包不需要封装,而是直接通过三层路由转发。性能更好,但需要底层网络支持 BGP。
举个栗子 🌰:
想象一下,你住在 A 小区(Node 1),朋友住在 B 小区(Node 2)。
- Flannel 方式:就像你们俩都住在一个巨大的快递中心里,不管你在哪个房间,快递员(宿主机内核)都会把你的包裹打包成一个标准集装箱(VXLAN),通过高速公路(物理网络)送到对方小区的快递中心,再拆开给你。
- Calico 方式:就像 A 小区和 B 小区之间有直达的地铁线路。你直接从家门口走到地铁站,坐地铁直达朋友家门口,中间没有额外的打包环节,速度更快,但对地铁网络(底层网络)的要求更高。
第二层洋葱:Service 的发现与负载均衡
既然 Pod 有 IP,为什么我们还需要 Service?
因为 Pod 是易失性的。Pod 可能因为调度、故障、扩容等原因随时消失并重建,IP 也会变。如果我们的应用直接依赖 Pod IP,那每次 Pod 重启,客户端都得重新配置,这简直是噩梦。
于是,Service 应运而生。它是一个抽象的概念,定义了一组具有相同功能的 Pod 的逻辑集合,并提供了一个稳定的入口点。
1. Service 的类型
K8s 主要提供三种类型的 Service:
- ClusterIP(默认):在集群内部暴露一个虚拟 IP。只有集群内的其他 Pod 可以访问它。这是最常用的类型,用于微服务之间的内部通信。
- NodePort:在每个节点上开放一个静态端口(30000-32767)。外部流量可以通过
<NodeIP>:<NodePort>访问 Service。它会自动将流量转发到后端的 Pod。 - LoadBalancer:云服务商通常会创建一个外部负载均衡器(如 AWS ELB, GCP LB),将流量分发到 NodePort 或直接到 Pod。
2. Service 是如何工作的?
这里有一个关键的知识点:Service 并没有自己的 IP 栈,它是由 kube-proxy 管理的。
kube-proxy 是每个节点上都运行的一个进程,它的职责就是维护网络规则,实现 Service 的流量转发。目前主要有两种工作模式:
模式一:Userspace 模式(已过时)
早期的 kube-proxy 会在用户空间运行一个代理程序,监听 Service 的 ClusterIP 和端口。所有进入的流量都会被重定向到这个代理程序,再由它转发到后端 Pod。这种方式灵活性高,但性能较差,因为涉及上下文切换。
模式二:Iptables 模式(主流,但有限制)
这是目前大多数集群默认的模式。kube-proxy 监听 API Server,一旦发现 Service 或 Endpoint 变化,就动态更新宿主机的 iptables 规则。
举个例子:
假设有一个 Service my-service,ClusterIP 为 10.96.0.10,端口 80,后端有两个 Pod:10.244.1.5:8080 和 10.244.2.3:8080。
kube-proxy 会生成类似的 iptables 规则:
# 匹配目标 IP 为 10.96.0.10 且端口为 80 的流量
-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-MARK-MASQ
# 跳转到后端 Pod 的规则
-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-XXXXXX
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-AAAAAA
-A KUBE-SVC-XXXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BBBBBB
-A KUBE-SVC-XXXXXX -j KUBE-SEP-CCCCCC
# KUBE-SEP-AAAAAA 会将流量 DNAT 到 Pod 10.244.1.5:8080
-A KUBE-SEP-AAAAAA -s 10.244.1.5/32 -p tcp -j KUBE-MARK-MASQ
-A KUBE-SEP-AAAAAA -p tcp -j DNAT --to-destination 10.244.1.5:8080
问题所在: Iptables 模式是静态规则的。如果后端 Pod 数量很多(比如几千个),iptables 规则会变得极其庞大,导致更新延迟甚至性能下降。而且,它不支持基于权重的负载均衡,只能做简单的轮询或随机。
模式三:IPVS 模式(推荐,高性能)
IPVS (IP Virtual Server) 是 Linux 内核中的一个模块,提供了更好的负载均衡能力。kube-proxy 使用 IPVS 代替 iptables 来管理 Service。
IPVS 的优势:
- 高性能:使用哈希表存储规则,查找速度快,适合大规模集群。
- 丰富的算法:支持轮询 (rr)、加权轮询 (wrr)、最少连接 (lc) 等多种负载均衡算法。
- 动态更新:规则更新几乎无延迟。
如果你想体验 IPVS,只需要在 kube-proxy 的配置文件中设置 mode: ipvs。
3. Endpoint:Service 的桥梁
Service 本身不处理流量,它只是一个标签选择器。真正的流量转发依赖于 Endpoint(或 EndpointSlice)。
Endpoint 是 Service 的后端 Pod 列表。每当有 Pod 匹配 Service 的 label selector 时,kube-controller-manager 就会更新对应的 Endpoint 对象。kube-proxy 监听到 Endpoint 的变化,从而更新本地的转发规则。
你可以手动查看 Endpoint:
kubectl get endpoints my-service -o yaml
输出可能长这样:
apiVersion: v1
kind: Endpoints
metadata:
name: my-service
subsets:
- addresses:
- ip: 10.244.1.5
targetRef:
kind: Pod
name: my-pod-1
- ip: 10.244.2.3
targetRef:
kind: Pod
name: my-pod-2
ports:
- port: 8080
protocol: TCP
第三层洋葱:Ingress - 七层流量的守门员
前面提到的 Service (NodePort/LoadBalancer) 是在四层 (TCP/UDP) 进行负载均衡的。但现代 Web 应用通常需要七层 (HTTP/HTTPS) 的能力,比如根据域名、URL 路径路由流量。
这就是 Ingress 的用武之地。
Ingress 是一个 API 对象,定义了外部访问集群内部服务的规则(如 host 和 path)。而 Ingress Controller 是实现这些规则的程序,常见的有 Nginx Ingress, Traefik, HAProxy 等。
工作流程:
- 用户在 Ingress 资源中定义规则:
example.com/api指向api-service,example.com/web指向web-service。 - Ingress Controller 监听到 Ingress 资源的变化。
- Ingress Controller 动态配置其底层负载均衡器(如 Nginx 配置文件),生成相应的路由规则。
- 外部流量到达 Ingress Controller 的入口(通常是一个 LoadBalancer Service 或 NodePort),然后根据 HTTP 头中的 Host 或 Path 转发到具体的后端 Service。
实战演练:如何排查容器网络故障?
好了,理论讲得差不多了。现在,假设你的应用突然无法访问了,作为 K8s 管理员,你该怎么办?别慌,我们按照从内到外、从简到繁的顺序来排查。
场景一:Pod 内部无法访问外部网络
症状:Pod 里的应用 ping 不通百度,或者 curl 外部 API 超时。
排查步骤:
检查 DNS:
kubectl exec -it <pod-name> -- cat /etc/resolv.conf确认 nameserver 是否正确。如果是 CoreDNS 问题,检查 CoreDNS Pod 的状态和日志。
检查 CNI 插件状态: 登录到节点,查看 CNI 插件的日志。例如,如果是 Flannel:
journalctl -u kube-flannel -f看是否有报错,比如 IP 池耗尽、网络冲突等。
检查节点网络接口:
ip addr show cni0 # 或者 br0, flannel.1 等确认虚拟网桥是否存在,IP 是否配置正确。
测试连通性: 在 Pod 内 ping 网关 IP,再 ping 宿主机 IP,最后 ping 外部 IP。哪一步断了,问题就在哪里。
场景二:Pod 之间无法通信
症状:Pod A 可以访问 Pod B 的 IP,但无法访问 Service IP。
排查步骤:
检查 Endpoint:
kubectl get endpoints <service-name>确认 Endpoint 列表是否为空。如果为空,说明没有 Pod 匹配 Service 的 label,或者 Pod 处于非 Ready 状态。
检查 kube-proxy:
kubectl logs -n kube-system <kube-proxy-pod-name>看是否有错误日志,比如无法更新 iptables/IPVS 规则。
检查 NetworkPolicy: 如果你使用了 NetworkPolicy,确认是否有策略阻止了 Pod A 到 Pod B 的流量。
kubectl get networkpolicy -A抓包分析: 在 Pod A 和 Pod B 所在的节点上使用
tcpdump抓包,观察数据包是否正常发出和接收。# 在 Pod A 所在节点 tcpdump -i cni0 -nn port <pod-b-ip>
场景三:外部无法访问 Service
症状:浏览器访问 NodePort 或 LoadBalancer IP 超时。
排查步骤:
检查 Service 类型: 确认 Service 是 NodePort 还是 LoadBalancer。如果是 LoadBalancer,检查云厂商的控制台,确认负载均衡器是否创建成功,后端实例是否健康。
检查防火墙: 确保节点的防火墙允许了 NodePort 端口(30000-32767)或 LoadBalancer 的端口。
sudo iptables -L -n | grep <port>检查 Ingress Controller: 如果使用 Ingress,检查 Ingress Controller 的 Pod 是否运行正常,日志是否有错误。
kubectl logs -n ingress-nginx <ingress-controller-pod-name>测试内部连通性: 先在集群内部的一个 Pod 中 curl Service IP,如果能通,说明问题出在外部访问链路上(如 LoadBalancer、防火墙、DNS);如果不能通,说明问题出在 Service 或后端 Pod 上。
代码示例:一个简单的 Python 微服务通信演示
为了让你更直观地理解,我们用 Python 写一个简单的客户端和服务端,并在 K8s 中部署。
服务端 (server.py):
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
response = f"Hello from Pod {os.environ.get('HOSTNAME', 'unknown')}\n"
self.wfile.write(response.encode())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), Handler)
print("Server running on port 8080")
server.serve_forever()
客户端 (client.py):
import requests
import sys
def main():
service_name = sys.argv[1] if len(sys.argv) > 1 else 'my-service'
try:
response = requests.get(f'http://{service_name}:8080')
print(response.text)
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
main()
K8s 部署文件 (deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-server
spec:
replicas: 2
selector:
matchLabels:
app: my-server
template:
metadata:
labels:
app: my-server
spec:
containers:
- name: server
image: python:3.9-slim
command: ["python", "/app/server.py"]
volumeMounts:
- name: code
mountPath: /app
volumes:
- name: code
configMap:
name: server-code
---
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-server
ports:
- protocol: TCP
port: 8080
targetPort: 8080
---
apiVersion: v1
kind: ConfigMap
metadata:
name: server-code
data:
server.py: |
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
response = f"Hello from Pod {os.environ.get('HOSTNAME', 'unknown')}\\n"
self.wfile.write(response.encode())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), Handler)
server.serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-client
spec:
replicas: 1
selector:
matchLabels:
app: my-client
template:
metadata:
labels:
app: my-client
spec:
containers:
- name: client
image: python:3.9-slim
command: ["python", "/app/client.py", "my-service"]
volumeMounts:
- name: code
mountPath: /app
volumes:
- name: code
configMap:
name: client-code
---
apiVersion: v1
kind: ConfigMap
metadata:
name: client-code
data:
client.py: |
import requests
import sys
def main():
service_name = sys.argv[1] if len(sys.argv) > 1 else 'my-service'
try:
response = requests.get(f'http://{service_name}:8080')
print(response.text)
except Exception as e:
print(f"Error: {e}")
if __name__ == '__main__':
main()
执行与验证:
应用配置:
kubectl apply -f deployment.yaml查看 Pod 状态:
kubectl get pods查看客户端日志:
kubectl logs -l app=my-client -f你应该能看到类似
Hello from Pod my-server-xxxxx-yyyyy的输出。如果多次查看,会发现 Hostname 不同,说明 Service 在做负载均衡。测试 Service 内部 DNS:
kubectl exec -it <client-pod-name> -- nslookup my-service确认 DNS 解析是否正常。
结语:拥抱复杂性,享受掌控感
Kubernetes 的网络模型确实复杂,但它为我们提供了一种标准化的、可扩展的方式来管理分布式系统的通信。从 Pod 的独立 IP,到 Service 的稳定入口,再到 Ingress 的智能路由,每一层都有其存在的意义。
当你遇到网络问题时,记住:分层排查,由内而外。先确认 Pod 本身是否正常,再检查 Service 和 Endpoint,最后看外部网络和安全策略。
希望这篇文章能帮你拨开迷雾,建立起对 K8s 网络的清晰认知。网络是 K8s 的灵魂,掌握了它,你就掌握了驾驭容器的钥匙。加油,未来的 K8s 专家!
