惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
I
InfoQ
The Register - Security
The Register - Security
L
LangChain Blog
H
Help Net Security
The GitHub Blog
The GitHub Blog
S
Schneier on Security
博客园 - 【当耐特】
W
WeLiveSecurity
Attack and Defense Labs
Attack and Defense Labs
IT之家
IT之家
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Google DeepMind News
Google DeepMind News
The Cloudflare Blog
H
Heimdal Security Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
雷峰网
雷峰网
N
Netflix TechBlog - Medium
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
L
Lohrmann on Cybersecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
V
Visual Studio Blog
博客园 - 聂微东
PCI Perspectives
PCI Perspectives
Last Week in AI
Last Week in AI
A
Arctic Wolf
宝玉的分享
宝玉的分享
T
The Blog of Author Tim Ferriss
S
Secure Thoughts
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
SegmentFault 最新的问题
SecWiki News
SecWiki News
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
Schneier on Security
Schneier on Security
P
Proofpoint News Feed
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
AI
AI
Engineering at Meta
Engineering at Meta

Mohuishou

如何实现支持多集群的 Kubernetes Operator? 第三方应用如何调用我们 kubebuilder 生成的自定义资源? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 4. localhost 就一定是 localhost 么? Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(四) 漏桶算法 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试 Go工程化(七) Go Module Go工程化(六) 配置管理 Go工程化(五) API 设计下: 基于 protobuf 自动生成 gin 代码 Go工程化(三) 依赖注入框架 wire Go工程化(二) 项目目录结构 Go工程化(一) 架构整洁之道阅读笔记 Go并发编程(十一) 总结 Go并发编程(十) 深入理解 Channel Go并发编程(九) 深入理解 Context Go并发编程(八) 深入理解 sync.Once Go并发编程(七) 深入理解 errgroup Go并发编程(六) 深入理解 WaitGroup Go并发编程(五) 深入理解 sync/atomic Go并发编程(四) 深入理解 Mutex Go并发编程(三) data race Go并发编程(二) Go 内存模型 Go并发编程(一) goroutine Go错误处理最佳实践 微服务(二) 服务发现&多租户 微服务(一) 微服务概览 5. 栈下: 深入理解 defer 4. 栈上: 如何实现一个计算器 Go Struct 初始化风格的抉择 3. 数组下: 使用 GDB 调试 Golang 代码 2. 数组上: 深入理解 slice 1. 链表: 深入理解container/list&LRU缓存的实现 Go设计模式24-总结(更新完毕) Go设计模式23-中介模式 Go设计模式22-解释器模式 Go设计模式21-命令模式 Go设计模式20-备忘录模式 Go设计模式19-访问者模式 Go设计模式18-迭代器模式 Go设计模式17-状态模式 Go设计模式16-职责链模式(Gin的中间件实现) Go设计模式15-策略模式 Go模板模式14-模板模式 Go设计模式13-观察者模式(实现简单的EventBus) Go设计模式12-享元模式 Go设计模式11-组合模式 Go设计模式10-门面模式 Go设计模式09-适配器模式 Go设计模式08-装饰器模式 Go设计模式07-桥接模式 Go设计模式06-代理模式(generate实现类似动态代理) Go设计模式05-创建型模式总结 Go设计模式04-原型模式 Go设计模式03-建造者模式 Go设计模式02-工厂模式&DI容器 笔记-让你最快速地改善代码质量的20条编程规范 Go设计模式01-单例模式 一点拙见-如何写好一个技术预研报告? Go Web小技巧(四)在单个仓库中支持多个 go mod 模块 Go Web 小技巧(三)Gin 参数绑定 Go Web 小技巧(二)GORM 使用自定义类型 Go Web 小技巧(一)简化Gin接口代码 善用工具之postman高级用法概述 go generate and ast hexo-next-algolia-search全文搜索 docker镜像瘦身&优化 GORM避坑指南之含关联关系的更新 Github Actions介绍&自动构建Github Pages博客 在blog中内嵌在线PPT 记一次net http内存泄漏 使用TravisCI自动部署Blog 使用Goland调试Go程序 一个十分边缘的gorm的bug Httprouter介绍及源码阅读 Gin源码阅读 从0.1开始
Go工程化(四) API 设计上: 项目结构 & 设计
Mohuishou · 2021-03-01 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

本系列为 Go 进阶训练营 笔记,访问 博客: Go进阶训练营, 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。

3 月开始会尝试爆更模式,争取做到两天更新一篇文章,如果感兴趣可以拉到文章最下方获取关注方式。3 月进度: 01/15

Go 工程化(二) 项目目录结构 中我们大概讲了一下 api 目录,但是并没有详细的说明,留在这这篇文章一起讲。

API 设计将分为四个部分:

  • 首先会讲一下 API 的项目目录结构,在项目中 api 该如何组织,以及 api 依赖该如何处理
  • 第二个部分会讲一下 API 该如何设计,包括错误码的设计
  • 第三个部分会讲一下如何构造一个 protobuf 的代码生成器,自动生成 gin 相关代码,这个是因为我们目前主要是 http 的服务,grpc 相关基础设施建设不完全,所以依赖现有的基础设施得到更好的体验。并且做到在 service 层的代码支持 grpc 和 http 多种方式,使后续架构变化更加灵活。
  • 第四个部分会给出一个 demo,辅助大家更好的理解

由于篇幅原因拆分成了两篇文章,本文涉及到一二部分,下一篇文章为讲解三四部分

API 定义方式

b 站内部主要使用 grpc 作为内部通信的方式,因为他使用 protobuf 文件定义可以支持对语言代码生成,同时还避免了手写文档导致的文档错误过时等情况,具体的原因其实在第一课的笔记当中就有提到,如果感兴趣可以查看 微服务(二) 服务发现&多租户#gRPC

我们目前使用类似 http restful 的方式进行对外对内提供服务,但是我们之前的 API 管理其实是比较混乱的,分为以下几种情况:

  1. 暴露给 web 的 api:有使用 swagger 的,有在文档平台上写文档的,还有没有写文档的
  2. 暴露给其他服务调用的 api: 有注册到内部的接口网关的,但是内部的接口网关上有的有参数,有的没有,没有返回值定义

所以就存在很多问题:

  1. 想要接口不知道从哪儿找,只能到处问人
  2. 有时候从内部网关平台上找到接口但是不知道怎么调用,没有写任何参数,有的写了还有可能是错的
  3. 有的压根没有接口文档,对接的同学也没有时间写,然后让你直接看代码
  4. 有的对接同学扔给你一个接口文档,然后试了半天发现,有问题,沟通排查之后发现文档很久没有更新了 o(╥﹏╥)o

所以课程上毛老师提到的利用 protobuf 来定义接口的方式非常令人心动,因为 protobuf 当中包含了接口的函数签名,入参和返回值同时还支持注释,就是一份天然的文档,同时也不用担心出现代码更新了但是文档没有更新的情况,因为它既是文档也是代码,服务端也需要使用,所以代码更新之后文档也一定会更新。自然而然的就少了很多沟通的成本。
api 定义方式 (1).jpg
如上图所示于此同时我们还可以利用 protobuf 文件生成对应语言的客户端代码,就不用每个项目都去维护一套 sdk 了,同时我们使用接口生成代码,在 go 当中可以使用 gomock 非常方便的对代码进行 mock。

API Project

使用 protobuf 定义接口可以解决我们找到 api 文档之后,文档不准确,缺失的问题,但是我们应该如何找到我们的 api 呢?我们生成出的 api 文件调用方应该如何引用呢?难道我们给每个调用方都去开一个项目的权限么?那明显是不太行的,接下来我们就看看我们 api 该如何管理和组织。

毛老师他们仿照 googleapis/googleapisistio/api 等知名项目在 b 站内部搞了一个 bapis 的仓库用于同一存放 api 定义文档,然后通过 ci/cd 生成对应的客户端代码放到各个语言的子仓库当中
Frame 1 (1).jpg
工作流程如上图所示

  • 开发同学修改了 proto 文件定义之后 push 到对应的业务应用仓库当中
  • 然后触发 cicd 流程将 proto 文件复制到 api project 当中
    • 首先会对 proto 文件进行静态代码分析,查看是否符合规范
    • 然后 clone api project 创建一个新的分支
    • 然后 push 代码,创建一个 merge request 请求
  • 然后我们对应负责的同学收到 code review 的通知之后进行 code review,没有问题就会合并到 api project 的主分支当中了
  • 然后就会触发 cicd 生成对应语言的客户端代码,push 到对应的各个子仓库当中了

API Project Layout

我们的 api 项目是如何定义的呢?看下图
Frame 1 (2).jpg

  • 首先是在业务项目当中,我们顶层会有一个 api 目录
    • 在 api 目录当中我们会按照 product name/app name/版本号/app.proto 的方式进行组织
    • 具体怎么组织可能每个公司都不太一样,但是总的来说就是应用的 唯一名称+版本号 来进行一个区分
  • 在 api project 当中和业务应用类似,也有一个 api 目录,通过上图的两个框就可以发现这是一模一样的
    • 除此之外 api project 还有用于注解的 annotations 文件夹
    • 有一些第三方的引用,例如 googleapis 当中的一些 proto 文件

API 设计

API 兼容性设计

随着应用的不断开发,业务的不断发展我们的 api 肯定会不断的进行修改,在修改 api 的时候考虑 api 的兼容性就会很重要了,如果我们做了一些破坏性的变更就有可能会导致依赖我们的服务或者是客户端报错,这样就会带来事故。

向下兼容的变更

  • 新增接口
  • 新增参数字段
  • 新增返回字段
    • 在不改变其他响应字段的行为的前提下,非资源(例如,ListBooksResponse)的响应消息可以扩展而不必破坏客户端的兼容性。即使会引入冗余,先前在响应中填充的任何字段应继续使用相同的语义填充。

一般而言新增都是相对安全的,但是我们要注意的是新增字段不能改变我们原本的逻辑,如果改变了 api 的逻辑,那就不一定安全了

向下不兼容的变更(破坏性变更)

  • 删除或重命名服务,字段,方法或枚举值
    • 在做这种修改的时候需要修改我们 api 的版本号,常见有两种方式
    • 如果只有很少的 api 变动可以创建一个 XXXV2 的方法
    • 如果变动的 api 比较多,可以直接新启一个 v2 的包
  • 修改字段的类型
    • 严禁修改字段的类型,修改字段的类型可能会导致客户端崩溃
  • 修改现有请求的可见行为
  • 给资源消息添加 读取/写入 字段

API 命名规范

包名

产品名product
应用名app
版本号v1
包名product.app.v1
目录结构api/product/app/v1/xx.proto

API 定义

  • 命名规则:方法 + 资源
  • 标准方法:参考 Google API 设计指南
标准方法HTTP 映射
ListGET
GetGET
UpdatePUT 或者 PATCH
CreatePOST
DeleteDELETE

除了标准的也有一些非标准的,例如同步数据可能会用 Sync  等,不过大部分的 api 应该都是标准的

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// api/product/app/v1/blog.proto

syntax = "proto3";

package product.app.v1;

import "google/api/annotations.proto";

// blog service is a blog demo
service BlogService {

rpc GetArticles(GetArticlesReq) returns (GetArticlesResp) {
option (google.api.http) = {
get: "/v1/articles"
additional_bindings {
get: "/v1/author/{author_id}/articles"
}
};
}
}

注意,一般而言我们应该为每个接口都创建一个自定义的 message,为了后面扩展,如果我们用 Empty 的话后续就没有办法新增字段了

API Error

错误定义

先说我们当前的问题,我们一直用的 http 然后我们返回是使用的下面这种格式,然后 http code 统一返回 200

1
2
3
4
5
{
"code": 1,
"msg": "xxx",
"data": {}
}

这种做法就存在一个比较大的问题,做监控的时候不太好做,很多现成的东西没有办法直接使用,因为我们都返回的成功。
参照 google 的错误定义,将 http code 和 grpc 错误码进行映射,返回对应的错误信息
image.png
但是这样还是不行,因为这样很多业务错误信息无法区分,毛老师他们的 kratos v2 的做法是做了两层,使用下面的方式进行定义

1
2
3
4
5
6
7
8
9
10
message Status {
// 错误码,跟 grpc-status 一致,并且在HTTP中可映射成 http-status
int32 code = 1;
// 错误原因,定义为业务判定错误码
string reason = 2;
// 错误信息,为用户可读的信息,可作为用户提示内容
string message = 3;
// 错误详细信息,可以附加自定义的信息列表
repeated google.protobuf.Any details = 4;
}

和我们当前的方式差不太多,但是我们是在原来的基础上返回了 http code,剩下的字段还是和原来保持一致

错误传播

这一点我们之前做的还行,错误传播这一部分很容易出的问题就是,当前服务直接把上游服务的错误给返回了,这样会导致一些问题:

  • 如果我调用了多个上游服务都报错了,我应该返回哪一个错误
  • 直接返回导致必须要有一个全局错误码,不然的话就会冲突,但是全局错误码是很难定义的

正确的做法应该是把上游错误信息吞掉,返回当前服务自己定义的错误信息就可以了。

总结

毛老师课上讲的 api 设计思路用起来还是挺爽的,我们已经在一个项目当中进行了试点,cicd 的流程也跑了起来,最爽的一点就是终于不用找接口文档了,然后还节省了一些代码量,我们之前的接口调用方式都是十分原始的,每个项目都自己去封装相关的 sdk 然后我们对单元测试还有要求,http 接口的 mock 是挺麻烦的事情,通过 protobuf 定义接口之后我写了一个结合内部网关的 sdk 代码生成器,直接生成相关接口代码,go interface 的 mock 实现也在 ci 流程中生产好了,调用方只需要调用不同的实现就行了。
下一篇我们就通过写一个 从 proto 生成 gin 代码的生成器来看看这个代码生成器改如何实现。

参考文献

  1. Go 进阶训练营-极客时间
  2. GitHub - istio/api: API definitions for the Istio project
  3. GitHub - envoyproxy/data-plane-api: [READ ONLY MIRROR] Envoy REST/proto API definitions and documentation.
  4. GitHub - googleapis/googleapis: Public interface definitions of Google APIs.
  5. API 设计指南  |  Google Cloud

关注我获取更新

猜你喜欢