Kratos技术系列|从Kratos设计看Go微服务工程实践
導讀
github.com/go-kratos/kratos(以下簡稱Kratos)是一套輕量級 Go 微服務框架,致力于提供完整的微服務研發(fā)體驗,整合相關(guān)框架及周邊工具后,微服務治理相關(guān)部分可對整體業(yè)務開發(fā)周期無感,從而更加聚焦于業(yè)務交付。Kratos在設(shè)計之初就考慮到了高可擴展性,組件化,工程化,規(guī)范化等。對每位開發(fā)者而言,整套 Kratos 框架也是不錯的學習倉庫,可以了解和參考微服務的技術(shù)積累和經(jīng)驗。
接下來我們從Protobuf、開放性、規(guī)范、依賴注入這4個點了解一下Kratos 在Go微服務工程領(lǐng)域的實踐。
?曹國梁?
6年Go微服務研發(fā)經(jīng)歷
騰訊云高級研發(fā)工程師
Kratos Maintainer,gRPC-go contributor
基于Protocol Buffers(Protobuf)的生態(tài)
在Kratos中,API定義、gRPC Service、HTTP Service、請求參數(shù)校驗、錯誤定義、Swagger API json、應用服務模版等都是基于Protobuf IDL來構(gòu)建的:
舉一個簡單的helloworld.proto例子:
syntax = "proto3";package helloworld;import "google/api/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; import "errors/errors.proto";option go_package = "github.com/go-kratos/kratos/examples/helloworld/helloworld";// The greeting service definition. service Greeter { // Sends a greetingrpc SayHello (HelloRequest) returns (HelloReply) ?{option (google.api.http) = { // 定義一個HTTP GET 接口,并且把 name 映射到 HelloRequest get: "/helloworld/{name}",}; // 添加API接口描述(swagger api) option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { description: "這是SayHello接口";};} }// The request message containing the user's name. message HelloRequest { // 增加name字段參數(shù)校驗,字符數(shù)需在1到16之間string name = 1 [(validate.rules).string = {min_len: 1, max_len: 16}]; }// The response message containing the greetings message HelloReply {string message = 1; }enum ErrorReason { // 設(shè)置缺省錯誤碼option (errors.default_code) = 500; // 為某個錯誤枚舉單獨設(shè)置錯誤碼USER_NOT_FOUND = 0 [(errors.code) = 404];CONTENT_MISSING = 1 [(errors.code) = 400];; }以上是一個簡單的helloworld服務定義的例子,這里我們定義了一個Service叫Greeter,給Greeter添加了一個SayHello的接口,并根據(jù)googleapis規(guī)范給這個接口添加了Restful風格的HTTP接口定義,然后還利用openapiv2添加了接口的Swagger API描述,同時還給請求消息結(jié)構(gòu)體HelloRequest中的name字段加上了參數(shù)校驗,最后我們在文件的末尾還定義了這個服務可能返回的錯誤碼。
這時我們在終端中執(zhí)行:kratos proto client api/helloworld/ helloworld.proto 便可以生成以下文件:
由上,我們看到Kraots腳手架工具幫我們一鍵生成了上面提到的能力。從這個例子中,我們可以直觀感受到使用使用Protobuf帶來的開發(fā)效率的提升,除此之外Kratos還有以下優(yōu)點:
清晰:做到了定義即文檔,定義即代碼
收斂,統(tǒng)一:將邏輯都收斂統(tǒng)一到一起,通過代碼生成工具來保證HTTP Service、grpc Service等功能具有一致的行為
跨語言:眾所周知Protobuf是跨語言的,java、go、python、php、js、c等等主流語言都支持
擁抱開源生態(tài):比如Kratos復用了google.http.api、protoc-gen-openapiv2、protoc-gen-validate 等等一些犀利的Protobuf周邊生態(tài)工具或規(guī)范,這比起自己造一個IDL的輪子要容易維護得多,同時老的使用這些輪子的gRPC項目遷移成本也更低
開放性
一個基礎(chǔ)框架在設(shè)計的時候就要考慮未來的可擴展性,那Kratos是怎么做的呢?
1. Server Transport
我們先看下服務協(xié)議層的代碼:
上面是Kratos RPC服務協(xié)議層的接口定義,這里我們可以看到如果想要給Kratos新增一個新的服務協(xié)議,只要實現(xiàn)Start()、Stop()、Endpoint()這幾個方法即可。這樣的設(shè)計解耦了應用和服務協(xié)議層的實現(xiàn),使得擴展服務協(xié)議更加方便。
從上圖中我們可以看到App層無需關(guān)心底層服務協(xié)議的實現(xiàn),只是一個容器管理好應用配置、服務生命周期、加載順序即可。
2. Log
我們再看一個Kratos日志模塊的設(shè)計:
這里Kratos定義了一個日志輸出接口Logger,它的設(shè)計的非常簡單 - 只用了一個方法、兩個輸入、一個輸出。我們知道一個包暴露的接口越少,越容易維護,同時對使用和實現(xiàn)方的心智負擔更小,擴展日志實現(xiàn)會變得更容易。但問題來了,這個接口從功能上來講似乎只能輸出日志level和固定的kv paris,如何能支持更高級的功能?比如輸出 caller stack、實時timestamp、 context traceID ?這里我們定義了一個回調(diào)接口Valuer:? ? ? ? ? ? ? ? ? ? ? ??
這個Valuer可以被當作key/value pairs中的value被Append到日志里,并被實時調(diào)用。
我們看一下如何給日志加時間戳的Valuer實現(xiàn):
使用時只要在原始的logger上再append一個固定的key和一個動態(tài)的valuer即可:
這里的With是一個Helper function,里面new了一個新的logger(也實現(xiàn)了Logger接口),并將key\value pairs暫存在新的logger里,等到Log方法被調(diào)用時再通過斷言.(Valuer)的方式獲取值并輸出給底層原始的logger。
所以我們可以看到僅僅通過兩個簡單的接口+一個Helper function的組合我們就實現(xiàn)了日志的大多數(shù)功能,這樣大大提高了可擴展性。實際上還有日志過濾、多日志源輸出等功能也是通過組合使用這兩接口來實現(xiàn),這里待下次分享再展開細講。
3. Tracing
最后我們來看下Kratos的Tracing組件,這里Kratos采用的是CNCF項目OpenTelemetry。
OpenTelemetry在設(shè)計之初就考慮到了組件化和高可擴展性,其實現(xiàn)了OpenTracing和W3C Trace Context的規(guī)范,可以無縫對接zipkin、jaeger等主流開源tracing系統(tǒng),并且可以自定義Propagator 和 TraceProvider。通過otel.SetTracerProvider()我們可以輕易得替換Span的落地協(xié)議和格式,從而兼容老系統(tǒng)中的trace采集agent;通過otel.SetTextMapPropagtor()我們可以替換Span在RPC中的Encoding協(xié)議,從而可以和老系統(tǒng)中的服務互相調(diào)用時也能兼容。
工程流程
我們知道在工程實踐的時候,強規(guī)范和約束往往比自由和更多的選擇更有優(yōu)勢,那么在Go工程規(guī)范這塊我這里主要介紹三塊:
1. 面向包的設(shè)計規(guī)范
Go 是一個面向包名設(shè)計的語言,Package 在 Go 程序中主要起到功能隔離的作用,標準庫就是很好的設(shè)計范例。Kratos也是可以按包進行組織代碼結(jié)構(gòu),這里我們抽取Kratos根目錄下主要幾個Package包來看下:
/cmd: 可以通過 go install 一鍵安裝生成工具,使用戶更加方便地使用框架。
/api: Kratos框架本身的暴露的接口定義
/errors: 統(tǒng)一的業(yè)務錯誤封裝,方便返回錯誤碼和業(yè)務原因。
/config: 支持多數(shù)據(jù)源方式,進行配置合并鋪平,通過 Atomic 方式支熱更配置。
/internal:存放對外不可見或者不穩(wěn)定的接口。
/transport: 服務協(xié)議層(HTTP/gRPC)的抽象封裝,可以方便獲取對應的接口信息。
/middleware: 中間件抽象接口,主要跟transport 和 service 之間的橋梁適配器。
/third_party: 第三方外部的依賴
可以看到Kratos的包命名清晰簡短,按功能進行劃分,每個包具有唯一的職責。
在設(shè)計包時我們還需要考慮到以下幾點:
包的設(shè)計必須以使用者為中心,直觀且易于使用,包的命名必須旨在描述它提供的內(nèi)容,如果包的名稱不能立即暗示這一點,則它可能包含一組零散的功能。
包的目的是為特定問題域而提供的,為了有目的,包必須提供,而不是包含。包不能成為不同問題域的聚合地,隨著時間的推移,它將影響項目的簡潔和重構(gòu)、適應、擴展和分離的能力。
高便攜性,盡量減少依賴其他代碼庫,一個包與其它包依賴越少,一個包的可重用性就越高。
不能成為單點依賴,當包被單一的依賴點時,就像一個公共包(common),會給項目帶來很高的耦合性。
2. 配置
首先,我們來看下常見的基礎(chǔ)框架是怎么初始化配置的:
這是Go標準庫HTTP Server配置初始化的例子,但是這樣做會有如下幾個問題:
&http.Server{}由于是一個取址引用,里面的參數(shù)可能會被外部運行時修改,這種運行時修改帶來的危害是不可把控的。
無法區(qū)分nil和0值,當里面的參數(shù)值為0的時候,不知道是用戶未設(shè)置還是就是被設(shè)置成了0。
難以分辨必傳和選傳參數(shù),只能通過文檔說明來隱式約定,沒有強約束力。
那么Kraots是怎么解決這些問題的呢?答案就是Functional Options 。我們看下transport/http/client.go的代碼:
Client.go中定義了一個回調(diào)函數(shù)ClientOption,該函數(shù)接受一個定義了一個存放實際配置的未導出結(jié)構(gòu)體clientOptions的指針,然后我們在NewClient的時候,使用可變參數(shù)進行傳遞,然后再初始化函數(shù)內(nèi)部通過 for 循環(huán)調(diào)用修改相關(guān)的配置。
這么做有這么幾個好處:
由于clientOptions結(jié)構(gòu)體是未導出的,那么就不存在被外部修改的可能。
可以區(qū)分0值和未設(shè)置,首先我們在new clientOptions時會設(shè)置默認參數(shù),那么如果外部沒有傳遞相應的Option就不會修改這個默認參數(shù)。
必選參數(shù)顯示定義,可選值則通過Go可變參數(shù)進行傳遞,很好的區(qū)分必傳和選傳。
3. Error規(guī)范
Kratos為微服務提供了統(tǒng)一的Error模型:
Code用作外部展示和初步判斷,服務端無需定義大量全局唯一的XXX_NOT_FOUND,而是使用一個標準Code.NOT_FOUND錯誤代碼并告訴客戶端找不到某個資源。錯誤空間變小降低了文檔的復雜性,在客戶端庫中提供了更好的慣用映射,并降低了客戶端的邏輯復雜性。同時這種標準的大類Code的存在也對外部的觀測系統(tǒng)更友好,比如可以通過分析Nginx Access Log中的HTTP StatusCode來做服務端監(jiān)控和告警。
Reason是具體的錯誤原因,可以用來更詳細的錯誤判定。每個微服務都會定義自己Reason,那么要保持全局唯一就需要加上領(lǐng)域前綴,比如User_XXX。
Message錯誤信息可以幫助用戶輕松快捷地理解和解決API 錯誤
Metadata中則可以存放一些標準的錯誤詳情,比如retryInfo、error stack等
這種強制規(guī)范,避免了開發(fā)人員直接透傳Go的error 從而導致一些敏感信息泄露。
接下來我們看下Error結(jié)構(gòu)體還實現(xiàn)了哪些接口:
實現(xiàn)了GRPCStatus () *status.Status 接口,這樣就實現(xiàn)了從http status code到grpc status code的轉(zhuǎn)換,這樣Kratos Error可以被gRPC直接轉(zhuǎn)成google.rpc.Status傳遞出去。
實現(xiàn)了標準庫errors包的Is (error) bool接口,這樣使用者可以直接調(diào)用errors.Is()來比較兩個erorr中的reason是否相等,避免了使用==來直接判斷error是否相等這種錯誤姿勢。
依賴注入
依賴注入?(Dependency Injection)可以理解為一種代碼的構(gòu)造模式,按照這樣的方式來寫,能夠讓你的代碼更加容易維護,一般在Java的項目中見到的比較多。
依賴注入初看起來比較違反直覺,那么為什么Go也需要依賴注入?假設(shè)我們要實現(xiàn)一個用戶訪問計數(shù)的功能。我們先看看不使用依賴注入的項目代碼:
type Service struct {redisCli *redis.Client }func (s *Service) AddUserCount(ctx context.Context) {//do some business logics.redisCli.Incr(ctx, "user_count") }func NewService(cfg *redis.Options) *Service {return &Service{redisCli: redis.NewClient(cfg)} }這種方式比較常見,在項目剛開始或者規(guī)模小的時候沒什么問題,但我們?nèi)绻紤]下面這些因素:
Redis是基礎(chǔ)組件,往往會在項目的很多地方被依賴,那么如果哪天我們想整體修改redis sdk的甚至想把redis 整體替換成mysql時,需要在每個被用到的地方都進行修改,耗時耗力還容易出錯。
很難對App這個類寫單元測試,因為我們需要創(chuàng)建一個真實的redis.Client。
使用依賴注入改造后的Service:
type DataSource interface{Incr(context.Context, string) }type Service struct {dataSource DataSource }func (s *Service) AddUserCount(ctx context.Context) {//do some business logics.dataSource.Incr(ctx, "user_count") }func NewService(ds DataSource) *Service {return &Service{dataSource: ds} }上面代碼中我們把*redis.Client實體替換成了一個DataSource接口,同時不控制dataSource的創(chuàng)建和銷毀,把dataSource生命周期控制權(quán)交給了上層來處理,以上操作有三個主要原因:
因為Service層已不再關(guān)心dataSource的創(chuàng)建和銷毀,這樣當我們需要修改dataSource實現(xiàn)的時候,只要在上層統(tǒng)一修改即可,無需在各個被依賴的地方一一修改。
因為依賴的是一個接口,我們寫單元測試的時候只要傳遞一個mock后的Datasource實現(xiàn)即可 。
這里dataSource這個基礎(chǔ)組件不再被會到處創(chuàng)建,可以做到復用一個單例節(jié)省資源開銷。
Go 的依賴注入框架有兩類,一類是通過反射在運行時進行依賴注入,典型代表是 uber 開源的 dig,另外一類是通過 generate 進行代碼生成,典型代表是 Google 開源的 wire。使用 dig 功能會強大一些,但是缺點就是錯誤只能在運行時才能發(fā)現(xiàn),這樣如果不小心的話可能會導致一些隱藏的 bug 出現(xiàn)。使用 wire 的缺點就是功能限制多一些,但是好處就是編譯的時候就可以發(fā)現(xiàn)問題,并且生成的代碼其實和我們自己手寫相關(guān)代碼差不太多,更符合直覺,心智負擔更小。所以Kratos更加推薦 wire,Kratos的默認項目模板中 kratos-layout 也正是使用了 google/wire 進行依賴注入。
我們來看下wire使用方式:
我們首先要定義一個ProviderSet,這個Set會返回構(gòu)建依賴關(guān)系所需的組件Provider。如下所示,Provider往往是一些簡單的工廠函數(shù),這些函數(shù)不會太復雜:
type RedisSource struct {redisCli *redis.Client }// RedisSource實現(xiàn)了Datasource的Incr接口 func (ds *RedisSource) Incr(ctx context.Context, key string) {ds.redisCli.Incr(ctx, key) }// 構(gòu)建實現(xiàn)了DataSource接口的Provider func NewRedisSource(db *redis.Client) *RedisSource {return &RedisSource{redisCli: db} }// 構(gòu)建*redis.Client的Provider func NewRedis(cfg *redis.Options) *redis.Client {return redis.NewClient(cfg) } // 這是一個Provider的集合,告訴wire這個包提供了哪些Provider var ProviderSet = wire.NewSet(NewRedis, NewRedisSource)接著我們要在應用啟動處新建一個wire.go文件并定義Injector,Injctor會分析依賴關(guān)系并將Provider串聯(lián)起來構(gòu)建出最終的Service:
// +build wireinjectfunc initService(cfg *redis.Options) *service.Service {panic(wire.Build(redisSource.ProviderSet, //使用 wire.Bind 將 Struct 和接口進行綁定了,表示這個結(jié)構(gòu)體實現(xiàn)了這個接口, wire.Bind(new(data.DataSource), new(*redisSource.RedisSource)),service.NewService),) }最后執(zhí)行wire .后自動生成的代碼如下:
//go:generate go run github.com/google/wire/cmd/wire //+build !wireinjectfunc initService(cfg *redis.Options) *service.Service {client := redis2.NewRedis(cfg)redisSource := redis2.NewRedisSource(client)serviceService := service.NewService(redisSource)return serviceService }由此我們可以看到只要定義好組件初始化的Provider函數(shù),還有把這些Provider組裝在一起的Injector就可以直接生成初始化鏈路代碼了,上手還是相對簡單的,生成的代碼所見即所得,容易Debug。
綜上可見,Kratos是一款凝結(jié)了開源社區(qū)力量以及Go同學們大量微服務工程實踐后誕生的一款微服務框架。現(xiàn)在騰訊云微服務治理治理平臺(微服務平臺TSF)也已支持Kratos框架,給Kratos賦予了更多企業(yè)級服務治理能力、提供多維度服務,如:應用生命周期托管、一鍵上云、私有化部署、多語言發(fā)布。
(掃描二維碼查看Go接入TSF騰訊云文檔)
免費體驗館
消息隊列CKafka
分布式、高吞吐量、高可擴展性的消息服務,具備數(shù)據(jù)壓縮、同時支持離線和實時數(shù)據(jù)處理等優(yōu)點。
掃碼即可免費體驗
免費體驗路徑:云產(chǎn)品體驗->基礎(chǔ)->消息隊列CKafka
消息隊列TDMQ
一款基于 Apache 頂級開源項目 Pulsar 自研的金融級分布式消息中間件。其計算與存儲分離的架構(gòu)設(shè)計,使得它具備極好的云原生和 Serverless 特性,用戶按量使用,無需關(guān)心底層資源。
掃碼點擊“立即使用”,即可免費體驗
微服務平臺TSF
穩(wěn)定、高性能的技術(shù)中臺。一個圍繞著應用和微服務的 PaaS 平臺,提供應用全生命周期管理、數(shù)據(jù)化運營、立體化監(jiān)控和服務治理等功能。TSF 擁抱 Spring Cloud 、Service Mesh 微服務框架,幫助企業(yè)客戶解決傳統(tǒng)集中式架構(gòu)轉(zhuǎn)型的困難,打造大規(guī)模高可用的分布式系統(tǒng)架構(gòu),實現(xiàn)業(yè)務、產(chǎn)品的快速落地。
掃碼點擊“免費體驗”,即可免費體驗
微服務引擎TSE
高效、穩(wěn)定的注冊中心托管,助力您快速實現(xiàn)微服務架構(gòu)轉(zhuǎn)型。
掃碼點擊“立即申請”,即可免費體驗
彈性微服務TEM
面向微服務應用的 Serverless PaaS 平臺,實現(xiàn)資源 Serverless 化與微服務架構(gòu)的完美結(jié)合,提供一整套開箱即用的微服務解決方案。彈性微服務幫助用戶創(chuàng)建和管理云資源,并提供秒級彈性伸縮,用戶可按需使用、按量付費,極大程度上幫用戶節(jié)約運維和資源成本。讓用戶充分聚焦企業(yè)核心業(yè)務本身,助力業(yè)務成功。
掃碼點擊“立即申請”,即可免費體驗
往期
推薦
《【陣容擴大】三位騰訊Maintainer加入Apache Pulsar生態(tài)項目RocketMQ-on-Palsar》
《Apache Pulsar事務機制原理解析|Apache Pulsar 技術(shù)系列》
《騰訊云中間件月報(2021年第六期)》
掃描下方二維碼關(guān)注本公眾號,
了解更多微服務、消息隊列的相關(guān)信息!
解鎖超多鵝廠周邊!
戳原文,了解更多騰訊微服務平臺相關(guān)信息
總結(jié)
以上是生活随笔為你收集整理的Kratos技术系列|从Kratos设计看Go微服务工程实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据上报痛点解决方案
- 下一篇: 大牛书单 | 云原生技术领域好书推荐