所有文章 > API设计 > B站微服务API管理

B站微服务API管理

引言

API 管理是应用开发中不可或缺的一部分。在早期服务数量不多的情况下,团队可以自行负责 API 管理。但随着公司规模逐渐扩张,业务接口数量爆炸式增长,此时 API 管理的任务应由统一的接口管理平台来承担,结束各自为政的局面。统一管理能够最大程度地发挥 API 的价值,减少跨部门沟通与协作的成本。本期文章将带领大家一窥 B 站在 API 管理方面所作的设计与思考,重点介绍我们是如何收集 API 元信息并对其进行井井有条的管理,又是如何配置这些庞大 API 资源来减轻业务管理负担,粘合跨部门间合作。

管理现状

目前,我们的元信息统一管理平台线上接口数达到12w+,应用(含测试应用)总数近2w。如此庞大规模的接口由平台统一收集并管理,可节约大量人力维护与沟通成本,契合当前“降本增效”的主基调。

01 服务上线流程

一个服务的上线过程通常分为这样几个阶段:

1. 需求评审与分析

PM 输出需求,PMO 组织需求评审会,业务线开发评估需求的开发方案与所需工时,从而确定迭代周期。

2. 撰写与维护 API 文档

在项目开发中,Web 项目的前后端分离开发,需要由前后端开发共同定义接口,编写接口文档,之后大家都根据这个接口文档进行开发,一直维护到项目结束。API 文档在后端技术方案确定后即可编写。尽可能早地提供给对接方,有助于对接方提前思考实现方式和规避隐患。

3. 前后端联调与测试

前端根据 API 文档初步实现功能,后端在开发完成后发布至测试环境提供给前端联调。

4. 发布上线

当一切就绪后,服务被发布至线上,API 开始对外提供服务,需求上线。

上述整个过程中都离不开对接口文档的管理,一个优秀的接口文档能够让前端与后端开发人员更好地配合,提高工作效率,方便新加入的成员查看和维护接口、测试人员进行接口测试。

02 API管理

 2.1 为什么需要对API进行统一管理

在过去没有统一管理的模式下,虽说每个团队每个项目都有编写 API 文档的意识,但免不了出现各种管理模式的差异,例如 A 团队习惯将文档编写在知识库中,B 团队习惯将文档用 swagger 生成并托管至版本管理系统等等。这种管理模式上的差异会直接导致对接沟通上的低效,无法及时得发现API的异常,难以管理接口的版本迭代。因此我们始终推荐对 API 进行统一管理,降低对接时的沟通成本,并在接口出现变更时及时同步调用方,减少信息 gap,通过标准化的中心收集模式敏锐地捕捉到每一次接口调整。

 2.2 API元信息的收集与更新

在整个 API 管理过程中,首先需要保证接口元信息完备性和准确性,管理平台需要充分收集接口信息。B 站的接口元信息的收集之路:手动维护 -> 自动生成。

2.2.1 手动维护

过去,我们在内网私有化部署过一套 YAPI,通过部门、业务域、应用的三级划分的粒度管理着各个服务的接口。研发通过在 YAPI 的可视化界面上手动录入应用的详细接口信息。接口发布后,前后端的研发根据文档着手进行编码,测试同学则根据文档上的接口逐个进行测试,负责人对该文档进行审批。总之,项目干系方始终都会围绕着这份文档来推进项目。

手动维护的缺点:

  • 人力成本高

市面上有很多的五花八门的 API 信息管理平台,如知名的 Eolink、YAPI、Apifox、Postman,但无论部署哪个平台都无法解决一个非常核心的问题:数据的来源始终是“人”,需要人工去操作与更新。 尤其在项目的开发阶段,接口文档的改动频次实际上是很高的,需要开发同学多次到平台上调整接口文档,保证接口数据始终正确。

  • 信息同步不及时

手工模式下,开发同学每次完成接口的增改都需要及时到平台上同步最新的改动并通知具体的订阅方。若某次变更未被及时同步,造成调用方与被调方之间信息不对齐,很容易造成故障。

2.2.2 自动生成

接口管理平台的作用是自动采集应用 API 并生成一份详细且准确的接口文档,使开发将精力全部集中在 API 本身的设计上,无需额外关注接口文档的撰写与维护,从而解放研发同学的双手,提高开发效率。

为此,我们设计了如下架构,研发同学遵循统一的 API 标准定义接口,走完正常应用打包上线流程,接口采集自动完成。

代码中定义接口

对于习惯使用 Golang 进行开发的同学:

// Demo service responds to incoming requests.
service DemoService {
option (google.api.default_host) = "api.example.com";
// DemoBody method receives a simple message and returns it.
rpc DemoBody(SimpleMessage) returns (SimpleMessage) {
option (google.api.http) = {
post: "/poc/probe/demo_body"
body: "*"
};
}
}
// 请求、回复消息
message SimpleMessage {
int32 id = 1 [(google.api.field_behavior) = REQUIRED];
Embedded embedded = 2;
}

message Embedded {
int64 int64_val = 1 [(gogoproto.moretags) = 'default:"1"'];
string string_val = 2;
// 一个字符串列表
repeated string repeated_string_val = 3;
// 一个字符串Map
map<string, string> map_string_val = 4;
}

上述 Proto 片段中定义了一个名称为 DemoService 的 RPC 服务,该服务包含一个简单的 RPC方法 DemoBody,并且引入 Google 官方提供的 annotations.proto 对该 gRPC API 增加 HTTP Post 方法的拓展定义。这种使用 Protobuf IDL 定义对应的 REST API 和 gRPC API的方式是Google API 指南中所推荐的最佳实践,也是B站在 Kratos V2框架中定义 API 的方式。对于框架是如何注册 HTTP 与 gRPC 服务感兴趣的同学欢迎体验 Kratos 框架,这里先不详细展开。DemoService 中除了对 HTTP 方法的定义,还包括服务概要,默认域名等标记。在 Message 中除了字段类型的定义,某些字段还带有属性行为的标记。我们支持用户使用 gogoproto 以及 google.api.field_behavior 中定义的消息对字段进行一些特殊标记,如定义字段默认值,是否必填,示例用法等参数相关的属性。

info:
title: DemoService API
description: Demo service responds to incoming requests.
paths:
/bilibili.api.probe.v1.DemoService/DemoBody:
...(同/poc/probe/demo_body)
/poc/probe/demo_body:
post:
tags:
- DemoService
description: DemoBody method receives a simple message and returns it.
operationId: DemoService_DemoBody
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/bilibili.api.probe.v1.SimpleMessage'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/bilibili.api.probe.v1.SimpleMessage'
components:
schemas:
bilibili.api.probe.v1.Embedded:
type: object
properties:
stringVal:
type: string
default: hello
repeatedStringVal:
type: array
items:
type: string
description: 一个字符串列表
mapStringVal:
type: object
additionalProperties:
type: string
description: 一个字符串Map
bilibili.api.probe.v1.SimpleMessage:
required:
- id
type: object
properties:
id:
type: integer
format: int32
embedded:
$ref: '#/components/schemas/bilibili.api.probe.v1.Embedded'
description: 请求、回复消息
tags:
- name: DemoService

最终通过工具生成上述对应 OpenAPI 文档,为 Proto 中对 HTTP 方法的定义提供标准 OpenAPI 格式的接口信息,将 gRPC Method 视为 POST 方法,生成一条类似的接口信息。

对于习惯使用JAVA进行开发的同学而言,同样地:

@GetMapping("/demo")
@Operation(summary = "用户接口 - debug",description = "示例")
@Parameter(name = "count", required = false)
public String debug(@RequestParam(defaultValue = "128", required = false) int count) {
String randomString = RandomStringUtils.randomAlphabetic(count);
LOGGER.debug(randomString);
return randomString;
}

上述代码引入io.swagger.v3包,定义了一个path为 ‘/demo’ 的接口,使用 swagger 注解对 controller 类中的map方法进行修饰。暴露生成接口文档方式十分简单,在 B 站自研的JAVA web框架 Pleidaes/ Kraten下开发,应用启动后直接调用 /api-docs 的接口就可以轻易拿到。这种利用 swagger 注解提供的来声明和操作输出,为 JAVA 应用实时生成接口文档是JAVA同学熟知的一种方式,对平台来说,要做的只是调用接口拿到 JAVA 应用的文档即可。

文档收集

我们的终极目标是做到全公司研发同学使用统一的框架,使用统一的 API 的定义和生成方式,管理平台就可以采集的标准且权威的API元信息,即实现公司API标准化。但这个目标不是一蹴而就的,在研发开发流程尚不”标准”时,我们通过多种渠道尽可能得获取到应用的接口信息,保证平台接口数据的完备性。

a. CI 时生成(最佳实践)

对于采用 gRPC 协议的接口来说,B 站采用的是单仓库管理模型管理协议文件:将协议原始文件 Proto 集中放到一个仓库中,根据内部服务治理的标准将文件划分至独立的命名空间进行细粒度管理,对外提供根据 Proto生成的目标语言仓库,例如 go 语言 proto-gen-go 仓库, JAVA语言 proto-gen-java 仓库,依赖某服务时直接 import 该仓库即可。

接口管理平台作为 Proto 以及 stub 的管理员,肩负着管理内部统一 Proto仓库的责任。但不论是协议文件还是存根代码,只是接口的定义,包括版本、命令定义、资源定义和错误码定义等等,不适宜直接作为接口文档展示给调用方。我们在推动内部 API 标准化的过程中,对定义 Proto 文件中接口的参数、默认行为、属性、必要注释等行为给出统一的规范,在通过在CI Pipeline 中安装 protoc-gen-bilibili-openapi 插件,该插件负责解析代码中的 Proto 文件,并对应生成一份标准 OpenAPI 文档。当用户请求合并代码到主分支时,自动触发流水线中接口平台的埋入的任务:扫描代码中的 Proto,提取出接口信息,生成对应文档后导入接口管理平台,完成对该应用接口文档的自动刷新。用户只是完成了一次基本的 Proto 的书写,就不再需要考虑后续其他的协作方面的事宜,接口文档,测试,桩代码,一切交给管理平台进行打理。

b. 在线服务采集

对于 Java 语言应用而言,应用部署后通过注册中心暴露服务地址,接口管理平台到指定环境中调用该地址下 /api-docs 接口获取到应用的接口文档,并与历史接口版本进行比对与更新,实现对 Java 应用接口文档的自动更新,整个过程对于开发者来说没有额外维护文档的负担,也不再需要关心自己的接口数据如何去暴露和分享给调用方。

API标准化尚未推广之前,公司内的 go 服务可能使用的是 Kratos 早期定义接口的方式,这部分应用通过 /metadata 接口对外暴露 path 信息。平台通过该接口采集到所有的接口路径后,由用户对接口的文档进行手动补全,等应用实现API标准化后再逐步由半自动进入全自动收集的模式。

03 版本管理

API 是用户与应用之间的约定,包括 URI 模式,有效负载结构,字段和参数名称,预期行为以及其他内容。在应用迭代的过程中,不可避免需要添加新的资源、修改资源或调整接口参数,随之会带来的接口变更管理的问题。例如,某个应用新加的 feature 改动了接口,但此时改动没经过测试,相当于只是草稿版本。开发希望能将草稿版本的接口分享给联调的人员,又不想影响正式版本,这在过去其实是一件比较棘手的事情。接口管理平台要做的就是通过版本管理区分好接口不同状态、不同来源、不同时期的信息。

 3.1 接口版本控制

接口参数的每一点变更一定都源于代码的变动,代码的提交才可能会导致接口版本的升级。服务本身没有变更,开发者代码没有产生过提交,是不会导致接口凭空变化的。基于此点共识,我们将应用代码的提交 (Commit ID) 与管理平台上的接口版本 (API Version) 关联。

接口管理平台对接口的正式版本及测试版本进行区分。测试版本的来源是测试环境中的应用,与 dev 代码分支的某次 commit 记录相关联;正式版本的来源是生产环境中的应用,与主分支代码的某个 tag 相关联。

研发每次提交代码,不管是用于测试发布还是正式发布,接口管理平台都会为接口生成相应新版本,对比新版本与历史版本的差异,这在多人协作开发的项目中非常受用。研发同学将某一次实验性版本的应用部署到测试环境后,就可以在接口平台上直接对刚刚提交的接口进行初步验证,或者由自动化测试又或是 QA 进行系统的测试;而正式版本经过完整的流水线测试、全链路灰度验证、部署在生产环境后,可直接被分享给调用方。

3.2 应用版本管理

对于接口的调用方来说,大多只会关心接口功能及使用参数。比如说调用方需要接入某个应用时,他想知道应用当前 V2 版本使用哪些接口可以满足他的需求,而不会关心这些接口有哪些版本。或者说,调用方接入的是历史版本 V1,暂时还不想升级到 V2,他想知道 V1 版本使用的接口是哪些参数。这种场景下,直接将V1版本的接口文档发给调用方即可。

应用是接口的集合,应用版本是接口版本的集合。有了接口的版本控制之后,再进行应用的版本管理就变得很容易了。接口管理平台可以为一组接口版本创建一个快照,这个快照就是应用版本。当应用每次正式上线后,我们可以通为应用创建一个该版本的接口快照,通过这样的管理方式,我们可以观察每个接口在各个应用版本下的变更情况,并追踪接口在应用中的生命周期变化。

04 接口协作、分享、调试

API 管理平台不仅是对接口元信息进行管理,打通数据、提升研发效率以及发掘元数据本身的价值同样是 API 管理平台的使命。

联动接口周边服务

例如我们将 API 管理平台与 API Gateway 打通,对于需要集成服务网关功能的接口,只需要在API 管理平台就上可以方便得跳转到对应地方进行配置,其他与接口有关的配置同样如此, 用户可以将接口管理平台作为入口,跳转至其他基础平台,提高用户效率的同时更好得与其他平台进行配合。

文档导出、分享

我们对这些接口信息以不同的格式进行展示,支持导出标准的 OpenAPI 格式的 json 文件。对于那些习惯使用第三方工具查看接口数据的研发同学来说,可通过工具导入 OpenAPI 文件或直接订阅平台,在本地客户端实时查看自己关注的接口。

接口调试、运行

接口调试可以简单分为两种:

  • 对于 HTTP 接口,类似 Apifox、Postman ,拿到接口数据,对接口进行的简单的临时调试
  • 对于GRPC接口,类似 Bloomrpc,导入 Proto 文件及其依赖后就可以对服务中的方法进行调试。

API 管理平台对这种两种接口的请求方式的进行了统一,用户不需要关心自己的接口是哪种协议,就可以直接点击调试。平台管理本身管理着 Proto 仓库,拥有全部内部协作的 Proto 元信息,即使需要客户端 Token 的 RPC 也可轻松发起,帮助用户进行初步的接口调试。并且在平台调试时也无需像普通调试工具一样指定域名、IP 后才可调试。平台侧打通注册中心,获取服务的信息,自动为用户键入目标地址。用户对于调试这件事仅仅需关注两点:接口及返回结果, 其他均由接口管理平台包办。

05 接口Mock

5.1 为什么需要对测试对象的依赖进行

Mock?

Mock 的本质是在调试期间构造出一些虚拟的返回对象。一个常见的场景:前后端分离,前端开发某个页面,需要后端先完成 API 的开发工作,两者进度不一致,出现前端等待后端的情况。如果使用 Mock 就可以减小这种影响,通过 Mock API 事先编写好 API 的数据生成规则,请求接口平台动态生成 API 的返回数据。前端开发可以通过访问 Mock API 来获得页面所需要的数据,继续开展工作。

Mock的好处:

  • 提高测试覆盖率

通过 Mock 构造各种正常和异常的返回结果,更充分地测试目标对象

  • 避免真实依赖的对测试产生影响
  • 提高测试效率

依赖的真实行为可能延时高,资源消耗大,而模拟是一种非常快的行为,能  加快整个测试流程。

 5.2 服务级Mock架构设计

Mock 粒度由细到粗分为方法级、类级别、接口级、服务级。大多 API-test 工具由于无法覆盖全部接口,只做到接口级别 Mock,但对于拥有全部接口元信息的接口管理平台来说,做到服务级别的 Mock 是顺水推舟的事情。

狭隘的理解服务端 Mock 是将服务的所有的接口无差别地全部 Mock,相当于是接口级别的极端做法。例如,某次在测试环境中进行服务联调时,对于某些尚未开发完成的接口或者不能在测试环境中被调用的接口,可采用 Mock 进行过渡;但对于已经上线的接口来说,流量应直接透传至真实服务,待拿到真实的响应数据后再返回给上游。这样不管是对减轻测试用例管理的负担还是提高测试的准确性都有很大的增益。

基于上述思想,我们设计了如下图所示的架构:

对于需要进行被 Mock 的服务,接口管理平台会在注册中心为该服务的注册出一个染色实例,并与注册中心维持心跳,在测试环境中部署的真实服务则作为兜底用的基准版本。收到流量时,匹配指定染色成功的请求会被注册中心转发至 Mock 实例,该实例实际是接口管理平台在提供服务。Mock 实例判断流量是否命中事先配置好的规则,若命中成功,则直接返回 规则中的响应; 若未能命中规则或未配置规则,则将流量原封不动地转发给基准版本的实例,由基准返回。我们的做法实际上是通过修改注册中心上服务与服务地址的映射关系,将依赖服务地址改成 Mock 地址实现 Mock 注入。

06 微服务标准化

我们一直致力于实现公司内部的微服务的标准化,包括接口规范化、接口标准化、数据格式统一化,这些标准是明确、可行且统一的,以保证各个微服务之间的可互操作性。B 站内部使用 Go 语言开发同学多数是在 Kratos 框架下进行开发的,使用 JAVA 语言开发的同学使用自研的 Pleiades/ Kraten 框架,这两种框架都为平台能顺利采集到接口信息提供了极大的便利。我们借助框架的力量,将 OpenAPI 格式 API 的标准集成在框架中,编译时期或 CI 阶段产生接口文档,开发各种配套的 Swagger、OpenAPI 的工具用来充分提取文档的接口信息。

API 元信息的管理是 API 标准化中的一环,我们希望利用标准的力量来做更多的事情,如监控、自动生成代码…探索更多的玩法,成为强有力的生产力工具。

本文章转载微信公众号@哔哩哔哩技术

#你可能也喜欢这些API文章!