Kratos 框架 v2 版本架構演進之路
Kratos 是一套輕量級 Go 微服務框架,包含大量微服務相關功能及工具。名字來源於遊戲《戰神》,該遊戲以希臘神話爲背景,講述了奎託斯(Kratos)由凡人成爲戰神並展開弒神屠殺的冒險歷程。
寫在前面
從 2021 年 2 月份,github 上 kratos v2
(下文簡稱 kratos) 版本第一次代碼提交,到功能模塊的討論,修改,測試,最終定稿,已經過去了 13 個月,在社區各位夥伴的貢獻下,kratos v2
已經從 2.0.0 alpha1
版本迭代到了 2.2.1
版本,已經具備微服務框架的完整能力。在此感謝各位社區夥伴的貢獻。
概覽
kratos v1
(下文簡稱 v1) 版本在設計時,後期的可擴展性考慮較少,框架模塊與實現強依賴,類似於全家桶,導致框架本身靈活性不高,框架使用者無法更換框架模塊的具體實現,沒有辦法在不衍生下游版本的前提下對框架功能實現進行替換,在實際的企業開發中對於企業的多樣化需求,無法輕鬆地應對,遇到這種需求時,只能通過修改框架代碼來實現。而kratos v2
版本它更像是一個採用 Go 語言構建微服務的工具箱,開發者可以按照自己的習慣像搭積木一樣來構建自己的微服務。也正是由於這樣的原因,kratos v2
並不會直接綁定某種特定的基礎設施,所以可以很輕鬆地將任意您想要的庫集成到項目中,與 kratos v2
共同協作。
kratos v2
版本的設計思想就是支持高度自由的定製化,框架制定接口規範,然後通過插件來實現具體需求,實現高度可拔插的微服務框架,企業在開發時可以選擇框架已經提供的插件實現,也可以自己定製插件,實現了高度的可定製。並且在 kratos v2
版本中 API定義
、gRPC Service
、HTTP Service
、請求參數校驗
、錯誤定義
、Swagger API json
、應用配置模版
等都是基於 Protobuf IDL 來構建的:
項目生態
圍繞着 kratos v2
版本的核心設計理念,設計瞭如下的項目生態:
-
kratos
框架核心,包含了基礎的 CLI 工具,內置支持了 HTTP/gRPC 傳輸協議,提供了服務的完整聲明週期管理,提供瞭如 API 、日誌 、錯誤處理、 配置、監控、序列化、註冊發現、元數據傳遞、傳輸層、中間件等組件能力和相關接口定義。 -
contrib
基於框架核心定義的基礎接口,實現了對配置文件、日誌系統、服務發現、監控等基礎服務設施的適配,可以讓開發者直接集成到 Kratos 項目中來。 -
aegis
服務可用性的相關算法如:限流、熔斷等。算法放在了獨立的項目中,幾乎沒有外部依賴,即使您的項目沒有依賴 Kratos 框架,您也可以直接任意項目中使用它。 -
layout
參考了《領域驅動設計》和《簡潔架構設計》的項目模板,並且提供了 Makefile 腳本和 Dockerfile 文件。我們推薦您使用 kratos 提供的項目結構,但您可以隨意修改這個模板,或者使用自己喜歡的項目結構,框架本身不對項目做任何限制,您可以按照自己的想法來使用,具有很強的可定製性。 -
gateway
一個使用 Go 語言開發的 API Gateway,後續您可以使用它作爲您項目的微服務網關,用於微服務 API 的治理,項目正在研發中,敬請期待。
架構設計
kratos v2
版本在設計階段主要進行了以下幾個方面的思考:
-
面向包的設計理念
-
Transport HTTP/gRPC
-
應用生命週期管理
-
配置規範的思考
-
業務錯誤的設計
-
日誌接口的設計
-
Metadata 傳遞和使用
-
Middleware 使用
-
簡化的 DDD layout 實現
面向包的設計理念
在 kratos v2
框架中,我們主要是參考了 Go 的基礎庫設計思想,包名按照實際功能劃分,每個包都具有單一的職責,當用戶不可見或者不穩定的接口放到了 / internal 目錄中。並且在框架中不同包具有不同的功能特性:
-
/cmd
cmd 中包含了可以通過 go install 或 go get 一鍵安裝的命令行工具,使用戶可以更加方便的使用框架。 -
/errors
統一的業務錯誤封裝,便捷的返回錯誤碼以及具體的業務錯誤原因。 -
/config
支持多數據源接入,可以對配置進行合併,平鋪,通過 Atomic 方式支持配置熱更新。 -
/transport
傳輸層(HTTP/gRPC)的抽象封裝。 -
/middleware
中間件的抽象接口,主要作爲 transport 和 service 之間的橋樑適配器。 -
/metadata
跨服務跨協議間的元數據傳遞及使用 -
/registry
註冊中心的抽象接口,可以實現支持各種服務註冊與發現中心,如:etcd、consul、nacos。
Transport HTTP/gRPC
kratos v2
框架對傳輸層進行了抽象,用戶也可以實現自己的傳輸層,框架默認實現了 gRPC
和 HTTP
兩種通信協議傳輸層。Transport
主要的接口:
// 服務的啓動和停止,用於管理服務生命週期。
type Server interface {
Start(context.Context) error
Stop(context.Context) error
}
// 用於實現註冊到註冊中心的終端地址
// 如果不實現這個方法則不會註冊到註冊中心
type Endpointer interface {
Endpoint() (*url.URL, error)
}
// 請求頭的元數據
type Header interface {
Get(key string) string
Set(key string, value string)
Keys() []string
}
// Transporter is transport context value interface.
type Transporter interface {
// 代表實現的通訊協議的類型。
Kind() Kind
// 提供的服務終端地址。
Endpoint() string
// 用於標識服務的方法路徑
Operation() string
RequestHeader() Header
ReplyHeader() Header
}
應用生命週期管理
在 kratos v2
中,可以通過實現 transport.Server
接口,然後通過 kratos.New 啓動器進行管理服務生命週期。啓動器主要處理:
-
server 生命週期管理
-
registry 註冊中心管理
// AppInfo is application context value.
type AppInfo interface {
ID() string
Name() string
Version() string
Metadata() map[string]string
Endpoint() []string
}
配置規範的思考
在使用 kratos v2
中,配置源可以指定多個,並且 Config
包會對配置合併成 key/value
,然後用戶可以通過 Scan
或者 value
獲取對應鍵值的內容,主要功能如下:
-
內置實現了基於本地文件的數據源。
-
用戶可以通過插件接入自定義數據源如:nacos、consul、apollo 等。
-
支持配置熱更新(watch),通過 Atomic 方式變更已有鍵的值。
-
支持自定義數據源 Decode 實現。
-
支持對 flags、環境變量 佔位符的替換。
-
可以對鋪平的 key/value,進行二次賦值替換。
配置規範的思考
在 kratos v2
中,默認通過 proto 定義配置的模板,主要有以下幾點好處:
-
可以定義統一的模板配置
-
添加對應的配置校驗
-
更好的管理配置
-
多語言支持
message Bootstrap {
Server server = 1;
Data data = 2;
}
message Server {
message HTTP {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
message GRPC {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
HTTP http = 1;
GRPC grpc = 2;
}
message Data {
message Database {
string driver = 1;
string source = 2;
}
message Redis {
string network = 1;
string addr = 2;
google.protobuf.Duration read_timeout = 3;
google.protobuf.Duration write_timeout = 4;
}
Database database = 1;
Redis redis = 2;
}
server:
http:
addr: 0.0.0.0:8000
timeout: 1s
grpc:
addr: 0.0.0.0:9000
timeout: 1s
data:
database:
driver: mysql
source: root:root@tcp(127.0.0.1:3306)/test
redis:
addr: 127.0.0.1:6379
read_timeout: 0.2s
write_timeout: 0.2s
業務錯誤處理
在 kratos v2
中,業務錯誤主要通過 proto enum
進行定義。在 errors
包中,主要實現了 HTTP
和 gRPC
的接口:
-
StatusCode() int
-
GRPCStatus() *grpc.Status
業務錯誤,主要參考了 gRPC errdetails.ErrorInfo
的實現:
-
code
錯誤碼,跟 http-status 一致,並且在 grpc 中可以轉換爲 grpc-status。 -
message
錯誤信息,用戶可讀的信息,可作爲用戶提示內容。 -
reason
錯誤原因,定義爲業務判定的錯誤碼。 -
metadata
錯誤元信息,可以向錯誤附加可擴展信息。
實際使用
編寫 proto
文件
syntax = "proto3";
package helloworld.v1;
import "errors/errors.proto";
option go_package = "github.com/go-kratos/kratos-layout/api/helloworld/v1;v1";
option java_multiple_files = true;
option java_package = "helloworld.v1.errors";
option objc_class_prefix = "APIHelloworldErrors";
enum ErrorReason {
USER_NOT_FOUND = 0;
CONTENT_MISSING = 1;
}
在 biz
中依賴 proto enum
定義錯誤
var (
// ErrUserNotFound is user not found.
ErrUserNotFound = errors.NotFound(v1.ErrorReason_USER_NOT_FOUND.String(), "user not found")
)
使用錯誤
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
save, err := uc.repo.Save(ctx, g)
if err != nil {
return nil, ErrUserNotFound.WithMetadata(map[string]string{"error":err.Error()})
}
return save, nil
}
判定錯誤
// reason
if err != nil {
if errors.Reason(err) == v1.ErrorReason_USER_NOT_FOUND.String() {
// TODO: do something
}
return nil, err
}
// errors.As
if err != nil {
if se := new(errors.Error); errors.As(err,&se) {
switch se.Reason {
case v1.ErrorReason_USER_NOT_FOUND.String():
// TODO: do something
}
}
}
// errors.Is
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// TODO: do something
}
}
日誌接口設計
在 kratos v2
日誌模塊中,主要分爲 Logger
、Helper
、Filter
、Valuer
的實現。爲了方便擴展,Logger
接口定義非常簡單:
type Logger interface {
Log(level Level, keyvals ...interface{}) error
}
這個 Logger
接口,非常容易組合和擴展:
// 也可以定義多種日誌輸出 log.MultiLogger(out, err),例如:info/warn/error,file/agent
logger := log.NewStdLogger(os.Stdout)å
// 根據日誌級別進行過慮日誌,或者 Key/Value/FilterFunc
logger := log.NewFilter(logger, log.FilterLevel(log.LevelInfo))
// 輸出結構化日誌
logger.Log(log.LevelInfo, "msg", "log info")
如果需要過濾日誌中某些不應該被打印明文的字段,例如 password 等信息,可以通過 log.NewFilter()
來實現過濾功能。
logger := log.NewFilter(
log.DefaultLogger,
log.FilterLevel(log.LevelInfo), // 通過 Level 過濾日誌
log.FilterKey("password"), // 通過 Key 過濾日誌
log.FilterValue("123456"), // 通過 Value 過濾日誌
log.FilterFunc(func(level Level, keyvals ...interface{}) bool { // 通過自定義 FilterFunc
return level == log.LevelError
})
logger.Log(log.LevelInfo, "password", "123456") // 輸出格式爲:password=***
通常在使用日誌的過程中,我們可以通過 log.With()
和 Hook
定製 Fields
,例如 timestamp
、caller
、trace
等。在 kratos
日誌模塊中,主要通過實現 Valuer
進行定製化。
type Valuer func(ctx context.Context) interface{}
func Value(ctx context.Context, v interface{}) interface{} {
if v, ok := v.(Valuer); ok {
return v(ctx)
}
return v
}
所以,我們在 kratos v2
項目中可以這樣使用日誌模塊:
logger := log.NewStdLogger(os.Stdout)
logger = log.NewFilter(logger, log.FilterLevel(log.LevelInfo))
logger = log.With(logger, "app", "helloworld",
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"trace_id", log.TraceID(),
"span_id", log.SpanID(),
)
helper := log.NewHelper(logger)
helper.WithContext(ctx).Info("info log")
Metadata 傳遞和使用
微服務之間主要通過 HTTP/gRPC
進行接口交互,所以在服務架構中應該進行統一的元數據傳遞和使用。在 HTTP/gRPC
中,其實是通過 HTTP Header
進行傳遞,在框架中首先通過 metadata
包將元數據封裝成 key/value
結構,然後攜帶到 Transport Header
中。
Metadata
默認 Key
格式爲:
-
x-md-blobal-xxx
全局傳遞,例如mirror
、color
、criticality
-
x-md-local-xxx
局部傳遞,例如caller
並且用戶可以在 middleware/metadata 中定製自己的key prefix
,配置固定的元數據傳遞。
使用
Metadata
的主要用法爲:
-
配置
client/server
對應的middleware/metadata
插件,可以自定義傳遞key prefix
,或者metadata
常量,例如caller
。 -
然後通過
metadata
包,NewClientContext
或者FromServerContext
進行配置或者獲取。
// server
grpcSrv := grpc.NewServer(
grpc.Address(":9000"),
grpc.Middleware(
metadata.Server(),
),
)
// client
conn, err := grpc.DialInsecure(
context.Background(),
grpc.WithEndpoint("127.0.0.1:9000"),
grpc.WithMiddleware(
metadata.Client(),
),
)
// 獲取
if md, ok := metadata.FromServerContext(ctx); ok {
extra = md.Get("x-md-global-extra")
}
// 傳遞
ctx = metadata.AppendToClientContext(ctx, "x-md-global-extra", "2233")
Middleware 使用
kratos v2
內置了一系列的中間件用於處理日誌、指標、跟蹤鏈等通用場景。用戶也可以通過實現 Middleware
接口,開發自定義 middleware
,進行通用的業務處理,比如用戶鑑權等。主要的內置中間件:
-
recovery
用於 recovery panic -
tracing
用於啓用 trace -
logging
用於請求日誌的記錄 -
metrics
用於啓用 metrics -
validate
用於處理參數校驗 -
metadata
用於啓用元信息傳遞 -
etc...
簡化的 DDD 實現
如果你嘗試學習 Go,或者你正在爲自己建立一個 PoC
或一個玩具項目,這個項目佈局是沒啥必要的。從一些非常簡單的事情開始(一個 main.go 文件綽綽有餘)。當有更多的人蔘與這個項目時,你將需要更多的結構,包括需要一個 Toolkit
來方便生成項目的模板,儘可能大家統一的工程目錄佈局。
.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── api // 下面維護了微服務使用的proto文件以及根據它們所生成的go文件
│ └── helloworld
│ └── v1
│ ├── error_reason.pb.go
│ ├── error_reason.proto
│ ├── error_reason.swagger.json
│ ├── greeter.pb.go
│ ├── greeter.proto
│ ├── greeter.swagger.json
│ ├── greeter_grpc.pb.go
│ └── greeter_http.pb.go
├── cmd // 整個項目啓動的入口文件
│ └── server
│ ├── main.go
│ ├── wire.go // 我們使用wire來維護依賴注入
│ └── wire_gen.go
├── configs // 這裏通常維護一些本地調試用的樣例配置文件
│ └── config.yaml
├── generate.go
├── go.mod
├── go.sum
├── internal // 該服務所有不對外暴露的代碼,通常的業務邏輯都在這下面,使用internal避免錯誤引用
│ ├── biz // 業務邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,repo 接口在這裏定義,使用依賴倒置的原則。
│ │ ├── README.md
│ │ ├── biz.go
│ │ └── greeter.go
│ ├── conf // 內部使用的config的結構定義,使用proto格式生成
│ │ ├── conf.pb.go
│ │ └── conf.proto
│ ├── data // 業務數據訪問,包含 cache、db 等封裝,同時也是 rpc 調用的 acl 防腐層,它實現了 biz 的 repo 接口。我們可能會把 data 與 dao 混淆在一起,data 偏重業務的含義,它所要做的是將領域對象重新拿出來,我們去掉了 DDD 的 infra層。
│ │ ├── README.md
│ │ ├── data.go
│ │ └── greeter.go
│ ├── server // http和grpc實例的創建和配置
│ │ ├── grpc.go
│ │ ├── http.go
│ │ └── server.go
│ └── service // 實現了 api 定義的服務層,類似 DDD 的 application 層,處理 DTO 到 biz 領域實體的轉換(DTO -> DO),同時協同各類 biz 交互,但是不應處理複雜邏輯
│ ├── README.md
│ ├── greeter.go
│ └── service.go
└── third_party // api 依賴的第三方proto
├── README.md
├── google
│ └── api
│ ├── annotations.proto
│ ├── http.proto
│ └── httpbody.proto
└── validate
├── README.md
└── validate.proto
未來規劃
綜上可見,kratos v2
是一款凝結了開源社區力量以及 Go 同學們大量微服務工程實踐後誕生的一款微服務框架,現階段 kratos v2
框架已經功能逐漸完善,後續先期會將精力主要放在 kratos gateway
上,同時會開始 Kratos API interface
和服務治理平臺 Kratos ui
的規劃。在此也歡迎廣大 gopher 加入 kratos
社區參與到 kratos
相關生態的開發中。
相關資料
-
官網
go-kratos.dev -
kratos
https://github.com/go-kratos/kratoskratos gateway
https://github.com/go-kratos/gatewaykratos contrib
https://github.com/go-kratos/kratos/tree/main/contribkratos aegis
https://github.com/go-kratos/aegis
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/kZK0jLUNMTBtHgfFxI8_VQ