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

末日使者
• 阅读 8351

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

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

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

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


@Headers({ "Accept: application/json" })

public interface ServiceClient {

    @RequestLine("GET /")

    List<Service> list();

}

@Headers({ "Accept: application/json" })

public interface RouteClient {

    @RequestLine("GET /")

    List<Route> list();

}

@Headers({ "Accept: application/json" })

public interface PluginClient {

    @RequestLine("GET /")

    List<Plugin> list();

}

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

编写单元测试


public class SyncFeignClientTest {

    public static final String SERVER = "http://devops2:8001";

    private ServiceClient serviceClient;

    private RouteClient routeClient;

    private PluginClient pluginClient;

    @Before

    public void setup(){

        BasicConfigurator.configure();

        Logger.getRootLogger().setLevel(Level.INFO);

        String service = SERVER + "/services";

        serviceClient = Feign.builder()

                .target(ServiceClient.class, service);

        String route = SERVER + "/routes";

        routeClient = Feign.builder()

                .target(RouteClient.class, route);

        String plugin = SERVER + "/plugins";

        pluginClient = Feign.builder()

                .target(PluginClient.class, plugin);

    }

    @Test

    public void aggressionTest() {

        long current = System.currentTimeMillis();

        System.out.println("开始调用聚合查询");

        serviceTest();

        routeTest();

        pluginTest();

        System.out.println("调用聚合查询结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

    }

    @Test

    public void serviceTest(){

        long current = System.currentTimeMillis();

        System.out.println("开始获取Service");

        String service = serviceClient.list();

        System.out.println(service);

        System.out.println("获取Service结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

    }

    @Test

    public void routeTest(){

        long current = System.currentTimeMillis();

        System.out.println("开始获取Route");

        String route = routeClient.list();

        System.out.println(route);

        System.out.println("获取Route结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

    }

    @Test

    public void pluginTest(){

        long current = System.currentTimeMillis();

        System.out.println("开始获取Plugin");

        String plugin = pluginClient.list();

        System.out.println(plugin);

        System.out.println("获取Plugin结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

    }

}

测试结果:


开始调用聚合查询

开始获取Service

{"next":null,"data":[]}

获取Service结束!耗时:134毫秒

开始获取Route

{"next":null,"data":[]}

获取Route结束!耗时:44毫秒

开始获取Plugin

{"next":null,"data":[]}

获取Plugin结束!耗时:45毫秒

调用聚合查询结束!耗时:223毫秒

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做一个异步非阻塞的接口聚合教程:

首先,引入依赖


<dependency>

    <groupId>com.playtika.reactivefeign</groupId>

    <artifactId>feign-reactor-core</artifactId>

    <version>1.0.30</version>

    <scope>test</scope>

</dependency>

<dependency>

    <groupId>com.playtika.reactivefeign</groupId>

    <artifactId>feign-reactor-webclient</artifactId>

    <version>1.0.30</version>

    <scope>test</scope>

</dependency>

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


@Headers({ "Accept: application/json" })

public interface ServiceClient {

    @RequestLine("GET /")

    Flux<Service> list();

}

@Headers({ "Accept: application/json" })

public interface RouteClient {

    @RequestLine("GET /")

    Flux<Service> list();

}

@Headers({ "Accept: application/json" })

public interface PluginClient {

    @RequestLine("GET /")

    Flux<Service> list();

}

然后编写单元测试


public class AsyncFeignClientTest {

    public static final String SERVER = "http://devops2:8001";

    private CountDownLatch latch;

    private ServiceClient serviceClient;

    private RouteClient routeClient;

    private PluginClient pluginClient;

    @Before

    public void setup(){

        BasicConfigurator.configure();

        Logger.getRootLogger().setLevel(Level.INFO);

        latch= new CountDownLatch(3);

        String service= SERVER + "/services";

        serviceClient= WebReactiveFeign

                .<ServiceClient>builder()

                .target(ServiceClient.class, service);

        String route= SERVER + "/routes";

        routeClient= WebReactiveFeign

                .<RouteClient>builder()

                .target(RouteClient.class, route);

        String plugin= SERVER + "/plugins";

        pluginClient= WebReactiveFeign

                .<PluginClient>builder()

                .target(PluginClient.class, plugin);

}

    @Test

    public void aggressionTest() throws InterruptedException {

        long current= System.currentTimeMillis();

        System.out.println("开始调用聚合查询");

        serviceTest();

        routeTest();

        pluginTest();

        latch.await();

        System.out.println("调用聚合查询结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

}

    @Test

    public void serviceTest(){

        long current= System.currentTimeMillis();

        System.out.println("开始获取Service");

        serviceClient.list()

                .subscribe(result ->{

                    System.out.println(result);

                    latch.countDown();

                    System.out.println("获取Service结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

});

}

    @Test

    public void routeTest(){

        long current= System.currentTimeMillis();

        System.out.println("开始获取Route");

        routeClient.list()

                .subscribe(result ->{

                    System.out.println(result);

                    latch.countDown();

                    System.out.println("获取Route结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

});

}

    @Test

    public void pluginTest(){

        long current= System.currentTimeMillis();

        System.out.println("开始获取Plugin");

        pluginClient.list()

                .subscribe(result ->{

                    System.out.println(result);

                    latch.countDown();

                    System.out.println("获取Plugin结束!耗时:" + (System.currentTimeMillis() - current) + "毫秒");

});

}

}

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

测试结果:


开始调用聚合查询

开始获取Service

开始获取Route

开始获取Plugin

{"next":null,"data":[]}

{"next":null,"data":[]}

获取Plugin结束!耗时:215毫秒

{"next":null,"data":[]}

获取Route结束!耗时:216毫秒

获取Service结束!耗时:1000毫秒

调用聚合查询结束!耗时:1000毫秒

Process finished with exit code 0

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

下面开始性能测试:

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

开始调用聚合查询
开始获取Service
{"next":null,"data":[]}
获取Service结束!耗时:169毫秒
开始获取Route
{"next":null,"data":[]}
获取Route结束!耗时:81毫秒
开始获取Plugin
{"next":null,"data":[]}
获取Plugin结束!耗时:93毫秒
调用聚合查询结束!耗时:343毫秒
summary: 238515, average: 238

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

开始调用聚合查询
开始获取Service
开始获取Route
开始获取Plugin
{"next":null,"data":[]}
{"next":null,"data":[]}
获取Route结束!耗时:122毫秒
{"next":null,"data":[]}
获取Service结束!耗时:122毫秒
获取Plugin结束!耗时:121毫秒
调用聚合查询结束!耗时:123毫秒
summary: 89081, average: 89

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

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
4年前
SQL语句中发送微信消息
利用EasySQLMAIL的外部接口表功能来发送。可以支持Oracle、SQLServer、MySQL、PostgreSQL、Informix数据库。步骤如下:(1)在数据源的“外部接口功能”中启用“允许从该数据源上的接口表中发送企业微信或钉钉消息”选项,并选择可以用来发消息的企业微信应用(注意记录下应用ID,后面的S
Easter79 Easter79
4年前
springboot2.0下的zuul路由网关初探
Zuul作为微服务系统的网关组件,用于构建边界服务,致力于动态路由、过滤、监控、弹性伸缩和安全。为什么需要ZuulZuul、Ribbon以及Eureka结合可以实现智能路由和负载均衡的功能;网关将所有服务的API接口统一聚合,统一对外暴露。外界调用API接口时,不需要知道微服务系统中各服务相互调用的复杂性,保护了内部微服务单元的API接口;网关可以做
Wesley13 Wesley13
4年前
SOA
面向服务的体系结构(ServiceOrientedArchitecture,SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来。接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构建在各种这样的系统中的服务可以一种统一和通用的方式进行交互。实现SOA架构
徐小夕 徐小夕
5年前
微前端架构初探以及我的前端技术盘点
前言最近几年微前端一直是前端界的热门议题,它类似于微服务架构,主要面向于浏览器端,能将一个复杂而庞大的单体应用拆分为多个功能模块清晰且独立的子应用,且共同服于务同一个主应用。各个子应用可以独立运行、独立开发和独立部署。微前端架构概念的诞生及应用对于提供复杂应用服务的企业来说显然是一种机遇,同样也是一种挑战.本文主要就微前端架构的概念和实现方案做一
Stella981 Stella981
4年前
Python微信机器人
Python微信机器人本文目录一简介二登录微信三微信好友男女比例四微信好友地域分布五微信聊天机器人一简介wxpy基于itchat,使用了Web微信的通讯协议,,通过大量接口优化提升了模块的易用性,并进行丰富的功能扩展。实现了微信登录、收发消息、搜索好友、数
Wesley13 Wesley13
4年前
UCKeFu 3.0.0 发布,增加呼叫中心平台功能
  UCKeFuv3.0.0发布,UCKeFu基于JAVA语言开发,是一个全渠道融合的客户支持服务平台,聚合企业内部多个客服渠道,帮助各种行业各种规模的企业建立完整客服体系。  通过将邮件、短信、电话语音、WebIM在线客服、微信、微博、H5页面、APP接口等多个渠道来源的客户服务请求与对话汇聚在一个管理平台,用统一的方式来响应和支
Stella981 Stella981
4年前
Spring Cloud与微服务构建:Spring Cloud简介
SpringCloud简介<fontsize4微服务因该具备的功能</font微服务可以拆分为"微"和"服务"二字。"微"即小的意思,那到底多小才算"微"呢?可能不同的团队有不同的答案。从参与微服务的人数来讲,单个微服务从架构设计、代码开发、测试、运维人数加起来是8~10人才算"微"。那么何为"服务"呢?按照"微服务"概念
Easter79 Easter79
4年前
SpringCloud学习笔记(七)之路由网关Zuul
是什么Zuul包含了对请求路由和过滤两个最主要的功能:其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。而过滤功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。Zuul和Eureka进行整合,将zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微
Stella981 Stella981
4年前
Spring Cloud 里 Eureka 什么是微服务及微服务的特点
1.什么是微服务/微服务的特点单一职责:微服务中的每一个服务都对应唯一的业务能力,做到单一职责微:微服务的服务拆分粒度很小,列如一个用户管理就可以作为一个服务,每个服务虽小,但五脏俱全独立:自治是说服务间相互独立,互不干扰团队独立:每个服务都是一个独立的开发团队,人数不
Stella981 Stella981
4年前
SpringBoot入门(简单详细教程)
SpringBoot简介  简化Spring应用开发的一个框架;整个Spring技术栈的一个大整合;J2EE开发的一站式解决方案;微服务  martinfowler;微服务:架构风格(服务微化);一个应用应该是一组小型服务;可以通过HTTP的方式进行互通;单体应用:ALLINONE;微服务:每一个功