云原生应用架构:微服务开发最佳实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1.2 服务通信策略

服务之间想要通信就必须先“认识”对方,这就是所谓的服务发现。本节我们将介绍与服务发现、服务通信、基于服务网格的流量管理等相关的内容。

1.服务发现

服务发现(Service Discovery)是自动监测并发现计算机网络内设备或服务的机制,可以让调用者动态感知到网络设备的变化情况。现实世界中最大的服务发现系统可能就是DNS了,通过它,我们可以很方便地访问互联网中的海量网站。

有的读者可能会说,为什么一定需要服务发现呢?我让应用程序在启动时读取一个配置,配置里面包含所有要访问的服务的地址不就可以了吗?

没错,如果服务数量比较少,这种方案也许可行,但对于一个包含了成百上千个服务的应用而言,这种方案就行不通了。首先,新加入网络中的服务是无法被其他服务自动发现的,必须修改配置,应用也不得不重启以加载配置,而配置频繁变更会带来更高的维护成本。另外,一个大规模的服务集群,其节点动态伸缩情况、版本、可用状态等都会经常变化,若没有一个自动的机制去探测这些变化,系统几乎没办法正常运转。因此,对于微服务架构来说,服务发现的重要性不言而喻。

通常一个服务发现系统中有如下三个角色。

● 服务提供者:可以理解为一个Web服务(协议可以多样化),对外暴露一些API,有一个IP地址和端口作为服务地址。在微服务应用中可以认为服务提供者就是上游服务,即被调用的服务。

● 服务消费者:消费服务的一方,通过访问服务提供者的API来获取数据,实现功能。在微服务应用中可以认为服务消费者就是下游服务,即调用者。

● 注册中心:也叫注册表,可以理解为一个中心化的数据库,用来存储服务地址信息,是服务提供者和服务消费者之间的桥梁。

如果一个服务既要访问上游服务,又要提供API给下游服务,那么它既是服务提供者,也是服务消费者。微服务应用中有很多服务都具有这样的特性。服务发现机制的模式主要有两种,下面我们具体介绍。

(1)客户端发现模式

在这种模式下,服务启动时会将自己的地址信息注册到注册中心,客户端访问注册中心获取要访问的服务地址,然后向服务提供者发起请求,如图2-5所示。

图2-5

客户端发现模式的一个典型实现是Netflix的Eureka,它提供了注册中心和客户端,配合Netflix的Ribbon组件一起实现服务发现和负载均衡功能。Eureka对服务地址的更新基于发布订阅模式中的拉取(pull)方式,即客户端每隔一段时间会定期到注册中心拉取最新的数据。Eureka还具有缓存及TLL机制,不同的服务获取到的数据并不一定具有强一致性,它是一种更倾向于可用性优先的设计。这也很容易理解,因为这种设计已经满足了Netflix当时的应用场景:节点与服务的关系是相对静态的,服务上线后,节点的IP地址和端口信息都相对固定,变更少,对一致性的依赖也就降低了。

在容器、Kubernetes等云原生技术兴起后,这种模式就有些跟不上时代脚步了。比如在容器弹性伸缩时,这种弱一致性的服务发现机制会导致无法及时感知到服务地址的变化,从而出现访问错误。另外,容器化很好地封装了应用,所以我们对编程语言无须太关心,但客户端发现模式会存在语言绑定的SDK,甚至要引入一些语言相关的依赖包,这让应用显得有些臃肿。

(2)服务端发现模式

相对客户端发现模式而言,服务端发现模式最主要的变化是增加了一个中心化的代理,将服务信息的拉取、负载均衡等功能集中到了一起。服务消费者发起的请求会由代理接管,代理从注册中心查询到服务提供者的地址,然后根据地址将请求发送给服务提供者,如图2-6所示。

图2-6

Kubernetes的服务发现机制就基于这种模式。每一个Kubernetes的Node节点上都会有一个名为kube-proxy的代理,它会实时检测服务和端口信息。当发生变化后,kube-proxy会在对应的Node节点处修改相应的iptables路由规则,客户端服务就可以方便地通过服务名称访问到上游服务。这个kube-proxy就是我们上面提到的代理,它同时还提供了负载均衡的功能。你可以在任意一个Kubernetes集群的kube-system命名空间中找到它。

服务端发现模式相比客户端发现模式有一定的优势。它不需要SDK,隐藏了实现细节,消除了对语言和类库的限制。

它带来的缺点是,因为引入了代理,请求会多转发一次,增加了服务响应的延迟。对于以Kubernetes为基础构建的云原生应用来说,使用默认的服务发现机制是首选策略,但如果你的应用是混合部署的,比如既有Kubernetes集群内的服务,也有集群外的服务,且它们之间还需要交互,那么就要考虑服务发现的集成方案。比如HashiCorp的Consul项目就可以与Kubernetes集成。它提供的Helm Chart可以让你方便地在集群中安装Consul,并提供了Kubernetes服务与Consul的自动同步机制。这些特性可以让应用很好地工作在跨集群或者异构的工作负载环境中,为服务间通信提供了便利。

2.服务通信

微服务架构将应用程序构建为一组服务,并部署在多个实例上,它们必须以进程间通信的方式进行交互。因此进程间通信技术在微服务架构中扮演着重要的角色,技术方案也很多。比如可以选择基于请求/响应的通信机制,如HTTP、gRPC;也可以使用异步的通信机制。消息的格式可以是基于文本的JSON、XML,也可以是基于二进制的Protocol buffers、Thrift。

(1)交互方式

关于交互方式,我们一般考虑两个维度。首先是对应关系,分为一对一和一对多两种。

● 一对一:客户端请求由一个服务响应。

● 一对多:客户端请求由多个服务响应。

另外一个要考虑的维度是响应方式,分为同步和异步两种。

● 同步:客户端发送请求,等待服务端实时响应。

● 异步:客户端发送请求后无须等待,甚至不关心返回结果(类似通知),服务端的响应可以是非实时的。

基于以上两个维度的通信机制有下面两种具体实现。

● 请求/响应:这是最常见的通信机制,客户端发送请求,服务端收到请求执行逻辑,返回结果。一个Web应用的大部分使用场景都基于这种机制。当然它也可以是异步的,比如客户端发送完请求后不需要等待,而是让服务端以回调(callback)方式将结果返回。一般服务端执行一个比较耗时的任务时会采用这种异步回调的方式。

● 发布/订阅:其实就是设计模式中的观察者模式,客户端发布一条消息,被一个或多个感兴趣的服务订阅并响应,订阅该消息的服务就是所谓的观察者。发布/订阅是最常见的异步通信机制,比如上面介绍的服务发现一般就是基于这种机制实现的。

(2)通信协议和格式

我们再来看一下在服务通信过程中关于通信协议和格式的选择。

● REST/HTTP

REST是一组架构设计的约束条件和原则,基于REST风格设计的API称为RESTful API。它以资源这个概念为核心,配合使用HTTP的方法,从而实现对资源数据的操作。比如下面的URL代表使用HTTP的GET方法来获取订单数据。

RESTful API有很多优点:简单易读,很容易设计;测试方便,可以用curl之类的命令或工具直接测试;实现容易,只要构建一个Web服务器就可以使用;HTTP应用广泛,更容易集成。

当然,它也有一些缺点,比如工作在七层,需要多次交互才能建立连接,性能稍逊一筹;只支持一对一的通信,如果想在单一请求中获取多个资源,需要通过API聚合等方式实现。

尽管如此,RESTful API依然是构建Web应用的事实标准,对内负责前后端的请求和响应,对外负责定义API接口,供第三方调用。

● RPC

RPC即远程过程调用,它的工作方式是使用一种接口定义语言(IDL)来定义接口和请求响应消息,然后渲染出对应的服务端和客户端桩(Stub)程序,客户端可以通过这个桩程序像调用本地方法一样去调用远端的服务。

RPC一般包含传输协议和序列化协议,比如gRPC使用HTTP/2协议传输Protobuf二进制数据,Thrift支持多种协议和数据格式。下面是通过Protobuf定义的一个获取用户信息的接口,客户端通过传入包含用户ID的消息体,调用GetUser接口接收返回的UserResponse。根据语言的不同,消息体可能会被渲染成对象或者结构体。从这一点来讲,RPC比HTTP封装性更好,更符合面向领域建模理念。

从上面的示例可以看到,使用RPC也很简单,并且因为具有多语言SDK的支持,也可以认为它不会受到语言的限制。另外,RPC通常优先选择二进制格式数据进行传输,虽然二进制数据不可读,但传输效率要高于HTTP/JSON这样的明文格式数据。RPC的通信机制也更丰富,不仅仅支持请求/响应这种一去一回的方式,比如gRPC还支持流(Streaming)式传输。

RPC更适合服务之间的调用,或者组织内部各个系统之间调用。对于需要暴露给外界的OpenAPI,还是要优先选择使用RESTful API。原因很简单,基于HTTP的RESTful API对调用方的技术栈是没有任何要求的,而RPC因为需要特定的IDL语言渲染出桩程序,调用方需要在自己的应用中引入桩程序,因此会产生依赖,另外,如果将IDL源文件共享给调用方,也会有安全方面的隐患。

除了上面提到的两种协议,还有基于TCP的Socket、SOAP等,这些协议都有它们特殊的使用场景,但不是构建无状态微服务应用的首选,这里就不多介绍了。

(3)使用场景

目前我们的应用程序中包括以下通信方式和使用场景,如图2-7所示。

图2-7

● OpenAPI:对外暴露的RESTful API,基于HTTP,是典型的一对一同步通信。

● UI界面:和OpenAPI类似,前端UI会通过HTTP请求来调用后端服务的接口。

● 服务间同步通信:在微服务应用内部,服务之间是通过gRPC进行调用的,这样可以获得更好的性能。

● 服务间异步通信:服务间并不总是使用直接调用的方式交互的,有些场景更适合以异步事件驱动的方式交互。比如有一个业务场景是,用户要执行一个预测任务(Forecast service),该任务完成后会给消息队列发送一个任务完成的消息,另外一个用户服务(User service)会订阅该消息,并将预测结果添加到收件箱。

● 跨系统通信:我们的服务和其他存量系统间也需要交互,一般情况下同步和异步方式都会用到。对于比较新的系统,通常还是以gRPC方式进行通信的,个别遗留系统,因为技术栈等原因,会使用RESTful接口通信,异步场景中使用回调的方式进行通信。

3.基于服务网格的流量管理

微服务应用一般会有三种请求来源:来自外界的请求(比如对外提供的API)、来自组织内部其他存量系统的访问请求,以及微服务应用内部服务之间的请求。这些流量都需要管理,比如实现动态路由、按比例切分,或者添加超时、重试这些弹性能力。一些传统的基于公共库的解决方案(如Spring Cloud)已经提供了非常完善的流量管理功能,不过相对于服务网格这样的云原生解决方案还存在一些劣势。

● 语言绑定:应用需要以类库(包)的方式引入这些流量管理功能,所使用的语言必须和框架编写语言一致,此时异构的微服务应用就无法使用统一的解决方案了。

● 耦合:公共库这种解决方案尽管在功能上是和业务逻辑解耦的,但因为引入了对类库的依赖,使用上需要增加配置,或者在应用内添加一些代码(标注),因此这不是一种透明的方案。另外,这些依赖包也会包含在应用程序的发布包里,在代码层面上其实也是耦合的。

● 运维成本:基于公共库的流量管理通常以单独的进程部署,有一定的人力成本和资源成本。

服务网格是近几年流行的一种流量管理技术。简单来说,服务网格是一个用来管理服务间通信的网络基础设施,通过在每个微服务旁边部署一个边车(Sidecar)代理来实现各种流量管理功能。相比起公共库这样的方案,它具有比较明显的优势。

● 对应用透明:这是服务网格受到青睐的主要原因,也是它最大的特点。服务网格基本的工作原理是通过边车代理来转发请求,这就使得边车有能力对请求做出相应的操作,比如根据请求头将不同的请求转发到不同的服务。这有点像通信框架里的拦截器。而这些流量管理的能力都是由边车代理提供的,应用不需要修改代码或添加配置就能获取这些能力,这使得服务网格可以被透明接入应用,且不受开发语言和技术栈的限制。

● 以云原生的方式使用:服务网格完全遵循云原生的理念,将网络相关的非功能性需求下沉到基础设施层,与应用解耦。在使用层面上,服务网格也是通过声明式配置这样的云原生方式让应用获取流量管理能力的。

当然,服务网格也并非没有缺点,比较让人担心的一个问题就是延迟。因为边车代理的引入,原本服务到服务的直接调用变成了三次调用:服务A到边车代理A,再从边车代理A到边车代理B,最后从边车代理B到服务B。这必然会增加一定的延迟。也因为转发次数的增多,调试难度相应增加,需要借助分布式追踪等特性来辅助调试。

经过4年左右的发展,服务网格技术也逐渐成熟,成了微服务应用在流量管理方面重要的技术选型方案,越来越多的团队已经实现了生产环境下的落地实践。不过新技术的引入必然会带来一定的成本和风险,应结合自己团队的现状和业务特性分析它的可行性。笔者对于是否使用服务网格有以下几点建议。

● 流量管理等方面的需求:如果你目前的应用不具有流量管理这方面的能力,而你又有迫切的需求,可以在技术选型时将服务网格作为重要的方案进行评估。

● 异构微服务应用流量管理能力的统一:如果你的微服务应用是一个异构的、使用不同语言开发的应用,无法使用单一的框架或类库,同时你又想使用统一的方案去实现流量管理,那么服务网格是一个不错的选择。

● 现有方案的痛点:如果你的应用架构目前已经具有了流量管理能力,但却存在着不少痛点,比如不同技术栈的应用使用不同的实现方式且无法统一、框架升级和维护成本高、SDK的更新导致业务服务也需要更新等,而这些痛点给你带来了长期的困扰并大幅降低了开发效率,此时可以考虑借助服务网格解决这些痛点。

● 遗留系统的技术栈升级:如果你有一个老旧的系统,假设它是一个单体应用,庞大且难以维护,你正打算将它改造成微服务应用。这种情况下就非常适合引入服务网格。一方面,基于新架构去接入服务网格不需要考虑老旧系统的兼容问题,实现成本较低;另一方面,应用改造本来就要花费较高的成本,引入服务网格带来的额外成本就显得不值一提了。

● 云原生应用的演进:如果你的团队热衷于技术创新,想要打造云原生应用,那么落地服务网格将是一个重要的演进过程。

尽管服务网格有很多优势,但笔者依然建议要基于自身情况具体分析,同时还要考虑接入和后续的维护成本。本书的第4章会详细介绍我们团队使用服务网格技术来管理微服务应用的落地实践。