最近帮几个团队排查K8s环境里的访问问题,发现一个特别有意思的现象:明明Docker、Nginx、Kubernetes这些词大家天天挂在嘴边,可真问到“一个请求从外部打进来,到底经过了谁的手,改了哪些包,最后怎么进到应用容器里的”,能一口气说清楚的人不多。更多人只是把YAML配好了,能跑就行,一旦出问题就无从下手。这篇文章就想把这层窗户纸捅破。我会用一套最典型的部署架构——应用跑在Docker容器里,由K8s编排调度,Nginx负责接收外部流量并转发——把三者协作的机制、流量路径、配置要点和常见坑位一次讲透。适合刚接触K8s的开发、运维,也适合那些已经在用但没时间深究原理的同学。1. 从"三张牌"说清职责边界:Docker、K8s、Nginx各管哪一段很多混乱的根源在于把三个工具当成同一层的东西去理解。其实它们的抽象层级完全不同,搞清楚边界,后面的协作逻辑自然就通了。1.1 Docker解决的是"单机上的封装与隔离"Docker最核心的贡献,是让应用连同它的运行环境、依赖、配置文件一起被打进镜像里,然后在宿主机上以容器的形式跑起来。它有两大隔离手段:文件系统隔离,通过UnionFS叠加镜像层;资源隔离,通过Namespace做进程、网络、文件系统的视图隔离,通过Cgroup限制CPU、内存。这里要特别提一下网络。默认桥接模式下,Docker会在宿主机上创建一块docker0网桥,每个容器分到一个虚拟IP,容器之间可以互通,但外部访问必须通过-p参数做端口映射。这个映射本质上是一条DNAT规则:宿主机IP:映射端口 → 容器IP:容器端口。容器IP是动态的,重启就变。这就引出一个关键问题:单机环境下,IP变了还能手动改映射关系,可用性要求不高;但到了集群规模,几十上百个副本、滚动更新、故障重启随时发生,靠人去记容器IP完全不现实。这也是K8s必须出场的原因。1.2 K8s解决的是"集群规模的调度与编排"如果把Docker比作“单台机器上的打包工具”,K8s就是“整个集群的操作系统”。它管理的是成百上千台宿主机组成的资源池,负责决定容器往哪里调度、怎么保证副本数量、如何滚动升级、如何做服务发现和负载均衡。K8s里最小的调度单位是Pod。Pod与Docker容器不是包含关系,但一个Pod通常包含一个或多个容器。关键点在于:同一个Pod里的容器共享同一个网络命名空间和同一个Volume。这意味着什么?它们共享同一个IP、共享localhost,端口不能重复,这就为后面讲“Pod内容器协作”埋下了伏笔。控制平面由API Server、Controller Manager、Scheduler、etcd组成,数据平面由每台节点上的kubelet和kube-proxy组成。kubelet负责拉镜像、启停容器、上报状态;kube-proxy负责维护Service虚拟IP到后端Pod的转发规则。1.3 Nginx的位置:它既可能是流量入口,也可能是应用的一部分Nginx在这套架构里是最“灵活”的角色,因为它可以出现在多个位置。最常见的是集群边缘的Ingress Controller,负责接收所有外部HTTP流量;也可以以业务容器的身份出现在Pod里,比如前端Vue项目用Nginx托管静态资源;还能作为Sidecar容器跟主业务容器挤在同一个Pod里,专门处理日志、缓存、限流等旁路功能。这三种形态对应三种完全不同的协作方式,很多人搞混就是因为没分清自己说的“Nginx”到底是在哪一层。后面我会单独用一整章来讲三种形态的选型和配置差异,这里先记住一句话:Nginx本身是Web服务器和反向代理,它在K8s里的具体职责取决于它被部署在哪个位置,而不是它自身的功能。2. 一次请求的完整旅程:从用户点击到容器内进程的逐跳拆解理解了角色分工,接下来沿着一条真实请求路径,看看三者是怎么一步步协作的。以一个典型前端页面加后端API为例,前端由Nginx托管,后端是Spring Boot应用。2.1 外部流量进入集群:Ingress Controller是怎么半路拦截的用户在浏览器输入域名。DNS解析指向Ingress Controller对外暴露的接入点。这个接入点可能是云厂商的负载均衡器,也可能是节点的NodePort端口。到达接入点后,流量首先进入Ingress Controller(通常是ingress-nginx,本质是一个跑在K8s里的Nginx),它根据域名和路径规则,把请求转发给对应的Service。这一步的关键在于,Ingress Controller并不归用户的应用服务直接管,它是一个独立部署的组件,读取Ingress对象里的规则,动态维护自己的Nginx配置。注意Ingress和Ingress Controller的区别。Ingress只是一个资源对象,你写一个YAML声明“域名api.example.com要转发到某Service的8080端口”,它本身不干活。真正干活的是Ingress Controller,它监听API Server上Ingress资源的变化,把规则实时转成自己的Nginx配置文件,再reload。2.2 到达Service后:ClusterIP与Pod的匹配过程请求到达ingress-nginx之后,Nginx根据配置把流量转发到后端Service的ClusterIP:Port。注意,这个ClusterIP不是某个容器,也不是某台机器的IP,它是一个虚拟IP,只在集群内部可以访问。流量到了ClusterIP,实际上是被节点的iptables或IPVS规则接管的。kube-proxy会维护ClusterIP到Pod Endpoints的映射关系,通过NAT技术把目的地址改写成一个真实Pod的IP。这里有负载均衡、有健康检查端点淘汰机制,但对我们感知来讲,看起来就像访问了一个稳定的内部域名。复杂的地方在于,这里存在两层转发:外部→Ingress Controller→Service→Pod。ingress-nginx转发用的是proxy_pass,指向的是http://某Service名.某命名空间.svc.cluster.local:端口。它先通过集群DNS解析出Service的ClusterIP,再经由iptables规则转到具体Pod。2.3 Pod内部:容器与容器之间的协作细节一个最常见的场景:Pod里跑两个容器,一个是前端Nginx容器,一个是后端API容器,它们共享同一个网络命名空间。Nginx监听80端口,后端服务监听8080端口,Nginx配置里proxy_pass http://localhost:8080。因为是同一个Pod,容器间通信直接走localhost,不走Service,也不经过任何iptables,这是效率最高的通信方式。这带来一个好处:两个容器的生命周期绑定了。Pod死,大家一起死,不存在前端容器还活着、后端容器跑到了别的Pod里的状态错乱。这也是很多人喜欢把强关联的两个服务硬塞进同一个Pod的原因,但我不推荐过度使用,因为Pod的副本扩展是整体扩的,两个容器如果扩容频率不同、资源需求差异大,塞在一起会很别扭。2.4 关键补充:别忽略反向代理后的请求头重写在链路末尾,业务容器拿到的HTTP请求其实已经经过了多次代理。如果不处理,后端的访问日志里看到的客户端IP全是Ingress Controller的Pod IP,HTTPS请求也会被误读为HTTP。所以正确配置必须在每层代理都显式传递原始信息。ingress-nginx默认会设置X-Forwarded-For,但如果你在应用容器里又嵌套了一层Nginx,这层也要配置:server { listen 80; location /api/ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://localhost:8080; } }这个坑非常常见,后面我专门讲Nginx真实IP误判时还会再展开。3. Nginx在K8s中的三种形态:选错位置,配置逻辑完全不同同样是Nginx,部署在集群入口、业务容器内、还是Sidecar上,配置逻辑和运维方式差别非常大。我见过有人把ingress-nginx的配置思路套到业务Nginx上,结果ConfigMap热更新失效,折腾大半天的。3.1 形态一:Ingress Controller,集群流量的统一闸口这是Nginx在K8s里最重的一个角色。它本身以Deployment或DaemonSet方式跑在集群里,对外暴露端口,对内监听Ingress资源。配置主要通过ConfigMap实现,且支持热加载。部署上有个常见坑:很多人图省事,在ingress-nginx的Deployment里直接改环境变量或挂载自定义配置文件,这其实是不推荐的。官方支持的动态配置方式是通过ConfigMap,修改后Controller会自动reload,不用手工操作。如果你硬挂自己的nginx.conf,会绕过它本身的模板渲染逻辑,升级时很容易破裂。这个形态适合的场景:你有多个服务需要共享同一个域名、统一做TLS证书管理、需要按域名或路径做流量分发。它解决的问题是“避免每个服务都自己暴露一组端口映射”,让外部流量从单一入口进入集群,安全策略、认证鉴权也可以在这一层统一控制。Ingress资源示例:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-service port: number: 80803.2 形态二:镜像内置的Nginx,应用自己的前端网关这是最容易被低估的形态。很多应用镜像里自带Nginx,前端静态资源由它托管,API请求由它反向代理到后端。它的特点跟宿主机上直接跑Nginx没有本质区别,但它跑在Pod里,生命周期跟随容器,配置要打进镜像或用ConfigMap挂载。这里有两个必须注意的生命周期问题。第一,Nginx进程必须是容器的PID 1,也就是nginx -g "daemon off;",不能后台运行。否则容器会直接退出,K8s认为它挂了。第二,优雅退出时要能收到SIGTERM信号,Nginx需要调nginx -s quit而不是nginx -s stop,前者会处理完当前请求再退出,后者直接掐断。如果在Dockerfile里用CMD ["nginx"],会走后台模式,这是很多人容器反复重启的元凶。配置管理上,推荐镜像内保留一份兜底配置,业务差异部分用ConfigMap挂载覆盖,比如放在/etc/nginx/conf.d/下。3.3 形态三:Sidecar容器里的Nginx,旁路能力的好帮手Sidecar模式比较进阶。同一个Pod里,主业务容器只负责核心逻辑,旁路Nginx容器专门做日志加工、访问控制、局部缓存甚至请求降级。它和业务容器共享网络命名空间,所以业务容器访问sidecar只需请求localhost。典型的例子:主容器是个不怎么处理HTTP的Worker进程,但对外需要暴露一个健康检查端口。这时可以塞一个轻量Nginx进去,专门响应/healthz,主容器完全不感知。又比如你想给某个旧服务临时加一层限流,但不想改它的代码,Sidecar Nginx改个配置就能顶上。选型时要克制:Sidecar会跟主容器同生共死,Pod每扩容一个副本,Sidecar就跟着多跑一份,资源开销翻倍。能用Ingress层解决的灰度、限流需求,就别急着引入Sidecar。它适合的是“跟实例强绑定的旁路逻辑”,而不是“集群级通用能力”。4. Service与ClusterIP:让Docker容器在动态漂移中始终被找得到前面几章已经多次提到Service,这一章专门把它的协作机制讲透。因为在K8s环境里,容器IP是随时变化的,Nginx的proxy_pass不能写死Pod IP,必须通过Service来解耦。4.1 ClusterIP不是负载均衡器,是稳定入口的虚拟化Service有三种类型:ClusterIP、NodePort、LoadBalancer。最基础的是ClusterIP,它只在集群内部网络可达。你可以把它理解成“K8s给一组动态Pod分配的固定门牌号”。Pod可以死、可以迁移、可以扩容,但Service的IP不变,Nginx或者其他服务只管记这个门牌号就行。kube-proxy负责维护这个虚拟IP和真实Pod之间的转发关系,转发模式有iptables和IPVS两种。iptables模式配置简单,但大规模场景下规则多了性能下降明显;IPVS模式在内核态做负载均衡,更高效,适合几千条以上规则。现在主流K8s发行版都默认或推荐IPVS。4.2 DNS解析链路:从Service名到ClusterIP再到Pod在一个服务访问另一个服务时,比如API服务需要访问Redis,K8s提供了内置的DNS组件,DNS记录的格式是服务名.命名空间.svc.cluster.local。同一个命名空间里,可以简写成redis;跨命名空间,必须带前缀:redis.cache.svc.cluster.local。这个DNS解析过程很有意思:查询到的是Service的ClusterIP,但实际流量最终仍会被kube-proxy规则转发到具体Pod。所以从Nginx的视角看,它只知道自己在访问一个固定IP,实际上每一次请求都可能被转发到不同的Pod上——这就是Service自带负载均衡的本质。4.3 什么时候需要Headless Service默认的Service会分配ClusterIP并做负载均衡。但有些场景不需要负载,比如Pod想直接发现所有后端实例的IP,自己做负载或选主。这时可以用clusterIP: None的Headless Service。它不创建虚拟IP,DNS直接返回所有后端Pod的IP列表,客户端自己从中选。这个机制在StatefulSet场景里用得很多,比如数据库主从、Kafka集群建连。但普通无状态应用我不建议直接上Headless,白白丢失了Service这一层的负载均衡能力,还会让DNS查询压力变大。Service选择后端靠的是Label Selector,也就是Pod上打的标签。YAML示例:apiVersion: v1 kind: Service metadata: name: api-service spec: selector: app: api ports: - protocol: TCP port: 80 targetPort: 8080这里port是Service对外暴露的端口,targetPort是容器里实际监听的端口。很多人配错,要么忘了targetPort要么指向不对,最终表现是Service存在但访问就是个空响应。4.4 Service端口与Pod端口的对齐逻辑上面那个YAML里,Service监听80,转发到容器的8080。如果Pod里有两个容器,或容器里监听了多端口,还可以通过ports.name来对齐,而不必写死端口号。这对后续想调整端口而不用改Service的编排方式非常有用,推荐有一定规模的项目用命名端口。5. 实际运维中"协作"的四个高频坑:崩溃现场还原最后用真实踩坑经历来收尾。这些坑不是配置语法问题,而是协作机制理解偏差导致的,比语法错误隐蔽得多。5.1 改了ConfigMap,Nginx配置却没生效某次我在ingress-nginx里改了一条连接超时参数,等了五分钟,压测发现完全不生效。排查后发现,ingress-nginx的ConfigMap,和它实际运行时的nginx.conf之间,存在模板渲染和reload的环节。虽然Controller理论上监听ConfigMap变化,但需要它内部触发热更新,这个流程如果被日志级别、Pod线程卡住或者它在滚动重启中,会有延迟或丢失。解决方式是:确认Controller日志里出现Reloading相关记录,如果没有,检查ConfigMap是否真的挂载进了Controller Pod。一旦碰上Controller进程自身异常,最直接的兜底是重启该Pod,别在生产上为了省事等它自己恢复。5.2 健康检查路径写错:容器一直重启却流量秒断这个坑太经典了。有一个服务在Pod里既有Nginx又有应用进程,我为了“简单”,把readinessProbe的路径指向了Nginx的默认页面/。结果应用线程池被下游拖垮,Nginx本身还活着,探针一直通过,K8s就不会摘除这个Pod的流量。用户请求转发到底层,全部超时。反过来,如果livenessProbe指向一个依赖外部资源的接口,外部抖动时探针失败,K8s疯狂重启容器,业务直接被误杀。正确做法是:livenessProbe就查进程本身是否活着,比如查一个本地的/tmp/healthy文件;readinessProbe才查业务是否可以接流量,比如查应用的健康接口。5.3 Nginx日志里全是来源IP,却找不到真实客户端这个现象在两级反代架构下非常普遍。客户端→Ingress Controller→业务Pod,业务Pod里的Nginx记录的$remote_addr是Ingress Controller的Pod IP,不是真实客户端IP。如果你需要在业务日志里还原真实IP,必须让Ingress层把客户端IP放到X-Forwarded-For里,业务Nginx用set_real_ip_from声明信任哪一段来源,再用real_ip_header X-Forwarded-For替换$remote_addr。这里千万注意set_real_ip_from要精确匹配Ingress Controller的IP段,不能一股脑全信任,否则伪造IP头的恶意请求会直接穿透你的访问控制逻辑。5.4 超时设置层层叠加:明明后端很快,用户却感觉慢Nginx有代理超时,K8s Service层有连接超时,Ingress Controller默认还有自己的超时限制。如果你不统一梳理,用户的一个请求可能被某一层的超时硬生生拖出几十秒才报错。我的做法是建立一张“超时清单”:Ingress Controller的超时、业务Nginx的proxy_read_timeout、后端应用自己的处理超时,三者要按“外部 ≥ 中间层 ≥ 应用层”的次序配置。之前遇到一个上传接口,前端等45秒超时,Ingress层60秒超时,业务Nginxproxy_read_timeout写的75秒,看着层层都合理,实则用户等45秒就断了,而服务端还在慢慢跑。结尾整套K8s的容器编排与Nginx协作,说白了就是“分层解耦”四个字:Docker管单机封装,K8s管集群调度与网络漂移,Nginx在不同层级充当流量闸门或应用组件。理解每层的职责边界和流量链路,比背一堆YAML参数有用得多。我自己的体会是,排查这类问题最好的工具链很朴素:先kubectl describe看Pod状态,再curl ServiceIP测Service转发,两层都通了才轮到怀疑Nginx配置。按这个顺序来,大部分问题能在十分钟内定位。
最近帮几个团队排查K8s环境里的访问问题,发现一个特别有意思的现象:明明Docker、Nginx、Kubernetes这些词大家天天挂在嘴边,可真问到“一个请求从外部打进来,到底经过了谁的手,改了哪些包,最后怎么进到应用容器里的”,能一口气说清楚的人不多。更多人只是把YAML配好了,能跑就行,一旦出问题就无从下手。这篇文章就想把这层窗户纸捅破。我会用一套最典型的部署架构——应用跑在Docker容器里,由K8s编排调度,Nginx负责接收外部流量并转发——把三者协作的机制、流量路径、配置要点和常见坑位一次讲透。适合刚接触K8s的开发、运维,也适合那些已经在用但没时间深究原理的同学。1. 从"三张牌"说清职责边界:Docker、K8s、Nginx各管哪一段很多混乱的根源在于把三个工具当成同一层的东西去理解。其实它们的抽象层级完全不同,搞清楚边界,后面的协作逻辑自然就通了。1.1 Docker解决的是"单机上的封装与隔离"Docker最核心的贡献,是让应用连同它的运行环境、依赖、配置文件一起被打进镜像里,然后在宿主机上以容器的形式跑起来。它有两大隔离手段:文件系统隔离,通过UnionFS叠加镜像层;资源隔离,通过Namespace做进程、网络、文件系统的视图隔离,通过Cgroup限制CPU、内存。这里要特别提一下网络。默认桥接模式下,Docker会在宿主机上创建一块docker0网桥,每个容器分到一个虚拟IP,容器之间可以互通,但外部访问必须通过-p参数做端口映射。这个映射本质上是一条DNAT规则:宿主机IP:映射端口 → 容器IP:容器端口。容器IP是动态的,重启就变。这就引出一个关键问题:单机环境下,IP变了还能手动改映射关系,可用性要求不高;但到了集群规模,几十上百个副本、滚动更新、故障重启随时发生,靠人去记容器IP完全不现实。这也是K8s必须出场的原因。1.2 K8s解决的是"集群规模的调度与编排"如果把Docker比作“单台机器上的打包工具”,K8s就是“整个集群的操作系统”。它管理的是成百上千台宿主机组成的资源池,负责决定容器往哪里调度、怎么保证副本数量、如何滚动升级、如何做服务发现和负载均衡。K8s里最小的调度单位是Pod。Pod与Docker容器不是包含关系,但一个Pod通常包含一个或多个容器。关键点在于:同一个Pod里的容器共享同一个网络命名空间和同一个Volume。这意味着什么?它们共享同一个IP、共享localhost,端口不能重复,这就为后面讲“Pod内容器协作”埋下了伏笔。控制平面由API Server、Controller Manager、Scheduler、etcd组成,数据平面由每台节点上的kubelet和kube-proxy组成。kubelet负责拉镜像、启停容器、上报状态;kube-proxy负责维护Service虚拟IP到后端Pod的转发规则。1.3 Nginx的位置:它既可能是流量入口,也可能是应用的一部分Nginx在这套架构里是最“灵活”的角色,因为它可以出现在多个位置。最常见的是集群边缘的Ingress Controller,负责接收所有外部HTTP流量;也可以以业务容器的身份出现在Pod里,比如前端Vue项目用Nginx托管静态资源;还能作为Sidecar容器跟主业务容器挤在同一个Pod里,专门处理日志、缓存、限流等旁路功能。这三种形态对应三种完全不同的协作方式,很多人搞混就是因为没分清自己说的“Nginx”到底是在哪一层。后面我会单独用一整章来讲三种形态的选型和配置差异,这里先记住一句话:Nginx本身是Web服务器和反向代理,它在K8s里的具体职责取决于它被部署在哪个位置,而不是它自身的功能。2. 一次请求的完整旅程:从用户点击到容器内进程的逐跳拆解理解了角色分工,接下来沿着一条真实请求路径,看看三者是怎么一步步协作的。以一个典型前端页面加后端API为例,前端由Nginx托管,后端是Spring Boot应用。2.1 外部流量进入集群:Ingress Controller是怎么半路拦截的用户在浏览器输入域名。DNS解析指向Ingress Controller对外暴露的接入点。这个接入点可能是云厂商的负载均衡器,也可能是节点的NodePort端口。到达接入点后,流量首先进入Ingress Controller(通常是ingress-nginx,本质是一个跑在K8s里的Nginx),它根据域名和路径规则,把请求转发给对应的Service。这一步的关键在于,Ingress Controller并不归用户的应用服务直接管,它是一个独立部署的组件,读取Ingress对象里的规则,动态维护自己的Nginx配置。注意Ingress和Ingress Controller的区别。Ingress只是一个资源对象,你写一个YAML声明“域名api.example.com要转发到某Service的8080端口”,它本身不干活。真正干活的是Ingress Controller,它监听API Server上Ingress资源的变化,把规则实时转成自己的Nginx配置文件,再reload。2.2 到达Service后:ClusterIP与Pod的匹配过程请求到达ingress-nginx之后,Nginx根据配置把流量转发到后端Service的ClusterIP:Port。注意,这个ClusterIP不是某个容器,也不是某台机器的IP,它是一个虚拟IP,只在集群内部可以访问。流量到了ClusterIP,实际上是被节点的iptables或IPVS规则接管的。kube-proxy会维护ClusterIP到Pod Endpoints的映射关系,通过NAT技术把目的地址改写成一个真实Pod的IP。这里有负载均衡、有健康检查端点淘汰机制,但对我们感知来讲,看起来就像访问了一个稳定的内部域名。复杂的地方在于,这里存在两层转发:外部→Ingress Controller→Service→Pod。ingress-nginx转发用的是proxy_pass,指向的是http://某Service名.某命名空间.svc.cluster.local:端口。它先通过集群DNS解析出Service的ClusterIP,再经由iptables规则转到具体Pod。2.3 Pod内部:容器与容器之间的协作细节一个最常见的场景:Pod里跑两个容器,一个是前端Nginx容器,一个是后端API容器,它们共享同一个网络命名空间。Nginx监听80端口,后端服务监听8080端口,Nginx配置里proxy_pass http://localhost:8080。因为是同一个Pod,容器间通信直接走localhost,不走Service,也不经过任何iptables,这是效率最高的通信方式。这带来一个好处:两个容器的生命周期绑定了。Pod死,大家一起死,不存在前端容器还活着、后端容器跑到了别的Pod里的状态错乱。这也是很多人喜欢把强关联的两个服务硬塞进同一个Pod的原因,但我不推荐过度使用,因为Pod的副本扩展是整体扩的,两个容器如果扩容频率不同、资源需求差异大,塞在一起会很别扭。2.4 关键补充:别忽略反向代理后的请求头重写在链路末尾,业务容器拿到的HTTP请求其实已经经过了多次代理。如果不处理,后端的访问日志里看到的客户端IP全是Ingress Controller的Pod IP,HTTPS请求也会被误读为HTTP。所以正确配置必须在每层代理都显式传递原始信息。ingress-nginx默认会设置X-Forwarded-For,但如果你在应用容器里又嵌套了一层Nginx,这层也要配置:server { listen 80; location /api/ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://localhost:8080; } }这个坑非常常见,后面我专门讲Nginx真实IP误判时还会再展开。3. Nginx在K8s中的三种形态:选错位置,配置逻辑完全不同同样是Nginx,部署在集群入口、业务容器内、还是Sidecar上,配置逻辑和运维方式差别非常大。我见过有人把ingress-nginx的配置思路套到业务Nginx上,结果ConfigMap热更新失效,折腾大半天的。3.1 形态一:Ingress Controller,集群流量的统一闸口这是Nginx在K8s里最重的一个角色。它本身以Deployment或DaemonSet方式跑在集群里,对外暴露端口,对内监听Ingress资源。配置主要通过ConfigMap实现,且支持热加载。部署上有个常见坑:很多人图省事,在ingress-nginx的Deployment里直接改环境变量或挂载自定义配置文件,这其实是不推荐的。官方支持的动态配置方式是通过ConfigMap,修改后Controller会自动reload,不用手工操作。如果你硬挂自己的nginx.conf,会绕过它本身的模板渲染逻辑,升级时很容易破裂。这个形态适合的场景:你有多个服务需要共享同一个域名、统一做TLS证书管理、需要按域名或路径做流量分发。它解决的问题是“避免每个服务都自己暴露一组端口映射”,让外部流量从单一入口进入集群,安全策略、认证鉴权也可以在这一层统一控制。Ingress资源示例:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-service port: number: 80803.2 形态二:镜像内置的Nginx,应用自己的前端网关这是最容易被低估的形态。很多应用镜像里自带Nginx,前端静态资源由它托管,API请求由它反向代理到后端。它的特点跟宿主机上直接跑Nginx没有本质区别,但它跑在Pod里,生命周期跟随容器,配置要打进镜像或用ConfigMap挂载。这里有两个必须注意的生命周期问题。第一,Nginx进程必须是容器的PID 1,也就是nginx -g "daemon off;",不能后台运行。否则容器会直接退出,K8s认为它挂了。第二,优雅退出时要能收到SIGTERM信号,Nginx需要调nginx -s quit而不是nginx -s stop,前者会处理完当前请求再退出,后者直接掐断。如果在Dockerfile里用CMD ["nginx"],会走后台模式,这是很多人容器反复重启的元凶。配置管理上,推荐镜像内保留一份兜底配置,业务差异部分用ConfigMap挂载覆盖,比如放在/etc/nginx/conf.d/下。3.3 形态三:Sidecar容器里的Nginx,旁路能力的好帮手Sidecar模式比较进阶。同一个Pod里,主业务容器只负责核心逻辑,旁路Nginx容器专门做日志加工、访问控制、局部缓存甚至请求降级。它和业务容器共享网络命名空间,所以业务容器访问sidecar只需请求localhost。典型的例子:主容器是个不怎么处理HTTP的Worker进程,但对外需要暴露一个健康检查端口。这时可以塞一个轻量Nginx进去,专门响应/healthz,主容器完全不感知。又比如你想给某个旧服务临时加一层限流,但不想改它的代码,Sidecar Nginx改个配置就能顶上。选型时要克制:Sidecar会跟主容器同生共死,Pod每扩容一个副本,Sidecar就跟着多跑一份,资源开销翻倍。能用Ingress层解决的灰度、限流需求,就别急着引入Sidecar。它适合的是“跟实例强绑定的旁路逻辑”,而不是“集群级通用能力”。4. Service与ClusterIP:让Docker容器在动态漂移中始终被找得到前面几章已经多次提到Service,这一章专门把它的协作机制讲透。因为在K8s环境里,容器IP是随时变化的,Nginx的proxy_pass不能写死Pod IP,必须通过Service来解耦。4.1 ClusterIP不是负载均衡器,是稳定入口的虚拟化Service有三种类型:ClusterIP、NodePort、LoadBalancer。最基础的是ClusterIP,它只在集群内部网络可达。你可以把它理解成“K8s给一组动态Pod分配的固定门牌号”。Pod可以死、可以迁移、可以扩容,但Service的IP不变,Nginx或者其他服务只管记这个门牌号就行。kube-proxy负责维护这个虚拟IP和真实Pod之间的转发关系,转发模式有iptables和IPVS两种。iptables模式配置简单,但大规模场景下规则多了性能下降明显;IPVS模式在内核态做负载均衡,更高效,适合几千条以上规则。现在主流K8s发行版都默认或推荐IPVS。4.2 DNS解析链路:从Service名到ClusterIP再到Pod在一个服务访问另一个服务时,比如API服务需要访问Redis,K8s提供了内置的DNS组件,DNS记录的格式是服务名.命名空间.svc.cluster.local。同一个命名空间里,可以简写成redis;跨命名空间,必须带前缀:redis.cache.svc.cluster.local。这个DNS解析过程很有意思:查询到的是Service的ClusterIP,但实际流量最终仍会被kube-proxy规则转发到具体Pod。所以从Nginx的视角看,它只知道自己在访问一个固定IP,实际上每一次请求都可能被转发到不同的Pod上——这就是Service自带负载均衡的本质。4.3 什么时候需要Headless Service默认的Service会分配ClusterIP并做负载均衡。但有些场景不需要负载,比如Pod想直接发现所有后端实例的IP,自己做负载或选主。这时可以用clusterIP: None的Headless Service。它不创建虚拟IP,DNS直接返回所有后端Pod的IP列表,客户端自己从中选。这个机制在StatefulSet场景里用得很多,比如数据库主从、Kafka集群建连。但普通无状态应用我不建议直接上Headless,白白丢失了Service这一层的负载均衡能力,还会让DNS查询压力变大。Service选择后端靠的是Label Selector,也就是Pod上打的标签。YAML示例:apiVersion: v1 kind: Service metadata: name: api-service spec: selector: app: api ports: - protocol: TCP port: 80 targetPort: 8080这里port是Service对外暴露的端口,targetPort是容器里实际监听的端口。很多人配错,要么忘了targetPort要么指向不对,最终表现是Service存在但访问就是个空响应。4.4 Service端口与Pod端口的对齐逻辑上面那个YAML里,Service监听80,转发到容器的8080。如果Pod里有两个容器,或容器里监听了多端口,还可以通过ports.name来对齐,而不必写死端口号。这对后续想调整端口而不用改Service的编排方式非常有用,推荐有一定规模的项目用命名端口。5. 实际运维中"协作"的四个高频坑:崩溃现场还原最后用真实踩坑经历来收尾。这些坑不是配置语法问题,而是协作机制理解偏差导致的,比语法错误隐蔽得多。5.1 改了ConfigMap,Nginx配置却没生效某次我在ingress-nginx里改了一条连接超时参数,等了五分钟,压测发现完全不生效。排查后发现,ingress-nginx的ConfigMap,和它实际运行时的nginx.conf之间,存在模板渲染和reload的环节。虽然Controller理论上监听ConfigMap变化,但需要它内部触发热更新,这个流程如果被日志级别、Pod线程卡住或者它在滚动重启中,会有延迟或丢失。解决方式是:确认Controller日志里出现Reloading相关记录,如果没有,检查ConfigMap是否真的挂载进了Controller Pod。一旦碰上Controller进程自身异常,最直接的兜底是重启该Pod,别在生产上为了省事等它自己恢复。5.2 健康检查路径写错:容器一直重启却流量秒断这个坑太经典了。有一个服务在Pod里既有Nginx又有应用进程,我为了“简单”,把readinessProbe的路径指向了Nginx的默认页面/。结果应用线程池被下游拖垮,Nginx本身还活着,探针一直通过,K8s就不会摘除这个Pod的流量。用户请求转发到底层,全部超时。反过来,如果livenessProbe指向一个依赖外部资源的接口,外部抖动时探针失败,K8s疯狂重启容器,业务直接被误杀。正确做法是:livenessProbe就查进程本身是否活着,比如查一个本地的/tmp/healthy文件;readinessProbe才查业务是否可以接流量,比如查应用的健康接口。5.3 Nginx日志里全是来源IP,却找不到真实客户端这个现象在两级反代架构下非常普遍。客户端→Ingress Controller→业务Pod,业务Pod里的Nginx记录的$remote_addr是Ingress Controller的Pod IP,不是真实客户端IP。如果你需要在业务日志里还原真实IP,必须让Ingress层把客户端IP放到X-Forwarded-For里,业务Nginx用set_real_ip_from声明信任哪一段来源,再用real_ip_header X-Forwarded-For替换$remote_addr。这里千万注意set_real_ip_from要精确匹配Ingress Controller的IP段,不能一股脑全信任,否则伪造IP头的恶意请求会直接穿透你的访问控制逻辑。5.4 超时设置层层叠加:明明后端很快,用户却感觉慢Nginx有代理超时,K8s Service层有连接超时,Ingress Controller默认还有自己的超时限制。如果你不统一梳理,用户的一个请求可能被某一层的超时硬生生拖出几十秒才报错。我的做法是建立一张“超时清单”:Ingress Controller的超时、业务Nginx的proxy_read_timeout、后端应用自己的处理超时,三者要按“外部 ≥ 中间层 ≥ 应用层”的次序配置。之前遇到一个上传接口,前端等45秒超时,Ingress层60秒超时,业务Nginxproxy_read_timeout写的75秒,看着层层都合理,实则用户等45秒就断了,而服务端还在慢慢跑。结尾整套K8s的容器编排与Nginx协作,说白了就是“分层解耦”四个字:Docker管单机封装,K8s管集群调度与网络漂移,Nginx在不同层级充当流量闸门或应用组件。理解每层的职责边界和流量链路,比背一堆YAML参数有用得多。我自己的体会是,排查这类问题最好的工具链很朴素:先kubectl describe看Pod状态,再curl ServiceIP测Service转发,两层都通了才轮到怀疑Nginx配置。按这个顺序来,大部分问题能在十分钟内定位。