隨着微服務的遍地開花,愈來愈多的公司開始採用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的三分之一!正好在乎料之中!