基于 GraphQL/Node.js 的高性能 Web 框架研究

背景

在产品业务线逐渐成熟稳定后,降低开发管理成本成为首要目标,前端开发团队(iOS/Android/H5/React Native/Flutter/小程序等)因此开始趋向融合,俗称的“大前端”,人力资源利用率和生产效率也因此得以大幅提升,后端基础架构以及业务团队也能够统一前端视角,不需要针对多端开发多个功能相同的接口,但基于微服务的系统设计方式,产生了各类终端直接与微服务交互的模式

这种模式对特定的某一类终端可能并不太友好,尤其是多业务聚合的前端页面,前端需要维护和多个后端业务组件的关系,各业务请求也相互独立,增加了联调、维护成本以及页面初始化时间;另外针对非多端统一业务,后台业务组件开发人员也需要面向多端进行适配,这种耦合关系不仅也增加了联调成本,也导致了开发者不能聚焦到核心业务功能的开发

因此诞生了 Backend For Frontend (BFF) 的概念,即专为特定终端服务的后端组件,这种组件解耦了大量前端团队和后端团队之间的关系,使其各司其职,前端专注于提升性能、优化用户体验,后端专注于核心业务,减少了沟通、维护成本,从整体上提升了效能

本文的目标是调研基于 Node.js 的 Web 框架、技术栈,开发、整合相关工具或库,作为 BFF 组件的高性能脚手架或应用级框架

概览

基于 GraphQL/Node.js 的高性能 Web 框架研究_第1张图片

技术栈选型

选型方向 主要职责 优势 劣势 适用范围 代表框架
Web 级 (HTTP/HTTPS)服务启动,请求、响应的解析和包装,路由注册、分发,框架和请求生命周期管理 初始学习成本较低,渐进式架构,高度契合业务需求 对业务理解、技术细节能力要求较高,项目管理难度较大,最佳实践需要持续演进 小型团队、持续迭代、需求不确定或变更频繁、创新型/探索性产品 express/koa/fastify/hapi 等
应用级/企业级 应用框架设计和组件管理,应用生命周期管理,API 风格、编程范式选型,插件模型,相关开发、测试和运维工具等,这种类别的框架即是最轻量的无任何具体业务代码的应用 架构成熟,快速成型,管理成本低 初始学习成本较高,不易调试,约束多、功能高度依赖插件生态 大型团队、需求明确/产品定位清晰、项目周期短、微服务组件通用脚手架 egg/sails 等
渐进式 介于两者之间 介于两者之间 介于两者之间 介于两者之间 next.js 等

本次预研的项目模型:前端性能监控数据分析服务

该项目从内部前端监控系统中孵化,现有系统存在系统组件功能边界不清晰、前端数据处理逻辑过重导致页面难以维护和优化体验、用户登陆/权限管理/系统配置等运营管理功能组件缺失导致这些功能难以分配(如果有需求,一般临时分配给其他后端组件去实现,可以预见的是,临时方案最后差不多都变成了永久方案)

确定了上述 3 种选型方向,结合项目实际需求、可用资源、环境和以上基本目标,定义了如下技术栈

语言 Web 框架 API 模式 缓存 数据存储/分析 部署 监控
Node.js/Typescript fastify.js GraphQL MVC/依赖注入 MemoryCache ClickHouse/Elasticsearch Docker pm2

Node.js

Typescript

fastify.js

GraphQL

优势
  • 可单业务多次查询也可多业务聚合查询,具有较高的灵活性,多业务聚合查询时,由于请求数变少了,不仅降低了流量,增加了有效载荷比,而且相比从外网同时查询多个微服务,从内网查询网络速度要快很多
  • 面向结果查询,查询语句即查询结果,相比 REST 面向过程和逻辑的命令式查询,除了关注查询语句,不需要再关注和维护数不清的接口地址以及每个接口差异化的请求参数和响应数据格式
  • 对于所有微服务具有统一的查询语句、响应数据和异常处理规范,客户端不需要在网络接口层拦截请求和响应,抹平 API 的差异化
  • 对于多终端具有统一规范的客户端实现框架,不需要关注底层实现细节,拿来即用
  • 查询校验,查询语句和响应结果会基于 Schema 进行自动校验,降低了联调成本
  • 文档维护,可直接基于相同的语法查询 Schema 定义,不需要额外维护一份接口文档
劣势
  • Schema 维护问题
    GraphQL 在 API 层多引入了一层 Schema 定义,需要与业务模型(类/接口等),数据库 Schema 的相关定义保持同步,违背了 Single Source Of Truth 原则

  • Schema 设计要求比 REST 更高
    REST 的接口设计相互独立,隔离性较强,没有强约束,更易实现和版本化,而 GraphQL 必须先设计好 Schema,整个系统或组件的 Schema Type 都在同一空间内,无法定义同名 Type,如何合理地管理和组织各种 Type 以及它们之间的关系对设计者提出了更高的要求

    例如,如果没有充分理解 GraphQL 的理念和设计模式,为了避免设计 Type 和它们之间复杂的关系,可能会导致这样糟糕的设计,只是在 GraphQL 的基础上包装了另一层 REST

    type Query {
      request(query: String!): Response
    }
    
    type Response {
      body: String
    }
    

    另外,在版本化上,两者差异也较大,针对无感知版本(已发布的接口上有了变更但用户无感知,例如:新增了某些字段,优化了某段逻辑的性能等),两者可能没什么差别,但对于增量版本来说,REST 只需要开发新的接口,对旧接口不做任何变更,就不会产生其他影响,而 GraphQL 要么提供新的接口和重新设计的 Schema,要么在原来的接口上定义新的 Type 和 Resolver 以及组织新的关系(可能会产生不兼容变更),无论哪种方式都不会比 REST 变更更少更稳妥

  • REST 替代方案
    存在基于 RESTful API 的替代方案

    • JSON API
    • JSON Schema
    • OData
  • 查询性能的不可预测性
    查询字段和深度可自由指定的特性提供了高度的灵活性,但也导致了无法针对特定查询语句优化和查询性能的不可预测性,易产生低效查询,降低服务器性能甚至使服务器宕机

    比如经典的 N + 1 查询问题,对查询深度、单字段查询性能和单次查询性能的统计分析和限制也需要深入考虑

  • 无法利用 HTTP 缓存
    GraphQL 是构建在 HTTP 协议之上的另一层协议规范,一般使用 POST 方法和请求体携带查询语句,HTTP 语义以及现有大部分基础网络设施(代理/Web 缓存/CDN)都不支持缓存 POST 请求(POST 是非幂等请求,即多次在任意时刻执行相同的请求可能会得到不同的结果),如果使用 GET 方法,查询语句只能拼接到 URL 查询字符串中,大部分浏览器对 URL 的长度都有限制,因此为了保证任意长度的查询语句都能得到执行,只能牺牲 HTTP 缓存的性能优势

  • 错误处理
    错误一般可划分为以下几种类别:

    • 网络错误
    • HTTP 错误
    • 业务错误

    在业务错误处理上,相对于 REST API 更加灵活的基于状态码的统一模型,GraphQL 基于 Schema 对查询语句与响应结果执行校验,如果 Schema 校验没通过,则返回特定的异常描述信息,比如哪些字段没通过校验,预期类型是什么,实际类型是什么;如果 resolver 在执行过程中抛出异常,那么也将触发异常返回流程,返回的异常信息可能取决于具体的实现,有的直接返回详尽的异常信息,包括调用堆栈,这在开发和测试环境没什么问题,但在生产环境,出于安全性考虑,我们一般需要进行脱敏处理,隐藏具体的异常信息,将其转换成符号化的状态码和相应的状态码描述,这在 GraphQL 中可能需要实现提供相关的异常信息过滤转换机制

  • 文件上传
    官方规范中没有涉及文件上传,需要社区或应用开发者自己支持

  • 鉴权
    GraphQL 的鉴权逻辑编写没有 REST 灵活,REST 可以基于功能或操作权限设计接口粒度(虽然可能会产生很多接口),将这部分鉴权逻辑前置于网关层统一处理,接口或业务层只需处理数据或资源权限,而对于 GraphQL,所有鉴权逻辑只能作为业务逻辑的一部分写在 resolver 或业务层中

  • 解决方案的适用性
    GraphQL 是一种协议规范,某些痛点解决方案不一定在所有平台上都有对应的实现,比较依赖社区的活跃性和支持度

  • 学习成本
    不管是基于 GraphQL 规范和参考实现构建自己的解决方案,还是直接采用开源社区的集成方案,比如 apollo client/server,学习成本相对 REST 来说要高很多,增加的复杂度也会使应用调试更加困难

部分解决方案
  • 针对 Schema 维护问题,遵循 Single Source Of Truth 设计原则,动态生成 Schema,比如 TypeGraphQL
  • 针对 REST 替代方案,GraphQL 是一个相对比较完整的 API 规范,REST 具体的某一种替代方案可能只是 GraphQL 的一种 Poorly Implementation
  • 针对查询性能问题,Facebook 提供了 DataLoader Batch/Cache 方案以合并和缓存查询请求,也可以根据具体场景使用 PersistGraphQL 方案
  • 针对无法利用 HTTP 缓存问题,也可利用 PersistGraphQL 解决(使用 GET 请求指定预定义查询语句,避免动态查询导致 URL 过长的问题)
  • 针对文件上传,graphql-multipart-request-spec 定义了文件上传的实现规范(社区规范)
  • 针对解决方案的适用性,目前只能依靠社区活跃度

架构模式

缓存

数据存储/分析服务

DevOps

监控

参考链接

  • Event loop
  • 微服务架构:BFF 和网关是如何演化出来的?
  • Pattern: Backends For Frontends
  • BFF @ SoundCloud
  • How to build blazing fast REST APIs with Node.js, MongoDB, Fastify and Swagger
  • How to build a blazing fast GraphQL API with Node.js, MongoDB and Fastify
  • GraphQL Concepts Visualized
  • GraphQL Quick Start
  • Five Common Problems in GraphQL Apps (And How to Fix Them)
  • 5 reasons you shouldn’t be using GraphQL
  • Pain Points of GraphQL
  • What are the cons of GraphQL?
  • Comparing API Architectural Styles: SOAP vs REST vs GraphQL vs RPC
  • GraphQL in a Micro Services Architecture
  • GraphQL Best Practices
  • GraphQL Schema Design: Building Evolvable Schemas
  • How Airbnb is Moving 10x Faster at Scale with GraphQL and Apollo
  • How Netflix Scales its API with GraphQL Federation

你可能感兴趣的:(BFF)