BFF 模式: Backends For Frontends
英文原文地址:https://samnewman.io/patterns/architectural/bff/,水平有限,翻译有误敬请谅解。
很多年前以前,就有人提出了BFF的概念。就是为前端UI展示和外部方提供单一用途的边缘服务。
介绍
众所周知,我们一般公司的架构都是一套后端的WebService(这里统称WebService,即Web服务。),对应着前端的一些展示页面以及承载相应的CRUD的需求。
随着移动应用的越来越普及,这样的后端服务将变成噩梦。所以基于SAAS的解决方案普遍增长。
但是好景不长,我们马上发现移动时代的到来,让我们遇到了一些问题。比如我们有一些服务器端功能,我们希望通过桌面 Web UI 和一个或多个移动 UI 来展示这些功能。对于最初以桌面 Web UI 为开发目标的系统,我们经常面临适应这些新类型用户界面的问题,通常是因为我们已经将桌面 Web UI 与我们的后端服务紧密耦合。
以通用为目的的API后端(The General-Purpose API Backend)
适应多种 UI 类型的第一步通常是提供单一的服务器端 API,并随着时间的推移根据需要添加更多功能以支持新类型的移动交互:
通用API后端
如果这些不同的 UI 想要进行相同或非常相似的调用,那么这种通用 API 很容易成功。然而,移动体验的性质通常与桌面 Web 体验截然不同。首先,移动设备的功能非常不同。我们的屏幕空间更小,这意味着我们可以显示的数据更少。打开大量与服务器端资源的连接会耗尽电池寿命并限制数据计划。其次,我们想在移动设备上提供的交互性质可能截然不同。想想一个典型的实体零售商。在桌面应用程序上,我可能允许您查看待售商品、在线订购或在店内预订。但在移动设备上,我可能希望允许您扫描条形码进行价格比较或在店内为您提供基于上下文的优惠。随着我们构建越来越多的移动应用程序,我们逐渐意识到人们使用它们的方式非常不同,因此我们需要公开的功能也会有所不同。
因此,实际上,我们的移动设备会希望进行不同的调用,更少的调用,并希望显示与桌面设备不同的(可能更少)数据。这意味着我们需要在 API 后端添加其他功能来支持我们的移动界面。
通用 API 后端的另一个问题是,它们按定义向多个面向用户的应用程序提供功能。这意味着,在推出新交付时,单个 API 后端可能会成为瓶颈,因为要对同一个可部署工件进行如此多的更改。
通用 API 后端倾向于承担多项职责,因此需要大量工作,这通常会导致专门成立一个团队来处理此代码库。这可能会使问题变得更加严重,因为现在前端团队必须与一个单独的团队进行交互才能进行更改 - 这个团队必须平衡不同客户团队的优先级,还要与多个下游团队合作以使用可用的新 API。可以说,此时我们刚刚在我们的架构中创建了一个智能中间件,它不专注于任何特定的业务领域 - 这与许多人对合理的面向服务架构的看法相悖。
通用API后端常见架构
为前端引入 BFF 层
我看到 REA 和 SoundCloud 都在使用一种解决此问题的方法,即不采用通用 API 后端,而是为每个用户体验提供一个后端 - 或者如(前 SoundCloud 员工)Phil Calçado所称的“前端的后端”(BFF)。从概念上讲,您应该将面向用户的应用程序视为两个组件 - 一个位于外围的客户端应用程序和一个位于外围的服务器端组件(BFF)。
BFF 与特定的用户体验紧密耦合,通常由与用户界面相同的团队维护,从而更容易根据 UI 的要求定义和调整 API,同时简化了客户端和服务器组件的发布流程。
每个用户界面使用一个服务器端 BFF
BFF 专注于单个 UI,而且只专注于该 UI。这样可以集中精力,因此体积会更小。
我们究竟需要多少个 BFF 层?
谈到在不同平台上提供相同(或类似)的用户体验,我看到了两种不同的方法。我更喜欢的模型是严格为每种不同类型的客户提供一个 BFF - 这是我在 REA 看到的模型:
不同的移动平台,不同的 BFF,如 REA 所用
另一种模型是我在 SoundCloud 看到的,它为每种类型的用户界面使用一个 BFF。因此,Android 和 iOS 版本的监听器原生应用程序都使用相同的 BFF:
为不同的移动后端设置一个 BFF,就像 SoundCloud 那样
我对第二种模型的主要担忧是,使用单个 BFF 的客户端类型越多,它就越有可能因处理多个问题而变得臃肿。不过,这里要理解的关键是,即使共享 BFF,它也适用于同一类用户界面 - 因此,虽然 SoundCloud 的 iOS 和 Android 监听器原生应用程序使用相同的 BFF,但其他原生应用程序会使用不同的 BFF(例如,新的 Creator 应用程序 Pulse 使用不同的 BFF)。如果同一个团队同时拥有 Android 和 iOS 应用程序以及 BFF,我也更愿意使用此模型 - 如果这些应用程序由不同的团队维护,我更倾向于推荐更严格的模型。因此,您可以看到您的组织结构是决定哪种模型最有意义的主要驱动因素之一(康威定律再次获胜)。值得注意的是,我采访过的 SoundCloud 工程师表示,如果今天再次做出决定,他们可能会重新考虑为 Android 和 iOS 监听器应用程序使用一个 BFF。
我非常喜欢 Stewart Gleadow(他又赞扬了 Phil Calçado 和 Mustafa Sezgin)的一条指导方针,那就是“一种体验,一个 BFF”。因此,如果 iOS 和 Android 的体验非常相似,那么就更容易证明拥有一个 BFF 是合理的。然而,如果它们差异很大,那么拥有单独的 BFF 更有意义。
Pete Hodgson 观察到,BFF 在团队边界内协调一致时效果最佳,因此团队结构应该决定您拥有多少个 BFF。因此,如果您有一个移动团队,您应该有一个 BFF,但如果您有单独的 iOS 和 Android 团队,那么您将拥有单独的 BFF。我担心的是,团队结构往往比我们的系统设计更具流动性。因此,如果您有一个移动 BFF,然后将团队分为 iOS 和 Android 专业,您是否也必须拆分 BFF?如果 BFF 已经分开,那么拆分团队会更容易,因为您可以重新分配已经独立的资产的所有权。不过,BFF 和团队结构的相互作用很重要,我们将很快对此进行进一步探讨。
通常,减少 BFF 数量的目的是为了重用服务器端功能以避免过多的重复,但还有其他方法可以解决这个问题,我们将很快介绍。
多个下游服务(微服务!)
对于后端服务数量较少的架构,BFF 可能是一种有用的模式。然而,对于使用大量服务的组织来说,它们可能是必不可少的,因为聚合多个下游调用以提供用户功能的需求急剧增加。在这种情况下,对 BFF 的一次调用通常会导致对微服务的多次下游调用。例如,想象一个电子商务公司的应用程序。我们希望提取用户愿望清单中的商品列表,显示库存水平和价格:
Name | Stocks | Price |
---|---|---|
The Brakes - Give Blood | In Stock! | $5.99 |
Blue Juice - Retrospectable | Out Of Stock | $17.50 |
Hot Chip - Why Make Sense? | Going fast | $9.99 |
多个服务保存着我们想要的信息。愿望清单服务存储有关清单的信息以及每个项目的 ID。目录服务存储每个项目的名称和价格,库存水平存储在我们的库存服务中。因此,在我们的 BFF 中,我们将公开一种检索完整播放列表的方法,该方法至少包含 3 个调用:
进行多次下游调用来构建愿望清单视图
从效率的角度来看,并行运行尽可能多的调用会更加明智。在对 Wishlist 服务的初始调用完成后,理想情况下,我们希望同时运行对其他服务的调用,以减少总体调用时间。这种需要混合并行运行的调用和顺序运行的调用很快就会变得难以管理,尤其是在更复杂的场景中。这是一个响应式编程风格可以提供帮助的领域(例如RxJava或Finagle 的 Futures系统提供的风格),因为多个调用的组合变得更易于管理。
然而,理解故障模式变得很重要。在上面的例子中,我们可以坚持要求所有下游调用都必须返回,以便我们向客户端返回有效负载。但是这合理吗?显然,如果 Wishlist 服务关闭,我们什么也做不了,但如果只有 Inventory 服务关闭,那么降低我们传回给客户端的功能不是更好吗,也许只需删除库存水平指示器?这些问题必须首先由 BFF 本身来管理,但我们还需要确保调用 BFF 的客户端可以解释部分响应并正确呈现它。
重用和 BFF (Reuse and BFFs)
每个用户界面只有一个 BFF 的一个问题是,BFF 之间可能会出现大量重复。例如,它们可能最终执行相同类型的聚合,具有相同或相似的代码来与下游服务交互等。有些人对此的反应是希望将它们合并在一起,因此有一个通用的聚合 Edge API 服务。这种模型已被反复证明会导致代码高度臃肿,多个问题被挤压在一起。
正如我之前多次说过的,我对跨服务重复代码相当放心。也就是说,虽然在单一进程边界内,我通常会尽我所能将重复重构为合适的抽象,但面对跨服务重复时,我不会做出同样的反应。这主要是因为我通常更担心提取共享代码可能会导致服务之间的紧密耦合 - 比一般的重复,我更担心这一点。话虽如此,在某些情况下,这样做是合理的。
我的同事 Pete Hodgson 指出,如果没有 BFF,那么“通用”逻辑最终往往会嵌入到不同的客户端中。由于这些客户端使用的技术堆栈非常不同,因此识别这种重复的情况可能很困难。由于组织倾向于为服务器端组件使用通用技术堆栈,因此可能更容易发现和排除具有重复的多个 BFF。
当确实需要提取共享代码时,有两个明显的选择。第一种通常是最便宜但更麻烦的,即提取某种共享库。这可能有问题的原因是共享库是耦合的主要来源,尤其是在用于生成用于调用下游服务的客户端库时。尽管如此,在某些情况下,这样做感觉是对的 - 尤其是当被抽象的代码纯粹是服务内部的问题时。
另一种选择是在新服务中提取共享功能,如果您可以概念化新服务具有围绕相关领域建模的功能,那么它可以很好地发挥作用。
这种方法的一个变体可能是将聚合职责推到下游服务。以上面我们讨论的呈现愿望清单为例。假设我们在两个地方呈现愿望清单 - Android、iOS Web。我们的每个 BFF 都在进行相同的三个调用:
多个 BFF 执行相同的任务
相反,我们可以更改 Wishlist 服务来为我们进行下游调用,从而简化调用者的工作:
将聚合任务进一步推向下游,以消除 BFF 中的重复
我不得不说,在两个地方使用相同的代码并不一定会导致我想要以这种方式提取服务,但如果创建新服务的交易成本足够低,或者我在多个地方使用它(例如可能在桌面网络上),我肯定会考虑这样做。我认为,当你准备第三次实现某件事时,创建一个抽象的古老格言仍然感觉像是一个很好的经验法则,即使在服务级别也是如此。
桌面 Web 及其他领域的 BFF
您可以将 BFF 视为解决移动设备限制的一种用途。桌面 Web 体验通常在功能更强大、连接性更好的设备上提供,在这些设备上进行多次下游调用的成本是可控的。这可以让您的 Web 应用程序直接对下游服务进行多次调用,而无需 BFF。
不过,我也见过在 Web 上使用 BFF 也很有用的情况。当您在服务器端生成大部分 Web UI 时(例如使用服务器端模板),BFF 显然是可以完成此操作的地方。它还可以在某种程度上简化缓存,因为您可以在 BFF 前面放置一个反向代理,从而允许您缓存聚合调用的结果(尽管您必须确保相应地设置缓存控件,以确保聚合内容的到期时间与聚合中最新内容所需的时间一样短)。事实上,我见过它多次被使用,但没有将其称为 BFF - 事实上,通用 API 后端通常就是从这样的野兽中发展而来的。
我见过至少有一个组织将 BFF 用于需要拨打电话的其他外部方。回到我常举的音乐商店的例子,我可能会公开 BFF,以允许第三方提取版税支付信息、提供 Facebook 集成或允许流式传输到一系列机顶盒设备:
使用 BFF 向第三方公开 API
这种方法尤其有效,因为第三方通常没有能力(或意愿)使用或更改他们所做的 API 调用。使用通用 API 后端,您可能不得不保留旧版本的 API,以满足一小部分无法进行更改的外部方的需求 - 使用 BFF 可以大大减少这个问题。
自治
我们经常会看到这样的情况:一个团队负责前端,另一个团队负责创建后端服务。一般来说,我们会尝试通过转向围绕业务垂直领域协调的微服务来避免这种情况,但即便如此,也有一些情况很难避免。首先,在一定规模或复杂程度下,需要多个团队参与。其次,实现良好的 Android 或 iOS 体验所需的技术技能深度通常需要专门的团队。
因此,构建用户界面的团队面临着这样的情况:他们正在调用另一个团队正在开发的 API,而且 API 通常在用户界面开发的同时不断发展。BFF 可以在这里提供帮助,特别是如果它由创建用户界面的团队拥有。他们在创建前端的同时发展 BFF 的 API。他们可以快速迭代两者。BFF 本身仍然需要调用其他下游服务,但这可以在不中断用户界面开发的情况下完成。
使用 BFF 时的团队所有权边界示例
使用像这样沿团队边界对齐的 BFF 的另一个好处是,创建界面的团队可以更加灵活地思考功能所在的位置。例如,他们可以决定将功能推送到服务器端,以促进未来的重用并简化原生移动应用程序,或者允许更快地发布新功能(因为您可以绕过应用商店审核流程)。如果团队同时拥有移动应用程序和 BFF,则可以独立做出此决定 - 它不需要任何跨团队协调。
General Perimeter Concerns
有些人使用 BFF 来实现通用的边界问题,例如身份验证/授权或请求日志记录。我对此很纠结。一方面,这些功能中的大部分都是通用的,我倾向于使用位于上游的另一层来实现它,也许使用类似于 Nginx 或 Apache 服务器层的东西。另一方面,这样的额外层不能不增加延迟。BFF 通常用于微服务环境中,由于进行的网络调用数量众多,我们已经对延迟非常敏感。此外,您必须部署更多的层来构建类似生产的堆栈,这会使开发和测试变得更加复杂——将所有这些问题都放在 BFF 中作为一个更独立的解决方案可能会很有吸引力:
使用网络设备实现通用边界关注
正如我们之前所讨论的,解决这种重复问题的另一种方法是使用共享库。假设您的 BFF 使用相同的技术,这应该不会太难,尽管微服务架构中关于共享库的常见警告也适用。
何时使用
对于仅提供 Web UI 的应用程序,我认为 BFF 仅在服务器端需要大量聚合时才有意义。否则,我认为其他 UI 组合技术同样有效,无需额外的服务器端组件(我希望很快会谈到这些)。
但是,当您需要为移动 UI 或第三方提供特定功能时,我会强烈建议从一开始就为每一方使用 BFF。如果部署额外服务的成本很高,我可能会重新考虑,但 BFF 可以带来的关注点分离在大多数情况下使其成为一个相当有吸引力的主张。如果构建 UI 和下游服务的人员之间存在显著的分离,我会更倾向于使用 BFF,原因如上所述。
References
- 在我写这篇文章之后,ThoughtWorks 的 Lukasz Plotnicki 发表了一篇关于 SoundCloud 使用 BFF 模式的精彩文章
- Lukasz在最近一期的软件工程播客中接受了有关该模式(以及其他内容)的采访。
- SoundCloud 的 Bora Tunca 在microxchg 2016 的一次演讲中也进行了更详细的介绍。
结论
前端后端解决了使用微服务时移动开发的一个迫切问题。此外,它们还为通用 API 后端提供了引人注目的替代方案,许多团队不仅将其用于移动开发,还将其用于其他目的。通过限制它们支持的消费者数量,它们更容易使用和更改,并帮助开发面向客户的应用程序的团队保留更多的自主权。
感谢 Matthias Käppler、Michael England、Phil Calçado、Lukasz Plotnicki、Jon Eaves、Stewart Gleadow 和 Kristof Adriaenssens 在研究本文时提供的帮助,以及 Giles Alexander、Ken McCormack、Sriram Viswanathan、Kornelis Sietsma、Hany Elemary、Martin Fowler、Vladimir Sneblic 和 Pete Hodgson 提供的一般反馈。我也非常感谢任何进一步的反馈,所以请随时在下面发表评论!