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