1. 为什么Service这件事值得单独拎出来讲
刚接触Kubernetes的人,十有八九会在Service上卡一下。Pod跑起来了,日志也正常,可就是访问不到;或者集群内部调得好好的,一到外部接入就各种超时。我见过不少团队,Deployment写得漂漂亮亮,一到Service和Endpoints就开始凭感觉配,出了问题只能重启碰运气。
这篇内容就是围绕Kubernetes中的Service用法以及如何通过Endpoints关联容器外服务这两个核心点展开的。前者解决的是集群内部服务发现与负载均衡的问题,后者解决的是一个更现实的场景:你的业务跑在K8s里,但有些依赖——比如一套老系统、一台独立部署的数据库、一个第三方提供的固定IP接口——根本不在集群里,你又不想让代码里写死IP。这时候Endpoints就是那把钥匙。
适合谁来读?如果你已经能跑起Pod、写过Deployment,但对Service的类型选择、Endpoints的手动维护还停留在“照着文档抄”的阶段,那这篇基本能帮你把这块补上。我会从设计思路讲到实操配置,再到踩过的坑,尽量把每个“为什么这么配”说清楚,而不是只给一段YAML让你复制。
先给个整体认知:Service本质上是给一组Pod提供一个稳定的访问入口和虚拟IP,它靠Label Selector找到后端Pod,自动维护Endpoints列表;而当你把Selector去掉、手动写Endpoints时,Service就变成了一个指向任意IP:Port的“软路由”。这两条路径,一条是自动挡,一条是手动挡,用对了场景才不出问题。
2. Service的整体设计与类型选型思路
2.1 Service到底解决了什么问题
Pod是“短命”的,重启、扩缩容、调度到别的节点,IP都会变。如果前端直接拿Pod IP去调后端,那基本等于给自己埋雷。Service的出现就是为了给这组动态的Pod一个固定的虚拟IP(ClusterIP)和DNS名字,调用方只管认这个名字,后面的Pod怎么变由Service去兜。
这里有个容易被忽略的点:Service的ClusterIP是虚拟的,它不对应任何真实网卡。流量能到Pod,靠的是每个节点上的kube-proxy维护的转发规则(iptables或ipvs模式)。所以你ping ClusterIP是ping不通的,这是正常现象,不代表Service坏了。我见过有人拿ping去验证Service,然后误判故障,白白排查半天。
Service通过Selector匹配Pod的Label,匹配到的Pod的IP和端口会被写进同名的Endpoints对象里。kube-proxy再根据Endpoints里的地址做负载均衡。理解这条链路,后面手动维护Endpoints时你就知道自己在动哪一环了。
2.2 四种Service类型的取舍逻辑
选型这件事,很多人是“看到哪个用哪个”,其实每种类型背后对应的是不同的暴露范围。我按使用频率和场景给你捋一遍。
| 类型 | 暴露范围 | 典型场景 | 关键注意点 |
|---|---|---|---|
| ClusterIP | 仅集群内部 | 内部微服务互调 | 默认类型,最常用 |
| NodePort | 集群外可访问(通过节点IP+端口) | 临时对外暴露、测试环境 | 端口范围默认30000-32767,需规划 |
| LoadBalancer | 集群外,依赖云厂商LB | 生产环境对外服务 | 云上才有真实LB,自建集群需额外组件 |
| ExternalName | 无ClusterIP,返回CNAME | 把外部域名映射成集群内名字 | 只做DNS层映射,不做代理 |
选型的核心判断就一句话:调用方在不在集群里。在集群里,ClusterIP足够;不在集群里,才考虑NodePort或LoadBalancer。ExternalName是个特殊存在,它不转发流量,只是让集群内的服务能用K8s的DNS名字去访问一个外部域名,适合做“名字统一”的适配层。
提示:NodePort不是不能用,但它暴露的是节点端口,节点一多端口管理就乱,生产环境我更倾向LoadBalancer或Ingress配合ClusterIP,NodePort留给调试和临时验证。
2.3 自动挡与手动挡:Selector模式 vs 无Selector模式
这是本文的核心分水岭。默认情况下,Service带Selector,Endpoints由控制器自动生成和维护,你什么都不用管。但一旦你删掉Selector,Kubernetes就不会自动生成Endpoints了,需要你手动创建一个同名的Endpoints对象,把外部地址填进去。
为什么要有手动挡?因为现实里总有些服务不在集群内。比如一套历史遗留的授权服务部署在独立虚机上,或者某个数据源只提供固定IP。你当然可以在代码里写死IP,但那样一换地址就得改代码重新发版。用无Selector的Service加手动Endpoints,调用方仍然用集群内的DNS名字访问,地址变更只改Endpoints,代码零改动。这就是它最大的价值——把“外部地址”这件事收敛到K8s的配置层。
代价也要说清楚:手动维护意味着没有自动健康检查,Pod级别的探针管不到外部地址,地址挂了Service不会自动摘除。所以这套方案适合相对稳定的外部依赖,配合外部自身的监控来做。
3. 核心细节解析与实操要点
3.1 一个标准Service的字段拆解
先看一个最典型的ClusterIP Service,把每个字段的作用讲透,后面改起来心里有数。
apiVersion: v1 kind: Service metadata: name: demo-app-svc namespace: default spec: type: ClusterIP selector: app: demo-app ports: - name: http protocol: TCP port: 80 targetPort: 8080selector是匹配Pod Label的,必须和Pod模板里的labels对得上,差一个字符都匹配不到。port是Service自身暴露的端口,也就是调用方访问的端口;targetPort是Pod容器实际监听的端口。这两个经常被混淆——调用方访问demo-app-svc:80,流量最终打到Pod的8080。很多人配完发现连不上,一查是targetPort写成了容器没监听的端口。
name字段在多端口场景下是必填的,单端口可以省略,但我建议养成习惯都写上,后面加端口时不用回头改。protocol默认TCP,UDP场景要显式声明。
3.2 targetPort的三种写法与选择
targetPort支持数字、端口名、以及省略(默认等于port)。数字最直观,但有个隐患:如果Pod里容器端口改了,Service得同步改。用端口名可以解耦——在Pod的containerPort里给端口起个名字,Service的targetPort写这个名字,容器端口变了只要名字不变,Service不用动。
# Pod侧 ports: - name: web containerPort: 8080 # Service侧 targetPort: web这个技巧在多容器Pod或者端口经常调整的项目里特别省心。省略targetPort等于port,只适合两者恰好一致的简单场景,我不太推荐,容易在后期埋坑。
3.3 Endpoints对象的结构与手动维护要点
Endpoints的YAML结构很朴素,核心就是subsets里的addresses和ports。
apiVersion: v1 kind: Endpoints metadata: name: external-db-svc namespace: default subsets: - addresses: - ip: 192.0.2.10 - ip: 192.0.2.11 ports: - port: 3306 protocol: TCP几个硬性要求必须记住:Endpoints的name和namespace必须与Service完全一致,否则关联不上;addresses里填的是外部真实IP,不能是域名;ports里的port要和Service的targetPort对应上。这里最容易错的是把域名填进ip字段,Kubernetes只认IP,填域名不会报错但流量到不了。
手动维护时,地址的增删就是改这个对象。改完kube-proxy会在秒级内感知并更新转发规则,不需要重启任何组件。实测下来,改完Endpoints后新连接基本立刻生效,已有长连接不受影响,这点在做地址切换时要注意——老连接不会自动断。
注意:无Selector的Service,其Endpoints不会被控制器覆盖,但如果你误加了selector,控制器会接管并清空你手写的内容。所以手动挡的Service,spec里千万别留selector字段。
3.4 端口映射的对齐关系
手动Endpoints场景下,端口对齐是最容易翻车的地方。Service的port是调用方访问的端口,targetPort在这里其实不参与转发(因为没有Pod),真正决定流量去向的是Endpoints里ports的port。但为了语义清晰,我习惯让Service的targetPort和Endpoints的port保持一致。
举个实际配置:外部MySQL在192.0.2.10:3306,集群内想用external-db-svc:3306访问。
apiVersion: v1 kind: Service metadata: name: external-db-svc spec: ports: - port: 3306 targetPort: 3306 --- apiVersion: v1 kind: Endpoints metadata: name: external-db-svc subsets: - addresses: - ip: 192.0.2.10 ports: - port: 3306调用方连external-db-svc:3306,流量经kube-proxy转到192.0.2.10:3306。如果外部端口是3307而你想内部仍用3306,那就Service port写3306、Endpoints port写3307,这样内外端口解耦,调用方无感知。
4. 实操过程与核心环节实现
4.1 从零搭一个带Selector的Service并验证
先走一遍自动挡的完整流程,把验证方法固化下来,后面手动挡也照这个套路验。
第一步,准备一个带Label的Deployment。假设镜像里应用监听8080。
apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 2 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: app image: demo-app:1.0 ports: - containerPort: 8080第二步,创建Service,selector写app: demo-app,port 80,targetPort 8080。
第三步,验证。先看Endpoints有没有自动生成:
kubectl get endpoints demo-app-svc正常应该看到两个Pod IP和8080端口。如果addresses为空,八成是selector和Pod Label对不上,用kubectl get pods --show-labels核对。
第四步,集群内验证连通性。起一个临时Pod去curl:
kubectl run tmp-test --rm -it --image=busybox -- sh # 容器内执行 wget -qO- http://demo-app-svc.default.svc.cluster.local用完整DNS名能验证DNS解析和Service转发两条链路。如果完整名通、短名不通,那是namespace或search domain的问题;如果都不通,先查Endpoints再查kube-proxy。
4.2 手动Endpoints关联外部服务的完整落地
现在走手动挡。目标:集群内通过external-api-svc访问一台集群外的服务192.0.2.20:9000。
第一步,创建无Selector的Service。注意spec里只有ports,没有selector。
apiVersion: v1 kind: Service metadata: name: external-api-svc namespace: default spec: ports: - name: api port: 9000 targetPort: 9000 protocol: TCP第二步,创建同名Endpoints。
apiVersion: v1 kind: Endpoints metadata: name: external-api-svc namespace: default subsets: - addresses: - ip: 192.0.2.20 ports: - name: api port: 9000 protocol: TCP第三步,验证关联。kubectl get endpoints external-api-svc应该显示192.0.2.20:9000。然后起临时Pod访问external-api-svc:9000,能通就说明链路打通。
这里有个实操细节:Endpoints的ports里name字段建议和Service的port name一致,虽然单端口时不是强制,但多端口时靠name做匹配,不一致会导致端口映射错乱。我吃过这个亏,两个端口配下来流量全跑到一个端口上,排查了半天才发现是name对不上。
4.3 多外部地址的负载与故障切换
Endpoints的addresses支持填多个IP,kube-proxy会在这些地址间做负载均衡。这给外部依赖提供了一层简单的高可用。
subsets: - addresses: - ip: 192.0.2.20 - ip: 192.0.2.21 ports: - port: 9000但要注意,这种负载均衡是连接级别的,不是请求级别,长连接场景下分布可能不均。而且它不做健康检查,某个IP挂了,kube-proxy照样往里转发,直到你手动把它从addresses里删掉。所以这套方案的高可用是“半自动”的——地址池你维护,故障摘除你来做。如果外部服务本身有VIP或负载均衡器,更稳妥的做法是Endpoints里只填那个VIP,把高可用交给外部设施。
4.4 用ExternalName做域名映射的补充方案
如果外部依赖是以域名形式提供的,且你只是想在集群内用统一名字访问,ExternalName更轻量。
apiVersion: v1 kind: Service metadata: name: external-search spec: type: ExternalName externalName: search.example.internal集群内访问external-search会得到一条指向search.example.internal的CNAME记录。它不经过kube-proxy转发,纯DNS层解析,所以没有负载均衡能力,也不支持端口映射。适合“只是改个名字”的场景,不适合需要端口转换或地址池的情况。选它还是选手动Endpoints,判断标准就是:要不要做端口映射和多地址负载。要,就Endpoints;不要,ExternalName更省事。
5. 常见问题与排查技巧实录
5.1 Service访问不通的分层排查法
访问不通别急着改配置,按层往下查效率最高。我整理了一张速查表,基本覆盖了九成问题。
| 现象 | 可能原因 | 排查命令 | 处理 |
|---|---|---|---|
| Endpoints为空 | selector与Pod Label不匹配 | kubectl get ep <svc> | 核对labels |
| Endpoints有地址但连不上 | targetPort与容器端口不符 | kubectl describe svc | 改targetPort |
| 集群内短名不通、全名通 | DNS search域问题 | nslookup <svc> | 用全名或查resolv.conf |
| 手动Endpoints不生效 | name/namespace不一致 | kubectl get ep -n <ns> | 对齐名称 |
| 外部地址通但端口错 | Endpoints port与Service不对齐 | kubectl get ep -o yaml | 对齐端口 |
| 偶发超时 | 外部地址部分不可用 | 逐个curl外部IP | 摘除故障地址 |
排查顺序我习惯是:先看Endpoints有没有地址,再看端口对不对,最后才怀疑网络策略或kube-proxy。因为前两步是配置问题,占了大头,网络层问题反而少见。
5.2 手动Endpoints的几个隐蔽坑
第一个坑:给无Selector的Service误加了selector。加完之后控制器接管,你手写的Endpoints被清空,表现为“昨天还好好的今天突然不通”。改配置时一定检查spec里有没有残留selector。
第二个坑:Endpoints里填了域名。Kubernetes不校验格式,填域名不报错,但kube-proxy按IP处理,流量直接丢弃。这个错误很隐蔽,因为kubectl get ep看起来“有地址”,实际是无效的。
第三个坑:namespace不一致。Service在default,Endpoints建到了别的namespace,两者永远关联不上。多namespace环境里这个错很常见,创建时带上-n参数并核对。
第四个坑:改完Endpoints没验证长连接。地址切换后新连接走新地址,老连接还挂在旧地址上,如果旧地址已经下线,表现为“部分请求超时”。做地址变更时要么选低峰期,要么在应用层做重连。
5.3 关于kube-proxy模式的实操心得
iptables模式下,Service规则多了之后规则链会很长,大规模集群里转发性能会受影响;ipvs模式用哈希表,规模大时更稳。这个选择在集群初始化时就定了,后期改要动kube-proxy配置。如果你发现Service数量上千后转发延迟上升,可以往ipvs方向评估。不过对大多数中等规模集群,iptables足够用,不用过早优化。
另外,改完Service或Endpoints后,如果怀疑规则没生效,可以在节点上iptables-save | grep <svc-name>看规则有没有更新(iptables模式)。正常情况下秒级生效,如果长时间没变,检查kube-proxy Pod是否正常。
6. 一些配置层面的经验补充
Service的sessionAffinity默认是None,也就是每次连接随机选后端。如果你的应用有会话保持需求,可以设成ClientIP,同一客户端IP的请求会落到同一后端。但要注意这会削弱负载均衡效果,且在有NAT的环境里客户端IP可能被统一,导致流量集中。用之前想清楚是不是真的需要。
还有externalTrafficPolicy,NodePort和LoadBalancer类型下设为Local可以保留客户端源IP,但只有运行了Pod的节点才会响应,可能造成流量分布不均。这个参数和源IP保留、负载均衡是一组权衡,没有银弹,按实际需求选。
最后说个我自己的习惯:所有手动维护的Endpoints,我都会在metadata里加annotation标注维护人和用途,比如owner: platform-team、purpose: legacy-auth-service。集群里对象一多,半年后回头看一个裸Endpoints,根本不知道它指向哪、能不能删。这点小成本,能省掉后面很多沟通。
这套Service加Endpoints的组合,自动挡覆盖了绝大多数内部服务发现场景,手动挡则把集群外的依赖也纳入了统一的访问体系。把两条路径的边界和坑摸清楚,K8s里的服务连通性这块基本就不会再成为阻塞点。