分布式API设计 上
- 当作为一个架构师,选择分布式 API 框架,你需要注意哪些事项
- 当你作为一个服务提供者,提供分布式 API, 你需要注意哪些事项,至少有 10 点。
- 当你需要调用一个分布式 API,你需要注意哪些事项,至少有 10 点。
- 当调用出现故障或者性能优化问题,有哪些思路解决
分布式 API
软件世界,是通过 API 互相链接,这些 API 或者是进程内的 API,或者是分布式 API,通过这些 API,构造出精彩的复杂的企业应用,互联网世界,物联网世界。学习完本节后,当你是调用一个分布式 API 的时候,你会掌握 API 调用最佳实践,当你提供一个分布式 API 时候,你会清楚 API 设计原则,本章覆盖如下内容
- 分布式 API 的技术选型
- REST API 设计原则 (参考 远程调用 API 设计 下:REST,待定)
面向架构质量的设计
我们知道,架构主要目标是对软件系统分解成较小更容易实现的元素,如模块或者子系统,并能让这些元素协同完成业务需求,对于通常的程序员视角来说,架构貌似就是画几个框,然后连上线即可。
如下是一个分布式系统最简单的架构。看着很简单的俩框一线,但架构师却需要考虑的非常多,这也是架构师和普通程序员区别
A 服务的架构师需要考虑
- 如果服务 B 不可用,服务 A 如何保证高可用。比如宕机,故障,虚机漂移,网络故障
- 如果服务 B 出现阻塞,性能下降,服务 A 如何保性能不受影响
- 服务 A 调用服务 B,是否一定需要等待服务 B 的响应,能否解耦 A 和 B 调用,避免前面 2 个问题
- 服务 A 如果是通过其寻址服务到 B,如果寻址服务不可用,如何调用服务 B
- 如果服务 B 更新了 API,服务 A 未同步更新(兼容)
B 服务作为服务提供者,架构师需要考虑的更多
- 如何保证服务 B 支持大并发的操作,包括自身支持大并发,以及依赖的下游支持
- 如何保证服务 B 支持大流量操作,甚至是不正常突发高流量下仍然可用,比如断网恢复后的高流量
- 如果服务 B 的下游不可用,如何给服务 A 提供可用接口
- 服务 B 的更新重启,会对服务 A 产生什么样的影响
- 如何可观测服务 B 的调用
- 如果服务 B 是公网服务,如何保证安全,数据被授权用户获取
- 如果服务 B 更新了 API,服务 A 未及时更新(兼容考虑)
B 服务架构师还需要考虑意外情况,如服务 A 的 BUG
- 服务 A 应该只调用服务 B 一次,但实际服务 A 调用了多次
- 服务 A 调用频率应该是 1 分钟一次,但实际 1 秒一次
- 服务 A 应该先后顺序调用 B 俩次不同接口,但调用顺序相反。这种可能是 A 的 Bug,或者是事件驱动架构里顺序问题。
对这种 “俩框一线” 如此简单的架构,可以看到架构师相比于初级程序员,需要考虑较多,需要考虑 10 + 种情况。这种考虑其实也不是架构师挠秃头发想到的,而是基于技术架构的架构质量考虑的,有种说法,说是架构质量驱动了架构设计(ADD)。
分布式 API 的架构
分布式 API 建立在俩类协议基础上:
- TCP 协议,使用这类协议的分布式 API 有 Dubbo,RMI(EJB),gRPC,Thrift 等,这列分布式 API 具有良好的性能,延迟都在 1 毫秒级,适用于系统内部微服务之间调用。其缺点是不易被观察。
- 基于 HTTP 协议,比如 REST,SOAP 建立在 HTTP 协议基础上,HTTP 协议高可用和高性能,内置安全,并且支持跨内外网调用。而且 HTTP 上的调用易于被观测,如,通过 HTTP 代理观测,或者 HTTP 日志进行业务统计。因此基于 HTTP 的 REST 等 API 技术框架逐渐成为现在流行的分布式调用方式,适用于系统之间,用户和系统之间的调用。 基于 REST 调用延迟通常相对 TCP 方式的协议较大,即使内网之间调用,延迟在 10 + 毫秒。对于需要毫秒级延迟的场景,REST 协议不合适
分布式 API 通常有如下协议栈
- Client API ,以 Java 接口形式体现
- Client Stub,扮演了客户端的 Gateway,通常会将调用的目标类,参数等封装成指定协议的报文,调用底层 transport。Client Stub 有负责寻址,调用分布式服务端的节点,并等待返回结果返回调用者
- Client Transport: 负责传输部分,客户端报文到服务器端
- Server Transport :负责接收客户端报文
- Server Skeleton:负责把报文反序列化成调用参数,并调用实际的实现类 Service Impl
- Service Impl:接口实现类
分布式 API 设计通用考虑
下面列出了系统常使用的分布式 API 技术的公共特性
API 技术 | 基础协议 | 报文格式 | 语言支持 | 性能 | 内网 / 外网 | 可观测 | 通信方式 | API Schema | 负载均衡 |
---|---|---|---|---|---|---|---|---|---|
REST | HTTP | JSON | 通用 | 一般 | 内网 / 外网 | 容易 | 同步 | 支持 | Nginx 等 HTTP 代理 |
Spring Feigh | HTTP | JSON | 通用 | 一般 | 内网 / 外网 | 容易 | 同步 | 支持 | 客户端 |
RMI | TCP/HTTP | RMI | Java | 一般 | 内网 / 外网 | 二次开发 | 同步 | 支持 | 客户端 |
gRPC | TCP/ HTTP | Protobuf | 常用语言支持 | 好 | 内网 / 外网 | 二次开发 | 同步,异步,服务端推送 | 支持 | 客户端或者代理 |
Dubbo | TCP/HTTP | 多种 | 常用语言支持 | 好 | 内网 / 外网 | 二次开发 | 同步,异步 | 无 | 客户端或者代理 |
SOAP | HTTP | XML | 通用 | 一般 | 内网 / 外网 | 容易 | 同步,异步 | 支持 | Nginx 等 HTTP 代理 |
WebSocket | HTTP | 文本或者二进制,XML/JSON | 通用 | 好 | 内网 / 外网 | 容易 | 同步,异步 | 无 | Nginx 等 HTTP 代理 |
- 基础协议: 通常区分 TCP 或者 HTTP,构建在 HTTP 基础上的分布式 API,适用于系统之间或者内外网之间调用,直接构建在 TCP 上的协议通常性能更好,适用于内网之间调用,构建在 TCP 协议上的分布式 API 通常有更好的性能。目前基于 TCP 的分布式 API 都开始支持基于 HTTP,以方便互联网调用而不是企业内网的调用。如 RMI,Dubbo,gRPC
- 报文格式:通常文本格式更方便阅读,二进制格式性能更好。JSON 相比与 XML 文本格式,性能也更好。报文格式以影响兼容性,这点非常重要,XML 和 JSON 方式天然能实现向前,向后兼容,二进制则依赖序列化协议是否支持兼容。比如 Hession,gPRC 支持兼容,fury 依赖配置以是否支持兼容。兼容性是选择序列化协议的一个考虑点
- 编程语言支持: 由于 HTTP 协议是一种非常成熟协议,因此所有语言都支持。gRpc 和 Dubbo 等 API 框架,流行语言都支持,RMI 则只有 Java 支持。广泛的编程语言支持,能保证异构系统之间的互相调用。
- 性能:当考虑到性能的时候,推荐基于 TCP 实现的分布式 API 框架。随着 HTTP/2 和 HTTP/3 的推出,HTTP 协议也有较高的性能
- 内网 / 外网:如果分布式 API 暴漏给外网,则需要考虑到能穿透防火墙,通常 HTTP 协议比较适合,这也是 REST,SOAP 比较适合对外提供 API 或者系统 (云)之间的调用
- 可观测,基于 HTTP 协议的分布式 API 有较好的可观测,通过网关,或者 HTTP 日志。基于 TCP 的分布式 API 则需要框架支持或者二次开发支持。
- 通信方式,包括同步和异步调用,以及服务端推送。比如 gPRC 同时支持这三种方式
- API Schema: 通常分布式 API 都会提供 Schema 来说明 API 的调用地址 endpoint,入参,出参,以及参数校验规则。这些 Schema 除了能当做 API 文档外,还能生成客户端或者服务端代码,或者生成客户端或者服务测试代码和数据。Open API 提供了 REST 调用的 Schema。WSDL 提供了 SOPA 调用的 Schema,gRPC 则使用 protobuf
- 网关支持: API 网关可以提高架构的高可用和可观测,大部分布式 API 都支持网关,以 HTTP 协议为基础的 API 天然支持通过 Apache,Nginx,HAProxy 等作为网关,或者 Spring Cloud Gateway 。 对于 Dubbo,则可以使用 Apache Shengyu 网关, 或者二次开发 Spring Cloud Gateway,将 REST 请求转化为 Dubbo 请求。
分布式 API 设计其他考虑
在选择 API 框架,以及使用 API 过程中,还需要如下考虑
特性 | 说明 |
---|---|
安全调传输协议 | 是否支持安全调传输协议,系统之间,或者是内外网之间可以采用 SSL 传输协议。基于 HTTP 的 API 框架天然支持 SSL |
安全支持 | 安全支持,API 框架是否允许携带额外的安全信息,并进行认证和鉴权。比如 REST 通常使用 JWT 协议,在 HTTP Header 中携带加密的用户,角色等信息,REST 服务端会解密这些信息并认证或者授权用户是否能调用此 API |
降级说明 | API 可能不会按照预期调用,比如因为限流,或者下游不可用,应该提供降级的指示,并要求客户端稍后按照规定的方式重试,最常见的是退避指数间隔重试。有的分布式 API 框架内置限流降级功能,有的则需要服务提供者实现,或者按照业务优先级区分限流降级 |
性能说明 | API 应该通过文档申明哪些参数会影响性能 |
幂等说明 | API 一般要求幂等,既同样参数,多次调用的结果应该是一样的。比如订单 API,用户多次点击,应该只生成一个订单,而不是多次订单。 如果提供的 API 不能实现幂等,需要在 API 文档中说明多次调用的结果 |
是否支持泛化调用 | 分布式 API 框架通常通过 Schema 生成客户端 SDK(client stub),客户端依赖 SDK 调用微服务,但有些情况,强依赖 SDK 并不是个好主意,比如测试平台,或者 API 网关。 这些系统不会使用客户端 SDK。那么就要求 API 分布式框架支持泛化调用,即客户端可以提供类名和方法名,以及 JSON 作为参数。dubbo 支持泛化调用,而 gRpc,Thrift 不直接支持 |
批量调用 API | API 是否还有批量调用 API,比如,查询单个设备是否在线和批量查询多个设备是否在,查询单个商品库存,和批量查询商品库存。批量 API 有助于提升性能 |
传输数据压缩 | API 框架是否支持传输数据压缩,压缩有助于提高性能。比如 HTTP 协议天然支持报文压缩,有些分布框架采用的序列化协议也支持压缩功能 |
API 超时设定 | API 框架默认的超时时间都比较保守,gRCP,Thrift 默认超时时间不设置,dubbo 为例子则默认为 1S。建议为每一个 API 设定超时时间 |
重试策略 | 大部分 API 框架的重试间隔时间都采用退避指数,以避免频繁重拾导致服务器压力过大,甚至无法恢复到正常状态,linux 的 TCP 重传间隔 1S,2S,4S,8S,16S。 如果你作为客户端,调用分布式 API,失败情况下想重试,应该考虑重试策略 |
我参与物联云系统遇到过 3 个跟重试相关问题
1 jedis 早期版本的发送数据到 redis,会不间隔重试。导致迅速失败
2 弱网环境,操作系统使用退避指数间隔重试,导致弱网通信耗时较长
3 主网关和接入网关在重启后,访问量较大,导致所有访问都失败。怀疑设备是不断重试
有了如上知识,可以分析 SSE,GraphQL 和 MQTT,Thrift
API 技术 | 基础协议 | 报文格式 | 语言支持 | 性能 | 内网 / 外网 | 可观测 | 通信方式 | API Schema | 负载均衡 |
---|---|---|---|---|---|---|---|---|---|
SSE | HTTP | 同 WebSocket | 通用 | 好 | 内网 / 外网 | 难 | 服务器推送 | 无 | Nginx 等 HTTP 代理 |
GraphQL | HTTP | JSON | 通用 | 一般 | 内网 / 外网 | 容易 | 同步 | 支持 | Nginx 等 HTTP 代理 |
MQTT | TCP | MQTT | 通用 | 好 | 外网 | 难 | 异步 | 无 | 否 |
Thrift | TCP | 二进制 | 通用 | 好 | 内网 | 难 | 同步,异步 | 支持 | Nginx,HAProxy, 客户端自研 |
参考
1 架构修炼之道 —— 亿级网关、平台开放、分布式、微服务、容错等核心技术修炼实践
2 各个框架的官网功能特性
3 可观测性工程