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

随着微服务的遍地开花,越来越多的公司开始采用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的三分之一!正好在意料之中!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容