歡迎光臨
每天分享高質量文章

【RPC 專欄】Motan 中使用非同步 RPC 介面

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 


  • 為什麼慢?

  • 多執行緒加速

  • 非同步呼叫

  • RPC 非同步呼叫

  • 總結


這週六參加了一個美團點評的技術沙龍,其中一位老師在介紹他們自研的 RPC 框架時提到一點:RPC 請求分為 sync,future,callback,oneway,並且需要遵循一個原則:能夠非同步的地方就不要使用同步。正好最近在最佳化一個業務場景:在一次頁面展示中,需要呼叫 5 個 RPC 介面,導致頁面響應很慢。正好啟發了我。

為什麼慢?

大多數開源的 RPC 框架實現遠端呼叫的方式都是同步的,假設 [ 介面1,…,介面5]的每一次呼叫耗時為 200ms (其中介面2依賴介面1,介面5依賴介面3,介面4),那麼總耗時為 1s,這整個是一個序列的過程。

多執行緒加速

第一個想到的解決方案便是多執行緒,那麼[1=>2]編為一組,[[3,4]=>5]編為一組,兩組併發執行,[1=>2]序列執行耗時400ms,[3,4]併發執行耗時200ms,[[3,4]=>5]總耗時400ms ,最終[[1=>2],[[3,4]=>5]]總耗時400ms(理論耗時)。相比較於原來的1s,的確快了不少,但實際編寫介面花了不少功夫,建立執行緒池,管理資源,分析依賴關係…總之程式碼不是很優雅。

RPC中,多執行緒著重考慮的點是在客戶端最佳化程式碼,這給客戶端帶來了一定的複雜性,並且編寫併發程式碼對程式員的要求更高,且不利於除錯。

非同步呼叫

如果有一種既能保證速度,又能像同步 RPC 呼叫那樣方便,豈不美哉?於是引出了 RPC 中的非同步呼叫。

在 RPC 非同步呼叫之前,先回顧一下 java.util.concurrent 中的基礎知識:Callable 和 Future

public class Main {
   public static void main(String[] args) throws Exception{
       final ExecutorService executorService = Executors.newFixedThreadPool(10);
       long start = System.currentTimeMillis();
       Future resultFuture1 = executorService.submit(new Callable() {
           @Override
           public Integer call() throws Exception {
               return method1() + method2();
           }
       });
       Future resultFuture2 = executorService.submit(new Callable() {
           @Override
           public Integer call() throws Exception {
               Future resultFuture3 = executorService.submit(new Callable() {
                   @Override
                   public Integer call() throws Exception {
                       return method3();
                   }
               });
               Future resultFuture4 = executorService.submit(new Callable() {
                   @Override
                   public Integer call() throws Exception {
                       return method4();
                   }
               });
               return method5()+resultFuture3.get()+resultFuture4.get();
           }
       });
       int result = resultFuture1.get() + resultFuture2.get();
       System.out.println("result = "+result+", total cost "+(System.currentTimeMillis()-start)+" ms");
         executorService.shutdown();
   }
   static int method1(){
       delay200ms();
       return 1;
   }
   static int method2(){
       delay200ms();
       return 2;
   }
   static int method3(){
       delay200ms();
       return 3;
   }
   static int method4(){
       delay200ms();
       return 4;
   }
   static int method5(){
       delay200ms();
       return 5;
   }
   static void delay200ms(){
       try{
           Thread.sleep(200);
       }catch (Exception e){
           e.printStackTrace();
       }
   }
}

最終控制檯列印:

result = 15, total cost 413 ms

五個介面,如果同步呼叫,便是序列的效果,最終耗時必定在 1s 之上,而非同步呼叫的優勢便是,submit任務之後立刻傳回,只有在呼叫 future.get() 方法時才會阻塞,而這期間多個非同步方法便可以併發的執行。

RPC 非同步呼叫

我們的專案使用了 Motan 作為 RPC 框架,檢視其 changeLog ,0.3.0 (2017-03-09) 該版本已經支援了 async 特性。可以讓開發者很方便地實現 RPC 非同步呼叫。

1 為介面增加 @MotanAsync 註解

@MotanAsync
public interface DemoApi {
   DemoDto randomDemo(String id);
}

2 新增 Maven 外掛

<build>
   <plugins>
       <plugin>
           <groupId>org.codehaus.mojogroupId>


           <artifactId>build-helper-maven-pluginartifactId>
           <version>1.10version>
           <executions>
               <execution>
                   <phase>generate-sourcesphase>
                   <goals>
                       <goal>add-sourcegoal>
                   goals>
                   <configuration>
                       <sources>
                           <source>${project.build.directory}/generated-sources/annotationssource>
                       sources>
                   configuration>
               execution>
           executions>
       plugin>
   plugins>
build>

安裝外掛後,可以藉助它生成一個和 DemoApi 關聯的非同步介面 DemoApiAsync 。

public interface DemoApiAsync extends DemoApi {
 ResponseFuture randomDemoAsync(String id);
}

3 註入介面即可呼叫

@Service
public class DemoService {
   @MotanReferer
   DemoApi demoApi;
   @MotanReferer
   DemoApiAsync demoApiAsync;//<1>
   public DemoDto randomDemo(String id){
       DemoDto demoDto = demoApi.randomDemo(id);
       return demoDto;
   }
   public DemoDto randomDemoAsync(String id){
       ResponseFuture responseFuture = demoApiAsync.randomDemoAsync(id);//<2>
       DemoDto demoDto = (DemoDto) responseFuture.getValue();
       return demoDto;
   }
}

<1> DemoApiAsync 如何生成的已經介紹過,它和 DemoApi 並沒有功能性的區別,僅僅是同步非同步呼叫的差距,而 DemoApiAsync 實現的的複雜性完全由 RPC 框架幫助我們完成,開發者無需編寫 Callable 介面。

<2> ResponseFuture 是 RPC 中 Future 的抽象,其本身也是 juc 中 Future 的子類,當 responseFuture.getValue() 呼叫時會阻塞。

總結

在非同步呼叫中,如果發起一次非同步呼叫後,立刻使用 future.get() ,則大致和同步呼叫等同。其真正的優勢是在submit 和 future.get() 之間可以混雜一些非依賴性的耗時操作,而不是同步等待,從而充分利用時間片。

另外需要註意,如果非同步呼叫涉及到資料的修改,則多個非同步操作直接不能保證 happens-before 原則,這屬於併發控制的範疇了,謹慎使用。查詢操作則大多沒有這樣的限制。

在能使用併發的地方使用併發,不能使用的地方才選擇同步,這需要我們思考更多細節,但可以最大限度的提升系統的效能。

666. 彩蛋

如果你對 RPC 併發感興趣,歡迎加入我的知識一起交流。

知識星球

目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:

01. 除錯環境搭建
02. 專案結構一覽
03. API 配置(一)之應用
04. API 配置(二)之服務提供者
05. API 配置(三)之服務消費者
06. 屬性配置
07. XML 配置
08. 核心流程一覽

一共 60 篇++

贊(0)

分享創造快樂