前言
之前业务侧从node过度到go, 由于基本的业务逻辑由业务中台来承载, 复杂度不算高, 包括公司内SRE的管理基建模块也是用gin搭建的, 内部使用是没问题,但对外的复杂的业务逻辑对于gin的封装还远远不够。
Gin
第一版用的是 https://github.com/xinliangnote/go-gin-api 这个项目对于gin的封装主要拿来改了context 和 core 部分, go-gin-api对gin的封装很简单,借由gin的中间件利用sync.pool重新封装上下文,并在新的context上绑定paylod或者token等key标识,logger等方法,并提供响应的方法,方便后续开发,并在全局使用recover,和错误处理中间件。
if err != nil { ctx.AbortWithError(err) return } ctx.Payload(d)
可以达成上面的效果, 其实是挂载error到上下文,然后在gin中间件读取key统一处理错误。
Gin所面临的问题
在pay项目中使用感触就是上下文的传递太麻烦, mvc那一套必须传递上下文,不同service 必须 new service对象来实现内聚。
api := r.mux.Group("/api", core.WrapAuthHandler(r.interceptors.CheckLogin), r.interceptors.CheckSignature(), r.interceptors.CheckRBAC()) { // authorized authorizedHandler := authorized.New(r.logger, r.db, r.cache) api.POST("/authorized", authorizedHandler.Create()) api.GET("/authorized", authorizedHandler.List()) api.PATCH("/authorized/used", authorizedHandler.UpdateUsed()) api.DELETE("/authorized/:id", core.AliasForRecordMetrics("/api/authorized/info"), authorizedHandler.Delete()) }
在router层来构建路handler(各service对象) 如: authorized.New(r.logger, r.db, r.cache) , 这样能更有效的实现service的功能,不用重复的初始化service,或者依托方法上下文传递。但这样做还有问题, 还是避免不了由下层依赖服务的层层初始化, 且各种依赖gin来实现的项目各不相同,没有相同的约定会导致代码越写越花, 依托于包装gin context扩展性将会变的很差。
Kratos
Kratos是B站后端框架的开源版本。
官网 Principles
- 简单:不过度设计,代码平实简单;
- 通用:通用业务开发所需要的基础库的功能;
- 高效:提高业务迭代的效率;
- 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠;
- 健壮:通过良好的基础库设计,减少错用;
- 高性能:性能高,但不特定为了性能做 hack 优化,引入 unsafe ;
- 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能;
- 容错性:为失败设计,大量引入对 SRE 的理解,鲁棒性高;
- 工具链:包含大量工具链,比如 cache 代码生成,lint 工具等等;
使用感受:
kratos抽象出transport层, 下层为http(基于mux路由实现) 或grpc, http header 和grpc metadata 统一抽象为metadata, 接口层面采用protobuf进行定义,并使用https://github.com/lazada/protoc-gen-go-http 作为 protoc-gen插件生产http的pb文件。
syntax = "proto3"; package fd_biz_service.v1; import "google/api/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; option go_package = "git.gaoding.com/gaoding/fd-biz-service/api/v1;v1"; option java_multiple_files = true; option java_package = "dev.kratos.api.fd-biz-service.v1"; option java_outer_classname = "FD_BIZ_SERVICE_V1"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Helloworld examples"; version: "1.0"; contact: { name: "gRPC-Gateway project"; url: "https://github.com/grpc-ecosystem/grpc-gateway"; email: "none@example.com"; }; license: { name: "BSD 3-Clause License"; url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; }; extensions: { key: "x-something-something"; value { string_value: "yadda"; } } }; }; // The greeting service definition. service User { // Sends a greeting rpc TestUser (UserRequest) returns (UserReply) { option (google.api.http) = { get: "/user/{name}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a message."; operation_id: "getMessage"; tags: "echo"; responses: { key: "200" value: { description: "OK"; } } }; } rpc GetUserByToken (UserRequest) returns (UserReply) { option (google.api.http) = { get: "/user/token" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Get a message."; operation_id: "getMessage"; tags: "echo"; responses: { key: "200" value: { description: "OK"; } } }; } } // The request message containing the user's name. message UserRequest { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { }; string name = 1; } // The response message containing the greetings message UserReply { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "SimpleMessage" description: "A simple message." required: ["id"] } }; // Id represents the message identifier. string id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The unique identifier of the simple message." }]; }
上面的代码简单演示了定义一个user接口和swagger的定义,参数验证方面可以使用https://github.com/envoyproxy/protoc-gen-validate 实现
定义完后执行
api: protoc --proto_path=. \ --proto_path=./third_party \ --go_out=paths=source_relative:. \ --go-http_out=paths=source_relative:. \ --go-grpc_out=paths=source_relative:. \ --openapi_out=. \ $(API_PROTO_FILES)
可生成api pb文件, 内容如下
// Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 // protoc v3.20.0--rc1 // source: api/fd_biz_service/v1/user.proto package v1 import ( _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) // The request message containing the user's name. type UserRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } func (x *UserRequest) Reset() { *x = UserRequest{} if protoimpl.UnsafeEnabled { mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UserRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserRequest) ProtoMessage() {} func (x *UserRequest) ProtoReflect() protoreflect.Message { mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UserRequest.ProtoReflect.Descriptor instead. func (*UserRequest) Descriptor() ([]byte, []int) { return file_api_fd_biz_service_v1_user_proto_rawDescGZIP(), []int{0} } func (x *UserRequest) GetName() string { if x != nil { return x.Name } return "" } // The response message containing the greetings type UserReply struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields // Id represents the message identifier. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` } func (x *UserReply) Reset() { *x = UserReply{} if protoimpl.UnsafeEnabled { mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UserReply) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserReply) ProtoMessage() {} func (x *UserReply) ProtoReflect() protoreflect.Message { mi := &file_api_fd_biz_service_v1_user_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UserReply.ProtoReflect.Descriptor instead. func (*UserReply) Descriptor() ([]byte, []int) { return file_api_fd_biz_service_v1_user_proto_rawDescGZIP(), []int{1} } func (x *UserReply) GetId() string { if x != nil { return x.Id } return "" } var File_api_fd_biz_service_v1_user_proto protoreflect.FileDescriptor var file_api_fd_biz_service_v1_user_proto_rawDesc = []byte{ 0x0a, 0x20, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x64, 0x5f, 0x62, 0x69, 0x7a, 0x5f, 0x73, 0x65, 0x72, } var ( file_api_fd_biz_service_v1_user_proto_rawDescOnce sync.Once file_api_fd_biz_service_v1_user_proto_rawDescData = file_api_fd_biz_service_v1_user_proto_rawDesc ) func file_api_fd_biz_service_v1_user_proto_rawDescGZIP() []byte { file_api_fd_biz_service_v1_user_proto_rawDescOnce.Do(func() { file_api_fd_biz_service_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_fd_biz_service_v1_user_proto_rawDescData) }) return file_api_fd_biz_service_v1_user_proto_rawDescData } var file_api_fd_biz_service_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_api_fd_biz_service_v1_user_proto_goTypes = []interface{}{ (*UserRequest)(nil), // 0: helloworld.v1.UserRequest (*UserReply)(nil), // 1: helloworld.v1.UserReply } var file_api_fd_biz_service_v1_user_proto_depIdxs = []int32{ 0, // 0: helloworld.v1.User.TestUser:input_type -> helloworld.v1.UserRequest 0, // 1: helloworld.v1.User.GetUserByToken:input_type -> helloworld.v1.UserRequest 1, // 2: helloworld.v1.User.TestUser:output_type -> helloworld.v1.UserReply 1, // 3: helloworld.v1.User.GetUserByToken:output_type -> helloworld.v1.UserReply 2, // [2:4] is the sub-list for method output_type 0, // [0:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name } func init() { file_api_fd_biz_service_v1_user_proto_init() } func file_api_fd_biz_service_v1_user_proto_init() { if File_api_fd_biz_service_v1_user_proto != nil { return } if !protoimpl.UnsafeEnabled { file_api_fd_biz_service_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UserRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_fd_biz_service_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UserReply); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_fd_biz_service_v1_user_proto_rawDesc, NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 1, }, GoTypes: file_api_fd_biz_service_v1_user_proto_goTypes, DependencyIndexes: file_api_fd_biz_service_v1_user_proto_depIdxs, MessageInfos: file_api_fd_biz_service_v1_user_proto_msgTypes, }.Build() File_api_fd_biz_service_v1_user_proto = out.File file_api_fd_biz_service_v1_user_proto_rawDesc = nil file_api_fd_biz_service_v1_user_proto_goTypes = nil file_api_fd_biz_service_v1_user_proto_depIdxs = nil }
wire
kratos 利用wire来实现依赖注入,方便了许多,要对依赖关系很清晰, 入手需要一定的理解成本。
openapi
kratos直接生成的openapi是无法被导入到yapi中的, 线上直接看可以引入github.com/go-kratos/swagger-api/openapiv2,但是需要把一类接口定义写在一个protobuf文件中,很难修改和管理,推荐还是使用openapi_out protoc 的插件来生成文档,但此时是yaml的且不能在线导入,所以我写了个路由挂载上去转在线json,这样就能通过yapi的定时合并同步文档了。
配置
kratos的config可以支持render写法,可以吧env环境变量渲染到配置中,且支持默认配置, 由于kratos自带的env.NewSource()不能满足每次载入默认的本地配置, 可以先引入godotenv来进行环境变量文件的预载。
server: http: addr: 0.0.0.0:8000 timeout: 1s grpc: addr: 0.0.0.0:9000 timeout: 1s config: data: database: driver: mysql source: root:root@tcp(127.0.0.1:3306)/test redis: redis_addr: "${REDIS_ADDR:localhost:6379}" redis_pass: "${REDIS_PASS:}" redis_db: "${REDIS_DB:0}" read_timeout: 0.2s write_timeout: 0.2s
微服务架构的样板
可参照: https://github.com/go-kratos/beer-shop
因为我这边涉及到的业务线与技术中台的交互比较多,与beer-shop不同的是,我希望抽离出一个中台的sdk,然后处理与中台的业务交互逻辑, 这部分是可复用的,多个业务线共同可调用的, 所以这边的服务是把platform和data层面的ptClient抽离出来作为一个共用子仓库(微服务多仓库模式),其他业务线都属于单独的子仓库引用公共仓库。像nodejs可以使用npm link 进行调试,对应的go可以选择mod replace 将公共包指向本地的子仓库。
多仓库避免了一些问题: 内部的DRMS平台只能保证一个服务对应一个仓库,或者服务编排的形式发布,这种是高内聚的,而我对应的是业务线之间的关联并不强,同时滚动发布是不科学的。另一方面,单仓库容易一个公共的代码的bug影响多个业务线,也无形加大了测试成本。业务线子仓库锁定公共仓库版本,就降低了这种风险。