API 设计

  • 可发现性
  • 遵守KISS原则
  • 向后兼容性
  • 避免抽象泄漏: 尽可能减少公开的细节
  • 一致性
  • 由用例驱动的API设计.
浏览器原生支持的数据类型.
优点:
  • 可以以人类可读的形式格式化.
    强烈建议开启gzip以减少传输数据的大小, 可以将格式化用的空格压缩到可以忽略不计.
在传输数据大小方面压倒性地胜过JSON, 即使在经过gzip压缩后也大幅领先于JSON.
X-Rate-Limit-Limit 当前期间(period)允许的请求数
X-Rate-Limit-Remaining 当前期间(period)的剩余请求数
X-Rate-Limit-Reset 当前期间(period)剩余的秒数
在内部使用 HTTP/2 协议, 但无法直接通过 HTTP 进行访问.
如果需要在 Web 上使用, 则需要在服务器上准备一个用于代理转发请求的 agent.
优点:
  • 基于Proto的类型定义.
  • 多语言支持.
  • 高性能.
缺点:
  • 主要被设计用于服务与服务之间的通信, 而不是客户端到服务.
    gRPC对于HTTP的支持是通过一个转发网关来实现的.
重用了 HTTP 协议.
优点:
  • 具有类型定义.
  • 天生的多语言支持.
  • 尽管是 RPC, 却具有 HTTP URL 映射, 因此可以使用海量 HTTP 客户端.
  • 定义文件本身即是文档.
使用 HTTP 协议, 一项服务只需一个 URL 作为接口.
实现非常简单, 基本上等于发送 JSON RPC 作为 Payload 的普通 POST 请求.
优点:
  • 实现简单
  • 天生的多语言支持.
缺点:
  • 没有类型定义, 但是可以通过共用 TypeScript 接口的方式间接实现类型定义
基于 HTTP 协议, 只有一个 /graphql 作为接口.
  • 具有类型定义
  • 天生的多语言支持.
  • 独有的图联接查询机制, 由客户端决定查询结果的深度与结构, 因此GraphQL可以达到最高的请求利用率,
    这对于字段特别多的API特别有用, 能大大减小传输的数据量.
  • 服务端只需要设计通用的API, 无需专门设计复杂的请求, 客户端可以自行组合出新的请求.
  • 服务器负担更重, 为了返回大请求需要消耗更多的内存来存储中间数据.
  • 基于图联接的查询机制导致难以控制数据库查询所需的成本.
  • 图形式的字段返回方式对于很多项目来说可能是不必要的,
    对静态类型语言来说会提升结构体适配的难度.
  • 无法使用HTTP标准缓存, 因为GraphQL使用POST.
查询请求的动态程度较高, 查询方式符合图数据结构的情况:
要么一次获取多项数据, 要么一次获取很深的数据, 要么两者皆有.
例如社交网络会遇上需要查询"这些用户评论的评论的评论以及他们的个人信息"的情况, GraphQL就非常合适.
除了作为RPC接口以外, GraphQL还可以作为API网关使用, 使用GraphQL的目的是减少入站请求数量.
GraphQL常常被形容成与REST API竞争的一种协议, 这种描述本身是过度简化的.
GraphQL与REST API的竞争仅仅在查询具有很强的动态性时成立.
否则只需设计专门的REST API就可以满足需求.
当查询的内容在广度和深度上总是固定时, 使用GraphQL很可能是一个错误的决定:
  • 由于引入了GraphQL, 项目增加了多余的抽象和包装, 以及不可控的服务器查询执行成本.
  • 对于客户端开发来说, 可能会因为查询的返回值的类型定义过于复杂而变得难以维护.
GraphQL的参考实现, 源于graphql模块.
该模式由JavaScript对象直接定义GraphQL Schema, 模式的可读性比较差.
响应查询时执行广度优先的逐级解析, 先解析外层, 再解析里层.
每层解析都会调用相应字段的resolve方法, 以返回对应的数据.
该模式分别定义GraphQL Schema(字符串, 将被解析为AST)和resolvers(JavaScript对象),
使Schema与解析可以分开编写, 使代码变得清晰.
此模式是Facebook为解决深度嵌套查询(也称为N+1问题)的性能问题而发明的.
DataLoader的目标是通过制造了微缓冲区(存活时间非常短的缓冲区)将分散的子查询聚合为一个批处理查询.
聚合发生的时间点是由batchScheduleFn这一调度函数决定的,
直到聚合查询返回前, 请求会通过Promise阻塞.
默认的调度函数是enqueuePostPromiseJob, 该函数的功能基本上等价于nextTick或setImmediate.
GraphQL的N+1问题在于它图结构中的每一处查询嵌套的子查询都是独立的,
后端将为每个嵌套执行单独的查询, 这必定会严重损害数据库性能.
DataLoader具有缓存机制(确切地说是memoize), 因此调用 DataLoader#load 方法会得到相同结果.
缓存只能被手动清除, 与一般memoize实现不同, DataLoader可以针对特定参数清除缓存, 而无需清除全部缓存.
通用API在设计时未考虑通信成本, 它们的接口与传统的库和模块的接口是类似的.
例如:
  • 查询对象时, 不直接返回对象, 而是返回对象的标识符.
注: GraphQL并不是通用API.
除非相关API不会导致N+1问题(例如相关操作不在乎原子性, 并且只会导致最多2个请求: 查询ID, 获取数据),
否则请尽可能不设计通用API.
从工程角度来看, 只返回ID的API可以与其他通用API相互配合以重用, 可以节省下很多的后台开发成本.
通用API更容易设计缓存方案.
对前端来说, 通用API会面临与API请求有关的N+1问题(瀑布请求), 完成操作所需要经过的时间会显著增加.
对客户端软件而言很可能是不可接受的.
由于操作需要分解为多个步骤, 对于后端来说, 相关操作不可能成为原子操作, 因此可能出现同步问题.
在获取id到获取id对应的数据之间的空隙, 后端数据可能已经发生根本性变化, 这会带来隐患和麻烦.
由于请求数量增多, 每个请求都可能失败, 进而增加使用者编写正确代码的难度.
HTTP/2的多路复用不能解决原子性问题, 也不能保证多次请求不会发生失败.
对后端来说, 通用API会面临与SQL查询有关的N+1问题, 即数据库为了填充查询结果, 不得不进行多次SQL查询.
专用API可以实现特定的执行优化, 这使得专门API一定会比通用API有更好的性能.
服务器资源通常很昂贵, 应该尽可能避免后端的N+1问题.
在后端N+1问题上, Facebook的DataLoader算得上是一种可以接受的解决方案.
定制一个支持批量获取资源的API, 可以缓解N+1问题.
需要注意的是, 这只能应付最初级的N+1问题, 并且存在着许多陷阱.
不改变API的行为是分步的本质(不连续, 不上锁), 就没有原子性.
批量资源获取API很容易被误解为通用批量API, 以下是它与通用批量API的区别:
  • 通用批量API被设计成允许执行任何资源请求.
    这会引入一些潜在的设计问题, 例如制订异常处理策略会很困难, 对用户来说违反直觉.
  • 批量资源获取API仅用于特定类型的资源.
如果该API是公开的, 则有必要限制批量资源查询的数量.
当使用批量资源获取API时, 如果对应id的资源不存在或没有权限读取, 则需要有一种输出错误的方法.
在一些情况下, 的确可以将对应的值设置为null以代表其不存在, 但显然也存在更复杂的情况.
专用API是指那些仅为达到某一特定目的, 只为满足一种目的而设计出的API.
有一些非常现实的原因迫使人们设计专用的API.
对于API调用者来说, 专门API更容易使用, 减少了犯错的可能性.
专用API的增多会导致API变得难以维护, 因为单一修改可能引起的连锁反应范围变得更大.
专用API总是需要专门的测试文件, 这些测试文件通常会很庞大和复杂, 因为专用API总是会返回很多数据.