免责声明:尽管标题有争议,但本文并不是试图证明RPC比REST更好,或者GraphQL比RPC更好。相反,本文的目的是向你介绍这些方法的大致情况以及它们的优缺点。最终的选择将会是一个权衡。
尽管 HTTP 是一个应用层(例如,L7)协议,但在 API 开发方面,HTTP 实际上扮演着一个较低层次的传输机制的角色。
在 HTTP 上如何实现 API 有许多方法,它们在概念上有所不同:
...但是一个普通的开发人员需要知道的实际清单并不局限于这三个家伙。在这个领域中还有 JSON、gRPC、protobuf 等其它术语。让我们试着一次性了解所有这些术语吧!
首先,REST只是一种软件架构风格。它是一组设计约束,而不是具体的协议。REST 依赖于资源的概念。例如,一个 REST API 是由一组资源(名词)和与这些资源交互的有限数量的动作(动词,查询 fetch、创建 create、更新 update、删除 delete 等)组成。它在思想上非常接近最初的 HTTP 设计,主要基于资源(URLs)和方法(GET、POST、PUT、DELETE)。因此,从实现的角度来看,REST 模型到 HTTP 协议的映射相对简单:
# Create new bookPOST http://myapi.com/books/ (author="R. Feynman", year=1975) -> book_id
# Get book with ID = 1GET http://myapi.com/books/1 () -> (id=1, author="R. Feynman", year=1975)
# Update book with ID = 1PUT http://myapi.com/books/1 (id=1, author="Richard Feynman", year=1975) -> (...)
# Delete book with ID = 1DELETE http://myapi.com/books/1 () -> nu
可能每一个现代 web 框架都提供了构建一个 REST 风格的 Web 服务所需的所有现成工具。从客户端的角度来看,调用一个 REST API 非常简单——它只需要将指定的 HTTP 方法发送到一组预定义的 URLs。
然而,从 API 设计者的角度来看,如何用资源术语(即名词)表示现实世界的领域并保持有限数量的动作(即动词)是并不明显的。令人惊讶的是,尽管 REST 一次在当今被广泛使用,但被认为是真正的 REST 风格的现代 API 并不多。在设计 REST 风格的 Web 服务时坚持纯粹,或导致 API 设计非常笨重。
同样,RPC也是一种 API 设计技术。RPC 聚焦于动作(动词)概念,这通常使得涉及到的资源(名词)非常原始和特别。对于业务模型中的每个过程或事务,API 设计者只需要添加一个 RPC 端点:
# Create new bookcreateBook(author, year) -> book_id
# Get book by IDgetBook(book_id) -> book
# Change book's authorsetBookAuthor(book_id, author) -> null
# Delete book by IDdeleteBook(book_id) -> nu
在幕后,应该有另外一层将这样的过程调用映射到 HTTP 请求。例如,setBookAuthor(1, "Richard Feynman")会被这样映射:
<span role="presentation" style="box-sizing: border-box; padding-right: 0.1px;">POST http://myapi.com/set-book-author/ (book_id=1, author="Richard Feynman") -> null</span role="presentation" style="box-sizing: border-box; padding-right: 0.1px;">
现在,让我们比较一下 REST 和 RPC 中改变作者姓名的任务。虽然在 RPC 中的实现看起来比较简单,但在 REST 风格的实现中有许多有争议性的问题需要回答。如果作者的名字是 book 的一个属性,那么我们是否应该在发送PUT /books/1 时,提供一个包含修改过的作者字段的完整的 book 对象?如果客户端没有完全的 book 对象怎么办?我们应该先获取 book 对象,还是只传送 book ID 和新的作者名字?但是服务端的其它属性怎么办?如果一个几乎空白的PUT 请求到达,它们是否应该归零(这将是灾难性的,但是与 REST 原则的决定一致)?或者我们应该放弃 HTTPPUT 方法,开始使用PATCH 方法(你听说过这个方法吗)?
幸运的是,如果我们遵循 RPC 方案,我们可以简单忽略上面这些问题。但是,当然也有一个缺点——一个典型的 RPC API 通常包含大量的自定义过程。显然,这使得 RPC 模型到 HTTP 层的映射是一个不平凡的任务。好在 API 设计者很少需要考虑这一部分。有很多库在 HTTP 上实现了 RPC 层,其中一些库非常杰出(是的,我说的是谷歌的 gRPC 和脸书的 Thrift)。但是 RPC 方案还有一个更复杂的问题...
一打丰富的 REST 资源结合 3-5 个 HTTP 方法通常可以覆盖一百个用例。REST API 故意为领域模型引入了一个常规结构,使其增长和演化更加可控。相反,RPC APIs 是自然增长的。引入一个新的用例通常需要在已经膨胀的列表中再增加几个 API 端点。由于 API 设计没有强制面向实体的结构,因此在相对短的时间内,RPC 调用的数量可能会超过一个团队可以处理的最大复杂度。
现在,,我们已经知道 RPC 和 REST 方案都不理想。REST 存在过度和欠缺查询问题,可能会导致在设计阶段的比较耗神。但是如果我们尝试用一百个特别的 RPC 端点来取代面向实体的设计,随着时间的推移,维护过度增长的 RPC API 会是一个噩梦。
GraphQL试图解决这两种技术的弱点。假设面向实体的领域模型有助于开发人员长期安心,GraphQL 方案从定义模板开始,例如资源集(即名词)和它们的关联关系。听起来很像一张图,不是吗?有了正式的模板定义,GraphQL 在其上构建了一个相当复杂的服务器和客户端。厚客户端允许查询(QL 代表 query language,即查询语言)自定义的和组合的资源。厚服务器知道如何根据客户端的查询和领域模板来填充响应。
因此,GraphQL API 基本上由一个端点组成。即,没有臃肿的 API 了。同时,它超级灵活和可以定制的查询有助于避免从服务器查询不必要的数据。此外,开发人员仍然可以从正式的面向实体的领域模板中受益。但是凡事总是有代价的。这里的代价是 GraphQL 客户端和服务器端的极度复杂性。所以,是的,一切都关乎取舍...
活动部分的适当分类能够有助于你将注意力集中在 API 开发领域。现在,我们已经知道 REST 和 RPC 仅仅只是架构风格。而 GraphQL,我倾向于认为它也是一种风格,即使从技术上来讲它是一种由运行时支持的正式语言。
但是,如果 GraphQL 是由一个特定实现支持的,那么我们可能期望 RPC 和 REST 也有相同的实现支持。事实上,确实有很多著名的 RPC 框架——gRPC、Apache Thrift和Apache Avro等等。令人惊讶的是,REST 框架中似乎没有明显的领导者。可能是因为 REST 在技术上非常接近 HTTP,而且几乎每个 Web 框架都已经很好地支持了它。
嗯,但是protobuf怎么样?显然,它只是数据在通过网络被发送或存储到某个地方之前对其进行序列化的方法之一。gRPC 是一个特殊的 RPC 框架,完全依赖 protobuf。而且由于这两个技术通常是一起使用的,而且被一起发布,因此很多人都将 gRPC 和 protobuf 交互使用。然而,在你的REST风格的API中使用protobuf也非常不错,protobuf 可以用作一种编码格式,我们目前在工作中对我们的一些服务就是这么做的。因此,protobuf 不应该与 REST 或 RPC 框架一起比较,而是可以与 XML 或 JSON 一起比较。
更让你困惑的是,Apache Thrift 有它自己的序列化格式,称作 Thrift!即,gRPC 使用 protobuf 编码格式,而 Thrift 使用 Thrift 编码格式
总之,设计一个 HTTP API 有许多不同的架构风格。其中最流行的三种是 REST、RPC 和 GraphQL。每一种风格都可以用很多种方法实现,而且有很多著名的框架,例如 gRPC 和 Apache Thrift。在底层,它们依赖更低级别的机制,如数据序列化(protobuf、JSON、XML)或协议(JSON-RPC、 XML-RPC)。而且较低级别的代码有时会在不同的风格之间复用,这使得整个 API 开发领域乍一看都非常复杂。
作者介绍
Ivan Velichko 是一名涉及很多领域且具有 10 年实践经验的软件工程师;热衷于可靠性和分布式系统。软件开发不仅是专业还是爱好。最有意义的活动是增加对复杂系统的理解。
原文链接
API Developers Never REST