gRPC服務(wù)的響應(yīng)設(shè)計(jì)
1. 服務(wù)端響應(yīng)的現(xiàn)狀
做后端服務(wù)的開發(fā)人員對(duì)錯(cuò)誤處理總是很敏感的,因此在做服務(wù)的響應(yīng)(response/reply)設(shè)計(jì)時(shí)總是會(huì)很慎重。
如果后端服務(wù)選擇的是HTTP API(rest api),比如json over http,API響應(yīng)(Response)中大多會(huì)包含如下信息:
{
?"code":?0,
?"msg":?"ok",
?"payload"?:?{
????????...?...
?}
}
在這個(gè)http api的響應(yīng)設(shè)計(jì)中,前兩個(gè)狀態(tài)標(biāo)識(shí)這個(gè)請(qǐng)求的響應(yīng)狀態(tài)。這個(gè)狀態(tài)由一個(gè)狀態(tài)代碼(code)與狀態(tài)信息(msg)組成。狀態(tài)信息是對(duì)狀態(tài)代碼所對(duì)應(yīng)錯(cuò)誤原因的詳細(xì)詮釋。只有當(dāng)狀態(tài)為正常時(shí)(code = 0),后面的payload才具有意義。payload顯然是在響應(yīng)中意圖傳給客戶端的業(yè)務(wù)信息。
這樣的服務(wù)響應(yīng)設(shè)計(jì)是目前比較常用且成熟的方案,理解起來也十分容易。
好,現(xiàn)在我們看看另外一大類服務(wù):采用RPC方式提供的服務(wù)。我們還是以使用最為廣泛的gRPC為例。在gRPC中,一個(gè)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對(duì)于每個(gè)rpc方法(比如SayHello)都有約束,只能有一個(gè)輸入?yún)?shù)和一個(gè)返回值。這個(gè).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)
????...?...
}
我們看到對(duì)于SayHello RPC方法,protoc生成的go代碼中,SayHello方法的返回值列表中多了一個(gè)Gopher們熟悉的error返回值。對(duì)于已經(jīng)習(xí)慣了HTTP API那套響應(yīng)設(shè)計(jì)的gopher來說,現(xiàn)在問題來了! http api響應(yīng)中表示響應(yīng)狀態(tài)的code與msg究竟是定義在HelloReply這個(gè)業(yè)務(wù)響應(yīng)數(shù)據(jù)中,還是通過error來返回的呢?這個(gè)grpc官方文檔似乎也沒有明確說明(如果各位看官找到位置,可以告訴我哦)。
2. gRPC服務(wù)端響應(yīng)設(shè)計(jì)思路
我們先不急著下結(jié)論!我們繼續(xù)借用helloworld這個(gè)示例程序來測(cè)試一下當(dāng)error返回值不為nil時(shí)客戶端的反應(yīng)!先改一下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")
}
在上面代碼中,我們故意構(gòu)造一個(gè)錯(cuò)誤并返回給調(diào)用該方法的客戶端。我們來運(yùn)行一下這個(gè)服務(wù)并啟動(dòng)greeter_client[3]來訪問該服務(wù),在客戶端側(cè),我們得到的結(jié)果如下:
2021/09/20?17:04:35?could?not?greet:?rpc?error:?code?=?Unknown?desc?=?test?grpc?error
從客戶端的輸出結(jié)果中,我們看到了我們自定義的錯(cuò)誤的內(nèi)容(test grpc error)。但我們還發(fā)現(xiàn)錯(cuò)誤輸出的內(nèi)容中還有一個(gè)"code = Unknown"的輸出,這個(gè)code是從何而來呢?似乎grpc期待的error形式是包含code與desc的形式。
這時(shí)候就不得不查看一下gprc-go(v1.40.0)的參考文檔[4]了!在grpc-go的文檔中我們發(fā)現(xiàn)幾個(gè)被DEPRECATED的與Error有關(guān)的函數(shù):

在這幾個(gè)作廢的函數(shù)的文檔中都提到了用status包的同名函數(shù)替代。那么這個(gè)status包又是何方神圣?我們翻看grpc-go的源碼,終于找到了status包,在包說明的第一句中我們就找到了答案:
Package?status?implements?errors?returned?by?gRPC.?
原來status包實(shí)現(xiàn)了上面grpc客戶端所期望的error類型。那么這個(gè)類型是什么樣的呢?我們逐步跟蹤代碼:
在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,我們?cè)賮砜磇nternal/status包中Status結(jié)構(gòu)的定義:
//?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結(jié)構(gòu)體組合了一個(gè)*spb.Status類型(google.golang.org/genproto/googleapis/rpc/status包中的類型)的字段,繼續(xù)追蹤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,name=code,proto3"?json:"code,omitempty"`
?//?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,name=message,proto3"?json:"message,omitempty"`
?//?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,name=details,proto3"?json:"details,omitempty"`
?//?contains?filtered?or?unexported?fields
}
我們看到最后的這個(gè)Status結(jié)構(gòu)包含了Code與Message。這樣一來,grpc的設(shè)計(jì)意圖就很明顯了,它期望開發(fā)者在error這個(gè)返回值中包含rpc方法的響應(yīng)狀態(tài),而自定義的響應(yīng)結(jié)構(gòu)體只需包含業(yè)務(wù)所需要的數(shù)據(jù)即可。我們用一幅示意圖來橫向建立一下http api與rpc響應(yīng)的映射關(guān)系:

有了這幅圖,再面對(duì)如何設(shè)計(jì)grpc方法響應(yīng)這個(gè)問題時(shí),我們就胸有成竹了!
grpc-go在codes包[5]中定義了grpc規(guī)范要求的10余種錯(cuò)誤碼:
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
在這些標(biāo)準(zhǔn)錯(cuò)誤碼之外,我們還可以擴(kuò)展定義自己的錯(cuò)誤碼與錯(cuò)誤描述。
3. 服務(wù)端如何構(gòu)造error與客戶端如何解析error
前面提到,gRPC服務(wù)端采用rpc方法的最后一個(gè)返回值error來承載應(yīng)答狀態(tài)。google.golang.org/grpc/status包為構(gòu)建客戶端可解析的error提供了一些方便的函數(shù),我們看下面示例(基于上面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包提供了一個(gè)類似于fmt.Errorf的函數(shù),我們可以很方便的構(gòu)造一個(gè)帶有code與msg的error實(shí)例并返回給客戶端。
而客戶端同樣可以通過status包提供的函數(shù)將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函數(shù)可以很簡答地將rpc方法返回的不為nil的error中攜帶的信息提取出來。
4. 空應(yīng)答
gRPC的proto文件規(guī)范要求每個(gè)rpc方法的定義中都必須包含一個(gè)返回值,返回值不能為空,比如上面helloworld項(xiàng)目的.proto文件中的SayHello方法:
rpc?SayHello?(HelloRequest)?returns?(HelloReply)?{}
如果去掉HelloReply這個(gè)返回值,那么protoc在生成代碼時(shí)會(huì)報(bào)錯(cuò)!
但是有些方法本身不需要返回業(yè)務(wù)數(shù)據(jù),那么我們就需要為其定義一個(gè)空應(yīng)答消息,比如:
message?Empty?{
}
考慮到每個(gè)項(xiàng)目在遇到空應(yīng)答時(shí)都要重復(fù)造上面Empty message定義的輪子,grpc官方提供了一個(gè)可被復(fù)用的空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文件中導(dǎo)入該empty.proto并使用Empty即可,比如下面代碼:
//?xxx.proto
syntax?=?"proto3";
??
import?"google/protobuf/empty.proto";
service?MyService?{
?rpc?MyRPCMethod(...)?returns?(google.protobuf.Empty);
}
當(dāng)然google.protobuf.Empty不僅僅適用于空響應(yīng),也適合空請(qǐng)求,這個(gè)就留給大家可自行完成吧。
5. 小結(jié)
本文我們講述了gRPC服務(wù)端響應(yīng)設(shè)計(jì)的相關(guān)內(nèi)容,最主要想說的是直接使用gRPC生成的rpc方面的error返回值來表示rpc調(diào)用的響應(yīng)狀態(tài),不要再在自定義的Message結(jié)構(gòu)中重復(fù)放入code與msg字段來表示響應(yīng)狀態(tài)了。
btw,做API的錯(cuò)誤設(shè)計(jì),google的這份API設(shè)計(jì)方面的參考資料[7]是十分好的。有時(shí)間一定要好好讀讀哦。
參考資料
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/[email protected]/codes#Code
[6]?greeter_server:?https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
[7]?google的這份API設(shè)計(jì)方面的參考資料:?https://cloud.google.com/apis/design/errors
[8]?改善Go語?編程質(zhì)量的50個(gè)有效實(shí)踐:?https://www.imooc.com/read/87
[9]?Kubernetes實(shí)戰(zhàn):高可用集群搭建、配置、運(yùn)維與應(yīng)用:?https://coding.imooc.com/class/284.html
[10]?鏈接地址:?https://m.do.co/c/bff6eed92687
推薦閱讀
