资讯专栏INFORMATION COLUMN

如何使用OpenFeign+WebClient实现非阻塞的接口聚合

王岩威 / 1365人阅读

摘要:随着微服务的遍地开花,越来越多的公司开始采用用于公司内部的微服务框架。

随着微服务的遍地开花,越来越多的公司开始采用SpringCloud用于公司内部的微服务框架。

按照微服务的理念,每个单体应用的功能都应该按照功能正交,也就是功能相互独立的原则,划分成一个个功能独立的微服务(模块),再通过接口聚合的方式统一对外提供服务!

然而随着微服务模块的不断增多,通过接口聚合对外提供服务的中层服务需要聚合的接口也越来越多!慢慢地,接口聚合就成分布式微服务架构里一个非常棘手的性能瓶颈!

举个例子,有个聚合服务,它需要聚合Service、Route和Plugin三个服务的数据才能对外提供服务:

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface ServiceClient {
  3. @RequestLine("GET /")
  4. List list();
  5. }

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface RouteClient {
  3. @RequestLine("GET /")
  4. List list();
  5. }

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface PluginClient {
  3. @RequestLine("GET /")
  4. List list();
  5. }

使用声明式的OpenFeign代替HTTP Client进行网络请求

编写单元测试

</>复制代码

  1. public class SyncFeignClientTest {
  2. public static final String SERVER = "http://devops2:8001";
  3. private ServiceClient serviceClient;
  4. private RouteClient routeClient;
  5. private PluginClient pluginClient;
  6. @Before
  7. public void setup(){
  8. BasicConfigurator.configure();
  9. Logger.getRootLogger().setLevel(Level.INFO);
  10. String service = SERVER + "/services";
  11. serviceClient = Feign.builder()
  12. .target(ServiceClient.class, service);
  13. String route = SERVER + "/routes";
  14. routeClient = Feign.builder()
  15. .target(RouteClient.class, route);
  16. String plugin = SERVER + "/plugins";
  17. pluginClient = Feign.builder()
  18. .target(PluginClient.class, plugin);
  19. }
  20. @Test
  21. public void aggressionTest() {
  22. long current = System.currentTimeMillis();
  23. System.out.println("开始调用聚合查询");
  24. serviceTest();
  25. routeTest();
  26. pluginTest();
  27. System.out.println("调用聚合查询结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  28. }
  29. @Test
  30. public void serviceTest(){
  31. long current = System.currentTimeMillis();
  32. System.out.println("开始获取Service");
  33. String service = serviceClient.list();
  34. System.out.println(service);
  35. System.out.println("获取Service结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  36. }
  37. @Test
  38. public void routeTest(){
  39. long current = System.currentTimeMillis();
  40. System.out.println("开始获取Route");
  41. String route = routeClient.list();
  42. System.out.println(route);
  43. System.out.println("获取Route结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  44. }
  45. @Test
  46. public void pluginTest(){
  47. long current = System.currentTimeMillis();
  48. System.out.println("开始获取Plugin");
  49. String plugin = pluginClient.list();
  50. System.out.println(plugin);
  51. System.out.println("获取Plugin结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  52. }
  53. }
测试结果:

</>复制代码

  1. 开始调用聚合查询
  2. 开始获取Service
  3. {"next":null,"data":[]}
  4. 获取Service结束!耗时:134毫秒
  5. 开始获取Route
  6. {"next":null,"data":[]}
  7. 获取Route结束!耗时:44毫秒
  8. 开始获取Plugin
  9. {"next":null,"data":[]}
  10. 获取Plugin结束!耗时:45毫秒
  11. 调用聚合查询结束!耗时:223毫秒
  12. Process finished with exit code 0

可以明显看出:聚合查询查询所用的时间223毫秒 = 134毫秒 + 44毫秒 + 45毫秒

也就是聚合服务的请求时间与接口数量成正比关系,这种做法显然不能接受!

而解决这种问题的最常见做法就是预先创建线程池,通过多线程并发请求接口进行接口聚合!

这种方案在网上随便百度一下就能找到好多,今天我就不再把它的代码贴出来!而是说一下这个方法的缺点:

原本JavaWeb的主流Servlet容器采用的方案是一个HTTP请求就使用一个线程和一个Servlet进行处理!这种做法在并发量不高的情况没有太大问题,但是由于摩尔定律失效了,单台机器的线程数量仍旧停留在一万左右,在网站动辄上千万点击量的今天,单机的线程数量根本无法应付上千万级的并发量!

而为了解决接口聚合的耗时过长问题,采用线程池多线程并发网络请求的做法,更是火上浇油!原本只需一个线程就搞定的请求,通过多线程并发进行接口聚合,就把处理每个请求所需要的线程数量给放大了,急速降低系统可用线程的数量,自然也降低系统的并发数量!

这时,人们想起从Java5开始就支持的NIO以及它的开源框架Netty!基于Netty以及Reactor模式,Java生态圈出现了SpringWebFlux等异步非阻塞的JavaWeb框架!Spring5也是基于SpringWebFlux进行开发的!有了异步非阻塞服务器,自然也有异步非阻塞网络请求客户端WebClient!

今天我就使用WebClient和ReactiveFeign做一个异步非阻塞的接口聚合教程:

首先,引入依赖

</>复制代码

  1. com.playtika.reactivefeign
  2. feign-reactor-core
  3. 1.0.30
  4. test
  5. com.playtika.reactivefeign
  6. feign-reactor-webclient
  7. 1.0.30
  8. test

然而基于Reactor Core重写Feign客户端,就是把原本接口返回值:List<实体>改成FLux<实体>,实体改成Mono<实体>

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface ServiceClient {
  3. @RequestLine("GET /")
  4. Flux list();
  5. }

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface RouteClient {
  3. @RequestLine("GET /")
  4. Flux list();
  5. }

</>复制代码

  1. @Headers({ "Accept: application/json" })
  2. public interface PluginClient {
  3. @RequestLine("GET /")
  4. Flux list();
  5. }
然后编写单元测试

</>复制代码

  1. public class AsyncFeignClientTest {
  2. public static final String SERVER = "http://devops2:8001";
  3. private CountDownLatch latch;
  4. private ServiceClient serviceClient;
  5. private RouteClient routeClient;
  6. private PluginClient pluginClient;
  7. @Before
  8. public void setup(){
  9. BasicConfigurator.configure();
  10. Logger.getRootLogger().setLevel(Level.INFO);
  11. latch= new CountDownLatch(3);
  12. String service= SERVER + "/services";
  13. serviceClient= WebReactiveFeign
  14. .builder()
  15. .target(ServiceClient.class, service);
  16. String route= SERVER + "/routes";
  17. routeClient= WebReactiveFeign
  18. .builder()
  19. .target(RouteClient.class, route);
  20. String plugin= SERVER + "/plugins";
  21. pluginClient= WebReactiveFeign
  22. .builder()
  23. .target(PluginClient.class, plugin);
  24. }
  25. @Test
  26. public void aggressionTest() throws InterruptedException {
  27. long current= System.currentTimeMillis();
  28. System.out.println("开始调用聚合查询");
  29. serviceTest();
  30. routeTest();
  31. pluginTest();
  32. latch.await();
  33. System.out.println("调用聚合查询结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  34. }
  35. @Test
  36. public void serviceTest(){
  37. long current= System.currentTimeMillis();
  38. System.out.println("开始获取Service");
  39. serviceClient.list()
  40. .subscribe(result ->{
  41. System.out.println(result);
  42. latch.countDown();
  43. System.out.println("获取Service结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  44. });
  45. }
  46. @Test
  47. public void routeTest(){
  48. long current= System.currentTimeMillis();
  49. System.out.println("开始获取Route");
  50. routeClient.list()
  51. .subscribe(result ->{
  52. System.out.println(result);
  53. latch.countDown();
  54. System.out.println("获取Route结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  55. });
  56. }
  57. @Test
  58. public void pluginTest(){
  59. long current= System.currentTimeMillis();
  60. System.out.println("开始获取Plugin");
  61. pluginClient.list()
  62. .subscribe(result ->{
  63. System.out.println(result);
  64. latch.countDown();
  65. System.out.println("获取Plugin结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");
  66. });
  67. }
  68. }

这里的关键点就在于原本同步阻塞的请求,现在改成异步非阻塞了,所以需要使用CountDownLatch来同步,在获取到接口后调用CountDownLatch.coutdown(),在调用所有接口请求后调用CountDownLatch.await()等待所有的接口返回结果再进行下一步操作!

测试结果:

</>复制代码

  1. 开始调用聚合查询
  2. 开始获取Service
  3. 开始获取Route
  4. 开始获取Plugin
  5. {"next":null,"data":[]}
  6. {"next":null,"data":[]}
  7. 获取Plugin结束!耗时:215毫秒
  8. {"next":null,"data":[]}
  9. 获取Route结束!耗时:216毫秒
  10. 获取Service结束!耗时:1000毫秒
  11. 调用聚合查询结束!耗时:1000毫秒
  12. Process finished with exit code 0

显然,聚合查询所消耗的时间不再等于所有接口请求的时间之和,而是接口请求时间中的最大值!

下面开始性能测试:

普通Feign接口聚合测试调用1000次:

</>复制代码

  1. 开始调用聚合查询
  2. 开始获取Service
  3. {"next":null,"data":[]}
  4. 获取Service结束!耗时:169毫秒
  5. 开始获取Route
  6. {"next":null,"data":[]}
  7. 获取Route结束!耗时:81毫秒
  8. 开始获取Plugin
  9. {"next":null,"data":[]}
  10. 获取Plugin结束!耗时:93毫秒
  11. 调用聚合查询结束!耗时:343毫秒
  12. summary: 238515, average: 238

使用WebClient进行接口聚合查询1000次:

</>复制代码

  1. 开始调用聚合查询
  2. 开始获取Service
  3. 开始获取Route
  4. 开始获取Plugin
  5. {"next":null,"data":[]}
  6. {"next":null,"data":[]}
  7. 获取Route结束!耗时:122毫秒
  8. {"next":null,"data":[]}
  9. 获取Service结束!耗时:122毫秒
  10. 获取Plugin结束!耗时:121毫秒
  11. 调用聚合查询结束!耗时:123毫秒
  12. summary: 89081, average: 89

测试结果中,WebClient的测试结果恰好相当于普通FeignClient的三分之一!正好在意料之中!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75367.html

相关文章

  • Spring Cloud Alibaba基础教程:支持几种服务消费方式

    摘要:那么为什么可以带给我们这样的完美编码体验呢实际上,这完全归功于的封装,由于在服务注册与发现客户端负载均衡等方面都做了很好的抽象,而上层应用方面依赖的都是这些抽象接口,而非针对某个具体中间件的实现。 通过《Spring Cloud Alibaba基础教程:使用Nacos实现服务注册与发现》一文的学习,我们已经学会如何使用Nacos来实现服务的注册与发现,同时也介绍如何通过LoadBala...

    curlyCheng 评论0 收藏0
  • SpringCloud升级之路2020.0.x版-37. 实现异步客户端封装配置管理意义与设计

    摘要:对于异步的请求,使用的是异步客户端即。要实现的配置设计以及使用举例要实现的配置设计以及使用举例首先,我们要实现的,其包含三个重试重试的要在负载均衡之前,因为重试的时候,我们会从负载均衡器获取另一个实例进行重试,而不是在同一个实例上重试多次。 本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 为何需要封装异步 HT...

    fxp 评论0 收藏0
  • Spring Cloud 参考文档(Spring Cloud Commons:通用抽象)

    摘要:通用的抽象服务发现负载均衡和断路器等模式适用于所有客户端都可以使用的通用抽象层,独立于实现例如,使用或发现。重试失败的请求可以将负载均衡的配置为重试失败的请求,默认情况下,禁用此逻辑,你可以通过将添加到应用程序的类路径来启用它。 Spring Cloud Commons:通用的抽象 服务发现、负载均衡和断路器等模式适用于所有Spring Cloud客户端都可以使用的通用抽象层,独立于实...

    yangrd 评论0 收藏0
  • SpringCloud打造微服务平台--概览

    摘要:授权框架使第三方应用程序来获取对服务的有限访问机会。无论是通过编排资源所有者和服务之间的交互批准的资源所有者,或通过允许第三方应用程序来获取自己的访问权限。 SpringCloud打造微服务平台--概览 简述 SpringCloud是什么 Spring Boot和SpringCloud是什么关系 Spring Boot是Spring的一套快速WEB开发的脚手架,可建立独立的Sprin...

    siberiawolf 评论0 收藏0
  • Spring Cloud 参考文档(声明式REST客户端:Feign)

    摘要:继承支持通过单继承接口支持样板,这允许将通用操作分组为方便的基本接口。,记录基本信息以及请求和响应。例如,类定义参数和以下客户端使用注解使用类 声明式REST客户端:Feign Feign是一个声明式的Web服务客户端,它使编写Web服务客户端变得更容易,要使用Feign,请创建一个接口并对其进行注解,它具有可插拔的注解支持,包括Feign注解和JAX-RS注解,Feign还支持可插拔...

    wqj97 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<