微服务核心模块
这是微服务的基本架构图,不同终端可以通过网关调用我们的核心服务,每个服务可以独立水平扩展,它们各自管辖自己的数据库。下面是SpringCloud相关常见技术栈(模块),我们将通过一个简化后的真实案例来串联起它们:
Eureka/Nacos:服务注册中心,后者由阿里巴巴开源
Ribbon:负载均衡组件
Hystrix:熔断器组件
Feign:请求客户端组件
SpringCloud GateWay:网关组件,提供路由、过滤等功能
1. 准备工作
下面我们通过一个案例来整体介绍这些组件。案例背景:B2C商城里,用户在购物时会生成订单,除了支付业务本身的订单状态处理之外,系统还会围绕这些订单分别给商家、用户端做些处理。最典型的比如,商家端要做订单统计、用户端要做订单查询、积分计算等等。为了将不同端的订单处理分层解耦,通常会划分多个服务,最简单的方案是分为商家服务和用户服务,商家服务管理商家订单、用户服务管理用户订单。
当用户下单后,前端通过调用平台聚合层来分别调用商家、用户服务。
所以,我们可以新建三个服务项目,PlatformDemo、MerchantDemo、UserDemo。按照微服务的理论,每个服务管控自己的数据库,所以可以新建两个单独的库,分别是merchantdb、userdb,然后分别新建各自的订单表merchantorder、userorder。(其实就是垂直分库)
编译相关命令
clean compile package -Dmaven.test.skip=true
PlatformDemo怎么调用MerchantDemo和UserDemo呢?两种方式:
- platform直接通过http调用merchant和user,优点是:简单,缺点是:假如merchant和user是多实例的,那么platform需要手动维护每个实例的地址;
- 将merchant、user注册到一个服务注册中心,然后platform仅通过单一的【服务名称】来路由到不同的merchnt、user服务实例。优点是:服务实例水平扩展很方便,不需要在platform维护实例地址。缺点是:要安装单独的服务注册中心。
在实际场景中,肯定会选2,原因就在于,微服务的意义就是让服务实例更方便的水平扩展,假如每次还得在调用层手动维护实例地址,会非常麻烦。另外,注册中心只需要安装一次,也不存在其他太复杂的操作。
2. Nacos基本介绍
微服务比较常见的注册中心有Eureka、ZK、Consul、Nacos等。Nacos由阿里巴巴开源,它提供了服务注册、配置管理等功能。其简单易用的风格,越来越受到大家的关注,我们的生产级项目都已采用,目前运行良好。
实际上Nacos思路非常简单,它提供中心服务器(可集群扩展,消除单点)及控制台,服务提供者(比如Merchant服务)首先主动注册到中心服务,中心服务轮询其存活状态。服务消费者(比如Platform)根据固定的服务名从中心服务器调用目标服务。这种架构的优点是:服务提供者的水平扩展可以对服务消费者完全透明,后者不需要手动维护前者服务列表。
下面我们以Nacos为例,来对注册中心做个演示。
Nacos服务安装
安装过程可以看这里:
https://nacos.io/zh-cn/docs/quick-start.html我这里是按照源码方式安装,相关nacos命令在 distribution/target/nacos-server-$version/nacos/bin目录下。
启动命令:
sh startup.sh -m standalone
关停命令:
sh shutdown.sh
控制台页面:
http://localhost:8848/nacos/ 默认密码:nacos/nacos
使用Nacos进行服务注册
首先引入nacos依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>>0.2.1.RELEASE</version>
</dependency>
这里我们采用生产验证过的0.2.1版本
代码及配置方面的变动:
在主类上加上@EnableDiscoveryClient注解
在配置文件中新增如下内容:
server.port=0
spring.application.name=merchant-service
spring.cloud.nacos.discovery.server-addr=localhost:8848
这里将port设置为0,意味着每次启动都会使用随机端口号,这主要是因为同一类的微服务实例通常会有多个,使用同样的固定端口会造成端口占用的问题。
Nacos控制台初探
启动主类后,即可在控制台的【服务列表】中看到merchant-service 服务:
这里我们启动了3个实例,点击详情后,我们可以看到实例的权重及运行情况:
在这里,我们可以直接编辑实例的权重,也可以直接上下线实例,后面我们会对此进行演示。
借助Nacos进行微服务调用
如之前所说,我们需要在Platform中调用Merchant服务,完成订单入库的操作。由于Merchant已经注册在了Nacos,所以Platform必须借助Nacos来完成服务的调用。
Platform项目的配置和前面类似,这里不再赘述,我们直接看怎么轮询调用Merchant服务。为了更清楚的演示轮询过程,我们直接采用LoadBalancerClient+RestTemplate的方案手动调用服务。LoadBalancerClient用于通过服务名选取服务信息(ip地址、端口号),RestTemplate用于做Http请求。
下面首先配置RestTemplate:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(3000);//单位为ms
factory.setConnectTimeout(3000);//单位为ms
return factory;
}
}
然后新建测试类,核心测试代码如下:
ServiceInstance serviceInstance = loadBalancerClient.choose("merchant-service");
String url = String.format("http://%s:%s/merchant/saveOrder",serviceInstance.getHost(),serviceInstance.getPort());
System.out.println("request url:"+url);
Object value=restTemplate.postForObject(url,null,String.class);
代码解释:首先通过ServiceInstance根据权重获取服务信息,该信息包括ip+端口,然后拼接服务地址信息,最后通过RestTemplate进行Http请求。
注意:在test之前,先启动多个merchant服务实例。大家不妨测试一下,假如请求多次,是能看到均衡负载的效果的。
上面这种方式比较手工一点,实际上,我们可以直接让RestTemplate集成Ribbon,实现LoadBalance的效果,做法很简单:
- 在构建RestTemplate时加上@LoadBalanced注解:
@LoadBalanced
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}
- 请求服务时,直接使用服务名而非IP+端口:
restTemplate.postForObject("http://merchant-service/merchant/saveOrder",null,String.class);
3. 微服务调用之Feign
Feign是SpringCloud中非常常用的一个HTTP客户端组件,它提供了接口式的微服务调用API。
首先确保项目中已经导入了Feign依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
然后创建目标服务的接口,比如我们这里需要调用Merchant服务,那么可以新建MerchantService接口专门来处理与之相关的服务调用:
@FeignClient(value="merchant-service")
public interface MerchantService {
@PostMapping("/merchant/saveOrder")
public String saveMerchantOrder();
}
这个接口非常容易理解:使用@FeignClient将接口定义为服务接口,使用SpringMVC的@PostMapping、@GetMapping注解将接口方法定义为服务映射方法。就这样,调用微服务的方式就和普通方法调用的方式没太大区别(至少感觉上是这样)。
有时候,我们需要在发起Feign请求时,可以做一些统一的处理,比如:header设置、请求监控等。此时我们可以配置Feign拦截器来实现。
Feign拦截器的实现方式非常简单,主要分为两步:
- 实现feign.RequestInterceptor接口,重写其apply方法,如下:
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("token","123");
}
}
- 将其配置在@FeignClient(configuration)中:
@FeignClient(value="merchant-service",configuration = FeignRequestInterceptor.class)
public interface MerchantService {
@PostMapping("/merchant/saveOrder")
public String saveMerchantOrder();
}
此时我们可以先调整下Merchant服务的接口,使用@RequestHeader("token")来接收token参数。
Feign超时及重试机制
微服务之间调用最大的一个问题就是超时问题(没有之一)。比如说,当Platform调用Merchant时,由于网络不通或者Merchant服务响应缓慢,那么Platform是不能一直等待下去的,这样资源会一致被占用,前端也得不到快速响应。此时一般会设置超时时间。
大家可以测试一下,当连接不上服务端时,会报connect timeout,当服务端响应时间过长,会报read timeout。默认情况下,Feign是不会重试的,即重试逻辑为Retryer.NEVER_RETRY。我们可以根据实际情况作如下配置:
@Configuration
public class FeignConfigure {
@Bean
Request.Options feignOptions() {
return new Request.Options(
/**connectTimeoutMillis**/
1 * 1000,
/** readTimeoutMillis **/
1 * 5000);
}
@Bean
public Retryer feignRetryer() {
return new Retryer.Default();
}
}
该配置类里面,我们设置了连接超时未1秒、读取超时未5秒,然后默认重试机制会重试5次。测试方式比较简单,比如我们可以把Merchant服务从Nacos上摘除下来,或者在接口中手动设置sleep,这里不再给出。
调用方在收到超时异常时,很可能服务方会继续执行(比如执行过长导致de 超时),所以重试的前提是:【一定要保证服务方的幂等性】,即重复多次不会影响业务逻辑。
4. 微服务间的数据传输
在实际开发中,有一个很现实的问题是数据传输格式的约定问题。在微服务架构中,实现一个完整的功能需要涉及到多个服务的调用,每个服务都有与自己领域相关的数据封装,微服务之间的调用需要遵循对方的数据格式要求。以前面的订单为例,Platform在调用Merchant时,应该传入商户订单对象,然后被返回Merchant服务的响应对象。听起来很简单对吧?但是Platform和Merchant是不同项目,后者约定好的对象类在前者是不存在的,前者工程师需要手动新建匹配的类才行。在服务接口非常繁多的情况下,这种手工处理会占用工程师很多时间。为了让他们过的爽一点,我们应该让这些类/对象共享才对。
所以,笔者建议针对每个服务都新建一个DTO项目,专门用于定义数据传输对象。比如我们可以新建MerchantDTO,专门定义该服务对应的输入、输出对象,每次更新升级时,可以将其打入公司的私有仓库中。为了自动化这一过程,可以使用CI/CD工具(比如jenkins)自动拉取git代码并install/deploy到私有仓库。在需要调用Merchant服务时,在pom中加入依赖就可以了。在项目规模较小时,可以暂时只做一个DTO项目,涵盖所有服务,以后再拆也是OK的。
在DTO中,我们会约定两种类型的数据:请求参数值、响应返回值。请求参数与业务域相关。比如保存商户订单,那么请求参数就是商户订单数据,比如:
@ApiModel("商户订单实体")
@Setter
@Getter
public class MerchantOrderRequest {
@ApiModelProperty(name = "ordername",value = "订单名称")
private String ordername;
@ApiModelProperty(name="price",value = "价格")
private double price;
}
通常来说,响应返回值都会有些公共的字段,比如code、message等,一般来说会设计响应对象的基类,这样便于后面做统一的code处理:
@ApiModel(value = "默认响应实体")
@Setter
@Getter
public class DefaultResponseData {
@ApiModelProperty(name = "code",value = "返回码,默认1000是成功、5000是失败")
private String code;
@ApiModelProperty(name = "message",value = "返回信息")
private String message;
/**
* 额外数据
*/
@ApiModelProperty(name = "extra",value = "额外数据")
private String extra;
}
这里用到了swagger注解,这样在接口文档中就会有明确说明,方便调试。@Setter、@Getter主要用于生产Setter/Getter代码,有助于解放大家的双手,具体安装及依赖过程可以看这篇文章:
如何使用Lombok简化你的代码?我们改造一下之前的saveMerchantOrder方法,让其传入MerchantOrderRequest、返回DefaultResponseData。
public DefaultResponseData saveMerchantOrder(
@RequestBody MerchantOrderRequest merchantOrderRequest,
@RequestHeader("token") String token){
...
}
重新启动Merchant服务,打开swagger,可以看到请求和响应参数的描述:
User服务可以完全按照同样的处理策略,这里不再赘述。
5. 使用Hystrix进行熔断保护(降级)
在分布式/微服务环境中往往会出现各种各样的问题,比如网络异常,超时等,而这些问题可能会导致系统的级联失败,即使不断重试,也可能无法解决的,还会耗费更多的资源。比如说我们Platform在调用Merchant时,后者的数据库突然挂了,然后系统卡顿或者不停报错,用户此时可能会不断刷新,做更多的请求。这最终会让应用程序由于资源耗尽而导致雪崩。遇到这种情况,更好的做法是在调用阶段进行熔断保护并做降级处理。
熔断保护类似于电路中的保险丝,当电流异常升高时会自动切断电流,以保护电路安全。在开发中,熔断器通常有三个状态,即Closed、Open、Half-Open,如下图:
默认情况下,熔断器是关闭(Closed)的,一旦在某个时间窗口T(默认10秒)内发生异常或者超时的次数在N以上,那么熔断器就会开启(Open),然后在时间窗口S之后,熔断器会进入半开状态(Half-Open),此时假如新请求成功执行,那么会进入关闭状态(Closed),否则继续开启(Open)。为了让熔断后能快速降级,我们通常需要指定相应的fallback处理逻辑。
在SpringCloud中,我们主要使用Hystrix组件来完成熔断降级,下面看看怎么实现。
首先,我们得引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在启动类上加@EnableHystrix注解,开启Hystrix。
在API层,我们只需要加@HystrixCommand注解即可。如前面所说,当接口熔断后,我们需要指定降级逻辑,即指定fallback方法:
@GetMapping("/simpleHystrix")
@HystrixCommand(fallbackMethod = "fallbackHandler"
})
public String simpleHystrix(@RequestParam("count") Integer count){
System.out.println("执行.................");
int i=10/count;
return "success";
}
public String fallbackHandler(Integer count){
System.out.println("count="+count);
return "fail";
}
这里我们定义了一个简单的接口,当count=0时,很明显会发生异常,在某段时间内出现异常的次数达到阈值,新请求就会进入fallbackHandler进行处理,不会继续调用simpleHystrix的逻辑。我们可以通过commandProperties/@HystrixProperty指定一些基本的参数,比如:
commandProperties = {
@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,value = "3"),
@HystrixProperty(name =HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,value = "20000")
这里我们指定了10秒内出现3次异常,就会进入Open状态,然后再20秒之后,会进入Half-Open状态。
Feign整合Hystrix
在实际场景中,熔断器解决的大部分是微服务调用的问题,所以这里我们看看怎样让Feign整合Hystrix。
前面提到过的@FeignClient,其实直接支持配置Hystrix。它支持的方式有两种:fallback、fallbackFactory。前者比较简单,仅需要配置当前接口实现类作为降级函数,后者功能丰富一点,可以获取触发降级的原因。我们这里先用前者快速实现一下。
首先定义fallback类,该类实现服务接口及其所有方法,以MerchantService为例:
public class MerchantServiceFallBack implements MerchantService {
@Override
public DefaultResponseData saveMerchantOrder(MerchantOrderRequest merchantOrderRequest) {
DefaultResponseData responseData=new DefaultResponseData();
responseData.setCode("1001");
responseData.setMessage("fallback");
return responseData;
}
}
然后在@FeignClient中加上:
fallback = MerchantServiceFallBack.class
最后,别忘记在配置文件中开启feign-hystrix:
feign.hystrix.enabled=true
当调用MerchantService接口服务时,一旦出现异常情况,会转入MerchantServiceFallBack的逻辑。
6. API网关之Spring Cloud Gateway
API网关主要解决的问题有:API鉴权、流量控制、请求过滤、聚合服务等。它并非微服务的必需品,具体怎么用得看实际场景。目前比较流行的网关有Zuul、Spring Cloud GateWay等。前者比较老牌了,网上资料也较多,而后者是新贵,算是SpringCloud的亲儿子,个人感觉也更好用,我们以它为例来讲解API网关的常见用法。
Spring Cloud Gateway基于Spring5、Reactor以及SpringBoot2构建,提供路由(断言、过滤器)、熔断集成、请求限流、URL重写等功能。
SpringBoot/SpringCloud系列的组件太多,经常会出现版本不对应,以下是经过测试无误的搭配:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
最重要的一步是定义路由:
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p
.path("/api/order/gateWay")
.uri("http://localhost:8889"))
.build();
}
代码解释:当访问本服务的/api/order/gateWay时,会将请求转发到
:8889/api/order/gateWay。然后我们也可以在转发请求前进行过滤处理,比如新增header参数、请求参数等,大家可以自行测试:
filters(f -> f.addRequestHeader("token", "123"))
在实际项目中,网关所调用的目标服务都注册在注册中心里面,所以一般来说,会让网关访问注册中心地址。假如用的是Nacos,可以将uri中的http换成lb:
lb://platform-service