K8s 集群里要想把流量安稳地送到后端服务离不开一个默认答案nginx-ingress-controller。干了这么多年容器平台相关的工作我几乎在每个集群里都能见到它的身影它承担的是整个集群南北向流量的门户角色负责把外部请求按域名、按路径精确地转发到后端的 Pod 上。很多刚接触 k8s 的朋友会误以为 Ingress 本身就是一个网关组件其实它只是一份资源清单真正干活的正是 nginx-ingress-controller 这个流量大管家。这篇文章我想把 nginx-ingress-controller 从原理到落地再到排障的完整链路梳理一遍适合正在规划集群流量入口、或者已经被 502、证书、重定向问题折磨过的运维和开发同学。你不需要提前懂多少 Nginx 细节只要跑过基本的 k8s 部署跟着这篇文章走一遍就能搞清楚它到底是怎么工作的以及出了问题时该从哪里下手。1. 流量为什么需要大管家Ingress Controller 的原理与定位1.1 从 NodePort 到 Ingress流量入口的演进逻辑K8s 集群里的 Pod 是随时可能被重建的每次重建 IP 都会变化所以访问服务需要一个稳定的入口。Service 提供的是集群内部的虚拟 IP外部要访问传统方式就是 NodePort 和 LoadBalancer。NodePort 简单粗暴在每个节点上开一个高位端口把流量转给对应 Service但端口资源有限而且业务一多端口管理就是一场灾难。LoadBalancer 需要依赖云环境或者专门的负载均衡设备每暴露一个服务就要创建一个 LB成本高、维护难更重要的是它不具备七层路由能力没法根据域名、路径做灵活分发。Ingress 的出现就是为了解决这个矛盾它把外部流量如何路由到集群内部服务这件事抽象成一份声明式配置。你可以把它理解为网关的路由表——定义哪个域名、哪个路径对应到哪个 Service。但是这份路由表本身不执行任何转发动作真正读取它、转换成实际 Nginx 配置并对外提供服务的是 Ingress Controller。以 nginx-ingress-controller 为例它就是一个控制器逻辑 Nginx 进程的组合体全集群一般只需要部署一套就能撑起成百上千个服务的对外暴露。这里就引出一个关键点Ingress 不是 Service 的替代品而是构建在 Service 之上的一层路由抽象。流量路径是 客户端 - Ingress Controller - Service - PodController 拿到 Ingress 资源后会把 Service 背后的真实 Pod 地址解析出来写进自己的负载均衡上游列表。所以哪怕 Service 的 ClusterIP 是虚拟的最终转发的目的地仍然是你那些具体、健康的 Pod。1.2 nginx-ingress-controller 的工作原理watch、模板与热更新很多人在第一次看 nginx-ingress-controller 的 Pod 时会发现它里面有两个容器一个是 controller另一个是 Nginx。Controller 这个容器专门负责和 Kubernetes API 通信它通过 informer 机制实时监听 Ingress、Service、Endpoint、Secret 等资源的变化然后根据内置的模板引擎生成一份新的 nginx.conf再通过信号让 Nginx 平滑重载配置。Nginx 容器本身不感知 Kubernetes 的存在它只负责高效转发流量真正的大脑在 controller 里。这个机制里最容易被忽略的是 Endpoint 的作用。你在 Ingress 里写的是 serviceName但 controller 最终写入 Nginx upstream 的不是 Service 的 ClusterIP而是这个 Service 对应的 Endpoint 列表也就是真实 Pod 的 IP 和端口。Kubernetes 会持续维护 Endpoint一旦某个 Pod 挂掉Endpoint 发生变化controller 会立刻感知并重新生成 upstream 配置自动把故障 Pod 摘除。这个过程是实时发生的不需要人工干预这也是 nginx-ingress-controller 能作为生产级流量入口的核心保证。关于热更新很多人担心 Nginx reload 会不会导致连接中断。实际测试下来Nginx 的 reload 是平滑的新的 worker 进程启动后旧的 worker 会继续处理存量连接直到处理完再退出。对短连接请求几乎无感知对长连接会有一点延迟但整体影响非常小。所以 controller 每次配置变化都走 reload 是没问题的这也是它响应速度快的原因之一。2. 核心配置拆解Ingress 资源与 Controller 的配合机制2.1 Ingress 资源 YAML 的四大关键段在 Kubernetes 中Ingress 资源的 spec 主要由 rules 和 tls 组成rules 里又要关注 host、http.paths 以及每个 path 对应的 backend。架构上看一个成熟的 Ingress 对象简单来说就是域名 路径 转发目标的绑定但表达起来有几个细节需要注意。先看一份标准的配置示例apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress namespace: demo annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: /old(/|$)(.*) pathType: ImplementationSpecific backend: service: name: demo-service port: number: 8080 tls: - hosts: - api.example.com secretName: api-tls-secret注意 pathType 字段这是 networking.k8s.io/v1 之后必须显式声明的。它有三个取值Prefix前缀匹配/api 可以匹配 /api、/api/v1、/api/v1/users 等。Exact全路径精确匹配容不得一点偏差。ImplementationSpecific匹配规则完全由当前 Ingress Controller 自己决定。nginx-ingress-controller 对这个类型的处理方式更灵活它会结合重写规则和正则表达式的语义来做匹配。我建议在大多数场景中优先使用 Prefix它符合人类直觉也不容易出歧义。但如果你要在 path 里写正则那就只能选择 ImplementationSpecific。还有一个容易踩的坑Prefix 模式下/foo 和 /foo/ 会被 Kubernetes 规范化后视为同一路径而当路径末端含正则捕获组时必须配合 rewrite-target 才能正确转发否则后端收到的路径会跟预期不一致。2.2 常用 annotations 解析与踩坑记录annotations 是 nginx-ingress-controller 的核心武器它相当于把 Nginx 的配置能力以注解的方式透传出来。不同版本的 nginx-ingress-controller 支持的注解前缀统一为 nginx.ingress.kubernetes.io/但在社区迭代过程中有些字段名出现过细微变化所以参考时要留意版本。下面列几个我几乎每个集群都会用到的注解以及对应的坑rewrite-target这是重写机制中最常用的取值符合 Nginx rewrite 语法。当你把 path 写成正则并带上捕获组时rewrite-target 可以用 $1、$2 引用。它最常见的坑是把路径改得过于激进导致后端路由匹配不上直接 404。我的建议是 rewrite-target 里的目标路径一定要先在后端确认存在然后再绑定。ssl-redirect 与 force-ssl-redirectssl-redirect 在 Ingress 配置了 TLS 时自动把 HTTP 请求 301 到 HTTPSforce-ssl-redirect 则不管有没有 TLS 都会强制跳转。前者更适合温和的灰度策略后者适合全站强制 HTTPS 的场景。需要注意如果前面还有 CDN 或者云 LB 已经做过 HTTPS 卸载这里再跳转一次会多一跳要仔细检查链路。proxy-body-sizeNginx 默认允许的请求体大小是 1MB超过就会直接 413。凡是涉及文件上传的 Ingress必须显式调大这个值比如 20m、50m甚至按需更大会更好。这里说的是 Nginx 这一层的限制跟后端服务自己的限制是叠加关系两层都要检查。limit-rps 和 limit-connections按客户端 IP 做限流。用起来方便但有一个隐藏问题如果请求经过云 LoadBalancer 转发过来Nginx 看到的来源 IP 往往是 LoadBalancer 的内网 IP而不是真实客户端 IP限流策略就完全失效了。这种情况需要先解决真实 IP 透传的问题具体方法在第四章讲。configuration-snippet这个注解可以直接往 Nginx 的 server 块或 location 块里插入任意自定义片段功能非常强大但风险极高。一旦片段里的语法有问题整个 controller 生成的配置就 reload 失败影响的是所有 Ingress 规则不只有你这一条。我见过不止一次团队因为这个注解写错一个分号导致整个集群入口瘫痪的案例。能不用就不用实在要用也必须打开 Controller 的 ConfigMap 里 allow-snippet-annotations 开关并且设好变更评审流程。2.3 Controller 自身的配置通道ConfigMap 与 IngressClass除了每条 Ingress 自身的注解Controller 本身还有两个重要的配置通道一是全局 ConfigMap二是 IngressClass 资源。全局 ConfigMap 一般叫 ingress-nginx-controller位于 ingress-nginx 命名空间里面可以设置 proxy-connect-timeout、proxy-read-timeout、log-format-upstream、gzip 等全局参数。Controller 启动后会一直 watch 这份 ConfigMap修改后它会自动应用不需要重启 Pod。生产环境我一般会把 log-format-upstream 改成包含 $upstream_addr 和 $request_time 的格式排查问题的时候能省非常多的时间。另外 proxy-body-size 如果要做全局统一限制也可以在这里配省得每条 Ingress 都写注解。IngressClass 是较新版本里引入的资源用来解决一个集群里同时存在多个 Ingress Controller 时的路由隔离问题。比如你既有 nginx-ingress-controller又有 traefik就可以创建两个 IngressClass每个 Controller 只处理指定 class 的 Ingress。在部署 nginx-ingress-controller 时它默认会创建一个名为 nginx 的 IngressClass并且要求 Ingress 资源的 spec.ingressClassName 字段指向它。注意老的 kubernetes.io/ingress.class 注解在新版本里虽然还能用但已进入弃用流程新建资源建议直接写 ingressClassName 字段。小区分一下如果集群里没有指定 ingressClassName某些 Controller 会按默认 IngressClass来处理而有的版本里如果存在多个候选行为会变得不确定。所以最好的习惯是每个 Ingress 里都显式写清楚 ingressClassName不要依赖默认值。3. 从部署到上线nginx-ingress-controller 的完整实操3.1 安装前的网络与资源规划在真正执行 k8s 安装部署并交付 Ingress 之前先别急着敲命令想清楚三件事Controller 用什么方式对外暴露、放在哪些节点上、要不要保留客户端真实源 IP。公有云场景最省事的方式是给 Controller 的 Service 绑定 LoadBalancer云平台自动分配公网 IP流量链路变成 客户端 - 云LB - Controller Pod - 后端 Pod。自建机房没有云 LB一般用 hostNetwork 让 Controller Pod 直接占用节点网络再通过节点 IP 对外提供服务。这时候要注意用 nodeSelector 把 Controller 固定到特定的几个节点避免漂移导致节点 IP 变化影响 DNS 解析。资源规划方面Controller 不要省。一个 Nginx 进程本身开销不大但高并发下 worker 数量等于 CPU 核数CPU 不足会直接影响转发性能。我一般给生产 Controller 至少 2C4G 起步中大型集群 4C8G 比较稳。存储方面几乎无状态不需要卷但日志量大的集群要给日志留足磁盘空间。还有一点Controller Pod 的副本数至少两个两个副本分散在不同节点避免单点故障。3.2 用 Helm 一键部署与参数选型Helm Chart 是最主流的安装方式官方维护的 ingress-nginx 仓库已经很稳定。先准备环境kubectl create namespace ingress-nginx helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update然后我用一份自定义 values 文件安装这是我在生产环境里打磨过的一组参数可以直接参考controller: replicaCount: 2 service: type: LoadBalancer annotations: service.beta.kubernetes.io/aws-load-balancer-type: nlb publishService: enabled: true ingressClass: nginx allowSnippetAnnotations: false config: proxy-body-size: 64m log-format-upstream: $remote_addr - $host [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $upstream_addr $request_time resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 2Gi重点解释几个参数publishService.enabled 的作用是把 Controller 对外访问地址自动同步到 Ingress 资源的 Address 字段。设置了它之后你运行 kubectl get ingress 能看到每一条规则的状态地址非常直观。allowSnippetAnnotations 默认在较新版本中为 false意味着 configuration-snippet 注解默认不生效。如果你之前依赖这个注解做一些灵活配置需要在升级前显式打开否则升级后配置会静默失效。config 里的内容对应全局 ConfigMap这里我把 proxy-body-size 拉到了 64m同时把日志格式改成了带 upstream 地址和耗时的形式。注意 ConfigMap 键值不能写错否则 controller 启动时可能报错所以一般先确认版本里支持的 key 名称。执行安装命令helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx -f values.yaml安装完成后检查 Pod 状态和 Servicekubectl get pods -n ingress-nginx -o wide kubectl get svc -n ingress-nginx kubectl get ingressclass如果 service type 是 LoadBalancer等待 EXTERNAL-IP 字段从 pending 变成实际地址。用 hostNetwork 的话确认 Controller Pod 所在节点 IP 就是你预期的入口 IP。这里要提一个自建集群常见问题用 kubeadm 或其他方式搭建的集群里如果节点之间网络策略比较严格Controller Pod 跨节点访问后端时会受 NetworkPolicy 影响。部署之前建议确认集群里有没有相关的网络策略不然会出现Ingress 规则正常、后端也正常但请求就是不通的诡异问题。3.3 发布后端服务并验证流量转发假设后端服务叫 shop-api运行在 shop 这个命名空间端口 8080。先确认 Service 存在然后创建 IngressapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: shop-api-ingress namespace: shop annotations: nginx.ingress.kubernetes.io/proxy-body-size: 20m nginx.ingress.kubernetes.io/ssl-redirect: true spec: ingressClassName: nginx rules: - host: shop.example.com http: paths: - path: / pathType: Prefix backend: service: name: shop-api port: number: 8080应用这份配置kubectl apply -f shop-ingress.yaml kubectl get ingress -n shop如果 Controller 的 Service 已经有了外部地址可以直接本地验证curl -H Host: shop.example.com http://INGRESS_IP/health如果返回了后端服务的响应说明整条链路已经连通。此时还可以进 Controller Pod 里看一眼生成的 Nginx 配置kubectl exec -it controller-pod -n ingress-nginx -- cat /etc/nginx/conf.d/nginx.conf里面能看到每个 Ingress 规则对应的 server 块和 upstream 地址。当后端 Pod 变化时这里的 upstream 列表也会跟着更新。看完配置再查日志确认实际转发情况kubectl logs -n ingress-nginx -l app.kubernetes.io/nameingress-nginx --tail50日志里能清晰看到请求落到了具体哪个后端 Pod 的 IP 上这是后续所有排查工作的基础能力。很多线上问题光看这条日志就能定位一半。4. 线上疑难杂症排查思路与避坑清单4.1 502/504 网关错误的排查路径502 Bad Gateway 是 Ingress 场景最常见的故障含义是 Controller 和后端之间建连失败。排查路径基本固定按这个顺序来五分钟内能定位大部分问题第一步查 Endpoint。执行 kubectl get endpoints -n 看目标 Service 的 ENDPOINTS 列是不是空的。如果为空一定是后端 Service 的 selector 和 Pod 标签不匹配或者 Pod 没有 Ready。这个原因占比最高。第二步确认 Service 端口映射。kubectl get svc -n 看 targetPort 和 Pod 实际监听端口是否一致。写错了会直接导致连接失败。第三步确认后端进程的监听地址。容器里服务绑定了 127.0.0.1 的话Nginx 从外部访问必然失败所以容器内服务必须监听 0.0.0.0。第四步看后端负载。Pod 存活但是 CPU 打满、连接队列满Nginx 建连超时也会表现为 502。504 则是上游响应超时常见原因是后端处理太慢、数据库超时或者 livenessProbe 挂掉后 Pod 反复重启。可以临时通过 annotation 调大 proxy-read-timeout比如改成 300s如果调大后请求能通说明是后端慢如果依然秒断优先看后端进程日志和资源消耗。还有一个技巧值得分享在 Controller 的日志格式里主动加上 $upstream_status它记录了后端真实返回的状态码。比如 Nginx 返回 502但 $upstream_status 是 200说明请求已经到达后端并成功处理问题出在响应返回链路上多半是 keepalive 或 TLS 握手环节的问题。4.2 证书/TLS 配置的常见问题TLS 配置涉及的 Secret 在 Kubernetes 中必须和 Ingress 同命名空间跨 namespace 引用是不允许的这是初学者最容易踩的坑。创建 Secret 时我用这个命令kubectl create secret tls api-tls-secret --certtls.crt --keytls.key -n namespacetls.crt 必须包含完整的证书链最好把 fullchain.pem 的内容整个放进去。很多团队只放了一张证书 PEM导致高版本浏览器和移动客户端握手失败。判断这种问题的方法是用 openssl 验证证书链openssl s_client -connect shop.example.com:443 -servername shop.example.com如果返回的证书链不完整问题就出在 tls.crt 的内容上。还有一个容易忽略的细节配置 tls 字段后nginx-ingress-controller 默认会为这条规则开启 443 监听。如果你同时开启了 ssl-redirect 注解那 HTTP 请求会自动跳转到 HTTPS。但如果你在 CDN 或者云 LB 那里已经做了 HTTP 到 HTTPS 的跳转这里有可能会多跳一次导致链接多一次往返虽然不致命但对体验敏感的业务会有一定影响。证书轮换也是日常高频操作。Kubernetes 里 Secret 更新后Controller 能感知到并重新加载证书不需要重启 Pod。但要注意如果你的 Secret 是通过 cert-manager 自动管理的它的更新方式和手动 kubectl create 不同要确保 cert-manager 生成的 Secret 名称和 Ingress 里的 secretName 完全一致大小写也要一致。4.3 reload 频率过高与性能调优nginx-ingress-controller 每次感知到 Ingress、Service、ConfigMap 等资源变化都会重新生成配置并执行 reload。在正常频率下没问题但当集群里有大量 Ingress 频繁变更时reload 过于频繁会导致 Nginx worker 不断重建连接闪断QPS 抖动明显。我遇到过一种情况某个业务方在 CI/CD 流程里每次部署都会更新 Ingress 的 annotation比如加一个版本号结果一天几百次变更Controller 跟着 reload 几百次。解决方案是让他们把不参与路由的信息移出 Ingress保持 Ingress 资源的稳定性。如果确实有频繁变化的配置需求优先考虑放到 ConfigMap 或者独立的配置文件里不要动 Ingress 本体。性能方面Controller 默认的 worker 数量等于节点 CPU 核数。对大部分业务来说够用但如果你发现单 worker CPU 满、多 worker 闲置可能是 worker 之间连接分配不均。可以调整 worker-processes 为 auto 或者固定数值同时关注 worker_connections 和 keepalive 设置。还有一点打开 gzip 和开启 HTTP/2 也能显著提升小文件的传输效率在 ConfigMap 里配置 use-http2: true 即可。不过要提醒HTTP/2 连接的多路复用特性在某些老客户端下会有兼容性问题灰度后再全量更稳妥。4.4 云环境 LB 与 Ingress 协同的注意事项公有云上使用 nginx-ingress-controller 时流量链路通常是 客户端 - 云负载均衡 - Controller Pod - 后端 Pod。云 LB 的引入带来两个问题超时和真实 IP。超时问题非常典型。云上四层 LB 默认 idle timeout 大约在 60 秒左右而 Controller 到后端的 proxy-read-timeout 默认也是 60 秒。如果你的业务有超过 60 秒的请求比如大文件下载、WebSocket 长连接、或者视频处理类的同步接口很容易在中间某层被掐断。我的习惯是所有接入云 LB 的集群统一把 proxy-read-timeout、proxy-send-timeout 和 connect-timeout 调大比如 300s 或者按业务实际情况设定同时在云 LB 上把超时时间也保持一致避免有一层是短板。真实 IP 问题则需要额外关注。四层 LB 转发时源 IP 会被替换成 LB 的内网 IP。要保留真实客户端 IP有两个思路一是云 LB 开启 Proxy Protocol同时 Controller Service 上打开 use-proxy-protocol 开关二是把 Service 的 externalTrafficPolicy 设为 Local这样流量只转发到本节点上的 Controller Pod避免二次转发导致源 IP 被替换。第二种方式要注意开启了 Local 之后如果某个节点上没有 Controller Pod这个节点的 LB 健康检查就会失败流量就不会再进到这个节点所以高可用场景下要保证 Controller 的副本数能均匀覆盖到 LB 关联的节点上。还有一个经验是观察云 LB 的健康检查路径。默认健康检查可能是 TCP 端口探测但有些云 LB 会要求 HTTP 健康检查路径。这时候可以给 Controller 配置一个专门返回 200 的健康检查端点或者用 /healthz并确保云 LB 的健康检查路径与之一致。否则会出现一种奇怪现象LB 显示后端不健康但业务实际可以访问排查起来相当费劲。在整个 Ingress 类组件的运维过程中我个人体会最深的一点是不要等问题出来再猜而是从一开始就规范好日志、规范好 IngressClass 的命名、统一好 annotation 的使用范围。日志里带上 upstream 地址和耗时、Ingress 显式声明 className、敏感的自定义 snippet 注解保持关闭这三件事做好了日常排障的时间能缩短一大半。说句实话nginx-ingress-controller 的学习曲线不算陡坑都藏在细节里先把基础链路跑通再逐步加证书和重写规则每加一配置就 curl 验证一次后面会顺手得多。