Spring Cloud 微服务实践 (3) - 服务间的调用

用户头像
xiaoboey
关注
发布于: 2020 年 09 月 20 日
Spring Cloud 微服务实践 (3) - 服务间的调用

在前面的文章里我们已经构建了一个基本的微服务框架,并且通过Gateway的代理转发和重试机制,在终端用户和内部服务之间建立了一个边界(隔离),使得内部服务具有良好的伸缩性。这章我们继续探索扩展,增加新的内部服务并且实现服务间的相互调用。



本文的源代码: https://github.com/xiaoboey/from-zero-to-n/tree/master/one ,Spring Boot的版本是2.3.3,Spring Cloud的版本是Hoxton.SR8。



一般说到服务的调用,惯常的方式都是要知道这个被调用服务的地址、通讯协议和参数,然后根据这些信息来有针对性的开发调用代码。



使用TCP/IP协议传输二进制数据需要IP地址,使用Web Service交换XML封装的数据需要暴露endpoint,而基于HTTP协议的RESTful服务则是要发布一个url地址出来。那么我们已搭建好的这个微服务框架里,如果其他内部服务要调用service-one提供的方法,要怎么做呢?我们知道service-one可以启动多个个实例,并且可能会动态增加或减少(弹性伸缩),所以不能确定service-one实例的地址。



从前几篇文章我们对Spring Cloud的了解来看,大概可以按照这么一个思路来实现内部服务的相互调用:Eureka Server是服务注册中心,记录了所有服务实例的ip地址,所以可以通过Eureka Server获取到ip地址,然后组装url地址去调用service-one的mvc方法,实现服务间的调用。



或许也有人会说可以通过网关绕一个大圈子去调用,即使实际生产环境里我们会配置多个网关,但是网关毕竟要向外暴露服务,这个地址你总得固定下来是不是?这个方案是可行,但是在性能上不尽人意,内部服务间的通讯带宽基本是以每秒多少G为单位,而互联网呢?还是以每秒多少M为单位。舍近求远了,不够优雅。



上述的带宽值,我参照的是阿里云的ECS参数,内网通讯最少也是0.5G/S这样标示(确实是以G为单位是吧^_^),外网带宽则是从1M开始,可见内部通讯和外部通讯,差距还是蛮大的。



Spring Cloud为了实现微服务间的相互调用,已经在框架层做了很多工作,实现了两个比较优雅的方式:Ribbon和Feign。

1、服务调用之Ribbon

Ribbon提供了一个RestTemplate,实现了一些模板方法,可以通过服务的service-id(地址)来调用服务,类似如下的代码:

@Autowired
RestTemplate restTemplate;
...
restTemplate.getForObject("http://service-one/hello", String.class);

Ribbon的实现思路,就是我们前面分析说到的通过服务注册中心(Eureka)获取service-one的实例地址,然后进行调用。这种集成方式很Spring,比如JdbcTemplate,也是封装成模板方法来方便使用提高开发效率。



2、服务调用之Feign

Feign是Netflix开发的声明式、模块化的HTTP客户端。Spring Cloud 的早期发展,Netflix功不可没,像服务发现(Eureka),熔断(Hystrix),网关(Zuul)等,早期的Spring Cloud都是通过集成Netflix的实现来提供的这些能力。



Spring Cloud Feign是Spring Cloud Hystrix(熔断)和Spring Cloud Ribbon整合,我们只需要定义一个目标服务方法的接口,加上Feign的注解开启Feign,就可以直接通过该接口调用方法,比Ribbon方式更加简单和优雅。



使用Feign做服务调用,大致的开发流程如下:

在pom.xml文件中增加openfeign依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>



在启动类上增加注解@EnableFeignClients,开启Feign能力:

...
@EnableFeignClients
public class ServiceTwoApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceTwoApplication.class, args);
}
}



增加一个接口文件,这个接口对应要调用的服务(Controller),这里以service-one为例:

@FeignClient(value = "service-one")
public interface IServiceOneFeignClient {
@PostMapping("/hello")
String hello();
@PostMapping("/getPort")
Integer getPort();
}

注:value = "service-one",表示用Feign调用service-one提供的服务,然后hello和getPort就是service-one通过Controller发布出来的两个mvc方法,并且这里接口的定义也使用了Controller的PostMapping注解来表明url路径。



然后就是在需要时进行服务调用:

@Autowired
ServiceOneFeignClient serviceOneFeignClient;
...
serviceOneFeignClient.hello();



3、测试Feign方式的服务调用

增加子项目的方式我们可以继续用Spring Initizlizr来初始化,也可以复制现有的service-one来修改



增加一个新的子项目Service Two,按前面的方式添加Feign依赖,在启动类上开启Feign的注解,然后实现针对service-one的服务调用,再用Controller暴露方法进行测试。



其他完整的代码请大家在Github上去查看,这里我只贴一下 TwoController.java的代码:

import com.example.servicetwo.feignclient.IServiceOneFeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/feign-test")
public class TwoController {
private final IServiceOneFeignClient serviceOneFeignClient;
public TwoController(ServiceOneFeignClient serviceOneFeignClient) {
this.serviceOneFeignClient = serviceOneFeignClient;
}
@RequestMapping("/hello")
public String hello() {
return serviceOneFeignClient.hello();
}
@RequestMapping("/getPort")
public int getPort() {
return serviceOneFeignClient.getPort();
}
}



在给Gateway增加重试机制时,我们已经把Gateway的自动发现配置给删除了,所以现在增加一个新的service时都要在Gateway上进行手工配置。 增加了service-two信息的Gateway配置(application.yml)如下:

...
spring:
profiles:
active: dev
application:
name: gateway
cloud:
gateway:
routes:
- id: service-one
uri: lb://service-one
predicates:
- Path=/service-one/**
filters:
- StripPrefix=1
- name: Retry
args:
retries: 1
series:
- SERVER_ERROR
methods:
- GET
- POST
exceptions:
- java.io.IOException
- id: service-two
uri: lb://service-two
predicates:
- Path=/service-two/**
filters:
- StripPrefix=1
- name: Retry
args:
retries: 1
series:
- SERVER_ERROR
methods:
- GET
- POST
...

注:service-two的route配置基本就是照抄service-one



配置好后,依次启动eureka-server、gateway、service-one和service-two,然后访问http://localhost:8080/service-two/feign-test/getPort 进行测试。要等这些服务都就绪后才能正常测试,所以刚启动的话需要稍微等一等。注意TwoController我们多增加了一级路径“feign-test”表示是做Feign服务调用的测试。



一切正常的话,feign-test/getPort返回了实现这个功能的service-one实例的端口。



4、Feign的负载均衡

通过我们前面的努力,已经实现了用Feign进行微服务间的相互调用,但是从程序的健壮性考虑,我觉得还需要做进一步的探索。跟Gateway一样,我们任然启动多个service-one实例来测试一下,Feign是否会发现新的service-one实例并把负载分摊给不同的实例,也就是做负载均衡。



还是贴一下启动多个service-one实例的代码:

mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8082 --management.server.port=7082"
#如果是PowerShell,带=号的参数要用单引号引起来才能正确传递
mvn spring-boot:run -D'spring-boot.run.arguments="--server.port=8082 --management.server.port=7082"'



等新启动的service-one启动就绪后,也就是新实例向eureka-server注册,然后Feign获取到新实例的信息并向新实例发送请求调用服务,feign-test/getPort返回的端口就开始出现新实例的端口,说明Feign确实做了负载均衡,使得我们的微服务具备弹性伸缩能力。



5、Feign的重试

跟Gateway的测试一样,我们把service-one的实例关闭一个,继续刷对feign-test/getPort的调用,看看service-one实例的动荡是否会影响到调用端。



从测试的结果来看,调用端没有受影响。Feign默认配置已经做了负载均衡和重试,你只管放心做服务调用就好了。如果需要对Feign的重试进行自定义配置,也是可以的,这里我们先放下不去深究,保持这一系列文章的Simple and Stupid。



6、Feign的熔断机制(Hystrix)

之前我们可能都没有听说过“熔断”,微服务需要熔断,是因为业务拆分粒度很小,然后服务之间相互调用,使得服务之间会存在依赖关系。比如本文的service-two就依赖service-one,如果service-one所有实例整体故障会发生什么情况呢?这种情况Feign的负载均衡和重试也无力回天。熔断(Hystrix)是通过将依赖服务进行隔离的方式来阻止故障蔓延,阻止因为某个依赖服务出现故障而导致整个系统不可以用的“雪崩”效应。



“雪崩”这个说法比较形象,自然界的雪崩最初开始崩解的地方可能只是一条小裂缝,然后迅速发展为一场灾难。在我们的微服务体系中,服务间的依赖随着系统的日益庞大,依赖也会变得越来越复杂,“雪崩”的风险也就变得越来越高。



法国著名的思想家伏尔泰说“雪崩时没有一片雪花是无辜的”,为了让我们实现的服务成为“无辜的”,借助Feign的熔断机制进行故障隔离就变得很有必要,从而提高微服务系统的可用性和稳定性。



虽然“熔断”我们接触得少,“雪崩”后果也挺严重,但是利用Hystrix的fallback机制来对故障进行降级处理是比较简单的,先实现一个降级处理(fallback)的IServiceOneFeignClient,然后在IServiceOneFeignClient的FeignClient注解里用fallback参数指向它。这样Feign在调用service-one时,如果判断出service-one已经停摆,就会使用fallback进行降级处理。



ServiceOneFallback.java

import org.springframework.stereotype.Component;
@Component
public class ServiceOneFallback implements IServiceOneFeignClient {
@Override
public String hello() {
return "error";
}
@Override
public Integer getPort() {
return Integer.valueOf(-1);
}
}



修改IServiceOneFeignClient.java

...

@FeignClient(value = "service-one", fallback = ServiceOneFallback.class)
public interface IServiceOneFeignClient {
...
}



修改application.yml,开启Feign的hystrix:

feign:
hystrix:
enabled: true

注:Hystrix默认是关闭的。



代码完成后,照例测试一下进行验证。在所有微服务都启动就绪后,先访问http://localhost:8080/service-two/feign-test/getPort ,先确定能返回我们预期的端口号,整个体系运转正常。如果出现异常(type=Service Unavailable, status=503)则是未就绪,需要再等一会。



然后关闭service-one的所有实例,刷新feign-test/getPort,看看返回什么:

7、回顾与总结

至此我们已经实现了服务间的相互调用,并且借助Feign的负载均衡、重试和熔断,让我们从0开始搭建的这个微服务系统兼具了弹性伸缩、高可用和稳定性。



就我个人的理解,搭建到这个程度,虽然体现了微服务架构的一些特点和优势,但是还是没有那么让人心动。举例用到的业务功能(service-one和service-two)都是无状态的简单服务,真要是实际需求是这样的,直接用Spring Boot + Spring MVC做业务开发,再加上Nginx这样的反向代理做负载均衡,就已经完全胜任了。



Spring Cloud的优点,比如服务拆分粒度很细,有利于提高开发效率(小功能容易理解、控制和开发,团队分工后更容易齐头并进),独立部署后便于弹性扩容,有利于资源的合理分配;使用服务注册和发现后,服务之间采用Feign等非常成熟的轻量通讯进行调用,开发集成方便高效;熔断机制也让系统的稳定性大大提高。当然Spring Cloud还有很多其他的特性,市面上比较成熟的分布式系统开发的功能模块都被集成到了Spring Cloud全家桶里,比如配置中心、消息中心等我们还没接触的内容。并且这个全家桶还在日益壮大,像阿里云的Spring Cloud Alibaba是由第三方厂商实现并加入进来,使得整个生态更加繁荣。



从我们搭建的这个微服务体系来看,Spring Cloud的缺点大概就是这个生态太繁荣,“乱花渐欲迷人眼”,全家桶的成员比较多,部分是重复的,部分是新旧关系替换(比如Gateway和Zuul),并且像Netflix这样的大厂,看起来是要退出的感觉,而Alibaba呢则非常积极,出钱出力还帮吆喝。这些会给我们带来一些困扰,再加上要解决业务拆分带来的一些新问题,增加了学习的难度。



从微服务开发本身来看,业务拆分时不好拆分,细粒度服务的开发,如果从整体来看要考虑容错、分布式事务等整体能力,对团队的挑战比较大。另外从开发、测试环境,到生产环境,部署上启动的实例很多,不利于治理,也就是说基于微服务的方式进行开发,测试、运维团队也得升级“微服务”化。像容器化部署的Docker,容器编排的Kubernetes,这些都是在解决微服务的治理问题。



8、应对Spring Cloud的复杂性

Spring Cloud全家桶提供的能力繁多,我们的应对策略只能“化繁为简”,掌握核心的思想,搭建的架构也只集成核心的能力,其他的则看实际情况需要再加入进来。Spring Cloud在设计上的扩展性还是很好的。



后续文章的难度会增大,会涉及到数据库和权限控制,更偏向项目实战,所以先做一个高能预警。



上一篇: 《Spring Cloud 微服务实践 (2) - Gateway 重试机制

下一篇: 《Spring Cloud 微服务实践 (4) - OAuth2

发布于: 2020 年 09 月 20 日 阅读数: 114
用户头像

xiaoboey

关注

IT老兵 2020.07.20 加入

资深Coder,爱好钓鱼逮鸟。

评论

发布
暂无评论
Spring Cloud 微服务实践 (3) - 服务间的调用