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 ServiceHTTP Service請求參數校驗錯誤定義Swagger API json應用配置模版等都是基於 Protobuf IDL 來構建的:

項目生態

圍繞着 kratos v2 版本的核心設計理念,設計瞭如下的項目生態:

架構設計

kratos v2 版本在設計階段主要進行了以下幾個方面的思考:

面向包的設計理念

kratos v2 框架中,我們主要是參考了 Go 的基礎庫設計思想,包名按照實際功能劃分,每個包都具有單一的職責,當用戶不可見或者不穩定的接口放到了 / internal 目錄中。並且在框架中不同包具有不同的功能特性:

Transport HTTP/gRPC

kratos v2 框架對傳輸層進行了抽象,用戶也可以實現自己的傳輸層,框架默認實現了 gRPCHTTP 兩種通信協議傳輸層。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 啓動器進行管理服務生命週期。啓動器主要處理:

// 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 獲取對應鍵值的內容,主要功能如下:

配置規範的思考

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 包中,主要實現了 HTTPgRPC 的接口:

業務錯誤,主要參考了 gRPC errdetails.ErrorInfo 的實現:

實際使用

編寫 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 日誌模塊中,主要分爲 LoggerHelperFilterValuer 的實現。爲了方便擴展,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,例如 timestampcallertrace 等。在 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 格式爲:

使用

Metadata 的主要用法爲:

// 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,進行通用的業務處理,比如用戶鑑權等。主要的內置中間件:

簡化的 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 相關生態的開發中。

相關資料

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kZK0jLUNMTBtHgfFxI8_VQ