上一篇我们把ClusterIP聊透之后,有个朋友跑来问我:我在集群里跑的服务,集群内部访问一切正常,一到集群外面就抓瞎了,总不能每次测试都跳进容器里curl一下吧。这个问题特别典型,Kubernetes里大多数Service默认类型就是ClusterIP,它只能在集群内部网络里用,想暴露出去,就得换类型。而所有暴露方式里最基础、最好理解、也最方便排查的就是NodePort。无论你拼成kubernetes还是kubenetes,手滑输错单词这事我也没少干,但NodePort这个概念,值得你认真搞明白。这篇就把NodePort从原理到实操到踩坑,一次讲透。
1. 先搞明白NodePort到底解决了什么问题
1.1 ClusterIP的边界:只能在集群内部打转
Kubernetes里Service的基础形态是ClusterIP,它在集群内部有一个虚拟IP,Pod通过这个IP访问Service,再由Service把流量负载均衡到后端的Pod上。这套机制在集群内非常好用,所有内部服务都能通过一个稳定IP互相访问,不关心后端Pod漂到了哪个节点,也不关心Pod的IP换没换。
但是ClusterIP有一个天然边界:它的虚拟IP只在集群网络里生效。你在集群外网机器上无论如何都ping不通这个IP,也没法通过它发HTTP请求。原因很简单——ClusterIP是虚拟的,它只存在于kube-proxy生成的规则里,并不真实挂在某块网卡上,集群外根本没有路由能到达它。
这就带来了一个很现实的痛点:开发团队在集群里起了个服务,产品经理想在浏览器里看看效果,测试同学想跑一轮接口用例,你总不能让所有人都去学kubectl port-forward,更不能让他们随便跳进容器里操作。这时候就需要一个让集群外流量能进来的入口,NodePort就是最朴素、最直接的一种。
1.2 NodePort适用场景:测试暴露、临时入口、混合网络
NodePort做的事情一句话就能说清:在集群的每一个节点上,开一个固定的端口,任何外部请求只要打到“任意节点的IP + 这个端口”,就会被转发到对应的Service,再由Service负载均衡到后端Pod。
这个设计思路听着简单,但非常实用。我实际用下来,NodePort最常见的场景大概是这么几类。
第一,测试环境快速暴露服务。开发自测或者QA验证功能时,光有ClusterIP大家都访问不了,用NodePort把服务暴露出来,把“节点IP:NodePort”发给同事,浏览器直接就能打开,整个过程行云流水,比配什么网关、域名都快得多。
第二,临时 debug 入口。线上环境出了疑难杂症,需要临时暴露一个端口抓包或者看监控面板,又不想大动干戈改Ingress规则,临时起一个NodePort Service,验证完再删,干净利落。
第三,混合网络环境下的互通。比如你有一部分服务跑在Kubernetes集群里,另一部分还在传统VM上,两边要联调,通过NodePort让VM那侧直接访问“集群节点IP+端口”,很省事,不需要额外搭专线或者做复杂的网络打通。
当然,NodePort不是生产环境对外开放流量的“终级答案”,这后面我会专门讲它和LoadBalancer、Ingress的取舍。但先把NodePort吃透,后面理解其他类型会轻松很多。
2. 十分钟从零创建一个NodePort Service
2.1 先起一个演示用的Deployment
纸上谈兵没意思,直接动手创建一个。先准备一个最小化的Deployment,用nginx镜像充当业务Pod,方便验证访问是否成功。我在这里用不同节点数量来模拟真实场景,但就算你只有一个节点也能复现整个过程。
apiVersion: apps/v1 kind: Deployment metadata: name: webapp-deploy spec: replicas: 3 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: webapp image: nginx:alpine ports: - containerPort: 80replicas设置成3,是为了后面能清楚看到Service把请求分发到了不同Pod上。ngin x容器默认监听80端口,所以我们后面的targetPort直接用80。
保存为deploy.yaml,执行:
kubectl apply -f deploy.yaml kubectl get pods -o wide正常情况下你会看到3个Pod分布在不同的节点上,各自有独立的Pod IP。记住这些Pod IP,后面讲转发链路时会用上。
2.2 Service的YAML长什么样
Service的YAML才是今天的重头戏。下面这个就是最典型的NodePort类型Service:
apiVersion: v1 kind: Service metadata: name: webapp-svc spec: type: NodePort selector: app: webapp ports: - port: 80 targetPort: 80 nodePort: 30080关键点就在spec里面这几项:type明确写成NodePort;selector负责把带有app=webapp标签的Pod归入后端;ports里面有三个端口概念,很容易搞混,这里先记住nodePort是30080,也就是集群每一个节点上都要开出来的端口。
保存为svc.yaml,执行:
kubectl apply -f svc.yaml kubectl get svc -o wide输出大概长这样:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE webapp-svc NodePort 10.96.123.45 <none> 80:30080/TCP 10s注意PORT(S)这一列,写的是80:30080/TCP。冒号左边是Service的port(ClusterIP上跑的端口),右边是NodePort(节点上暴露的端口)。这说明这个Service同时在两个维度上提供服务:集群内部可以通过ClusterIP:80访问,集群外部可以通过任意节点IP:30080访问。
2.3 用kubectl验证访问链路
创建完成后,验证是必须的。先验证集群内部的ClusterIP访问是否正常:
kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- curl http://10.96.123.45然后验证集群外的NodePort访问。只要你的机器能访问到其中一个节点的地址,直接curl“节点IP:30080”就行:
curl http://192.168.1.11:30080 curl http://192.168.1.12:30080不管打哪个节点的IP,只要该节点能正常和集群通信,都会返回nginx的欢迎页。这里值得多做一步:反复访问十几次,再从kubectl get endpoints观察后端Pod的IP变化,能很直观看到负载均衡效果。
提示:如果集群只有minikube这类单节点环境,NodePort端口有时候默认只暴露在特定网络接口上,可以先用kubectl cluster-info确认控制面地址,再拼上NodePort端口去访问。
3. 底层原理:三个端口与数据包流转
3.1 port、targetPort、nodePort的分工
很多人第一次写Service都会被三个端口绕晕,其实拿日常生活类比一下就完全懂了。port是Service对外亮出的“服务总机号码”,集群内部的人打这个号码就能找到服务;targetPort是后端Pod上真正运行的“分机号码”,总机接通后要把电话转到这个分机;nodePort则是小区大门的“访客通道号”,外部的人先通过通道进小区,再由保安转接到总机。
具体到这个例子里:外部请求走“节点IP:30080”,nodePort=30080是入口;到了集群内部,kube-proxy会把流量转向Service的ClusterIP:80,这里port=80是虚拟IP上挂的端口;最终再由转发链转到某个后端Pod的80端口,targetPort=80是容器里nginx监听的端口。
三个端口的关系可以整理成下面这张表:
| 端口 | 代表含义 | 访问位置 | 默认行为 |
|---|---|---|---|
| port | Service在ClusterIP上监听的端口 | 集群内部访问用“ClusterIP:port” | 必填项 |
| targetPort | 后端Pod容器内实际监听的端口 | 每次转发时的最终目的地 | 默认与port相同 |
| nodePort | 每个节点对外开放的端口 | 集群外部访问用“节点IP:nodePort” | 默认随机(30000-32767) |
在创建NodePort Service时,nodePort其实可以不写,系统会从30000到32767之间自动分配一个端口。但生产环境我一般建议显式指定,因为自动分配的端口每次重建Service都可能变,客户端那边的防火墙规则、安全组策略就可能对不上,显式指定反而省心。
3.2 kube-proxy和iptables是怎么干活的
NodePort能工作的核心,是kube-proxy在集群每个节点上写入了一系列iptables规则。你可以在任意节点上执行iptables-save,找到和webapp-svc相关的链,里面大致会有三类规则。
第一类是NodePort入口规则,匹配目的端口是30080的流量,把它导流到对应的Service链:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/webapp-svc" -m tcp --dport 30080 -j KUBE-SVC-XXXX第二类是ClusterIP入口规则,匹配目的IP是ClusterIP且端口是80的流量,也导到同一个Service链:
-A KUBE-SVC-XXXX -d 10.96.123.45/32 -p tcp -m comment --comment "default/webapp-svc" -m tcp --dport 80 -j KUBE-SEP-XXXX第三类是分发规则,Service链会按权重把流量随机送到不同的后端。每个后端会对应一条KUBE-SEP链:
-A KUBE-SVC-XXXX -m comment --comment "default/webapp-svc" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-AAAA -A KUBE-SVC-XXXX -m comment --comment "default/webapp-svc" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BBBB -A KUBE-SVC-XXXX -m comment --comment "default/webapp-svc" -j KUBE-SEP-CCCC看到没,当请求打到某个节点的30080端口时,这个节点上的iptables规则会先做一轮DNAT,把目标从“节点IP:30080”改写成“Pod的IP:80”,然后再把包送进集群网络里,最终到达某个nginx容器。
这里有个很多人忽略的细节:如果你添加或删除了Service后端Pod,iptables规则会立刻变化。因为kube-proxy通过API Server监听Service和Endpoint的变化,一旦发现变动马上重写规则。这也解释了为什么Pod重启之后IP变了,但Service访问依然正常——规则里的后端IP会跟着更新。
3.3 externalTrafficPolicy:保不保留源IP的分岔路
NodePort部署到一定规模后,你会碰到一个隐藏选项:externalTrafficPolicy。这个字段默认是Cluster,还有一个可选值是Local。别小看这个字段,它决定了流量进来后会不会丢失客户端的真实源IP,也关系到转发效率。
默认Cluster模式下,流量进入某个节点后,如果这个节点上没有对应Pod,kube-proxy会把包再转发给其他节点上的Pod。这个“跨节点转发”的过程会做一次SNAT,把源IP改写成当前节点的IP。好处是负载均衡非常均匀,任何节点上的NodePort都能访问服务;坏处是你后端的Pod只能看到节点IP,看不到真实客户端IP。有些日志分析类应用需要真实源IP,这就有点尴尬了。
换成externalTrafficPolicy: Local后,NodePort流量只在本机流转,节点上如果本来就有后端Pod,就转发给本机Pod;如果本机没有Pod,请求直接拒绝。这样做的好处是保留了源IP,Pod日志里能看到真实的客户端地址,同时少了一层转发,性能略好。坏处也明显:你访问一个没有Pod的节点,服务就不可用了。所以设置为Local时,接入层前面最好再加一层负载均衡,只把流量转发给那些确实运行了Pod的节点。
注意:实际生产里,如果需要真实客户端IP,又不想牺牲高可用,一般不会光靠NodePort加Local,而是选用LoadBalancer配合对应的负载均衡产品,或者在Ingress层处理。NodePort适合快速调试场景,Local模式更适合You明确知道流量只会打到特定节点时再用。
4. 实战踩坑:常用问题与排查实录
4.1 nodePort超出范围或端口冲突
NodePort最常犯的错就是端口号不在允许范围内。默认情况下Kubernetes只允许30000到32767之间的端口作为nodePort,你可以通过API Server的启动参数--service-node-port-range调整,但绝大多数情况下不应该改,这个范围是官方为了避免和宿主机常用端口冲突特意划出来的。
我之前就见过有人在Service里写nodePort: 8080,apply时报错:
The Service "webapp-svc" is invalid: spec.ports[0].nodePort: Invalid value: 8080: provided port is not in the valid range. The range of valid ports is 30000-32767这个报错写得很明白,照着改就行。另一种情况是端口本身在范围内,但和宿主机上已经运行的进程冲突了,kube-proxy在尝试监听这个端口时会失败,Service状态看起来异常,NodePort一直访问不了。排查时可以登录节点执行ss -tlnp | grep 30080,先看看端口有没有被占用。有一点需要特别提醒:哪怕这台节点当前没有后端Pod,nodePort端口也会被kube-proxy监听,所以有些节点看似空闲,端口仍然不能用。
4.2 怎么排查“curl不通”
NodePort建好了,curl请求却一直超时或者connection refused,这种事情太常见了。别慌,按照下面这个思路一层层往后排查,通常几分钟就能定位问题。
先看Service本身有没有后端:
kubectl get endpoints webapp-svc如果endpoints列表是空的,说明selector没匹配到任何Pod。最常见的两种原因:一是Deployment的label写错了,二是Service和Pod不在同一个namespace。前者用kubectl get pods --show-labels对比一下就能看出来端倪。
再看Pod是否Ready。如果Pod一直处于CrashLoopBackOff或者Pending状态,Service即使有endpoints也不可能正常转发,用kubectl get pods看看状态,再kubectl logs看容器日志。
如果Service和Pod都没问题,那就要判断是不是iptables规则没生效。可以登录节点执行:
iptables -t nat -L KUBE-NODEPORTS -n | grep 30080有对应的规则就说明规则层面到位了。如果规则缺失,多半是kube-proxy异常,检查kube-proxy Pod状态,或者直接重启对应节点的kube-proxy组件。
最后还要检查一下防火墙和安全组。很多云平台默认会在安全组里挡住非标准端口,你在集群内部怎么测都通,一换外部机器就不行,十有八九是安全组没放行3万以上的端口。这种问题纯靠折腾Kubernetes是解决不了的。
4.3 NodePort的一些天然短板
用多了NodePort,你会慢慢感受到它的几个先天限制。第一个就是端口数量有限,默认只有30000到32767共2768个端口。如果服务数量多,或者单个Service开了多个端口,端口迟早会不够用。第二个是全节点监听,每个Service的NodePort会占用集群内每一个节点的同端口,节点多了以后端口利用率很低。第三个是外层缺少七层能力,NodePort本质上只能做四层TCP/UDP转发,没法根据域名、路径做路由,如果要在同一个端口上挂多个域名不同服务,NodePort做不了。
还有一点很少被提到:NodePort的访问体验依赖节点本身的网络健康。某个节点宕机或者网络分区了,你拿这个节点IP去访问NodePort自然失败。所以在生产环境如果用NodePort对外提供服务,前面一般都会配一个负载均衡器做节点健康检查,把故障节点自动摘掉。
5. 到了该问的问题:NodePort和LoadBalancer、Ingress怎么选
5.1 一张表看懂三种暴露方式
同一个“让集群外访问”的需求,Kubernetes给了好几套方案,NodePort只是最底层的那一个。与其每次都纠结,不如把它们的定位搞清楚。
| 暴露方式 | 工作层级 | 访问入口 | 典型场景 | 成本 |
|---|---|---|---|---|
| ClusterIP | 四层/虚拟IP | 集群内部IP | 服务间调用 | 无 |
| NodePort | 四层/节点端口 | 每个节点固定端口 | 测试暴露、临时入口 | 无 |
| LoadBalancer | 四层/云负载均衡 | 云厂商LB实例IP | 云上生产服务 | 有一定云资源费用 |
| Ingress | 七层/HTTP路由 | 域名或LB | HTTP/S服务统一路由分发 | 需要额外部署控制器 |
一个容易混淆的点:LoadBalancer类型并不是凭空多出来一套机制,它实际上是在NodePort外面包了一层云厂商的负载均衡器。你创建一个type为LoadBalancer的Service时,kube-proxy照样会在每个节点上开NodePort端口,然后云平台创建一个LB实例,把流量转发到各节点的NodePort上。所以理解了NodePort,LoadBalancer的原理也就能猜个八九不离十了。
Ingress则是更高层的抽象,它不看IP和端口,而是根据域名和URL路径来做转发,把流量路由到不同的Service上。它适合HTTP服务比较多的场景,因为一个入口能通向所有Service,还方便挂HTTPS证书、做限流和灰度发布。
5.2 我的一般性建议
我自己实际选型时基本遵循这样的思路:集群内部服务互调,一律用ClusterIP,别为了图省事全都上NodePort——Internal traffic不需要也不应该暴露到外部。开发测试环境或者临时排查问题,NodePort是最快的选择,三分钟搞定访问链路。云上生产环境对外提供服务,优先考虑LoadBalancer类型的Service,交给云平台处理负载均衡和健康检查。如果服务是HTTP/HTTPS为主,尤其还要做域名区分、证书管理、灰度路由,那就把Ingress控制器优先考虑,让Service保持ClusterIP模式,由Ingress统一入口转发。
这里提醒一点:很多人在生产环境图省事直接创建NodePort然后防火墙只放行某一个节点IP,这个做法也挺危险,容易形成单点,还是要遵循合理方案的思路,让流量先经过一层能感知节点健康状态的负载均衡机制。
最后再分享一个小细节。我平时排查网络问题时,会特意创建一个临时的NodePort Service来快速验证某个Pod端口是否正常。比如想知道某个应用容器的端口是不是真的在监听,就直接起一个Service指向它,然后从外部curl一下节点端口。如果通了,说明容器和网络链路都正常;如果不通,再深入排查容器本身。这个办法省了无数次kubectl exec进去敲命令的麻烦。NodePort这东西看着简单,但在日常运维里,顺手程度远远超出你的预期。