Dubbo源碼-網絡通信之提供者消費者通信

更多請移步我的博客

引入

之前簡單看過Dubbo基於SPI的“微核+插件”形式的架構模式,Dubbo因爲這種架構模式使得擴展十分簡單,另外Dubbo的框架分層十分清晰,看起源碼來相對輕鬆不少。

在使用Dubbo時突然想到幾個問題,Dubbo默認使用tcp長鏈接,消費者們可能同時發起調用,提供者是怎樣處理這些請求的?消費者和生產者之間鏈接如何複用?消費者和提供者之間幾個長鏈接?要搞清楚這幾個問題要從Dubbo的exchange和transport層來找答案。

  • exchange 信息交換層:封裝請求響應模式,同步轉異步,以 Request, Response 爲中心,擴展接口爲 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 網絡傳輸層:抽象 mina 和 netty 爲統一接口,以 Message 爲中心,擴展接口爲 Channel, Transporter, Client, Server, Codec

Dubbo在這兩層提供了不同的實現,信息交換層默認的是自定義協議dubbo,傳輸層默認使用Netty。下面的一些探討以Dubbo的默認配置爲基礎進行,可能因爲Dubbo版本不同其中一些類名或者方法名稱不一致,但不影響對其本身的理解。可藉由具體實現更好的理解Dubbo的抽象分層。

網絡通信模型

dubbo交互通訊

  • Client表示應用具體的某個服務引用,每個服務的引用可根據提供者的配置(connections)建立多個鏈接。
    Dubbo協議缺省每服務每提供者每消費者使用單一長連接。

  • 在NettyServer中區分Boss和Woker的角色,Boss負責建立新鏈接,Woker負責鏈接建立後的IO工作,Woker的數量可由配置指定。

  • Dispatcher部分Dubbo按照不同場景提供了幾種不同的實現,也可根據自身業務特點進行擴展。

    1. all(默認) 所有消息都派發到線程池,包括請求,響應,連接事件,斷開事件,心跳等。
    2. direct 所有消息都不派發到線程池,全部在IO線程上直接執行。
    3. message 只有請求響應消息派發到線程池,其它連接斷開事件,心跳等消息,直接在IO線程上執行。
    4. execution 只請求消息派發到線程池,不含響應,響應和其它連接斷開事件,心跳等消息,直接在IO線程上執行。
    5. connection 在IO線程上,將連接斷開事件放入隊列,有序逐個執行,其它消息派發到線程池。

    上面提到的線程池指執行業務調用的線程池,此線程池可自行配置,默認爲FixedThreadPool,大小爲200,拒絕策略爲AbortPolicyWithReport,隊列爲SynchronousQueue(不是很瞭解,後面再深入瞭解下)。上圖中從ChannelEventRunner之後進入線程池。

服務啓動

Dubbo在解析service標籤時會向外暴露服務,同時會檢查打開網絡服務。下圖是dubbo服務暴露的時序圖(摘自dubbo官網)。

dubbo-export

在export階段,會調用相應協議的export方法去完成invoker到exporter轉化同時根據url(如下)中的address打開本地網絡通信服務,默認啓動netty服務。

URL內容

dubbo://10.1.87.93:20880/com.xx.IAdminUserLoginService?accesslog=/Users/childe/logs/access.log&anyhost=true&application=dmall-provider&connections=4&default.delay=-1&default.retries=0&default.service.filter=-monitor,-exception&default.timeout=10000&delay=-1&dubbo=2.5.9&generic=false&interface=com.xx.IAdminUserLoginService&loadbalance=roundrobin&logger=slf4j&methods=checkUser,getUserById,getUserByAccount&monitor=dubbo%3A%2F%2Fzk1.daily.com%3A2181%2Fcom.alibaba.dubbo.registry.RegistryService%3Fapplication%3Ddmall-provider%26backup%3Dzk2.daily.com%3A2181%2Czk3.daily.com%3A2181%26dubbo%3D2.5.9%26file%3D%2FUsers%2Fchilde%2F.dubbo%2FDDD-soa.cache%26logger%3Dslf4j%26owner%3Dmazha%26pid%3D21874%26protocol%3Dregistry%26refer%3Ddubbo%253D2.5.9%2526interface%253Dcom.alibaba.dubbo.monitor.MonitorService%2526pid%253D21874%2526timestamp%253D1528104644722%26registry%3Dzookeeper%26timestamp%3D1528104644709&owner=childe&pid=21874&revision=1.0.28&side=provider&timestamp=1528104644712&uptime=1528104644716&version=1.0.0.fsm.chen

啓動netty服務的代碼如下,分別有boss和worker線程池供netty來完成新鏈接的建立及網絡IO。在創建NioServerSocketChannelFactory時我們可以通過Dubbo的配置(dubbo:protocol 中iothreads)來指定IO的線程數量。默認數量爲CPU可用核數加1,但不能超過32個。

//NettyServer
protected void doOpen() throws Throwable {
    NettyHelper.setNettyLoggerFactory();
    ExecutorService boss = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerBoss", true));
    ExecutorService worker = Executors.newCachedThreadPool(new NamedThreadFactory("NettyServerWorker", true));
    ChannelFactory channelFactory = new NioServerSocketChannelFactory(boss, worker, getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS));
    bootstrap = new ServerBootstrap(channelFactory);

    //Dubbo採用組合的形式來組織自身Handler,在此部分斷點可清楚的看到其最終組合出來的handler都包含了哪些
    //NettyHandler->NettyServer(本身實現了Channelhandler接口)->MultiMessageHandler
    //->HeartbeatHandler->AllChannelhandler(根據選擇的Dispatcher方式決定)->DecodeHandler
    //->HeaderExchangeHandler->DubboProtocol$XX(內部匿名實現了Channelhandler)

    final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
    channels = nettyHandler.getChannels();
    bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
        public ChannelPipeline getPipeline() {
            NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec() ,getUrl(), NettyServer.this);
            ChannelPipeline pipeline = Channels.pipeline();
            pipeline.addLast("decoder", adapter.getDecoder());
            pipeline.addLast("encoder", adapter.getEncoder());
            pipeline.addLast("handler", nettyHandler);
            return pipeline;
        }
    });
    // bind
    channel = bootstrap.bind(getBindAddress());
}

Provider完成一次業務請求操作流程如下: 接收數據->AllChannelHandler->ChannelEventRunner->DecodeHandler->HeaderExchangeHandler->DubboProtocol::reply->Invoker::invoke->Impl

除了IO及具體業務的執行過程,一次處理最重要的就是數據的交換啦,即:請求數據根據協議轉成對應的數據結構,業務相應數據也要根據協議進行轉換。

下面這段代碼起的便是承上啓下的作用,在這個步驟中,根據Request的信息構造出Response,並調用具體協議的reply實現獲取業務相應數據。

在構造Response時回寫入了Request的ID和Version。這是channel複用的關鍵。每個channel的Request都有一個唯一的ID,類型爲long。

//HeaderExchangeHandler
Response handleRequest(ExchangeChannel channel, Request req) throws RemotingException {
    Response res = new Response(req.getId(), req.getVersion());
    if (req.isBroken()) {
        Object data = req.getData();

        String msg;
        if (data == null) msg = null;
        else if (data instanceof Throwable) msg = StringUtils.toString((Throwable) data);
        else msg = data.toString();
        res.setErrorMessage("Fail to decode request due to: " + msg);
        res.setStatus(Response.BAD_REQUEST);

        return res;
    }
    // find handler by message class.
    Object msg = req.getData();
    try {
        // handle data.
        Object result = handler.reply(channel, msg);
        res.setStatus(Response.OK);
        res.setResult(result);
    } catch (Throwable e) {
        res.setStatus(Response.SERVICE_ERROR);
        res.setErrorMessage(StringUtils.toString(e));
    }
    return res;
}

服務引用

Consumer啓動時會從註冊中心拉取訂閱的Provider信息,並建立和Provider的鏈接,時序圖如下(摘自dubbo官網):
dubbo-refer

Consumer的一次調用時序: DubboInvoker::doInvoke->NettyClient->AbstractClient::send->AbstractPeer->HeaderExchangeChannel::send->DefaultFuture::send->NettyChannel::send->DefaultFuture::received

Consumer每發起一次調用時會構建出本次調用的Request,每個Request有唯一的ID。Dubbo中用AtomicLong實現ID,短時間內不會重複,所以可作爲唯一標識。

//HeaderExchangeChannel
public ResponseFuture request(Object request, int timeout) throws RemotingException {
    if (closed) {
        throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
    }
    // create request.
    // 無參構造方法中生成一個ID
    Request req = new Request();
    req.setVersion("2.0.0");
    req.setTwoWay(true);
    req.setData(request);
    DefaultFuture future = new DefaultFuture(channel, req, timeout);
    try {
        channel.send(req);
    } catch (RemotingException e) {
        future.cancel();
        throw e;
    }
    return future;
}
//Request

private static final AtomicLong INVOKE_ID = new AtomicLong(0);

public Request() {
    mId = newId();
}

private static long newId() {
    // getAndIncrement() When it grows to MAX_VALUE, it will grow to MIN_VALUE, and the negative can be used as ID
    return INVOKE_ID.getAndIncrement();
}

Consumer發起調用後,使用DefaultFuture將異步變成同步,等待Provider的返回。當Provider有數據寫回,我們將其轉換爲Response後,通過ID就可以知道通知哪個DefaultFuture來進行後續的處理了。這樣就完成了channel的複用。

//DefaultFuture
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());
    }
}

疑問回答

  1. 消費者和提供者之間長鏈接?
    缺省是長鏈接,也可以使用其他的(自定義)的通訊方式。

  2. 消費者和提供者之間幾個長鏈接?
    缺省每服務每提供者每消費者使用單一長連接,如果數據量較大,可以使用多個連接。

  3. 消費者和提供者之間連接如何複用?
    請求和應答消息使用同一個ID作爲唯一標識來區分同一個鏈接內的不同請求。

  4. 官網釋疑的幾個問題

    • 爲什麼要消費者比提供者個數多?
      因 dubbo 協議採用單一長連接,假設網絡爲千兆網卡,根據測試經驗數據每條連接最多隻能壓滿 7MByte(不同的環境可能不一樣,供參考,PS:1024Mbit=128MByte),理論上 1 個服務提供者需要 20 個服務消費者才能壓滿網卡。

    • 爲什麼dubbo協議不能傳大包?
      因 dubbo 協議採用單一長連接,如果每次請求的數據包大小爲 500KByte,假設網絡爲千兆網卡,每條連接最大 7MByte(不同的環境可能不一樣,供參考),單個服務提供者的 TPS(每秒處理事務數)最大爲:128MByte / 500KByte = 262。單個消費者調用單個服務提供者的 TPS(每秒處理事務數)最大爲:7MByte / 500KByte = 14。如果能接受,可以考慮使用,否則網絡將成爲瓶頸。

    • 爲什麼採用異步單一長連接?
      因爲服務的現狀大都是服務提供者少,通常只有幾臺機器,而服務的消費者多,可能整個網站都在訪問該服務,比如 Morgan 的提供者只有 6 臺提供者,卻有上百臺消費者,每天有 1.5 億次調用,如果採用常規的 hessian 服務,服務提供者很容易就被壓跨,通過單一連接,保證單一消費者不會壓死提供者,長連接,減少連接握手驗證等,並使用異步 IO,複用線程池,防止 C10K 問題。

參考及擴展鏈接

netty之boss-worker
C10K問題
Dubbo框架設計
dubbo協議

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章