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 -> Pod,Controller 拿到 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-redirect:ssl-redirect 在 Ingress 配置了 TLS 时,自动把 HTTP 请求 301 到 HTTPS;force-ssl-redirect 则不管有没有 TLS 都会强制跳转。前者更适合温和的灰度策略,后者适合全站强制 HTTPS 的场景。需要注意,如果前面还有 CDN 或者云 LB 已经做过 HTTPS 卸载,这里再跳转一次会多一跳,要仔细检查链路。
- proxy-body-size:Nginx 默认允许的请求体大小是 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 状态和 Service:
kubectl 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 存在,然后创建 Ingress:
apiVersion: 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/name=ingress-nginx --tail=50日志里能清晰看到请求落到了具体哪个后端 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 --cert=tls.crt --key=tls.key -n <namespace>tls.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 验证一次,后面会顺手得多。