☁️ 百毫秒级 Node.js Serverless 环境 - 小程序云函数背后的故事

主题简介

支付宝小程序云是蚂蚁集团提供的以云原生、高可用为基础的服务端解决方案。它支持多端接入,能够降低成本、免除运维烦扰、支撑高并发业务需求。

本次分享将介绍小程序云函数背后的故事,从我们对 serverless 的探索和尝试开始,到现在如何实现 Node.js 百毫秒冷启动 serverless 运行环境以及我们在追求极致性能方面做的一些方案尝试。此外,还将介绍蚂蚁基于此进行的一些业务实践工作。

💡 为什么想要 Serverless

在前后端分离、微服务等等概念大行其道的今天,服务端设计越来越倾向于从领域模型出发,让服务和系统更加内聚。而前端由于直面用户,需要从用户体验角度出发,对性能和体验有更多的追求。导致在接口层面常常出现一些设计上的矛盾。进而演化出 B ackend F or Frontend(BFF)等概念,恰好 Node.js 越来越成熟,因此前端渐渐接手了服务端 controller 层的工作。

在蚂蚁内部,有较为成熟的 Chair 框架来支持前端完成 BFF 开发工作,且随着 BFF 越来越得心应手,也出现了一些快速迭代的创新性业务直接由前端同学通过 Chair 来完成。但是,Chair 应用作为一种标准的应用类型,对于前端同学来说,仍然有不小的使用成本。

首先,由于蚂蚁架构的复杂性,在上手开发前,需要对整体架构有一定的了解,导致服务端应用开发有不少的学习成本和上手门槛。

其次,服务端应用需要占用 CPU 和内存等资源,无法做到像前端应用一样随意申请,因此会有申请门槛和复杂的申请流程。进而导致一些团队将多个服务合并到一个 Chair 应用中,出现了不少的巨石应用。不仅仅开发体验变得很差,迭代和发布流程也变得冗长。另外,对于一些长尾应用,因业务调整,长期缺乏维护后,既没人愿意改动其中的逻辑,更没人敢下线应用,造成了不少的资源浪费。

最复杂的是,应用发布后,还需要随着业务水位变化,及时进行服务器扩缩容等运维操作。

这对于大多数业务开发同学来说,就显得有点太过于复杂了。因为从业务开发视角来看,最重要的是进行业务代码的编写,而现在却需要关注非常多额外的事情,正所谓"想要喝水,还得先学会挖井"。

既然标准应用有如此多的不便,是否有更好的替代方案呢?这时候,我们就自然而然的想到了 serverless。

🔍 Serverless 的探索

由于一些架构复杂性等原因,我们没法直接使用现有的一些 serverless 服务,需要走一条自建的道路,因此我们开启了对 serverless 的探索之旅。

考虑这些问题症结在于标准应用架构的复杂,因此我们首先想到的是对应用进行更细粒度的拆分,将应用拆分为应用基座及函数模块。应用基座交由框架开发者来统一维护,配合底层运行平台来进行伸缩容等运维。这样,就对开发者屏蔽了底层运行环境和运维逻辑,让开发者只需要感知到函数模块,可以做到专注于业务开发,而无需关心额外的部署等问题。

这个方案虽然很好的解决了开发者层面的问题,但归根到底仍然是一种标准应用模式,因此没有实现真正意义上的 serverless,导致仍然存在不少的问题。

一方面,函数仍然以 server 应用模式运行,在启动时需要进行端口监听、服务注册等逻辑,因此启动速度仍然很慢。为了保证性能,我们不得不作出妥协,提前进行一些容器预热操作,以期通过空间来换时间。这就造成函数运行,需要保持一定数量容器持久运行,无法做到资源缩 0,存在不小的资源消耗。

另一方面,标准应用为微服务架构,没有统一的流量入口,因此 1 - N 的弹性伸缩依赖监控数据来进行决策,而监控数据存在一定的延迟,加上决策时间,从流量洪峰到决策出需要扩容有 10s 左右的延迟。再加上,新增节点通过服务发现被其他应用感知也有耗时,操作延迟进一步增加。导致弹性伸缩反应相对较慢。

总结来说,通过应用架构模式来实现 serverless,仅得到了 serverless 的"皮",没有得到 serverless 的"心"。

☁️ 小程序云 Serverless

随着小程序以其即扫即用的特性,为用户提供了无需下载、无需安装、即刻享用的便利体验,而逐渐成为我们日常生活中不可或缺的一部分。如何支持小程序开发者低门槛、高效和低成本地进行开发,也成为我们重点关注的目标。

此时,serverless 技术就提供了一个比较理想的解决方案,可以极大地降低后端的研发门槛。开发者可以只关注业务逻辑,而无需关心运行环境和服务器等底层资源,减轻了运维负担。同时自动弹性伸缩特性,能够有效应对流量的波动,保证小程序的稳定运行,还能避免资源闲置的浪费,帮助开发者有效地降低成本。

借此契机,我们在小程序云场景下,对我们的 serverless 架构进行了升级,期望实现正真意义的 serverless。

🧱 整体架构

我们期望实现函数实例 0 - 1 - N 的自动且高效的伸缩容。因此我们将函数的流量入口统一收口到函数网关,这样函数网关能够实时感知到函数流量变化,通过函数流量、规格等配置信息从函数调度器获取函数实例。函数调度器则依据函数并发度情况,决策是否需要新增或复用实例。当函数网关获取到函数实例后,将请求分发给对应函数实例进行处理,实现高效的 1 - N 伸缩。待一定时间无流量访问后,函数调度器将回收函数实例,实现函数实例的缩 0。

🚀 极速冷启动性能

这个架构带来的一大挑战,即是如何实现运行时的极速冷启动性能。根据之前的经验,函数运行时如果以 server 架构来启动,是无法满足需求的。与之不同的是,client 端启动逻辑则相对轻量很多,启动速度也很快。因此,我们抛弃了传统的 server 架构,将运行时改为了 client 模式。在部署单元上,新增一个节点网关来暂存函数请求。当函数实例被调度运行后,函数运行时将以 client 的身份启动,再从节点网关拉取请求来进行处理。

虽然改为 client 模式后,减少了端口监听、服务注册等逻辑,让启动速度有明显的提升,但启动速度仍然在秒级,距离我们的目标仍然有不小的差距。所以我们需要进一步对启动逻辑进行优化。

这里,我们又想到了预热的方案。不过不同于之前需要针对每个函数都进行预热的方案,我们将函数拆分为了运行时和业务函数两个部份。对于运行时部份,其实不同函数间都是相同的。我们可以针对此部份进行统一预热,这样预热的容器是可以通用的,可以服务于所有的函数,就可以较小的资源消耗来提升所有函数的启动性能。

因此,我们在函数实际运行前,会提前启动函数运行时进程,并通过系统调用将进程阻塞住,保存为种子进程。待真实函数调用请求进来后,再从种子进程直接 fork 出一个函数运行进程,并将业务函数代码挂载上去,以形成一个完整的函数实例,来处理请求。这样的话,当实际处理请求时,函数实例并不是从 0 开始冷启动的,而是从已经完成运行时初始化的状态开始执行的。由此,函数的启动性能得到了极大的提升。

对于 Node.js 运行时的适配来说,我们将尽可能多的初始化逻辑放到了制作种子进程阶段。这样实际运行阶段,只需要运行很少的逻辑,即可开始运行业务函数处理逻辑。

经过这些架构和逻辑的调整和优化,目前基线函数,以函数网关统计的 e2e 冷启动运行耗时提升到了 50ms 左右,热启动耗时在 9ms 左右,和原来标准应用相比,所付出的额外耗时已经相对很小的。

🍭 函数代码启动优化

虽然我们基线函数的启动性能已经非常可观了,但真实函数运行不可能都是这么简单的逻辑,随着业务函数代码复杂度的提高,业务函数代码启动耗时必定会成为瓶颈。

我们首先想到的方案是从 Node.js 层面进行优化。众所周知,js 代码为解释型语言,在实际运行前,会经过模块寻址和编译等逻辑。经过测试,这部份的耗时占了不少的初始化时间。而这部份其实可以通过 V8 提供的代码编译缓存来解决。我们在构建阶段,对业务函数代码进行预加载,并将 js 的模块寻址以及代码编译结果通过 Node.js 提供的 Script.createCachedData 接口导出并缓存下来。等函数启动时,不用再重复进行解析,直接从缓存中进行恢复。经过此优化,启动速度能提升约 20% 左右。

Node.js 缓存方案虽然能提升代码加载性能,但运行耗时没有特别好的办法进行优化。针对业务函数部份,是否可以有和函数运行时一样的方案,既能进行提前预热,也不用付出太多的资源消耗呢。

目前,我们正在进行此部份的优化工作。我们考虑从操作系统层面,通过进程快照技术来解决。我们准备一套和运行环境相同的构建环境,在函数代码构建阶段,来直接运行业务函数,并将启动后的函数进程的运行状态以及堆栈等数据通过进程快照保存下来。这样,在实际运行函数实例时,从进程快照中将函数进程恢复出来,即可直接处理请求调用。这个方案仅需要付出一定的磁盘资源开销,即可让函数冷启动实现近乎于热启动的效果。可以实现一个完全意义上的极速启动性能。

🌰 实践案例

🌊 Server Side Render

支付宝内有许多在线 H5 页面,例如花呗、借呗、保险、双十一/双十二大促、红包码、五福等,这些页面的打开速度对于支付宝整体的产品体验和业务转化率有着极大影响。因此,提升支付宝内在线 H5 页面的首屏打开速度,对于基础平台和业务而言,都至关重要。业界普遍采用 SSR 方案,可以极好的解决中低端机型 CPU 资源有限的问题,极大的提升用户体验,尤其是在低端手机场景下,差距非常明显。现在,我们也有了真正意义上的 serverless,因此我们通过函数来实现 SSR 逻辑。在访问页面时,通过 CDN 回源到函数,在函数中渲染首屏内容。

最后的效果也非常符合预期,在车险业务中,低端机情况下,肉眼可见首屏耗时降低了一半以上。

💨 Chair Function

基于新的 serverless 架构,我们对内部使用的 Chair 框架进行了改造,提供了 Chair 函数应用研发流程。按照模块拆分为不同的函数应用,再以接口维度,发布为一个个函数,既解决了巨石应用的问题,也提升了发布效率。最快仅需要 1 分钟,即可完成一个接口的上线流程。

🙏 参考资料

本文来自 CCF TF 128 分享

相关推荐
秋月华星1 小时前
【flutter】TextField输入框工具栏文本为英文解决(不用安装插件版本
前端·javascript·flutter
千里码aicood1 小时前
[含文档+PPT+源码等]精品基于Python实现的校园小助手小程序的设计与实现
开发语言·前端·python
青红光硫化黑2 小时前
React基础之React.memo
前端·javascript·react.js
大麦大麦2 小时前
深入剖析 Sass:从基础到进阶的 CSS 预处理器应用指南
开发语言·前端·css·面试·rust·uni-app·sass
m0_616188493 小时前
Vue3 中 Computed 用法
前端·javascript·vue.js
六个点4 小时前
图片懒加载与预加载的实现
前端·javascript·面试
Patrick_Wilson4 小时前
🔥【全网首篇】30分钟带你从0到1搭建基于Lynx的跨端开发环境
前端·react.js·前端框架
Moment4 小时前
前端 社招 面筋分享:前端两年都问些啥 ❓️❓️❓️
前端·javascript·面试
Moment4 小时前
一坤时学习 TS 中的装饰器,让你写 NestJS 不再手软 😏😏😏
前端·javascript·面试
子洋4 小时前
AnythingLLM + SearXNG 实现私有搜索引擎代理
前端·人工智能·后端