gRPC 服務的響應設計
1. 服務端響應的現狀
做後端服務的開發人員對錯誤處理總是很敏感的,因此在做服務的響應 (response/reply) 設計時總是會很慎重。
如果後端服務選擇的是 HTTP API(rest api),比如 json over http,API 響應 (Response)中大多會包含如下信息:
{
"code": 0,
"msg": "ok",
"payload" : {
... ...
}
}
在這個 http api 的響應設計中,前兩個狀態標識這個請求的響應狀態。這個狀態由一個狀態代碼 (code) 與狀態信息 (msg) 組成。狀態信息是對狀態代碼所對應錯誤原因的詳細詮釋。只有當狀態爲正常時(code = 0),後面的 payload 才具有意義。payload 顯然是在響應中意圖傳給客戶端的業務信息。
這樣的服務響應設計是目前比較常用且成熟的方案,理解起來也十分容易。
好,現在我們看看另外一大類服務:採用 RPC 方式提供的服務。我們還是以使用最爲廣泛的 gRPC 爲例。在 gRPC 中,一個 service 的定義如下 (我們借用一下 grpc-go 提供的 helloworld 示例 [1]):
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto
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 {
string message = 1;
}
grpc 對於每個 rpc 方法 (比如 SayHello) 都有約束,只能有一個輸入參數和一個返回值。這個. proto 定義通過 protoc 生成的 go 代碼變成了這樣:
// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld_grpc.pb.go
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
... ...
}
我們看到對於 SayHello RPC 方法,protoc 生成的 go 代碼中,SayHello 方法的返回值列表中多了一個 Gopher 們熟悉的 error 返回值。對於已經習慣了 HTTP API 那套響應設計的 gopher 來說,現在問題來了! http api 響應中表示響應狀態的 code 與 msg 究竟是定義在 HelloReply 這個業務響應數據中,還是通過 error 來返回的呢?這個 grpc 官方文檔似乎也沒有明確說明(如果各位看官找到位置,可以告訴我哦)。
2. gRPC 服務端響應設計思路
我們先不急着下結論!我們繼續借用 helloworld 這個示例程序來測試一下當 error 返回值不爲 nil 時客戶端的反應!先改一下 greeter_server[2] 的代碼:
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, errors.New("test grpc error")
}
在上面代碼中,我們故意構造一個錯誤並返回給調用該方法的客戶端。我們來運行一下這個服務並啓動 greeter_client[3] 來訪問該服務,在客戶端側,我們得到的結果如下:
2021/09/20 17:04:35 could not greet: rpc error: code = Unknown desc = test grpc error
從客戶端的輸出結果中,我們看到了我們自定義的錯誤的內容 (test grpc error)。但我們還發現錯誤輸出的內容中還有一個 "code = Unknown" 的輸出,這個 code 是從何而來呢?似乎 grpc 期待的 error 形式是包含 code 與 desc 的形式。
這時候就不得不查看一下 gprc-go(v1.40.0) 的參考文檔 [4] 了!在 grpc-go 的文檔中我們發現幾個被 DEPRECATED 的與 Error 有關的函數:
在這幾個作廢的函數的文檔中都提到了用 status 包的同名函數替代。那麼這個 status 包又是何方神聖?我們翻看 grpc-go 的源碼,終於找到了 status 包,在包說明的第一句中我們就找到了答案:
Package status implements errors returned by gRPC.
原來 status 包實現了上面 grpc 客戶端所期望的 error 類型。那麼這個類型是什麼樣的呢?我們逐步跟蹤代碼:
在 grpc-go/status 包中我們看到如下代碼:
type Status = status.Status
// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
return status.New(c, msg)
}
status 包使用了 internal/status 包中的 Status,我們再來看 internal/status 包中 Status 結構的定義:
// internal/status
type Status struct {
s *spb.Status
}
// New returns a Status representing c and msg.
func New(c codes.Code, msg string) *Status {
return &Status{s: &spb.Status{Code: int32(c), Message: msg}}
}
internal/status 包的 Status 結構體組合了一個 * spb.Status 類型 (google.golang.org/genproto/googleapis/rpc/status 包中的類型) 的字段,繼續追蹤 spb.Status:
// https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status
type Status struct {
// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
Code int32 `protobuf:"varint,1,opt,`
// A developer-facing error message, which should be in English. Any
// user-facing error message should be localized and sent in the
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
Message string `protobuf:"bytes,2,opt,`
// A list of messages that carry the error details. There is a common set of
// message types for APIs to use.
Details []*anypb.Any `protobuf:"bytes,3,rep,`
// contains filtered or unexported fields
}
我們看到最後的這個 Status 結構包含了 Code 與 Message。這樣一來,grpc 的設計意圖就很明顯了,它期望開發者在 error 這個返回值中包含 rpc 方法的響應狀態,而自定義的響應結構體只需包含業務所需要的數據即可。我們用一幅示意圖來橫向建立一下 http api 與 rpc 響應的映射關係:
有了這幅圖,再面對如何設計 grpc 方法響應這個問題時,我們就胸有成竹了!
grpc-go 在 codes 包 [5] 中定義了 grpc 規範要求的 10 餘種錯誤碼:
const (
// OK is returned on success.
OK Code = 0
// Canceled indicates the operation was canceled (typically by the caller).
//
// The gRPC framework will generate this error code when cancellation
// is requested.
Canceled Code = 1
// Unknown error. An example of where this error may be returned is
// if a Status value received from another address space belongs to
// an error-space that is not known in this address space. Also
// errors raised by APIs that do not return enough error information
// may be converted to this error.
//
// The gRPC framework will generate this error code in the above two
// mentioned cases.
Unknown Code = 2
// InvalidArgument indicates client specified an invalid argument.
// Note that this differs from FailedPrecondition. It indicates arguments
// that are problematic regardless of the state of the system
// (e.g., a malformed file name).
//
// This error code will not be generated by the gRPC framework.
InvalidArgument Code = 3
... ...
// Unauthenticated indicates the request does not have valid
// authentication credentials for the operation.
//
// The gRPC framework will generate this error code when the
// authentication metadata is invalid or a Credentials callback fails,
// but also expect authentication middleware to generate it.
Unauthenticated Code = 16
在這些標準錯誤碼之外,我們還可以擴展定義自己的錯誤碼與錯誤描述。
3. 服務端如何構造 error 與客戶端如何解析 error
前面提到,gRPC 服務端採用 rpc 方法的最後一個返回值 error 來承載應答狀態。google.golang.org/grpc/status 包爲構建客戶端可解析的 error 提供了一些方便的函數,我們看下面示例(基於上面 helloworld 的 greeter_server[6] 改造):
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return nil, status.Errorf(codes.InvalidArgument, "you have a wrong name: %s", in.GetName())
}
status 包提供了一個類似於 fmt.Errorf 的函數,我們可以很方便的構造一個帶有 code 與 msg 的 error 實例並返回給客戶端。
而客戶端同樣可以通過 status 包提供的函數將 error 中攜帶的信息解析出來,我們看下面代碼:
ctx, _ := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "tony")})
if err != nil {
errStatus := status.Convert(err)
log.Printf("SayHello return error: code: %d, msg: %s\n", errStatus.Code(), errStatus.Message())
}
log.Printf("Greeting: %s", r.GetMessage())
我們看到:通過 status.Convert 函數可以很簡答地將 rpc 方法返回的不爲 nil 的 error 中攜帶的信息提取出來。
4. 空應答
gRPC 的 proto 文件規範要求每個 rpc 方法的定義中都必須包含一個返回值,返回值不能爲空,比如上面 helloworld 項目的. proto 文件中的 SayHello 方法:
rpc SayHello (HelloRequest) returns (HelloReply) {}
如果去掉 HelloReply 這個返回值,那麼 protoc 在生成代碼時會報錯!
但是有些方法本身不需要返回業務數據,那麼我們就需要爲其定義一個空應答消息,比如:
message Empty {
}
考慮到每個項目在遇到空應答時都要重複造上面 Empty message 定義的輪子,grpc 官方提供了一個可被複用的空 message:
// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto
// A generic empty message that you can re-use to avoid defining duplicated
// empty messages in your APIs. A typical example is to use it as the request
// or the response type of an API method. For instance:
//
// service Foo {
// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
// }
//
// The JSON representation for `Empty` is empty JSON object `{}`.
message Empty {}
我們只需在. proto 文件中導入該 empty.proto 並使用 Empty 即可,比如下面代碼:
// xxx.proto
syntax = "proto3";
import "google/protobuf/empty.proto";
service MyService {
rpc MyRPCMethod(...) returns (google.protobuf.Empty);
}
當然 google.protobuf.Empty 不僅僅適用於空響應,也適合空請求,這個就留給大家可自行完成吧。
5. 小結
本文我們講述了 gRPC 服務端響應設計的相關內容,最主要想說的是直接使用 gRPC 生成的 rpc 方面的 error 返回值來表示 rpc 調用的響應狀態,不要再在自定義的 Message 結構中重複放入 code 與 msg 字段來表示響應狀態了。
btw,做 API 的錯誤設計,google 的這份 API 設計方面的參考資料 [7] 是十分好的。有時間一定要好好讀讀哦。
我的聯繫方式:
-
微博:https://weibo.com/bigwhite20xx
-
微信公衆號:iamtonybai
-
博客:tonybai.com
-
github: https://github.com/bigwhite
-
“Gopher 部落” 知識星球:https://public.zsxq.com/groups/51284458844544
參考資料
[1] helloworld 示例: https://github.com/grpc/grpc-go/tree/master/examples/helloworld
[2] greeter_server: https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
[3] greeter_client: https://github.com/grpc/grpc-go/tree/master/examples/helloworld/greeter_client
[4] gprc-go(v1.40.0) 的參考文檔: https://pkg.go.dev/google.golang.org/grpc#section-readme
[5] codes 包: https://pkg.go.dev/google.golang.org/grpc@v1.40.0/codes#Code
[6] greeter_server: https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
[7] google 的這份 API 設計方面的參考資料: https://cloud.google.com/apis/design/errors
[8] 改善 Go 語⾔編程質量的 50 個有效實踐: https://www.imooc.com/read/87
[9] Kubernetes 實戰:高可用集羣搭建、配置、運維與應用: https://coding.imooc.com/class/284.html
[10] 鏈接地址: https://m.do.co/c/bff6eed92687
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/nGTrKBHLVmHLuRMffHcKfw