前言:网络是 K8s 微服务架构最容易翻车的地方
如果你问 K8s 初学者"最头疼什么",十有八九会说"网络"。
不是没有原因的。在传统 Java 开发里,服务间通信就是一个 IP + 端口,配到 application.properties 里完事。但到了 K8s,你会面对一堆陌生概念的轰炸:Pod IP 为什么每次重启都变?Service 的 ClusterIP 和 NodePort 什么时候用哪个?Ingress 和 LoadBalancer 有什么区别?Istio 是什么,我必须用吗?
本文的目标是彻底讲清楚这些问题,而不只是把配置贴给你。我们从一个真实的 Java SaaS 产品出发,按照流量从外部进入集群、在集群内流转、最终到达目标 Pod 这条路径,逐层拆解 K8s 的网络体系。
读完本文,你能独立完成:
- 为 Java SaaS 配置生产级 Ingress(含 HTTPS、限流、CORS、灰度)
- 理解并排查 CoreDNS 解析问题
- 给微服务加上 Istio Service Mesh,实现零代码改动的 mTLS 加密和熔断
- 用 NetworkPolicy 实现数据库访问白名单
一、先建立正确的网络全景图
在讲任何具体配置之前,必须先有一张整体的"地图"。很多人学 K8s 网络学得一头雾水,根本原因是脑子里没有这张图。
bash
┌───────────────────────────────────────────────────────────────────┐
│ 互联网用户 │
│ 浏览器 / 移动端 / 第三方系统 │
└───────────────────────┬───────────────────────────────────────────┘
│ HTTPS 请求 api.yoursaas.com
▼
┌───────────────────────────────────────────────────────────────────┐
│ 云厂商负载均衡(阿里云 SLB / AWS ELB) │
│ 公网 IP: 1.2.3.4 ← DNS 解析到这里 │
│ 职责:四层 TCP 转发,将流量引入集群节点 │
└───────────────────────┬───────────────────────────────────────────┘
│ 转发到 NodePort 或直接到 Pod
▼
┌───────────────────────────────────────────────────────────────────┐
│ Nginx Ingress Controller Pod(运行在集群内) │
│ 职责:七层 HTTP 路由,解析 Host/Path,转发到对应 Service │
│ /api/v1 → java-saas-api-service:80 │
│ /api/ai → ai-gateway-service:80 │
│ 负责:TLS 终止、限流、CORS、灰度分流 │
└───────────────────────┬───────────────────────────────────────────┘
│ HTTP(TLS 已在 Ingress 层终止)
▼
┌───────────────────────────────────────────────────────────────────┐
│ Service(ClusterIP) │
│ java-saas-api-service: 10.96.100.50:80 │
│ 职责:稳定的虚拟 IP,通过 kube-proxy/iptables 实现四层负载均衡 │
│ DNS 名称:java-saas-api-service.production.svc.cluster.local │
└──────────┬─────────────────┬──────────────────┬───────────────────┘
│ 负载均衡 │ │
▼ ▼ ▼
Pod (10.244.1.5) Pod (10.244.2.8) Pod (10.244.3.2)
Spring Boot :8080 Spring Boot :8080 Spring Boot :8080
[Envoy sidecar] [Envoy sidecar] [Envoy sidecar]
│
│ 访问数据库(集群内东西向流量)
▼
mysql-service(CoreDNS 解析 → ClusterIP → MySQL Pod)
这张图揭示了 K8s 网络的分工:
- CoreDNS:负责"名字 → IP"的解析,是集群内所有服务发现的基础
- Service:提供稳定的 ClusterIP,屏蔽 Pod IP 的变化
- Ingress Controller:集群的"南北向"流量入口,七层路由
- Istio/Envoy sidecar:处理"东西向"服务间通信的治理(可选)
弄清楚分工,就知道遇到问题该往哪个层排查,而不是茫然无措。
二、CoreDNS:服务发现的地基,不懂它就会莫名其妙地报错
2.1 DNS 是如何工作的
每当你在 Spring Boot 配置文件里写 jdbc:mysql://mysql-service:3306/saasdb,这个 mysql-service 名字是怎么被解析成 IP 的?
答案是 CoreDNS。K8s 集群中每创建一个 Service,Controller Manager 就会在 CoreDNS 中自动注册一条 A 记录,格式固定为:
bash
<service-name>.<namespace>.svc.cluster.local → ClusterIP
Pod 内的 DNS 解析请求会先发给 CoreDNS(ClusterIP 通常是 10.96.0.10),CoreDNS 查询 K8s API 后返回对应的 ClusterIP。
不同访问场景下,DNS 名称的写法不同:
yaml
# ── 场景 1:同一 namespace 内访问(最简写法)──────────────────
# production namespace 内的 java-saas-api 访问 mysql-service
spring:
datasource:
url: jdbc:mysql://mysql-service:3306/saasdb
# CoreDNS 会自动补全为 mysql-service.production.svc.cluster.local
# ── 场景 2:跨 namespace 访问(必须用完整 DNS 名或至少加 namespace)
# production namespace 的服务访问 ai-platform namespace 的 LLM 服务
spring:
ai:
base-url: http://llm-service.ai-platform:8080
# 或完整写法(在极少数 DNS 配置特殊的集群中更稳定):
# http://llm-service.ai-platform.svc.cluster.local:8080
# ── 场景 3:访问集群外部服务(用 ExternalName Service 包装)──
# 把外部 MySQL RDS 的域名包装成集群内可用的 Service 名称
# 这样迁移到集群内 MySQL 时,Java 代码不需要修改任何配置
创建 ExternalName Service,让 Java 应用通过集群内名称访问外部 RDS:
yaml
# external-mysql-service.yaml
# 这个 Service 没有 selector,也没有 endpoints
# 只是把集群内的 DNS 名称 "mysql-service" 映射到外部域名
apiVersion: v1
kind: Service
metadata:
name: mysql-service # Java 应用配置的名称保持不变
namespace: production
spec:
type: ExternalName
externalName: your-rds.rds.aliyuncs.com # 外部 RDS 域名
ports:
- port: 3306
这个技巧在"逐步迁移数据库到 K8s 集群内"的过程中极其有用:Java 代码和 application.yml 完全不动,只需要把这个 ExternalName Service 删掉,换成指向集群内 MySQL StatefulSet 的普通 Service。
2.2 一个让人崩溃的真实 Bug:DNS 解析偶发超时
在某个上线两周的生产环境里,Java 服务间调用随机出现 2-5 秒的延迟,但频率很低,大约每小时十几次。监控没有告警,用户偶尔投诉"转圈"。排查了三天,最终定位到 DNS 解析超时。
根本原因是这样的:Linux 内核的 conntrack(连接追踪表)在高并发下可能满载,导致 UDP 包(DNS 查询使用 UDP)丢失,触发 DNS 客户端的超时重试(默认 5 秒)。
排查方法:
bash
# 进入出问题的 Pod,测试 DNS 解析速度
kubectl exec -it <pod-name> -n production -- sh
# 正常情况应该在 10ms 内返回
time nslookup mysql-service
# Server: 10.96.0.10 ← CoreDNS 的 ClusterIP
# Name: mysql-service.production.svc.cluster.local
# Address: 10.96.100.80
# real 0m0.008s ← 正常
# 如果卡住超过 1 秒,基本可以确定是 DNS 问题
# 在 Node 上检查 conntrack 表使用情况
cat /proc/sys/net/netfilter/nf_conntrack_count # 当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_max # 最大连接数
# 如果 count 接近 max,说明 conntrack 表快满了
解决方案------在 Pod 的 DNS 配置中减少无效查询次数:
yaml
# 在 Deployment 的 Pod spec 中添加
spec:
dnsConfig:
options:
# ndots 默认是 5,意味着 "mysql-service" 会被依次尝试:
# mysql-service.production.svc.cluster.local ← 成功,返回
# mysql-service.svc.cluster.local ← 如果上面失败才会到这里
# mysql-service.cluster.local
# mysql-service
# 改为 2 后,直接从完整域名开始查,减少无效 UDP 查询
- name: ndots
value: "2"
- name: timeout
value: "2" # 单次超时 2 秒(默认 5 秒)
- name: attempts
value: "3" # 最多重试 3 次
dnsPolicy: ClusterFirst # 保持默认:先查 CoreDNS,再查外部 DNS
改完之后,那个困扰三天的偶发延迟彻底消失。
2.3 CoreDNS 自定义:让集群内访问公司内网老系统
有时集群内的 Java 服务需要调用公司内网的老系统(比如用了十年的 ERP),但这些系统域名不在公网 DNS 上。通过修改 CoreDNS ConfigMap 添加自定义解析规则:
yaml
# 修改 CoreDNS 配置(谨慎操作,改错会影响整个集群 DNS)
# 先备份现有配置:
# kubectl get configmap coredns -n kube-system -o yaml > coredns-backup.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health { lameduck 5s }
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
# 新增:将 company.internal 域名转发到公司内网 DNS 服务器
# 这样 old-erp.company.internal 就能在集群内解析了
forward company.internal 192.168.1.10 192.168.1.11 {
policy sequential # 主备模式:先用第一个,失败再用第二个
}
# 新增:硬编码几个特殊服务的 IP(不走 DNS,直接返回)
hosts {
10.0.0.100 legacy-payment.company.internal
10.0.0.101 legacy-crm.company.internal
fallthrough # 其他域名继续走后续插件处理
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload # 支持热重载,修改 ConfigMap 后约 1 分钟生效
loadbalance
}
💡 操作安全提示 :修改 CoreDNS ConfigMap 前务必先备份,改完后用
kubectl rollout restart deployment coredns -n kube-system让配置热重载生效,同时观察 CoreDNS Pod 日志确认无报错。生产环境建议先在 staging 集群验证。
三、Ingress:把外部流量安全、精确地引入集群
3.1 NodePort、LoadBalancer、Ingress 该用哪个?
这三种对外暴露方式让很多人迷糊,用一个表格彻底说清楚:
| 方式 | 工作层 | 典型使用场景 | 生产推荐 |
|---|---|---|---|
| NodePort | L4 TCP | 临时测试、快速验证 | ❌ 不推荐 |
| LoadBalancer | L4 TCP | 单个服务对外暴露(非 HTTP) | ⚠️ 有限使用 |
| Ingress | L7 HTTP/HTTPS | 多服务统一入口、域名路由 | ✅ 首选 |
NodePort 的端口范围是 30000-32767,如果你有 10 个服务要对外暴露,就要记住 10 个奇怪的端口,且无法做域名路由,完全不实用。LoadBalancer 每个 Service 对应一个云厂商 LB,以阿里云为例每个 LB 约 200 元/月,10 个服务就是 2000 元/月,成本失控。
正确的生产架构:只用一个云厂商 LB,绑定到 Nginx Ingress Controller;所有 HTTP/HTTPS 服务都通过 Ingress 规则路由,无论多少个服务都共用这一个 LB。非 HTTP 服务(如 MySQL、Redis 对外暴露)才单独使用 LoadBalancer。
3.2 安装 Nginx Ingress Controller
bash
# 使用 Helm 安装(强烈推荐,便于升级和配置管理)
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.replicaCount=2 \ # 生产至少 2 副本,高可用
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set controller.resources.requests.cpu=100m \
--set controller.resources.requests.memory=90Mi \
--set controller.metrics.enabled=true \ # 开启 Prometheus 指标采集
--set controller.podAnnotations."prometheus\.io/scrape"=true
# 等待 Ingress Controller 就绪(会自动创建云厂商 LoadBalancer)
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/component=controller \
-n ingress-nginx --timeout=120s
# 获取 LoadBalancer 的外部 IP(这就是 DNS 要解析到的公网 IP)
kubectl get svc ingress-nginx-controller -n ingress-nginx
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# ingress-nginx-controller LoadBalancer 10.96.200.10 1.2.3.4 80:31080/TCP,443:31443/TCP
# ↑ 把这个 IP 配置到你的 DNS
3.3 生产级 Ingress 完整配置(含 HTTPS、限流、CORS)
这份配置是在多个生产项目中打磨出来的,覆盖了最常见的生产需求。每一行注解都有存在的理由:
yaml
# ingress-java-saas-production.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: java-saas-ingress
namespace: production
annotations:
# ── 基础配置 ──────────────────────────────────────────────
# 指定使用 nginx ingress controller(集群可能有多个 ingress controller)
kubernetes.io/ingress.class: "nginx"
# ── HTTPS 证书(配合 cert-manager 自动管理)────────────────
# cert-manager 看到这个注解,会自动申请并续签 Let's Encrypt 证书
# 证书会存储在 spec.tls.secretName 指定的 Secret 中
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# ── 请求限流(防 DDoS、防接口爬虫)───────────────────────
# 同一 IP 最多维持 100 个并发连接
nginx.ingress.kubernetes.io/limit-connections: "100"
# 同一 IP 每秒最多 50 个新请求
nginx.ingress.kubernetes.io/limit-rps: "50"
# 超出限流的请求返回 429(Too Many Requests),而不是默认的 503
nginx.ingress.kubernetes.io/limit-req-status-code: "429"
# ── 超时配置 ────────────────────────────────────────────
# 普通 API 超时 30 秒
nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
nginx.ingress.kubernetes.io/proxy-send-timeout: "30"
# AI 接口单独配 Ingress 并设更长超时(见下方 ai-ingress.yaml)
# ── 请求体大小(文件上传场景)──────────────────────────
# 默认 1m,上传文件必须调大,否则报 413 Request Entity Too Large
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# ── CORS 跨域配置 ──────────────────────────────────────
nginx.ingress.kubernetes.io/enable-cors: "true"
# 生产环境要精确指定允许的域名,不要用 * 通配
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.yoursaas.com,https://admin.yoursaas.com"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, PATCH, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type, X-Trace-Id, X-Tenant-Id"
nginx.ingress.kubernetes.io/cors-max-age: "3600"
# ── 性能优化 ────────────────────────────────────────────
# 开启 GZIP 压缩(JSON 响应通常能压缩 70%,显著降低带宽)
nginx.ingress.kubernetes.io/enable-gzip: "true"
# 开启 HTTP/2(提升多请求并发性能)
nginx.ingress.kubernetes.io/http2-push-preload: "true"
# ── 安全加固 ────────────────────────────────────────────
# 添加安全响应头(防点击劫持、XSS 等)
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Frame-Options: DENY";
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-XSS-Protection: 1; mode=block";
more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
spec:
# TLS 配置:cert-manager 会自动把证书填入这个 Secret
tls:
- hosts:
- api.yoursaas.com
- admin.yoursaas.com
secretName: yoursaas-tls-secret
rules:
# ── API 服务路由 ────────────────────────────────────────
- host: api.yoursaas.com
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: java-saas-api-service
port:
number: 80
- path: /api/v2
pathType: Prefix
backend:
service:
name: java-saas-api-v2-service # v2 版本服务(配合金丝雀发布)
port:
number: 80
# AI 接口路由到专门的 AI 网关(超时更长,单独 Ingress 更灵活)
- path: /api/ai
pathType: Prefix
backend:
service:
name: ai-gateway-service
port:
number: 80
# 健康检查端点(不需要鉴权,直接暴露给监控系统)
- path: /actuator/health
pathType: Prefix
backend:
service:
name: java-saas-api-service
port:
number: 80
# ── 管理后台路由(独立域名,便于单独控制访问权限)──────
- host: admin.yoursaas.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-frontend-service
port:
number: 80
配置 cert-manager 自动签发 HTTPS 证书(一次配置永久生效):
bash
# 安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# 等待 cert-manager 就绪
kubectl wait --for=condition=ready pod -l app=cert-manager -n cert-manager --timeout=60s
yaml
# cluster-issuer-letsencrypt.yaml
# ClusterIssuer 是集群级别的证书签发者,所有 namespace 都可以用
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# Let's Encrypt 生产环境(测试用 https://acme-staging-v02.api.letsencrypt.org/directory)
server: https://acme-v02.api.letsencrypt.org/directory
email: devops@yoursaas.com # 证书到期前 30 天会收到提醒邮件
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx # 通过 Nginx Ingress 完成域名所有权验证
⚠️ 踩坑记录 #1:CORS 配置了但浏览器还是报跨域错误
遇到过好几次这个问题,表现是:Ingress 明明配了 CORS 注解,但前端请求还是报
No 'Access-Control-Allow-Origin' header。根因通常有两个:
原因一 :Java Spring Boot 后端也配置了 CORS(
@CrossOrigin或WebMvcConfigurer),而且配置与 Ingress 的不一致。两层 CORS 相互干扰,导致响应头重复或缺失。解决方案:二选一,要么只在 Ingress 配 CORS,移除 Spring Boot 的 CORS 配置;要么只在 Spring Boot 配,移除 Ingress 的 CORS 注解。不要两边都配。原因二 :
cors-allow-origin配置的域名有尾部斜杠或大小写问题,如https://app.yoursaas.com/(多了斜杠)和浏览器发的Origin: https://app.yoursaas.com不匹配。严格比对字符串,一个字符都不能差。
3.4 金丝雀发布:不停服、无风险地验证新版本
金丝雀发布(Canary Release)的名字来源于矿工带金丝雀下矿------金丝雀先探路,有毒气先死金丝雀,矿工撤退。在软件发布中,先让少量真实流量打到新版本,验证没问题再全量。
Nginx Ingress 的金丝雀功能只需要几行注解,不需要 Istio,这在中小规模团队中非常实用。
完整的金丝雀发布步骤:
bash
# 第一步:部署 v2 版本(此时 v2 还没有流量)
kubectl apply -f java-saas-api-v2-deployment.yaml
kubectl apply -f java-saas-api-v2-service.yaml
# 确认 v2 的 Pod 已经就绪(不就绪不要放流量!)
kubectl rollout status deployment/java-saas-api-v2 -n production
yaml
# 第二步:创建金丝雀 Ingress,让 10% 流量去 v2
# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: java-saas-ingress-canary
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
# ── 金丝雀核心配置 ────────────────────────────────────
nginx.ingress.kubernetes.io/canary: "true" # 开启金丝雀模式
# 策略一:按权重分流(最常用)
# 10% 的请求随机路由到 v2
nginx.ingress.kubernetes.io/canary-weight: "10"
# 策略二:按 HTTP Header 分流(内测用)
# 请求头带 X-Canary: always 的,100% 去 v2
# 这样内部测试人员可以稳定复现 v2 的行为
# nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
# nginx.ingress.kubernetes.io/canary-by-header-value: "always"
# 策略三:按 Cookie 分流(AB 测试用)
# cookie 中有 canary=always 的用户走 v2(适合前端 AB 测试)
# nginx.ingress.kubernetes.io/canary-by-cookie: "canary"
spec:
rules:
- host: api.yoursaas.com
http:
paths:
- path: /api/v1
pathType: Prefix
backend:
service:
name: java-saas-api-v2-service # 指向 v2
port:
number: 80
bash
# 第三步:观察 v2 的错误率和响应时间(在 Grafana 中)
# 同时也可以看日志
kubectl logs -n production -l app=java-saas-api-v2 --tail=50 -f
# 第四步:v2 稳定后,逐步提高权重
kubectl annotate ingress java-saas-ingress-canary -n production \
nginx.ingress.kubernetes.io/canary-weight=30 --overwrite
# 观察 10 分钟...
kubectl annotate ingress java-saas-ingress-canary -n production \
nginx.ingress.kubernetes.io/canary-weight=50 --overwrite
# 观察 10 分钟...
kubectl annotate ingress java-saas-ingress-canary -n production \
nginx.ingress.kubernetes.io/canary-weight=100 --overwrite
# 第五步:全量切换完成后,更新主 Ingress 指向 v2,删除金丝雀 Ingress
kubectl delete ingress java-saas-ingress-canary -n production
# 然后更新主 Ingress 中 /api/v1 的 backend 指向 java-saas-api-v2-service
# 如果 v2 有问题,一键回滚:直接删除金丝雀 Ingress,流量 100% 回到 v1
kubectl delete ingress java-saas-ingress-canary -n production
⚠️ 踩坑记录 #2:金丝雀 Ingress 创建后主 Ingress 也受影响,两个服务都报错
遇到过一次,创建金丝雀 Ingress 后,原本稳定的 v1 服务也开始偶发 502。排查发现,金丝雀 Ingress 的
host和path配置与主 Ingress 完全一样,但backend指向的 v2 Service 此时还没完全就绪,Nginx 在 v1 和 v2 之间轮询时,打到 v2 的请求全部失败。铁律 :创建金丝雀 Ingress 之前,必须先确认 v2 的所有 Pod 已经通过了 readinessProbe(
kubectl rollout status)。
四、Istio Service Mesh:东西向流量的终极治理(按需引入)
4.1 你真的需要 Istio 吗?先问这三个问题
Istio 很强大,但也很重------它会给每个 Pod 注入一个 Envoy sidecar,额外消耗 50-100MB 内存和 0.1 核 CPU。在引入之前,建议先问自己三个问题:
- 微服务数量 ≥ 10 个吗? 服务少的话,Java 代码里加 Resilience4j 就够了,不必引 Istio
- 有安全合规要求,必须服务间通信加密吗? 有的话,Istio 的 mTLS 是最优雅的方案
- 需要不改代码就能做跨服务的流量灰度吗? 需要的话,Istio VirtualService 无可替代
如果三个问题都是"否",先不要上 Istio。如果至少有一个"是",继续往下看。
4.2 Istio 工作原理:代码无感知的流量拦截
Istio 最精妙的设计是"零侵入"------你的 Java 代码完全不知道 Istio 的存在,但所有网络流量都经过 Envoy sidecar 的处理:
bash
没有 Istio 的请求路径:
java-saas-api:8080 ──── TCP ────▶ mysql-service:3306
(明文,无加密,无熔断,无遥测)
有 Istio 的请求路径:
java-saas-api:8080
│ (JVM 发出 TCP 连接)
▼
Envoy sidecar (iptables 拦截,Java 完全无感知)
│ (mTLS 握手,身份验证)
▼ 加密传输
Envoy sidecar (解密,转发)
│
▼
mysql-service:3306
(全程加密,有熔断,有延迟统计,有访问控制)
这个"无感知拦截"是通过 iptables 规则实现的。Istio 的 InitContainer(istio-init)在 Pod 启动时修改 iptables,让所有进出 Pod 的流量都经过 localhost:15001(Envoy 监听的端口)。Java 程序以为自己在直接连 mysql-service,实际上连接被透明地路由到了 Envoy。
4.3 安装 Istio 并开启 sidecar 注入
bash
# 下载 istioctl(建议固定版本,避免生产环境随意升级)
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.20.0 sh -
export PATH=$PWD/istio-1.20.0/bin:$PATH
# 验证集群兼容性
istioctl x precheck
# ✔ No issues found when checking the cluster. Istio is safe to install or upgrade!
# 安装 Istio(生产环境用 default profile,比 demo 更轻量)
istioctl install --set profile=default -y
# 验证 Istio 控制面就绪
kubectl get pods -n istio-system
# NAME READY STATUS RESTARTS
# istiod-6f9d65d9b7-xk2p1 1/1 Running 0 ← 控制面
# istio-ingressgateway-7d9f8b-mn4q7 1/1 Running 0 ← 入口网关
# 为 production namespace 开启 sidecar 自动注入
# 之后在此 namespace 创建的 Pod 都会自动注入 istio-proxy
kubectl label namespace production istio-injection=enabled
# 重新部署现有的 Pod,让 sidecar 生效
kubectl rollout restart deployment -n production
# 验证 sidecar 已注入(READY 列应该是 2/2,而不是 1/1)
kubectl get pods -n production
# NAME READY STATUS
# java-saas-api-xxx-yyy 2/2 Running ← 2 个容器:业务容器 + istio-proxy
⚠️ 踩坑记录 #3:开启 Istio 注入后,Java 应用启动时报
Connection refused这是 Istio 注入后最常见的坑,几乎每个第一次上 Istio 的团队都会踩。
场景:Java Spring Boot 启动时需要连接 MySQL(Flyway 数据库迁移),但此时
istio-proxy容器还没完全启动,iptables 规则还没生效,Java 发出的连接被 Envoy 拒绝(因为 Envoy 还没就绪就已经在拦截流量了)。结果是 Spring Boot 启动失败,Pod 进入 CrashLoopBackOff。现象特征:日志里看到类似
Connection refused to mysql-service:3306,但手动进 Pod 执行curl mysql-service:3306又是通的------因为那时候 Envoy 已经就绪了。解决方案(选其一):
方案 A:在 Pod annotations 加一行,让 Java 应用等 Envoy 完全就绪后再启动:
yamlspec: template: metadata: annotations: proxy.istio.io/config: | holdApplicationUntilProxyStarts: true方案 B(Istio 1.7+):在 istio ConfigMap 中全局开启(不用每个 Deployment 都加):
bashistioctl install --set profile=default \ --set meshConfig.defaultConfig.holdApplicationUntilProxyStarts=true -y
4.4 VirtualService + DestinationRule:东西向流量的精细治理
这是 Istio 最核心的两个 CRD,理解了它们,Istio 80% 的用法就掌握了。
DestinationRule 定义"到达目标服务时的处理策略"(连接池、熔断、负载均衡算法),VirtualService 定义"流量如何路由到目标服务"(比例分流、Header 匹配、重试、超时)。两者配合使用:
yaml
# ── DestinationRule:定义服务策略 ─────────────────────────────────
# destination-rule-java-saas.yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: java-saas-api-dr
namespace: production
spec:
host: java-saas-api-service # 对应 K8s Service 的名称
trafficPolicy:
# 连接池配置(防止下游服务因连接数过多被压垮)
connectionPool:
tcp:
maxConnections: 200 # TCP 连接池上限
connectTimeout: 5s
http:
http1MaxPendingRequests: 100 # 排队等待的最大请求数
http2MaxRequests: 1000 # H2 最大并发请求数
maxRequestsPerConnection: 100 # 每个连接最多处理 100 个请求(防连接复用过久)
# 熔断(Outlier Detection):自动摘除不健康的 Pod
# 原理:持续统计每个 Pod 的错误率,超标则临时从负载均衡中摘除
outlierDetection:
consecutiveGatewayErrors: 5 # 连续 5 次 502/503/504 就触发熔断
consecutiveLocalOriginErrors: 5 # 或连续 5 次连接失败
interval: 30s # 每 30 秒统计一次
baseEjectionTime: 30s # 第一次被熔断,隔离 30 秒
maxEjectionPercent: 50 # 最多熔断 50% 的 Pod(保证服务还能用)
minHealthPercent: 30 # 如果健康 Pod < 30%,停止熔断(避免雪崩)
# 负载均衡策略
loadBalancer:
simple: LEAST_CONN # 最少连接数(比默认轮询对 Java 服务更友好)
# 定义版本子集(用于 VirtualService 中的灰度路由)
subsets:
- name: v1
labels:
version: "1.0.0" # 匹配 Pod label version=1.0.0
- name: v2
labels:
version: "2.0.0"
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 50 # v2 作为金丝雀,给更小的连接池
---
# ── VirtualService:定义路由规则 ────────────────────────────────
# virtual-service-java-saas.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: java-saas-api-vs
namespace: production
spec:
hosts:
- java-saas-api-service # 拦截所有发往这个 Service 的流量
http:
# ── 规则 1(优先匹配):内测用户通过 Header 固定走 v2 ──
# 只要请求头带 X-Canary: true,100% 路由到 v2
# 测试、QA、产品同学可以在浏览器插件里固定加这个 Header
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: java-saas-api-service
subset: v2
timeout: 60s
# ── 规则 2:正常流量按比例分流 ─────────────────────────
- route:
- destination:
host: java-saas-api-service
subset: v1
weight: 90 # 90% 到 v1
- destination:
host: java-saas-api-service
subset: v2
weight: 10 # 10% 到 v2(金丝雀)
# 超时配置(比 Java 代码里的更靠近网络层,更准确)
timeout: 30s
# 重试策略:网络抖动时自动重试,Java 代码不用处理
retries:
attempts: 3
perTryTimeout: 8s
# 只在以下情况重试(幂等请求),不要对 POST 写操作重试
retryOn: "gateway-error,connect-failure,retriable-4xx"
4.5 mTLS:让服务间通信强制加密
开启 mTLS 后,集群内所有服务间通信都会经过双向 TLS 握手------不只是加密,还有身份验证(确保通信双方都是可信的集群内服务,而不是攻击者伪造的请求)。
yaml
# 为 production namespace 开启严格 mTLS
# STRICT 模式:所有进入 production namespace 的流量必须是 mTLS
# 如果某个旧服务还在发明文 HTTP,会被拒绝(迁移期改用 PERMISSIVE)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default-mtls
namespace: production
spec:
mtls:
mode: STRICT
---
# 精细访问控制:只允许 java-saas-api 访问 MySQL Pod
# 即使其他服务拿到了 MySQL 的 Service DNS,也无法建立连接
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: mysql-access-policy
namespace: production
spec:
selector:
matchLabels:
app: mysql
action: ALLOW
rules:
- from:
- source:
# 基于 mTLS 证书中的服务身份(ServiceAccount),不是 IP 地址
# IP 地址可以被伪造,ServiceAccount 证书不能
principals:
- "cluster.local/ns/production/sa/java-saas-api-sa"
五、NetworkPolicy:不用 Istio 也能做基础网络隔离
Istio 的 AuthorizationPolicy 工作在 L7(HTTP 层),NetworkPolicy 工作在 L3/L4(IP + 端口层)。它们不是替代关系,而是互补:NetworkPolicy 是防火墙,Istio 是应用层访问控制。
对于没有引入 Istio 的团队,NetworkPolicy 是实现网络隔离的基础工具,必须掌握。
最重要的一条规则是:先默认拒绝所有流量,再精确放行需要的流量。
yaml
# ── 第一步:默认拒绝 production namespace 的所有进出流量 ───────────
# 这是零信任的基础,不设这条,后面的放行规则就是摆设
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # {} 匹配 namespace 内所有 Pod
policyTypes:
- Ingress
- Egress
---
# ── 第二步:放行 DNS(这条必须在 default-deny-all 之后立刻加!)───
# 忘了这条,Pod 内所有域名解析失败,Java 报 UnknownHostException
# 这是开启 NetworkPolicy 后最常见的新手坑
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53 # TCP DNS(大于 512 字节的响应用 TCP)
---
# ── 第三步:放行 Nginx Ingress → java-saas-api 的流量 ────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: java-saas-api # 应用到 API Pod
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx # 只允许 Ingress Controller
ports:
- port: 8080
---
# ── 第四步:放行 java-saas-api → mysql 的流量 ────────────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-mysql
namespace: production
spec:
podSelector:
matchLabels:
app: mysql # 应用到 MySQL Pod(定义谁能访问我)
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: java-saas-api # 只有带这个 Label 的 Pod 可以访问
ports:
- port: 3306
---
# ── 第五步:放行 API Pod → 外部 AI 服务的 HTTPS 出站流量 ─────────
# 调用 OpenAI / 通义千问 等外部 AI API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-external-ai
namespace: production
spec:
podSelector:
matchLabels:
app: java-saas-api
policyTypes:
- Egress
egress:
- ports:
- port: 443 # 只放行 HTTPS(外部 AI API 都是 HTTPS)
protocol: TCP
# 注意:这里没有 to 字段,意味着可以访问任意外部 IP 的 443 端口
# 如果 IP 范围已知,可以用 ipBlock 精确限制
💡 调试 NetworkPolicy 的技巧 :策略生效后,直接进 Pod 用
curl和telnet验证连通性:
bash# 验证 MySQL 连通(应该能连接) kubectl exec -it deployment/java-saas-api -n production -- \ nc -zv mysql-service 3306 # 验证 Redis 是否被拒绝(应该 Connection refused 或 timeout) kubectl exec -it deployment/java-saas-api -n production -- \ nc -zv redis-service 6379 -w 3 # 验证 DNS 是否正常(应该立即返回) kubectl exec -it deployment/java-saas-api -n production -- \ nslookup mysql-service
六、综合实战:完整的 Java SaaS 网络配置一键部署
把本文所有内容整合为一个可以直接 kubectl apply 的配置包。这是一个"最小可用生产配置",可以作为你自己项目的起点:
bash
# 部署顺序:
# 1. 基础网络策略(先建隔离,再建通道)
kubectl apply -f network-policy-default-deny.yaml
kubectl apply -f network-policy-allow-dns.yaml
kubectl apply -f network-policy-allow-ingress-to-api.yaml
kubectl apply -f network-policy-allow-api-to-mysql.yaml
# 2. 安装并配置 Ingress Controller(如果还没安装)
helm upgrade --install ingress-nginx ...
# 3. 配置 cert-manager 和 HTTPS 证书签发器
kubectl apply -f cluster-issuer-letsencrypt.yaml
# 4. 创建 Ingress 规则
kubectl apply -f ingress-java-saas-production.yaml
# 5. 验证整条链路
# 解析 DNS(应该解析到云 LB 的公网 IP)
nslookup api.yoursaas.com
# 访问健康检查(应该返回 200)
curl -v https://api.yoursaas.com/actuator/health
# 验证证书(应该看到 Let's Encrypt 签发的证书)
curl -v https://api.yoursaas.com 2>&1 | grep "issuer"
# 验证限流(快速发 100 个请求,超过 rps=50 的应该返回 429)
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" https://api.yoursaas.com/api/v1/health &
done | sort | uniq -c
七、常见问题 FAQ
Q1:Ingress Controller 和 API Gateway(Kong/APISIX)各自的定位是什么?
Ingress Controller 是 K8s 生态的标准七层入口,职责是将外部流量路由到正确的 Service,附带基础的限流和证书管理。API Gateway 是业务层的 API 管理工具,功能更丰富:JWT 认证鉴权、API 版本管理、开发者门户、精细化计费统计等。两者通常是串联关系:外部流量先经过 Ingress Controller(做 TLS 终止和基础路由),再进入 API Gateway(做业务层 API 治理)。如果你的 SaaS 有开放平台需求(API 对第三方开发者开放),一定需要 API Gateway;如果只是内部微服务,Ingress Controller 够用。
Q2:Istio 引入后,Spring Cloud 的 Feign 熔断还需要吗?
这是个争议性问题。Istio 的熔断(Outlier Detection)工作在 Sidecar 层,是基于连接级别的;Resilience4j 工作在 Java 应用层,可以感知业务逻辑错误(如数据库查询超时返回 null 但 HTTP 200)。两者维度不同,不是简单替代关系。笔者的实践建议:Istio 负责网络层的熔断和重试(可以移除 Feign 的 Ribbon 负载均衡,由 Envoy 接管),保留 Resilience4j 处理业务层的降级逻辑。这样既减少了 Java 代码中的网络治理样板代码,又保留了业务层的精细控制。
Q3:Spring Cloud Gateway 和 Nginx Ingress 二选一还是都要用?
如果你已经在用 Spring Cloud Gateway 做微服务网关,它可以作为 K8s 集群内的一层路由;Nginx Ingress 仍然是集群的外部入口。两者不冲突,形成两级路由:互联网 → Nginx Ingress(TLS 终止、域名路由)→ Spring Cloud Gateway(认证、限流、服务路由)→ 各微服务。如果没有历史包袱,新项目可以考虑用 Nginx Ingress + Istio 替代 Spring Cloud Gateway,减少技术栈复杂度。
Q4:服务网格一定要用 Istio 吗?有没有更轻量的替代品?
Istio 功能最全但也最重。轻量替代方案:① Linkerd :只关注 mTLS 和可观测性,比 Istio 轻 10 倍,学习曲线低,适合中小规模;② Cilium (基于 eBPF):不需要 sidecar,直接在内核层做流量拦截,性能损耗极小,是近两年的热门选择;③ 不用 Service Mesh:微服务数量 < 10 个,Java 侧用 Resilience4j + Spring Security 处理熔断和通信安全,完全够用。选型原则:够用就好,不要为了技术而技术。
Q5:NetworkPolicy 实际生效需要哪些前提?
NetworkPolicy 本身只是 K8s API 对象,实际执行需要 CNI 插件支持。Flannel 不支持 NetworkPolicy(它只提供基础网络,不做策略执行)。支持 NetworkPolicy 的 CNI:Calico、Cilium、Weave Net、Canal(Flannel + Calico 混合)。如果你的集群用了 Flannel,创建 NetworkPolicy 对象不会报错,但策略完全不生效,这是个非常隐蔽的陷阱。确认方法:kubectl get pods -n kube-system | grep -E "calico|cilium|weave",看有没有对应的 CNI Pod 在运行。
总结
本文沿着"请求从外部进入集群,到最终到达 Java Pod"这条路径,系统讲解了 K8s 的四层网络体系:
| 层次 | 核心组件 | 解决的核心问题 | Java SaaS 关注点 |
|---|---|---|---|
| DNS 解析 | CoreDNS | 服务名 → ClusterIP | Spring Boot 用 Service 名连 MySQL,DNS 超时排查 |
| 外部入口 | Nginx Ingress | 互联网流量路由进集群 | HTTPS、限流、CORS、金丝雀发布 |
| 内部通信 | Istio + Envoy | 服务间流量治理与加密 | 零代码改动的 mTLS、熔断、重试 |
| 网络隔离 | NetworkPolicy | Pod 级别防火墙 | 数据库访问白名单、多租户隔离 |
💬 一句话总结:K8s 网络的每一层都有明确分工------CoreDNS 是"电话簿",Ingress 是"大楼前台",Istio 是"内部安保",NetworkPolicy 是"防火墙"。Java 工程师掌握这四层,就能独立设计和排查生产级微服务网络架构,而不需要每次都依赖运维同学。
第02篇 :K8s 存储与配置管理:ConfigMap、Secret、PV/PVC 实战------Java SaaS 多租户配置最佳实践
第04篇 :K8s 弹性伸缩实战:HPA、VPA、KEDA------Java SaaS 应对流量洪峰的秘密武器
📝 如果本文对你有帮助,欢迎点赞收藏,你的支持是持续创作的最大动力!
有问题欢迎评论区留言,笔者看到必回。