15 | 跨平台架构:如何设计 BFF 架构系统?

[toc]

前言

本文来自拉勾网课程整理

首先请你想一想:如果没有一套灵活的可扩展的系统架构,结果会怎样?

这方面我深有感触,在我们的App 没有良好的系统架构之前,每一个微小的改动都需要“大动干戈”。具体来说,由于强耦合性,每次改动我们都需要和各个业务部门商讨详细的技术方案;功能开发完毕后,又要协调各个部门进行功能回归测试。整个过程下来,不仅耗费太多精力和时间,还容易在跨部门、跨团队沟通之间生出许多事来。

而一套良好的系统架构,不仅仅是一款App的基石,也是整套代码库的规范。有了良好的系统架构,业务功能开发者就能做到有据可依,团队之间的沟通变成十分顺畅;各个功能团队之间也能并行开发,保证彼此快速迭代,提高效率。

因此,我们在推动工程化实践的同时也需要不断优化系统架构。在2017 年,我和公司同事就设计与实现了一套基于原生技术的跨平台系统架构,能让所有开发者同时在iOSAndroid 平台上工作。

如今这套架构经过不断改进,依然在使用。我们现在开发的 Moments App,它所用的跨平台系统架构,正是我吸取了当初的经验与教训,使用 BFFMVVM 重新架构与实现的。

这一章,我们主要先聊聊如何使用 BFFbackend for frontend服务于前端的后端)来设计跨平台的系统架构,以提高可重用性,进而提升开发效率。MVVM 的设计与实现,我会在后面几章详细介绍。

为什么使用 BFF ?

我们的 Moments App 是一款类朋友圈的App,随着功能的不断完善,目前几乎所有 App 的数据源都由多个微服务所支持。在 Moments App 中,后台微服务包括:

  • 用于用户管理与鉴权的用户服务
  • 用于记录朋友关系的朋友关系服务
  • 用于拉黑管理的黑名单服务
  • 用于记录每条朋友圈信息的信息服务
  • 用于头像管理的头像服务
  • 用于点赞管理的点赞服务等等。
ee67d94276c334a68fd7b1d3276f0020

当我们需要呈现朋友圈界面时,App 需要给各个微服务发送请求,然后把返回的信息整理,合并和转换成我们所需要的信息进行呈现。

这些网络请求的顺序和逻辑非常复杂。有些请求需要串行处理,例如只有完成了用户服务的请求以后,才能继续其他请求;而有些请求却可以并行发送,比如在得到信息服务的返回结果以后,可以同时向头像服务和点赞服务发送请求。

接着,在得到了所有结果以后,App 需要整理和合并数据的逻辑也非常复杂,如果请求返回结果的顺序不一致,往往会导致程序出错。于是,为了解决这一系列的问题,我们引入了 BFF 服务。

63a6de87bd0fa5ccf7d8e4c289f0df20

BFF 是一个服务于不同前端的后台服务,所有的前端(比如 iOSAndroidWeb) 都依赖它。而且 BFF 是一个整合服务,它负责把前端的请求统一分发到各个具体的微服务上,然后把返回数据整合在一起统一返回给前端。

可以说,有了 BFF,我们的 App 就不再需要往多个微服务发送请求,也不再需要处理复杂的并发请求,这样就有效减低了复杂度,避免竞态条件等非预期情况发生。除此以外, 使用BFF 还有以下好处。

首先App 仅需依赖一个 BFF 微服务,就能有效地管理 App 对微服务的依赖。众所周知,当 App 版本发布以后,我们没有办法强迫用户更新他们设备上的 App,如果我们需要变动某个微服务的地址,原有的 App 将无法访问新的微服务地址,但是有了 BFF 以后,我们可以通过 BFF 统一路由到新的微服务去。

第二,不同的微服务可能提供不一样的数据传输方式,例如有的提供 RESI API,有的提供 gRPC,而有的提供 GraphQL。在没有 BFF 的情况下,App 端必须实现各个技术栈来访问各个微服务。一旦有了 BFF 以后,App 只需要支持一种传输方式,极大减轻移动端开发和维护成本。

第三,由于 BFF 统一处理所有的数据,iOSAndroid 两端都可以得到由 BFF 清理并转换好的数据,无须在各端重复开发一样的数据处理代码。这极大减少了工作量,让我们可以把重心放在提高用户体验上。

ff36c2ac2ca1140afc4f697ee8a29015

第四BFF 在提升整套系统安全性的同时,提高整体性能。

具体来说,因为我们的 App 是通过公网连接到后台微服务的,所有微服务都需要公开给所有外部系统进行访问。这就会面临隐私信息暴露等安全问题,比如用户会通过 App 获得本来不应该公开的黑名单信息。

但我们引入 BFF 以后,可以为微服务配置安全规则(如 AWS 上的 Security Group)只允许 BFF 能访问,例如上述的黑名单管理服务,就可以设置除了 BFF 以外不允许任何其他外部系统(包括我们的 App)直接访问,从而有效保证了隐私信息与公网的隔离。

与此同时, BFF 还可以同步访问多个不同的数据源,统一管理数据缓存,这无疑能有效提升整套系统的性能。

47931f5e06aa3b4051e756c58227bfd6

BFF 的技术选型——GraphQL

既然 BFF 那么好用,那应该怎样实现一个 BFF 服务呢?我经过多个项目的实践总结发现,GraphQL 是目前实现 BFF 架构的最优方案。
什么是 GraphQL?

具体来说,和 REST APIgRPC 以及 SOAP 相比, GraphQL 架构有以下几大优点。

  • GraphQL 允许客户端按自身的需要通过Query来请求不同数据集,而不像 REST APIgRPC 那样每次都是返回全部数据,这样能有效减轻网络负载。
  • GraphQL能减轻为各客户端开发单独 Endpoint 的工作量。比如当我们开发 App Clip 的时候,App Clip 可以在 Query 中以指定子数据集的方式来使用和主 App 相同的 Query,而无须重新开发新 Endpoint
  • GraphQL 服务能根据客户端的 Query 来按需请求数据源,避免无必要的数据请求,减轻服务端的负载。

下面我们以一个例子来看看GraphQL 是怎样处理不同的 Query 的。

假设我们要开发一个显示某大 V朋友圈的 App Clip,当用户使用 App Clip 时不需要鉴权,不必查看黑名单,就直接可以看到该大 V 的朋友圈信息,那么我们在访问GraphQL 的流程会就简化了(如下图所示)。

6c1f4f54f77c32efe58adb07581a446a

和我们的主App请求相比,App Clip 不需要显示点赞信息,返回的结果就可以精简了。而且由于不需要进行鉴权,也不需要查询朋友关系、黑名单和点赞等信息,BFF 也无须向这些微服务发起请求,从而有效减轻了 BFF 服务的负载。

另外一方面,和 REST API 相比,GraphQL 的数据交换都由 Schema 统一管理,能有效减少由于数据类型和可空类型不匹配所导致的问题。

除此之外,GraphQL 还能减轻版本管理的工作量。因为 GraphQL 能支持返回不同数据集,从而无须像 REST API 那样为每个新功能不断地更新 Endpoint 的版本号。

如何使用 GraphQL 实现 BFF

既然我们确定了 GraphQL,那需要选择一个服务框架来帮我们实现。具体怎么实现呢?为了方便演示,我选择了 Apollo Serve

Apollo Serve 是基于 Node.jsGraphQL 服务器,目前非常流行。使用它,可以很方便地结合 ExpressWeb 服务,而且还可以部署到亚马逊Lambda,微软 Azure FunctionsServerless 服务上。

再加上 Apollo Serve 在我们公司的生产环境上使用多年,一直稳定地支撑着 App 正常运行,因为比较熟悉,所以我就选了它。

下面一起看看具体怎么做。

第一步,使用 GraphQL,我们先要为前后端传递的数据定义 schema。 在这里我写了 Moment 类型的部分 Schema 定义。比如在 Moment 类型里,我定义了 id,type,titleuser details 等属性,其中 user details 属性的类型是 User Details,它定义了 nameavatar 等属性。其的代码示例如下所示。

enum MomentType {
  URL
  PHOTOS
}
type Moment {
  id: ID!
  userDetails: UserDetails!
  type: MomentType!
  title: String # nullable
  photos: [String!]! # non-nullable but can be empty
}
type UserDetails {
  id: ID!
  name: String!
  avatar: String!
  backgroundImage: String!
}

如果你想要查看完整定义,可以点击仓库中查看。

GraphQL 支持枚举类型,比如上面的MomentType就是一个枚举类型,它只有两个值URLPHOTOS,在数据传输过程中,它们是通过字符串传送给前端的。

Moment是一个类型定义,在 Swift 中可以对应成struct,而在 Kotlin 中则对应为data class。这个类型有id、userDetails等属性。这些属性可以是基础数据类型,如String、ID、Int等;也可以是自定义类型,如自定义的UserDetails

当数据类型后面有!时,表示该属性不能为null。这其中需要注意一点,那就是!在数组定义里面的使用。比如photos: [String!]!,表示该数组不能为null,而且不能存放值为null的数据。而photos: [String!]则表示photos数组自身可能为null,但还是不能存放值为null的数据 。再来看photos: [String]!,这表示photos数组自己不可以为null, 但是可以放值为null的数据。

第二步,有了 Schema 的定义以后,接下来我们可以定义 Query 和 Mutation,以便为 App 提供查询和更新的接口。

type Query {
  getMomentsDetailsByUserID(userID: ID!): MomentsDetails!
}

这表示该 GraphQL 服务提供一个名叫getMomentsDetailsByUserIDQuery,该Query接受userID作为入口参数,并返回MomentsDetails

一般 Query只能用于查询,如果要更新,则需要使用Mutation,下面是一个 Mutation 的定义

type Mutation {
  updateMomentLike(momentID: ID!, userID: ID!, isLiked: Boolean!): MomentsDetails!
}

其实 Mutation 是一个会更新状态的Query,因为在更新后还是可以返回数据的。例如上例中updateMomentLike接受了momentIDuserIDisLiked作为入口参数,在更新状态后也可以返回MomentsDetails

第三步,有了以上的定义以后,我们可以借助 resolver 来查询或者更新数据。

const resolvers = {
  Query: {
    getMomentsDetailsByUserID: (_, {userID}) => momentsDetails,
  },
  Mutation: {
    updateMomentLike: (_, {momentID, userID, isLiked}) => {
      for (const i in momentsDetails.moments) {
        if (momentsDetails.moments[i].id === momentID) {
          if (momentsDetails.moments[i].isLiked === isLiked) {
            break
          }
          momentsDetails.moments[i].isLiked = isLiked;
          if (isLiked) {
            const likedUserDetails = getUserDetailsByID(userID)
            momentsDetails.moments[i].likes.push(likedUserDetails);
          } else {
            // remove the item for that user
            momentsDetails.moments[i].likes = momentsDetails.moments[i].likes.filter((item) => item.id !== userID);
          }
          break;
        }
      }
      return momentsDetails;
    }
  }
};


resolvers的大致逻辑是,在 get Moments Details By User ID 查询里面,直接把momentsDetails的数据返回。在 update moment like 更新里面,我们更新了momentsDetails 的 is Liked属性来表示用户是否点赞。在 Moments AppBFF中,我们维护了一个内存数据库,而在真实生产环境中,可以访问 MySQL、MongoDB 来直接存储数据,或者通过其他微服务来桥接数据库的访问。

到此为止,我们就通过GraphQL实现了一个 BFF。 注意,这只是一个例子,并不是每个 BFF 都必须通过 Apollo Server 以及 Node.js来实现。你可以根据所做团队成员的技能来挑选适合你们的技术栈。

比如,Kotlin 是一个不错的选择,因为大部分 Android开发者都熟悉Kotlin语言,而且 Kotlin 还可以完美兼容JVM。特别JVM生态非常发达,我们可以利用Kotlin 和基于JVM的开源库构建稳定的BFF 方案。

总结

这一章我介绍了如何使用 BFF 来设计跨平台的系统架构,以及如何使用 GraphQL实现 BFF。虽然GraphQL 有众多优点,但并非十全十美,甚至可以说,世界上并没有完美的技术。所以,在使用 GraphQL过程中,我们需要注意以下两点。

  • 在定义 Schema 的过程中,需要前后台开发者共同协商沟通,特别要注意nullable类型的处理,如果前端定义有误,很容易引起 App的崩溃。
  • GraphQL 通常使用 HTTP POST请求,但有些 CDN (content delivery network,内容分发网络)对 POST 缓存支持不好,当我们把 GraphQL 的请求换成 GET 时,整个 Query 会变成 JSON-encoded字符串并放在Query String里面进行发送。此时,要特别注意该 Query String 的长度不要超过 CDN所支持的长度限制(比如Akamai支持最长的 URL 是 8892 字节),否则请求将会失败。

你可能感兴趣的:(15 | 跨平台架构:如何设计 BFF 架构系统?)