🤩 蚂蚁 WebApp 架构 - 「Unio」 来啦!

684 阅读14分钟

主题:本次分享介绍基于边缘计算和 serverless 的蚂蚁新一代的 WebApp 架构 Unio 的演进过程以及如何解决业务难题。

大纲

  1. 蚂蚁前端托管的演进和 Unio 的诞生
  2. Unio 的架构
  3. Unio 在蚂蚁的实践案例
  4. Unio 的开放与未来

本文由前端技术专家梅霖分享。想要探索前端技术的无限可能,就请关注我们吧!🤗

背景

在 2022 年蚂蚁体验技术部基础服务组开始尝试基于阿里云的 EdgeRoutine 解决生产中遇到的 SEO、端外灰度等问题,通过在 ER 上设计并部署了一套路由方案解决了上述问题。到了 23 年我们决定融合蚂蚁前端统一托管基座、EdgeRoutine 设计新一代 WebApp 架构 - Unio,并基于 Unio 实践上线了蚂蚁内部的 SSR 方案。

本文会结合在 18届 D2 大会上分享的 PPT 来介绍 Unio 的演进过程和业务中的实践。

image.png

目录

本次分享主要包括四部分:

  • 第一部分「蚂蚁前端托管的演进和 Unio 的诞生」会介绍蚂蚁前端托管的发展历史

  • 第二部分「Unio 的结构」会架构分层,并详细解释各层的设计思路和链路

  • 第三部分「Unio 在蚂蚁的实践案例」会介绍一个复杂的落地场景:SSR 应用,从中可以理解如何基于 Unio 来设计企业级 WebApp 应用场景。

  • 第四部分 「Unio 的开放与未来」会把视角转向控制面,来看如何设计一个开放的控制面系统以承接扩展业务。

image.png

蚂蚁前端托管的演进和 Unio 的诞生

下图是蚂蚁官网的前端链路:现在仍然还是 java 应用托管页面 html ,静态资源经过 CDN 回源到一个空应用上,这个应用有复杂的 nginx 配置,能基于不同的域名和路径 proxy 到不同的 OSS 上,从而找到对应的 js / css / png 等。

这样的方案需要前端在发布代码的时候进行复杂的操作,已经是前端老古董的存在了:

  • 不仅要把静态资源发布到 OSS 上
  • 还要发一份配置文件到后端应用上用于维护静态资源的版本
  • 一旦需要修改 HTML 还要改后端应用上托管的模板代码

image.png

到了 2016 年前后,体验技术部建设起统一的前端基座用来承接所有的前端流量。

对于运行时链路,前端基座在 CDN 的背后作为源站,基于 CDN 来抗住大流量的请求。这里的前端基座不同于上面的空应用,只是简单的将请求做转发,而是具备了灰度、域名映射、ACL 等多种功能的一个 nodejs 应用。

这套方案的另一层意义在于配合控制面的建设,把前端应用抽象为几个核心概念

  • pack - 描述一个应用
  • 域名 - 描述类似 render.alipay.com / www.antgroup.com 这样的前端域名
  • 路径映射 - 描述具体的路径映射到 OSS 的关系

对前端应用研发者而言,前端应用的研发是一个整体,包括 html / json / js / json 都是托管在统一基座上,在统一的研发平台进行研发、构建、打包、发布。

image.png

然而有一种特殊的应用类型 - 中后台应用。中后台应用常见是一些后台系统,具有两个特点

  • 有登录态
  • 应用复杂度极高,一个中台后面有无数个页面,可能由不同的团队进行维护

这两个问题都没有办法在上述的前端基座上解决,为此当时维护蚂蚁内部复杂中台页面的团队设计了一个中后台的基座,中后台基座更像是一个纯函数,在这个基座上解决了登录和微应用组装的逻辑,所有的静态资源托管仍然是基于前端基座来解决。

image.png

好了,让我们回顾下蚂蚁目前的前端底层现状:

image.png

注意这里的 3 个现状,其实是我们设计的初衷和演进的必然结果。可能聪明的你会发现自己公司的有类似的基建,但可能和我们的架构完全不同。这就是架构必须要贴合具体的业务场景来发展,没有最完美,只有最合适。

假设这时候你是蚂蚁的 System Desgin 的面试官,面试者介绍了上述的架构,来想想看,这套架构是否存在什么问题呢?

  • 首先,我们的建设都围绕在 CDN 的后面,大家都知道 CDN 的好处是有缓存,因此只有部分请求回源到了源站,但如果想要做基于 User-Agent 的一些判断(识别爬虫)、定制缓存等等,目前的源站是无法实现的。

  • 其次,如果想要做 SSR 方案,是无法直接基于前端统一基座来做,因为 SSR 不可避免的存在 OOM、流量突增等问题,而统一基座承载了目前的所有蚂蚁前端的流量,首要考虑的就是稳定性,这就带来了矛盾点。

  • 最后就是定制成本的问题。从架构维护者的角度来看,整个公司会有各种各样的前端需求,中后台微应用、SSR、RSC、SEO 等等,而每个需求都有模型设计、部署、发布的过程,现有的控制面能力无法实现这样的扩展。实际场景上,为了实现上述的中后台场景就专门增加了一套站点模型用于描述一个站点内的多个微应用的配置逻辑。

image.png

上述都是来源于蚂蚁的实际生产场景中遇到问题,如果你能看出其他的问题欢迎留言讨论。

因此我们为了解决这些痛点,首先去寻找恰当的基础设施:

  • EdgeRoutine: help.aliyun.com/document_de…

    EdgeRoutine (下面简称:ER)可以在 CDN 边缘运行 JS 代码,因此可以在代码中处理每一条请求,实现自由的请求重写、重定向、响应重写、缓存控制等能力。

image.png

  • FC: help.aliyun.com/zh/fc/produ…

    依托于不同的 serverless 函数基础设施,比如阿里云的 FC、小程序云。可以实现类似于 SSR、RSC 等重 CPU 的功能。

image.png

基于此来推理下新的架构?

把 ER 和 FC 塞到链路中很简单,但再思考一下上面的第三个问题:「定制成本高」。如果我们只是 case by case 去解决具体的问题,那这套架构势必不能满足全公司的诉求。

image.png

Unio 的架构

好,这时候来看看我们的设计思路。我们将这套架构命名为 Unio

拉丁文中有the number one, oneness, unity含义,作统一性、一致性解释,对应 WebApp 的全局架构特性。

英文中有珠蚌含义,后面会在上面孵化出更多的珍珠,对应 WebApp 的可拓展架构特性。

Unio 是 web 应用运行时的分层架构模型,并为不同的研发场景提供统一抽象及编程接口。

image.png

应用层:指的是前端应用业务代码的研发框架,当前的典型实现就是蚂蚁内部的各类研发框架(可以参考对外的 Umi 等框架)。这一层研发框架只需要实现模型中其他层所需的协议即可,其他不做额外要求。

容器层:指浏览器或客户端侧的运行时容器,当前的典型实现就是燕鸥(可以参考对外的 qiankun)运行时容器。燕鸥前端运行时容器主要做了微应用加载、请求代理、监控上报等功能,如果这些基础功能可以以接口的方式提供出去,则可以为应用层研发框架提供快速的场景组合能力,同时也提供一定的口子,供指定业务场景拓展容器层能力。

image.png

流量层:指在流量入口处实现的一些路由能力,当前的典型实现是端外灰度、SEO,实际部署环境可以是 ER,也可以是平台提供的统一服务。

这一层可以让研发场景通过统一的声明式编程界面,做一些前置的流量处理,如登录拦截,灰度路由等。

image.png

结合目前 ER 的能力,可以将 HTTP 请求和响应做定制,定制的原子能力为 request rewrite (请求重写)、redirect (重定向)、response rewrite(响应重写)。

一个应用可能有多个路由功能,一个具体的路由功能可以有多个原子能力组成。这样的分层结构可以实现流量层对不同应用实现不同的路由处理。

image.png image.png image.png

对于托管层,即上面提到的前端统一托管基座。

托管层:本质上一个 nodejs 应用,实现了域名映射、路径映射、灰度计、渲染等能力。

域名映射:

路径映射:某些应用需要定制类似 /p/h5/abc/index.html ,映射即为定制 path 映射到实际应用的映射关系

灰度计算:HTML / JSON 支持通过请求头实现灰度控制,托管层要基于请求头找到不同的资源,并能配合 CDN 的多副本机制实现灰度和全量的缓存隔离。

渲染:指的是注入一些环境变量,并非 SSR 这种服务端渲染。

image.png image.png

计算层:专注于处理请求与响应,以及一些服务端 JS 的逻辑,通常部署在 serverless 基建上,比如小程序云、阿里云等。典型实现是 SSR 场景。

image.png

细心的同学会发现计算层和流量层都是使用了 Web API fetchEvent 的编程界面,这是源于 Unio 定义了一套蚂蚁内部的标准 js container 实现。

定义 js container 的背景在于:通过一套统一的 js container 标准可以保证对应用层研发以相同的编程界面提供服务,而在底层通过不同的 runtime 抹平差异。

    addEventListener('fetch', event => {
      event.respondWith(new Response('Hello world'));
    });
    export default (req: NextRequest) => {
      return NextResponse.json({
        name: `Hello, from ${req.url} I'm now an Edge Function!`,
      });
    };
    import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

    serve((_req) => {/* .. */});

除 deno 之外,均采用了 Service Worker 编程界面,采用 addEventListener('fetch', ...)作为其入口。

image.png

上面详细地讲了 Unio 的架构,是不是还是有点困惑?这套架构该如何使用?

没关系,让我们举个 🌰。

Unio 在蚂蚁的实践案例

这是蚂蚁的保险业务的 SSR 诉求:

保险业务场景下,存在缓存命中率低,在线场景多的问题。需要通过 SSR 技术来提升首评加载时间,提升业务转化。尽量通过业务低成本的改造,获得大的性能提升。

保险低频访问和产品类目繁多等业务特点,造就线上用户体验也比较有特点。

  1. 75%的首次访问,缓存命中低,首屏时间较长

  2. 近20%的中低端设备,设备资源有限,首屏时间较长

  3. 弱网下,多次http、rpc请求链路,容易超时

image.png

CSR 存在一个无法解决问题,基本链路是:请求 HTML -> 请求 JS -> 请求数据 -> 渲染数据。至少需要 3 个请求才能将内容展示,而 SSR 仅需一个请求即可展示内容,天然比 CSR 快。

但 SSR 存在一个众所周知的问题,在千人千面场景下,SSR 的内容无法缓存,因此我们将 SSR 仅用于可缓存部分,最差情况为纯骨架屏。

此外,目前的单页应用存在一个通用的 JS bundler 体积问题,体积越大前端加载的耗时将会越长,并且这是缓存也无法解决的问题。RSC 技术将组件渲染都放在了服务端,可以显著减少客户端的 bundler 大小,只要在应用层框架建设一个通用的基础 runtime,使之在各个应用之间可以复用。

image.png

因此,最终 Unio SSR 快的秘诀其实就是想尽一切办法,在各个环节使用缓存的方式来达到极致的首屏体验的。

那假如用户是第一次访问,客户端还没有缓存,是否还会这么快呢?

这个基本就是强则愈强的故事的,只要你的页面每分钟都有流量进来,基本是可以确保 CDN 缓存一直是热的,这样即便是客户端还没有缓存,ER 也能做到在毫秒级内响应给客户端。

但假如你的页面真的是流量特别低,几个小时才有零星的几次访问,CDN 永远都是冷的,此时若客户端还没有缓存,那么确实可能依然还会慢。但这种情况下,即便 CSR 也一样会慢,这几乎是一个无解的问题。

image.png

Unio SSR 和普通 CSR 的效果对比:

图片.gif

Unio 的开放和未来

可以把进度条拉回到最开始,部署侧设计了应用、域名、路径应用等模型用于托管层消费。

现在有业务方提了一个 SEO 需求,希望能针对爬虫返回提前渲染好的页面。我们要如何做呢?试着在 Unio 架构中实现吧?

针对爬虫的预渲染页面不属于正常的应用打包产物,因此需要增加一个新的产物模型定义。

OK,产物有了,现在如何识别爬虫呢?很简单,上面的 Unio 流量层设计中定义了「路由」这样一个模型在流量层消费,我们只要增加一个 SEO 路由来识别 User-Agent 就可以实现了。

这样在原有的控制面中做了很大的改造,终于把这两个模型也加进去了,很好,SEO 终于跑起来了。

现在保险的 SSR 需求来了,我们是不是还要在控制面持续增加新模型?

image.png

我们将运行时和控制面的本质理解为:

将产物发到指定存储对象的指定位置,在 Unio 各层去消费

因此抽象了通用模型 - resource

将部署流程拆分为四步:

定义 resource -> 创建部署 -> 灰度部署 -> 全量部署

image.png image.png image.png image.png

基于通用模型就可以轻松解决模型和部署的扩展问题,但带来了新的问题:

以 SSR 为例:至少需要 SSR 产物、SSR路由、RSC 产物、RSC 路由四个 resource 实例。这样上游在部署的时候势必要理解整个链路,这样其实相当于是把复杂度转移到了上游。

image.png

因此,我们抽象了 feature 。顾名思义,feature 就是描述一个具体的特性,以这种产品化的方式来解决复杂 resource 的构建。

将现有的能力抽象成无数个 feature,比如过去两年我们做的 SEO、全球加速、端外灰度、钉钉灰度、这些能力会涉及一个或多个 resource 的部署,且平台接入成本也很高。但现在把逻辑收到控制面后,控制面提供统一的接口给研发平台,对应用而言只是一个简单的 feature 绑定即可。

image.png

因此对于 SSR 函数,在 Unio 中要定义:

resource SSR 路由 ,描述 SSR 的流量和配置

code SSR 函数,即执行 react server rendering

resource SSR 产物,即构建生成的 server.js,用于 SSR 函数消费

code SSR 路由中间件,进行 SSR HTML 的组装

code RSC 函数,即执行 react server component 渲染

resource RSC 产物,即构建生成的 server.js,用于 RSC 函数消费

之后定义实现 SSR feature 和 RSC feature ,在 feature 内实现上述 resource 创建即可。

对于 SSR 框架研发者,Unio 能做到的是抹除了所有基础设施,不需要学习 CDN、函数计算、托管的原理等等,只需要关注如何消费 SSR 产物完成 renderToPipeableStream 即可。

对一个普通的 H5 应用而言,只要完成代码改造,在研发平台做一个 feature 绑定即可完成应用从 CSR 到 SSR 的改造,全程都不需要感知到 Unio 的存在。

image.png image.png

Unio 最终的愿景是:通过基础能力的增强,一方面提供给用户更多开箱即用的功能直接给业务带来便利,另一方面是能支撑蚂蚁集团内更多创新业务的发展,不要让基础服务成为业务技术发展的卡点。

image.png

附:分享 PPT

Unio - 蚂蚁新一代 WebApp 架构