后台换前台演示
在我之前的一篇文章中,我描述了Backend-for-Frontend模式。简而言之,它为多个后端部分提供了一个单一的门面。此外,它为每一个客户类型,例如桌面、移动,提供它所需要的数据,而不是以该客户类型所要求的格式提供更多。
用例
想象一下下面这个用例。在一个电子商务商店,主页应该同时显示多个不相关的数据:
- 产品。企业可以配置哪些商品在主页上显示。它们可以是通用的、"英雄 "产品,也可以是个性化的、客户之前订购的产品。
- 新闻。同样,新闻报道可以是通用的或个性化的。
- 档案相关信息
- 购物车内容
- 与业务无关的信息,如构建号、构建时间戳、版本等。
根据客户的情况,我们想要更多或更少的数据。例如,在显示尺寸有限的客户端上,我们可能想把一个产品限制在它的名字和它的图像上。另一方面,在桌面上,我们很乐意同时显示上述内容,加上一句口号(或一个更吸引人的--更长的--名字)和完整的描述。
每个客户都需要特定的数据,出于性能方面的考虑,我们希望通过一次调用来获取这些数据。这听起来像是BFF的一个用例。
设置演示
为了简化事情,我将只保留三个数据源:产品、新闻和技术数据。三个不相关的数据源足以强调这个问题。
在演示中,我使用Python和Flask,但底层技术并不重要,因为BFF是一种架构模式。
最初的情况是一个单体。单体为每个数据源提供一个端点,并为所有数据源提供一个单一的聚合端点。
Python
@app.route("/")
def home():
return {
'products': products, #1
'news': news, #1
'info': debug #1
}
- 以某种方式在内部获取数据,例如,从数据库中获取数据
在这一点上,一切都很好。我们可以根据客户的情况提供不同的数据。
- 如果我们想把责任放在客户端,我们提供一个专门的端点
- 如果我们想让它在服务器端,我们可以从请求中读取
User-Agent(或者约定一个特定的X-HTTP头)。
由于这对演示没有任何帮助,我不会在下文中根据客户端提供不同的数据。
迁移到微服务
在某个时刻,组织决定迁移到微服务架构。原因可能是CTO在一篇博文中读到了微服务,因为团队领导想在其简历中加入微服务,甚至是因为开发规模过大,组织确实需要发展。在任何情况下,单体必须被分割成两个微服务:一个是提供产品的目录,一个是提供......新闻的新闻源。
下面是每个微服务的代码。
Python
@app.route("/info")
def info():
return debug #1
@app.route("/products")
def get_products():
return jsonify(products) #2
- 每个微服务都有自己的
debug端点 - 有效载荷不再是一个对象,而是一个数组
@app.route("/info")
def info():
return debug #1
@app.route("/news")
def get_news():
return jsonify(news) #1
- 如上所述
现在,每个客户端需要两次调用并过滤掉不相关的数据。
专用的后端-前端
由于上面强调的问题,一个解决方案是开发一个应用程序来进行聚合和过滤。每个客户类型都应该有一个,而且应该由客户的同一个团队来负责。同样,对于这个演示,有一个只做聚合的程序就足够了。
@app.route("/")
def home():
products = requests.get(products_uri).json() #1
catalog_info = requests.get(catalog_info_uri).json() #2
news = requests.get(news_uri).json() #1
news_info = requests.get(news_info_uri).json() #2
return {
'products': products,
'news': news,
'info': { #3
'catalog': catalog_info,
'news': news_info
}
}
- 获取数据
- 获取调试信息
- 返回的JSON应该被设计成易于在客户端消费。为了说明这一点,我选择让调试数据嵌套,而不是顶层。
在API网关层面上的后端换前端
如果你正在提供API,无论是内部还是外部世界,你很有可能已经在使用API网关。如果没有,你也许应该深入考虑开始使用。在下文中,我假设你确实使用了一个。
在上一节中,我们开发了一个专门的后端对前端的应用程序。然而,请求已经通过了网关。在这种情况下,网关可以被看作是一个部署BFF插件的容器。我将使用Apache APISIX来演示如何做到这一点,但这个想法也可以在其他网关上进行复制。
首先,没有通用的方法来实现我们想要的结果。由于这个原因,我们不能依赖现有的插件,而是要设计自己的插件。APISIX记录了如何做到这一点。我们的目标是像上面那样从所有端点获取数据,但要通过插件。
首先,我们需要暴露一个专门的端点,例如,/bff/desktop 或/bff/phone 。APISIX允许通过public-API插件实现这种虚拟端点。接下来,我们需要开发我们的插件,bff 。下面是配置片段。
YAML
routes:
- uri: / #1
plugins:
bff: ~ #2
public-api: ~ #2
- 为了演示的目的,我宁愿把它设置在根目录下,而不是
/bff/* - 声明这两个插件。注意,我使用的是独立模式。
首先,我们需要描述这个插件,并且不要忘记在文件的末尾返回它。
Lua
local plugin_name = 'bff-plugin'
local _M = { --1
version = 1.0,
priority = 100, --2
name = plugin_name,
schema = {}, --3
}
return _M --4
- 表需要被命名为
_M - 在这种情况下,
priority是不相关的,因为不涉及其他插件(但public_api)。 - 不需要模式,因为没有配置
- 不要忘记返回!
一个拥有公共API的插件需要定义一个api() 函数,返回一个描述匹配HTTP方法、匹配URI和处理函数的对象。
Lua
function _M.api()
return {
{
methods = { 'GET' },
uri = "/",
handler = fetch_all_data,
}
}
end
现在,我们必须定义fetch_all_data 函数。这只是一个对目录和新闻源微服务进行HTTP调用的问题。如果你对具体的细节感兴趣,请看一下代码。
在这一点上,(单一)客户端可以查询http://localhost:9080/ ,并获得完整的有效载荷。
在一个基于微服务的 "现实生活 "组织中,每个团队都应该是相互独立的。通过这种方法,每个人都可以把自己的BFF开发成一个插件,并在网关中独立部署。
奖金:一个穷人的BFF
微服务架构给客户带来了两个问题:
- 需要获取所有数据并过滤掉不必要的数据
- 对每个服务的多次调用
BFF模式可以解决这两个问题,但要付出定制开发的代价,无论它是用于专用应用程序还是网关插件。如果你不愿意花时间进行定制开发,你仍然可以通过使用Apache APISIX的一个灵巧的插件batch-requests ,来避免#2。
batch-requests插件接受多个请求,通过HTTP流水线从APISIX发送这些请求,并向客户端返回一个聚合的响应。在客户端需要访问多个API的情况下,这大大改善了性能。
--批量请求
实质上,客户端需要向先前配置的端点发送以下有效载荷。
JSON
{
"timeout": 502,
"pipeline": [
{
"method": "GET",
"path": "/products"
},
{
"method": "GET",
"path": "/news"
},
{
"method": "GET",
"path": "/catalog/info"
},
{
"method": "GET",
"path": "/news/info"
}
]
}
响应将反过来看起来像这样。
JSON
[
{
"status": 200,
"reason": "OK",
"body": "{\"ret\":200,\"products\":\"[ ... ]\"}",
"headers": {
"Connection": "keep-alive",
"Date": "Sat, 11 Apr 2020 17:53:20 GMT",
"Content-Type": "application/json",
"Content-Length": "123",
"Server": "APISIX web server"
}
},
{
"status": 200,
"reason": "OK",
"body": "{\"ret\":200,\"news\":\"[ ... ]\"}",
"headers": {
"Connection": "keep-alive",
"Date": "Sat, 11 Apr 2020 17:53:20 GMT",
"Content-Type": "application/json",
"Content-Length": "456",
"Server": "APISIX web server"
}
},
{
"status": 200,
"reason": "OK",
"body": "{\"ret\":200,\"version\":\"...\"}",
"headers": {
"Connection": "keep-alive",
"Date": "Sat, 11 Apr 2020 17:53:20 GMT",
"Content-Type": "application/json",
"Content-Length": "78",
"Server": "APISIX web server"
}
},
{
"status": 200,
"reason": "OK",
"body": "{\"ret\":200,\"version\":\"...\"}",
"headers": {
"Connection": "keep-alive",
"Date": "Sat, 11 Apr 2020 17:53:20 GMT",
"Content-Type": "application/json",
"Content-Length": "90",
"Server": "APISIX web server"
}
}
]
这取决于客户端是否能过滤掉不必要的数据。这不如真正的BFF好,但我们还是成功地在4个调用中完成了一个。
总结
微服务架构带来了大量的技术问题需要应对。其中,需要将所需的数据分配给每一种客户端。BFF模式的目的就是为了应对这个问题。
在上一篇文章中,我从理论的角度描述了该模式。在这篇文章中,我使用了一个非常简单的电子商务用例来演示如何在有或没有Apache APISIX的帮助下实现BFF。
这篇文章的完整源代码可以在GitHub上找到。