目录
Dubbo 实现接口版本管理和灰度发布的 Java 示例代码
Spring Cloud 实现接口版本管理和灰度发布的 Java 示例代码
服务API版本控制设计与实践
在 vivo 应用商店的发展过程中,客户端频繁迭代带来服务端 API 版本控制难题。业界有The Knot 无版本、Point-to-Point 点对点、Compatible Versioning 兼容性版本控制三种方案。vivo 应用商店服务端改造时,Old-Service 采用Point-to-Point 模式,内部 RPC 服务采用The Knot 模式;兼容性版本控制常用API 版本号、客户端版本号、新增功能标识参数等策略。此外,接口设计要注重场景化、鉴权和服务隔离,同时考虑技术迭代对版本控制的影响,选择最适合的方案。
详细总结
vivo 应用商店从百万日活发展到几千万日活,客户端经历上百个版本迭代,服务端也从单体架构升级到服务集群和微服务架构。在此过程中,服务端面临兼容不同版本客户端 API 的问题,主要采用以下三种版本控制方案:
方案名称 | 特点 | 应用场景 | 优缺点 |
---|---|---|---|
The Knot 无版本 | 平台 API 永远只有一个版本,所有用户必须使用最新 API | 内部服务 | 对服务端维护有一定简化,但要求服务使用者及时适配最新版本,不适用用户产品 |
Point-to-Point 点对点 | 平台 API 版本自带版本号,用户根据需求选择 | 应用商店服务端改造中 Old-Service 维护历史版本 | 开发和运维的维护成本会随版本增加而增加 |
Compatible Versioning 兼容性版本控制 | 平台只有一个版本,最新版本兼容以前版本 API 行为 | 应用商店多数业务场景 | 最常用,能较好兼容不同版本客户端 |
服务端改造分两个阶段:
- 阶段一:新版本新接口采用新的 JSON 协议,已有功能接口根据客户端版本区分,返回不同协议格式内容。
- 阶段二:新的版本商店依赖的所有接口完成协议升级后,升级后端架构和框架,拆分和独立新工程,历史工程只提供给历史版本使用。例如针对大流量高并发及基础服务场景进行独立服务拆分,提取公共内部 RPC 服务。
兼容性版本控制的具体策略如下:
策略名称 | 应用场景 | 实施方式 | 注意事项 |
---|---|---|---|
API 版本号控制 | 应用商店首页列表等接口逻辑变化大的场景 | 在接口 URL 上新增版本字段(如/{version}/index )或在请求头携带版本参数,控制层根据版本处理不同逻辑 |
避免在 service 层简单根据版本判断导致逻辑复杂和影响低版本 |
客户端版本号控制 | 商店穿插 Banner 等需适配不同版本的场景 | 开发分配资源 ID,设置支持客户端版本范围,将过滤逻辑统一封装到工具类或建立统一资源上层类实现过滤 | 代码可读性差、客户端版本号不确定(火车发布模式导致版本跳跃不连续),需合理设计过滤逻辑 |
新增功能标识参数 | 已有接口进行功能增强的场景,如应用商店增量更新算法升级 | 在原来接口基础上新增整数字段作为标志参数,服务端通过位运算判断逻辑 | 客户端摆脱功能与版本的强一致性,便于接口功能扩展 |
接口设计还需注重:
- 场景化:在接口设计时带上场景因素,如在路径上定义场景(
/app/{scene}/upgrade
),便于分析线上不同来源请求量级和问题。 - 鉴权和服务隔离:做好接口调用记录、鉴权和服务隔离,评估与服务调用方的调用量,避免问题发生,出问题时能快速发现和分析来源,降低损失。
随着技术发展,如 Flutter、Weex 等技术可减少客户端发版频次和服务端兼容性处理成本,开发应考虑后续扩展性和可维护性,选择最适合业务的方案。
版本接口例子:
@GetMapping("/app/{scene}/upgrade/{currentVersion}")
public ResponseEntity<String> upgradeApp(
@PathVariable String scene,
@PathVariable String currentVersion) {
// 处理逻辑
return ResponseEntity.ok("处理结果");
}
@PostMapping("/app/{scene}/upgrade")
public ResponseEntity<String> upgradeApp(
@PathVariable String scene,
@RequestBody UpgradeRequest request) {
// UpgradeRequest是包含deviceInfo和userPreferences的Java类
return ResponseEntity.ok("处理结果");
}
关键问题
- 为什么说 The Knot 无版本模式不太适用于用户产品?
- 答案:The Knot 无版本模式要求所有用户必须使用最新 API,任何 API 修改都会影响平台所有用户。而用户产品中,用户对产品的更新节奏难以统一,强制用户升级可能导致用户流失,所以该模式不太适用于用户产品。
- 客户端版本号控制策略中,如何解决客户端版本号不连续带来的问题?
- 答案:将某个资源支持对应的客户端版本作为资源对象的一个成员属性,赋予资源统一、灵活的过滤能力,开发分配资源 ID 并设置支持客户端版本范围,把过滤逻辑统一封装到工具类或建立统一资源上层类实现过滤,避免简单的硬编码 if-else 判断,以此应对版本号不连续问题。
- 在接口设计中,场景化的作用体现在哪些方面?
- 答案:场景化可帮助精细化分析请求,比如在应用商店客户端检测应用版本的场景中,明确请求时机(如用户启动、切换 wlan 环境、定时检测等)。通过在接口路径带上场景(如
/app/{scene}/upgrade
),能区分不同来源的请求,便于分析请求量级、判断请求有效性、排查问题等,对线上业务分析有很大帮助。
- 答案:场景化可帮助精细化分析请求,比如在应用商店客户端检测应用版本的场景中,明确请求时机(如用户启动、切换 wlan 环境、定时检测等)。通过在接口路径带上场景(如
关于接口升级版本的一些思考
在需求开发涉及线上主接口改动时,保持 “前后向兼容” 的接口可直接发版,包括请求新增字段(被调用方需处理有无该字段的情况,http 请求自行判断,idl 用optional
约束)、返回新增字段、请求删除字段(注意 idl 序号不复用);返回删除字段不兼容。对于不兼容的接口改动,可新增接口并通知上层迁移,或在流量低时停机升级,也可上下游约定统一切换时间。
详细总结
在接口升级版本的需求开发中,若涉及线上主接口改动需谨慎对待。
- 可直接发版的接口改动类型:保持 “前后向兼容” 的接口改动可直接发版,有如下几种情况:
接口改动类型 具体要求 注意事项 请求新增字段 被调用方必须能同时处理有该字段(新版)和无该字段(旧版)的情况。http 请求自行判断有无数据传入,idl 使用 optional
进行约束,不能用required
/ 返回新增字段 无论 http 请求还是 idl 请求方式,都可直接新增 / 请求删除字段 可直接修改升级 idl 中后续若对请求改动,尽量不要复用原来序号 - 不兼容的接口改动类型及影响:返回删除字段属于不兼容改动。对于 http 请求,要求调用方已完全废弃该字段使用;对于 idl(指生成 json 请求响应格式的,如 JSON - RPC/XML - RPC 等),会出现解析响应失败导致请求失败的情况。若为生成二进制格式的 idl(如 Protobuf、Thrift、Avro 等),除类似考虑外,还需额外注意 idl 结构体中的序号不要复用。
- 不兼容接口的发版处理方式:
- 新增接口并通知迁移:对于无法兼容的接口升级或重构,新增一个接口,并向上层通报原接口不再维护,建议迁移。
- 其他方式:在流量很低的情况下,可考虑停机升级;也可通过上下游约定统一切换时间(如利用配置中心切换)来解决接口不兼容问题。
关键问题
- 为什么请求新增字段时 idl 要用
optional
约束而不能用required
?- 答案:使用
optional
约束,能使被调用方同时处理有该字段(新版)和无该字段(旧版)的情况,符合前后向兼容要求,不限制调用方升级时间。若用required
,旧版调用方没有该字段时会导致请求失败,无法实现前后向兼容。
- 答案:使用
- 返回删除字段不兼容对不同类型 idl 的影响有何差异?
- 答案:对于生成 json 请求响应格式的 idl(如 JSON - RPC/XML - RPC 等),会出现解析响应失败导致请求失败;对于生成二进制格式的 idl(如 Protobuf、Thrift、Avro 等),除解析失败外,还需额外注意结构体中的序号不要复用,否则可能引发更多兼容性问题。
- 在什么情况下选择停机升级不兼容接口更好?
- 答案:当接口流量很低时,选择停机升级更好。因为此时停机对用户的影响较小,相比新增接口并通知迁移或约定统一切换时间,停机升级操作更简单直接,能快速完成接口升级,减少因接口不兼容带来的开发和协调成本。
有哪些工具或框架可以帮助管理RPC接口的版本?
在管理 RPC 接口的版本时,有许多工具和框架可以提供帮助,以下是一些常见的工具和框架:
- gRPC:是一个高性能、开源和通用的 RPC 框架。它使用 Protocol Buffers(protobuf)作为接口描述语言,protobuf 支持定义不同版本的消息结构,通过在消息定义中添加、修改或删除字段,并使用合适的字段修饰符(如
optional
、required
、repeated
)来处理版本兼容性。gRPC 本身也支持在服务端和客户端处理不同版本的请求和响应,通过在客户端和服务端分别实现相应的逻辑来确保版本的兼容。例如,在服务端可以根据请求的版本信息调用不同的处理函数,客户端可以根据服务端返回的版本信息正确解析响应。 - Thrift:是由 Apache 开发的一个可扩展的跨语言服务开发框架。Thrift 有自己的接口描述语言(IDL),通过 Thrift IDL 可以定义不同版本的接口,并且在升级接口时,可以使用一些技巧来保持兼容性,如添加新的字段时使用
optional
修饰符。Thrift 提供了多种语言的代码生成器,根据 IDL 文件生成不同语言的客户端和服务端代码,方便在不同语言的项目中实现对不同版本接口的支持。例如,在更新接口版本后,重新生成代码并部署到客户端和服务端,确保它们能够正确处理新的接口版本。 - Dubbo:是一个开源的高性能 RPC 框架,常用于 Java 生态系统。Dubbo 提供了丰富的服务治理功能,其中包括对接口版本的管理。Dubbo 支持在服务端和客户端配置接口的版本号,通过版本号来区分不同版本的服务。当客户端调用服务时,会根据配置的版本号来选择合适的服务提供者。同时,Dubbo 还支持服务的灰度发布等功能,方便在接口升级时逐步将流量切换到新版本的服务上,减