作者 | 姜桥 责编 | 欧阳姝黎
在之前的<<干货|如何步入Service Mesh微服务架构时代>>一文中,实战演练了Service Mesh微服务架构的具体玩法,该案例中通过Istio+Kubernetes的组合,一组以Spring Boot框架开发的服务,在自身没有实现任何服务注册发现逻辑的情况下,被部署到Kubernetes后便能通过服务名直接完成服务接口调用,并且还能对调用进行限流、熔断及负载均衡等一系列服务治理操作。
这一切是怎么发生的呢?在今天介绍Service Mesh微服务架构有关服务注册发现原理的文章中将为大家揭晓答案。
传统微服务注册中心实现机制
在阐述Service Mesh服务注册发现机制前,先简单回顾下在以Spring Cloud为代表的传统微服务中是如何实现服务注册与发现的。
传统微服务体系中,注册中心无疑是整个微服务体系最重要的组成部分,没有服务注册中心提供的统一服务注册发现功能,微服务本身就无从谈起。不过相较于Service Mesh架构中服务注册发现,很大程度上是依赖于基础设施(例如数据面[envoy]、控制面[istio]以及Kubernetes集群),而无需微服务亲力亲为。在Spring Cloud传统架构中,服务注册、发现及健康性检查等服务治理逻辑则都需要微服务自己上下打点。
虽然不用重复造轮子,都有现成的服务治理组件及框架,但从应用运行形态上说,与服务注册发现相关的逻辑都是微服务直接与注册中心产生的交互。从系统架构上看,这种方式显然是将服务治理逻辑与业务应用耦合了,其运行逻辑如下图所示:
从上图可以看到,微服务启动时会通过集成的服务发现SDK,向注册中心(应用配置的注册中心地址)发送注册信息,注册中心在收到服务注册请求后将存储服务的基本信息;之后,如有服务消费者要调用该服务,那么调用方通过服务发现组件(例如Ribbon)就可以向注册中心查询目标微服务的地址列表,并通过获取的服务地址列表,以某种负载策略向目标微服务发起调用了。
而当服务节点发现变更时,新的节点会被重新注册,而下线的节点基于服务探活机制也会及时从服务注册中心被踢除,基于这样的服务注册/发现机制,微服务之间的通信顺畅就能得到保证。从这个角度看,注册中心与服务之间的健康性检查就显得很重要,如果注册中心不能及时将下线或故障的节点从可用服务器地址列表剔除,那么就很可能会造成微服务某些调用的失败。
那么一般怎样进行服务健康性检查呢?从方式上看,主流的健康性检查方式,主要有以下3种:
1、微服务主动探活
服务主动探活就是微服务定期向注册中心发送租约信息以表面自己的存活。在实际场景中主动探活是我们使用注册中心时用得最多的一种方式,如果服务规模不大,或者使用了类似于Eureka这样的最终一致性注册中心,那么主动探活就是一种最佳选择,它可以较大程度地避免服务部署在Kubernetes集群后,因为Pod IP重用而导致的旧有服务节点依然存活的问题,毕竟续约信息都是带着服务基础信息上报到注册中心的。
但这种方式也有明显的弊端,它会造成注册中心写操作压力变大。如果大量的服务同时发布,节点产生较大的变动,那么就可能产生大量的通知事件,从而对整个微服务体系的稳定产生较大影响。
此外,主动续约,也并不完全说明服务是健康的,因为某些特殊情况下可能会存在虽然服务无法对外提供服务,但还能正常向注册中心发出租约信息的问题。
2、注册中心发起健康性检查
前面分析了微服务主动探活方式的优缺点,而如果由注册中心发起健康性检查会怎么样呢?
这种方式是指微服务在注册时,同时向注册中心暴露自己的健康检查端点(如/actuator/health),注册中心通过定期访问来探测服务节点是否存活。
不过这种方式也不是完全没有问题,例如前面提到的Pod IP重用问题,如果其他微服务重用了之前节点的IP,那么就会发生失效节点被激活的假象。当然也有相应的解决方案,例如后面会讲到Istio微服务注册发现时Envoy会对服务名称进行二次Check的方式。
3、调用方负载均衡器进行健康性检查
第3种方案比较极端,注册中心不进行任何服务探活,全部由微服务调用方所在的负载均衡器进行探活。这种方案常见的一种场景就是 gRPC 的失效节点自动摘除功能。但这种方案依然具有 IP 被重用问题,所以此种方式在实际的场景中并不是很受欢迎。
前面讲了微服务注册发现常见的三种服务探活方式,其实三种方案各有利弊。以 Spring Cloud 微服务体系中,使用最广泛的几种注册中心为例,如 Eureka、Consul、Nacos,它们的对比如下表所示:
从表格中可以看到,除了Eureka主要采用服务主动探活方式外,Consul 及 Nacos都采用了多种服务探活方式,从而尽量规避不同方式的弊端,这也是为什么目前大部分实践逐步抛弃Eureka而采用Consul或Nacos的原因。
除了健康性检查外,上面表格还分别列出了这几种注册中心的一致性协议。这里顺带普及下CAP理论。CAP是分布式系统中重要的一种概念,它分别由一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三部分组成。在CAP理论中,一个分布式系统不可能三种都满足,一般只能同时满足其中的两种,例如Eureka和Nacos只能满足可用性和分区容错性,而无法满足一致性;Consul则只能满足一致性和分区容错性,而无法完全满足可用性。
在注册中心的场景中,一致性一般要求并不高,只要能达到最终一致性即可。毕竟在微服务架构中,涉及节点的注册和反注册,注册中心和客户端之间的通信需要一定时间,一致性本身也很难达到。所以在注册中心的选型中,一般会优先选择AP的系统,这也是目前还在以Spring Cloud构建微服务的实践中,除了自研外,开源技术中会优先选择Nacos作为服务注册中心的原因。
Kubernetes服务注册与自动发现
前面以一定篇幅回顾了 Spring Cloud 传统微服务体系中,注册中心有关的基本逻辑及常见开源技术。在具体讲述 Service Mesh 架构中服务注册发现的逻辑前,有必要先了解下Kubernetes容器编排中,与 Service 服务资源有关的概念。
因为最流行的 Service Mesh 方案(如Istio),大都选择了与Kubernetes集群相结合的方案,而其服务注册逻辑也主要是利用了 Kubernetes 的内部服务发现机制。例如 Istio 就是通过监听 Kubernetes Pod 的变化来实现服务发现的。当然这并不是说在 Service Mesh 中不能选择传统的注册中心方案,只不过实施起来可能需要改造或自研注册中心才能满足需求(兼容新旧微服务体系时,可能会这么考虑)。如果是全新设计的 Service Mesh 微服务架构,最佳的方案还是选择像 Istio 这样直接利用 Kubernetes 自身功能实现服务发现。
在 Kubernetes 中能够为 Service Mesh 微服务提供内部服务注册发现的基础就是 Service 类型的资源。Service 是 Kubernetes 抽象出来的服务概念,一组 Pod 的集合就是 Kubernetes 的 Service。在 Kubernetes 中 Pod 是最小的容器编排单元,一个 Pod 编排资源中可以定义多个容器,Pod 内的容器可以通过本地访问在 Pod 中通信,一组容器共享一个 Pod IP,这就是 Kubernetes 之所以被称为容器编排平台最核心的体现。
但 Pod 是有生命周期的,也就是说 Pod 的 IP 地址并不固定,它会随着 Pod 生命周期的变化而变化,所以如果以 Pod 为服务调用对象,那么IP地址的经常变化会导致调用方服务发现逻辑不稳定,从而使系统变得复杂。为了解决这个问题,Kubernetes 中就抽象出了 Service 资源类型,虽然 Pod的IP地址会变化,但是抽象的 Service 名称却是固定的,所以 Kubernetes 集群中通过 Service 名称就能访问这些后端 IP,调用方则不用再直接感知这些后端Pod IP的变化了。具体 Pod 与 Service 之间的映射关系,则完全由Kubernetes集群本身来实现。它们之间的关系如下图所示:
如上图所示,现在要负载均衡地访问一组相同的服务副本——订单,通过 Service 资源类型的定义,就可以将其对外表示成一个进程或服务资源对象,Kubernetes 会为其分配集群内的固定IP地址,之后通过请求 Service(集群内用名称、集群外可通过 Ingress 或 NodePort 方式暴露),Service 自己就会负载均衡地将古玩交易请求转发给相应的 Pod 节点。
Service 资源与 Pod 编排对象之间的关联,虽说从实现上是由 Kubernetes 集群自己管理的,但在服务发布时则是需要我们自己通过资源定义进行关联。以一段Kuberntes发布代码为例:
apiVersion: v1
kind: Service
metadata:
name: micro-order
labels:
app: micro-order
service: micro-order
spec:
type: ClusterIP
ports:
- name: http
#此处设置80端口的原因在于改造的Mock FeignClient代码默认是基于80端口进行服务调用
port: 80
targetPort: 9091
selector:
app: micro-order
—
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-order-v1
labels:
app: micro-order
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: micro-order
version: v1
template:
metadata:
labels:
app: micro-order
version: v1
spec:
containers:
- name: micro-order
image: 10.211.55.2:8080/micro-service/micro-order:1.0-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19091
#环境参数设置(设置微服务返回gRPC服务端的地址+端口)
env:
- name: GRPC_SERVER_HOST
value: micro-pay
- name: GRPC_SERVER_PORT
value: “18888”
如上述 Kubernetes 发布文件中高亮的代码,在定义Pod编排对象时通过metadata 标签定义了这一组 Service 的 label,然后通过selector标签指定响应的标签,这样Service就可以访问带有这组标签定义的Pod集合了。
以上就是 Kubernetes 实现服务注册发现基本原理,其中涉及的逻辑将被利用在Service Mesh 微服务平台 Istio 的设计实现中。
Istio 服务注册发现
接下来我们以 Istio(基于istio1.9架构)为代表的 Service Mesh 架构为例,看看它是如何实现服务注册发现的。经过前面的铺垫,相信你多少已经了解了一点 Istio 实现服务注册发现的基本原理:“通过监听 Kubernetes 节点变化及 Service 资源类型实现”。
接下来我们将进一步细化它,从运行逻辑的视角来分析下在Istio中控制面与数据面是如何配合实现微服务注册发现的。具体如下图所示:
上图所描述的就是 Istio 基于 Kubernetes 实现微服务注册发现的核心逻辑。Kubernetes 集群环境安装 Istio 后,Kubernetes 在创建 Pod 时,就会通过 Kube-APIserver 调用控制面组件的 Sidecar-Injector 服务、自动修改应用程序的描述信息并将其注入 SideCar,之后在创建业务容器的 Pod 中同时创建 SideCar 代理容器。SideCar 则会通过 xDS 协议与Istio控制面各组件连接,这里我们着重介绍Pilot控制面组件实现服务发现的内容。
首先明确的是在新版的 Istio中,服务发现的逻辑已经上移至控制面组件 Pilot,Pilot会监听 Kube-APIServer 中关于 Service、Endpoint、Pod等资源的变化,并实时下发给各微服务SideCar中的Pilot Agent 代理组件。这个过程是通过xDS 标准协议下发的,而下发的基本逻辑是 Envoy 启动时 Pilot 会把所有服务实例信息推送给 Envoy,后续则是有更新就推送更新数据至所有 Envoy,这样各个微服务的 Envoy 就能实时感知微服务实例节点的变化了。
这种方式保证各个微服务的 Envoy 代理能随时感知到 Kubernetes 集群中服务节点的变化,也天然实现了微服务的健康性检测逻辑,当然为了防止 Pod IP 重用的问题,Envoy 在接收 Pilot 推送的变动实例信息时也会对服务名称进行二次 Check,如果发现 IP 所对应的 Service 名称与之前不一致,则及时更新本地数据信息,防止调用出错。
通过上述服务发现系统的支持,服务消费方在调用目标微服务时,流量会被本 Pod 中的 Envoy 代理自动劫持,此时 Envoy 就会根据自身存储的服务实例映射信息,及控制面配置下发的服务治理规则,对调用执行负载均衡、熔断、限流等服务治理规则。
这就是以 Istio 为代表的 Service Mesh 微服务架构实现服务注册发现的基本逻辑,可以看到 Envoy 数据面与Pilot 控制面组件的配合自动实现了服务发现逻辑,而这一切对微服务本身来说都是无感知的!
后记
本篇文章简单总结和概述了从传统微服务架构到 Service Mesh 有关服务注册发现逻辑的变化,也进一步从侧面也感受到了以 Istio 为代表的 Service Mesh 微服务架构,在架构设计理念上的进步。希望本篇文章的内容能够对你有所启发,并进一步打开你进入 Service Mesh微服务架构时代的窗户,在后面的文章中我们还会进一步分享与Service Mesh原理和实践相关的知识,敬请期待!
共同学习,写下你的评论
评论加载中...
作者其他优质文章