RPC 的原理和簡單使用


RPC 的原理和簡單使用

RPC 的概念

RPC,Remote Procedure Call ,翻譯成中文就是遠程過程調用,是一種進程間通信方式。它允許程序調用另一個地址空間(通常是共享網絡的另一台機器上)的過程或函數。在調用的過程中,不用程序員顯式編碼這個遠程調用的細節。即無論是調用本地的接口/服務還是遠程的接口/服務,本質上編寫的調用代碼基本相同。

說起 RPC,就不能不提到分布式,這個促使RPC誕生的領域。

假設你有一個計算器接口,Calculator 模塊,以及它的實現類 CalculatorImpl。那么在系統還是單體應用時,你要調用 Calculator 的 add 方法來執行一個加運算,直接實例化一個 CalculatorImpl 對象,然后調用 add 方法就行了。這其實就是非常普通的本地函數調用,因為在同一個地址空間,或者說在同一塊內存,所以可以直接實現。這也是我們一直以來的程序調用方式,本地調用。

img

現在,基於高性能和高可靠等因素的考慮,我們決定將系統改造為分布式應用系統。將很多可以共享的功能都單獨拎出來,比如上面說到的計算器,你單獨把它放到一個服務里頭,讓別的服務去調用它。

img

這下可難辦了:服務 A 里並沒有 CalculatorImpl 這個類,那它要怎樣調用服務 B 的 CalculatorImpl 的 add 方法呢?

你可能會說,可以模仿 B/S 架構的調用方式,在 B 服務暴露一個 Restful 接口,然后 A 服務通過調用這個 Restful 接口來間接調用 CalculatorImpl 的 add 方法。

很好,這已經很接近 RPC 了。不過如果是這樣,那每次調用時,都需要寫一串發起 http 請求的代碼,比如

res = requests.get("URL")

但是,兩個問題:

  1. http 協議較為復雜,效率低,相對笨重
  2. 調用方式不像本地調用簡單方便,無法做到讓調用者感知不到遠程調用的邏輯

RPC 的實現

RPC 的原理

實際情況下,RPC 很少用 http 協議來進行數據傳輸。畢竟只是想傳輸一下數據,何必動用到一個文本傳輸的應用層協議呢。一般我們會選擇直接傳輸二進制數據。

不管你用何種協議進行數據傳輸,一個完整的 RPC 過程,都可以用下面這張圖來描述:

img

以左邊的 Client 端為例,Application 就是 RPC 的調用方,Client Stub 就是我們上面說到的代理對象,也就是那個看起來像是 Calculator 的實現類。其實內部是通過 RPC 方式來進行遠程調用的代理對象。至於 Client Run-time Library,則是實現遠程調用的工具包,比如 Python 的 socket 模塊。最后通過底層網絡實現實現數據的傳輸。

這個過程中最重要的就是序列化反序列化,因為傳輸的數據包必須是二進制的。直接丟一個 Python 對象過去,人家也不認識。我們必須把 Python 對象序列化為二進制格式,傳給 Server 端。Server 端接收到之后,再反序列化為 Python 對象。

Python 實現 RPC

Python 實現 RPC 需要使用 rpyc 模塊。首先當然是安裝模塊:

pip3 install rpyc -i https://pypi.douban.com/simple

安裝好之后,我們就可以使用 rpyc,很容易地搭建起 Python 版本的 RPC 客戶端和服務端了。

客戶端 client.py 的代碼為:

import rpyc

# 參數主要是host, port
conn = rpyc.connect('localhost', 9999)
# test是服務端的那個以"exposed_"開頭的方法
print('start')
while 1:
    try:
        num = int(input('請輸入一個數字[任意非數字退出]:'))
        cResult = conn.root.cal(num)    # 這一句是客戶端的精華,調用服務端的函數
        print(cResult)
    except Exception:
        break
print('end')

conn.close()

服務端 server.py 的代碼為:

from rpyc import Service
from rpyc.utils.server import ThreadedServer

class TestService(Service):

    # 對於服務端來說, 只有以"exposed_"打頭的方法才能被客戶端調用,所以要提供給客戶端的方法都得加"exposed_"
    def exposed_cal(self, num):
        return num * 2

sr = ThreadedServer(TestService, port=9999, auto_register=False)
sr.start()

上面的代碼執行效果為:

rpcpythonrpycdemo

gRPC 框架

目前流行的開源 RPC 框架還是比較多的,比如阿里巴巴的 Dubbo、Facebook 的 Thrift、Google 的 gRPC、Twitter 的 Finagle 等。

  • gRPC 是 Google 公布的開源軟件,基於最新的 HTTP 2.0 協議,並支持常見的眾多編程語言。RPC 框架是基於 HTTP 協議實現的,底層使用到了 Netty 框架的支持。
  • Thrift 是 Facebook 的開源 RPC 框架,主要是一個跨語言的服務開發框架。用戶只要在其之上進行二次開發就行,應用對於底層的 RPC 通訊等都是透明的。不過這個對於用戶來說需要學習特定領域語言這個特性,還是有一定成本的。
  • Dubbo 是阿里集團開源的一個極為出名的 RPC 框架,在很多互聯網公司和企業應用中廣泛使用。協議和序列化框架都可以插拔是極其鮮明的特色。

接下來我們以使用較為廣泛的 gRPC 為例學習下 RPC 框架的使用。

gRPC 是 Google 開放的一款 RPC (Remote Procedure Call) 框架,建立在 HTTP2 之上,使用 Protocol Buffers。

protocol buffers 是 Google 公司開發的一種數據描述語言,采用簡單的二進制格式,比 XML、JSON 格式體積更小,編解碼效率更高。用於數據存儲、通信協議等方面。

通過一個 .proto 文件,你可以定義你的數據的結構,並生成基於各種語言的代碼。目前支持的語言很多,有 Python、golang、js、java 等等。

有了 protocol buffers 之后,Google 進一步推出了 gRPC。通過 gRPC,我們可以在 .proto 文件中也一並定義好 service,讓遠端使用的 client 可以如同調用本地的 library 一樣使用。

img

基於這個原理,我們甚至可以實現跨語言的方法調用。比如上面的圖片中,gRPC Server 是由 C++ 寫的,Client 則分別是 Java 以及 Ruby,Server 跟 Client 端則是通過 protocol buffers 來信息傳遞。

接下來,我們按照下面的流程,搭建一個 gRPC 模型。

  1. 安裝 grpc 模塊:

    pip3 install grpcio grpcio-tools -i https://pypi.douban.com/simple
    
  2. 定義功能函數 calculate.py,示例中的是用來計算給定數字的平方根:

    import math
    
    # 求平方
    def square(x):
        return math.sqrt(x)
    
  3. 創建 calculate.proto 文件,在這里描述我們要使用的 message 以及 service:

    syntax = "proto3";
    
    message Number {
        float value = 1;
    }
    
    service Calculate {
        rpc Square(Number) returns (Number) {}
    }
    
  4. 生成 gRPC 類。這部分可能是整個過程中最“黑盒子”的部分,我們將使用特殊工具自動生成類。在當前目錄下執行下面的命令:

    python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. calculate.proto
    

    你會看到生成了兩個文件:

    • calculate_pb2.py 包含 message 信息(calculate_pb2.Number
    • calculate_pb2_grpc.py 包含 server(calculate_pb2_grpc.CalculatorServicer)和 client(calculate_pb2_grpc.CalculatorStub
  5. 創建 gRPC 服務端:

    import grpc
    import calculate_pb2
    import calculate_pb2_grpc
    import calculate
    from concurrent import futures
    import time
    
    
    # 創建一個 CalculateServicer 繼承自 calculate_pb2_grpc.CalculateServicer
    class CalculateServicer(calculate_pb2_grpc.CalculateServicer):
        def Square(self, request, context):
            response = calculate_pb2.Number()
            response.value = calculate.square(request.value)    # 在這里進行計算
            return response
    
    
    # 創建一個 gRPC server
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    # 利用 add_CalculateServicer_to_server 這個方法把上面定義的 CalculateServicer 加到 server 中
    calculate_pb2_grpc.add_CalculateServicer_to_server(CalculateServicer(), server)
    # 讓 server 跑在 port 50051 中
    print('Starting server. Listening on port 50051.')
    server.add_insecure_port('[::]:50051')
    server.start()
    
    # 因為 server.start() 不會阻塞,添加睡眠循環以持續服務
    try:
        while True:
            time.sleep(24 * 60 * 60)
    except KeyboardInterrupt:
        server.stop(0)
    
    
  6. 啟動 gRPC server 服務端:

    python server.py
    
    

    1582904482412

  7. 創建 gRPC 客戶端 client.py

    import grpc
    import calculate_pb2
    import calculate_pb2_grpc
    
    # 打開 gRPC channel,連接到 localhost:50051
    channel = grpc.insecure_channel('localhost:50051')
    # 創建一個 stub (gRPC client)
    stub = calculate_pb2_grpc.CalculateStub(channel)
    # 創建一個有效的請求消息 Number
    number = calculate_pb2.Number(value=int(input('請輸入一個數字:')))
    # 帶着 Number 去調用 Square
    response = stub.Square(number)
    print(response.value)
    
    
  8. 運行 gRPC 服務端:

    python client.py
    
    

    rpcpythongrpcdemo

  9. 最終的文件結構:

    img

總結

RPC 主要用於公司內部的服務調用,性能消耗低,傳輸效率高,實現復雜。

HTTP 主要用於對外的異構環境,瀏覽器接口調用,App 接口調用,第三方接口調用等。

RPC 適用場景(大型的網站,內部子系統較多、接口非常多的情況下適合使用 RPC):

  • 長鏈接。不必每次通信都要像 HTTP 一樣去 3 次握手,減少了網絡開銷。
  • 注冊發布機制。RPC 框架一般都有注冊中心,有豐富的監控管理。發布、下線接口、動態擴展等,對調用方來說是無感知、統一化的操作。
  • 安全性,沒有暴露資源操作。
  • 微服務支持。就是最近流行的服務化架構、服務化治理,RPC 框架是一個強力的支撐。

RPC 沒那么簡單

要實現一個 RPC 不算難,難的是實現一個高性能高可靠的 RPC 框架。

比如,既然是分布式了,那么一個服務可能有多個實例,你在調用時,要如何獲取這些實例的地址呢?

這時候就需要一個服務注冊中心,比如在 Dubbo 里頭,就可以使用 Zookeeper 作為注冊中心。在調用時,從 Zookeeper 獲取服務的實例列表,再從中選擇一個進行調用。

那么選哪個調用好呢?這時候就需要負載均衡了,於是你又得考慮如何實現復雜均衡,比如 Dubbo 就提供了好幾種負載均衡策略。

這還沒完,總不能每次調用時都去注冊中心查詢實例列表吧,這樣效率多低呀,於是又有了緩存,有了緩存,就要考慮緩存的更新問題,blablabla……

你以為就這樣結束了,沒呢,還有這些:

  • 客戶端總不能每次調用完都干等着服務端返回數據吧,於是就要支持異步調用;
  • 服務端的接口修改了,老的接口還有人在用,怎么辦?總不能讓他們都改了吧?這就需要版本控制了;
  • 服務端總不能每次接到請求都馬上啟動一個線程去處理吧?於是就需要線程池;
  • 服務端關閉時,還沒處理完的請求怎么辦?是直接結束呢,還是等全部請求處理完再關閉呢?
  • ……

如此種種,都是一個優秀的 RPC 框架需要考慮的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM