如何使用OpenFeign+WebClient實現非阻塞的接口聚合

隨着微服務的遍地開花,愈來愈多的公司開始採用SpringCloud用於公司內部的微服務框架。react

按照微服務的理念,每一個單體應用的功能都應該按照功能正交,也就是功能相互獨立的原則,劃分紅一個個功能獨立的微服務(模塊),再經過接口聚合的方式統一對外提供服務!web

然而隨着微服務模塊的不斷增多,經過接口聚合對外提供服務的中層服務須要聚合的接口也愈來愈多!慢慢地,接口聚合就成分佈式微服務架構裏一個很是棘手的性能瓶頸!json

舉個例子,有個聚合服務,它須要聚合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毫秒架構

也就是聚合服務的請求時間與接口數量成正比關係,這種作法顯然不能接受!併發

而解決這種問題的最多見作法就是預先建立線程池,經過多線程併發請求接口進行接口聚合!app

這種方案在網上隨便百度一下就能找到好多,今天我就再也不把它的代碼貼出來!而是說一下這個方法的缺點:框架

本來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的三分之一!正好在乎料之中!

相關文章
相關標籤/搜索