API 设计

  • 可发现性
  • 遵守KISS原则
  • 向后兼容性
  • 避免抽象泄漏: 尽可能减少公开的细节
  • 一致性
  • 由用例驱动的API设计.

浏览器原生支持的数据类型.

优点:

  • 可以以人类可读的形式格式化.
    强烈建议开启gzip以减少传输数据的大小, 可以将格式化用的空格压缩到可以忽略不计.

在传输数据大小方面压倒性地胜过JSON, 即使在经过gzip压缩后也大幅领先于JSON.

X-Rate-Limit-Limit 当前期间(period)允许的请求数
X-Rate-Limit-Remaining 当前期间(period)的剩余请求数
X-Rate-Limit-Reset 当前期间(period)剩余的秒数

RPC的缺点在于很容易设计出不良的API, 例如为特种任务设计专门的API.
这些不良的API会随着软件发展逐渐成为一种具有维护成本的东西, 并最终成为API开发的绊脚石.

在内部使用 HTTP/2 协议, 但无法直接通过 HTTP 进行访问.
如果需要在 Web 上使用, 则需要在服务器上准备一个用于代理转发请求的 agent.

优点:

  • 基于Proto的类型定义.
  • 多语言支持.
  • 高性能.

缺点:

  • 主要被设计用于服务与服务之间的通信, 而不是客户端到服务.
    (尽管支持客户端到服务的场景在技术上是可以实现的)

重用了 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总是会返回很多数据.