共计 9179 个字符,预计需要花费 23 分钟才能阅读完成。
概要
本文是学习 B 站毛剑老师的《API 工程化分享》的学习笔记,分享了 gRPC 中的 Proto 治理形式,Proto 分仓源码形式,Proto 独立同步形式,Proto git submodules 形式,Proto 我的项目布局,Proto Errors,服务端和客户端的 Proto Errors,Proto 文档等等
目录
- Proto IDL Management
- IDL Project Layout
- IDL Errors
- IDL Docs
Proto IDL Management
- Proto IDL
- Proto 治理形式
- Proto 分仓源码形式
- Proto 独立同步形式
- Proto git submodules 形式
Proto IDL
gRPC 从协定缓冲区应用接口定义语言 (IDL)。协定缓冲区 IDL 是一种与平台无关的自定义语言,具备凋谢标准。开发人员会创作 .proto 文件,用于形容服务及其输出和输入。而后,这些 .proto 文件可用于为客户端和服务器生成特定于语言或平台的存根,使多个不同的平台可进行通信。通过共享 .proto 文件,团队可生成代码来应用彼此的服务,而无需采纳代码依赖项。
Proto 治理形式
煎鱼的一篇文章:真是头疼,Proto 代码到底放哪里?
文章中通过多轮探讨对 Proto 的存储形式和对应带来的优缺点,一共有如下几种计划:
- 代码仓库
- 独立仓库
- 集中仓库
- 镜像仓库
镜像仓库
在我本人的微服务仓库外面,有一个 Proto 目录,就是放我本人的 Proto,而后在我提交我的微服务代码到骨干或者某个分支的时候,它可能触发一个 mirror 叫做主动同步,会镜像到这个集中的仓库,它会帮你复制过来,相当于说我不须要把我的源码的 Proto 凋谢给你,同时还会主动复制一份到集中的仓库
在煎鱼的文章外面的集中仓库还是分了仓库的,B 站大仓是一个对立的仓库。为什么呢?因为比如像谷歌云它整个对外的 API 会在一个仓库,不然你让用户怎么找?到底要去哪个 GitHub 上来找?有这么多 project 怎么找?基本找不到,应该建对立的一个仓库,一个我的项目就搞定了
咱们最早衍生这个想法是因为无心中看到了 Google APIs 这个仓库。大仓能够解决很多问题,包含高度代码共享,其实对于 API 文件也是一样的,集中在一个 Repo 外面,很不便去检索,去查阅,甚至看文档,都很不便
咱们不像其余公司喜爱弄一个 UI 的后盾,咱们喜爱 Git,它很不便做扩大,包含 CICD 的流程,包含 coding style 的 check,包含兼容性的检测,包含 code review 等等,你都能够基于 git 的扩大,gitlab 的扩大,GitHub 的一些 actions,做很多很多的工作
Proto 分仓源码形式
过来为了对立检索和标准 API,咱们外部建设了一个对立的 bapis 仓库,整合所有对内对外 API。它只是一个申明文件。
- API 仓库,不便跨部门合作;
- 版本治理,基于 git 管制;
- 规范化查看,API lint;
- API design review,变更 diff;
- 权限治理,目录 OWNERS;
集中式仓库最大的危险是什么呢?是谁都能够更改
大仓的外围是放弃了读权限的治理,针对写操作是有宏观治理的,就是你能够看到我的 API 申明,然而你实际上调用不了,然而对于迁入 check in,提到骨干,你能够在不同层级加上 owner 文件,它外面会形容谁能够合并代码,或者谁负责 review,两个角色,那就能够不便利用 gitlab 的 hook 性能,而后用 owner 文件做一些细粒度的权限治理,针对目录级别的权限治理
最终你的共事不能轻易迁入,就是说把文件的写权限,merge 权限敞开掉,只容许通过 merge request 的评论区去回复一些指令,比方说 lgtm(looks good to me),示意 review 通过,而后你能够回复一个 approve,示意这个代码能够被胜利 check in,这样来做一些细粒度的权限测验
怎么迁入呢?咱们的想法是在某一个微服务的 Proto 目录下,把本人的 Proto 文件治理起来,而后主动同步进去,就相当于要写一个插件,能够主动复制到 API 仓库外面去。做完这件事件之后,咱们又分了 api.go,api.java,git submodule,就是把这些代码应用 Google protobuf,protoc 这个编译工具生成客户端的调用代码,而后推到另一个仓库,也就是把所有客户端调用代码推到一个源码仓库外面去
Proto 独立同步形式
挪动端采纳自定义工具形式,在同步代码阶段,自动更新最新的 proto 仓库到 worksapce 中,之后依赖 bazel 进行构建整个仓库
- 业务代码中不依赖 target 产物,比方 objective-c 的 .h/.a 文件,或者 Go 的 .go 文件(钻石依赖、proto 未更新问题)
源码依赖会引入很多问题
- 依赖信息失落
- proto 未更新
- 钻石依赖
依赖信息失落
在你的工程外面依赖了其余服务,依赖信息变成了源码依赖,你基本不晓得依赖了哪个服务,以前是 protobuf 的依赖关系,当初变成了源码依赖,服务依赖信息失落了。将来我要去做一些全局层面的代码盘点,比方说我要看这个服务被谁依赖了,你曾经搞不清楚了,因为它变成了源码依赖
proto 未更新
如果我的 proto 文件更新了,你如何保障这个人从新生成了 .h/.a 文件,因为对它来说这个依赖信息曾经失落,为什么每次都要去做这个动作呢?它不会去生成 .h/.a 文件
钻石依赖
当我的 A 服务依赖 B 服务的时候,通过源码依赖,然而我的 A 服务还依赖 C 服务,C 服务是通过集中仓库 bapis 去依赖的,同时 B 和 C 之间又有一个依赖关系,那么这个时候就可能呈现对于 C 代码来说可能会注册两次,protobuf 有一个束缚就是说重名文件加上包名是不容许反复的,否则启动的时候就会 panic,有可能会呈现钻石依赖
- A 依赖 B
- A 依赖 C
- A 和 B 是源码依赖
- A 和 C 是 proto 依赖
- B 和 C 之间又有依赖
那么它的版本有可能是对不齐的,就是有危险的,这就是为什么 google basic 构建工具把 proto 依赖的名字治理起来,它并没有生成 .go 文件再 checkin 到仓库外面,它不是源码依赖,它每一次都要编译,每次都要生成 .go 文件的起因,就是为了版本对齐
Proto git submodules 形式
通过屡次探讨,有几个外围认知:
- proto one source of truth,不应用镜像形式同步,应用 git submodules 形式以仓库中目录模式来承载;
- 本地构建工具 protoc 依赖 go module 下的相对路径即可;
- 基于分支创立新的 proto,submodules 切换分支生成 stub 代码,同理 client 应用联调切换同一个分支;
- 保护 Makefile,应用 protoc + go build 对立解决;
- 申明式依赖形式,指定 protoc 版本和 proto 文件依赖(基于 BAZEL.BUILD 或者 Yaml 文件)
proto one source of truth
如果只在一个仓库外面,如果只有一个正本,那么这个正本就是惟一的假相并且是高度可信赖的,那如果你是把这个 proto 文件拷来拷去,最终就会变得源头更新,拷贝的文件没方法保障肯定会更新
镜像形式同步
实际上保护了本地微服务的目录外面有一个 protobuf 的定义,镜像同步到集中的仓库外面,实际上是有两个正本的
应用 git submodules 形式以仓库中目录模式来承载
git submodules 介绍
子模块容许您将 Git 存储库保留为另一个 Git 存储库的子目录。这使您能够将另一个存储库克隆到您的我的项目中并放弃您的提交离开。
图中 gateway 这个目录就是以本地目录的模式,然而它是通过 git submodules 形式给承载进来的
如果公司内代码都在一起,api 的定义都在一起,那么大仓相对是最优解,其次才是 git submodules,这也是 Google 的倡议
咱们偏向于最终 proto 的治理是集中在一个仓库外面,并且只有一份,不会做任何的 copy,通过 submodules 引入到本人的微服务外面,也就是说你的微服务外面都会通过 submodules 把集中 API 的 git 拷贝到本地我的项目外面,然而它是通过 submodeles 的形式来承载的,而后你再通过一系列 shell 的工具让你的整个编译过程变得更简略
IDL Project Layout
Proto Project Layout
在对立仓库中治理 proto,以仓库为名
根目录:
- 目录构造和 package 对齐;
- 简单业务的性能目录辨别;
- 公共业务性能:api、rpc、type;
目录构造和 package 对齐
咱们看一下 googleapis 大量的 api 是如何治理的?
第一个就是在 googleapis 这个我的项目的 github 外面,它的第一级目录叫 google,就是公司名称,第二个目录是它的业务域,业务的名称
目录构造和 protobuf 的包名是齐全对齐的,不便检索
简单业务的性能目录辨别
v9 目录下分为公共、枚举、谬误、资源、服务等等
公共业务性能:api、rpc、type
在 googleapis 的根目录下还有相似 api、rpc、type 等公共业务性能
IDL Errors
- Proto Errors
- Proto Errors:Server
- Proto Errors:Client
Proto Errors
- 应用一小组规范谬误配合大量资源
- 谬误流传
用简略的协定无关谬误模型,这使咱们可能在不同的 API,API 协定(如 gRPC 或 HTTP)以及谬误上下文(例如,异步,批处理或工作流谬误)中取得统一的体验。
应用一小组规范谬误配合大量资源
服务器没有定义不同类型的“找不到”谬误,而是应用一个规范 google.rpc.Code.NOT_FOUND 错误代码并通知客户端找不到哪个特定资源。状态空间变小升高了文档的复杂性,在客户端库中提供了更好的习用映射,并升高了客户端的逻辑复杂性,同时不限度是否蕴含可操作信息。
咱们以前本人的业务代码对于 404,对于某种资源找不到的错误码,定义了上百上千个,请问为什么大家在设计 HTTP restful 或者 grpc 接口的时候不必人家规范的状态码呢?人家有规范的 404,或者 not found 的状态码,用状态码去映射一下通用的错误信息不好吗?你不可能调用一个接口,返回几十种具体的错误码,你基本对于调用者来说是无奈应用的。当我的接口返回超过 3 个自定义的错误码,你就是面向谬误编程了,你一直依据错误码做不同的解决,十分难搞,而且你每一个接口都要去定义
这里的外围思路就是应用规范的 HTTP 状态码,比方说 500 是外部谬误,503 是网关谬误,504 是超时,404 是找不到,401 是参数谬误,这些都是通用的,十分规范的一些状态码,或者叫错误码,先用它们,因为不是所有的谬误都须要咱们叫业务上 hint,进一步解决,也就是说我调你的服务报错了,我大概率是啥都不做的,因为我无奈纠正服务端产生的一个谬误,除非它是带一些业务逻辑须要我做一些跳转或者做一些非凡的逻辑,这种不应该特地多,我感觉两个三个曾经十分多了
所以说你会发现大部分去调用他人接口的时候,你只须要用一个通用的规范的状态码去映射,它会大大降低客户端的逻辑复杂性,同时也不限度说你蕴含一些可操作的 hint 的一些信息,也就是说你能够蕴含一些批示你接下来要去怎么做的一些信息,就是它不抵触
谬误流传
如果您的 API 服务依赖于其余服务,则不应自觉地将这些服务的谬误流传到您的客户端。
举个例子,你当初要跟挪动端说我有一个接口,那么这个接口会返回哪些错误码,你始终讲不分明,你为什么讲不分明呢?因为咱们整个微服务的调用链是 A 调 B,B 调 C,C 调 D,D 的错误码会一层层透传到 A,那么 A 的错误码可能会是 ABCD 错误码的并集,你感觉你能形容进去它返回了哪些错误码吗?基本形容不进去
所以对于一个服务之间的依赖关系不应该自觉地将上游服务产生的这些错误码无脑透传到客户端,并且已经跟海内很多公司,像 Uber,Twitter,Netflix,跟他们很多的华人的敌人交换,他们都不倡议大家用这种全局的错误码,比如 A 部门用 01 结尾,B 部门用 02 结尾,相似这样的形式去搞所谓的小人契约,或者叫涣散的没有束缚的软弱的这种约定
在翻译谬误时,咱们倡议执行以下操作:
- 暗藏实现详细信息和机密信息
- 调整负责该谬误的一方。例如,从另一个服务接管 INVALID_ARGUMENT 谬误的服务器应该将 INTERNAL 流传给它本人的调用者。
比方你返回的错误码是 4,代表商品已下架,我对这个谬误很感兴趣,然而错误码 4 在我的我的项目外面曾经被用了,我就把它翻译为我还没应用的错误码 6,这样每次翻译的时候就能够对上一层你的调用者,你就能够交代分明你会返回错误码,因为都是你定义的,而且是你翻译的,你感兴趣的才翻译,你不感兴趣的统统返回 500 谬误,就是外部谬误,或者说 unknown,就是未知谬误,这样你每个 API 都能讲清楚本人会返回哪些错误码
在 grpc 传输过程中,它会要求你要实现一个 grpc states 的一个接口的办法,所以在 Kraots 的 v2 这个工程外面,咱们先用后面定义的 message Error 这个谬误模型,在传输到 grpc 的过程中会转换成 grpc 的 error_details.proto 文件外面的 ErrorInfo,那么在传输到 client 的时候,就是调用者申请服务,service 再返回给 client 的时候再把它转换回来
也就是说两个服务应用一个框架就可能对齐,因为你是基于 message Error 这样的谬误模型,这样在跨语言的时候同理,通过 ErrorInfo 应用同样的模型,这样就解决了跨语言的问题,通过模型的一致性
Proto Errors:Server
errors.proto 定义了 Business Domain Error 原型,应用最根底的 Protobuf Enum,将生成的源码放在 biz 大目录下,例如 biz/errors
- biz 目录中外围保护 Domain,能够间接依赖 errors enum 类型定义;
- data 依赖并实现了 biz 的 Reporisty/ACL,也能够间接应用 errors enum 类型定义;
- TODO:Kratos errors 须要反对 cause 保留,反对 Unwrap();
在某一个微服务工程外面,errors.proto 文件实际上是放在 API 的目录定义,之前讲的 API 目录定义实际上是你的服务外面的 API 目录,刚刚讲了一个 submodules,当初你能够了解为这个 API 目录是另外一个仓库的 submodules,最终你是把这些信息提交到那个 submodules,而后通过 reference 这个 submodules 获取到最新的版本,其实你能够把它打成一个本地目录,就是说我的定义申明是在这个中央
这个 errors.proto 文件其实就列举了各种错误码,或者叫谬误的字符串,咱们其实更倡议大家用字符串,更灵便,因为一个数字没有写文档前你基本不晓得它是干啥的,如果我用字符串的话,我能够 user_not_found 通知你是用户找不到,然而我通知你它是 3548,你基本不晓得它是什么含意,如果我没写文档的话
所以咱们倡议应用 Protobuf Enum 来定义谬误的内容信息,定义是在这个中央,然而生成的代码,依照 DDD 的战术设计,属于 Domain,因为业务设计是属于畛域的一个货色,Domain 外面 exception 它最终的源码会在哪?会在 biz 的大目录下,biz 是 business 的缩写,就是在业务的目录下,举个例子,你能够放在 biz 的 errors 目录下
有了这个认知之后咱们会做三个事件
首先你的 biz 目录保护的是畛域逻辑,你的畛域逻辑能够间接依赖 biz.errors 这个目录,因为你会抛一些业务谬误进来
第二,咱们的 data 有点像 DDD 的 infrastructure,就是所谓的基础设施,它依赖并实现了 biz 的 repository 和 acl,repository 就是咱们所谓的仓库,acl 是防腐层
因为咱们之前讲过它的整个依赖倒置的玩法,就是让咱们的 data 去依赖 biz,最终让咱们的 biz 零依赖,它不依赖任何人,也不依赖基础设施,它把 repository 和 acl 的接口定义放在 biz 本人目录下,而后让 data 依赖并实现它
也就是说最终我这个 data 目录也能够依赖 biz 的 errors,我可能通过查 mysql,后果这个货色查不到,会返回一个 sql no rows,但必定不会返回这个谬误,那我就能够用依赖 biz 的这个 errors number,比如说 user_not_found,我把它包一个 error 抛出去,所以它能够依赖 biz 的 errors
目前 Kratos 还不反对根因保留,根因保留是什么呢?刚刚说了你可能是 mysql 报了一个外部的谬误,这个外部谬误你实际上在最上层的传输框架,就是 HTTP 和 grpc 的 middleware 外面,你可能会把日志打进去,就要把堆栈信息打进去,那么根因保留就是通知你最底层产生的谬误是什么
不反对 Unwrap 就是不反对递归找根因,如果反对根因当前呢,就能够让 Kratos errors 这个 package 能够把根因传进去,这样子既能搞定咱们 go 的 wrap errors,同时又反对咱们的状态码和 reason,大类谬误和小类谬误,大类谬误就是状态码,小类谬误就是我刚刚说的用 enum 定义的具体信息,比方说这个商品被下架,这种就不太好去映射一个具体的错误码,你可能是返回一个 500,再带上一个 reason,可能是这样的一个做法
Proto Errors:Client
从 Client 生产端只能看到 api.proto 和 error.proto 文件,相应的生成的代码,就是调用测的 api 以及 errors enum 定义
- 应用 Kratos errors.As() 拿到具体类型,而后通过 Reason 字段进行断定;
- 应用 Kratos errors.Reason() helper 办法(外部依赖 errors.As)疾速断定;
拿到这两个文件之后你能够生成相应代码,而后调用 api
举个例子,图中的代码是调用服务端 grpc 的某一个办法,那么我可能返回一个谬误,咱们能够用 Kratos 提供的一个 Reason 的 short car,一个快捷的办法,而后把 error 传进去,实际上在外部他会调用规范库的 error.As,把它强制转换成 Kratos 的 errors 类型,而后拿到外面的 Reason 的字段,而后再跟这个枚举值断定,这样你就能够断定它是不是具体的一个业务谬误
第二种写法你能够拿到原始的咱们 Kratos 的 Error 模型,就是以下这个模型
new 进去之后用规范库的 errors.As 转换进去,转换进去之后再用 switch 获取它外面的 reason 字段,而后能够写一些业务逻辑
这样你的 client 代码跨语言,跨传输,跨协定,无论是 grpc,http,同样是用一样的形式去解决
IDL Docs
- Proto Docs
Proto Docs
基于 openapi 插件 + IDL Protobuf 正文(IDL 即定义,IDL 即代码,IDL 即文档),最终能够在 Makefile 中应用 make api 生成 openapi.yaml,能够在 gitlab/vscode 插件间接查看
- API Metadata 元信息用于微服务治理、调试、测试等;
因为咱们能够在 IDL 文件下面写上大量的正文,那么当讲到这个中央,你就明确了 IDL 有什么样的益处?
IDL 文件它既定义,同时又是代码,也就是说你既做了申明,而后应用 protoc 能够去生成代码,并且是跨语言的代码,同时 IDL 自身既文档,也就是说它才真正满足了 one source of truth,就是惟一的事实标准
最终你能够在 Makefile 中定义一个 api 指令,而后生成一个 openapi.yaml,以前是 swagger json,当初叫 openapi,用 yaml 申明
生成 yaml 文件当前,当初 gitlab 间接反对 openapi.yaml 文件,所以你能够间接关上 gitlab 去点开它,就能看到这样炫酷的 UI,而后 VSCode 也有一个插件,你能够间接去查看
还有一个很要害的点,咱们当初的 IDL 既是定义,又是代码,又是文档,其实 IDL 还有一个核心作用,这个定义示意它是一个元信息,是一个元数据,最终这个 API 的 mate data 元信息它能够用于大量的微服务治理
因为你要治理的时候你比方说对每个服务的某个接口进行路由,进行熔断进行限流,这些元信息是哪来的?咱们晓得以前 dubbo 2.x,3.x 之前都是把这些元信息注册到注册核心的,导致整个数据中心的存储爆炸,那么元信息在哪?
咱们想一想为什么 protobuf 是定义一个文件,而后序列化之后它比 json 要小?因为它不是自描述的,它的定义和序列化是离开的,就是原始的 payload 是没有任何的定义信息的,所以它能够高度的 compressed,可被压缩,或者说叫更紧凑
所以说同样的情理,IDL 的定义和它的元信息,和生成代码是离开的话,意味着你只有有 one source of truth 这份惟一的 pb 文件,基于这个 pb 文件,你就有方法把它做成一个 api 的 metadata 的服务,你就能够用于做微服务的治理
你能够选一个服务,而后看它有些什么接口,而后你能够通过一个管控面去做熔断、限流等性能,而后你还能够基于这个元信息去调试,你做个炫酷的 UI 能够让它有一些参数,甚至你能够写一些扩大,比方说这个字段叫 etc,倡议它是什么样的值,那么你在渲染 UI 的时候能够把默认值填进去,那你就很不便做一些调试,甚至蕴含测试,你基于这个 api 去生成大量的 test case
参考
API 工程化分享
https://www.bilibili.com/video/BV17m4y1f7qc/
接口定义语言
https://docs.microsoft.com/zh-cn/dotnet/architecture/grpc-for-wcf-developers/interface-definition-language
真是头疼,Proto 代码到底放哪里?
https://mp.weixin.qq.com/s/cBXZjg_R8MLFDJyFtpjVVQ
git submodules
https://git-scm.com/book/en/v2/Git-Tools-Submodules
kratos
https://github.com/go-kratos/kratos
error_details.proto
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
pkg/errors
https://github.com/pkg/errors
Modifying gRPC Services over Time