大佬用9000字直接带你搞定微服务的核心组件!

微服务的核心组件

微服务的架构设计之前总结过,微服务的思想是分离,微服务模式下将应用程序拆分为不同的微小服务,通过使用或者组合不同的服务来完成不同的业务功能。那么一旦分离后再组合,就意味着服务之间一定会存在相互调用的过程,在前面微服务的定义中提到过,微服务之间都使用粗糙的通信机制,它一定是轻量级的,而且是可以支持跨语言调用的,包括微服务本身对客户端提供服务也是采用这种机制的。因此,设计并实现适合的通信组件来提供远程调用的能力是微服务架构的核心。下面来了解微服务的远程调用方式。

微服务的远程调用方式

微服务架构中常用的调用方式有两种:一种是通过异步的消息交换来通信,这里服务调用双方一般使用一些中间件,如RabbitMQ、Kafka等;另一种是通过HTTP的资源接口,通过JSON作为信息载体的格式,即REST API的方式进行远程通信。

1. 异步消息通信

先来看一下异步消息是如何通信的,这里以RabbitMQ的工作原理为例,如图2.6所示。

首先消息由生产者发送到一个统一的交换中心(又称交易所),交换中心根据一定的转发规则(如直接、主题匹配、扇出等方式),将消息转发到对应的队列中,然后通过消息队列最终将消息传递给消费者。消费者可以选择只订阅自己关心的消息。

可以利用消息机制来做很多事情,如可以做一个简单的任务队列,也可以去订阅和发布消息。当然,订阅和发布的方式有很多,最常见的就是图2.7RabbitMQ远程调用工作原理中描述的定向转发和Topic机制,这里对消息机制的原理不做阐述,利用这种发布和订阅的工作方式,我们可以通过消息做到服务的远程调用。

但是,在微服务中这并不是常见的用法,因为本身消息机制的实现会依赖相关的中间件或框架,服务调用双方都需要集成相同的消息服务或技术框架,这本身就会加重微服务架构的通信方式,而且过度松散异步的操作也为代码带来了一定的复杂度。所以,在微服务中最常见的通信还是基于HTTP的REST API。

2. REST API

REST(Representational State Transfer,表述性状态转移)用来描述创建HTTP API的标准方法,REST API的核心概念是资源,对于资源有4种常见的行为:查看、创建、编辑和删除,都可以直接映射到HTTP中已实现的GET、POST、PUT和DELETE方法。REST本身并没有创造新的技术、组件或服务,只是正确地使用Web的现有特征和能力,更好地使用现有Web标准中的一些准则和约束。

如果一个架构符合REST的约束条件和原则,就称它为RESTful架构,当然理论上REST架构风格并不是绑定在HTTP上的,只不过目前HTTP是唯一与REST相关的实现,所以一般情况下REST API都是表示基于HTTP的RESTful接口。

既然是一种标准、一种架构风格,就必然会有它的相关概念和设计原则。表2.1所示为REST API中的一些重要术语。

同时,REST API也有很多设计原则,如URL中永远不能包含动词,那么URL如何表示自己的行为呢?前面提到过4种常见的行为(查看、创建、编辑和删除)映射到HTTP中已实现的方法中,具体如下。

(1)GET(查看):从服务器或资源列表中检索特定资源。

(2)POST(创建):在服务器上创建一个新资源。

(3)PUT(编辑):更新服务器上的资源,提供整个资源。

(4)PATCH(编辑):更新服务器上的资源,仅提供已更改的属性。

(5)DELETE(删除):从服务器中删除资源。

下面两个不是很常用。

(1)HEAD(查看):检索有关资源的元数据,如数据的哈希值或上次更新的时间。

(2)OPTIONS(查看):检索有关允许消费者使用资源的信息。客户端和服务端的交互是无状态的,GET请求通常是可以被缓存的,资源使用复数,URL中可以有表述版本的信息,举例如下。

按照顺序对应的含义如下。

(1)使用2.0版本的接口,创建一个产品。

(2)使用2.0版本的接口,根据ID查看产品信息。

(3)使用1.0版本的接口,根据ID全量更新产品信息。

(4)使用1.0版本的接口,根据ID部分更新产品信息。

(5)使用1.0版本的接口,根据ID删除产品。

RESTful的接口还有很多设计原则,这里不再赘述,感兴趣的读者可以查阅相关资料进行学习,在微服务中使用的HTTP通信协议就是采用的RESTful的接口设计,所以熟练掌握REST API的设计用法在微服务架构中是十分重要的。

HTTP通信方法

一般在项目中采用什么技术来完成HTTP通信?在一些早期的项目中,可以看到Apache HttpComponents的身影,它的功能也十分强大,但是在使用时,需要编写大量的基础代码,往往还需要进行二次封装,而在如今Spring Boot盛行的时代,大家更热衷于现取现用,正所谓约定大于配置,所有的基础工作都按照一定的约定交由框架来完成。下面以Spring为例,主要介绍RestTemplate和WebClient两种方式进行HTTP的通信。

1. RestTemplate

RestTemplate是Spring Web中提供的用于在客户端完成同步的HTTP请求的核心类,大大简化了与HTTP服务器的通信,并实现了RESTful的设计原则。RestTemplate默认采用JDK原生的HTTP连接工具实现,当然也可以切换库,如Apache HttpComponents、Netty和OKHttp等第三方的HTTP库。我们以默认的实现为例,首先需要声明一个RestTemplate,这里采用Spring Boot的注解式声明方式,代码如下。

当然,还可以给它初始化一些公共属性,如URL、用户名密码,或者一些连接超时等设置,代码如下。

完 成 RestTemplate 的 声 明 之 后 就 可 以 使 用 它 了 , 之 前 说 过RestTemplate实现了RESTful的设计原则,所以RestTemplate提供了便捷的方法去实现HTTP的GET、POST、PUT、PATCH和DELETE方法。例如,getForEntity()就是以GET的方式发送HTTP请求,而postForEntity()则 是 以 POST 的 方 式 发 送 HTTP 请 求 。 当 然 , 还 有 其 他 实 现 , 如patchForObject()、put()、delete()和optionsForAllow()等方法,这里就不一一介绍了。下面以GET和POST两种最常见的方式来介绍RestTemplate的用法,其他的大同小异。

(1)RestTemplate的GET方法。

RestTemplate提供了getForObject和getForEntity两种方式发送GET的HTTP请求,其中getForObject方法可以直接将响应的Body转换为指定的类型,方法定义如下。

其中,第一个方法比较常用,按照顺序传递URL的参数,通过在URL中定义{}来表示参数的站位,{}中可以写具体含义的单词,也可以写数字,如{0},代码如下。

此外,getForObject还提供了另外两种重载方法,分别提供了通过Map传递参数和没有参数两种功能。没有参数很好理解,不再赘述,下面的代码展示了通过Map传递参数的方式。

不难看出,Map中的key对应着URL中{}里的单词,使用Map的不足之处是当参数太多时,顺序容易弄错,而且方法会写得很长,不易读,也不易维护。

getForObject方法能满足我们大部分的需求,但有时可能需要获取除Body之外的信息,如响应头、响应状态码等,这时就需要getForEntity了,getForEntity和getForObject一样,提供了3种实现,方法定义如下。

可以发现,getForEntity的方法参数和getForObject的一样,唯一的区别是getForEntity的返回类型是ResponseEntity。这里不再对每个方法进行详细介绍了,下面还是以第一个方法为例,代码如下。

(2)RestTemplate的POST方法。

与GET的方式一样,POST也提供了postForObject和postForEntity两种方式来完成POST的HTTP请求。方法定义如下。

POST的6个方法定义与GET几乎一样,所以这里没有把方法说明粘贴进来,仔细观察可以发现,POST的方法多了一个request的参数,这个参数会被放进请求的Body中,当没有需要时也可以传入null,举例如下。

关于RestTemplate的其他方法就不再列举了,感兴趣的读者可以查看Spring Web库的源码,或者访问Spring的官网查阅相关教程。

2. WebClient

WebClient相比RestTemplate是一个较新的HTTP访问方式,之前提到过,RestTemplate是一个同步的请求方式,当请求发出后,当前线程会等待,直到有响应后才会继续执行后续代码。其实远程调用是一个可以异步的过程,在等待请求响应时,我们完全可以做其他的事情,所以RestTemplate在一些性能要求比较高的地方使用就显得不是那么合适了。

这时就需要使用可以异步完成请求的WebClient了,当然,我们可以仍然使用RestTemplate,然后通过线程池或CompletableFuture等方式创建新的线程来执行RestTemplate的请求而不阻塞当前线程的执行。不过这样做不是特别优雅,而且每次还需要自行维护关于创建不同线程的代码。

随着JavaScript的Reactive设计理念越来越流行,不少语言和框架开始相继模仿。Spring也采用了“No blocking”(无阻塞)的方式,推出了Spring WebFlux,关于WebFlux的功能有很多,这里主要来看一下WebFlux中WebClient的用法。

相比RestTemplate,WebClient最大的优势就是可以使用Reactive的方式执行非阻塞的HTTP请求,即异步的请求服务端。WebClient同样实现了RESTful的设计原则,支持GET、POST、PUT、PATCH和DELETE等操作,而且写法更接近于流式,示例代码如下。

这是一个GET方法,代码很好理解,只不过是流式的写法,先定义HTTP的方法,然后定义URI,最后定义返回类型。再来看一个POST的例子,代码如下。

很 显 然 , 相 比 GET 方 法 多 了 一 个 syncBody 方 法 , 类 似 于RestTemplate的request参数,这个方法会把该参数当作请求的Body发送到服务端。仔细观察可以发现,与RestTemplate相比有一个最大的不同,就是方法的返回值变成了Mono类型,其实除了Mono类型,WebFlux还提供了Flux类型,代码如下。

首先通过代码可以发现,Mono和Flux的区别在于前者是单个元素,后者是集合,当然这不是最关键的,Mono中也可以是一个集合,如Mono<List<User>>、Mono和Flux之间可以相互转换。关键在于WebClient是异步的请求,在调用它时得到的返回类型不可能是直接的期待返回的类型,如findAllUsers得到的不是List<User>类型,而是Flux<User>类型。

这就好比我们通过CompletableFuture创建了一个线程A,然后在执行时会立刻返回一个CompletableFuture类型的对象,这时主线程就不会阻塞,而是继续执行。当我们需要得到返回值时,可以通过CompletableFuture的join方法,将线程A加入当前线程,这时如果线程A已经执行完成,那么当前线程就会立刻得到线程A的执行结果,如果线程A还没有执行完,那么当前线程就会等待,直到线程A执行完成。这是一个典型的异步执行方法的例子,而WebClient的Mono和Flux就和CompletableFuture具有相同的作用,而且更加强大。

例如,可以提前定义对返回数据的操作,代码如下。

再如,可以打包合并多个Mono或Flux,然后只执行一次join,代码如下。

可以看到,Mono提供了类似join的方法:block。如果你更喜欢使用 CompletableFuture , Mono 和 Flux 也 可 以 轻 松 地 转 换 成CompletableFuture,代码如下。

服务的注册与发现

解决了服务的远程调用问题之后,是不是就足够了呢?答案肯定是不够的。言归正传,设想一下,在微服务架构中会有多个服务进行交互,假设我们使用的是HTTP的通信方式,那么系统结构应该为如图2.8所示的网状调用结构,多个微服务直接相互调用。

HTTP的交互方式显然比消息队列简单得多,多个微服务相互调用也变得简单而直接。但不管是哪种方式,随着服务越来越多,微服务的调用网也将越来越大、越来越复杂。你会发现需要管理的服务信息越来越多,这些信息可能包括服务的IP、端口和URL等数据,而且这些信息需要在每个服务的消费方进行维护。

在图2.8中,服务A需要维护服务B和服务E的信息,服务B则需要维护服务A、服务D和服务E的消息,一旦某一个服务的地址或端口发生变化,所有调用它的消费方都需要进行相应的配置修改。笔者曾经做过拥有一百多个服务组成的产品级项目,当时的架构设计服务之间就是直接相互调用,所有的服务信息都是服务调用方在自己的服务端的配置文件中进行配置。

当然,一个服务不可能与一百多个服务都进行交互,但可能会与十几个服务进行通信,当服务信息发生变化时,需要非常熟悉系统的人花费接近一周的时间进行人工测试和排查,然后去修改被影响的服务的配置,才能保证服务更新后系统的正常运行,而且就算是再熟悉系统的人也可能会有遗漏。

那么如何才能解决这个问题呢?答案非常简单,既然人工检测和更新效率低下且容易出错,那就改用自动即可。回忆一下1.2.3节中介绍过的SOA的服务调用设计,微服务架构中沿用了服务注册这一设计,提供服务注册的服务通常称为注册中心,如图2.9所示。

服务注册中心的设计有效地解决了这个问题,服务提供者可以理解为微服务中的一个服务,服务消费者可以是其他的微服务,也可以是BFF(Backend For Frontend,用于前端的后端)、API网关等服务。关于BFF和API网关将在后续章节陆续介绍。

当服务提供者需要对外提供服务时,会主动向服务注册中心注册,注册中心会保存各个服务提供者的信息,并且通过心跳等机制定期检查服务提供者的健康情况,一旦检测到服务不可用,就要根据一定的规则从注册中心剔除该服务的信息。

服务消费者只需配置注册中心的信息,然后通过唯一标识(如服务提供者的应用名称),就可以查询到服务提供者的信息,也包括服务提供者的健康状态。当服务提供者的信息发生变化时,如修改了一些服务实例的端口号,只要服务提供者在注册时所使用的唯一标识不变,服务消费者是无须修改任何代码或配置的,注册中心会将最新的、可用的服务提供者的信息返回服务消费者。

那么,具体的技术实现有哪些?目前作为注册中心,比较主流的可能是通过ZooKeeper、Consul和Eureka这样的服务框架来完成,当然也完全可以自己设计一个,笔者曾经用Redis写过一个服务注册中心。

要实现注册中心并不难,难的是需要开发全套的、完整的服务治理框架,Spring在这一点上有着天然的优势。因此,我们还是以SpringCloud为例,Spring在众多框架中选择Netflix的Eureka作为默认的基础服务注册框架,当然Spring也分别实现了基于ZooKeeper和Consul的注册中心,同时Spring也在开发一套原生的注册中心,以摆脱第三方的框架,但是无论是从成熟度还是从运用的广度来看,Spring CloudNetflix Eureka还是目前的首选。

下面来看看Spring Cloud NetflixEureka的具体实践。Spring Cloud Netflix Eureka主要分为Server和Client两个概念,注册中心就是Server,其他的服务注册者和服务调用者都是Client。当然,如果你部署的是一个注册中心的集群,那么注册中心之间也会以Client的身份相互注册。

下面先来看如何配置Eureka的Server,首先需要引入相关的包,其中最主要的是spring
cloud-starter-eureka-server,如果你使用的是Gradle,那么可以按照如下代码配置。

笔者比较喜欢从Spring的官网上自动生成干净的新项目,然后稍作 修 改 即 可 投 入 使 用 , 推 荐 大 家 一 个 网 址 :

https://start.spring.io/,该网站可以自由选择Spring的组件进行组合,然后生成项目初始代码,如图2.10所示的Spring应用初始化工具界面。在完成了项目的基本配置后,只需简单的几个步骤就可以启动注册 中 心 了 , 首 先 需 要 在 Spring Boot 对 应 的 启 动 类 上 增 加@EnableEurekaServer注解,代码如下。

然后,需要在application.yml中增加一些Eureka Server的配置,此处只做一些简单的配置,详细配置可以参考Spring的官方教程,代码如下。

当然,除了域名也可以使用ip-address来配置注册中心的IP地址,笔者这里配置了计算机的HOSTS,编辑/etc/hosts文件,内容如下。

直接使用hostname配置的注册中心,完成这项基本配置后,就可以直接启动服务了,服务启动后会提供一个监控页面,访问地址为http://ms-registry1:8080,监控页面如图2.11所示。

该页面会显示Eureka Client的注册信息及它们的健康情况,现在只有一个Eureka Server在运行,可以看到在图2.11的最下面,当前在Eureka注册的实例显示的是无可用的实例,在DS Replicas (副本)中显示的是localhost,即没有副本,只有本地一个实例。

我们可以尝试配置一个简单的集群,来看看这个监控页面的变化,首先需要再启动一个Eureka Server的实例。假设我们配置的新的Eureka Server 实 例 的 应 用 名 称 是 ms-register2 , 域 名 是 msregistry2,application.yml中的配置如下。

那么,回到ms-registry1,之前其他的配置都不变,唯一需要修改的是application.yml中的配置,代码如下。

我们增加了client下service-url的配置,正如之前所说的,这里将ms-registry1当作ms registry2的Client,这时再次访问http://ms-registry1:8080,来看看现在的变化,监控页面如图2.12所示。

这时DS Replicas显示的不再是localhost,而是ms-registry2,这表示ms-registry1和ms registry2已经建立同步副本的关系。访问一下ms-registry2的监控页面,监控页面如图2.13所示。

这说明ms-registry1已经成功注册到了ms-registry2中,通常我们会将多个Eureka Server相互注册,构成一个统一的集群,以达到高可用的目的。

配置完Server,如何配置Client呢?无论是服务提供者还是服务消费者,对于Eureka Server来说都是Client,这里需要引入spring
cloud-starter-netflix-eureka-client的库,新建一个工程,项目可以通过https://start.spring.io/进行初始化,build.gradle代码如下。

application.yml的配置可以参考上文配置Server集群时的client下service-url的配置,毕竟Server相互之间就是Server与Client的关系,如果Server之间都相互进行注册,那么service url只配置一个实例的地址即可,Eureka Server之间可以进行信息复制,具体代码如下。

如果不配置服务的端口,Spring默认为8080,一般我们会设置服务的端口为0,那么Spring就会生成一个随机数作为端口号,如果端口占用就会重新生成一个,这样也省去了我们去关心端口的配置,服务消费者只需通过应用名称(如ms-provider),就可以对该服务进行调用,如果使用的是RestTemplate,那么调用的URL可以写成如下代码。

Client的配置还有一点不同,就是在SpringBoot的启动类上不再使用@EnableEurekaServer注解,而是使用@EnableDiscoveryClient或@EnableEurekaClient注解,代码如下。

当 然 , 有 时 我 们 也 使 用 @EnableEurekaClient , 那 么 它 和@EnableDiscoveryClient有什么区别呢?从两者的所属库就可以看出,@EnableEurekaClient是Netflix Eureka Client中的实现,它只支持Eureka作为注册中心,如果你使用Eureka,那么可以使用@EnableEurekaClient 。 @EnableDiscoveryClient 是 Spring CloudCommon中的实现,使用时Spring会根据你的classpath中的依赖来判断目前使用的是Eureka还是ZooKeeper,或者是Consul,然后动态地去初始化注册服务的配置。

启动后,再次访问注册中心页面,监控页面如图2.14所示。

在当前注册的实例列表中,除本身两个相互注册的注册中心msregistry1和ms-registry2,ms-provider也出现在列表中,此时表明服务已经注册成功。

负载均衡

如果服务部署了多个实例?这时又如何处理?

首先可能想到的是反向代理,提到负载均衡,很多人都想到Nginx,确实在以往的单应用模式下的系统中,最常见的做法就是通过Nginx来完成负载均衡策略,如随机、轮询和权重等。例如,我们一般会部署多个Tomcat实例作为多个Web服务器,然后通过Nginx的反向代理来分发客户端的请求到不同的Web服务器,而Nginx可以定义不同的规则来分发这些请求,以达到负责均衡的目的。反向代理好比是集中式的统一入口,所有的服务信息都需要被描述在入口处,然后由入口来决定请求的去向,也就是负载的策略。

当然,也有通过一些中间件的方式来达到负载均衡,如SOA中的消息总线模式,通过中间的消息层来转发请求,从而达到负载均衡的目的。

那么,在微服务中又是如何设计的呢?微服务强调分离,提倡职责单一,虽然引入了注册中心的概念,但是注册中心除了负责对服务信息的统一微服,还包括对服务的健康状况等信息进行监控,而真正去完成负载均衡任务的是服务的消费者,这样做也比较符合逻辑,消费者自行决定需要采用哪些策略去调用服务提供者。

不过,消费者在做决定时,可以通过注册中心的消息来帮助自己做决定。例如,注册中心告诉消费者服务A的B实例是不可用状态,那么消费者在请求服务A时,则会把B实例剔除,如果注册中心告诉消费者B实例已经恢复为可用状态,那么消费者会重新把B实例加入自己可调用的目标中。

下面以Spring Cloud为例,一起来看看在Java的微服务项目中远程调用的具体技术实践。

Spring Cloud的设计是让服务之间采用HTTP的方式进行远程调用,所以Spring Web框架本身就提供RestTemplate、WebClient等HTTP通信的实现。而Spring Cloud Ribbon是客户端的负载均衡器,还有Spring Cloud Feign,Feign是对Ribbon的封装,提供了一些便捷的高级功能。如果不考虑使用客户端的负载均衡,就可以完全不集成该组件。在大多数情况下,通过软方法在客户端使用负载均衡也是一个不错的选择,下面介绍Spring Cloud Ribbon和Spring Cloud Feign的用法。

新建一个工程,build.gradle配置如下。

然后需要使用和provider相同的方式,将项目实例加入注册中心中,application.yml的配置如下。

1. Spring Cloud Ribbon

Spring Cloud Ribbon的用法十分简单,首先是要引入spring
cloud-starter-netflix-ribbon的库,下面以使用RestTemplate为例,需要在初始化RestTemplate时加上@LoadBalanced注解,代码如下。

如果使用了WebFlux,那么WebClient的用法如下。

然后调用时需要build()一下,如调用一个程序用户的接口,其代码如下。

当你在使用RestTemplate请求其他服务时,就会默认使用Ribbon的负载均衡策略进行请求,默认的是轮询,即会对所有可用的服务实例进行轮询访问。可以编写测试程序来测试代码是否生效。

当然,Ribbon为我们提供了7种负载均衡策略,如表2.2所示。

然后,可以通过配置application.yml文件来配置我们想要的策略。例如,想将负载均衡策略改成随机选择RandomRule,那么配置如下。

配置完成后,还需要配置对应IRule类型的Bean去覆盖原有的Rule实例,代码如下。

2. Spring Cloud Feign

Feign其实是对Ribbon的一个高级的封装,负载策略与Ribbon一致。首先,Feign在Ribbon的基础上提供了更加简便的服务调用方式,可以像调用本地方法一样调用远程服务;其次,Feign还集成了断路器:Spring Cloud Netflix Hystrix。这里先来介绍Spring CloudFeign是如何优化远程调用方式的。

首先,需要引入
spring-cloud-starter-openfeign的库,然后在Spring Boot的启动类上加上@EnableFeignClients注解,代码如下。

然后,需要定义一个接口,并加上@FeignClient注解的value指定具体要调用的服务提供者的应用名称,接口的方法必须与服务提供者的Controller中的方法一样,代码如下。

服务消费者在需要调用该服务时,直接通过Spring的依赖注入的方式,即可自动注入FeignClient,然后调用响应的方法,代码如下。

可以编写测试来测试代码是否生效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值