聊一个被问烂但每次都能踩出新问题的话题:Kubernetes里的Service和Ingress,到底是怎么把流量导进集群、再分发到Pod上的。我见过不少朋友把这两个概念背得滚瓜烂熟,一上生产就翻车——要么Service类型选错导致外部访问不通,要么Ingress Controller装了一堆却不知道流量实际走的是哪条链路。这篇就把Kubernetes流量负载这条线完整拆开,从Service存在的本质、三种工作模式,到Ingress的定位和选型,再到排障思路,全部过一遍。
先说个结论:流量负载这件事,K8s给的不是一套方案,而是分层方案。Service负责把Pod的IP漂移问题解决掉,提供稳定的访问入口和基本的负载均衡能力;Ingress负责把"外部流量进入集群"这件事做标准化,让HTTP、HTTPS路由规则能被统一管理。两者是上下游关系,不是替代关系。
1. 从"Pod会死"说起:Service解决了什么本质问题
1.1 Pod的IP为什么靠不住
K8s里一个Deployment管理的Pod,天生就是"用完即弃"的。滚动更新、节点故障、资源不足触发驱逐、HPA扩容缩容,任何一个动作都可能导致Pod被销毁重建。新Pod的IP和旧Pod完全不一样,而且这个变化没有任何规律可循。
如果业务代码里直接写死某个Pod的IP去调用,那这个服务根本活不过第一次滚动发布。这就需要一个"不动的东西"挡在Pod前面——不管后端Pod IP怎么变,前端只需要记一个固定地址。Service就是那个"不动的东西"。
Service通过标签选择器(selector)动态维护一组Pod的列表,你可以把它理解为"Pod的通讯录"。只要Pod的标签符合selector规则,新创建的Pod会自动被登记进这份通讯录里,删掉的Pod也会自动被移除。
1.2 Service的最小可用模型
先看一个最简单的例子。下面这个Deployment跑着三个副本,每个Pod都带了一个app: nginx标签:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80对应的Service长这样:
apiVersion: v1 kind: Service metadata: name: nginx-svc spec: selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80这里有几个关键点要理解透彻:
port是Service自己暴露的端口,集群内其他组件访问这个端口就相当于访问服务。targetPort是转发到Pod容器里的端口。也就是说,Service收到流量后,会把目标端口改写为targetPort再发给后端Pod。selector决定了流量发给谁,是Service的核心逻辑单元。
创建之后,你执行kubectl get svc会看到一个ClusterIP,比如10.96.10.5。这个IP在集群生命周期内基本稳定,Pod换了多少茬都不影响。
1.3 Endpoints是Service和Pod之间的桥梁
很多人忽略了Endpoints这个对象,但排障时它才是真正要盯的东西。Service并不直接连Pod,它通过Endpoints对象持有Pod的IP列表。
kubectl get endpoints nginx-svc如果输出里后端地址是空的,第一反应不是去看Service配置,而是检查Pod的标签有没有写错。只要selector匹配不到任何Pod,Endpoints就是空的,Service转发自然没有任何目标。这也是我在生产环境里排查"服务突然500/503"时的第一检查项。
从K8s 1.21开始,Endpoints逐渐被EndpointSlice替代,支持更大的后端规模且性能更好,但排查思路完全一致。看kubectl get endpointslices一样能拿到后端IP列表。
1.4 内置的负载均衡:怎么分流量
Service的负载均衡能力来自kube-proxy这个组件,虽然是集群级的"流量泵",但它的均衡逻辑在数据平面。kube-proxy会监听Service和Endpoints的变化,实时维护转发规则。默认情况下流量以近似相等概率转发给每个后端Pod。
这个均衡策略并不是Service本身提供的,而是转发规则的模式决定的,具体在后面的章节展开。这里先记住:Service天然自带基本均衡,不保证会话保持,也不感知后端实际健康状态——它只认Endpoints里有没有这个Pod。
2. 三种Service类型的流量边界:ClusterIP、NodePort、LoadBalancer怎么选
Service的类型是新手最容易搞混的地方。其实只要记住一句话:ClusterIP管集群内部互访,NodePort把端口暴露到宿主机上,LoadBalancer交给云厂商LB收尾。三者面向的流量范围逐层扩大。
2.1 ClusterIP:默认但最常用的类型
不写type字段,默认就是ClusterIP。它分配一个集群内虚拟IP,只有集群内部能访问,Pod、节点、集群内运行的其他服务都能通过这个IP或者通过DNS名称访问。
DNS名称规则是<service-name>.<namespace>.svc.cluster.local,这个由集群内置的CoreDNS解析。比如上面那个Service,在default命名空间里,其他Pod直接访问nginx-svc.default.svc.cluster.local,简写nginx-svc也行。
生产环境里我用ClusterIP的场景最多:微服务之间的RPC调用、内部API、后台任务队列的入口,都应该用ClusterIP。它不暴露给外部,天然安全,而且不会占用节点端口资源。
2.2 NodePort:把服务开到宿主机端口上
NodePort是ClusterIP的超集。它在每个节点上都开一个指定端口(默认范围30000-32767),外部通过任意节点IP:NodePort就能访问集群里的服务。
apiVersion: v1 kind: Service metadata: name: nginx-nodeport spec: type: NodePort selector: app: nginx ports: - port: 80 targetPort: 80 nodePort: 30080注意几个坑:
nodePort不写的话会从30000-32767里随机分配一个,生产环境建议写死,否则防火墙策略和外部依赖根本没法做。- NodePort的端口范围可以改,通过apiserver的
--service-node-port-range参数调整,但改完要重启组件,尽量别折腾。 - 访问
节点IP:30080到达的是任意节点的kube-proxy,它会二次转发到后端Pod。这个转发可能跨节点,链路更长,也多了一跳网络开销。
NodePort适合什么呢?测试环境临时暴露服务、物理机裸部署的场景、云上不想挂LB的临时方案。生产环境大规模使用不是好主意,一来端口资源有限,二来每个服务都要抢占节点端口,管理混乱,三来外部流量入口太多,没有统一的负载均衡层。
2.3 LoadBalancer:云厂商LB的完美衔接
LoadBalancer类型的Service会在ClusterIP和NodePort的基础上,向云厂商申请一个外部负载均衡器。比如AWS的ELB、阿里云的SLB。创建时云平台的cloud-controller-manager会自动创建LB实例,并把公网IP绑定到Service上。
apiVersion: v1 kind: Service metadata: name: nginx-lb spec: type: LoadBalancer selector: app: nginx ports: - port: 80 targetPort: 80创建后,外部流量路径是:公网 -> 云LB -> 任意节点NodePort -> kube-proxy -> Pod。
也就是说,LoadBalancer并没有自己发明新的转发机制,它本质上是"云LB + NodePort"的组合。云LB把流量分发到各节点暴露的端口上,然后继续走NodePort的逻辑。
使用上有两个经验:
- 云LB的健康检查尽量配置成"检查节点端口"而不是检查Pod本身,因为LB到节点之间还有一层转发,只探测Pod会让LB误判。
- 如果追求更高性能,很多云厂商支持直通模式(bypass kube-proxy),比如阿里云的
service.beta.kubernetes.io/backend-loadbalancer-address-type: eth1这种注解,让Pod直接注册到LB后端。这个后面单独说。
三种类型对比如下:
| Service类型 | 访问范围 | 依赖组件 | 典型场景 |
|---|---|---|---|
| ClusterIP | 集群内部 | kube-proxy | 微服务内部调用、数据库访问入口 |
| NodePort | 节点外网可达 | kube-proxy | 测试暴露、裸金属集群、无LB环境 |
| LoadBalancer | 公网/云内网 | cloud-controller-manager | 对外服务入口、生产流量接入 |
3. kube-proxy的转发链路:从iptables到IPVS,流量到底怎么"流"过去的
Service能不能正常工作、性能好不好、要不要开会话保持,这些问题全部指向kube-proxy的转发模式。K8s长期以来有两种主流模式:iptables和IPVS。说实话现在没人再用userspace模式了,但理解它曾经存在的意义有助于你明白为什么后来两个模式一个在性能上更优、一个在兼容性上更稳。
3.1 iptables模式:默认但靠概率
iptables模式是对的默认选择。kube-proxy会为每个Service生成一组iptables规则,包括PREROUTING链、OUTPUT链和POSTROUTING链。
转发逻辑大致是这样:
- 访问ClusterIP的包进入网络协议栈。
- iptables规则命中
-d <ClusterIP>,跳转到对应的自定义链。 - 自定义链里有若干条DNAT规则,把目标地址改写为Endpoints里某个Pod的IP。
- 利用
statistic mode random probability这个match让每条DNAT规则有大致相等的概率被命中。
为什么说概率?因为iptables的负载均衡不是真正的调度算法,它用的就是--probability 0.5这种概率抽签。后端数量少还好,几十个后端时概率分配会有偏差。
还有一个隐藏痛点:长连接场景下,iptables规则只在建立连接时做DNAT,连接后续的包都靠conntrack自动转发。如果后端的Pod因为滚动更新关闭了,但旧连接还在conntrack表里,新发往该连接的包就会被转发到一个已消失的Pod IP上,表现为偶发504或Connection refused。
这种问题典型的触发条件是:长连接 + 频繁滚动更新 + 高并发。排查起来特别头疼,因为不是所有请求都失败,只挂那些存活的旧连接。
3.2 IPVS模式:真正的内核级调度
IPVS模式解决了iptables的痛点。它把负载均衡逻辑移到了内核的netfilter框架中,使用哈希表存储后端地址,通过调度算法实现真正的负载分发。
把kube-proxy切成IPVS模式需要改启动参数:
kube-proxy --proxy-mode=ipvs如果用的是kubeadm,在kube-proxy的ConfigMap里改:
kind: ConfigMap metadata: name: kube-proxy namespace: kube-system data: config.conf: | mode: "ipvs"切换后,IPVS支持多种调度算法,常用的有:
rr:轮询,最常用,后端均匀分发。lc:最少连接,当前连接数最少的后端优先。wrr:加权轮询,按权重分配。sh:源地址哈希,相同来源IP固定到同一后端,天然会话保持。sed、nq等动态调度算法,按需选择即可。
IPVS模式增加了什么能力?十条以内的规则性能差异感知不明显,到了几百上千条Service规则时对比如下:
| 对比维度 | iptables | IPVS |
|---|---|---|
| 规则存储结构 | 线性链表遍历 | 哈希表查找 |
| 大规规模Service性能 | 规则越多查找越慢 | 性能稳定 |
| 负载均衡方式 | 概率随机 | 真正的调度算法 |
| 可观测性 | 困难 | ipvsadm -Ln直观查看 |
| 长连接问题 | 滚动更新时有conntrack残留 | 依赖后端存活检查,同样需谨慎 |
我个人的开发环境已经全面切换到IPVS。默认的rr调度配合ipvsadm -Ln输出,后端分布一目了然,排障体验比iptables好太多。
但注意,IPVS不是银弹,滚动更新时同样可能把流量转发给正在终止的Pod。Kubernetes中Pod的terminating状态不会立即从Endpoints里摘除,尤其当服务有大量长连接时。针对这一点,生产环境建议开启Pod的preStop钩子,延迟几秒退出并调用kubectl等待Endpoints摘除,再关闭进程。这是一种常见但非常重要的优雅退出手段。
3.3 会话保持(Session Affinity)
有的业务需要同一个客户端的请求固定打到同一个Pod,比如WebSocket或者依赖内存会话的应用。Service层次的会话保持用sessionAffinity控制:
spec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800这是Service自带的能力,iptables模式下通过按源IP概率匹配实现,IPVS模式下通过sh调度或类似机制实现。但它的粒度是"源IP",经过多层代理后所有用户可能都来自同一个代理IP,导致会话集中到一个Pod上出现热点。
遇到这种情况,不要在Service层硬扛,把会话路由逻辑交给Ingress层或者应用层处理,cookie、JWT、业务标识都比源IP靠谱。
4. Ingress为什么是"门面"而不是"入口路由器":Controller选型与流量路径拆解
4.1 Ingress对象本身不转发流量
这是最多人误解的一点。很多人以为创建了一个Ingress资源,Kubernetes集群就会自动把流量转发进来。实际上Ingress只是一堆规则声明,真正干活的是Ingress Controller。
Ingress资源长这样:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-ingress spec: rules: - host: app.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-svc port: number: 80意思是:当请求Host为app.example.com且路径以/api开头时,转发到api-svc的80端口。创建之后,如果你没装任何Controller,这条规则永远不会被实施。
Ingress Controller是一个独立部署的组件,通常也是一个Deployment+Service。它读取Ingress对象的规则,在自己的负载均衡层(说穿了就是一个反向代理,最常用的是Nginx)里动态生成配置。外部流量打到Controller,Controller再按照Ingress规则转发给对应的Service,然后继续走Service负载均衡到达Pod。
完整的流量路径是:
客户端 -> DNS解析 -> 云LB/节点端口 -> Ingress Controller Pod -> Service ClusterIP -> Pod4.2 主流Ingress Controller对比
选型之前先理清目前主流的选项:
- ingress-nginx(Kubernetes社区维护):应用最广泛,文档全,配置项丰富,基于Nginx实现。性能好,社区折腾空间大,很多生产环境选这个。
- Traefik:天生拥抱动态配置,配置热加载,支持中控面板。Go写的,部署轻量,适合云原生环境,尤其是服务发现频繁变化的场景。
- HAProxy Ingress:性能非常强,四层和七层统一支持,适合大规模流量,但配置复杂度和文档完整度弱于前两者。
- APISIX Ingress:基于Apache APISIX,插件生态丰富,像限流、WAF、可观测性集成做得比较好,适合需要网关能力和Ingress一体的场景。
- 云厂商托管的ALB Ingress Controller(AWS、阿里云等):流量不再经过集群节点,云LB直接注册Pod作为后端。性能好、免运维,但和具体云平台强绑定,换平台就得换实现。
我的建议是:中小规模、追求生态兼容、团队习惯Nginx的,选ingress-nginx;流量规模很大、要求高吞吐低延迟的,测试一下HAProxy和APISIX;能接受云绑定、希望少操心基础设施的,用云托管的Ingress方案。
4.3 annotation:Ingress的真正玩法
Ingress资源的可用能力很多不靠字段,而是靠Annotation开洞。不同Controller的annotation命名空间不一样,这里以ingress-nginx为例说几个高频且好用的:
metadata: annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: 50m nginx.ingress.kubernetes.io/limit-rps: "10"以上代码demo展示的是原有请求路径重写、HTTPS强制跳转、请求体限制、单IP速率限制。具体应用参照官方文档。
实际处理时,rewrite-target和path的重写关系容易踩坑。用捕获组的前提是在path里使用正则语法,比如:
spec: rules: - host: app.example.com http: paths: - path: /api/(.*) pathType: ImplementationSpecific backend: service: name: api-svc port: number: 80配合nginx.ingress.kubernetes.io/rewrite-target: /$1,外部请求/api/orders就会被重写成/orders再转发给后端。如果不加rewrite-target,后端收到的路径还是/api/orders,一些不期望带前缀的后端服务就会出404。
4.4 TLS终止:证书在哪一层
Ingress最常见的TLS能力是终止(TLS termination)。把证书挂到Ingress资源上,Controller负责解密HTTPS请求,然后再以HTTP转发给后端。证书以Secret方式存储:
apiVersion: v1 kind: Secret metadata: name: app-tls namespace: default type: kubernetes.io/tls data: tls.crt: <base64证书> tls.key: <base64私钥>spec: tls: - hosts: - app.example.com secretName: app-tlsIngress Controller拿到证书后自动在监听443端口时配置该证书。好处是证书统一管理、可以自动续期(比如配合cert-manager),坏处是后端链路从客户端到Ingress之间是加密的,Ingress到Pod之间是明文HTTP,敏感业务如果严格等保要求,还得开启后端也启用TLS,即"从Ingress到Service也走HTTPS"。
5. Service与Ingress协同排障:肉眼可见的坑与排查链路
流量负载出问题,症状大致就那几类:外面访问不上、时好时坏、偶尔超时、404/503。多数情况下问题都能顺着链路逐层定位。
5.1 第一排查链路:从Ingress到Service到Pod
用一条开放的路径做例子,配好Ingress后访问返回503,如何排查?
第一步,看Ingress转发目标是否正常:
kubectl get ingress kubectl describe ingress <name>重点看Rules和Backends两栏。如果Backend里显示的是Service名和端口,说明Ingress规则本身没问题。
第二步,查Service的Endpoints:
kubectl get endpoints <service-name>这里最关键。如果Endpoints为空,说明后端Pod的标签和Service selector对不上,直接去查Pod:
kubectl get pods -l app=nginx kubectl get pods --show-labels对比标签和Service的selector是否一致是排障中最常见的一个坑。比如Deployment的matchLabels写对了,但template里的labels少写了一个,创建出来的Pod不匹配Service selector,Endpoints就是空的。
第三步,Service本身通不通:
kubectl run tmp-pod --image=busybox --rm -it -- sh wget -O- http://<service-cluster-ip>:<port>如果这一步成功,说明集群内网络链路没问题,问题在Ingress Controller那一层。很多人在Service层已经不通后面,还去翻Ingress的配置,方向就错了。
第四步,查Ingress Controller日志和上游配置:
kubectl logs -f -n ingress-nginx <controller-pod名> kubectl exec -it -n ingress-nginx <controller-pod名> -- cat /etc/nginx/nginx.conf重点看upstream里的server地址是否指向Pod IP,如果不是或为空,Controller的动态配置没生效,基本就是权限问题——Controller缺get/list/watch这几个资源的RBAC权限。
5.2 时好时坏:长连接+滚动更新的经典问题
表现是服务使用正常,但每次发版时就会出现一批请求失败,过一会自己恢复。这类问题大多数情况都是旧连接被转发到已经终止的Pod。
排查方法是开启kube-proxy日志和查看conntrack:
conntrack -S | grep insert_failed重点关注这个数值。如果持续增长,说明有大量新连接命中了无效后端。
止血操作是把Pod的terminationGracePeriodSeconds调大,同时给容器加preStop钩子:
spec: containers: - name: app lifecycle: preStop: exec: command: - sh - -c - "sleep 5 && wget -q -O- http://127.0.0.1:10255/quit; sleep 3"这个做法依赖实际后端和平台能力,核心思想是:在进程真正退出前,先让它有5到10秒的时间从Endpoints摘除并处理完存量请求。
不同Controller也有上游摘除时间的设置,比如ingress-nginx的nginx.ingress.kubernetes.io/keep-alive-requests和proxy-next-upstream、proxy-next-upstream-timeout,让请求在upstream连接失败时自动重试下一个后端:
nginx.ingress.kubernetes.io/proxy-next-upstream: "error timeout http_502 http_503 http_504" nginx.ingress.kubernetes.io/proxy-next-upstream-timeout: "5"这个配置加上后,滚动更新期间的异常请求会明显减少,但不能依赖它兜底。
5.3 404与503:别把锅全甩给Ingress
404先确认访问的域名和路径是否匹配Ingress规则。我见过一个案例,页面一直404,最后发现域名带端口访问,Host匹配上了,但path是/之后接了一层层代理,真正后端需要的是/api/v1,而在Ingress层没有rewrite,路径前缀直接透传,导致后端路由不认。
503则优先排查Service的Endpoints是否为空,或后端Pod是否全部CrashLoopBackOff。如果Pod状态正常但Ingress返回503,再查Controller配置里upstream的状态是否正常。
5.4 四层流量也想用Ingress?看需求再决定
Ingress本质是七层HTTP负载,它不擅长做纯TCP/UDP的四层负载。如果业务需要暴露数据库端口、Redis端口、或者自定义协议的RPC端口,nginx-ingress的官方实现支持了TCP/UDP服务暴露,但是把流量从外部导到Ingress Controller的方式又是NodePort或LB,链路更长,而且配置不直观。
这种场景更合适的做法是直接用LoadBalancer Service,或者上一台真正的四层负载均衡组件。如果确实需要统一入口,可以考虑在Ingress之外叠加一套MetalLB(裸金属负载均衡)或者云LB,把不同协议分开处理,不需要硬塞给Ingress。
6. 生产环境的负载收敛经验:什么时候该上服务网格
内容的推进到这里,已经把Service和Ingress都讲完了。但真上生产时还会有个问题浮出来:链路长了之后,流量调皮的现象越来越多,比如某个节点上的Pod损耗高、某个Service的某个后端疯狂超时响应。这时你需要评估是否引入服务网格。
6.1 服务网格到底补充了什么
Service和Ingress的负载均衡是"细粒度到Pod IP"的。Pod IP通了就转发,不通就换下一个,但后端实例的真实健康状态、处理耗时、错误率它通通不知道。当后端逐步出现"半死不活"状态——端口能通TCP但业务逻辑频繁报错——Service和Ingress仍然会往这个半死实例转发请求。
服务网格(比如Istio、Linkerd)带来的核心价值是:
- L7的负载均衡策略,可以精确到权重、熔断、超时、重试。
- 服务间调用的可观测性:每个请求的耗时、状态码、调用链,全部能可视化。
- 流量镜像、金丝雀灰度发布:把一部分流量复制或按比例切到新版本。
这些都是Service和Ingress不直接提供的能力。尤其是"按Header或Cookie引流",Ingress可以通过annotation做一部分,但复杂灰度规则还是得靠服务网格。
6.2 小规模团队别盲目上网格
服务网格不是免费的。Sidecar注入会带来额外的CPU内存开销,跨Pod的调用链路多一层代理,延迟实测会多几毫秒到几十毫秒不等。同时配置复杂度大幅上升,VirtualService、DestinationRule、Sidecar这些概念的学习成本和运维成本都不低。
团队规模在十人以下、服务数量五十个以内的场景,我依然建议先把Service和Ingress的细节摸透、把健康检查和优雅退出做好,就足够支撑业务了。等灰度发布、故障注入、全链路Trace成为刚需,再考虑上网格。一上来就铺服务网格,纯属给基础设施加戏。
6.3 可观测性:流量负载的"温度计"
不管最后用不用服务网格,可观测性都是流量负载这块必须配套的。至少要做到:
- Service和Endpoints的指标:Pod一段时间内的请求量、错误率、延迟。
- NodePort和Ingress Controller自身的指标:连接数、QPS、并发量。
- 应用层的指标:5xx比例、超时比例。
常用的监控改造方案是在Ingress Controller侧接入Prometheus指标,nginx-ingress官方暴露了很多nginx_ingress_controller_requests、nginx_ingress_nginx_connections等指标,配合Grafana面板直接能用。Service层的流量数据主要靠各业务应用自己上报指标,或者用kube-state-metrics拿Service和Endpoints状态。
如果连监控都没有,谈流量调优和负载均衡就是无源之水——流量到底打到哪了,只能靠猜。
6.4 最后实操经验:上线前必须做一遍的流量测试
分享一个我自己每次部署新服务前都会做的流程,不算复杂,但能覆盖大多数流量负载常见坑:
- 部署Deployment后,先确认Pod全部Running,标签正确。
- 创建Service后立刻检查Endpoints是否填充了Pod IP。
- 在集群内起一个临时Pod,通过ClusterIP访问一遍业务,确认Service转发正常。
- 如果开了NodePort,改一下防火墙安全组放行端口,再从节点IP访问。
- 配置Ingress后,先从集群内部访问Controller的Service,再走公网域名访问,逐层排除。
- 做一次滚动更新,重点观察100到200个实时请求的失败率,验证preStop和优雅退出是否生效。
- 启用监控告警,把Ingress 5xx比例、Endpoints空、Pod重启次数这几个指标都配上告警。
这套流程走完,大多数流量负载翻车的场景都能提前暴露出来,而不是等业务方半夜反馈"网站打不开"了才去排查。真正踩过的坑才会记得住,Service和Ingress都是看起来简单、用起来藏东西的组件,希望这篇能帮你少走几步弯路。