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

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(@CrossOriginWebMvcConfigurer),而且配置与 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 的 hostpath 配置与主 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。在引入之前,建议先问自己三个问题:

  1. 微服务数量 ≥ 10 个吗? 服务少的话,Java 代码里加 Resilience4j 就够了,不必引 Istio
  2. 有安全合规要求,必须服务间通信加密吗? 有的话,Istio 的 mTLS 是最优雅的方案
  3. 需要不改代码就能做跨服务的流量灰度吗? 需要的话,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 完全就绪后再启动:

yaml 复制代码
spec:
  template:
    metadata:
      annotations:
        proxy.istio.io/config: |
          holdApplicationUntilProxyStarts: true

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

bash 复制代码
istioctl 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 用 curltelnet 验证连通性:

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 应对流量洪峰的秘密武器


📝 如果本文对你有帮助,欢迎点赞收藏,你的支持是持续创作的最大动力!

有问题欢迎评论区留言,笔者看到必回。

相关推荐
@卓越俊逸_角立杰出@1 小时前
深度拆解跨境支付系统架构:从资金流、账本系统到全球清算网络设计
网络·系统架构
被考核重击1 小时前
前端高频面试题总结_性能_工程化_网络
前端·网络·性能优化·工程化
成为你的宁宁1 小时前
【Kubernetes监控实战:NFS持久化存储 + Prometheus Operator + etcd监控】
kubernetes·prometheus·etcd
Multipath7121 小时前
多卡多链路聚合路由器的原理、关键技术分析
网络·5g·安全·智能路由器·无人机·实时音视频
江华森1 小时前
Sealos 部署 Kubernetes 高可用集群 — 生产级技术笔记
笔记·容器·kubernetes
MAXrxc1 小时前
BGP小作业
网络
飞函安全1 小时前
石油化工企业园区面积大、网络复杂,飞函如何保证跨区域协同不掉线
网络·安全·私有化im
梦奇不是胖猫1 小时前
[ 计算机网络 | 第四章 ] 网络层 03 如何选择路径?
网络·计算机网络·智能路由器
艾莉丝努力练剑1 小时前
【Linux网络】传输层协议TCP(六)补充 - 面试题:HTTP 获取网页的完整过程
linux·运维·网络·tcp/ip·计算机网络·http·udp