dubbo剖析:六 網絡通信之 -- 請求響應模型

注:文章中使用的dubbo源碼版本為2.5.4

零、文章目錄

  • Dubbo的三種RPC調用方式
  • 關鍵類介紹
  • DefaultFuture實現
  • 調用入口流程
  • 服務引用方請求響應模型總結

一、Dubbo的三種RPC調用方式

1.1 異步&無返回值

a)服務引用配置如下:

    <dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
        <dubbo:method name="sayHello" async="true" return="false"/>
    </dubbo:reference>
  • async="true"
  • return="false"

b)服務調用方式如下:

String hello = demoService.sayHello("world" + i);
  • 因為使用了異步無返回值的模式,所以hello的值一直為null;

1.2 異步&有返回值

a)服務引用配置如下:

    <dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
        <dubbo:method name="sayHello" async="true"/>
    </dubbo:reference>
  • async="true"

b)服務調用方式如下:

demoService.sayHello("world" + i);
Future<String> temp = RpcContext.getContext().getFuture();
String hello = temp.get();
  • 因為使用了異步模式,demoService.sayHello()被調用后立即返回(此時RPC調用結果還未生成,RPC的執行過程不阻塞業務請求線程);
  • 服務引用方業務請求線程可以在合適的時候執行RpcContext.getContext().getFuture()獲取RPC調用結果;

1.3 異步變同步

a)服務引用配置如下:

    <dubbo:reference id="demoService" check="false" interface="com.alibaba.dubbo.demo.DemoService">
        <dubbo:method name="sayHello" async="false"/>
    </dubbo:reference>
  • async="false",默認值;

b)服務調用方式如下:

String hello = demoService.sayHello("world" + i);
  • 因為使用了同步模式,demoService.sayHello()被調用后直到收到RPC響應才返回,sayHello()方法返回時得到hello的值,期間業務請求線程被阻塞。

二、關鍵類介紹

2.1 RPC請求消息封裝(Request)

dubbo的交換層定義了RPC請求的封裝類Request,它包含了一個RPC請求所具備的關鍵信息。

public class Request {

    //請求ID生成器,AtomicLong.inc實現
    private static final AtomicLong INVOKE_ID = new AtomicLong(0);

    //RPC調用的請求ID,在單個Client端內全局唯一
    private final long mId;
    
    //RPC請求響應消息協議版本
    private String mVersion;

    //是否為雙向請求響應
    private boolean mTwoWay = true;
    
    //實際RPC調用的請求數據,對應了Invocation類,調用參數都封裝在這里了
    private Object mData;

    //Request初始化時,生成請求ID
    public Request() {
        mId = newId();
    }

    //請求ID生成方法
    private static long newId() {
        // getAndIncrement()增長到MAX_VALUE時,再增長會變為MIN_VALUE,負數也可以做為ID
        return INVOKE_ID.getAndIncrement();
    }
}

2.2 RPC響應消息封裝(Response)

dubbo的交換層定義了RPC響應的封裝類Response,它包含了一個RPC響應所具備的關鍵信息。

public class Response {

    /**
     * ok.
     */
    public static final byte OK = 20;

    /**
     * clien side timeout.
     */
    public static final byte CLIENT_TIMEOUT = 30;

    /**
     * server side timeout.
     */
    public static final byte SERVER_TIMEOUT = 31;

     // ...省略一些狀態碼...
    
    //RPC調用的請求ID,默認為0,從Request中獲取
    private long mId = 0;

    //RPC請求響應消息協議版本
    private String mVersion;

    //響應狀態碼,默認OK,出現異常時重新設置
    private byte mStatus = OK;

    //響應錯誤信息
    private String mErrorMsg;
    
    //實際RPC調用的響應數據,對應實際實現類的方法執行結果
    private Object mResult;
}

2.3 RPC調用Future接口(ResponseFuture)

dubbo的交換層定義了RPC調用的響應Future接口ResponseFuture,它封裝了請求響應模式,例如提供了將異步網絡通信轉換成同步RPC調用的關鍵方法Object get(int timeoutInMillis)

public interface ResponseFuture {

    /**
     * 獲取RPC遠程執行結果,異步IO轉同步RPC的關鍵方法
     */
    Object get() throws RemotingException;

    /**
     * 獲取RPC遠程執行結果,異步IO轉同步RPC的關鍵方法
     */
    Object get(int timeoutInMillis) throws RemotingException;

    /**
     * set callback. 響應回調模式
     */
    void setCallback(ResponseCallback callback);

    /**
     * RPC調用是否完成
     */
    boolean isDone();

}

三、DefaultFuture實現

DefaultFutureResponseFuture接口的實現類,具體實現了接口定義的方法。

3.1 請求響應信息的承載

  • DefaultFuture提供構造方法,在HeaderExchangeChannel.request()中被調用,用于構建RPC調用Future并返回給調用方使用;
  • DefaultFuture中包含以下關鍵屬性,用于承載請求響應信息;
public class DefaultFuture implements ResponseFuture {
    
    //<請求ID,消息通道> 的映射關系
    private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>();
    
    //<請求ID,未完成狀態的RPC請求> 的映射關系
    private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>();

    //RPC調用的請求ID,構造器中從Request獲取
    private final long id;

    //消息通道,構造器傳入
    private final Channel channel;

    //RPC請求消息,構造器傳入
    private final Request request;

    //RPC響應消息
    private volatile Response response;

    //RPC響應回調器
    private volatile ResponseCallback callback;

    //構造器
    public DefaultFuture(Channel channel, Request request, int timeout) {
        this.channel = channel;
        this.request = request;
        this.id = request.getId();
        this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        // put into waiting map.
        FUTURES.put(id, this);
        CHANNELS.put(id, channel);
    }
}

3.2 RPC執行超時檢測

  • DefaultFuture定義了RPC執行超時時間timeout和RPC執行開始時間start,他們都在DefaultFuture構建時被初始化;
  • DefaultFuture中啟動了一個后臺守護線程,用于周期性執行 “RPC超時檢測任務RemotingInvocationTimeoutScan。該任務不斷檢測 “未完成狀態的RPC請求FUTURE” 中哪些已經超時(通過比較System.currentTimeMillis() - starttimeout
  • 對已超時的RPC請求,構建相應的超時響應Response并觸發received()方法。
public class DefaultFuture implements ResponseFuture {

    //RPC超時輪詢線程,不斷輪詢超時狀態的FUTURES,主動移除并返回超時結果
    static {
        Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
        th.setDaemon(true);
        th.start();
    }

    //RPC執行超時時間,構造器傳入,默認值1s
    private final int timeout;
    
    //DefaultFuture構建時間
    private final long start = System.currentTimeMillis();

}

3.3 異步網絡通信轉同步RPC調用

  • DefaultFuture實現了ResponseFuture接口的重要方法get(int timeout),用于同步等待RPC執行結果的返回(成功/異常/超時);
  • DefaultFuture提供了靜態方法received(Channel channel, Response response),用于對客戶端收到的RPC執行響應Response進行處理。處理邏輯就是講響應結果放入response并通知在done上組織等待的業務線程,同時通知本次RPC請求綁定的回調器Callback
public class DefaultFuture implements ResponseFuture {

    //響應消息處理互斥鎖,get()、doReceived()、setCallback()方法中使用
    private final Lock lock = new ReentrantLock();
    //請求響應模式Condition,通過get()中的await和doReceived()中的signal完成IO異步轉RPC同步
    private final Condition done = lock.newCondition();

    //RPC響應消息接收方法
    public static void received(Channel channel, Response response) {
        try {
            DefaultFuture future = FUTURES.remove(response.getId());
            if (future != null) {
                future.doReceived(response);
            } else {
                logger.warn("The timeout response finally returned at "
                        + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
                        + ", response " + response
                        + (channel == null ? "" : ", channel: " + channel.getLocalAddress()
                        + " -> " + channel.getRemoteAddress()));
            }
        } finally {
            CHANNELS.remove(response.getId());
        }
    }
    //將響應結果放入response,通知在done上等待的業務線程,并執行invokeCallback方法觸發所有綁定的Callbask執行
    private void doReceived(Response res) {
        lock.lock();
        try {
            response = res;
            if (done != null) {
                done.signal();
            }
        } finally {
            lock.unlock();
        }
        if (callback != null) {
            invokeCallback(callback);
        }
    }

    //RPC執行結果同步獲取方法,RPC的同步請求模式就依賴此方法完成,依賴done.await同步等待RPC執行結果
    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
                while (!isDone()) {
                    done.await(timeout, TimeUnit.MILLISECONDS);
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }
        return returnFromResponse();
    }

}

3.4 異步網絡通信轉同步RPC調用的兩個關鍵點

a)發起RPC調用請求的業務線程,是如何同步阻塞等待直到RPC響應返回的?

  • 業務請求線程調用HeaderExchangeClient.request()方法發送RPC請求消息到網絡,然后直接調用DefaultFuture.get()方法阻塞等待RPC執行結果;
  • get()阻塞等待的本質:循環檢測Response結果是否被設置成功,如果不成功使用Condition.await()阻塞直到結果返回;
  • NettyClient接收到RPC響應消息時,會調用DefaultFuture.received()方法,該方法中觸發了Condition.signal()通知業務請求線程解除阻塞等待狀態;

b)對于全雙工的網絡通信,在多線程并發請求響應的情況下,如果找到RPC響應Response對應的RPC請求Request?

  • 對于不同的服務消費者客戶端,請求響應自然與其網絡通道Channel綁定,不會存在消費者A接收到消費者B的RPC響應的情況;
  • 對于同一服務消費者客戶端,在RPC請求Request構建時生成并攜帶全局唯一自增ID,RPC響應Response會攜帶該ID返回。消費者客戶端只需維護 “唯一ID與RPC請求的關系Map<Long, DefaultFuture> FUTURES”即可定位RPC響應對應的RPC調用上下文;

四、調用入口流程

dubbo剖析:五 請求發送與接收 中講到,服務引用方調用dubbo的代理對象發起RPC請求時,最終會執行到DubboInvoker.doInvoke()方法:

    protected Result doInvoke(final Invocation invocation) throws Throwable {
        
        //入參構建及獲取ExchangeClient
        RpcInvocation inv = (RpcInvocation) invocation;
        ExchangeClient currentClient;
        if (clients.length == 1) {
            currentClient = clients[0];
        } else {
            currentClient = clients[index.getAndIncrement() % clients.length];
        }

        try {
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
            //case1. 異步,無返回值
            if (isOneway) { 
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                RpcContext.getContext().setFuture(null);
                return new RpcResult();
            } 
            //case2. 異步,有返回值
            else if (isAsync) {   
                ResponseFuture future = currentClient.request(inv, timeout);
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
                return new RpcResult();
            } 
            //case3. 異步轉同步(默認的通信方式)
            else {   
                RpcContext.getContext().setFuture(null);
                return (Result) currentClient.request(inv, timeout).get();
            }
        } catch (TimeoutException e) {
            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        } catch (RemotingException e) {
            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }
  • case1.異步&無返回值:直接調用ExchangeClient.send()方法,不需要請求響應模式的封裝;
  • case2.異步&有返回值:調用ExchangeClient.request()方法后,將ResponseFuture放入RPC請求上下文中,直接返回;
  • case3.異步轉同步:調用ExchangeClient.request()方法后,直接繼續調用ResponseFuture.get()同步等待獲取RPC執行結果;

五、服務引用方請求響應模型總結

服務引用方請求響應模型總結
  • 藍色代表“發送RPC請求”過程,由業務請求線程執行,通過NettyChannel將請求數據放入Netty的IO任務隊列后,構建ResponseFuture并返回。此時RPC請求發送及響應接收并未真正完成;
  • 紫色是基于Netty的網絡消息收發過程,通過當前網絡通道綁定的NioEventLoop線程輪詢完成;
  • 橙色代表“接收RPC響應”過程,該過程在 Dubbo業務線程池 中執行,處理RPC響應消息并交由ResponseFuture觸發接收響應的邏輯;
  • 綠色代表“獲取RPC調用結果”過程,由業務請求線程執行,阻塞直到從ResponseFuture中獲取到RPC響應結果;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容