google api design guide学习
前言
设计一个易用、可靠的API对于一个面向公共使用的系统是十分重要的。尤其是暴露给外部用户使用的OpenAPI,如果在初期没有提前设计好,后续由于大量存量用户的使用,就会导致无法轻易改动,形成历史包袱。最近看了下Google的API设计指南,稍作总结,与各位分享。
Google API设计指南总体上遵循了RESTful API面向资源的核心设计原则,但是在此基础上也额外提出了自己的指导理念,避免了一些RESTful API的副作用。
API设计指南核心知识
为了设计利用google api设计指南来设计面向资源的API,下面会介绍一些基本的前置知识。
资源
资源是根据业务抽象的一个被命名的整个实体,例如gmail当中的用户、邮件等都是资源。
资源集合与简单资源
一个集合包含相同类型的资源列表,例如一个用户拥有一组联系人。相反的,不是集合资源的资源可以称之为简单资源。
子资源
面向资源的API通常被构建为层次结构,一个资源下可以有0个或者多个子资源。下图是google gmail api的资源层次结构
资源名称
资源名称由集合 ID 和资源 ID 构成,按分层方式组织并以正斜杠分隔。如果资源包含子资源,则子资源的名称由父资源名称后跟子资源的 ID 组成,也以正斜杠分隔(资源名称不要再包含正斜杠)。例子如下:
1 | "//library.googleapis.com/shelves/shelf1/books/book2" |
- 资源名称中的非末尾资源 ID 必须只有 1 个网址段,而资源名称末尾的资源 ID 可以具有多个 URI 段:也就是最后个资源是可以有多个URI段的。比如最后个资源是个python文件,则路径可以是source/py/parser.py
- 资源名称(name):这里指的是完整资源名称,多个资源ID以斜杠组织起来的层次结构整体称之为资源名称。必须使用纯字符串来表示资源名称。注意我们使用资源名称来标识资源,而不是资源ID。资源名称应该像普通文件路径一样处理。当资源名称在不同组件之间传递时,必须将它视为原子值,并且不得有任何数据损失。资源名称可以通过正斜杠分隔快速得到资源ID.这样可以避免因错误解析或修改资源名称而导致的问题。将资源名称视为一个不可分割的原子字符串,以确保其一致性和稳定性。例如一个资源名称shelves/shelf1/books/book2 应该在处理时作为一个整体进行原子处理。下面的proto文件定义就是一个例子:
资源集合ID的命名规则:
- 必须是有效的 C/C++ 标识符。
- 必须是复数形式的首字母小写驼峰体。如果该词语没有合适的复数形式,例如“evidence(证据)”和“weather(天气)”,则应该使用单数形式。
- 必须使用简明扼要的英文词语。
- 应该避免过于笼统的词语,或对其进行限定后再使用。例如,rowValues 优先于 values。应该避免在不加以限定的情况下使用以下词语:
1 | elements |
核心理念
- 使用资源名称而不是资源ID来标识资源:这个和我们一般的直觉是相反的,使用原子的完整的资源名称可以带来诸多好处:
- 容易记忆:资源层次结构不需要记忆,而是作为一个院子整体
- 简化调用:传递字符串比传递多个有关系的资源实体更加简单
- 简化处理:系统不需要识别多个资源实体,而是作为原子处理
- 提升API设计灵活性:如果使用资源ID,就需要为每个资源实体设计API,但是作为一个原子资源名称,统一设计一个针对资源名称的API即可。由于只针对最核心的资源名称设计API,这样的资源API设计更加灵活、易于扩展。
方法
标准方法
主要指的是可以映射到RESTful API的方法。内容如下:
- List: 一般用在分页、结果排序等场景,获取单个有限、无缓存的集合。注意和自定义方法batchGet的区别,List的是获取单个资源集合下的资源,batchGet可以获取多个资源集合下的同类型资源例如:
1 | POST /v1/books:batchGet |
- Get: 需要一个资源名称和零个或多个参数作为输入,并返回指定的资源
- Create: Create 方法需要一个父资源名称、一个资源以及零个或多个参数作为输入。它在指定的父资源下创建新资源,并返回新建的资源。body包含创建的资源信息,URL资源路径中需要指定parent信息例如**/v1/shelves/shelf1/books**表示添加到shelf1书架上。
- Update: 方法需要一条包含一个资源的请求消息和零个或多个参数作为输入。它更新指定的资源及其属性,并返回更新后的资源。Update 方法 不得包含任何“重命名”或“移动”资源的功能,这些功能应该由自定义方法来处理。
- Delete: 方法需要一个资源名称和零个或多个参数作为输入,并删除或计划删除指定的资源。Delete 方法 应该返回 google.protobuf.Empty。
设计API时应优先使用标准API。以下是标准方法比自定义方法更适用的示例:
- 使用不同查询参数的查询资源(使用带有标准列表过滤的标准 list 方法)。
- 简单的资源属性更改(使用带有字段掩码的标准 update 方法)。
- 关闭一个通知(使用标准 delete 方法)。
自定义方法
自定义方法作为一个动词需要映射到资源上(采用冒号),例如恢复删除文件:
1 | POST /files/a/long/file/name:undelete |
google api设计指南提倡优先使用标准方法,但是仍然有很多情形使用标准方法无法满足。这些场景包括:
- 重启虚拟机。 设计备选方案可能是“在重启集合中创建一个重启资源”,这会让人感觉过于复杂,或者“虚拟机具有可变状态,客户端可以将状态从 RUNNING 更新到 RESTARTING”,这会产生可能存在哪些其他状态转换的问题。 此外,重启是一个常见概念,可以合理转化为一个自定义方法,从直观上来说符合开发者的预期。
- 发送邮件。 创建一个电子邮件消息不一定意味着要发送它(草稿)。与设计备选方案(将消息移动到“发件箱”集合)相比,自定义方法更容易被 API 用户发现,并且可以更直接地对概念进行建模。
- 提拔员工。 如果作为标准 update 方法实现,客户端需要复制企业提拔流程管理政策,以确保提拔发生在正确的级别,并属于同一职业阶梯等等。
- 批处理方法。 对于对性能要求苛刻的方法,提供自定义批处理方法可以有助于减少每个请求的开销。例如,accounts.locations.batchGet。
下面是设计指南整理的一些常用自定义方法:
标准字段
对常用的标准字段有个统一定义,确保不同的API中有相同的概念:
名称 | 类型 | 说明 |
---|---|---|
name | string | name 字段应包含相对资源名称 |
parent | string | 对于资源定义和 List/Create 请求,parent 字段应包含父级相对资源名称 |
create_time | Timestamp | 创建实体的时间戳。 |
update_time | Timestamp | 最后更新实体的时间戳。注意:执行 create/patch/delete 操作时会更新 update_time。 |
delete_time | Timestamp | 删除实体的时间戳,仅当它支持保留时才适用。 |
expire_time | Timestamp | 实体到期时的到期时间戳。 |
start_time | Timestamp | 标记某个时间段开始的时间戳。 |
end_time | Timestamp | 标记某个时间段或操作结束的时间戳(无论其成功与否)。 |
read_time | Timestamp | 应读取(如果在请求中使用)或已读取(如果在响应中使用)特定实体的时间戳。 |
time_zone | string | 时区名称。它应该是 IANA TZ |
名称,例如“America/Los_Angeles”。如需了解详情,请参阅 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones。 | ||
region_code | string | 位置的 Unicode 国家/地区代码 (CLDR),例如“US”和“419”。如需了解详情,请访问 http://www.unicode.org/reports/tr35/#unicode_region_subtag。 |
language_code | string | BCP-47 语言代码,例如“en-US”或“sr-Latn”。如需了解详情,请参阅 http://www.unicode.org/reports/tr35/#Unicode_locale_identifier。 |
mime_type | string | IANA 发布的 MIME 类型(也称为媒体类型)。如需了解详情,请参阅 https://www.iana.org/assignments/media-types/media-types.xhtml。 |
display_name | string | 实体的显示名称。 |
title | string | 实体的官方名称,例如公司名称。它应被视为 display_name 的正式版本。 |
description | string | 实体的一个或多个文本描述段落。 |
filter | string | List 方法的标准过滤器参数。请参阅 AIP-160 |
。 | ||
query | string | 如果应用于搜索方法(即 :search |
),则与 filter 相同。 | ||
page_token | string | List 请求中的分页令牌。 |
page_size | int32 | List 请求中的分页大小。 |
total_size | int32 | 列表中与分页无关的项目总数。 |
next_page_token | string | List 响应中的下一个分页令牌。它应该用作后续请求的 page_token。空值表示不再有结果。 |
order_by | string | 指定 List 请求的结果排序。 |
progress_percent | int32 | 指定操作的进度百分比 (0-100)。值 -1 表示进度未知. |
request_id | string | 用于检测重复请求的唯一字符串 ID。 |
resume_token | string | 用于恢复流式传输请求的不透明令牌。 |
labels | map<string, string> | 表示 Cloud 资源标签。 |
show_deleted | bool | 如果资源允许恢复删除行为,相应的 List 方法必须具有 show_deleted 字段,以便客户端可以发现已删除的资源。 |
update_mask | FieldMask | 它用于 Update 请求消息,该消息用于对资源执行部分更新。此掩码与资源相关,而不是与请求消息相关。 |
validate_only | bool | 如果为 true,则表示仅应验证给定请求,而不执行该请求。 |
错误
错误模型
Google API 的错误模型由 google.rpc.Status 逻辑定义,该实例在发生 API 错误时返回给客户端。以下代码段显示了错误模型的总体设计。模型十分的精炼,主要包含内容就是code、msg、detail。错误实例配合资源从而指明了具体什么资源上产生了什么问题。千万不要为某个资源针对性实现错误实例,而是通过错误实例组合资源的方式来提供简单、高效的面向资源的错误信息。
1 | package google.rpc; |
错误代码
Google API 必须使用 google.rpc.Code 定义的规范错误代码。单个 API 应避免定义其他错误代码,因为开发人员不太可能编写用于处理大量错误代码的逻辑。参考信息:每次 API 调用平均处理 3 个错误代码意味着大多数应用逻辑仅用于错误处理,这并不利于良好的开发者体验。
错误消息
提供错误消息的时候遵循如下原则:
- 不要假设用户是您 API 的专家用户。用户可以是客户端开发者、运维人员、IT 人员或应用的最终用户。
- 不要假设用户了解有关服务实现的任何信息,或者熟悉错误的上下文(例如日志分析)。
- 如果可能,应构建错误消息,以便技术用户(但不一定是 API 开发人员)可以响应错误并改正。
- 确保错误消息内容简洁。如果需要,请提供一个链接,便于有疑问的读者提问、提供反馈或详细了解错误消息中不方便说明的信息。此外,可使用详细信息字段来提供更多信息。
错误详情
Google API 为错误详细信息定义了一组标准错误负载,您可在 google/rpc/error_details.proto 中找到这些错误负载。 它们涵盖了对于 API 错误的最常见需求,例如配额失败和无效参数。与错误代码一样,开发者应尽可能使用这些标准载荷。
下面是一些示例 error_details 载荷:
- ErrorInfo 提供既稳定又可扩展的结构化错误信息。它提供了人、应用可以依赖的稳定且可扩展的错误信息。每个 ErrorInfo 都有三项信息:错误网域、错误原因和一组错误元数据,例如此示例。如需了解详情,请参阅 ErrorInfo 定义。对于 Google API,主要错误网域是 googleapis.com,相应的错误原因由 google.api.ErrorReason 枚举定义。如需了解详情,请参阅 google.api.ErrorReason 定义。
- RetryInfo:描述客户端何时可以重试失败的请求,这些内容可能在以下方法中返回:Code.UNAVAILABLE 或 Code.ABORTED
- QuotaFailure:描述配额检查失败的方式,这些内容可能在以下方法中返回:Code.RESOURCE_EXHAUSTED
- BadRequest:描述客户端请求中的违规行为,这些内容可能在以下方法中返回:Code.INVALID_ARGUMENT
错误本地化
google.rpc.Status 中的 message 字段面向开发人员,必须使用英语。
如果需要面向用户的错误消息,请使用 google.rpc.LocalizedMessage 作为您的详细信息字段。虽然 google.rpc.LocalizedMessage 中的消息字段可以进行本地化,请确保 google.rpc.Status 中的消息字段使用英语。
默认情况下,API 服务应使用经过身份验证的用户的语言区域设置或 HTTP Accept-Language 标头或请求中的 language_code 参数来确定本地化的语言。
错误映射
这里的核心是系统设计错误模型时有一个内部的相对统一的错误模型,根据具体的外部应用场景(例如不同协议、不同语言客户端实现、不同系统应用场景)在映射成一个外部使用的具体错误模型。这样好处是:
- 保护内部实现:屏蔽API实现细节,方便更改内部实现的时候不影响已经使用的API和客户端
- 清晰的错误信息:暴露更易于理解的错误信息
- 灵活处理:例如将多种内部错误代码映射成一个外部错误代码
TIPS: 如果你的系统复杂度不是特别高,不直接采用这种内外分离的错误设计模型也是可以的。但是像google这样大的系统,是非常值得使用的
处理错误
处理不是自身产生的错误时主要包含以下工作
- 重试错误:一般可以用指数会退重试请求
- 传播错误:隐藏机密信息、重新调整错误内容等
- 重现错误:尝试复现错误
生成错误
这个主要是针对服务器开发者的要求。熟悉常见的错误码、错误负载。错误负载建议优先使用google提供的标准错误负载。
命名规则
总体原则简单、直观、一致。采用美式英语、合理的使用缩写、单一职责、避免概念模糊都是常用手段。这块内容具体可以参考google api design guide中的内容,这边不再赘述。几个自己觉得值得关注的几点:
- 单数与复数:软件包使用单数,其他字段方法则合理的使用复数
- 关注接口、方法、字段的命名规则:平时用的比较多。接口注意面向资源,方法注意祈使语气、字段注意一些约定俗成的命名习惯。
- 请求、响应消息:采用xxxRequest和xxxResponse。像ES的请求响应消息就是如此命名的。
设计模式
- 表示范围:用约定俗成的半开区间[,)
- 为资源设计labels字段: 常用能力,让用户可以为资源附加元数据
- 长时间运行的操作:如果某个 API 方法通常需要很长时间才能完成,您可以通过适当设计,让其向客户端返回“长时间运行的操作”资源,客户端可以使用该资源来跟踪进度和接收结果。 Operation 定义了一个标准接口来使用长时间运行的操作。 各个 API 不得为长时间运行的操作定义自己的接口,以避免不一致性。
- 列表分页:一开始就支持分页。
- 通配符列出子集合:有时,API 需要让客户跨子集执行 List/Search 操作。例如,“API 图书馆”有一组书架,每个书架都有一系列书籍,而客户希望在所有书架上搜索某一本书。在这种情况下,建议在子集合上使用标准 List,并为父集合指定通配符集合 ID “-”。对于“API 图书馆”示例,我们可以使用以下 REST API 请求:
1 | GET https://library.googleapis.com/v1/shelves/-/books?filter=xxx |
- 排序提供order_by字段
- 验证请求字段validate_only解决请求副作用
- 请求提供requestId
- 枚举第一个字段提供默认值xxx_UNSPECIFIED=0来安全返回
- 使用扩展巴科斯范式(Extended Backus-Naur Form,简写为“EBNF”)语法定义API可以接受的数据格式:
1 | Production = name "=" [ Expression ] ";" ; |
- 避免使用无符号整型(有些编程语言不支持)
- 部分响应:通过FieldMask来过滤,而不是设计独立的接口
- 使用资源视图返回自定义资源: 通过一个枚举表示视图,命名可以是xxxView
1 | package google.example.library.v1; |
- 使用Etag使得客户端利用缓存数据
- 输出字段:protobuf提供OUTPUT_ONLY的标记来标识输出字段,告知开发者应该不使用该字段来作为客户端输入。例如像create_time这种仅仅由服务端生成的字段
- 单例资源:当只有一个资源实例存在于其父资源中(如果没有父资源,则在 API 中)时,可以使用单例资源。。省略create/delete方法,一般依赖父资源进行隐式创建和删除。例如
1 | get: "/v1/{name=users/*/settings}" |
- 流式半关闭:服务端发起半关闭信号,客户端还能接受服务端的请求,但是不能发新消息给服务端
- 采用网域范围名称: 用于避免名字冲突,例如Kubernetes API 版本:networking.k8s.io/v1
- boolean、枚举、字符串类型选择:boolean最不可扩展,enum和string扩展性(注意约定可以支持的值)都很好,但是string可以更好兼容外部系统,因为很多外部系统使用字符串来标识特定值或代码
- 采用流式处理大负载
- **用optional primitive字段区分设置为null还是未设置值
**
内嵌API文档
这块是教我们如何针对资源、字段方法等写注释的。一些惯用词可以了解下:
1 | must, must not, required, shall, shall not, should, should not, recommended, may, and optional |
版本控制
版本控制主要就是用于支持API修改的场景。主要推荐基于发布版本的版本控制:准备三个版本alpha、beta、正式。标记为弃用的API的更新不能引入beta和正式版。beta版本中弃用时间满足180天的可以删除代码。
面向资源设计API的步骤
这个基本和RESTful API倡导的理念是一致的。总体的API设计流程可以采用如下方式:
- 确定 API 提供的资源类型。
- 确定资源之间的关系。
- 根据类型和关系确定资源名称方案。
- 确定资源架构。
- 将最小的方法集附加到资源。
指南中哪些点值得借鉴到我们自己的系统中
其实google api设计指南有不少内容是关于结合protobuf来设计gRPC API的。指南中的内容应用多少取决于你系统本身的大小和复杂度。
- 基于gRPC实现的系统或者面向public使用的大型系统:可以考虑基于gRPC然后重度参考google api设计指南来实现。google这块很有经验,拷贝人家最佳实践是没错的。
- 不使用gRPC的中小型系统:面向资源的API设计、命名规则、错误等内容仍然是值得效仿的。尤其面向资源的API设计结合google api design补充的自定义方法等能力可以使得我们设计出一个足够正交、高效的API体系。
google设计指南解决了哪些RESTful API的副作用
完全遵循RESTful来设计API,你会发现很多时候根本没法满足业务。google的API设计指南其实提供了不少机制来弥补典型RESTful API的缺点,更加具备落地性:
- 标准方法和自定义方法:REST API 倾向于使用标准的 CRUD 操作,这可能导致无法表示某些特定的、非 CRUD 类型的操作。Google API 设计指南建议,在某些情况下,可以使用自定义方法来实现这些操作。例如,你可以创建一个自定义方法(如 POST /v1/users/{userId}:suspend)来暂停一个用户,而不是仅仅依赖于 RESTful 风格的 CRUD 操作。
- 部分响应:REST API 有时会导致过度获取或过度发送数据的问题。Google API 设计指南推荐使用部分响应(partial responses)来解决这个问题。客户端可以通过指定需要的字段来减少返回的数据量,例如 GET /v1/users?fields=name,age。
- 标准化错误处理:REST API 可能会导致不一致的错误处理。Google API 设计指南提供了一套统一的错误处理规范,包括使用适当的 HTTP 状态码和结构化的错误响应。这使得客户端可以更容易地处理错误情况。
参考资料
- [1] google api design guide
- [2] 凤凰架构:REST 设计风格