兼容性
本章提供了有关版本控制部分中给出的破坏和保持兼容性修改的详细说明。
并不总是绝对清楚什么是不兼容的修改,这篇指南 应该(should) 被当成参考性的,而不是覆盖到所有情况。
下面列出的这些规则只涉及客户端兼容性,默认 API 作者了解部署(包括实现细节的变化)的需求。
一般的目标是服务端升级 minor 或 patch 不能影响客户端的兼容性:
- 代码兼容:针对 1.0 编写的代码在 1.1 上编译失败
- 二进制兼容:针对 1.0 编译的代码与 1.1 客户端的库链接/运行失败(具体的细节依赖客户端,不同情况有不同变化)
- 协议兼容:针对 1.0 构建的程序与 1.1 服务端通信失败
- 语义兼容:所有组件都能运行但产生意想不到的结果
简而言之:旧的客户端应该与相同 major 版本的新服务端正常工作,并且能够轻松地升级到新的 minor 版本。
由于客户端使用了自动生成和手写的代码,除了理论上的基于协议的考虑,还有一些实际的问题。通过生成新版本的客户端库来测试你的修改,并保证测试通过。
下面的讨论将 proto 信息分为三类:
- 请求信息(例如
GetBookRequest
) - 响应信息(例如
ListBooksResponse
) - 资源信息(例如
Book
,包括在其他资源消息中使用的任何消息)
这三类有不同的规则,例如请求信息只会从客户端发送到服务端,响应信息只会从服务端发送到客户端,但资源信息一般会在两者之间互相发送。尤其是可被修改的资源需要根据读取/修改/写入的循环来考虑。
保持向后兼容的修改
向 API 服务中添加 API 接口
从协议的角度看,这种修改总是安全的。唯一需要考虑的是客户端库可能已经通过手写的代码使用了新 API 接口的名字。如果新接口与其它完全正交,这种情况不太可能发生。如果是已存接口的简化版本,则很可能引起冲突。
向 API 接口中添加方法
除非添加了一个与现有客户端库中方法冲突的方法,这种修改没有问题。
一个会破坏兼容性的例子:如果有 GetFoo
方法,C# 代码生成器已经创建了 GetFoo
和 GetFooAsync
方法。因此从客户端角度来看,在 API 接口中添加 GetFooAsync 方法将会破坏兼容性。
向方法添加 HTTP 绑定
假设绑定没有引入任何歧义,使服务端响应以前被拒绝的 URL 是安全的。当将现有操作应用于新的资源名称时,可能(may) 会这样做。
向请求信息添加字段
添加请求字段可以是兼容的,只要不指定该字段的客户端在新版本中与旧版本表现相同。
会导致错误的最明显例子是分页:如果 API 的 v1.0 版本不支持,除非 page_size
默认值是无穷大(这样是不好的)才能在 v1.1 中加入分页。否则 v1.0 的客户端原本希望通过一次请求取得所有结果,但实际只能取到一部分。
向响应信息添加字段
只要不改变其他响应字段的行为,就可以扩展不是资源的响应消息(例如 ListBooksResponse
),而不会破坏兼容性。即使导致冗余,任何在旧的响应消息中的字段也应该存在于新的响应中并保持它原来的语义。
例如,1.0 中的一个查询请求的响应有 bool 类型的字段 contained_duplicates
来指示因为重复而忽略掉的结果。在 1.1 中,我们在 duplicate_count
字段中提供更详细的信息,尽管从 1.1 版本来看是多余的,但 contained_duplicates
字段 必须(must) 要保留。
向枚举添加值
只在请求信息中使用的枚举类型可以自由扩展来添加新元素。例如,使用资源视图时,新的视图能够添加到新 minor 版本中。客户端从来不需要接收此枚举,所以也不需要关心它。
对于资源消息和响应消息,默认假设客户端应该处理它意识不到的枚举值。但是 API 作者应该意识到编写能够正确处理新枚举值的代码可能是困难的。应该(should) 在文档中记录当遇到未知枚举值时客户端的期望行为。
proto3 允许客户端接收它们不关心的值并且当执行重新序列化消息时会保持值不变,所以这样就不会打破读取/修改/写入循环的兼容性。JSON 格式允许发送数值,其中该值的“名称”是未知的,但是服务端通常不会知道客户端是否真正知道特定值。因此 JSON 客户端可能知道它们已经收到了以前对他们未知的值,但他们只会看到名称或数字而不是两个都有。在读取/修改/写入循环中将相同的值返回给服务端不应该修改这个值,因为服务端应该理解这两种形式。
添加只输出(output-only)的资源字段
可以(may) 添加仅由服务端提供的资源实体中的字段。服务端 可以(may) 验证请求中的值是否有效,但是如果该值被省略则 一定不能(must not) 失败。
破坏向后兼容的修改
删除/重命名服务、接口、字段名、方法或枚举值
从根本上说,如果客户端代码使用了某些字段,那么删除或重命名它将会破坏兼容性,并且 必须(must) 增加 major 版本号。引用旧名称的一些语言(如 C# 和 Java)在编译时会失败, 另一些语言会引起运行时异常或数据丢失。协议格式的兼容性在这里是无关紧要的。
修改 HTTP 绑定
这里的 修改 实际指 删除 和 添加。例如,你想要支持 PATCH,但已发布的版本支持 PUT,或者已经使用了错误的自定义动词,你 可以(may) 添加新的绑定,但是 一定不要(must not) 移除旧的,因为和删除服务的方法一样会破坏兼容性。
修改字段类型
尽管新类型是协议兼容的,能够改变客户端库自动生成的代码,因此 必须(must) 要升级 major 版本。会导致需要编译的静态类型的语言在编译期就发生错误。
修改资源名的格式
资源 一定不能(must not) 修改名字-这意味着集合名不能被修改。
不像其他大多数破坏兼容性的修改,这会影响 major 版本号:如果客户端期望使用 v2.0 访问在 v1.0 中创建的资源(或反过来),则应该在两个版本中使用相同的资源名称。
对资源名的验证也 不应该(should not) 改变,原因如下:
- 如果验证变严格,之前成功能请求现在可能会失败
- 如果比之前文档中记录的验证要宽松,依据之前文档的客户端可能会被破坏。客户端很可能在其他地方保存了资源名,并且对字符集和名字的长度敏感。或者,客户端可能会执行自己的资源名称验证来保持与文档一致。(例如,当开始支持 EC2 资源的长 ID 时,亚马逊向用户发出了许多警告并提供了迁移的时间)
请注意这样的修改只能在 proto 的文档中可见。因此当评审 CL 时审查除注释外的修改是不够的。
修改已有请求的可见性(visible behavior)
客户端总是依赖 API 的行为和语义,即使没有明确支持或记录此行为。因为在大多数情况下修改 API 的行为和语义在客户端看来是破坏性的。如果某行为不是加密隐藏的,你 应该(should) 假设用户已经依赖它了。
因为这个原因加密分页 token 是个好主意,以防止用户创建自己的 token,以及防止当 token 行为发生变化时可能带来的不兼容性。
在 HTTP 定义中修改 URL 格式
除了上面列出的资源名称的变化,这里还要考虑两种类型的修改:
- 自定义方法名:虽然不是资源名称的一部分,但自定义方法名称是 REST 客户端 POST 请求 URL 的一部分。更改自定义方法名称不应该破坏 gRPC 客户端,但是公共 API 必须假定它们具有 REST 客户端。
- 资源参数名:从
v1/shelves/{shelf}/books/{book}
到v1/shelves/{shelf_id}/books/{book_id}
的修改不会影响替代的资源名称,但可能会影响代码生成。
在资源消息中添加读/写字段
客户端会经常执行读取/修改/写入的操作。大多数客户端不支持它们意识不到的字段值,特别是 proto3 不支持。你可以指定任意消息类型(而不是原始类型)中缺失的字段表示更新时不会被修改,但这样使删除这样的字段变的困难。原始类型(包括 string
和 bytes
)不能简单地使用这种方法,因为明确地设置 int32
的值为 0
和不对它设置值在 proto3 中并没有区别。
使用字段掩码来进行所有更新操作不会有问题,因为客户端不会隐式覆盖其不知道的字段。然而这是一个不寻常的决定,因为大部分 API 允许全部资源被更新。