资讯详情

资讯详情

从ClusterIP到NodePort:解开Kubernetes外部访问的第一道门槛

1. 从ClusterIP到NodePort我们为什么需要这个Service类型写这篇的时候我翻了翻这个系列前面的内容从Pod、Deployment一路走过来大部分人其实已经能用Deployment把应用跑起来了。但接下来会遇到一个特别尴尬的问题我启动的服务我自己都访问不到。为什么因为Kubernetes里Pod的IP是内网地址而且Pod天生就是“短命鬼”——它可能被重建、被调度到别的节点IP随之变化。你不可能把IP写死到配置文件里也不该自己维护一个服务发现机制。这就是Service存在的意义它给一组Pod提供一个稳定的访问入口。Service有多种类型ClusterIP、NodePort、LoadBalancer、ExternalName。ClusterIP是默认的它只在集群内部网络有效你在集群外面根本没辙。那问题来了集群外面的人怎么访问比如说你在浏览器里访问一个Web应用或者让外部系统调用你的API。NodePort这个类型就是从“集群内部能访问”跨越到“集群外部也能访问”的第一道门槛。它的做法很直白在每个节点上开一个端口然后把发到这个端口的流量转发给后端的Pod。我把三类Service类型常被误解的地方先放到这儿ClusterIP不是不能配而是默认只能在集群内部用NodePort不是替代ClusterIP而是在ClusterIP基础上加了“节点端口”这个入口LoadBalancer是云厂商的事NodePort的转发逻辑它底层也在用。这个系列本来就是一个K8s入门到实战的连续教程这一篇的核心就是把NodePort这个Service类型讲透。你不需要有任何云厂商背景也不需要理解太深的网络原理但读完这篇你应该能自己写一个NodePort的YAML、部署上去、从外部访问成功并且知道出问题时从哪儿排查。2. NodePort的工作原理一次流量的完整旅程2.1 从用户浏览器到Pod中间发生了什么假设我已经在集群里部署了一个Web服务给这个服务配置了NodePort类型的Service指定的nodePort是30080。现在有一个用户在浏览器里输入http://192.168.1.10:30080来访问我的应用其中192.168.1.10是集群中某个节点的IP。这一整个请求链路从外到内大概是这样的浏览器发起请求目标地址是节点IP:30080。请求先到达节点上的网络接口也就是那个节点的物理网卡。然后进入了节点内核的iptables规则或者IPVS规则这些规则是kube-proxy组件提前设置好的。流量在这一步被拦截匹配到Service对应的规则目标端口会被改写比如改写成ClusterIP的80端口。接着流量走向ClusterIP实际上被转发到某一个后端Pod的IP和端口上。这一步会经过负载均衡策略常见的是随机或者轮询。最后到达Pod里运行的容器容器里的进程处理请求并返回响应。响应沿着同样的路径返回到用户浏览器。整个链路里用户感知的只是一个IP:端口他根本不知道集群里有哪个Pod在处理。这就是Service做了一层抽象的价值。2.2 NodePort与ClusterIP的关系很多人以为NodePort是一个单独的Service类型跟ClusterIP是“二选一”的关系。这是个误区。在Kubernetes里当你创建一个ServiceType指定为NodePort时系统会自动分配一个ClusterIP。也就是说NodePort类型的Service内部访问用的ClusterIP同样存在外部访问用的是节点IP加nodePort端口。换个说法NodePort是ClusterIP的超集它在ClusterIP的基础上额外在每个节点上开了一个端口做流量接入。所以ClusterIP的DNS解析、集群内部Pod之间的服务发现功能在NodePort类型的Service上一样都好用。你完全可以在集群内部用service-name.namespace.svc.cluster.local去访问这个Service同时外部又能通过节点IP访问。2.3 kube-proxy在其中的角色iptables与IPVS这一步是Kubernetes网络里的核心机制。每个节点上都运行着一个叫kube-proxy的组件它的职责是监听API Server上Service和Endpoint的变化然后把这些变化同步到节点本地的转发规则里。kube-proxy有几种工作模式老版本的默认是iptables模式新版本很多环境配置的是IPVS模式。iptables模式用的是Linux内核的iptables规则链它会把Service的相关信息翻译成一条条链规则比如KUBE-SVC-XXXX、KUBE-SEP-XXXX这些链。IPVS模式则把负载均衡工作交给内核的IPVS模块它本质上是一个内核态的四层负载均衡器通过调度算法把流量分发到后端的Pod。在性能上IPVS模式在Service数量多的时候比iptables模式表现更好因为它用的是hash表规则查找速度更快。但不管用哪种模式从用户角度来看效果是一样的发往节点IP:NodePort的流量最终都会被转发到某一个后端Pod上。提示iptables模式在Service数量达到几千上万个时可能会出现明显的性能问题。如果你在生产环境里Service数量很多建议确认一下kube-proxy用的模式。可以用kubectl get cm kube-proxy -n kube-system -o yaml看一下配置或者检查节点上的kube-proxy启动参数。3. 动手实践创建第一个NodePort类型的Service3.1 准备一个后端应用在创建Service之前至少得先有一个能跑的应用。我这里用一个最简化的Deployment来做示例后端镜像选择轻量的Web服务器比如Nginx。你可以先新建一个YAML文件apiVersion: apps/v1 kind: Deployment metadata: name: webapp labels: app: webapp spec: replicas: 2 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80部署它kubectl apply -f deployment.yaml再确认两个Pod都处于Running状态kubectl get pods -l appwebapp3.2 编写NodePort类型的Service YAML接下来是重头戏写一个Service类型指定为NodePort。apiVersion: v1 kind: Service metadata: name: webapp-service spec: type: NodePort selector: app: webapp ports: - port: 80 targetPort: 80 nodePort: 30080三个关键字段得拆开讲portService本身的端口也就是ClusterIP上监听的端口。其他Pod通过ClusterIP访问这个Service时用的就是这个端口。targetPort流量最终要转发到的容器端口。在我们的例子里Nginx监听在80端口所以targetPort也是80。nodePort每个节点上暴露给外部流量的端口。这个字段是可选的如果不写Kubernetes会自动从配置的范围内分配一个。另外注意selector它决定了这个Service会关联哪些Pod。我们给Deployment里的Pod打了app: webapp这个标签Service的selector必须跟它匹配才能把Pod挂到Service后面。应用这个Servicekubectl apply -f service.yaml查看Servicekubectl get svc webapp-service你会看到类似这样的一行输出NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE webapp-service NodePort 10.110.80.1 none 80:30080/TCP 10s注意看PORT(S)这一列格式是80:30080/TCP前面的80是Service端口后面的30080就是NodePort。集群里每个节点的30080端口现在都被监听起来等待转发了。3.3 从外部访问服务现在从集群外部访问。先拿到节点的IPkubectl get nodes -o wide每个节点都会显示一个INTERNAL-IP。选其中一个用浏览器或者curl访问curl http://节点IP:30080正常情况下你会看到Nginx的默认欢迎页内容。如果一切顺利这一步就算走通了。如果本地环境用的是kind、minikube这类工具访问方式会略有不同。比如kind创建的集群节点实际上是容器需要先把节点端口映射到宿主机minikube则可以用minikube service webapp-service直接帮你打开访问入口。网上这类工具的坑很多后面我会专门讲。3.4 从集群内部访问NodePort类型的Service别以为NodePort只给外部用。集群内部的Pod一样可以绕过ClusterIP直接通过节点IP:nodePort访问Service它同样能正常转发。不过这里有个逻辑要搞清楚如果集群内部访问建议优先用ClusterIP因为少一跳转发链路性能更好。NodePort的存在是为了满足集群外部的访问诉求它不是内部访问的最佳路径。4. 深入解析端口范围为什么你的nodePort会分配失败4.1 默认端口范围与配置位置Kubernetes默认的NodePort端口范围是30000到32767。这个范围是写在API Server的启动参数里的对应的参数是--service-node-port-range。如果你手动指定nodePort那么它必须在这个范围内。比如你写29999创建的时候就会直接报错。如果你想改这个范围需要在API Server的启动参数中调整。这牵扯到控制和平面组件的配置每个发行版的改法不一样生产环境改它需要谨慎。通用的建议是不要改用默认范围就好。30000到32767这个端口段在常规业务里很少被用到冲突的可能性小安全上也相对独特。4.2 跨命名空间的端口冲突一个很容易踩的坑同一个集群里不同命名空间下两个Service不能使用相同的nodePort。我举个实际例子。你在namespace-a里创建了一个ServicenodePort指定为30080一切正常。然后在namespace-b里再创建一个Service也想用30080Kubernetes会直接拒绝报错类似The Service xxx is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated原因不难理解nodePort是节点级别的端口而节点是所有命名空间共享的同一个端口不可能同时被两个Service占用。所以设计Service时想清楚端口规划的问题避免冲突。如果你不手动指定nodePortKubernetes自动分配时也会自动避开已经使用的端口这块不用太担心。4.3 NodePort与宿主机的端口冲突还有一个隐蔽的问题nodePort会绑定到节点的所有网络接口上。如果节点上某个进程已经占用了30080这个端口那么kube-proxy在设置转发时会跟现有进程冲突。不过通常来说Kubernetes自动分配的nodePort会尽量避免常见端口手动指定的时候你才需要额外小心。指定之前先登录节点确认一下端口没被占用ss -lntu | grep 30080没有输出说明端口是空的可以放心使用。4.4 小范围集群的端口枯竭问题如果你管理的集群节点很多比如上百台理论上每个Service的nodePort在所有节点上都会占用一遍。好在端口范围有2768个可用值32767 - 30000 1一般的业务规模很难用完。只有当你的Service数量极其庞大或者有人恶意地把nodePort范围改得很小才可能遇到端口分配枯竭的问题。如果真的遇到这种极端情况解决方案有两个方向。一是规划好端口分配避免不必要的Service都开NodePort二是考虑把nodePort范围扩大注意这需要重启API Server而且必须确保新范围不跟节点上其他服务冲突。5. 三个端口的爱恨纠缠port、targetPort与nodePort的边界5.1 它们分别是谁的端口这三个端口是Service配置里最容易搞混的东西。每次有新手问我我都用一个快递的比喻来回答。port是快递站的门牌号。别人要找这个服务站先按这个门牌号来。targetPort是最终收货人的房间号。快递到了得找到具体哪间房。nodePort是快递站在每个小区外面设的自提柜。外面的人不用进小区在自提柜就能取件。展开到技术细节上port是Service暴露在ClusterIP上的端口是集群内部访问Service时用的端口targetPort是Pod里容器实际监听流量的端口转发规则最终会把流量送到这里nodePort是Service暴露在每个Kubernetes节点上的端口是集群外部进入流量的入口。5.2 端口不一致的场景有一种情况很多人会写错容器里监听的端口和应用默认端口不一致。比如你的容器里有这样一个进程nginx -g daemon off; listen 8080;它监听的是8080端口但你没有改YAMLtargetPort写成了80。那么流量到达容器后根本不会被这个进程接收到服务自然不通。正确的做法是让targetPort跟容器里实际监听的端口保持一致。如果你用的是自制镜像端口是代码逻辑决定的先确认清楚了再写。5.3 服务端口不一致的另一个例子还有一种场景集群内部有多个应用互相调用每个应用的Service约定的端口可以跟Pod的端口不一样。比如你有一个后端服务它在容器里监听的是5000但Service的port你可以设置成80。集群内部其他应用调它用的是service-name:80转发到Pod时才会改成5000。从调用方的视角来看它只关心Service端口根本不关心Pod内部的端口细节。这就是Service抽象的一层好处。6. 多节点场景下的NodePort访问方式6.1 任意节点IP加节点端口都能访问NodePort特性里有一个常被忽略的点只要Service创建成功集群里所有节点的指定端口都会开放流量都可以被转发。假设你的集群有三个节点IP分别是192.168.1.10、192.168.1.11、192.168.1.12。那么192.168.1.10:30080、192.168.1.11:30080、192.168.1.12:30080这三个地址全部都能访问这个Service。这一点有实际意义你不必关心用户当前访问的是哪个节点任意入口都会转发到同一个Service后端。即使某个节点挂了用其他节点的IP加端口访问依然可以通。但这同时带来一个安全和管理上的问题每个节点都暴露了端口相当于攻击面增大了。管控网络策略时需要把这一整段端口都考虑进去。6.2 为什么同一Service在不同节点上表现会有差异在某些特殊情况下你会发现访问节点A不通但访问节点B是通的。常见的原因有几种节点A上的kube-proxy崩溃了规则没有同步上节点A的网络插件比如Calico、Flannel有问题导致转发链路断裂节点A的安全组或防火墙拦截了这个端口。排查这类问题第一反应是去看节点A上的kube-proxy日志和iptables规则是否完整而不是怀疑Service本身有问题。6.3 用externalTrafficPolicy控制访问来源多节点场景下有一个字段非常关键externalTrafficPolicy。它有两个值Cluster和Local。默认值是Cluster。意思是流量进入任意节点后可能被转发到其他节点上的Pod这个转发过程会引入一次额外的网络跳数同时会隐藏客户端的真实IP。当它设置为Local时流量进入某个节点后只会被转发到“本节点上”的Pod不会跨节点转发。好处是访问来源IP可以保留下来而且转发路径更短性能稍微好一些。代价是如果某个节点上没有对应的Pod流量到达这个节点后会被丢弃。所以Local策略通常需要跟Pod的调度规则配合使用确保每个节点上都有Pod副本。什么场景下适合用Local比如你的应用需要记录用户的真实IP做日志分析或风控或者你对转发性能比较敏感。反之如果你的Pod副本数很少调度得比较集中用Local反而可能造成大量节点上访问不通。注意externalTrafficPolicy这个字段只在NodePort和LoadBalancer类型的Service上生效。它不是一个日常随手改的字段但排查问题时一定要想到它。7. 会话保持把用户请求固定到同一个Pod7.1 为什么需要会话保持对于无状态应用来说请求被转发到哪个Pod都无所谓。但对于有状态应用比如Session登录、WebSocket长连接、临时文件上传这类场景用户的前后多次请求最好落在同一个Pod上否则会出问题。举个例子用户在登录接口写入了一些状态到Pod A的内存里下一步操作被转发到了Pod BPod B没这个状态直接返回未登录。这是典型的会话失败。NodePort类型的Service可以用一个字段解决这个问题sessionAffinity。7.2 配置sessionAffinity把Service的YAML加一行spec: sessionAffinity: ClientIP含义很直接来自同一个客户端IP的请求会由负载均衡器固定在同一个后端Pod上。展开看一下完整示例apiVersion: v1 kind: Service metadata: name: webapp-service spec: type: NodePort selector: app: webapp sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800 ports: - port: 80 targetPort: 80 nodePort: 30080timeoutSeconds字段控制的是会话保持的有效期默认是10800秒也就是3小时。超过这个时间后来自这个IP的新请求可能会被转发到其他Pod。这个配置本身很简单但追求高可用的时候要权衡sessionAffinity是Service层面的如果Pod重启了IP还在但Pod内部的内存状态可能已经没了会话还是会断。所以它只能作为辅助手段真正的有状态服务还是得靠持久化和独立的状态存储。7.3 会话保持与负载均衡的矛盾开启sessionAffinity之后负载均衡的效果会打折扣。因为同一个IP的流量始终到同一个Pod相当于这个IP被“粘住”了。在流量大的场景下某些Pod可能很忙而其他Pod相对空闲。没有任何方案是万能的。如果你的业务真的有会话保持需求优先从应用层面解决比如把Session放到Redis里这样Pod层面的故障不会导致会话丢失如果不是特别需要建议不要开启这个功能保持默认的负载均衡能力更省心。8. NodePort的安全议题暴露在外的端口如何管控8.1 每个节点都是入口每个入口都要管NodePort直接暴露的是节点IP和端口。在云环境里这就意味着你的安全组规则需要为每个节点放行NodePort端口段。很多人在配置安全组时习惯只放行常用的22、80、443然后猛然发现自己NodePort的30080端口在外面访问不到。第一反应是Service配置错了其实大概率是安全组没放行。排查这种问题时建议先用另一个节点试试如果所有节点都不通优先怀疑安全组如果只有一个节点不通优先怀疑那个节点本身的问题。8.2 NodePort端口段本身的安全建议30000到32767这个范围在默认配置下对公网完全开放。如果你的业务只是内部测试建议安全组里只允许特定来源IP访问这个端口段不要对全互联网开放。如果企业有严格的安全规范不要直接用NodePort上生产环境而是改用LoadBalancer类型配合云平台的安全策略。LoadBalancer会创建一个云负载均衡器由它来过滤流量安全边界更清晰。8.3 通过Ingress收敛NodePort的暴露面有一种常见的架构在NodePort前面再套一层Ingress或者用一个统一的入口网关。具体来说你可以创建一个NodePort只代理Ingress Controller其他业务统统用Ingress规则来路由。这样集群对外暴露的NodePort端口数量就大大减少只需要一个入口端口然后由Ingress根据域名或者路径转发到不同的Service上。这是我在实际项目中比较推荐的做法。NodePort是基础能力但直接把几十个Service都开成NodePort暴露出去既不安全也不优雅。9. 遇到问题怎么办NodePort常见故障排查实录9.1 端口范围校验失败创建Service时报错提示端口不在范围内信息类似The Service webapp-service is invalid: spec.ports[0].nodePort: Invalid value: 25000: must be between 30000 and 32767这个没什么好说的把nodePort改成30000以上的值就行。如果要自定义范围得去改API Server的启动参数。9.2 从外部访问不通但集群内访问正常这是最典型的故障之一。出现这个现象时请按这个顺序排查确认Service类型是不是NodePort不是的话当然不通确认nodePort在安全组里是否放行确认节点防火墙比如firewalld或者ufw有没有拦截端口确认kube-proxy状态正常Pod有没有Running。如果上面都没问题试着从另外的节点访问同一个端口看看是不是只有特定节点有问题。9.3 所有节点都无法访问但Service状态正常这种情况我遇到过不少次。Service的STATUS是正常的CLUSTER-IP也有了端口也对了但就是通不了。当时排查下来发现是kube-proxy的Pod挂了。在kube-system命名空间下kube-proxy是以DaemonSet形式运行的理论上每个节点都有一个Pod。如果某个节点上的kube-proxy异常退出那个节点上的转发规则就可能是错的或者缺失的。查看kube-proxy状态kubectl get pods -n kube-system | grep kube-proxy再排查对应的日志kubectl logs kube-proxy-pod -n kube-system9.4 访问到了但响应特别慢NodePort的转发链路本身不会造成明显的性能瓶颈如果出现响应慢的情况优先排查后端Pod的负载能力其次看节点之间的网络延迟。另外可以看看Kubernetes节点上的Pod分布情况。如果大部分Pod都集中在某一个节点上其他节点的流量都发到本地后还要跨节点转发到那个节点网络开销自然大得多。此时可以用externalTrafficPolicy: Local配合节点亲和性让每个节点本地的Pod处理本节点的流量降低跨节点请求。9.5 NodePort端口被自动重新分配有人不小心没有指定nodePortService创建后拿到一个随机端口比如31876。后来把Service删了重建端口变了。这不算故障但如果你对接了外部的DNS、负载均衡器或者防火墙规则端口一变这些配置就可能全都失效。解决方案就是在YAML里显式指定nodePort。如果首次创建时没有指定你可能会遇到“字段不可修改”的提示这时候需要先删除Service再用包含nodePort字段的YAML重新创建。删除再创建的瞬间会有短暂的服务中断要规划好时间窗口。经验在生产环境我建议所有需要外部访问的Service都把nodePort固定下来并形成一份端口登记表避免重建之后找不到原来的端口。10. 与Ingress配合NodePort的高级姿势10.1 为什么需要IngressNodePort解决了外部访问的问题但随之而来一个新的麻烦每一个Service都需要一个端口。假设有10个微服务你就要维护10个端口每个端口还得配置安全组规则每个端口可能还得绑定域名这很快就会变得难以管理。Ingress就是解决这个问题的组件。它本质上是一个七层路由器根据域名和URL路径把外部请求转发到集群内部不同的Service上。在Ingress的方案里你只需要把Ingress Controller服务暴露为一个NodePort其他业务都通过Ingress规则转发。这个架构在中小规模集群中非常实用。10.2 实际部署Ingress的方案示例以常用Ingress Controller为例创建一个NodePort类型的Service来暴露它apiVersion: v1 kind: Service metadata: name: ingress-nginx-controller namespace: ingress-nginx spec: type: NodePort ports: - name: http port: 80 targetPort: 80 nodePort: 30080 - name: https port: 443 targetPort: 443 nodePort: 30443 selector: app.kubernetes.io/name: ingress-nginx这样外部入口就收敛到两个端口30080给HTTP30443给HTTPS。其他Service不再每个人都暴露NodePort而是通过Ingress规则进行路由。10.3 什么情况下可以只用NodePort不用Ingress如果你的服务就那么两三个也没有域名的概念直接用NodePort反而更简单。比如一些内部工具、监控面板、调试接口NodePort是最轻量的方案。不要在架构复杂度没有达到一定程度时强行引入Ingress。工具是好工具但每多一层组件就多了很多排错变量。11. 版本演进与历史避坑11.1 kube-proxy模式的变化早些年Kubernetes的kube-proxy只有userspace和iptables两种模式。userspace模式是用户态代理性能较差后来默认转向iptables。再后来IPVS模式推出逐渐成为大规模集群的首选。如果你用的是很老的Kubernetes版本比如1.10以下遇到性能问题时可以检查一下kube-proxy的模式。新版本已经默认推荐IPVS模式除非你手动改成了iptables。11.2 不要依赖NodePort的端口稳定性NodePort的端口分配在Kubernetes里有一定的不确定性。如果创建时没有指定端口一定是自动分配而且这个自动分配的范围是集群级别的。不同环境里同一个Service可能拿到完全不同的端口。所以凡是依赖固定端口的场景务必显式指定nodePort凡是写自动化脚本或者对接CI/CD流水线的也建议先通过kubectl get svc获取当前端口不要硬编码到脚本里。11.3 多集群场景下的NodePort冲突如果你的公司维护了多个Kubernetes集群每个集群的nodePort范围可能默认一样。在跨集群访问的场景下比如两个集群通过联邦或者网关互通就需要确认端口分配情况避免因为端口相同导致路由冲突。这种问题通常不会在开发环境中暴露生产环境规模大了才会遇到。提前在集群建设规划时统一nodePort管理策略能少踩很多坑。12. 最后说点实在话NodePort在我眼里是Kubernetes网络模型里最“平易近人”的一个入口。它不需要你接触云厂商的负载均衡器上限不需要额外的收费一个节点一个端口就能打通内外的流量。但正因为实现简单它把许多复杂的网络细节藏在了背后遇到问题时反而容易让人无从下手。我个人的体会是想真正理解NodePort最好的办法不是背概念而是亲手把一个Service创建出来然后故意把targetPort写错一次、把nodePort写超范围一次、把安全组不放行一次。每一次报错都能让你对这个机制的印象深一分。把这些坑踩完了再看生产环境里的各种方案都能一目了然。最后分享一个小技巧排查NodePort问题时kubectl describe svc service-name这个命令的信息量非常大它会把Port、NodePort、Endpoints、Selector这些关键信息全部展示出来。我在现场排障时第一件事就是看这个输出能快速定位80%的问题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →