面向用户界面和外部方的单用途边缘服务
介绍
随着web的出现和成功,交付用户界面的实际方式已经从厚客户端应用程序转变为通过web交付的界面,这一趋势也使基于SAAS的解决方案总体上得以发展。通过web提供用户界面的好处是巨大的——主要是因为发布新功能的成本大大降低了,因为客户端安装的成本(在大多数情况下)被完全消除了。
然而,这个简单的世界并没有持续太久,不久之后,手机时代就到来了。现在我们有麻烦了。我们有服务器端功能,我们希望通过桌面web UI和一个或多个移动UI公开这些功能。考虑到最初开发的系统是以桌面web UI为基础的,我们在适应这些新类型的用户界面时经常会遇到问题,因为桌面web UI和我们支持的服务之间已经有了紧密的耦合。
通用API后端
容纳多种类型的UI的第一步通常是提供单一的服务器端API,并根据需要随时间增加更多功能以支持新类型的移动交互:
通用API后端
如果这些不同的ui想要进行相同或非常相似的调用,那么这种通用API很容易成功。然而,移动体验的本质往往与桌面web体验截然不同。首先,移动设备的价格非常不同。我们有更少的屏幕房地产,这意味着我们可以显示更少的数据。打开与服务器端资源的大量连接可能会耗尽电池寿命和有限的数据计划。其次,我们希望在移动设备上提供的交互的性质可能有很大的不同。想想一个典型的实体零售商吧。在桌面应用程序中,我可能允许您查看要出售的商品、在线订购或在商店中预订。不过,在移动设备上,我可能希望允许您扫描条形码进行价格比较,或者在商店中为您提供基于上下文的优惠。随着我们构建了越来越多的移动应用程序,我们逐渐意识到人们使用它们的方式非常不同,因此我们需要公开的功能也会有所不同。
因此,在实践中,我们的移动设备将要进行不同的呼叫,更少的呼叫,并将要显示不同的(可能更少)数据比他们的桌面对应。这意味着我们需要在我们的API后端添加额外的功能来支持我们的移动接口。
通用API后端的另一个问题是,根据定义,它们为多个面向用户的应用程序提供功能。这意味着,当推出新的交付时,单个API后端可能会成为瓶颈,因为许多更改都试图对同一个可部署工件进行。
通用API后端承担多个职责的趋势,因此需要大量工作,通常会导致专门创建一个团队来处理这个代码库。这会使问题变得更糟,因为现在前端团队必须与一个单独的团队进行交互以获得所做的更改—一个团队必须平衡不同客户团队的优先级,并且还必须与多个下游团队合作,在新的API可用时使用它们。可以说,在这一点上,我们刚刚在我们的体系结构中创建了一个智能的中间件,它并不关注任何特定的业务领域,这与许多人对明智的面向服务的体系结构应该是什么的看法背道而驰。
介绍前端的后端
对于这个问题,我在REA和SoundCloud中看到的一个解决方案是,不是有一个通用的API后端,而是每个用户都有一个后端,或者(前SoundClouder)Phil Calçado称之为前端后端(BFF)。从概念上讲,您应该将面向用户的应用程序视为两个组件:位于外围的客户端应用程序和位于外围的服务器端组件(BFF)。
BFF与特定的用户体验紧密耦合,通常由与用户界面相同的团队维护,从而使定义和调整API更容易满足UI的需要,同时也简化了对客户机和服务器组件的发布进行排队的过程。
每个用户界面使用一个服务器端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)是“一次经历,一次朋友”。因此,如果iOS和Android的体验非常相似,那么更容易证明拥有一个BFF是合理的。然而,如果它们相差很大,那么拥有单独的bff就更有意义了。
皮特·霍奇森观察到,当围绕团队边界对齐时,bff最有效,因此团队结构应该决定你拥有多少bff。所以如果你有一个移动团队,你应该有一个BFF,但是如果你有独立的iOS和Android团队,你会有独立的BFF。我担心的是团队结构往往比我们的系统设计更灵活。因此,如果你有一个移动的BFF,然后将团队分成iOS和Android专业,那么你是否也必须将BFF分开?如果bff已经是独立的,那么拆分团队会更容易,因为您可以重新分配已经独立的资产的所有权。但是,BFF和团队结构的相互作用是很重要的,我们稍后将对此进行探讨。
通常,实现少量bff的驱动因素是重用服务器端功能以避免过多的重复,但是还有其他方法可以处理这个问题,我们将很快介绍。
以及多个下游服务(微服务!)
对于有少量后端服务的体系结构,BFFs可能是一种有用的模式。然而,对于使用大量服务的组织来说,它们是必不可少的,因为聚合多个下游调用以提供用户功能的需求急剧增加。在这种情况下,一个对BFF的调用通常会导致多个对微服务的下游调用。例如,设想一个电子商务公司的应用程序。我们要在用户的愿望列表中提取一个项目列表,显示库存水平和价格:
多个服务保存我们想要的信息。Wishlist服务存储关于列表的信息,以及每个项目的id。目录服务存储每个项目的名称和价格,库存水平存储在我们的库存服务中。因此在我们的BFF中,我们将公开一个检索完整播放列表的方法,该方法至少包含3个调用:
进行多个下游调用以构建愿望列表的视图
从效率的角度来看,尽可能多地并行运行调用会更明智。一旦对Wishlist服务的初始调用完成,理想情况下,我们希望同时运行对其他服务的调用,以减少总的调用时间。这种需要将我们希望并行运行的调用与按顺序运行的调用混合起来的情况很快就会变得难以管理,特别是对于更复杂的场景。当多个调用的组合变得更容易管理时,这是一个反应式编程风格可以帮助的领域(例如RxJava或Finagle的futures系统提供的)。
但故障模式变得很重要。在上面的示例中,我们可以坚持所有下游调用都必须返回,以便我们将负载返回给客户机。然而,这是否明智?显然,如果Wishlist服务关闭了,我们不能做任何事情,但是如果只有Inventory服务关闭了,那么最好是降低我们传递给客户端的功能,也许只是删除stock level指示符?首先,这些问题必须由BFF自己管理,但我们还需要确保调用BFF的客户机能够解释部分响应并正确地呈现它。
再利用
每个用户界面都有一个BFF的一个关注点是,在BFF本身之间可能会有很多重复。例如,它们可能最终执行相同类型的聚合,具有用于与下游服务交互的相同或相似代码等。一些人对此作出反应,希望将这些代码合并在一起,从而拥有通用的聚合边缘API服务。这个模型一次又一次地证明了它会导致高度膨胀的代码,同时多个关注点挤在一起。
正如我之前多次说过的,我对跨服务的重复代码相当放心。这就是说,虽然在单个流程边界中,我通常会尽我所能将复制重构为适当的抽象,但当遇到跨服务的复制时,我没有相同的反应。这主要是因为我通常更担心提取共享代码导致服务之间紧密耦合的可能性——这比一般的复制更让我担心。也就是说,在某些情况下,这是有道理的。
我的同事皮特·霍奇森(Pete Hodgson)指出,如果你没有朋友,那么通常“共同”的逻辑最终会被烘焙到不同的客户身上。由于这些客户机使用非常不同的技术堆栈,因此很难确定发生这种重复的事实。随着组织倾向于为服务器端组件建立一个通用的技术堆栈,拥有多个重复的bff可能更容易被发现和排除。
当需要提取共享代码时,有两个明显的选项。第一种方法是提取某种共享库,这种方法通常最便宜,但更令人担忧。这可能有问题的原因是,共享库是耦合的主要来源,尤其是用于生成客户端库以调用下游服务时。尽管如此,有些情况下这感觉是对的——特别是当被抽象的代码纯粹是服务内部的一个关注点时。
另一种选择是在一个新服务中提取出共享功能,如果您能够概念化新服务具有围绕所讨论的域建模的某些内容,则该服务可以很好地工作。
这种方法的一个变体可能是将聚合责任推到更下游的服务上。以上面的例子为例,我们讨论了愿望列表的呈现。假设我们在两个地方呈现一个愿望列表-在Android上,iOS Web上。我们的每一个朋友都在打同样的三个电话:
多个BFF执行相同的任务
相反,我们可以更改Wishlist服务来为我们进行下游调用,从而简化呼叫者的工作:
进一步向下游推进集合关税,以消除bff中的重复
我不得不说,在两个地方使用相同的代码不一定会导致我想以这种方式提取服务,但如果创建新服务的事务成本足够低,或者我在多个地方(例如,在桌面web上)使用它,我肯定会考虑。我认为,即使在服务级别,当您将要第三次实现某个东西时创建一个抽象仍然是一个很好的经验法则。
桌面Web及其他领域的BFFs
你可以认为BFFs只是在解决移动设备的限制方面有用处。桌面web体验通常在更强大的设备上提供,具有更好的连接性,在这些设备上进行多个下游呼叫的成本是可控的。这允许您的web应用程序直接对下游服务进行多个调用,而无需BFF。
我也看到过在网络上使用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,这个决定可以由团队单独做出——这不需要任何跨团队的协调。
一般周边问题
有些人使用bff来实现一般的外围关注点,例如身份验证/授权或请求日志记录。我对这事很恼火。一方面,这个功能的大部分都是通用的,所以我倾向于使用另一个位于上游的层来实现它,可能使用类似Nginx或Apache服务器的层。另一方面,这样一个额外的层也会增加延迟。在微服务环境中经常使用BFFs,在微服务环境中,我们已经对延迟非常敏感,因为正在进行的网络调用的数量很多。此外,要构建类似于生产的堆栈,需要部署的层越多,开发和测试就越复杂—将所有这些关注点都放在BFF中作为一个更独立的解决方案可能会很有吸引力:
使用网络设备实现一般外围关注点
如前所述,消除这种重复的另一种方法是使用共享库。假设您的bff使用的是相同的技术,这应该不会太困难,尽管通常关于microservice体系结构中的共享库的警告是适用的。
何时使用
对于只提供web UI的应用程序,我怀疑只有在服务器端需要大量聚合时,BFF才有意义。否则,我认为其他UI组合技术也同样可以工作,而不需要额外的服务器端组件(我希望很快会讨论这些)。
不过,当您需要为移动用户界面或第三方提供特定功能时,我会从一开始就强烈考虑为每一方使用bff。如果部署额外服务的成本很高,我可能会重新考虑,但是在大多数情况下,BFF可以带来的关注分离使它成为一个相当有说服力的提议。基于上述原因,如果构建UI的人员和下游服务之间存在显著的分离,那么我更倾向于使用BFF。
进一步阅读(和观看)
自从我写这篇文章以来,ThoughtWorks的Lukasz Plotnicki发表了一篇关于SoundCloud使用BFF模式的伟大文章
卢卡斯在最近一期的软件工程播客中接受了关于模式(和其他事情)的采访。
来自SoundCloud的Bora Tunca在2016年microxchg的一次演讲中也谈到了更多细节。
结论
前端后端解决了使用微服务时移动开发的一个紧迫问题。此外,它们提供了通用API后端的令人信服的替代方案,许多团队将它们用于移动开发之外的其他用途。限制他们所支持的消费者数量的简单行为使他们更容易处理和更改,并帮助开发面向客户的应用程序的团队保留更多的自主权。