继 2019 年开源 Midway 框架之后,阿里一直在 Node.js 的前沿进行深度研究,除了加入 TC39 参与标准化建设,向上游 Node.js 项目持续贡献,与龙蜥社区合作优化之外,也在 Serverless 领域有了不小的成果。
今天,向大家介绍我们最新的面向云原生场景,面向 Serverless 架构下的新产品, 代号  Noslate。

Noslate 是什么?

欢迎访问项目了解更多内容:https://github.com/noslate-project/noslate
JavaScript 是开发者数量最庞大的编程语言,早些年 Node.js 等技术的出现,让 JavaScript 可以轻松地处理各类服务端任务。
但随着云原生/Serverless 等新架构概念的引导,弹性效率成为了全新的架构设计目标。为了能够让 JavaScript 任务拥有更高的弹性效率,进而满足在泛终端、全栈交付等领域的效率期待。我们逐步深入探索的过程中逐渐形成了 Noslate Project,旨在提升云原生场景下 JavaScript 的被调度性能,解决诊断性黑盒问题。
Noslate 它主要由三个子项目组成,分别体现了我们在提升 Javascript 任务弹性效率过程中碰到的问题以及解决方式:
  1. Node.js Distribution:初期针对函数计算冷启动场景优化,降低 Node.js 用户代码加载耗时,形成了针对性的 Node.js 发行版本。
  2. Noslate Workers:随着探索的深入,我们设计了面向轻量端云同构场景的 W3C Web-interoperable JavaScript 轻量化容器方案。在交付灵活度上和资源、执行效率上形成平衡,现在主要应用于中心化的 SSR 渲染等轻量任务场景,效果显著。
  3. Noslate Debugger:在落地业务过程中,我们发现在弹性效率提升后,对于异常和崩溃变得难以定位,得益于 Linux 系统 Coredump 机制的启发,我们设计了基于 Corefile 对问题进行离线诊断的 Noslate Debugger 产品,帮助用户实现问题的回溯和实时定位。
简而言之,Noslate 目标是通过提供完整的技术产品方案, 将 JavaScript 打造成云原生时代最灵活的交付语言。

为什么开源?

一方面我们希望通过开源加强项目产品化程度;另一方面希望在社区中吸收更多的实践场景进而继续完善产品设计,也欢迎大家参与到项目中来。
同时,依托阿里云龙蜥社区和 Anolis 操作系统的合作关系,我们得以在底层探索,实现技术的演进。

一、Noslate Workers

W3C Web-interoperable 运行时 Aworker,提供了一个轻量,近乎 0 冷启动的 JavaScript Serverless 运行环境。通过它,可以轻松的在已有的架构中集成轻量化类 Serverless 的能力。
与传统的 FaaS 架构不同是,这是一个在普通应用容器之上的轻量任务单位。得益于良好的动态任务高密度混部和隔离特性、以及基于任务状态拷贝 API 带来的近乎 0 冷启动特性,可以实现任务的即用即启与即停即抛,进而无需关心在整个大集群中任务节点的编排问题。
与既有架构的关系:
Noslate Workers 由两个主要组件组成:
  1. Aworker - 轻量、Web-interoperable JavaScript Runtime
  2. Noslated - Servlerss 化的 Aworker 调度管控实现

关于 Aworker

提供 Web API 标准的 Web-interoperable JavaScript 运行时,适合不直接依赖系统接口的业务逻辑部署。Aworker 实现了近似 Service Worker API 的规范,提供了基本的 Request-Response 服务 API。
因为提供了相比于 Node.js 的 API 更加高层次、抽象的定义,不会泄漏系统底层状态,Aworker 通过 Startup Snapshot 和 Warmfork 能力, 实现了更快的水平及垂直扩容,能够在毫秒级启动并处理流量,具备更高的弹性效率。

亮点特性一:Warmfork

熟悉 Linux 系统编程的同学都知道 fork(2) 系统调用有几个优势:
  1. 新进程可以继承母进程的当前状态,而无需从 main() 开始初始化;
  2. pcb、栈、内存页,页表都是纯内存复制,所以进程创建快 (<1ms);
  3. CopyOnWrite,新进程可以继承母进程的静态页表,可节省系统内存;
对于 Node.js 来说,因为其无法在主线程持有所有多线程的状态 (如锁,信号量等),所以 Node.js 进行 fork 的修改难度很大。其多线程设计主要 来源于 libuv 库和 V8 Platform Worker 线程:
  1. 因部分 IO 操作存在同步调用,如 dns,文件读写等,所以 libuv 使用 IO 线程将同步操作转换成异步操作;
  2. Node.js 的 V8 默认配置为多线程 GC、Background Compilation/Optimization 的方式;
Node.js 的单进程多线程模型可以由下图表示:
Aworker 的设计是采用单进程单线程的模型,也就是将上述模型中的 worker thread 单独抽出放到一独立进程中。Worker 因此可支持 fork,从而避免从 main() 开始的启动消耗,达到快速启动的目的。
为了支持单线程,Aworker 还做了如下修改:
  1. 使用了 Linux AIO 特性替掉了 libuv 中同步的文件系统操作(不是 POSIX AIO,两者有区别。Posix AIO 类似于 libuv 现有的实现);
  2. 使用 V8 的 SingleThread 模式,这是一个给低端设备(Low-end devices)实现的能力,不过非常符合 Serverless 资源模型;
而为了管理、隔离这些工作进程,我们需要一个轻量的业务进程容器管理组件 Turf ,该组件用于能通过 Warmfork 方式创建新的 Aworker 服务进程,并能提供一定的资源、环境的隔离能力,同时兼容 OCI。区别于传统 runc, rund 的容器,turf 旨在承载如 Aworker 这类轻 JS Runtime,它无需镜像运行,开销更低,可以支持更高的部署密度。
Alinode Warmfork 具体的对比:
提供 "被复制" 的进程,称为 "种子进程",其他服务进程都是该进程的克隆。譬如 Aworker 作为种子进程,它需要确定自己一个 "可被克隆" 的时间点,将自己的所在状态(内存)作为克隆进程的初始状态。
Warmfork 的系统时序如下:

亮点特性二:Startup Snapshot

Warmfork 能解决了单机上服务进程的快速启动,对于冷机启动需要采用 Startup Snapshot 方案。Startup Snapshot 和 CodeCache 的区别在于 Startup Snapshot 能够保存用户代码逻辑执行状态,而 CodeCache 只保存代码解析结果、仍然需要重新执行 用户代码逻辑。
设计上,Startup Snapshot 可提供携带用户代码逻辑的快速恢复,但是也有局限性:
  1. Startup Snapshot 对内存开销敏感,如果应用启动阶段用了大量内存,可能造成负优化;
  2. 用户代码启动需要没有歧义的状态,比如 IP 地址、日期、连接状态、服务发现结果等,针对这些歧义内容用户代码需要在进程恢复时有修正能力;
V8 的 Startup Snapshot Serializer 就是一个类似于 GC 的对象遍历器。这个遍历器通过遍历加入到 Snapshot 中的 Root 对象,遍历它所对应的对象图并按照对象关系生成一系列的反序列化指令。
Startup Snapshot 相当于从 V8 Context 对象与它的 globalThis 开始,遍历堆中所有的对象并将对象关系与引用序列化成 特有的字节码,形成一个线性的可存储状态。并在恢复时,解释执行这些字节码,恢复堆中的对象内容与他们之间的引用关系。
上述的两类和被调度性能相关的特性被统一归类为状态拷贝 API,具体使用可以参考官网文档中的《状态拷贝 API》章节[1],详细介绍了命令行参数和程序内的 Events。https://noslate.midwayjs.org/docs/noslate_workers/aworker/serialize-api/

Noslated

Noslate Container Deamon,作为 Noslate Workers 解决方案的核心管控程序,提供了实例调度、弹性扩缩容、配置管理、流量管理等能力。
基于健壮性考虑,它由两个角色组成:控制面(Control Plane)、数据面(Data Plane)
Noslated 对于实例的管控主要有三个模式:
  1. 基础模式 - 基于流量的扩缩容
  2. 即抛模式 - 运行完即销毁
  3. 预留模式 - 面向历史场景兼容,在此不额外展开,详情可以查阅官网【预留策略】。

1、基础模式

当流量进入 Data Plane 后,如果没有能够处理请求的 Worker 实例,会通过 requestQueueing 事件通知 Control Plane,它会根据当前水位决定扩容数量,如果当前已无法创建 Worker 实例,会返回资源上限报错。新的 Worker 实例启动后,会自动连接到 Data Plane,Data Plane 发现新的 Worker 实例连接后会主动触发初始化请求,初始化成功后开始消费请求队列里堆积的请求。
当 Worker 实例闲置一段时间后,Control Plane 会主动发起 GC 操作,告知 Data Plane 关闭流量,流量关闭后,Control Plane 会通知 Turf 关闭 Worker 实例,清理资源残留。

2、即抛模式

针对特定的灵活场景,一次性的轻量用户脚本执行(比如特别高密度的混部执行二方任务如 SSR),为了隔离不同请求间的上下文,可以针对每个请求创建一个实例,并在执行后销毁。
比如普通的 Node.js 实例带上业务逻辑启动一般都不会太快,如果直接用了响应用户流量会难以接受。得益于 Aworker 运行时并配合 Warmfork [2]以及 Startup Snapshot [3]能力,更快完成 Worker 实例启动。亦可把一部分业务自己的初始化逻辑放置到 Warmfrok 特性中,进而让新实例都是最少的初始化时间,这才让高密度混部二方任务成为可能。

3、预留模式

在此不额外展开,详情可以查阅官网【预留策略】[4]

二、Noslate Debugger

Noslate Debugger 是针对 V8 应用的离线分析工具,它可以分析 Node.js 等应用程序产生的 Corefile (Core 文件):
  1. 检查 Node.js/V8 应用程序的结构体、堆栈等内容
  2. 检查 V8 堆内的各种对象信息
  3. 从 Corefile 中导出 Heap Snapshot
  4. 业务无感获取 Corefile (通过 Arthur 工具)
  5. 已支持 Node.js / AWorker LTS 官方发行版
为了更好的解决问题而不是造轮子,在未来的几个月 Noslate Debugger 也会和国内社区 Node.js 稳定性领域优秀的开源软件 Easy Monitor 共建整合,在 Node.js/V8 的问题诊断领域形成合力,也是值得期待的事情。
优点一:基于 Corefile 的 "快照" 更适应 Serverless
Serverless 应用通常会使用大量生命周期短、规格小的任务实例,但在此类任务实例上获得调试诊断能力并不容易,这使得 Serverless 应用长期处于较为黑盒的窘境。比如,Inspector 需要稳定和长时的网络连接、运行时 Heap Snapshot 需要较多的计算和内存资源,都是和 Serverless 架构背道而驰的。
不管是 V8 的对象还是堆快照,它都是 "信息" 在内存中的存储,而 Inspector 功能就是可以在 "运行时" 能提取这些信息。Noslate Debugger 通过 Corefile 将这部分调试诊断能力转移到了离线时进行,让原有实时性要求高的在线诊断调试转化为只需简单文件上传即可集成使用。
在用户本地或云端服务上提供接近用户本地开发时的调试诊断体感:
Corefile (特指 GNU Corefile 格式) 主要记录的是 Node.js 进程的内存和寄存器转储(CoreDump: 内存到磁盘的过程)。所以它也是进程完整“信息”,被用于 Linux 系统应用 Crash(有损) 的调试载体,也可用于 GCore(无损) 产生进程快照用于离线分析。
优点二:更小的业务影响
对比原有线上 "堆快照" 对业务的影响长达数分钟,到只影响业务 RT 秒级(通过 GCore),甚至只有几十毫秒 (通过 Arthur 工具)。Corefile 快照也不会有任何运行时的"添油加醋",所以它也适合那些还未被GC的对象定位,譬如诊断已经结束了的业务处理等。
Arthur 是 Noslate Debugger 中用于低影响获取 core文件的工具,利用 fork 减少进程暂停时间,LZ4 压缩减少转储体积。带业务流量的线上环境抓取,业务影响 31.106 毫秒,Corefile 大小为 338 MB (进程原使用 1.44GB 物理内存)。

三、Node.js 发行版

我们还对 Node.js 的实例进行了定向弹性场景的优化,提高了用户代码的加载速度,从而降低了冷启动时间。主要包括 Require 关系加速、Bytecode Cache,优化效果提升可高达 100% ~ 200%。该发行版,同时包含来自阿里云基础软件团队在 ARM 架构的性能优化特性。

冷启动优化

PGO(Profile Guided Optimization),是一种根据运行时 Profiling Data 来进行编译优化的技术,这里我们借鉴了这一概念。主要是通过执行一遍之后收集启动阶段的热点数据生成缓存文件,后续通过内存映射直接加载高效的缓存文件,即可获得提升在 100% ~ 200% 的用户代码冷启动优化效果。

面向特定平台架构优化

Node.js 支持包括 x64、arm64 等在内的多种架构。但针对 ARM 芯片的快速发展,上游版本往往仅提供基础适配,缺少针对新指令集的优化,导致在 ARM 芯片上无法获得潜在的性能提升。当下主流云厂商大都提供了 ARM 架构、高性价比的运行环境。Noslate Node.js 发行版针对 ARM 等平台的优化可以让应用在这些架构上获得更高的性能和效率。目前 Noslate Node.js 发行版已经在进行针对阿里云 Ampere、阿里云倚天的定制优化,未来计划包括支持龙蜥社区中的其他架构。主要包括:zlib 特性优化、其他一些利用 SIMD 的性能提升都在 PR 合并和准别中。

详细了解

上面是对 Noslate Project 的简单介绍,如果想要详细了解可通过下述方式:
  • GitHub: https://github.com/noslate-project/noslate
  • 网站:https://noslate.midwayjs.org/
  • 龙蜥社区 SIG(特殊兴趣小组,有钉钉群):https://openanolis.cn/sig/web-platform
  • 邮件列表:[email protected]

致谢

感谢阿里巴巴集团内业务方的支持,同时还要特别感谢所有给本项目贡献过代码、一起探索过技术方向伙伴们(包括不限于 legendecas、mariodu、zhaolei0505、XadillaX、umuoy1、oraluben、hyj1991 等)。

参考资料

[1]
官网文档中《状态拷贝 API》章节: https://noslate.midwayjs.org/docs/noslate_workers/aworker/serialize-api/
[2]
Warmfork: https://noslate.midwayjs.org/docs/noslate_workers/aworker/intro/#warmfork
[3]
Startup Snapshot: https://noslate.midwayjs.org/docs/noslate_workers/aworker/intro/#startup-snapshot
[4]
预留策略: https://noslate.midwayjs.org/docs/noslate_workers/references/scale/#%E4%B8%89%E9%A2%84%E7%95%99%E7%AD%96%E7%95%A5
继续阅读
阅读原文