簡述RPC原理實現

穆書偉發表於2018-10-10

簡述RPC原理實現

前言

架構的改變,往往是因為業務規模的擴張。

隨著業務規模的擴張,為了滿足業務對技術的要求,技術架構需要從單體應用架構升級到分散式服務架構,來降低公司的技術成本,更好的適應業務的發展。

分散式服務架構的諸多優勢,這裡就不一一列舉了,今天圍繞的話題是服務框架,為了推行服務化,必然需要一套易用的服務框架,來支撐業務技術架構升級。

服務框架

服務架構的核心是服務呼叫,分散式服務架構中的服務分佈在不同主機的不同程式上,服務的呼叫跟單體應用程式內方法呼叫的本質區別就是需要藉助網路來進行通訊。

RPC Demo實現思路

原作者樑飛,在此記錄下他非常簡潔的rpc實現思路。

核心框架類

/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the
 * confidential and proprietary information of Alibaba.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Alibaba.com.
 */
package com.alibaba.study.rpc.framework;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * RpcFramework
 * 
 * @author william.liangf
 */
public class RpcFramework {

    /**
     * 暴露服務
     * 
     * @param service 服務實現
     * @param port 服務埠
     * @throws Exception
     */
    public static void export(final Object service, int port) throws Exception {
        if (service == null)
            throw new IllegalArgumentException("service instance == null");
        if (port <= 0 || port > 65535)
            throw new IllegalArgumentException("Invalid port " + port);
        System.out.println("Export service " + service.getClass().getName() + " on port " + port);
        ServerSocket server = new ServerSocket(port);
        for(;;) {
            try {
                final Socket socket = server.accept();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            try {
                                ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                                try {
                                    String methodName = input.readUTF();
                                    Class<?>[] parameterTypes = (Class<?>[])input.readObject();
                                    Object[] arguments = (Object[])input.readObject();
                                    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                                    try {
                                        Method method = service.getClass().getMethod(methodName, parameterTypes);
                                        Object result = method.invoke(service, arguments);
                                        output.writeObject(result);
                                    } catch (Throwable t) {
                                        output.writeObject(t);
                                    } finally {
                                        output.close();
                                    }
                                } finally {
                                    input.close();
                                }
                            } finally {
                                socket.close();
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 引用服務
     * 
     * @param <T> 介面泛型
     * @param interfaceClass 介面型別
     * @param host 伺服器主機名
     * @param port 伺服器埠
     * @return 遠端服務
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception {
        if (interfaceClass == null)
            throw new IllegalArgumentException("Interface class == null");
        if (! interfaceClass.isInterface())
            throw new IllegalArgumentException("The " + interfaceClass.getName() + " must be interface class!");
        if (host == null || host.length() == 0)
            throw new IllegalArgumentException("Host == null!");
        if (port <= 0 || port > 65535)
            throw new IllegalArgumentException("Invalid port " + port);
        System.out.println("Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
                Socket socket = new Socket(host, port);
                try {
                    ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                    try {
                        output.writeUTF(method.getName());
                        output.writeObject(method.getParameterTypes());
                        output.writeObject(arguments);
                        ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
                        try {
                            Object result = input.readObject();
                            if (result instanceof Throwable) {
                                throw (Throwable) result;
                            }
                            return result;
                        } finally {
                            input.close();
                        }
                    } finally {
                        output.close();
                    }
                } finally {
                    socket.close();
                }
            }
        });
    }

}
複製程式碼

定義服務介面

/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the
 * confidential and proprietary information of Alibaba.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Alibaba.com.
 */
package com.alibaba.study.rpc.test;

/**
 * HelloService
 * 
 * @author william.liangf
 */
public interface HelloService {

    String hello(String name);

}
複製程式碼

實現服務

/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the
 * confidential and proprietary information of Alibaba.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Alibaba.com.
 */
package com.alibaba.study.rpc.test;

/**
 * HelloServiceImpl
 * 
 * @author william.liangf
 */
public class HelloServiceImpl implements HelloService {

    public String hello(String name) {
        return "Hello " + name;
    }

}
複製程式碼

暴露服務

/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the
 * confidential and proprietary information of Alibaba.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Alibaba.com.
 */
package com.alibaba.study.rpc.test;

import com.alibaba.study.rpc.framework.RpcFramework;

/**
 * RpcProvider
 * 
 * @author william.liangf
 */
public class RpcProvider {

    public static void main(String[] args) throws Exception {
        HelloService service = new HelloServiceImpl();
        RpcFramework.export(service, 1234);
    }

}
複製程式碼

引用服務

/*
 * Copyright 2011 Alibaba.com All right reserved. This software is the
 * confidential and proprietary information of Alibaba.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Alibaba.com.
 */
package com.alibaba.study.rpc.test;

import com.alibaba.study.rpc.framework.RpcFramework;

/**
 * RpcConsumer
 * 
 * @author william.liangf
 */
public class RpcConsumer {
    
    public static void main(String[] args) throws Exception {
        HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
        for (int i = 0; i < Integer.MAX_VALUE; i ++) {
            String hello = service.hello("World" + i);
            System.out.println(hello);
            Thread.sleep(1000);
        }
    }
    
}
複製程式碼

小結

樑飛大大的部落格使用原生的jdk api就展現給各位讀者一個生動形象的rpc demo,實在是強。

這個簡單的例子的實現思路是:

  • 使用阻塞的socket IO流來進行server和client的通訊,也就是rpc應用中服務提供方和服務消費方。並且是端對端的,用埠號來直接進行通訊
  • 方法的遠端呼叫使用的是jdk的動態代理
  • 引數的序列化也是使用的最簡單的objectStream

服務框架

服務框架的核心是服務呼叫,分散式服務架構中的服務分佈在不同主機的不同程式上,服務的呼叫跟單體應用程式內方法呼叫的本質區別就是需要藉助網路來進行通訊。

下圖是服務框架的架構圖,主流的服務框架的實現都是這套架構,如 Dubbo、SpringCloud 等。

簡述RPC原理實現

  • Invoker 是服務的呼叫方

  • Provider 是服務的提供方

  • Registry 是服務的註冊中心

  • Monitor 是服務的監控模組

Invoker 和 Provider 分別作為服務的呼叫和被呼叫方,這點很明確。

但是僅有這兩者還是不夠的,因為作為呼叫方需要知道服務部署在哪,去哪呼叫服務,所以有了 Registry 模組,它的功能是給服務提供方註冊服務,給服務呼叫方發現服務。

Monitor 作為服務的監控模組,負責服務的呼叫統計以及鏈路分析功能,也是服務治理重要的一環。

核心模組

下圖是服務框架的流程圖,我們分服務註冊、發現、呼叫三個方面來進行流程分解。

簡述RPC原理實現

服務註冊是服務提供方向註冊中心註冊服務資訊;當提供服務應用下線時,負責將服務註冊資訊從註冊中心刪去。

服務發現是服務呼叫方從註冊中心訂閱服務,獲取服務提供方的相關資訊;當服務註冊資訊有變更時,註冊中心負責通知到服務呼叫方。

服務呼叫是服務呼叫方通過從註冊中心拿到服務提供方的資訊,向服務提供方發起服務呼叫,獲取呼叫結果。

對照上述流程圖,我們按照請求的具體過程進行分析。

作為服務呼叫方 Invoker 的具體流程是:

  1. Request 從下往上,由於服務呼叫方只能拿到服務提供方提供的 API 介面或者 API 介面的 JAR 包,所以服務呼叫方需要經過一層代理 Proxy 來偽裝服務的實現;
  2. 經過代理 Proxy 之後,會經過路由 Router、負載均衡 LoadBalance 模組,目的是從一堆從註冊中心拿到的服務提供方資訊中選出最合適的服務提供方機器進行呼叫。另外,還會經過 Monitor 監控等模組;
  3. 接著會經過服務編碼 Codec 模組,這個模組的目的是因為請求在網路傳輸前需要按照通訊協議以及物件的序列化方式,對傳輸的請求進行編解碼;
  4. 最終會經過網路通訊 Transporter 模組,這個模組將 Codec 編碼好的請求進行傳輸。

作為服務提供方 Provider 的具體流程是:

  1. Request 從上往下,經過網路通訊 Transporter 模組,獲取到的是由呼叫方傳送的Request位元組陣列。

  2. 接著經過服務編碼 Codec 模組,根據通訊協議解出一個完整的請求包,然後使用具體的序列化方式反序列化成請求物件。

  3. 緊接著會經過監控、限流、鑑權等模組。

  4. 最終會執行服務的真正業務實現 ServiceImpl,執行完後,結果按原路返回。

按照上述流程分解一個服務框架的相關工作,再去看一些開源的服務框架也就不難理解了。

一般服務框架的核心模組應該有註冊中心、網路通訊、服務編碼(通訊協議、序列化)、服務路由、負載均衡,服務鑑權,可用性保障(服務降級、服務限流、服務隔離)、服務監控(Metrics、Trace)、配置中心、服務治理平臺等。

註冊中心

註冊中心是用來註冊和發現服務的,需要具備的基本功能有註冊服務、下線服務、發現服務、通知服務變更等。

當前使用比較多的開源註冊中心有 ZookeeperETCDEureka 等。

Zookeeper 與 ETCD 在整體架構上都比較類似,使用方式非常便捷,應用比較廣泛。

這兩套系統按照 CAP 理論,屬於 CP 系統,可用性會差一點,但是作為中小規模服務註冊中心,還是遊刃有餘,並沒有某些人說的那麼差勁。 Eureka 是 Spring Cloud Netflix 微服務套件中的一部分,很不幸的是 Eureka 2.0 開源工作宣告停止。

網路通訊

服務的呼叫方和提供方都來自不同的主機的不同的程式,所以要進行呼叫,必然少不了網路通訊。可以說網路通訊是分散式系統的重中之重,網路通訊框架的好壞直接影響服務框架的效能。從零實現一套效能高,穩定性強的通訊框架還是非常難的,好在目前已經有很多開源的高效能的網路通訊框架。 針對 Java 生態有 Mina、Netty 等,目前使用最廣泛的也當屬 Netty。Netty 使用的是 per thread one eventloop 執行緒模型,這點與 Nginx 等其他高效能網路框架類似。另外,Netty 非常易用,所以網路通訊選擇 Netty 框架自然是毫無疑問的。

服務編碼

記憶體物件要經過網路傳輸前需要做兩件事:第一是確定好通訊協議,第二序列化。

通訊協議

通訊協議說白了在傳送資料前按照一定的格式來處理資料,然後進行傳送,保證接收方拿到資料知道按照什麼樣的格式進行處理。

有些同學可能不理解,為什麼需要通訊協議,不是有 TCP、UDP 協議了嗎?這裡說的不是傳輸層的通訊協議,應該是應用層的協議類似 HTTP。

因為的 TCP 協議雖然已經保證了可靠有序的傳輸,但是如果沒有一套應用層的協議,就不知道發過來的位元組資料是不是一個完整的資料請求,或者說是多個請求的位元組資料都在一起,無法拆分,這就是是所謂的粘包,需要按照協議進行拆包,拆成一個個完整的請求包進行處理。

協議的實現上一般大廠或者開源的服務框架選擇自建協議,更偏向服務領域。如 Dubbo,當然也有些框架直接使用 HTTP,HTTP/2,比如 GRPC 使用的就是 HTTP/2。

序列化

由於向網路層傳送的資料必須是位元組資料,不可能直接將一個物件傳送到網路,所以在傳送物件資料前,一般需要將物件序列化成位元組資料,然後進行傳輸。

在服務方收到網路的位元組資料時,需要經過反序列化拿到相關的物件。

序列化的實現目前現成比較多,如 Hessian、JSON、Thrift、ProtoBuf 等。Thrift 和 ProtoBuf 能支援跨語言,效能比較好,不過使用時需要編寫 IDL 檔案,有點麻煩。Hessian、JSON 使用起來比較友好,但是效能會差一點。


服務路由

服務路由指的是向服務提供方發起呼叫時,需要根據一定的演算法從註冊中心拿到的服務方地址資訊中選擇其中的一批機器進行呼叫。

路由的演算法一般是根據場景來進行選擇的,比如有些公司實施兩地三中心這種高可用部署,但是由於兩地的網路延時比較大,那這時就可以實施同地區路由策略,比如上海的呼叫方請求會優先選擇上海的服務進行呼叫,來降低網路延時導致的服務端到端的呼叫耗時。

還有些框架支援指令碼配置來進行定向路由策略。


負載均衡

負載均衡是緊接著服務路由的模組,負載均衡負責將傳送請求均勻合理的傳送到服務提供方的節點上,而備選機器,一般就是經過路由模組選擇出來的。

負載均衡的演算法有很多,如 RoundRobin、Random、LeastActive、ConsistentHash 等。

而且這些演算法一般都是基於權重的增強版本,因為需要根據權重來調節每臺服務節點的流量。


服務鑑權

服務鑑權是服務安全呼叫的基礎,雖然絕大部分服務都是公司內部服務,但是對於敏感度較高的資料還是需要進行鑑權的。

鑑權的服務需要對服務的呼叫方進行授權,未經授權的呼叫方是不能夠呼叫該服務的。

關於服務鑑權的實現大都是基於 token 的認證方案,如 JWT(JSON Web Token) 認證。


可用性保障

可用性保障模組是服務高可用的一個重要保證。

服務在互動中主要分成呼叫方和提供方兩種角色,作為服務呼叫方,可以通過服務降級提升可用性。作為服務提供方,可以通過服務限流、服務隔離來保證可用性。

服務降級

服務降級指的是當依賴的服務不可用時,使用預設的值來替代服務呼叫。

試想一下,假設呼叫一個非關鍵路徑上的服務(也就是說該呼叫獲取的結果是否實時,是否正確不是特別重要)出現問題,導致呼叫超時、失敗等,在沒有降級措施的情況下,會直接應用服務呼叫方業務。

因此,有些非關鍵路徑上服務呼叫,可以通過服務降級實現有損服務,柔性可用。 開源的降級元件有 Netflix 的 Hystrix,Hystrix 使用比較廣泛。

服務限流

服務降級保護的是服務的呼叫方,也就是服務的依賴方。而服務的提供方呢,如何保證服務的可用性呢? 服務限流指的是對服務呼叫流量的限制,限制其呼叫頻次,來保護服務。

在高併發的場景中,很容易出現流量過高,導致服務被打垮。這裡就需要限流來保證服務自身的穩定執行。 Hystrix 也是可以用來限流的,但是用的比較多的有 guava 的 RateLimiter,其使用的是令牌桶演算法,能夠保證平滑限流。

服務隔離

除了服務限流對服務提供方進行保護,就夠了嗎? 可能還不夠,考慮一下這樣的場景,假設某一個有問題的方法出現問題,處理非常耗時,這樣會堵住整個服務處理執行緒,導致正常的服務方法也不能夠正常呼叫。因此還需要服務隔離。 服務隔離指的是對服務執行的方法進行執行緒池隔離,保證異常耗時方法不會對正常的方法呼叫產生干擾,進而保護服務的穩定執行,提升可用性。


服務監控

服務監控是高可用系統不可或缺的重要支撐。

服務監控不僅包括服務呼叫等業務統計資訊 Metrics,還包括分散式鏈路追蹤 Trace。

分散式系統監控比單體應用要複雜的多,需要將大量的監控資訊進行聚合展示,尤其是在分散式鏈路追蹤方面,由於服務呼叫過程中涉及到多個分佈在不同機器上的服務,需要一個呼叫鏈路展示系統方便檢視呼叫鏈路中耗時和出問題的環節。

Metrics

Metrics 監控主要是服務呼叫的一些統計報表,包括服務呼叫次數、成功數、失敗數,以及服務方法的呼叫耗時,如平均耗時,耗時99線,999線等。全方位展示服務的可用性以及效能等資訊。

目前開源的 Metrics 監控有美團點評的 Cat、SoundCloud 的 Prometheus 以及基於 OpenTracking 的 SkyWalking。

Trace

Trace 監控是對分散式服務呼叫過程中的整體鏈路展示和分析。方便檢視鏈路上各個環境的效能問題。

分散式鏈路追蹤的原理大都是基於 Google 的論文 Dapper, a Large-Scale Distributed Systems Tracing Infrastructure。 開源的分散式鏈路追蹤系統有美團點評的 Cat,基於 OpenTracking 的SkyWalking、Twitter 的 ZipKin。


配置中心

配置中心不光是常見的系統需要,服務框架也需要,它能夠對系統中使用的配置進行管理,也能夠針對修改配置動態通知到應用系統。 一套完善的服務框架,必然少不了配置,如一些動態開關、降級配置、限流配置、鑑權配置等。

開源的配置中心有阿里的 Diamond,攜程的 Apollo。


治理平臺

治理平臺指的是對服務進行管理的平臺。

微服務微了之後,必然會導致服務數量的上升,如果沒有一個完善的治理平臺,服務規模擴大之後,很難去維護,也必然導致故障頻頻,並且極度影響開發效率。

治理平臺主要是服務功能的相關操作平臺,包括服務權重修改、服務下線、鑑權降級等配置修改等。 治理平臺跟服務框架的耦合比較強,所以開源的比較少。

其他

關於RPC原理實現詳解到這裡就結束了。

原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!

版權宣告:

作者:穆書偉

部落格園出處:www.cnblogs.com/sanshengshu…

github出處:github.com/sanshengshu…

個人部落格出處:sanshengshui.github.io/

相關文章