Service :微服务通信、负载、故障难题的解决方案
一、引言
在前面我们了解了 Deployment 和 DaemonSet 这两个 API 对象,它们都是在线业务, 只是以不同的策略部署应用,Deployment 创建任意多个实例,Daemon 为每个节点创建一个实例。 这两个 API 对象可以部署多种形式的应用,而在云原生时代,微服务无疑是应用的主流形态。 为了更好地支持微服务以及服务网格这样的应用架构,Kubernetes 又专门定义了一个新的对象:Service,它是集群内部的负载均衡机制,用来解决服务发现的关键问题。
二、为什么需要Service
有了 Deployment 和 DaemonSet,我们在集群里发布应用程序的工作轻松了很多。借助 Kubernetes 强大的自动化运维能力,我们可以把应用的更新上线频率由以前的月、周级别提 升到天、小时级别,让服务质量更上一层楼。
不过,在应用程序快速版本迭代的同时,另一个问题也逐渐显现出来了,就是“服务发现”。
在 Kubernetes 集群里 Pod 的生命周期是比较“短暂”的,虽然 Deployment 和 DaemonSet 可 以维持 Pod 总体数量的稳定,但在运行过程中,难免会有 Pod 销毁又重建,这就会导致 Pod 集合处于动态的变化之中。 这种“动态稳定”对于现在流行的微服务架构来说是非常致命的,试想一下,后台 Pod 的 IP 地 址老是变来变去,客户端该怎么访问呢?如果不处理好这个问题,Deployment 和 DaemonSet 把 Pod 管理得再完善也是没有价值的。
其实,这个问题也并不是什么难事,业内早就有解决方案来针对这样“不稳定”的后端服务,那 就是“负载均衡”,典型的应用有 LVS、Nginx 等等。它们在前端与后端之间加入了一个“中间 层”,屏蔽后端的变化,为前端提供一个稳定的服务。Kubernetes 就按照这个思路,定义了新的 API 对 象:Service。
Service 的工作原理和 LVS、Nginx 差不多,Kubernetes 会给它分配一 个静态 IP 地址,然后它再去自动管理、维护后面动态变化的 Pod 集合,当客户端访问 Service,它就根据某种策略,把流量转发给后面的某个 Pod。
下面的这张图来自 Kubernetes官网文档,比较清楚地展示了 Service 的工作原理:
这里 Service 使用了 iptables 技术,每个节点上的 kube-proxy 组件自动维护 iptables 规则,客户不再关心 Pod 的具体地址,只要访问 Service 的固定 IP 地址,Service 就 会根据 iptables 规则转发请求给它管理的多个 Pod,是典型的负载均衡架构。
看到这里我们也就明白了为什么需要Service,回想一下微服务通常以 Pod 为单位部署,但 Pod 有个 “致命缺点”——生命周期不稳定,扩容/缩容,重启,Pod 的 IP 地址都可能变化,一旦 Pod IP 变动,调用就会失败,整个微服务链路都会断。而 Service 的核心作用,就是为这些 “不稳定的 Pod” 提供一个稳定的访问入口,就像给一群动态变化 的 Pod 挂了个 “固定门牌”,不管 Pod 怎么变,调用方只要访问这个 “门牌” 就行。
简单说:Service = 稳定 IP + 自动负载均衡,是 微服务在 K8S 中实现 “服务发现” 和 “负载均衡 ” 的关键组件。
三、使用 YAML 描述 Service
我们先来看一下Service YAML:
apiVersion: v1
kind: Service
metadata:name: ngx-svc
spec:selector: # Service的标签选择器,和Pod标签匹配app: ngx-svcports:- port: 80 # Service的端口targetPort: 80 # Pod中Java服务的端口
Service 的定义非常简单,在“spec”里只有两个关键字段,selector 和 ports。
selector 和 Deployment/DaemonSet 里的作用是一样的,用来过滤出要代理的那些 Pod。 因为我们指定要代理 Deployment,所以 Kubernetes 就为我们自动填上了 ngx-dep 的标签, 会选择这个 Deployment 对象部署的所有 Pod。
从这里你也可以看到,Kubernetes 的这个标签机制虽然很简单,却非常强大有效,很轻松就 关联上了 Deployment 的 Pod。 ports 就很好理解了,里面的两个字段分别表示外部端口、内部端口,在这里 就是内外部都使用 80 端口。在这里也可以把 ports 改成“8080”等其他的端口,这样外部服务看到的就是 Service 给出的端口,而不会知道 Pod 的真正服务端口。
为了直观的看清楚 Service 与它引用的 Pod 的关系,我们还是用一张图来展示一下:
四、Kubernetes 里使用 Service
首先,我们创建一个 ConfigMap,定义一个 Nginx 的配置片段,它会输出服务器的地址、主 机名、请求的 URI 等基本信息:
apiVersion: v1
kind: ConfigMap
metadata:name: ngx-conf
data:default.conf: |server {listen 80;location / {default_type text/plain;return 200 'srv: $server_addr:$server_port\nhost: $hostname\nuri: $request_method $request_uri';}}
使用命令进行创建
kubectl apply -f ngx-conf.ymal
然后我们在 Deployment 的“template.volumes”里定义存储卷,再用“volumeMounts”把配置 文件加载进 Nginx 容器里:
apiVersion: apps/v1
kind: Deployment
metadata:name: ngx-dep
spec:replicas: 2selector:matchLabels:app: ngx-deptemplate:depmetadata:labels:app: ngx-depspec:volumes:- name: ngx-conf-volconfigMap:name: ngx-confcontainers:- image: nginx:alpinename: nginxports:- containerPort: 80volumeMounts:- mountPath: /etc/nginx/conf.dname: ngx-conf-vol
使用命令进行创建:
kubectl apply -f ngx-dep.yaml
我们可以清楚的看到我们部署的两个ngxin deployment已经启动。
部署这个 Deployment 之后,我们就可以创建 Service 对象了,用的还是 kubectl apply:
apiVersion: v1
kind: Service
metadata:name: ngx-svc
spec:selector: # Service的标签选择器,和Pod标签匹配app: ngx-depports:- port: 80 # Service的端口targetPort: 80
可以看到,Kubernetes 为 Service 对象自动分配了一个 IP 地址“10.108.220.201”,这个地址 段是独立于 Pod 地址段的。而且 Service 对象的 IP 地址还 有一个特点,它是一个“虚地址”,不存在实体,只能用来转发流量。
想要看 Service 代理了哪些后端的 Pod,你可以用 kubectl describe 命令:
这里显示 Service 对象管理了两个 endpoint,分别是“10.224.154.194:80”和“10.224.154.197:80”,初 步判断与 Service、Deployment 的定义相符,那么这两个 IP 地址是不是 Nginx Pod 的实际地 址呢?
我们还是用 kubectl get pod 来看一下,加上参数 -o wide:
把 Pod 的地址与 Service 的信息做个对比,我们就能够验证 Service 确实用一个静态 IP 地址 代理了两个 Pod 的动态 IP 地址,同时我们看到两个pod分别运行在不同的节点。
看到这里相信大家就明白了Service 怎么知道哪些 Pod 的 IP 是需要转发的,答案是Endpoints。
K8S 会自动维护一个和 Service 同名的 Endpoints 资源,它的作用就是记录所有符合 Service 标签选择 器的 Pod 的 IP 和端口。当 Pod 新增或删除时,Endpoints 会自动更新;当你访问 Service 时,Service 会先查 Endpoints,再把请求转发到 Endpoints 中的某个 Pod 上。
4.1 测试 Service 的负载均衡
因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段,所以我们需要用 kubectl exec 进入到 Pod 内部,再用 curl 等工具来访问 Service:
创建一个临时的 busybox 容器,循环向 Service 发送请求,观察返回的主机名变化(验证负载均衡):
kc run -it --rm --image=busybox:1.35 test-lb -- sh
在容器内部执行循环请求:
while true; do wget -qO- http://ngx-svc; sleep 1; done
预期结果:每次返回的 host
字段会在两个 Pod 的名称之间切换,证明负载均衡生效。
我们再试着删除一个 Pod,看看 Service 是否会更新后端 Pod 的信息,实现自动化的服务发现:
由于 Pod 被 Deployment 对象管理,删除后会自动重建,而 Service 又会通过 controllermanager 实时监控 Pod 的变化情况,所以就会立即更新它代理的 IP 地址。通过截图你就可以看到有一个 IP 地址“10.224.154.194”消失了,换成了新的“10.224.154.196”,它就是新创建的 Pod。
五、Service 的不同类型
由于 Service 是一种负载均衡技术,所以它不仅能够管理 Kubernetes 集群内部的服务,还能 够担当向集群外部暴露服务的重任。 Service 对象有一个关键字段“type”,表示 Service 是哪种类型的负载均衡。前面我们看到的 用法都是对集群内部 Pod 的负载均衡,所以这个字段的值就是默认的“ClusterIP”,Service 的 静态 IP 地址只能在集群内访问。
除了“ClusterIP”,Service 还支持其他三种类型,分别是“ExternalName”“LoadBalancer” “NodePort”。接下来就分别介绍一下他们。
5.1 ClusterIP:集群内访问,微服务间调用首选
特点:K8S 会给 Service 分配一个集群内部的虚拟 IP(仅集群内可访问),只能在集群中的 Pod 或节点上访问。
适用场景:微服务之间的内部调用,比如订单服务调用支付服务,两者都在 K8S 集群内,用 ClusterIP 最安全。
apiVersion: v1
kind: Service
metadata:name: ngx-svc
spec:type: ClusterIP # 类型为ClusterIP(默认类型,可省略)selector: # Service的标签选择器,和Pod标签匹配app: ngx-depports:- port: 80 # Service的端口targetPort: 80
5.2 NodePort:开发测试用,外部直接访问
特点:在 ClusterIP 的基础上,在集群的每个节点上开放一个 “固定端口”(比如 30080),外部可以 通过 “节点 IP:NodePort” 访问服务。
适用场景:开发或测试环境中,需要从本地电脑访问 K8S 中的 Java 服务(比如调试接口),不用复杂 的配置。 注意:NodePort 的端口范围默认是 30000-32767,不能随便选。 示例配置:
apiVersion: v1
kind: Service
metadata:name: ngx-svc-nodeport
spec:type: NodePort selector: # Service的标签选择器,和Pod标签匹配app: ngx-depports:- port: 80 # Service的端口targetPort: 80
通过上图就会看到“TYPE”变成了“NodePort”,而在“PORT”列里的端口信息也不一样,除了集群内部使 用的“80”端口,还多出了一个“30950”端口,这就是 Kubernetes 在节点上为 Service 创建的专 用映射端口。因为这个端口号属于节点,外部能够直接访问,所以现在我们就可以不用登录集群节点或者进 入 Pod 内部,直接在集群外使用任意一个节点的 IP 地址,就能够访问 Service 和它代理的后 端服务了。使用集群中任何一个节点IP + port就能调用服务。
为了更好理解NodePort 与 Service、Deployment 的对应关系,因此画成了图,你看了应该就能更好地明白它 的工作原理
5.3 LoadBalancer:生产环境首选,公网访问 + 自动负载
特点:借助云服务商(阿里云、AWS 等)的负载均衡器,为 Service 分配一个公网 IP,外部请求先到 云负载均衡器,再转发到 Service 和 Pod。
适用场景:生产环境中,需要对外暴露的 Java 服务(比如用户端访问的 API 网关、前端调用的后端服务),稳定性和可用性要求高。示例配置:
apiVersion: v1
kind: Service
metadata:name: ngx-svc-balancer
spec:type: LoadBalancer selector: # Service的标签选择器,和Pod标签匹配app: ngx-depports:- port: 80 # Service的端口targetPort: 80
配置后,云服务商(比如阿里云)会自动创建一个 SLB 负载均衡器,并分配公网 IP。外部用户访问这 个公网 IP,就能通过 API 网关调用后端的 Java 微服务,负载均衡由云 SLB 和 Service 共同实现。
5.4 ExternalName:访问集群外服务,不用硬编码 IP
特点:把 Service 映射到一个外部的 DNS 名称(比如mysql.abc.com),而不是集群内的 Pod。当访 问这个 Service 时,K8S 会自动把请求转发到外部的 DNS 地址。
适用场景:Java 服务需要调用集群外的服务,比如访问云数据库(MySQL、Redis)或第三方 API,- 不想在代码中硬编码 IP(避免 IP 变动导致服务不可用)。 示例配置:
apiVersion: v1
kind: Service
metadata:name: ngx-svc-balancer
spec:type: ExternalName selector: # Service的标签选择器,和Pod标签匹配app: ngx-depports:- port: 80 # Service的端口targetPort: 80
调用方式:直接用 Service 名称访问,比如jdbc:mysql://external-mysql-svc:3306/ order_db,K8S 会把external-mysql-svc解析为mysql.abc.com,后续即使外部 MySQL 的 IP 变了 ,只要 DNS 不变,代码就不用改。
六、小结
对于开发者来说,K8S Service 不是 “可选组件”,而是 “必用组件”—— 它解决了 Pod 动态 IP 的痛点,实现了微服务的自动负载均衡,还能灵活对接内部和外部服务。
最后快速回顾几个重点:
- Service 的核心:为 Pod 提供稳定访问入口,本质是 “稳定 IP + Endpoints + 负载均衡”;
- 4 种类型的选择:内部调用用 ClusterIP,开发测试用 NodePort,生产公网用 LoadBalancer,访问外 部服务用 ExternalName;
Tips: 为了大家快速高效的学习,已经将文章提交到了git仓库,涵盖后端大部分技术,以及后端学习路线,仓库内容会持续更新,建议 Star 收藏 以便随时查看https://gitee.com/bxlj/java-article。