AI实践哪家强?来 AICon, 解锁技术前沿,探寻产业新机! 了解详情
写点什么

从 Kratos 设计看 Go 微服务工程实践

  • 2021-07-16
  • 本文字数:6307 字

    阅读完需:约 21 分钟

从Kratos设计看Go微服务工程实践

导读


github.com/go-kratos/kratos(以下简称 Kratos)是一套轻量级 Go 微服务框架,致力于提供完整的微服务研发体验,整合相关框架及周边工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。Kratos 在设计之初就考虑到了高可扩展性,组件化,工程化,规范化等。对每位开发者而言,整套 Kratos 框架也是不错的学习仓库,可以了解和参考微服务的技术积累和经验。


接下来我们从 Protobuf开放性规范依赖注入 这 4 个点了解一下 Kratos 在 Go 微服务工程领域的实践。

基于 Protocol Buffers(Protobuf)的生态

在 Kratos 中,API 定义、gRPC Service、HTTP Service、请求参数校验、错误定义、Swagger API json、应用服务模版等都是基于 Protobuf IDL 来构建的:



举一个简单的 helloworld.proto 例子:


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-openapiv2、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 wireinject
func 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 !wireinject
func 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 赋予了更多企业级服务治理能力、提供多维度服务,如:应用生命周期托管、一键上云、私有化部署、多语言发布。


作者介绍


曹国梁:6 年 Go 微服务研发经历,腾讯云高级研发工程师,Kratos Maintainer,gRPC-go contributor


本文转载自公众号腾讯云中间件(ID:gh_6ea1bc2dd5fd)。


原文链接


从Kratos设计看Go微服务工程实践

2021-07-16 15:309144

评论

发布
暂无评论
发现更多内容

研产供销数据一体化,解码汽车集团企业的数据治理之道

袋鼠云数栈

数字化转型

云计算在商业运营中的潜力

天翼云开发者社区

云计算

人脸识别技术的优缺点及其在实际应用中的影响

数据堂

一次解决三大成本问题,升级后的 Zilliz Cloud 如何造福 AIGC 开发者?

Zilliz

SaaS Milvus Zilliz zillizcloud

WIZMAP-大规模 embedding 向量的可视化交互工具

Zilliz

机器学习 深度学习 Embedding 交互式可视化工具

国产化适配再进一步,融云完成欧拉、TDSQL、优炫等多方适配

融云 RongCloud

开源 运维 信创 融云 适配

5个祖传的Python自动化办公项目,治愈你的抑郁

程序员晚枫

Python 微信 自动化 机器人 办公

九科三周年专访丨创始人万正勇:拥抱AIGC新浪潮,赋能信创产业高质量发展

九科Ninetech

为什么多数企业的数字化转型都失败了?

优秀

数字化转型 企业数字化 企业数字化 PaaS 平台

3DCAT实时云渲染助力VR虚拟现实迈向成熟

3DCAT实时渲染

实时渲染

知识图谱之《海贼王-ONEPICE》领域图谱项目实战(含码源):数据采集、知识存储、知识抽取、知识计算、知识应用、图谱可视化、问答系统(KBQA)等

汀丶人工智能

自然语言处理 知识图谱 信息抽取

人脸识别技术在医疗行业的应用

数据堂

在现场!2023世界人工智能大会

新云力量

人工智能 AI 人工智能大会

拥抱抑郁,制心一处,一切美好是深度投入的产物

B Impact

PoseiSwap 更新第二期空投,持有 Zepoch 节点数量将决定空投回报

BlockChain先知

代码随想录训练营 Day09 - 字符串(下)

jjn0703

PoseiSwap 更新第二期空投,持有 Zepoch 节点数量将决定空投回报

股市老人

中国大模型的落地DNA,写在这个双螺旋结构里

脑极体

AI

性能认证+最佳案例,阿里云 ACK@Edge 产品技术、落地能力获信通院综合认可

阿里巴巴云原生

阿里云 容器 云原生 ACK

精彩回顾|【2023 ACDU 中国行·深圳站】数据库主题交流活动成功举办!

墨天轮

MySQL 数据库 oracle postgresql 腾讯云

2023世界人工智能大会如约而至!低代码开发:点燃数智时代,让AI风口助您飞跃

不在线第一只蜗牛

人工智能 低代码 人工智能大会 行业风口

Kubernetes网络模型Overlay和Underlay

虚实的星空

活动开启 | 以梦筑码 · 不负韶华 开发者故事征集令,讲出你的故事,有机会参加HDC.Together 2023

HarmonyOS开发者

HarmonyOS

提升UE5写实效果的项目设置

3DCAT实时渲染

虚幻引擎5 UE5

2023-07-06:RabbitMQ中的AMQP是什么?

福大大架构师每日一题

Rabbit 福大大架构师每日一题

手把手教学构建证券知识图谱/知识库(含码源):网页获取信息、设计图谱、Cypher查询、Neo4j关系可视化展示

汀丶人工智能

人工智能 自然语言处理 nlp 知识图谱

从Kratos设计看Go微服务工程实践_开源_腾讯云中间件_InfoQ精选文章