gRPC 的錯誤處理實踐
0 背景
我們內部系統全部統一採用gRPC協議和protobuf編解碼。統一的好處在於不需要在做任何協議、編解碼轉換,這樣就可以使我們所有業務採用同一個protobuf倉庫,基於CI/CD工具實現許多自動化功能。
我們要求所有服務提供者提前在獨立的路徑下定義好接口和錯誤碼的protobuf文件,然後提交到GitLab,我們通過GitLab CI的check階段對變更的protobuf文件做format、lint、breaking 檢查。然後在build階段,會基於protobuf文件中的註釋自動產生文檔,並推送至內部的微服務管理系統接口平臺中,還會根據protobuf文件自動構建Go/PHP/Node/Java等多種語言的樁代碼和錯誤碼,並推送到指定對應的中心化倉庫。推送到倉庫後,我們就可以通過各語言的包管理工具拉取客戶端、服務端的 gRPC 和錯誤碼的依賴,不需要口頭約定對接數據的定義,也不需要通過IM工具傳遞對接數據的定義文件,極大的簡化了對接成本。
1 判斷 Error 的錯誤原理
要了解怎麼處理gRPC的error之前,我們首先來看下Go普通的error是怎麼處理的。
我們在判斷一個error的根因,需要根因error是一個固定地址的指針類型,這樣我們才能夠使用官方的errors.Is方法判斷他是否爲根因。以下是一個代碼示例:
我們先看這個代碼errors.Is(wrapNewPointerError(), fmt.Errorf("i am error"))的執行步驟,首先構造了一個error,然後使用官方%w的方式將error進行了包裝,我們在使用errors.Is方法判斷的時候,底層函數會將error解包來判斷兩個error的地址是否一致。
因此我們第一個errors.Is執行的是個false。在使用這個代碼errors.Is(wrapConstantPointerError(), sentinelErr),因爲是固定地址的error,所以判斷根因錯誤的時候,執行的是true。
2 gRPC 網絡傳輸的 Error
我們客戶端在獲取到gRPC的error的時候,是否可以使用上文說的官方errors.Is進行判斷呢。如果我們直接使用該方法,通過判斷 error 地址是否相等,是無法做到的。原因是因爲我們在使用gRPC的時候,在遠程調用過程中,客戶端獲取的服務端返回的error,在tcp傳遞的時候實際上是一串文本。客戶端拿到這個文本,是要將其反序列化轉換爲error,在這個反序列化的過程中,其實是new了一個新的error地址,這樣就無法判斷error地址是否相等。
爲了更好的解釋gRPC網絡傳輸的error,以下描述了整個error的處理流程。
-
客戶端通過
invoker方法將請求發送到服務端。 -
服務端通過
processUnaryRPC方法,獲取到用戶代碼的error信息。 -
服務端通過
status.FromError方法,將error轉化爲status.Status。 -
服務端通過
WriteStatus方法將status.Status裏的數據,寫入到grpc-status、grpc-message、grpc-status-details-bin的header頭裏。 -
客戶端通過網絡獲取到這些
header頭,使用strconv.ParseInt解析到grpc-status信息、decodeGrpcMessage解析到grpc-message信息、decodeGRPCStatusDetails解析爲grpc-status-details-bin信息。 -
客戶端通過
a.Status().Err()獲取到用戶代碼的錯誤。
爲了方便理解,我們抓個包,看下error具體的報文情況。
3 檢查 gRPC 的 error 信息第一版本
通過上文描述,我們已經瞭解了gRPC在網絡中如何傳輸error,可以看到new出來的error是無法判等的。所以我們就想到,使用工具提前生成好error,這樣error的地址是不會改變的。這樣我們就可以使用errors.Is的方法去檢查根因error。
首先我們可以將錯誤碼編寫在proto裏,註釋,如下所示:
syntax = "proto3";
package engineering.helloworld;
option go_package = "engineering/helloworld;helloworld";
// @plugins=protoc-gen-go-errors
// 錯誤
enum Error {
// 未知類型
// @code=UNKNOWN
RESOURCE_ERR_UNKNOWN = 0;
// 找不到資源
// @code=NOT_FOUND
RESOURCE_ERR_NOT_FOUND = 1;
// 獲取列表數據出錯
// @code=INTERNAL
RESOURCE_ERR_LIST_MYSQL = 2;
// 獲取詳情數據出錯
// @code=INTERNAL
RESOURCE_ERR_INFO_MYSQL = 3;
}
然後我們可以通過執行proto錯誤插件,生成固定地址的error,將error註冊到全局map裏,同時我們還可以根據@code的註釋,生成gRPC的狀態碼。
func init() {
resourceErrUnknown = eerrors.New(int(codes.Unknown), "engineering.helloworld.RESOURCE_ERR_UNKNOWN", Error_RESOURCE_ERR_UNKNOWN.String())
eerrors.Register(resourceErrUnknown)
resourceErrNotFound = eerrors.New(int(codes.NotFound), "engineering.helloworld.RESOURCE_ERR_NOT_FOUND", Error_RESOURCE_ERR_NOT_FOUND.String())
eerrors.Register(resourceErrNotFound)
resourceErrListMysql = eerrors.New(int(codes.Internal), "engineering.helloworld.RESOURCE_ERR_LIST_MYSQL", Error_RESOURCE_ERR_LIST_MYSQL.String())
eerrors.Register(resourceErrListMysql)
resourceErrInfoMysql = eerrors.New(int(codes.Internal), "engineering.helloworld.RESOURCE_ERR_INFO_MYSQL", Error_RESOURCE_ERR_INFO_MYSQL.String())
eerrors.Register(resourceErrInfoMysql)
}
func ResourceErrUnknown() eerrors.Error {
return resourceErrUnknown
}
....
接着我們在獲取gRPC error後,需要使用FromError方法,轉換爲我們proto生成的error。在這個轉換過程中,我們會從之前註冊的全局error map裏,通過reason方法,找到對應的error,返回給用戶。用戶這個時候就可以通過errors.Is來判斷根因。
4 檢查 gRPC 的 Error 信息第二版本
按以上方案,確實可以解決根因問題,但該error,無法攜帶message,metadata信息。這就導致我們,很難準確定位一些問題。所以這個時候,我們需要在error裏做一些擴展,增加兩個方法。
這種方式可以讓我們攜帶信息,但是他會對原有的error錯誤做一次克隆,導致了error的地址變化,無法在通過error判等的方式進行校驗是否是根因。
這個時候,我們只能通過errors.Is中的(interface{ Is(error) bool })斷言方式,在我們自定義的error中,增加一個Is方法來判斷。
通過這種方式,我們不僅可以判斷根因,並且還可以將error裏攜帶更多排查有用的信息。
5 演示 gRPC 的 Error 的處理
爲了更好的演示 error,我們將 error 處理的方式做成了工具,通過執行腳本,我們就可以下載到對應的工具
bash <(curl -L https://raw.githubusercontent.com/gotomicro/egoctl/main/getlatest.sh)
通過該工具,就可以執行我們ego error的演示代碼
5.1 生成 error、grpc 的 pb 文件
我們在該演示代碼目錄下執行make gen,可以生成對應的error、grpc的pb文件,如下所示。
這些error爲了防止其他人不小心篡改,獲取error的時候,都是用方法來獲取,如下所示。
func ResourceErrUnknown() eerrors.Error {
return resourceErrUnknown
}
我們在server里根據客戶端發送的error,返回我們proto生成的error信息。
我們在client裏,判斷是否是這個error,並記錄error裏的錯誤信息。
5.2 執行指令
在目錄下執行make svc,我們可以啓動服務端 然後在目錄下,我們在執行make cli,我們可以啓動客戶端 執行完後,可以看到如下日誌:
服務端展示:
客戶端展示:
可以看到客戶端紅框裏,就是我們業務代碼裏記錄的日誌。我們通過官方的 errors.Is 判斷,能夠很優雅的做一些業務邏輯處理。
5.3 錯誤碼查看
錯誤碼,我們可以全部放在proto裏管理。那麼我們就可以很方便在proto裏查看錯誤碼,或者做的更好一點,將proto生成更好看的錯誤碼文檔。
自此我們將錯誤碼進行了詳細的介紹,下次我們會介紹gRPC如何做單元測試和mock服務的實踐,如何通過proto文件生成單元測試代碼。
6 鳴謝
感謝kratos的error的處理和生成工具,通過學習它的代碼和思想,我們將框架Ego基於error處理做了更多的改進,例如通過proto的註解生成grpc錯誤碼,生成固定地址的error。並且我們做了更多的proto工具,可以通過proto文件生成單元測試代碼、API 文檔等。
7 相關鏈接
-
項目演示代碼:https://github.com/gotomicro/go-engineering/tree/main/chapter_grpc_error/egoerror
-
項目框架:https://github.com/gotomicro/ego
-
proto 生成插 error 件:https://github.com/gotomicro/ego/tree/master/cmd/protoc-gen-go-errors
-
框架對 error 的處理:https://github.com/gotomicro/ego/blob/master/core/eerrors/errors.go
-
常量 error:https://dave.cheney.net/2016/04/07/constant-errors
-
Go1.13Error Wrapping 分析:https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3XLmGAlGDHfarbLyQoAh7Q