跳转至

Kubernetes 网络模型

作者:JiangChong | 撰写时间:2026年07月

学完本文你将能够: ✅ 理解 K8s 扁平网络模型和四大网络问题的解决方案 ✅ 区分 Flannel、Calico、Cilium 等 CNI 插件的适用场景 ✅ 掌握 Service 的五种类型和负载均衡原理 ✅ 使用 Ingress 和 NetworkPolicy 实现高级流量管理和安全隔离

前置阅读: Kubernetes 核心架构,建议了解基本网络概念(IP/端口/DNS)。

0. 本文概览

02、03 篇学会了部署应用和管理存储,但有一件事一直没解决:怎么从外面访问你的 Pod? Pod IP 是动态变化的,而且外部网络根本 ping 不到 10.244.x.x。

网络是连接「集群内部」和「外部世界」的桥梁。本文按这条主线走:

Pod 怎么上网?           → CNI 插件(4:给 Pod 配网卡)
Pod IP 会变,怎么稳定访问? → Service(5:固定入口 + 负载均衡)
怎么从外面进来?          → Ingress(6:HTTP 路由 + 域名)
怎么限制谁访问谁?        → NetworkPolicy(7:Pod 级防火墙)

Kind 用户注意: 本文的 Service 实操都可以在你的单节点 Kind 集群上跑,不需要多节点。


1. 网络基础知识(如果已了解可跳过)

以下是最基本的网络概念,理解它们能帮助你后续阅读。如果这些对你来说已经是常识,直接跳到下一节。

  • IP 地址:每台设备在网络中的"门牌号"。就像快递员通过门牌号找到你家一样,网络数据通过 IP 地址找到对应的设备。例如 10.1.1.2
  • 端口(Port):一台设备上的"房门号"。IP 地址把你带到设备上后,端口号决定数据交给哪个程序处理。例如 Vertica 数据库通常用端口 5433。
  • DNS:把人类好记的名字(如 google.com)翻译成 IP 地址(如 142.250.70.46)的"电话簿"。
  • NAT(网络地址转换):把私有 IP(如 192.168.x.x)映射成公网 IP 的技术。常见的路由器就是这样,让家里多台设备共用一个公网 IP 上网。不做 NAT 意味着 Pod 直接把真实 IP 拿出来通信,不做地址转换。
  • 路由表:设备上的一张"地图",告诉数据包"去某个 IP 应该走哪条路"。比如去 10.1.2.0/24 网段走 Node 2。

2. 为什么 K8s 网络比传统网络更复杂

在传统部署中,服务器的 IP 是固定的。你可以在配置文件中写好 192.168.1.10:5433,几年都不用改。

但在 Kubernetes 中:

  1. Pod 重启后 IP 会变:每次 Pod 重新创建,K8s 都会给它分配一个新的 IP 地址。你不能把 IP 写死在配置里。
  2. Pod 可以在任意节点上运行:同一个应用的两个副本可能运行在不同的物理节点上,它们需要通过节点网络互相通信。
  3. 需要稳定的服务端点:用户需要一个不变的入口来访问一组动态变化的 Pod。这就是 Service 的由来(后面会详细讲)。

简单说:传统网络是静态的、手动配置的;K8s 网络是动态的、自动管理的。


3. 四大网络问题

Kubernetes 网络模型需要解决四个核心问题:

  1. 容器间通信:同一 Pod 内多个容器通过 localhost 通信
  2. Pod 间通信:不同 Pod 之间直接通过 IP 通信,无需 NAT
  3. Pod 与 Service 间通信:通过 Service 抽象访问一组 Pod
  4. 外部访问:从集群外部访问 Service

K8s 网络模型的核心约定:每个 Pod 拥有独立的 IP 地址,所有 Pod 之间可以直接通信(扁平网络),无需 NAT 转换。

Pod A (10.1.1.2) ────→ Pod B (10.1.2.3)
       │                        │
       ▼                        ▼
   Node 1                   Node 2

4. CNI — 容器网络接口

CNI(Container Network Interface)是 K8s 网络插件的标准接口。可以把它理解为"K8s 的网卡驱动程序"——不同厂商提供不同的实现,但都遵循同一个接口规范。

CNI 插件负责最基本的网络操作:

  • ADD:把一个新的 Pod 接入网络,分配 IP、配置网络接口
  • DEL:从网络移除 Pod
  • CHECK:检查 Pod 的网络连接是否正常
  • VERSION:报告 CNI 版本

4.1 Flannel — 最简方案(推荐入门)

Flannel 是 K8s 社区最早、最简单的 CNI 插件。它采用 VXLAN 覆盖网络(overlay) 技术。

什么是覆盖网络(overlay)? 可以理解为在物理网络之上"铺设"一层虚拟网络。就像隧道——地铁在地下穿行,地面上的交通完全感知不到它的存在。Flannel 在节点之间建立这样的"隧道",Pod 的流量通过隧道到达另一个节点的 Pod,底层物理网络不需要做任何特殊配置。

Flannel 的优点:配置极其简单,几乎开箱即用。缺点是不支持 NetworkPolicy(后面会讲它的作用),性能有少量损耗(因为多了一层封装和解封装)。

4.2 Calico — 生产环境的稳妥选择

Calico 采用纯三层路由方案,不依赖隧道封装,性能更好。它把每个 Pod 的 IP 路由信息直接写入节点的路由表,流量以原生方式转发。如果你需要 NetworkPolicy 和更好的性能,Calico 是最主流的生产环境选择。

4.3 Cilium — 高性能与安全

Cilium 基于 eBPF(Linux 内核的一项技术)实现网络和安全功能。它不经过传统的 iptables,性能极高,并且能实现 L7(应用层)的网络安全策略。适合大规模集群和安全敏感场景。

4.4 选型建议

插件 模式 网络策略 性能 复杂度 适用场景
Flannel VXLAN 覆盖网络 不支持 中等 小规模、快速上手
Calico BGP 路由 / IPIP 支持(丰富全面) 生产环境、需网络策略
Cilium eBPF 支持(L3-L7 全覆盖) 极高 大规模、安全敏感场景
Weave Net 覆盖网络(快速数据路径) 支持 中等 多云/混合环境
  • 开发/测试环境 → Flannel 最省心
  • 生产环境、需要 NetworkPolicy → Calico 是最稳妥的选择
  • 关注性能、安全性、可观测性 → Cilium(eBPF 技术栈是趋势)
  • K8s on AWS → AWS VPC CNI(原生 VPC IP,性能最好)

4.5 深入了解:Calico BGP 模式

以下内容涉及路由协议,初次接触时可能不容易理解。可以先跳过,在了解 K8s 网络整体框架后再回来看。

Calico 的核心组件是 Felix 代理,运行在每个节点上,负责维护路由表和防火墙规则(iptables/ipset)。

节点之间通过 BGP(Border Gateway Protocol) 交换路由信息。BGP 是互联网骨干网络使用的路由协议,Calico 把它"借"来用在小规模的容器网络上。

Node 1 (10.0.1.10)                  Node 2 (10.0.1.20)
pod: 10.1.1.2                        pod: 10.1.2.3
     │                                    │
     ▼                                    ▼
路由表:                          路由表:
10.1.1.0/24 via local              10.1.2.0/24 via local
10.1.2.0/24 via 10.0.1.20          10.1.1.0/24 via 10.0.1.10
     │                                    │
     └─────────── BGP 路由交换 ────────────┘

当 Node 1 上的 Pod 要访问 Node 2 上的 Pod 时,数据包走 Linux 内核的三层路由直接转发,没有 VXLAN 封装的开销。这就是 Calico 性能更好的原因。

BGP 路由守护程序通常使用 BIRD 实现。一个典型的 BIRD 配置包含:

# BIRD 配置文件(运行在 K8s 节点上)
protocol bgp {
    local as 64512;
    neighbor 10.0.1.20;
}

5. Service — Pod 访问抽象

前面说过,Pod 是非持久化的,会因滚动更新、扩缩容、节点故障等原因不断创建和销毁,其 IP 地址也随之变化。Service 提供了一套稳定的虚拟 IP(VIP)和 DNS 名称来访问一组 Pod。

你可以把 Service 想象成公司总机号码——拨打总机,它会自动转接到当前可用的客服人员(Pod)。客服人员可能换了一批又一批,但总机号码不变。

5.1 动手实操:Service 如何解决 Pod IP 变化

# 1. 创建 Deployment + Service
kubectl create deployment nginx-svc --image=nginx:1.25 --replicas=2
# deployment.apps/nginx-svc created
kubectl expose deployment nginx-svc --port=80 --type=ClusterIP
# service/nginx-svc exposed

# 2. 看 Pod IP 和 Service IP
kubectl get pods -o wide -l app=nginx-svc
# NAME                       READY   STATUS    RESTARTS   AGE   IP[Pod IP]    NODE                           NOMINATED NODE   READINESS GATES
# nginx-svc-c798cf8f-6r47z   1/1     Running   0          26s   10.244.0.21   k8s-playground-control-plane   <none>           <none>
# nginx-svc-c798cf8f-h5c5t   1/1     Running   0          26s   10.244.0.20   k8s-playground-control-plane   <none>           <none>

kubectl get svc nginx-svc
# NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# nginx-svc   ClusterIP   10.96.166.95   <none>        80/TCP    78s         ← Service IP(固定不变)

# 3. 验证:通过 Service IP 访问 → 自动负载均衡到两个 Pod
kubectl run test --image=nginx:1.25 --rm -it --restart=Never -- curl -s 10.96.166.95
# --rm: 临时 Pod 跑完 curl 自动删除,不留垃圾。--restart=Never: 只跑一次,不重启
# 返回 nginx 欢迎页 ← 你通过不变的 Service IP 访问到了动态的 Pod!

# 4. 验证:删 Pod → Pod IP 变了 → Service IP 不变 → 还能访问
SVC_IP=$(kubectl get svc nginx-svc -o jsonpath='{.spec.clusterIP}')

POD_NAME=$(kubectl get pods -l app=nginx-svc -o jsonpath='{.items[0].metadata.name}')

kubectl delete pod $POD_NAME
# pod "nginx-svc-c798cf8f-6r47z" deleted from default namespace

kubectl get pods -o wide -l app=nginx-svc   # Pod IP 变了(新 Pod 替换)
# NAME                       READY   STATUS    RESTARTS   AGE   IP            NODE                           NOMINATED NODE   READINESS GATES
# nginx-svc-c798cf8f-h5c5t   1/1     Running   0          15m   10.244.0.20   k8s-playground-control-plane   <none>           <none>
# nginx-svc-c798cf8f-lcbrg   1/1     Running   0          12s   10.244.0.23   k8s-playground-control-plane   <none>           <none>          <-- New Pod

kubectl get svc nginx-svc                   # Service IP 不变
# NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# nginx-svc   ClusterIP   10.96.166.95   <none>        80/TCP    15m

kubectl run test --image=nginx:1.25 --rm -it --restart=Never -- curl -s $SVC_IP
# 还能访问!Pod 都换了但 Service 入口没变

清理:kubectl delete deploy nginx-svc && kubectl delete svc nginx-svc

这就是 Service 的核心价值——给动态变化的 Pod 一个固定不变的访问入口。

5.2 Service 类型

以下 YAML 定义了 K8s 的 5 种 Service 类型。从上到下依次是:ClusterIP(集群内部可达)、NodePort(节点端口暴露)、LoadBalancer(云负载均衡器)、ExternalName(映射到外部域名)、Headless(无 VIP,直连 Pod IP 列表)。

# ClusterIP(默认)— 只在集群内部可达
apiVersion: v1
kind: Service
metadata:
  name: vertica-main
spec:
  type: ClusterIP
  ports:
  - name: vertica
    port: 5433
    targetPort: 5433
  selector:
    vertica.com/subcluster: main
---
# NodePort — 通过节点的固定端口暴露
apiVersion: v1
kind: Service
metadata:
  name: vertica-external
spec:
  type: NodePort
  ports:
  - port: 5433
    nodePort: 32001    # 范围 30000-32767
  selector:
    vertica.com/subcluster: external
---
# LoadBalancer — 云提供商负载均衡器(AWS ELB、GCP LB)
apiVersion: v1
kind: Service
metadata:
  name: vertica-lb
spec:
  type: LoadBalancer
  ports:
  - port: 5433
    targetPort: 5433
---
# ExternalName — 映射到集群外部 DNS
apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  type: ExternalName
  externalName: db.external.example.com   # 返回 CNAME 记录
---
# Headless Service — 无 VIP,直接返回 Pod IP 列表
apiVersion: v1
kind: Service
metadata:
  name: vertica-headless
spec:
  clusterIP: None     # 关键标记
  ports:
  - port: 5433
  selector:
    app: vertica

五种类型怎么选:

类型 访问范围 典型场景
ClusterIP 集群内部 后端服务调用、微服务间通信。Vertica 子集群内部连接、应用连接数据库。最常用,也是默认类型
NodePort 集群外部(通过节点 IP:端口) 开发调试时从笔记本直接连 K8s 里的数据库、没有云 LB 的裸金属环境
LoadBalancer 外部(通过云 LB 的公网/内网 IP) 生产环境对外暴露服务。AWS/GCP 自动创建云 LB,自动分配公网 IP
ExternalName 集群内部 → 外部服务 集群内的应用访问集群外的数据库(如 RDS),用内部 DNS 名包装外部地址
Headless 直连 Pod StatefulSet 需要 Pod 间直接通信(如 Vertica 节点互访),不经过 VIP 转发

5.3 Headless Service 与 StatefulSet

Headless Service(clusterIP: None)不分配虚拟 IP。DNS 查询不会返回一个 VIP,而是直接返回所有就绪 Pod 的 IP 列表。

为什么需要 Headless? 普通 ClusterIP Service 是「总机转接」——你拨总机,总机帮你转到某个 Pod,你不需要知道具体哪个 Pod 接了电话。但有状态应用(如 Vertica)恰好相反——每个节点都需要知道其他每个节点的确切 IP,直接通信建立集群。

在 03 篇的 StatefulSet 实操(nginx-sts-0, nginx-sts-1, nginx-sts-2)中,配上 Headless Service 后每个 Pod 获得固定的 DNS:

nginx-sts-0.nginx-hl.default.svc.cluster.local   10.244.0.x  10.244.0.14
nginx-sts-1.nginx-hl.default.svc.cluster.local   10.244.0.y  10.244.0.25
nginx-sts-2.nginx-hl.default.svc.cluster.local   10.244.0.z  10.244.0.18

Pod 删了重建,名字不变 → DNS 不变 → 即使 IP 变了,其他 Pod 通过 DNS 名还能找到它。

验证一下:

# 接 03 篇的 StatefulSet 实验(如果已清理,重新 apply sts-demo.yaml)
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- nslookup nginx-sts-0.nginx-hl.default.svc.cluster.local
# Server: 10.96.0.10        ← CoreDNS 的 ClusterIP
# Address: 10.244.0.14      ← nginx-sts-0 的 Pod IP!不是 VIP,是 Pod 真实 IP
# 对比:普通 Service 的 nslookup 会返回 10.96.x.x(VIP),不会返回 Pod IP

# Server:       10.96.0.10
# Address:  10.96.0.10:53


# Name: nginx-sts-0.nginx-hl.default.svc.cluster.local
# Address: 10.244.0.14

# All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
# If you don't see a command prompt, try pressing enter.
# warning: couldn't attach to pod/dns-test, falling back to streaming logs: unable to upgrade connection: container dns-test not found in pod dns-test_default
# Server:       10.96.0.10
# Address:  10.96.0.10:53


# Name: nginx-sts-0.nginx-hl.default.svc.cluster.local
# Address: 10.244.0.14

# pod "dns-test" deleted from default namespace

验证 Headless 的效果——看一眼 CLUSTER-IP 列就清楚了:

kubectl get svc
# NAME         TYPE        CLUSTER-IP      PORT(S)
# nginx-hl     ClusterIP   None           ← Headless:没有 VIP
# nginx-svc    ClusterIP   10.96.166.95   ← 普通 ClusterIP:有 VIP

nginx-hlCLUSTER-IP: None 就是 clusterIP: None 的效果。nslookup 它时 CoreDNS 没有 VIP 可返回,直接给 Pod IP。

顺便看一下 kube-dns(CoreDNS 自己)也是有 VIP 的普通 Service——nslookup 输出的 Server: 10.96.0.10 就是它的地址:

kubectl get svc -n kube-system kube-dns
# NAME       TYPE        CLUSTER-IP    PORT(S)
# kube-dns   ClusterIP   10.96.0.10    53/UDP,53/TCP

对比普通 Service 和 Headless Service:

ClusterIP Service Headless Service
DNS 返回什么 VIP(10.96.x.x) Pod IP 列表
流量路径 VIP → kube-proxy → Pod 直连 Pod
适用场景 无状态 Web 服务 数据库集群、消息队列
Pod 间是否需要知道彼此 IP 不需要 需要

这一点对于 Vertica 等有状态数据库非常关键——每个 Pod 需要固定的网络标识来建立集群通信。

5.4 服务发现:从环境变量到 DNS

Pod 怎么知道 Service 的地址?两种方式——环境变量(早期)和 DNS(现代)。

环境变量(不推荐): Pod 启动时 K8s 自动注入类似 NGINX_SVC_SERVICE_HOST=10.96.166.95 的环境变量。致命缺点:Pod 创建后才存在的 Service 不会被注入(Pod 启动时 Service 还没建,环境变量就是空的)。

kubectl exec nginx-sts-0 -- env | grep NGINX_SVC
# 可能是空的——如果 Pod 比 Service 先创建的话

CoreDNS(标准方式): K8s 自带 DNS 服务,不需要任何配置。你可以通过 DNS 名访问任何 Service,无视创建顺序。

kubectl get pods -n kube-system | grep coredns
# coredns-xxxxx                                          1/1     Running                     ← K8s 内置的 DNS 服务器
# coredns-589f44dc88-cv9b4                               1/1     Running   0          5h57m  ← K8s 内置的 DNS 服务器
# coredns-589f44dc88-q8s89                               1/1     Running   0          5h57m  ← K8s 内置的 DNS 服务器

# etcd-k8s-playground-control-plane                      1/1     Running   0          5h58m
# kindnet-99phc                                          1/1     Running   0          5h58m
# kube-apiserver-k8s-playground-control-plane            1/1     Running   0          5h58m
# kube-controller-manager-k8s-playground-control-plane   1/1     Running   0          5h58m
# kube-proxy-74dfk                                       1/1     Running   0          5h58m
# kube-scheduler-k8s-playground-control-plane            1/1     Running   0          5h58m

每个 Pod 的 /etc/resolv.conf 自动指向 CoreDNS,所以 Pod 内直接用 DNS 名就能解析 Service。

kubectl exec nginx-sts-0 -- cat /etc/resolv.conf
# search default.svc.cluster.local svc.cluster.local cluster.local
# nameserver 10.96.0.10
# options ndots:5

动手对比:IP vs DNS 名 — 目的是证明「用名字好过用 IP」:

# 用 5.1 的 nginx-svc 试
kubectl create deployment nginx-svc --image=nginx:1.25 --replicas=2
kubectl expose deployment nginx-svc --port=80 --type=ClusterIP

# 方式一:用 IP(Pod 重建就失效)
SVC_IP=$(kubectl get svc nginx-svc -o jsonpath='{.spec.clusterIP}')  # 10.96.166.95
kubectl run test --image=nginx:1.25 --rm -it --restart=Never -- curl -s $SVC_IP  # ✅

# 方式二:用 DNS 名(永远有效,Pod 重建也没关系)
kubectl run test --image=nginx:1.25 --rm -it --restart=Never -- curl -s nginx-svc
# ✅ 同 Namespace 直接用 Service 名就行,不需要写全 FQDN

# DNS 全格式(跨 Namespace 必须写全)
# <service>.<namespace>.svc.cluster.local
# 同 Namespace 简写:<service>

每个 Pod 的 /etc/resolv.conf 自动指向 CoreDNS:

kubectl exec nginx-sts-0 -- cat /etc/resolv.conf
# nameserver 10.96.0.10           ← CoreDNS 的地址
# search default.svc.cluster.local svc.cluster.local cluster.local

清理: kubectl delete deploy nginx-svc && kubectl delete svc nginx-svc

# DNS 解析格式(仅供参考)
<service>.<namespace>.svc.cluster.local

# 例如
vertica-main.my-namespace.svc.cluster.local:5433
nginx-sts-0.nginx-hl.default.svc.cluster.local   10.244.0.x  10.244.0.14
nginx-sts-1.nginx-hl.default.svc.cluster.local   10.244.0.y  10.244.0.25
nginx-sts-2.nginx-hl.default.svc.cluster.local   10.244.0.z  10.244.0.18

# 同 Namespace 可以简写
vertica-main:5433

5.5 Service 负载均衡原理

你访问 10.96.166.95:80 时,数据包并没有一个叫 10.96.166.95 的真实设备——这是 kube-proxy 在节点上用 iptables 规则虚拟出来的。真实流程:

curl 10.96.166.95:80
  → iptables 看到目标 IP 是 10.96.166.95
    → 查规则:这个 VIP 对应 Pod [10.244.0.20, 10.244.0.23]
      → 随机选 10.244.0.20 → 转发

验证负载均衡确实在随机分发: 连刷几次,看 nginx 日志里的 Pod 名在变化:

kubectl create deployment nginx-svc --image=nginx:1.25 --replicas=2
kubectl expose deployment nginx-svc --port=80

# 连续访问 3 次
for i in 1 2 3; do
  kubectl run t-$i --image=nginx:1.25 --restart=Never -- curl -s nginx-svc
done
# pod/t-1 created
# pod/t-2 created
# pod/t-3 created

kubectl delete pod t-1 t-2 t-3 --wait=false  # 清理临时 Pod
# pod "t-1" deleted from default namespace
# pod "t-2" deleted from default namespace
# pod "t-3" deleted from default namespace

# 逐个看每个 Pod 的日志——两个都有请求记录,证明负载均衡生效
kubectl get pods -l app=nginx-svc -o name | while read pod; do
  echo "=== $pod ===" && kubectl logs $pod --tail=3
done
# === pod/nginx-svc-c798cf8f-h5c5t ===
# 10.244.0.43 - - [30/Jul/2026:09:17:29 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"  ← h5c5t 处理了 2 个
# 10.244.0.44 - - [30/Jul/2026:09:17:29 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"  ← h5c5t 处理了 2 个
# === pod/nginx-svc-c798cf8f-lcbrg ===
# 10.244.0.42 - - [30/Jul/2026:09:17:29 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"  ← lcbrg 处理了 1 个

kube-proxy 是规则的维护者: 运行在每个节点上,Watch API Server 上 Service 和 EndpointSlice 的变化。Pod 挂了 → EndpointSlice 更新 → kube-proxy 更新 iptables 规则 → 流量不再发到死 Pod。

两种模式:

  • iptables(默认):随机选 Pod,简单可靠。规则量大时更新慢
  • IPVS:支持 round-robin、least-connection 等算法,大规模集群性能更好

清理:kubectl delete deploy nginx-svc && kubectl delete svc nginx-svc

5.6 Session Affinity(会话亲和性)

问题: 刚才的负载均衡测试证明了每次请求可能打到不同 Pod。如果你的应用在 Pod 上存了登录状态(session),跳来跳去就会导致用户反复登录。Session Affinity 让同一客户端 IP 始终路由到同一个 Pod。

默认是 None(随机),加上 sessionAffinity: ClientIP 后 kube-proxy 会给每个客户端 IP 记录一个「上次转发到哪个 Pod」,后续请求优先走同一个。

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800   # 会话保持时间(默认 3 小时)

动手验证: 同一次 curl 连刷 3 次,看是不是都打到同一个 Pod:

1、不开启时:请求分散到两个 Pod

MY_IP=$(kubectl get pod nginx-svc-c798cf8f-h5c5t -o jsonpath='{.status.podIP}') # 10.244.0.20

POD_NAME=$(kubectl get pods -l app=nginx-svc -o jsonpath='{.items[0].metadata.name}') # nginx-svc-c798cf8f-h5c5t

# 从同一个 Pod 内连刷 3 次
kubectl exec $POD_NAME -- sh -c 'for i in 1 2 3; do curl -s nginx-svc > /dev/null; done'

kubectl get pods -l app=nginx-svc -o name | while read p; do echo "=== $p ===" && kubectl logs $p --tail=3; done
# 不开启时:请求分散到两个 Pod

# === pod/nginx-svc-c798cf8f-h5c5t ===
# 10.244.0.1 - - [30/Jul/2026:09:30:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← h5c5t 处理了 1 个
# === pod/nginx-svc-c798cf8f-lcbrg ===
# 10.244.0.20 - - [30/Jul/2026:09:30:55 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← lcbrg 处理了 2 个
# 10.244.0.20 - - [30/Jul/2026:09:30:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← lcbrg 处理了 2 个
2、开启后(需要 patch Service):同一个源 IP 的请求全打到同一个 Pod
# 1. 开启
kubectl patch svc nginx-svc -p '{"spec":{"sessionAffinity":"ClientIP"}}'
# service/nginx-svc patched

# 2. 从同一个 Pod 连刷 3 次
POD_NAME=$(kubectl get pods -l app=nginx-svc -o jsonpath='{.items[0].metadata.name}') # nginx-svc-c798cf8f-h5c5t
kubectl exec $POD_NAME -- sh -c 'for i in 1 2 3; do curl -s nginx-svc > /dev/null; done'

# 3. 看分布——这次全在同一个 pod/nginx-svc-c798cf8f-lcbrg @ 30/Jul/2026:09:41:38
kubectl get pods -l app=nginx-svc -o name | while read p; do echo "=== $p ===" && kubectl logs $p --tail=3; done
# === pod/nginx-svc-c798cf8f-h5c5t ===
# 10.244.0.43 - - [30/Jul/2026:09:17:29 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.44 - - [30/Jul/2026:09:17:29 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.1 - - [30/Jul/2026:09:26:41 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.1 - - [30/Jul/2026:09:26:41 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.1 - - [30/Jul/2026:09:30:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← h5c5t 处理了 0 个
# === pod/nginx-svc-c798cf8f-lcbrg ===
# 10.244.0.20 - - [30/Jul/2026:09:30:55 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.20 - - [30/Jul/2026:09:30:56 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 10.244.0.20 - - [30/Jul/2026:09:41:38 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← lcbrg 处理了 3 个
# 10.244.0.20 - - [30/Jul/2026:09:41:38 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← lcbrg 处理了 3 个
# 10.244.0.20 - - [30/Jul/2026:09:41:38 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-" ← lcbrg 处理了 3 个

# 4. 恢复
kubectl patch svc nginx-svc -p '{"spec":{"sessionAffinity":"None"}}'
# service/nginx-svc patched

Vertica 不需要 Session Affinity。 Vertica 的连接状态由数据库本身管理(每个连接有独立的 session),不依赖 K8s 的粘性路由。但一些老的 Web 应用确实需要它。


6. Ingress — 七层入口

为什么有了 Service 还需要 Ingress? Service 是 L4(TCP/IP 层)的——它只能按 IP:端口转发,不能看 HTTP 请求内容。如果你有三个 Service(/api/monitor/),想通过同一个域名 vertica.example.com 的不同路径访问不同的 Service,Service 做不到,Ingress 可以。

用户请求: https://vertica.example.com/monitor
  → Ingress Controller(看 HTTP Host + Path)
    → /monitor 匹配 → 转发到 vertica-monitor Service
    → /api 匹配     → 转发到 vertica-api Service
    → /            → 转发到 vertica-web Service

Ingress 规则只定义「什么请求转给谁」,实际的转发由 Ingress Controller 执行——它是集群里跑的一个反向代理(通常是 nginx)。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vertica-web
spec:
  ingressClassName: nginx            # 使用哪个 Ingress Controller
  tls:                                # HTTPS 证书
  - hosts:
    - vertica.example.com
    secretName: vertica-tls           # 证书存在 Secret 里
  rules:                              # 路由规则
  - host: vertica.example.com         # 匹配这个域名
    http:
      paths:
      - path: /monitor                # 路径是 /monitor
        pathType: Prefix
        backend:
          service:
            name: vertica-monitor     # → 转给这个 Service
            port:
              number: 80

注意: Ingress 只是 YAML 规则,必须安装 Ingress Controller 才能真正生效。Kind 可以通过 kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml 安装 nginx-ingress。

常见 Ingress Controller:

实现 特点
nginx-ingress 社区最主流,基于 Nginx
Traefik 自动服务发现,自带 Dashboard
AWS Load Balancer Controller AWS ALB/NLB 原生集成
Contour 基于 Envoy 代理

2024+ 更新: Ingress API 已被冻结。新项目建议优先评估 Gateway API(K8s 下一代入口网关标准),但 Ingress 目前仍然是绝大多数生产集群的主力。


7. NetworkPolicy — Pod 级防火墙

安全问题: K8s 默认所有 Pod 之间可以互相访问——集群内任何 Pod 都能连你的数据库 Pod 的 5433 端口。这在生产环境是不可接受的。NetworkPolicy 就是在 Pod 前面加一道防火墙。

NetworkPolicy 决定了「谁」可以访问「哪些 Pod」的「哪些端口」。注意:不是所有 CNI 都支持——Flannel 和 Kind 默认的 kindnet 不支持,需要 Calico 或 Cilium。

# 规则:只允许 app=my-application 的 Pod 访问 app=vertica 的 5433 端口
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-vertica-access
  namespace: vertica
spec:
  podSelector:                    # 受保护的 Pod(app=vertica)
    matchLabels:
      app: vertica
  policyTypes:
  - Ingress                       # 控制入站流量
  ingress:
  - from:                         # 允许谁进来
    - podSelector:                # 允许这些 Pod
        matchLabels:
          app: my-application     # → app=my-application
    - namespaceSelector:          # 也允许这个 Namespace 的所有 Pod
        matchLabels:
          kubernetes.io/metadata.name: monitoring  # K8s 自动打的标签,值就是 NS 名;除了 app=my-application 的 Pod 之外,整个 monitoring 命名空间的所有 Pod 也可以访问 Vertica 的 5433 端口。
    ports:                        # 允许的端口
    - protocol: TCP
      port: 5433                  # → 只能是 5433,其他端口一律拒绝

默认行为: 某个 Pod 没有被任何 NetworkPolicy 选中 → 全部放行。一旦被某条 NetworkPolicy 的 podSelector 选中,就按该策略的规则执行——未列出的来源和端口全部拒绝。

零信任模型——先全部禁止、再逐条放行:

restrict-vertica-access 自身就是零信任——ingress 里只列出了两条放行规则,其他一切来源和端口自动拒绝,不需要额外兜底策略。

其他 Pod → vertica NS 中的 app=vertica Pod → restrict-vertica-access 判断
  app=my-application → vertica:5433 → ✅ ingress 第一条匹配,放行
  monitoring NS 所有 Pod → vertica:5433 → ✅ ingress 第二条匹配,放行
  app=unknown → vertica:5433 → ❌ 没有匹配的 ingress 规则
  app=my-application → vertica:8080 → ❌ 端口不在允许列表中

NetworkPolicy 的核心逻辑:列出了才是允许,没列出就是禁止。


8. Vertica on K8s 网络实践

把本文学的网络概念套到 Vertica 部署上——每一层都有自己的角色:

网络层 本文概念 Vertica 怎么用
Pod 网络 CNI(4) Vertica 节点间扁平网络直连,对延迟敏感 → 推荐 Calico,不用 Flannel 的 VXLAN 封装
内部访问 ClusterIP Service(5.2) 每个子集群自动创建 ClusterIP Service,应用通过 <sc-name>:5433 连接
外部访问 NodePort / LoadBalancer(5.2) 从集群外用客户端连 Vertica:patch 子集群为 NodePort 或 LoadBalancer
Pod 间通信 Headless Service(5.3) StatefulSet + Headless 给每个 Pod 固定的 DNS,Vertica 节点通过 DNS 互相发现
服务发现 CoreDNS(5.4) Pod 内用 vertica-main.default.svc.cluster.local 解析到 Service IP
安全 NetworkPolicy(7) 限制只有应用 Pod 能访问 Vertica 的 5433 端口
# 外部访问:将子集群 sc 暴露为 NodePort
kubectl patch vdb verticadb-sample --type=merge \
  --patch '{"spec":{"subclusters":[{"name":"sc","serviceType":"NodePort","nodePort":32001}]}}'

更详细的 K8s 网络配置请参考 Vertica Eon on K8s 生产部署实战


9. 总结

层次 组件 作用
Pod 网络 CNI 插件 为 Pod 分配 IP、配置网络
Service ClusterIP/NodePort/LB 稳定的 IP 和 DNS 访问可变的 Pod 集合
入口 Ingress + Controller HTTP/HTTPS 路由和 TLS 终结
安全 NetworkPolicy Pod 间网络隔离和访问控制
服务发现 CoreDNS 基于 DNS 的 Service 发现

下一篇文章: Kubernetes RBAC 权限与安全 — 掌握 K8s 的认证授权和安全策略。