【實戰分享】從選型到專案落地,漫談 gRPC

又拍雲發表於2020-11-12

什麼是 gRPC?

gRPC 的幾種常見模式

在學習 gRPC 的時候,相信大家對於它的四種模式都有了解,我們來簡單回顧一下:

  • 簡單模式(Simple RPC):這種模式最為傳統,即客戶端發起一次請求,服務端響應一個資料,這和大家平時熟悉的 RPC 沒有什麼大的區別,所以不再詳細介紹。

  • 服務端資料流模式(Server-side streaming RPC):這種模式是客戶端發起一次請求,服務端返回一段連續的資料流。典型的例子是客戶端向服務端傳送一個股票程式碼,服務端就把該股票的實時資料來源源不斷的返回給客戶端。如果是使用我們容器雲功能的同學應該會發現,我們的容器實時日誌流就是使用了這個典型模式。

  • 客戶端資料流模式(Client-side streaming RPC):與服務端資料流模式相反,這次是客戶端源源不斷地向服務端傳送資料流,而在傳送結束後,由服務端返回一個響應。典型的例子是物聯網終端向伺服器報送資料。

  • 雙向資料流模式(Bidirectional streaming RPC):顧名思義,這是客戶端和服務端都可以向對方傳送資料流,這個時候雙方的資料可以同時互相傳送,也就是可以實現實時互動。典型的例子是聊天機器人。

接下來我們通過一個小例子來看看 gRPC 具體的使用流程。

假設我們有一個聊天機器人,現需要增加一個對外提供服務的介面。具體需求為,介面傳入引數是一個人名,返回一段內容是“Hello 人名”的音訊。如果這個是讓你在不使用 gRPC 的情況下,你會怎麼做?大家可能會選擇使用 restful api 來實現這個功能,傳入人名,返回音訊二進位制資料。

那麼如果使用 gRPC,我們需要怎麼來設計呢?

第一步,需要定義一個介面文件,也就是 proto 檔案。在定義內會定義一個 Service,接下來再在 Service 裡定義一個 SayHello 的方法。下面定義傳入引數,輸入 name 返回 message,需要注意 message 是 bytes 型別,即返回的格式是二進位制資料。對於 Golang 底層對應的是一個 bytes 資料,對於其他語言可能是位元組流或二進位制。

syntax = "proto3";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
bytes message = 1;

定義完成後 ,下一步就是使用 protoc 命令列工具生成程式碼。下圖左側是初始化的專案,你會發現,有一個單獨的目錄 protoc,存放了 hello.proto 這個檔案,這個檔案就是前面定義好的。

下圖右側是自動生成程式碼後的專案結構,生成了一個 pkg/helloworld 的包,裡面有一個 hello.pb.go,開啟這個檔案,你會發現剛才定義的 proto 已經被翻譯成了 Go 語言。具體 protoc 命令列工具如何使用,可以自行搜尋下,這裡不再過多展開。

定義好了 proto 檔案,以及生成了相應的 package 程式碼,下一步我們就可以編寫業務邏輯了。

Hello gRPC - 服務端業務程式碼

import (
"google.golang.org/grpc"
pb "grpc-hw/pkg/helloworld"
)
// server is used to implement helloworld.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
tempFile := "srv.wav"
err := exec.Command("flite", "-t", "Hello "+in.GetName(), "-o", tempFile).Run()
if err != nil {
return nil, fmt.Errorf("make audio failed: %v", err)
}
data, _ := ioutil.ReadFile(tempFile)
return &pb.HelloReply{Message: data}, nil
}
func main() {
lis, _ := net.Listen("tcp", port)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
s.Serve(lis)
}

在服務端側,需要實現 SayHello 的方法來滿足 GreeterServer 介面的要求。SayHello 方法的傳入引數,是在 proto 檔案中定義的 HelloRequest,傳出引數,是在 proto 檔案中定義的 HelloReply,以及一個 error。

業務邏輯也比較簡單,獲取 HelloRequest 中 Name 欄位,然後通過命令列行工具轉換成對應的音訊,將 bytes 陣列存在在 HelloReply 中返回。

Hello gRPC - 客戶端業務程式碼

func main() {
flag.StringVar(&address, "addr", address, "server address")
flag.StringVar(&name, "name", "world", "name")
flag.Parse()
// Set up a connection to the server.
conn, _ := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
tempFile := "cli.wav"
ioutil.WriteFile(tempFile, r.Message, 0666)
exec.Command("afplay", tempFile).Run()
}

我們再來看一下如何實現 Client。首先,建立一個 gRPC 的連線,並初始化 GreeterClient,然後直接呼叫下 GreeterClient 的 SayHello 的方法,將把返回結果另存為一個檔案,通過播放器播放就可以了。

總體而言,整個使用過程很簡單,並且非常容易上手,讓開發可以更加關注在客戶端、服務端業務的實現,不用操心傳輸層的事情。

gRPC 的使用總結

通過剛剛的小例子,我們來總結一下 gRPC 的使用:

  • 定義好介面文件

  • 工具生成服務端/客戶端程式碼

  • 服務端補充業務程式碼

  • 客戶端建立 gRPC 連線後,使用自動生成的程式碼呼叫函式

  • 編譯、執行

以上 5 步就是 gRPC 的簡單使用方法了。

gRPC 與 Protobuf

接下來我們來聊聊 gRPC 跟 Protobuf 之間的聯絡,當然在這之前我們需要先知道 Protobuf 是什麼。

Protobuf

Protobuf 是一個語言無關、平臺無關的可擴充套件的結構化資料序列化方案。大家可能會覺得它跟 JSON 好像沒什麼區別,功能上看起來是一樣的,但像上文那樣去定義 SayHello 的操作,JSON 是做不到的,JSON 只能定義一個請求體或者一個返回體,沒法定義一個方法,但是 Protobuf 是可以的。

Protobuf 多用於協議通訊、資料儲存和其他更多用途。它是一個比較靈活的、高效的、自動化的結構化資料序列機制,但是更小,更快並且更簡單。一旦定義好資料如何構造, 就可以使用特殊生成的程式碼來輕易地讀寫結構化資料,無需關心用什麼語言來實現。你甚至可以更新資料結構而不打破已部署的使用"舊有"格式編譯的程式。

上圖是 Protobuf 與 JSON 以及 JSON stream 三者之間的效能比較,可以明顯的看到在解碼的時候 Protobuf 比其他兩項快了不只一星半點。

Protobuf  與 XML、JSON 的吞吐量比較

上圖中,我們可以看到 Protobuf 還是有一些缺點的,比如瀏覽器的支援沒有其他幾個支援的好。但是,在資料安全方面,由於傳輸過程中採用的是加密壓縮後的位元組流,一般無法直接檢視,安全性非常好。以及在處理速度方面,因為編解碼效率很高使得整體吞吐量有了顯著提升。還有一點,定義方法,這個是其他兩種序列化協議所做不到的。

gRPC 跟 Protobuf 的聯絡

雖然每次 gRPC 與 Protobuf 都是同時出現的,但是其實兩者之間並沒有很深的聯絡。只是因為兩者都是由 Google 開發的,和 gRPC 本身負載無關,在使用時也可以選擇 JSON 等等,但是考慮到 Protobuf 有定義方法的優勢,在微服務裡還是很推薦使用的。

gRPC vs Restful API

上圖是 gRPC 與 Restful API 的對比,平常我們可能更多使用 Restful API。但從圖上可以看到,因為 gRPC 用的是 Protobuf,本身就比較小所以很快,不像 JSON 體積比較大、比較慢。另外,gRPC 是載入 HTTP/2 上面的,延遲比較低,也因為 HTTP/2 支援連結複用,這就可以讓多個 stream 共用一個連線,從而進一步提升速度。對比 Restful 則使用的是 HTTP 1.1,延遲比較高,而且在一般情況下,每次請求都需要建一下新的連線。

gRPC 是雙向的。什麼是雙向呢?比如我們平常做 Restful,都是從客戶端到服務端,但是服務端沒辦法直接主動向客戶端傳送資訊,gRPC 則可以。gRPC 也支援流,Restful只支援Request/Response 這樣的機制。gRPC是面向 API 的,沒有限制,也面向增刪改查。gRPC 可以通過 Protobuf 直接生成程式碼,而 Restful 需要依賴第三方。

另外 gRPC 支援 RPC 可以呼叫伺服器上的一些方法,而 Restful 是基於HTTP的語法,很多東西需要自己去定義。這方面相信大家都有感觸,比如 REST 定義post/put/delete/get時,因為每個人都有自己的習慣,所以協作時需要溝通討論進行指定。但是 gRPC 就不需要了,它支援定義函式式辦法,不需要大家去考慮如何設計語法。

以上就是 gRPC 跟 Restful API 的對比。

引入 gRPC 需要考慮哪些問題?

那麼當我們引入 gRPC 的時候需要考慮什麼呢?以下幾點肯定是不可避免的考慮項:

  • 是否可以滿足當前需求

  • 效能如何

  • 連線異常斷開後,是否需要客戶端重試

  • TCP 連線是否可以複用

  • 業務層改動是否足夠便利

  • 業務後期迭代是否會出現問題,如何避免

這個也是我們引入一項新的東西時,往往需要考慮到的問題。

回顧與總結

從選擇 gRPC 到整個專案落地,以及現在上線後正常使用。整個過程中,我對於專案的思考可以包括了過去、現在和未來三個階段。

對我而言,過去就是要去看我選擇的這個東西,用的人多不多,完善程度怎麼樣了?而現在則是要結合專案,看看合不合適,能不能使用。當然不能思考到能使用就結束,我們還需要考慮這個專案在未來的 3-5 年的發展,你引入它後在這個時間內需不需要大的變動。這個是非常重要的,雖然我們現在常說敏捷開發,也經常會進行很多的調整,但是在類似 gRPC 這種底層基礎來說,是固定的。

以上就是我今天的全部分享內容,講的比較簡單,希望能帶給大家一些收穫。

推薦閱讀

秋天的第一份“乾貨” I Referer 防盜鏈,為什麼少了個字母 R?

“網頁內容無法訪問”可能是跨域錯誤!

相關文章