Kratos是B站开源的一款go的微服务框架,最近PS5上的 战神·诸神黄昏比较火,主角就是奎托斯。这个框架的名字就取自他。
在进行框架选型时,对比了目前主流的很多go微服务框架,如Zero,最后对比之下,选择了Kratos,原因是Kratos给了开发人员更多的选择和更多的自由空间,方便自定义实现和选用很多东西,可以更快的适应已有的服务 以及 让开发者更快地上手使用(因为选用自己熟悉的工具)。
Kratos并不绑定于特定的基础设施,不限定于某种注册中心,或数据库ORM等,所以您可以十分轻松地将任意库集成进项目里,与Kratos共同运作。
原则
- 简单:不过度设计,代码平实简单;
- 通用:通用业务开发所需要的基础库的功能;
- 高效:提高业务迭代的效率;
- 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠;
- 健壮:通过良好的基础库设计,减少错用;
- 高性能:性能高,但不特定为了性能做 hack 优化,引入 unsafe ;
- 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能;
- 容错性:为失败设计,大量引入对 SRE 的理解,鲁棒性高;
- 工具链:包含大量工具链,比如 cache 代码生成,lint 工具等等;
这是kratos官方挂出的框架设计出发点,其中有几点是在现有工具中尤为宝贵,并且十分契合go开发风格的。如 简单,高效,扩展性,容错性。
有人说,使用go最大的好处就是无论什么人,他们水平高或低,可以写出相近的代码,大家都读得懂。
我也认为,go的上手难度较低,风格相对固定,性能也还可以,所以,在中国的互联网企业风靡了起来。
而kratos这个框架,无疑将go的这些特性进行了放大。接下来,我会将我在实际使用中,最关注的一些点做些分享和记录。
首先,是整个kratos的架构风格图,可以看到kratos将整个服务大体分为了3层,API / Service / DB
左侧标注了在 Service和DB层,使用依赖注入(DI)进行实现,工具名称为Wire。
从这张图中,可以看到Wire这个工具几乎贯穿Kratos架构始终,是一个大角色。
1. 传输协议
支持http + grpc两种调用方式,通过编写proto文件来实现。
一般,http开放给外部调用,可以使用restful风格定义。grpc面向内部微服务之间进行调用。
熔断限流
2. 日志
在kratos中,可以自定义日志框架选型,设置日志格式和输出内容,然后将logger对象以依赖注入的方式,分配给server中的grpc server和http server,这样就可以实现每次收到请求后的日志打印。
将logger对象以依赖注入的方式,注入到业务层,就可以在业务层中统一使用logger进行输出。
3. 错误处理
在grpc中,比较通用的一种错误处理方式就是直接通过 proto 预定义定义错误码,然后通过 proto-gen-go 生成帮助代码,直接返回 error。
{
// 错误码,跟 http-status 一致,并且在 grpc 中可以转换成 grpc-status
"code": 500,
// 错误原因,定义为业务判定错误码
"reason": "USER_NOT_FOUND",
// 错误信息,为用户可读的信息,可作为用户提示内容
"message": "invalid argument error",
// 错误元信息,为错误添加附加可扩展信息
"metadata": {
"foo": "bar"
}
}
结构是这样的。这里可以发现,为了兼容grpc,在http的返回结果中,code也无法自定义,只能跟随httpcode。所以这里客户端或者第三方去处理错误时,需要判断reason字段。
4. 配置管理
使用proto文件定义配置和生成struct,然后将yaml中的内容读取到对应struct 字段中进行使用。
在这里我们可以注意到,在kratos中,除了传输格式使用了proto进行定义之外,错误处理和配置管理,也使用了proto来进行。可以说,一切皆proto。
5. wire
Wire 是一个灵活的依赖注入工具(需要安装),通过自动生成代码的方式在编译期完成依赖注入。通过 Wire 进行初始化代码,可以很好地解决组件之间的耦合,以及提高代码维护性。
打开Kratos的示例项目,从main入口看,有一处调用了wireApp方法,这里就是一切的源头(万恶之源)。
这个方法调用的是main同目录的wire文件中的wireApp方法,同目录的wire_gen.go实现了此方法。
wire_gen中去实例化不同service和组建的对象,用于调用。关系图如下:
在每个模块中,只需要一个 ProviderSet 提供者集合,就可以在 wire 中进行依赖注入。
简单的描述一下:有一个数据库连接对象,service需要操作数据库,依赖数据库连接对象。这时候我们可以声明数据库连接对象在ProviderSet集合,然后在service对象处声明,我需要一个数据库连接对象。 然后我们使用wire工具,就可以自动帮我们生成依赖注入的代码。
这个功能使用的时候,我才真正明白了依赖注入。和使用java的依赖注入是完全不同的感觉。有了显式的依赖注入,让代码间的依赖关系一目了然。当我们查看同事代码时,只需要点开wire_gen.go 代码,什么处理过程 需要依赖哪些组件,清清楚楚,大大提高了代码的维护性。