欢迎光临
我们一直在努力

第03篇:K8s 网络深度解析:Ingress、Service Mesh 与 CoreDNS——Java 微服务通信全链路剖析(生产级实战)

前言:网络是 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 网络学得一头雾水,根本原因是脑子里没有这张图。

┌───────────────────────────────────────────────────────────────────┐
│ 互联网用户 │
│ 浏览器 / 移动端 / 第三方系统 │
└───────────────────────┬───────────────────────────────────────────┘
│ 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 记录,格式固定为:

<service-name>.<namespace>.svc.cluster.local → ClusterIP

Pod 内的 DNS 解析请求会先发给 CoreDNS(ClusterIP 通常是 10.96.0.10),CoreDNS 查询 K8s API 后返回对应的 ClusterIP。

不同访问场景下,DNS 名称的写法不同:

# ── 场景 1:同一 namespace 内访问(最简写法)──────────────────
# production namespace 内的 java-saas-api 访问 mysql-service
spring:
datasource:
url: jdbc:mysql://mysqlservice:3306/saasdb
# CoreDNS 会自动补全为 mysql-service.production.svc.cluster.local

# ── 场景 2:跨 namespace 访问(必须用完整 DNS 名或至少加 namespace)
# production namespace 的服务访问 ai-platform namespace 的 LLM 服务
spring:
ai:
base-url: http://llmservice.aiplatform:8080
# 或完整写法(在极少数 DNS 配置特殊的集群中更稳定):
# http://llm-service.ai-platform.svc.cluster.local:8080

# ── 场景 3:访问集群外部服务(用 ExternalName Service 包装)──
# 把外部 MySQL RDS 的域名包装成集群内可用的 Service 名称
# 这样迁移到集群内 MySQL 时,Java 代码不需要修改任何配置

创建 ExternalName Service,让 Java 应用通过集群内名称访问外部 RDS:

# external-mysql-service.yaml
# 这个 Service 没有 selector,也没有 endpoints
# 只是把集群内的 DNS 名称 "mysql-service" 映射到外部域名
apiVersion: v1
kind: Service
metadata:
name: mysqlservice # Java 应用配置的名称保持不变
namespace: production
spec:
type: ExternalName
externalName: yourrds.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 秒)。

排查方法:

# 进入出问题的 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 配置中减少无效查询次数:

# 在 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 添加自定义解析规则:

# 修改 CoreDNS 配置(谨慎操作,改错会影响整个集群 DNS)
# 先备份现有配置:
# kubectl get configmap coredns -n kube-system -o yaml > coredns-backup.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kubesystem
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 legacypayment.company.internal
10.0.0.101 legacycrm.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

# 使用 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)

这份配置是在多个生产项目中打磨出来的,覆盖了最常见的生产需求。每一行注解都有存在的理由:

# ingress-java-saas-production.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: javasaasingress
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: yoursaastlssecret

rules:
# ── API 服务路由 ────────────────────────────────────────
host: api.yoursaas.com
http:
paths:
path: /api/v1
pathType: Prefix
backend:
service:
name: javasaasapiservice
port:
number: 80

path: /api/v2
pathType: Prefix
backend:
service:
name: javasaasapiv2service # v2 版本服务(配合金丝雀发布)
port:
number: 80

# AI 接口路由到专门的 AI 网关(超时更长,单独 Ingress 更灵活)
path: /api/ai
pathType: Prefix
backend:
service:
name: aigatewayservice
port:
number: 80

# 健康检查端点(不需要鉴权,直接暴露给监控系统)
path: /actuator/health
pathType: Prefix
backend:
service:
name: javasaasapiservice
port:
number: 80

# ── 管理后台路由(独立域名,便于单独控制访问权限)──────
host: admin.yoursaas.com
http:
paths:
path: /
pathType: Prefix
backend:
service:
name: adminfrontendservice
port:
number: 80

配置 cert-manager 自动签发 HTTPS 证书(一次配置永久生效):

# 安装 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

# cluster-issuer-letsencrypt.yaml
# ClusterIssuer 是集群级别的证书签发者,所有 namespace 都可以用
apiVersion: certmanager.io/v1
kind: ClusterIssuer
metadata:
name: letsencryptprod
spec:
acme:
# Let's Encrypt 生产环境(测试用 https://acme-staging-v02.api.letsencrypt.org/directory)
server: https://acmev02.api.letsencrypt.org/directory
email: devops@yoursaas.com # 证书到期前 30 天会收到提醒邮件
privateKeySecretRef:
name: letsencryptprodkey
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,这在中小规模团队中非常实用。

完整的金丝雀发布步骤:

# 第一步:部署 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

# 第二步:创建金丝雀 Ingress,让 10% 流量去 v2
# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: javasaasingresscanary
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: javasaasapiv2service # 指向 v2
port:
number: 80

# 第三步:观察 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 的处理:

    没有 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 注入

    # 下载 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 完全就绪后再启动:

    spec:
    template:
    metadata:
    annotations:
    proxy.istio.io/config: |
    holdApplicationUntilProxyStarts: true

    方案 B(Istio 1.7+):在 istio ConfigMap 中全局开启(不用每个 Deployment 都加):

    istioctl install –set profile=default \\
    –set meshConfig.defaultConfig.holdApplicationUntilProxyStarts=true -y

    4.4 VirtualService + DestinationRule:东西向流量的精细治理

    这是 Istio 最核心的两个 CRD,理解了它们,Istio 80% 的用法就掌握了。

    DestinationRule 定义"到达目标服务时的处理策略"(连接池、熔断、负载均衡算法),VirtualService 定义"流量如何路由到目标服务"(比例分流、Header 匹配、重试、超时)。两者配合使用:

    # ── DestinationRule:定义服务策略 ─────────────────────────────────
    # destination-rule-java-saas.yaml
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
    name: javasaasapidr
    namespace: production
    spec:
    host: javasaasapiservice # 对应 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: javasaasapivs
    namespace: production
    spec:
    hosts:
    javasaasapiservice # 拦截所有发往这个 Service 的流量
    http:
    # ── 规则 1(优先匹配):内测用户通过 Header 固定走 v2 ──
    # 只要请求头带 X-Canary: true,100% 路由到 v2
    # 测试、QA、产品同学可以在浏览器插件里固定加这个 Header
    match:
    headers:
    x-canary:
    exact: "true"
    route:
    destination:
    host: javasaasapiservice
    subset: v2
    timeout: 60s

    # ── 规则 2:正常流量按比例分流 ─────────────────────────
    route:
    destination:
    host: javasaasapiservice
    subset: v1
    weight: 90 # 90% 到 v1
    destination:
    host: javasaasapiservice
    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 握手——不只是加密,还有身份验证(确保通信双方都是可信的集群内服务,而不是攻击者伪造的请求)。

    # 为 production namespace 开启严格 mTLS
    # STRICT 模式:所有进入 production namespace 的流量必须是 mTLS
    # 如果某个旧服务还在发明文 HTTP,会被拒绝(迁移期改用 PERMISSIVE)
    apiVersion: security.istio.io/v1beta1
    kind: PeerAuthentication
    metadata:
    name: defaultmtls
    namespace: production
    spec:
    mtls:
    mode: STRICT


    # 精细访问控制:只允许 java-saas-api 访问 MySQL Pod
    # 即使其他服务拿到了 MySQL 的 Service DNS,也无法建立连接
    apiVersion: security.istio.io/v1beta1
    kind: AuthorizationPolicy
    metadata:
    name: mysqlaccesspolicy
    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 是实现网络隔离的基础工具,必须掌握。

    最重要的一条规则是:先默认拒绝所有流量,再精确放行需要的流量。

    # ── 第一步:默认拒绝 production namespace 的所有进出流量 ───────────
    # 这是零信任的基础,不设这条,后面的放行规则就是摆设
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
    name: defaultdenyall
    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: allowdnsegress
    namespace: production
    spec:
    podSelector: {}
    policyTypes:
    Egress
    egress:
    to:
    namespaceSelector:
    matchLabels:
    kubernetes.io/metadata.name: kubesystem
    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: allowingresstoapi
    namespace: production
    spec:
    podSelector:
    matchLabels:
    app: javasaasapi # 应用到 API Pod
    policyTypes:
    Ingress
    ingress:
    from:
    namespaceSelector:
    matchLabels:
    kubernetes.io/metadata.name: ingressnginx # 只允许 Ingress Controller
    ports:
    port: 8080


    # ── 第四步:放行 java-saas-api → mysql 的流量 ────────────────────
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
    name: allowapitomysql
    namespace: production
    spec:
    podSelector:
    matchLabels:
    app: mysql # 应用到 MySQL Pod(定义谁能访问我)
    policyTypes:
    Ingress
    ingress:
    from:
    podSelector:
    matchLabels:
    app: javasaasapi # 只有带这个 Label 的 Pod 可以访问
    ports:
    port: 3306


    # ── 第五步:放行 API Pod → 外部 AI 服务的 HTTPS 出站流量 ─────────
    # 调用 OpenAI / 通义千问 等外部 AI API
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
    name: allowapitoexternalai
    namespace: production
    spec:
    podSelector:
    matchLabels:
    app: javasaasapi
    policyTypes:
    Egress
    egress:
    ports:
    port: 443 # 只放行 HTTPS(外部 AI API 都是 HTTPS)
    protocol: TCP
    # 注意:这里没有 to 字段,意味着可以访问任意外部 IP 的 443 端口
    # 如果 IP 范围已知,可以用 ipBlock 精确限制

    💡 调试 NetworkPolicy 的技巧:策略生效后,直接进 Pod 用 curl 和 telnet 验证连通性:

    # 验证 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 的配置包。这是一个"最小可用生产配置",可以作为你自己项目的起点:

    # 部署顺序:
    # 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 应对流量洪峰的秘密武器


    📝 如果本文对你有帮助,欢迎点赞收藏,你的支持是持续创作的最大动力! 有问题欢迎评论区留言,笔者看到必回。

    赞(0)
    未经允许不得转载:171主机测评 » 第03篇:K8s 网络深度解析:Ingress、Service Mesh 与 CoreDNS——Java 微服务通信全链路剖析(生产级实战)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址