11K8Service:轻松搞定服务发现和负载均衡
文章目录
经过前面几节课的学习,我们已经可以发布高可用的业务了,通过 PV 持久化地保存数据,通过 Deployment或Statefulset 这类工作负载来管理多实例,从而保证服务的高可用。
想一想,这个时候如果有别的应用来访问我们的服务的话,该怎么办呢?直接访问后端的 Pod IP 吗?不,这里我们还需要做服务发现(Service Discovery)。
为什么需要服务发现?
传统的应用部署,服务实例的网络位置是固定的,即在给定的机器上进行部署,这个时候的服务地址一般是机器的 IP 加上某个特定的端口号。
但是在 Kubernetes 中,这是完全不同的。业务都是通过 Pod 来承载的,每个 Pod 的生命周期又很短暂,用后即焚,IP 地址也都是随机分配,动态变化的。而且,我们还经常会遇到一些高并发的流量进来,这时候往往需要快速扩容,服务的实例数也会随之动态调整。因此我们在这里就不能用传统的基于 IP 的方式去访问某个服务了。这个对于所有云上的系统,以及微服务应用体系,都是一个大难题。这时我们就需要做服务发现来确定服务的访问地址。
今天我们就来聊聊 Kubernetes 中的服务发现 —— Service。
Kubernetes 中的 Service
在之前的课程中,我们知道 Deployment、StatefulSet 这类工作负载都是通过 labelSelector 来管理一组 Pod 的。那么 Kubernetes 中的 Service 也采用了同样的做法,如下图。
(https://platform9.com/wp-content/uploads/2019/05/kubernetes-service-discovery.jpg)
这样一个 Service 会选择集群所有 label 中带有 app=nginx 和 env=prod 的 Pod。
我们来看看这样的一个 Service 是如何定义的:
|
|
现在我们先来看如下一个 Deployment的定义:
|
|
我们创建好这个 Deployment后,查看其 Pod 状态:
|
|
我们再来创建下上面定义的 Service:
|
|
可以看到,这时候 Service 关联的 Endpoints 里面有三个 IP 地址,和我们上面看到的 Pod IP 地址完全吻合。
我们试着来缩容 Deployment 的副本数,再来看看 Service 关联的 Pod IP 地址有什么变化:
|
|
可见当 Pod 的生命周期发生变化时,比如缩容或者异常退出,Service 会自动把有问题的 Pod 从后端地址中摘除。这样实现的好处在于,我们可以始终通过一个虚拟的稳定 IP 地址来访问服务,而不用关心其后端真正实例的变化。Kubernetes 中 Service 一共有四种类型,除了上面讲的 ClusterIP,还有 NodePort、LoadBalancer 和 ExternalName。
其中 LoadBalancer 在云上用的较多,使用的时候需要跟各家云厂商做适配,比如部署对应的 cloud-controller-manager。有兴趣的话,可以查看这个文档,看看如何在云上使用。LoadBalancer主要用于做外部的服务发现,即暴露给集群外部的访问。
ExternalName 类型的 Service 在实际中使用的频率不是特别高,但是对于某些特殊场景还是有一些用途的。比如在云上或者内部已经运行着一个应用服务,但是暂时没有运行在 Kubernetes 中,如果想让在 Kubernetes 集群中的 Pod 访问该服务,这时当然可以直接使用它的域名地址,也可以通过 ExternalName 类型的 Service 来解决。这样就可以直接访问 Kubernetes 内部的 Service 了。
这样一来方便后续服务迁移到 Kubernetes 中,二来也方便随时切换到备份的服务上,而不用更改 Pod 内的任何配置。由于使用频率并不高,我们不做重点介绍,有兴趣可以参考这篇文档。
我们最后来看下另外一种 NodePort 类型的 Service:
|
|
顾名思义,这种类型的 Service 通过任一 Node 节点的 IP 地址,再加上端口号就可以访问 Service 后端负载了。我们看下面这个流量图,方便理解。
(https://miro.medium.com/max/1680/1*CdyUtG-8CfGu2oFC5s0KwA.png)
NodePort 类型的 Service 创建好了以后,Kubernetes 会在每个 Node 节点上开个端口,比如这里的 30000 端口。这个时候我们可以访问任何一个 Node 的 IP 地址,通过 30000 端口即可访问该服务。
那么如果在集群内部,该如何访问这些 Service 呢?
集群内如何访问 Service?
一般来说,在 Kubernetes 集群内,我们有两种方式可以访问到一个 Service。
如果该 Service 有 ClusterIP,我们就可以直接用这个虚拟 IP 去访问。比如我们上面创建的 nginx-prod-svc-demo 这个 Service,我们通过 kubectl get svc nginx-prod-svc-demo -n dmeo 或 kubectl get svc nginx-prod-svc-demo -n dmeo 就可以看到其 Cluster IP 为 10.111.193.186,端口号为 80。那么我们通过 http(s)://10.111.193.186:80 就可以访问到该服务。
当然我们也可以使用该 Service 的域名,依赖于集群内部的 DNS 即可访问。还是以上面的例子做说明,同 namespace 下的 Pod 可以直接通过 nginx-prod-svc-demo 这个 Service 名去访问。如果是不同 namespace 下的 Pod 则需要加上该 Service 所在的 namespace 名,即 nginx-prod-svc-demo.demo 去访问。
如果在某个 namespace 下,Service 先于 Pod 创建出来,那么 kubelet 在创建 Pod 的时候,会自动把这些 namespace 相同的 Service 访问信息当作环境变量注入 Pod 中,即 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 。这里 SVCNAME 对应是各个 Service 的大写名称,名字中的横线会被自动转换成下划线。比如:
|
|
知道了这两种访问方式,我们就可以在启动 Pod 的时候,通过注入环境变量、启动参数或者挂载配置文件等方式,来指定要访问的 Service 信息。如果是同 namespace 的 Pod,可以直接从自己的环境变量中知道同 namespace 下的其他 Service 的访问方式。
那么这样通过该 Service 进行访问时,Kubernetes 又是如何实现负载均衡的呢,即将流量打到后端挂载的各个 Pod 上面去?
集群内部的负载均衡如何实现?
这一切都是通过 kube-proxy 来实现的。所有的节点上都会运行着一个 kube-proxy的服务,主要监听 Kubernetes 中的 Service 和 Endpoints。当 Service 或 Endpoints 发生变化时,就会调用相应的接口创建对应的规则出来,常用模式主要是 iptables 模式和 IPVS 模式。iptables 模式比较简单,使用起来也方便。而 IPVS 支持更高的吞吐量以及复杂的负载均衡策略,你可以通过官方文档了解更多 IPVS 模式的工作原理。
目前 kube-proxy 默认的工作方式是 iptables 模式,我们来通过如下一个 iptables 模式的例子来看一下实际访问链路是什么样的。
(https://d33wubrfki0l68.cloudfront.net/27b2978647a8d7bdc2a96b213f0c0d3242ef9ce0/e8c9b/images/docs/services-iptables-overview.svg)
当你通过 Service 的域名去访问时,会先通过 CoreDNS 解析出 Service 对应的 Cluster IP,即虚拟 IP。然后请求到达宿主机的网络后,就会被kube-proxy所配置的 iptables 规则所拦截,之后请求会被转发到每一个实际的后端 Pod 上面去,这样就实现了负载均衡。
Headless Service
如果我们在定义 Service 的时候,将spec.clusterIP设置为 None,这个时候创建出来的 Service 并不会分配到一个 Cluster IP,此时它就被称为Headless Service。
现在我们来通过一个例子来看看 Headless Service 有什么特殊的地方。我们在上面的 Service 基础上,增加了spec.clusterIP为None,并命名为nginx-prod-demo-headless-svc:
|
|
通过 kubectl 创建成功后,我们现在 kubectl get 一下看看:
|
|
可以看到这个叫 nginx-prod-demo-headless-svc 的 Service 并没有分配到一个 ClusterIP,符合预期,毕竟我们已经设置了 spec.clusterIP 为 None。
我们来创建一个 Pod,看看 DNS 记录有没有什么差别。 Pod 的 yaml 文件如下:
|
|
该 Pod 创建出来后,我们通过 kubectl exec 进入 Pod 中,运行如下两条 nslookup 查询命令,依次查看两个 Service 对应的 DNS 记录:
|
|
我们可以看到正常 Service nginx-prod-svc-demo 对应的 DNS 记录的是与虚拟 IP 10.111.193.166 有关的记录,而 Headless Service nginx-prod-demo-headless-svc 则解析到所有后端的 Pod 的地址。总结下, Headless Service 主要有如下两种场景。
用户可以自己选择要连接哪个 Pod,通过查询 Service 的 DNS 记录来获取后端真实负载的 IP 地址,自主选择要连接哪个 IP;
可用于部署有状态服务。回顾下,我们在 StatefulSet 那节课也有 Headless Service 例子,每个 StatefulSet 管理的 Pod 都有一个单独的 DNS 记录,且域名保持不变,即
写在最后
Service 是 Kubernetes 很重要的对象,主要负责为各种工作负载暴露服务,方便各个服务之间互访。通过对一组 Pod 提供统一入口,Service 极大地方便了用户使用,用户只需要与 Service 打交道即可,而不用过多地关心后端实例的变动,比如扩缩容、容器异常、节点宕机,等等。
正是因为有了 Service 的支持,你在 Kubernetes 部署业务会非常方便,这是相比较于 Docker Swarm 以及 Mesos Marathon 巨大的技术优势,可以说,它是 Kubernetes 是运行大规模微服务的最佳载体。
好的,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。
-– ### 精选评论 ##### *星: > 请教下老师,服务A使用nodeport访问,服务B使用host模式,如何访问,宿主的ip不是固定的 ###### 讲师回复: > nodeport的话,可以通过任一宿主机ip+port 的方式来访问。所以服务A 和服务B可以通过任一宿主机ip来访问。 ##### *悦: > apiVersion: v1kind: Podmetadata: name: headless-svc-test-pod namespace: demospec: containers: - name: dns-test image: busybox:1.28 sleep 3600’]这个怎么和headless server 匹配麻烦老师说下 ##### *悦: > apiVersion: v1kind: Servicemetadata: name: nginx-prod-demo-headless-svcspec: clusterIP: None app: nginx env: prod type: ClusterIP——pod怎么匹配的 pod没有labels标签怎么匹配 ##### **彬: > 老师,你好,请教一下,配置clusterip时,通过coredns解析到虚拟IP,请求是怎么转发到宿主机上的 ###### 讲师回复: > 这部分官方文档讲解的比较清楚,还是图。https://kubernetes.io/zh/docs/concepts/services-networking/service/#proxy-mode-iptables
文章作者 anonymous
上次更新 2024-06-11