做K8s运维到今天,如果让我说哪个组件最容易把概念搞混,我一定选Ingress。Service和Pod那套逻辑不算难懂,但一碰到Ingress,脑子里得同时转三个对象:Ingress资源、Ingress Controller,还有Controller背后真正干活的nginx进程。这篇学习笔记5,就把Ingress-nginx实现K8s七层负载均衡这件事从头到尾讲透,讲清楚它解决什么问题、配置怎么下笔、生产环境怎么用好,以及那些文档里不写的坑。
直接说场景。有个项目要给十几个内部应用统一提供对外访问入口,如果照着Service那套思路,最省事的做法是每个Service开一个NodePort。可端口一多,维护的人就疯了,端口冲突、IP要暴露一堆、没法按域名转发,后来彻底换成Ingress-nginx,一个外部入口IP,一个80/443端口,几十个服务全接进来,按域名、按路径分流,干净利落。
这篇内容适合刚把Service用熟、想知道外部流量怎么按HTTP层规则进集群的人,也适合已经在用Ingress但被各种annotation整得云里雾里的朋友。我尽量把每一步为什么这么做讲清楚,能直接抄作业。
1. 先从四层Service的瓶颈说起:为什么非得上七层
1.1 NodePort和LoadBalancer用起来有多别扭
开始接触K8s的人最先接触到的外部访问方式就是NodePort。它逻辑很简单:把Service的某个端口映射到每个节点的同一端口上,外部请求到达节点端口后,kube-proxy再把流量转发到后端Pod。
听起来没问题,用的次数一多问题全冒出来了。第一是端口范围及其有限,默认只有30000到32767这一段,总共不到三千个端口,看着不少,可企业内部服务一多,几十上百个应用排队申请端口,冲突管理就能让人头大。第二是每个服务开一个端口,外部调用方要记一长串“IP加端口”的映射关系,域名基本没法用。第三是NodePort的转发是四层的,只认IP和端口,不认Host和URL,想按域名做路由分流,门都没有。
LoadBalancer类型稍微好一点,它由云厂商的负载均衡器对接K8s Service,给服务分配一个独立的公网入口IP,但成本跟着上去了。每个Service一个LB实例,费用翻倍不说,配置改动还要等云厂商那边同步。裸机环境或内网环境想搭LoadBalancer,还得额外部署起LoadBalancer的实现组件,比如MetalLB,又是一层维护成本。说到底,四层方案解决的只是“能不能访问”的问题,企业真正需要的是“一个入口、多个服务、按规则分流”,这个需求四层解决不了。
1.2 七层和四层的本质区别
四层负载均衡拿到的是IP和端口,它做决策的依据是“这个包该转发到哪台机器”。七层负载均衡拿到的是完整的HTTP请求,它可以看URL路径、域名Host、请求头Header、Cookie,甚至请求体内容。
用生活里的例子说,四层LBS相当于酒店前台,客人说“我要找302房间”,不管是谁,只要有房号就能带你过去。七层Ingress则是总机转接,你说“找财务部张三”,它会按分机表转给对应的人。前者认编号,后者认规则。
这个概念一旦建立起来,Ingress的价值就清楚了。它把负载均衡从IP端口层面提升到了HTTP路由层面。同样的入口IP和端口,根据Host是app1.example.com还是app2.example.com,或者根据路径是/api还是/web,把请求转发给不同的Service,最终到不同的Pod。这就是七层负载均衡最核心的场景。
1.3 Service并没有被替代,而是分工变了
很多初学者以为有了Ingress就可以不用Service了,这是理解上比较大的误区。Ingress转发规则里写的后端明确指向某个Service,Service做的是把流量继续转发到Pod,同时完成负载均衡。Ingress不直接连Pod。
在七层架构下,Service承担的职责更专注于集群内部的服务发现和负载均衡,外部流量入口交给Ingress,两者的分工是:Ingress负责“入口分流”,Service负责“内部转发”。理解了这个依赖关系,后面看Ingress配置就不会犯“后端地址写Pod IP”这种错误了。
2. Ingress和Ingress-nginx到底怎么配合
2.1 先分清Ingress资源和Ingress Controller
这是学习Ingress时最容易卡住的点。很多教程说“部署Ingress”,其实说的不是同一件事。K8s里有两个东西都叫Ingress相关。
Ingress资源是一个API对象,它是用户写的一份规则清单,YAML文件里声明了“哪个域名的什么路径转到哪个Service”,它本身没有任何执行能力。Ingress Controller是一个常驻运行的Pod,它一直在监听API Server里Ingress资源的变化,拿到这份规则清单之后,把规则翻译成nginx的配置,然后让真正的nginx去干活。
用最直白的话说:Ingress资源是“需求文档”,Ingress Controller是“执行团队”,nginx是“具体干活的工人”。你只写需求文档,没人执行,集群没反应;你只跑Controller,没文档,它也没事可干。两者必须配合。
我们说的Ingress-nginx就是官方维护的Controller实现之一,它本质是一个带着nginx的Pod,里面跑了一个控制循环,监控Ingress、Service、Endpoint三类资源的变化,动态生成nginx.conf,一旦规则有变化就会重载nginx。
2.2 Controller内部到底做了什么
把Controller的启动流程拆开看,能理解很多现象。它启动后会先做一次全量同步,把集群里所有Ingress都拉到本地,生成一份nginx.conf基础配置,然后通过认证信息访问K8s API,建立长连接监听后续变化。
这里有几个细节点值得关注。Controller默认监听宿主机或Service的80和443端口,HTTP和HTTPS请求都由它接收。它生成的nginx配置里,每个Ingress的Host会对应一个server块,每个path会对应一个location块。默认后端default-backend负责响应那些没有匹配到任何规则的请求。
如果你进到Controller的Pod里执行kubectl exec进入容器,能看到nginx.conf文件,静态查看当前生效的规则。这也是排查问题时最高效的手段之一,后面我会专门讲。
2.3 一个请求从外部打到Pod的完整链路
把链路走一遍,印象会非常深刻。假设外部客户端访问http://app.example.com/api/user。
第一步,DNS把域名解析到Ingress Controller所在的节点IP或负载均衡器IP。第二步,HTTP请求到达Controller的80端口,nginx根据请求的Host头找到对应的server块,再根据URL路径/api匹配到location规则。第三步,规则里写着转发到某个Service的某个端口,nginx通过Service的ClusterIP和端口把请求发过去。第四步,Service经过kube-proxy维护的规则,把请求转发给后端Pod。第五步,Pod把响应原路返回。
整条链路里,Controller充当了七层入口,Service做了四层内部转发,各司其职。有一个容易忽略的点:如果Service的负载均衡策略是默认的round-robin,nginx在转发时自己也会做一次负载均衡,实际请求的分发是两层叠加的,别把流量分布看成一个单一环节去分析。
3. 部署Ingress-nginx的实战操作
3.1 版本选择和前置条件
部署前第一件事是确认Ingress-nginx版本与K8s集群版本的兼容性。版本选错,轻则功能异常,重则Controller启动不了。建议直接查官方版本的对应表,简单说,K8s 1.27以前的集群,Controller v1.7或v1.8都能用;1.28以上尽量选v1.9或更新版本。我用的是Controller v1.8.1配K8s 1.26,一路顺畅。
集群方面有几个前置要求。Ingress Controller需要读取集群内资源,所以要保证API Server可访问,RBAC配置正常。Controller默认不指定namespace,可以单独建一个nginx-ingress命名空间来隔离资源,后面管理起来清爽很多。
3.2 部署方式对比
部署Ingress-nginx主流两条路:直接apply官方YAML,或者用Helm装。官方提供的deploy.yaml适合快速上手,里面把Deployment、Service、ConfigMap、RBAC等一次性都建好,一条命令搞定。Helm方式适合需要大规模定制参数的情况,通过values.yaml控制副本数、镜像仓库、资源限制等。
我自己的习惯是,测试环境直接apply官方YAML,生产环境用Helm,因为生产环境往往要改好几个关键参数,比如副本数、Pod调度策略、资源request和limit等,写进values里统一管理更可维护。
部署完成之后,验证手段是kubectl get pods -n ingress-nginx。如果Controller的Pod是Running状态,再看日志,看到类似“successfully loaded configuration”的提示,说明Controller已经正常启动并同步了配置。这里提醒一下,Controller启动后可能要花十几秒到几十秒做首次全量同步,别刚启动就看两眼没动静就慌,稍微等一下。
3.3 部署后立刻要做的三件事
第一件事是确认Controller的Service类型。如果是裸机集群,默认生成的Service可能是LoadBalancer类型,但没有云厂商支持时它的EXTERNAL-IP会一直是pending状态。这时需要改成NodePort或者用hostNetwork模式,让Controller直接复用宿主机网络端口。用hostNetwork时注意端口占用,宿主机80、443如果有其他服务先占住,Controller会起不来。
第二件事是配置IngressClass。IngressClass是K8s用来指定使用哪个Controller的资源。一个集群可以同时装多个Ingress Controller,比如Ingress-nginx和Traefik,Ingress资源里通过ingressClassName字段指定到自己要用的那个。Ingress-nginx部署时会自动创建一个名为nginx的IngressClass,一般不用手动建,但要在Ingress里显式声明。
第三件事是检查RBAC。如果Controller报权限不足的日志,大概率是RBAC没配好。官方YAML已经带了ServiceAccount、ClusterRole和ClusterRoleBinding,但如果你手动裁剪过文件,这部分最容易漏。Controller需要list、watch、get Ingress、Service、Endpoint等资源,权限少了它什么都干不了。
4. 写一个真实的Ingress配置
4.1 YAML逐段拆解
纸上谈兵半天,不如直接上一份能跑起来的配置。下面是一个最常见的Ingress示例,把app.example.com域名的所有请求转发到backend-service的8080端口。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress namespace: production spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: backend-service port: number: 8080拆开看每一段。apiVersion用networking.k8s.io/v1,这是当前正式版本,老教程里写的extensions/v1beta1早废弃了,别抄旧配置。ingressClassName: nginx对应前面说的IngressClass,这是指定Controller的关键字段。rules下面定义一个host,nginx会为这个域名生成server块。path的pathType有两个常用值,Prefix和Exact,Prefix表示路径前缀匹配,Exact要求路径完全相等。这里/用Prefix,所有路径都能命中。
尝试访问app.example.com时,如果域名解析到了Controller所在的入口IP,这个Ingress就能把请求转发给backend-service。首次实验建议后端服务用echo类型镜像,这样看到的响应里带着Pod名称和Host信息,能非常直观地确认请求到了哪个Pod,帮助自己验证转发路径对不对。
4.2 pathType和rewrite-target,这里最容易踩坑
pathType用Prefix看似简单,实际容易出岔子。比如后端Service里的接口路径都是/api开头,而Ingress规则写的path是/api,前端请求的是/api/user,那没问题;可如果后端应用自身不感知前缀,只是把Ingress的路径原样转发过去,就经常出现404。
Ingress-nginx默认把请求路径原封不动转发给后端。比如规则path是/app,请求来了/app/login,nginx转发给后端时还是/app/login。如果后端服务的接口实际只暴露/login,就会打不到接口。这时要用rewrite-target注解改写路径,把Ingress接收到的路径重写之后再转发。
metadata: annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: app.example.com http: paths: - path: /app/(.*) pathType: Prefix backend: service: name: backend-service port: number: 8080注解rewrite-target这里写成/$1,$1对应path正则里第一个括号捕获的内容。请求/app/login时,捕获到的是login,rewrite之后后端收到的是/login,完美匹配。如果多个path共用rewrite,可以用/$1/$2这种形式,把第一段路径也捕获进来,按需拼装。踩过的坑是正则写错或target写错,比如写成rewrite-target: /,结果所有路径都被改成/,后端永远只看到根路径,误伤面很大。
4.3 HTTPS终结与证书配置
企业内部对安全要求高,对外入口往往需要启用HTTPS。Ingress-nginx做HTTPS终结的做法是:TLS握手在Controller这一层完成,Controller把HTTPS解密后,再用HTTP转发给后端Service。这样后端Pod不用处理证书,证书管理集中在入口处。
配置方式分两步。第一步创建TLS Secret,里面包含证书和私钥。第二步在Ingress里声明tls字段,引用这个Secret。
apiVersion: v1 kind: Secret metadata: name: app-tls namespace: production type: kubernetes.io/tls data: tls.crt: base64编码的证书内容 tls.key: base64编码的私钥内容spec: tls: - hosts: - app.example.com secretName: app-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: backend-service port: number: 8080证书管理在稍大规模的环境里一般是接入cert-manager实现自动签发和续期,这个可以单独一个主题说。至少要注意TLS Secret是和Ingress同namespace的,跨namespace引用做不到。证书私钥泄露风险高,尽量别提交到Git仓库里,用外部Secret方案管理更好。
5. 生产环境必须掌握的配置技巧
5.1 annotations速查
Ingress-nginx的底座是nginx,但在K8s里调整行为不是直接去改nginx.conf,而是通过Ingress资源上的annotations。下面这张表是我平时最常用的一组,建议收藏。
| annotation | 作用 | 示例值 | 使用场景 |
|---|---|---|---|
| nginx.ingress.kubernetes.io/rewrite-target | 重写转发后的路径 | /$1 | 路径带前缀时去掉前缀 |
| nginx.ingress.kubernetes.io/ssl-redirect | 是否强制跳转HTTPS | "true" | 全站HTTPS时开启 |
| nginx.ingress.kubernetes.io/proxy-body-size | 允许的请求体大小 | 8m | 后端有上传文件需求 |
| nginx.ingress.kubernetes.io/proxy-connect-timeout | 连接后端超时时间 | 30 | 调整后端响应慢时的等待时间 |
| nginx.ingress.kubernetes.io/limit-rps | 单IP每秒请求数限制 | 10 | 给接口做简单限流 |
| nginx.ingress.kubernetes.io/enable-cors | 是否开启跨域 | "true" | 前后端分离场景 |
用annotation有一个原则:宁可在Ingress级别配置,也别去改Controller的ConfigMap。Ingress级别的配置只影响当前这条规则,粒度更细,排查时不用担心影响到其他服务。
在实际配置时,如果发现某个annotation没生效,先别急着怀疑语法,去检查Controller版本对它的支持情况。有些annotation名字在不同版本里改过,有的老版本只认nginx.ingress.kubernetes.io开头,更新版本会兼容新前缀,但官方文档里只列新的。
5.2 上传大小和超时参数的调整细节
生产环境特别常见的一个需求是允许大文件上传。默认配置里Ingress-nginx对请求体大小限制是1m,接口一旦收到稍大一点的POST请求就直接返回413。处理方法就是在Ingress上加上proxy-body-size注解。
annotations: nginx.ingress.kubernetes.io/proxy-body-size: 50m但要提醒的是,这个50m还受后端Service对应Pod的实际处理能力影响。如果Pod里的服务本身没有提高限制,Nginx放行了也会被后端拒绝。所以调完Ingress之后,后端服务的参数也要一起调。
超时参数的调整思路类似。如果后端服务内部有耗时很长的操作,比如报表导出、批量任务,容易出现网关返回504的情况。这时把proxy-read-timeout调大,比如3600秒,能让长时间请求不被nginx中断。调整之后最好实际压测验证,好几百秒的超时时间如果配多了,等于让所有慢请求都挂在后端,反而拖垮资源。
5.3 灰度发布和会话保持的进阶玩法
Ingress-nginx在灰度发布上提供了一套可用的组合方案。它可以通过nginx.ingress.kubernetes.io/canary注解来标记某个Ingress为金丝雀规则,配合canary-weight按流量比例放量,或者canary-header按请求头定向分发。
举个具体场景,集群里已有v1版本的Service,现在要上线v2。把v2的Service接到一个新的Ingress,
annotations: nginx.ingress.kubernetes.io/canary: "true" nginx.ingress.kubernetes.io/canary-weight: "10"这样v2这条规则只接收10%的流量,验证没问题之后把百分比调到50,最后完全切换。这个能力在功能迭代频繁的团队里非常实用,比直接在Service层面改selector要简单。
会话保持方面,如果后端是有状态应用,登录态存在本地Session里,同一用户后续请求必须打到同一Pod,可以在Ingress上设置nginx.ingress.kubernetes.io/affinity: cookie,并指定会话cookie名称。配置之后Controller会通过Set-Cookie控制后续请求的粘性路由,用户请求始终落在同一个后端Pod上。如果没有这个需求就别开,会话保持会削弱负载均衡的效果。
6. 常见问题与排查实录
6.1 访问返回404,第一反应查default-backend
几乎每个新手第一次配Ingress都遇到过404。404的含义是没有匹配到任何后端。排查路径我先建议自问三个问题:域名是否真的解析到了Controller入口IP?Ingress里的host配置和访问的域名是否完全一致?path是否匹配?这三个里只要有任何一个对不上,大概率就是404。
如果以上都对,再看default-backend。Ingress-nginx默认的default-backend是一个返回404的空服务,Controller启动时会创建它。当请求的Host没有匹配到任何Ingress规则,或者路径全部没命中时,nginx就会把请求转到default-backend并返回404。所以看到404,基本就是“有请求过来,但规则没接住”。
排查时用命令kubectl logs到Controller的Pod里看访问日志,能看到请求接收的记录。如果日志里连请求都没有,说明请求压根没打到Controller,这时候要查入口链路,比如Service类型、hostNetwork、DNS解析,路线完全不同。
6.2 配置改了但感觉没生效
Ingress资源更新之后,Controller需要时间感知变化并重载nginx配置。正常场景下这个时间是秒级的,但如果你操作的Ingress存在多处冲突,更新就可能不会正常生效。
常见的冲突包括同一个域名host在多个Ingress里出现,尤其在不同namespace下,Controller可能随机选一个规则做匹配。Inress-nginx对相同host的规则合并逻辑有一定设定,遇到跨namespace同名host时行为可能不符合预期。这种场景最好收敛成一条Ingress来管理,必要时把rules拆开而不是在多个Ingress之间分散。
也可以直接到Controller的Pod里看nginx.conf,检查对应server块的内容是否符合预期。如果nginx.conf里已经生成了新配置,但访问结果还是旧的样子,那要检查浏览器缓存、客户端DNS缓存或者中间是否存在其他负载均衡器缓存了旧响应。
6.3 Websocket长连接总是断连
Websocket这类长连接应用通过Ingress-nginx接入时,最常见的症状就是连接建立后很快断开。原因是nginx默认的read timeout是60秒,Websocket长期不发消息时连接被回收了。
处理方法是在Ingress上调整超时时间:
annotations: nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"把读写超时都拉长到一小时,大部分业务场景就够用了。如果后端还依赖的是HTTP/2,那还要检查Controller是否启用了HTTP/2支持,不过一般Websocket场景走HTTP/1.1就行。
6.4 排查命令,这几条救过命
最后把排查命令整理成一套固定动作。先看Controller状态,kubectl get pods -n ingress-nginx;再看Controller日志尾部,kubectl logs -f deployment/ingress-nginx-controller -n ingress-nginx;接下来进Pod查Nginx实时配置,kubectl exec -it -n ingress-nginx -- cat /etc/nginx/nginx.conf;再核对Ingress、Service、Endpoint三个对象,kubectl get ingress -A、kubectl get svc -A、kubectl get endpoints -A。最后从集群外发起请求,curl -v加上-H参数指定Host做验证。
这套流程走完,大部分问题都能定位到具体环节。我自己在实际排查中还有一个小习惯:把curl的响应头和时间一起输出,比如curl -v --max-time 5,能看到连的是哪个IP、哪个端口,响应有没有重定向,耗时多少,信息量比单纯看状态码大得多。
说实话,第一次用Ingress-nginx时我也被Controller和Ingress资源的区别搞懵过,后来完全是因为动手配了一个真实域名,把请求从浏览器一路追到后端Pod日志里,才彻底想通这个链路。K8s里很多组件光看文档越看越绕,上手跑一遍问题就清楚了。这份笔记如果能让刚开始接触七层负载均衡的人少走一点弯路,我就没白记录了。