隨著應用開發的推進,在眾多事項中,有一點我們較少擔憂:計算能力。由於雲服務供應商的出現,管理數據中心的憂慮減少了。所有資源都能在幾秒內按需獲取,這也導致了數據量的增加。大量數據在單一請求中通過多種媒介生成和傳輸。
數據量的增加帶來了序列化、反序列化及傳輸成本等額外活動。雖然我們不擔心計算資源,但延遲成為了負擔。我們需要減少傳輸。過去為此開發了許多消息協議。SOAP過於臃腫,而REST是簡化版,但我們需要更高效的框架。這正是遠程過程調用(RPC)的用武之地。
本文將探討RPC的定義及其多種實現,重點關注Google的RPC實現——gRPC。我們還將比較REST與RPC,並深入了解gRPC的各個方面,包括安全性、工具支持等。
什麼是RPC?
RPC代表遠程過程調用。其定義已體現在名稱中。過程調用即函數/方法調用,關鍵在於“遠程”二字。如果我們能夠遠程調用函數,那將如何?
簡而言之,若一個函數存於伺服器上,且需從客戶端觸發,是否能簡化至如同本地方法/函數調用?RPC(遠程過程調用)的本質即為客戶端營造一種錯覺,彷彿調用的是本地方法,實則是遠端機器上的方法,此過程巧妙地抽象了網絡層的任務。其美妙之處在於合約保持極度嚴謹與透明(後文將詳述)。
RPC調用涉及的步驟:
典型的REST處理流程如下:
而RPC則將此流程簡化至:
這是因為與請求相關的所有複雜性已被抽象化,我們無需操心(後續將於代碼生成部分討論),只需專注於數據與邏輯。
gRPC:其意義、原因及運作方式
至此,我們已探討了RPC,即遠程方法/函數調用,帶來諸如“嚴格的合約定義”、“抽象數據傳輸與轉換”、“降低延遲”等益處,後續將深入討論。接下來,我們將深入探究RPC的一種實現——gRPC,它基於RPC概念構建的框架。
RPC的多種實現包括:
-
gRPC(由Google開發)
-
Thrift(Facebook)
-
Finalge(Twitter)
Google的RPC版本稱為gRPC,於2015年推出,自此逐漸受到重視,成為微服務架構中最受青睞的通訊機制之一。
gRPC採用protocol buffers(一種開源訊息格式)作為客戶端與伺服器間的預設通訊方式,同時也以HTTP/2為預設協議。gRPC支援四種通訊類型:
-
Unary[典型的客戶端與伺服器通訊]
接著來看gRPC中廣泛使用的訊息格式——協定緩衝區,又稱為protobuf。一個protobuf訊息類似以下所示:
message Person { |
在此,‘Person’是我們希望傳輸的訊息(作為請求/回應的一部分),它包含字段‘name’(字串類型)、‘id’(字串類型)和‘email’(字串類型)。數字1、2、3代表資料在序列化為二進位格式時的位置(例如‘name’、‘id’和‘has_ponycopter’)。
開發者一旦創建了包含所有訊息的Protocol Buffer文件,便可使用協定緩衝區編譯器(一個二進制文件)來編譯該協定緩衝區文件,從而生成所有與訊息互動所需的實用類別和方法。例如,如所示,生成的程式碼(取決於所選語言)將呈現此種樣貌。
如何定義服務?
我們需定義利用上述訊息進行傳送/接收的服務。
在撰寫完必要的請求與回應訊息類型後,下一步是編寫服務本身。
gRPC服務同樣在Protocol Buffers中定義,並使用”service”和”RPC”關鍵字來定義服務。
請查看以下proto文件的內容:
message HelloRequest { message HelloResponse { service HelloService { |
在此,HelloRequest與HelloResponse是訊息,而HelloService則提供了一個名為SayHello的單向RPC,該RPC以HelloRequest作為輸入,並以HelloResponse作為輸出。
如前所述,HelloService 目前僅包含一個單一的 RPC。但它可以包含多個 RPC,並且這些 RPC 可以是多種類型(單一/客戶端串流/伺服器串流/雙向)。
要定義一個串流 RPC,只需在請求/回應參數前加上 ‘stream ’ 前綴,串流 RPC 的 proto 定義及生成代碼.
在上面的代碼庫連結中:
-
streaming.proto:此文件為用戶自定義
-
streaming.pb.go 及 streaming_grpc.pb.go:這些文件是在運行 proto 編譯器 命令後自動生成的。
gRPC與REST的對比
我們確實談了不少關於gRPC的內容,也提到了REST。然而,我們未曾深入探討兩者之間的差異。既然已有REST這樣成熟且輕量的通信框架,為何還需尋求另一種通信框架呢?讓我們更深入地了解gRPC相對於REST的各方面,並探討各自的優缺點。
為了進行比較,我們需要一些評估標準。因此,讓我們將比較分解為以下幾個參數:
-
訊息格式:Protocol Buffers 與 JSON 的比較
-
在所有數據大小(小/中/大)的情況下,Protocol Buffers 的序列化與反序列化速度遠優於 JSON。基準測試結果。
-
序列化後的 JSON 是可讀的,而 Protocol Buffers(以二進制格式)則否。不確定這是否是一個缺點,因為有時您可能希望在 Google 開發者工具或 Kafka 主題中查看請求詳情,而在使用 Protocol Buffers 的情況下,您無法從中獲取任何信息。
-
-
通訊協議:HTTP 1.1 vs. HTTP/2T
-
REST基於HTTP 1.1;REST客戶端與服務器之間的通訊需要建立TCP連接,而此過程涉及三次握手。當客戶端發送請求並從服務器接收回應後,該TCP連接即告終止。處理下一個請求時,必須重新建立TCP連接。每次請求都需建立TCP連接,這增加了延遲。
-
因此,基於HTTP 2的gRPC通過使用持續連接來應對這一挑戰。我們應記住,HTTP 2中的持續連接與WebSockets中的不同,後者是劫持TCP連接且數據傳輸未經監控。在gRPC連接中,一旦TCP連接建立,它將被重用於多個請求。來自同一客戶端與服務器對的所有請求都會多路復用到同一TCP連接上。
-
-
僅需關注數據與邏輯:代碼生成作為首要考量
-
gRPC通過其內置的protoc編譯器原生支持代碼生成功能。對於REST API,則需要使用如Swagger這樣的第三方工具來自動生成各種語言的API調用代碼。
-
在gRPC的情況下,它抽象了序列化/反序列化、建立連接和發送/接收消息的過程;我們真正需要關心的是我們想要發送或接收的數據以及邏輯。
-
-
傳輸速度
-
由於二進制格式比JSON格式輕得多,因此gRPC情況下的傳輸速度比REST快7到10倍。
-
特點 |
REST |
gRPC |
通信協議 |
遵循請求-響應模型。它可以與任一HTTP版本工作,但通常與HTTP 1.1一起使用 |
遵循客戶端-響應模型並基於HTTP 2。某些服務器有解決方案使其能與HTTP 1.1(通過REST網關)工作 |
瀏覽器支持 |
在各處均可使用 |
有限支援。需使用gRPC-Web,此為網頁的擴展,基於HTTP 1.1 |
載荷數據結構 |
主要使用JSON及XML基礎的載荷來傳輸數據 |
默認使用協議緩衝區傳輸載荷 |
代碼生成 |
需使用第三方工具如Swagger來生成客戶端代碼 |
gRPC對多種語言的代碼生成有原生支持 |
請求快取 |
易於在客戶端和服務器端快取請求。大多數客戶端/服務器原生支持此功能(例如透過cookies) |
默認不支持請求/回應快取 |
再次,目前gRPC在瀏覽器支持方面尚無,因為大多數UI框架對gRPC的支持仍有限或無。雖然在內部微服務通信時,gRPC是大多數情況下的自動選擇,但在需要UI整合的外部通信中則不然。
經過對gRPC和REST兩種框架的比較後,何時使用哪一個?
-
在具有多個輕量級微服務的微服務架構中,數據傳輸效率至關重要時,gRPC將是理想的選擇。
-
若需支援多種語言的代碼生成,gRPC應成為首選框架。
-
憑藉gRPC的串流功能,像交易或OTT這樣的即時應用將從中受益,而非使用REST進行輪詢。
-
若頻寬受限,gRPC能提供更低的延遲和更高的吞吐量。
-
若需求是快速開發及高速迭代,REST應為首選方案。
gRPC概念
負載均衡
儘管持續連接解決了延遲問題,卻引入了另一挑戰,即負載均衡。由於gRPC(或HTTP2)建立持續連接,即便存在負載平衡器,客戶端仍與負載平衡器後的伺服器建立持續連接,類似於黏性會話。
透過示範,我們可以理解這個問題或挑戰。而程式碼及部署檔案位於:https://github.com/infracloudio/grpc-blog/tree/master/grpc-loadbalancing.
從上述示範的程式碼基礎中,我們可以發現,負載均衡的重擔落在客戶端上。這導致了gRPC的一個優勢,即持久連接,在這種變化下不復存在。但gRPC仍可因其其他優點而被使用。
深入了解負載均衡在gRPC中的應用。
在上述示範的程式碼基礎中,僅展示了輪詢負載均衡策略。但gRPC確實支援另一種名為”pick-first”的OOB客戶端負載均衡策略。
此外,自訂客戶端側負載均衡亦受到支援。
清晰的合約
在REST中,客戶端與伺服器之間的合約雖有文檔記錄,但並非嚴格。若回顧SOAP,合約是透過wsdl文件公開的。在REST中,我們透過Swagger及其他方式公開合約。但嚴謹性不足,我們無法確定在客戶端程式開發期間,伺服器端是否已更改合約。
透過gRPC,合約無論是透過proto檔案或由proto檔案產生的生成存根,都與客戶端和服務端共享。這就像是進行遠端函數呼叫。由於我們正在進行函數呼叫,因此我們清楚知道需要傳送什麼以及預期會得到什麼回應。建立與客戶端的連接、處理安全性、序列化-反序列化等的複雜性被抽象化。我們所關心的只是數據。
考慮以下程式碼基礎:
https://github.com/infracloudio/grpc-blog/tree/master/greet_app
客戶端使用存根(由proto檔案產生的程式碼)來建立客戶端物件並呼叫遠端函數:
```sh
import greetpb "github.com/infracloudio/grpc-blog/greet_app/internal/pkg/proto"
cc, err := grpc.Dial(“<server-address>”, opts)
if err != nil {
log.Fatalf("could not connect: %v", err)
}
c := greetpb.NewGreetServiceClient(cc)
res, err := c.Greet(context.Background(), req)
if err != nil {
log.Fatalf("error while calling greet rpc : %v", err)
}
```
同樣地,服務端也使用相同的存根(由proto檔案產生的程式碼)來接收請求物件並建立回應物件:
```sh
import greetpb "github.com/infracloudio/grpc-blog/greet_app/internal/pkg/proto"
func (*server) Greet(_ context.Context, req *greetpb.GreetingRequest) (*greetpb.GreetingResponse, error) {
// 對'req'進行某些操作
return &greetpb.GreetingResponse{
Result: result,
}, nil
}
```
兩者都使用位於此處的相同存根,該存根由proto檔案生成。
而該存根是使用以下proto編譯器命令生成的。
```sh
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/pkg/proto/*.proto
```
安全性
gRPC的認證與授權機制運作於兩個層面:
-
呼叫級別的認證/授權通常通過在呼叫時應用於元數據中的令牌來處理。基於令牌的認證示例
-
通道級別的認證使用連接級別應用的客戶端證書。它還可以包括應用於通道上每個呼叫的自動呼叫級別認證/授權憑證。基於證書的認證示例
這兩種機制可以單獨或同時使用來加強服務的安全性。
中介軟體
在REST中,我們使用中介軟體來達成多種目的,例如:
-
速率限制
-
請求/回應前後驗證
-
解決安全威脅
在gRPC中我們也能達成相同效果。在gRPC中,這些功能被稱為攔截器,雖然術語不同,但它們執行類似的任務。
在’greet_app’代碼庫的中介軟體分支中,我們已整合了日誌記錄器和Prometheus攔截器。
查看如何配置攔截器以使用Prometheus和日誌記錄包這裡。
但我們還可以整合其他包到攔截器中,用於目的如防止恐慌和恢復(處理異常)、追蹤,甚至是認證等等。
Proto文件的打包、版本控制及編碼實踐
打包
讓我們跟隨包裝分支的步驟。
首先從‘Taskfile.yaml’開始,任務‘gen-pkg’指示‘protoc –proto_path=packaging packaging/*.proto –go_out=packaging’。這意味著‘protoc’(編譯器)將把‘packaging/*.proto’中的所有文件轉換成相應的‘go’文件,這些文件將存放在‘packaging’目錄下,如標誌‘–go_out=packaging’所示。
其次,在‘processor.proto’文件中,定義了兩個消息類型:‘CPU’和‘GPU’。CPU是一個包含三個內建數據類型字段的簡單消息,而GPU則有一個名為‘Memory’的自定義數據類型。‘Memory’是一個獨立的消息,定義在另一個文件中。
那麼如何在‘processor.proto’文件中使用‘Memory’消息呢?通過使用引入。
即使你在提到引入後嘗試通過運行任務‘gen-pkg’生成proto文件,它也會拋出錯誤。因為默認情況下,‘protoc’假設‘memory.proto’和‘processor.proto’在不同的包中。因此,你需要在兩個文件中指定相同的包名。
選項‘go_package’指示編譯器為go文件創建一個名為‘pb’的包名。如果需要創建其他語言的proto文件,包名將是‘laptop_pkg’。
版本管理
-
gRPC中的變更可分為兩類:重大變更和非重大變更。
-
非重大變更包括新增服務、向服務添加新方法、向請求或回應的proto添加字段,以及向枚舉添加值。
-
重大變更如重命名字段、更改字段數據類型、字段編號、重命名或移除包、服務或方法,這些都要求對服務進行版本管理。
-
可選包裝。
代碼實踐
-
請求消息必須以request結尾,例如`CreateUserRequest`。
-
回應消息必須以response結尾,例如`CreateUserResponse`。
-
若回應訊息為空,可選擇使用空物件 `CreateUserResponse` 或使用 `google.protobuf.Empty`
-
套件名稱必須有意義且需版本化,例如:套件 `com.ic.internal_api.service1.v1`
工具
gRPC 生態系統支援多種工具,以簡化非開發任務,如文件製作、gRPC 伺服器的 REST 閘道、整合自訂驗證器、程式碼檢查等。以下是一些能協助我們達成目標的工具:
-
protoc-gen-grpc-gateway — 用於建立 gRPC REST API 閘道的插件。它允許 gRPC 端點作為 REST API 端點,並執行從 JSON 到 proto 的轉換。基本上,您定義一個帶有自訂註解的 gRPC 服務,它使這些 gRPC 方法能透過 REST 使用 JSON 請求存取。
-
protoc-gen-swagger — 此為grpc-gateway的輔助插件,能根據gRPC gateway所需的特定註解生成swagger.json檔案。您可將此檔案匯入您選擇的REST客戶端(例如 Postman),並對您所公開的方法進行REST API呼叫。
-
protoc-gen-grpc-web — 此插件允許前端透過gRPC呼叫與後端通訊。關於此主題的獨立部落格文章將於未來發布。
-
protoc-gen-go-validators — 此插件允許為proto消息字段定義驗證規則。它為proto消息生成一個Validate() error方法,您可以在GoLang中調用該方法來驗證消息是否符合您預先定義的期望。
-
https://github.com/yoheimuta/protolint — 此插件用於向proto文件添加lint規則
使用POSTMAN進行測試
與使用Postman或類似工具如Insomnia測試REST API不同,測試gRPC服務並不十分舒適。
注意:gRPC服務亦可透過CLI使用evans-cli等工具進行測試。但前提是gRPC伺服器需啟用反射功能(若未啟用,則需提供proto檔案路徑)。變更設定以啟用反射及如何進入evans-cli的REPL模式。進入evans-cli的REPL模式後,即可直接從CLI測試gRPC服務,詳細流程可參閱evans-cli的GitHub頁面。
Postman目前提供測試gRPC服務的測試版。
以下是操作步驟:
-
開啟Postman
-
前往左側邊欄的‘APIs’
-
點擊‘+’號建立新API:
-
在彈出視窗中,輸入‘名稱’、‘版本’及‘架構詳情’,然後點擊建立[除非您需要從GitHub/Bitbucket等來源匯入]。此步驟適用於您希望複製貼上proto合約的情況。
5. 你的 API 將如以下所示建立。點擊版本 ‘1.0.0’,前往定義並輸入你的 proto 合約。
-
請記住,此處不支援導入功能,因此最好將所有相依的 protos 存放在同一位置。
-
上述步驟將有助於保留合約以供未來使用。
-
接著點擊 ‘新建’ 並選擇 ‘gRPC 請求’:
-
輸入 URI 並從已保存的 API 列表中選擇 proto:
-
輸入你的請求訊息並點擊 ‘呼叫’:
在上述步驟中,我們已經了解了如何透過POSTMAN測試gRPC API的過程。使用POSTMAN測試gRPC端點的流程與測試REST端點有所不同。值得注意的是,在建立並保存proto合約時(如#5所示),所有proto消息和服務定義必須位於同一位置。因為POSTMAN不支持跨版本訪問proto消息。
總結
本文中,我們對RPC有了初步了解,並將其與REST進行了比較及討論兩者間的差異,接著深入探討了由Google開發的RPC實現——gRPC。
gRPC作為一個框架,在微服務架構的內部通信中尤為關鍵。它亦可用於外部通信,但需配合REST網關。對於流式傳輸和即時應用而言,gRPC是不可或缺的。
隨著Golang在服務器端腳本語言領域的崛起,gRPC也正成為通信框架的實際標準。
Source:
https://dzone.com/articles/understanding-grpc-concepts-use-cases-amp-best-pra