RPC 的原理和簡單使用
RPC 的概念
RPC,Remote Procedure Call ,翻譯成中文就是遠程過程調用,是一種進程間通信方式。它允許程序調用另一個地址空間(通常是共享網絡的另一台機器上)的過程或函數。在調用的過程中,不用程序員顯式編碼這個遠程調用的細節。即無論是調用本地的接口/服務還是遠程的接口/服務,本質上編寫的調用代碼基本相同。
說起 RPC,就不能不提到分布式,這個促使RPC誕生的領域。
假設你有一個計算器接口,Calculator 模塊,以及它的實現類 CalculatorImpl。那么在系統還是單體應用時,你要調用 Calculator 的 add 方法來執行一個加運算,直接實例化一個 CalculatorImpl 對象,然后調用 add 方法就行了。這其實就是非常普通的本地函數調用,因為在同一個地址空間,或者說在同一塊內存,所以可以直接實現。這也是我們一直以來的程序調用方式,本地調用。
現在,基於高性能和高可靠等因素的考慮,我們決定將系統改造為分布式應用系統。將很多可以共享的功能都單獨拎出來,比如上面說到的計算器,你單獨把它放到一個服務里頭,讓別的服務去調用它。
這下可難辦了:服務 A 里並沒有 CalculatorImpl 這個類,那它要怎樣調用服務 B 的 CalculatorImpl 的 add 方法呢?
你可能會說,可以模仿 B/S 架構的調用方式,在 B 服務暴露一個 Restful 接口,然后 A 服務通過調用這個 Restful 接口來間接調用 CalculatorImpl 的 add 方法。
很好,這已經很接近 RPC 了。不過如果是這樣,那每次調用時,都需要寫一串發起 http 請求的代碼,比如
res = requests.get("URL")
但是,兩個問題:
- http 協議較為復雜,效率低,相對笨重
- 調用方式不像本地調用簡單方便,無法做到讓調用者感知不到遠程調用的邏輯
RPC 的實現
RPC 的原理
實際情況下,RPC 很少用 http 協議來進行數據傳輸。畢竟只是想傳輸一下數據,何必動用到一個文本傳輸的應用層協議呢。一般我們會選擇直接傳輸二進制數據。
不管你用何種協議進行數據傳輸,一個完整的 RPC 過程,都可以用下面這張圖來描述:
以左邊的 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()
上面的代碼執行效果為:
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 一樣使用。
基於這個原理,我們甚至可以實現跨語言的方法調用。比如上面的圖片中,gRPC Server 是由 C++ 寫的,Client 則分別是 Java 以及 Ruby,Server 跟 Client 端則是通過 protocol buffers 來信息傳遞。
接下來,我們按照下面的流程,搭建一個 gRPC 模型。
-
安裝 grpc 模塊:
pip3 install grpcio grpcio-tools -i https://pypi.douban.com/simple
-
定義功能函數
calculate.py
,示例中的是用來計算給定數字的平方根:import math # 求平方 def square(x): return math.sqrt(x)
-
創建 calculate.proto 文件,在這里描述我們要使用的 message 以及 service:
syntax = "proto3"; message Number { float value = 1; } service Calculate { rpc Square(Number) returns (Number) {} }
-
生成 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
)
-
創建 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)
-
啟動 gRPC server 服務端:
python server.py
-
創建 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)
-
運行 gRPC 服務端:
python client.py
-
最終的文件結構:
總結
RPC 主要用於公司內部的服務調用,性能消耗低,傳輸效率高,實現復雜。
HTTP 主要用於對外的異構環境,瀏覽器接口調用,App 接口調用,第三方接口調用等。
RPC 適用場景(大型的網站,內部子系統較多、接口非常多的情況下適合使用 RPC):
- 長鏈接。不必每次通信都要像 HTTP 一樣去 3 次握手,減少了網絡開銷。
- 注冊發布機制。RPC 框架一般都有注冊中心,有豐富的監控管理。發布、下線接口、動態擴展等,對調用方來說是無感知、統一化的操作。
- 安全性,沒有暴露資源操作。
- 微服務支持。就是最近流行的服務化架構、服務化治理,RPC 框架是一個強力的支撐。
RPC 沒那么簡單
要實現一個 RPC 不算難,難的是實現一個高性能高可靠的 RPC 框架。
比如,既然是分布式了,那么一個服務可能有多個實例,你在調用時,要如何獲取這些實例的地址呢?
這時候就需要一個服務注冊中心,比如在 Dubbo 里頭,就可以使用 Zookeeper 作為注冊中心。在調用時,從 Zookeeper 獲取服務的實例列表,再從中選擇一個進行調用。
那么選哪個調用好呢?這時候就需要負載均衡了,於是你又得考慮如何實現復雜均衡,比如 Dubbo 就提供了好幾種負載均衡策略。
這還沒完,總不能每次調用時都去注冊中心查詢實例列表吧,這樣效率多低呀,於是又有了緩存,有了緩存,就要考慮緩存的更新問題,blablabla……
你以為就這樣結束了,沒呢,還有這些:
- 客戶端總不能每次調用完都干等着服務端返回數據吧,於是就要支持異步調用;
- 服務端的接口修改了,老的接口還有人在用,怎么辦?總不能讓他們都改了吧?這就需要版本控制了;
- 服務端總不能每次接到請求都馬上啟動一個線程去處理吧?於是就需要線程池;
- 服務端關閉時,還沒處理完的請求怎么辦?是直接結束呢,還是等全部請求處理完再關閉呢?
- ……
如此種種,都是一個優秀的 RPC 框架需要考慮的問題。