第十二章 WebAssembly 在 ByteFaaS 中的应用

2,228 阅读27分钟

作者:彭璟文、殷智慧

1. 前言

近年来,无论是 Serverless 还是 WebAssembly,都越来越受到开发者的广泛关注,作为字节跳动内部的函数计算平台,ByteFaaS 在 WebAssembly 方向上也有着不少的探索和实践。我们利用 WebAssembly 技术构建出了极致轻量化的函数运行时,并辅以全新设计的精简架构,打造出了云边一体的 Serverless 解决方案,拓展了 FaaS 的边界和应用场景,为业务带去了更多的可能。

本文将先带领大家认识 ByteFaaS 平台,分别介绍经典 FaaS 和 FaaS Worker(轻量级函数)方向。随后通过 WebAssembly 运行时、精简架构、开发者支持三个方面,详细介绍 FaaS Worker 的设计与实现。最后在文章结尾,会对 WebAssembly 函数能够为用户所带来的收益进行大致总结。

2. ByteFaaS 平台介绍

ByteFaaS 是字节跳动内部的函数计算平台,遵循事件驱动和服务函数化的理念,在整体产品形态上与公有云厂商的 FaaS 产品相近,提供了包括服务函数化、自动扩缩容、多种事件源支持、多种开发语言支持、服务高可用等平台能力。ByteFaaS 平台为用户屏蔽了资源和运维细节,并提供日志查询、性能监控和报警等功能,极大降低了开发者的开发运维成本。截止 2022 年中旬,ByteFaaS 平台上线函数 10w+,日均活跃函数 1.9w+,日均发布函数次数 7000+,在线流量高峰约 70w QPS,消息触发器流量高峰 1.2 亿 QPS,调用量和计算资源规模在业界处于全球领先水平。有关 ByteFaaS 函数计算平台的更多信息,可参考 ByteFaaS 函数计算团队出品的图书《Serverless 核心技术和大规模实践》[1]。

在函数运行时和数据面架构方面,ByteFaaS 大致包含两大类,一类是基于容器或虚拟机隔离技术的,我们称作经典 FaaS,另一类则是采用进程内隔离方式的,如 WebAssembly,称作轻量级函数。

2.1 经典 FaaS

通常在函数计算平台中,每个函数实例是一个虚拟机或者容器,函数进程运行在函数实例中,与此同时数据面架构也围绕着虚拟机和容器这两种隔离技术设计。经典 FaaS 运行时的优势是符合用户一直以来的开发思维,并且在运行时层面几乎没有限制,在其他环境(例如在物理机、PaaS 平台上)可以执行的代码,稍加改动甚至不做修改,就可以移植到 FaaS 平台上运行,整体迁移的成本比较低。尤其是在 ByteFaaS 推出了 FaaS Native 方案后,现有应用迁移至 FaaS 的成本可以忽略不计。

不过经典 FaaS 运行时的劣势也比较明显,主要集中在如下几个方面。

  1. 函数实例的启动时间相对比较长,虽然通过预热或快照等方式可以大幅度降低冷启动时延,达到百毫秒级别,但对于时延敏感的应用,依然需要通过预留实例等手段尽量绕过冷启动,用资源换取时间。

  2. 单个函数实例需要独占一个虚拟机或容器实例,资源开销相对较高,进而导致成本较高。尤其是对于那些代码简单且流量小的函数,预留实例会造成资源浪费,不预留实例则会导致函数调用时延抖动。

  3. 实例扩容可能带来额外的函数调用时延开销,突发流量下的函数时延不稳定。

  4. 对于边缘等资源非常受限的场景,有限的资源总量无法支撑平台使用虚拟机或者容器运行函数实例。

2.2 轻量级函数

随着经典 FaaS 在实际业务中的落地实践增多,单一架构和技术路线无法很好地支撑所有应用场景,FaaS 需要针对业务特点进行定向优化,给出特定的解决方案。针对经典 FaaS 的固有问题,ByteFaaS 尝试采用进程内隔离的方式来解决,构建出了冷启动速度极快且资源开销极低的函数执行方案,称其为轻量级函数。下图对比了虚拟机隔离、容器隔离和进程内隔离这三类隔离方式。

12-1.png

图 1. 虚拟机隔离、容器隔离和进程内隔离对比

虚拟机和容器是经过长期生产验证的、非常优秀的隔离技术,它们兼容性好,可以支持几乎任何原生二进制可执行程序,但是由于隔离是在虚拟机级别和进程级别,函数实例中除了用户代码外,有相当一部分内容是重复的,操作系统和运行时部分被复制了多次,也因此带来了资源开销高和启动速度慢的问题。

而采用进程内隔离时,用户代码可以运行在同一个函数运行时进程内,共享函数运行时,极大程度地复用了函数实例的公共部分。正是由于进程内隔离方案将函数公共部分最大限度地从函数实例中剥离出去,使得函数实例变得极度精简,从而能够实现亚毫秒级别的函数冷启动速度以及低至数百 KB 的函数实例内存开销,单个函数运行时进程内可同时运行成百上千个函数实例。

ByteFaaS 采用 WebAssembly 技术打造了上述的轻量级函数运行时,也为轻量级函数运行时设计了新的轻量级数据面架构,实现了极致短的冷启动时间,函数高密部署,极致成本优化,并能有效应对流量突增陡降。同时也可将 FaaS 带去边缘等资源受限的应用场景。我们将 ByteFaaS 的轻量级函数运行时和架构统称为 ByteFaaS Worker。

3. WebAssembly 函数运行时

起初 WebAssembly 诞生于浏览器环境,作为 JavaScript 的补充,尝试解决大型 Web 应用执行速度慢的问题。后来随着 WebAssembly 标准的演进以及各种 Standalone WebAssembly Runtime 的开源,WebAssembly 也开始在服务端场景中崭露头角。ByteFaaS 利用 WebAssembly 技术实现了一种采用进程内隔离机制的轻量级函数运行时,将函数实例冷启动时间缩短至亚毫秒级,并提供了多种常用编程语言的 SDK,降低用户的学习接入成本,辅助用户快速开发 WebAssembly 函数。

3.1 WebAssembly 简介

WebAssembly 是一种可运行在现代网络浏览器中的新型代码,它的设计目的不是为了发明一种新的编程语言,而是为如 C、C++ 和 Rust 等语言提供一个高效的编译目标。它在安全的、可移植、轻量化、高效率的虚拟机沙箱中执行,并且可以在不同平台上实现接近本地的运行速度。如下图,各种语言的代码通过编译器编译成 WebAssembly 模块,编译好的 WebAssembly 模块可以在各平台上的 WebAssembly 虚拟机中运行。

12-2.png

图 2. WebAssembly 支持多语言多架构

WebAssembly 天然的轻量、安全、快速、可移植等特性与 FaaS 的需求非常契合,可以帮助 FaaS 实现极致的轻量化和极致的冷启动速度。而对于 ByteFaaS 用户而言,采用一种可以编译至 WebAssembly 的语言编写函数代码即可,不会引入过多的学习成本,理论上所有基于 LLVM 架构的高级语言都可以编译到 WebAssembly。

3.2 Hostcall + WASI

业界通常把执行 WebAssembly 字节码的沙箱环境称作 WebAssembly VM,运行 WebAssembly VM 的进程被称为 Host 进程,VM 中执行的代码被称作 Guest 程序。由于 WebAssembly 本身只是一种可执行格式,Guest 程序自身只能完成纯计算操作,想要拓展 Guest 的能力范围,就需要 Host 提供相应的接口出来供 Guest 调用,这些接口统称为 Hostcall。

虽然 WebAssembly 最初诞生于浏览器环境,但是 WebAssembly 技术有着上述简介中的执行快速、安全等优点,渐渐地开发者尝试将它运用在非 Web 环境。而在非 Web 环境中应用 WebAssembly 就需要解决其和外界交互的问题,此时 WASI 就应运而生了。WASI 为 WebAssembly 程序用 Hostcall 的方式提供了一套系统 API,封装了诸多例如读写文件、读取环境变量、写标准输出等接口,并且支持运行时对 WASI 接口进行严格的权限控制,相当于为 WebAssembly 提供了类似 Linux Syscall 的接口能力。

例如,想要在 WebAssembly 程序中读取环境变量,则可以调用 envrion_get() 和 environ_sizes_get() 接口从 Host 侧读取到当前执行环境的环境变量信息,具体如下:

// 读取环境变量的值
// 缓存空间的大小需要和 `environ_sizes_get` 返回的大小对齐
pub fn environ_get(arg0: i32, arg1: i32) -> i32;

// 返回对应环境变量的数据长度
pub fn environ_sizes_get(arg0: i32, arg1: i32) -> i32;

通常情况下,WASI 接口会被用户所使用语言的标准库所调用,在开发者编写代码时,使用语言标准库中相对应的接口即可,无需关系其背后所调用到的 Hostcall 接口。例如,使用 Rust 语言开发 WebAssembly 程序时,使用 std::env 包中的环境变量 API 即可实现环境变量的读取,具体如下:

use std::env;

let key = "HOME";
match env::var(key) {
    Ok(val) => println!("{}: {:?}", key, val),
    Err(e) => println!("couldn't interpret {}: {}", key, e),
}

在 ByteFaaS 平台上,WebAssembly 函数运行时支持标准的 WASI ABI,用户可使用任意语言编写代码,编译出符合 WASI 规范的 WebAssembly Module,即可在 FaaS 平台上运行。

3.3 运行时能力拓展

由于业务的复杂性,仅有 WASI 接口是不够的。比如仅仅基于 WASI 较难复用传统函数下常用的一些操作,比如在函数内发起 HTTP / RPC 请求、连接并操作 DB 或消费 MQ 事件等。在这个背景下,ByteFaaS 与服务框架团队合作,参考 io_uring 的实现在 WebAssembly 内部定义了一套通用的异步 Hostcall 框架 BytedRing,并基于这套框架在 Host 侧注入各种上层实现,从而轻松地拓展了运行时能力。

BytedRing Hostcall 框架提供了 4 个核心接口,定义如下:

// 提交一个 hostcall event
fn submit(event_id: i32, service_provider_id: i32, data_ptr: i32, data_len: i32)

// 非阻塞查询某个 hostcall event 返回状态
fn poll(event_id_ptr: i32, size_ptr: i32)

// 阻塞等待某个 hostcall event 返回
fn wait(event_id_ptr: i32, size_ptr: i32)

// 读取 id=event_id 的 hostcall event 返回的 payload
fn read(event_id: i32, service_provider_id: i32, data_ptr: i32)

系统架构示意如下图所示:

12-3.png

图 3. BytedRing 交互流程设计

当然,开发者一般情况下不需要直接操作这些基础的 Hostcall,ByteFaaS Worker 执行环境在此基础上封装了公司内部开发需求常用的能力,包括 HTTP Client/Server、TTHeader Client/Server等等,开发者只需要在项目内引入对应语言的 SDK 即可轻松调用相关 Hostcall,具体用法可以参考开发者支持章节。

除上述基础能力之外,目前 ByteFaaS 也在与 Mesh 团队共建设计 ByteRuntime。与 Dapr 提供的能力类似,ByteRuntime 与公司内部自研组件相结合,支持以 Mesh Sidecar 的形式为函数运行时提供更加丰富的能力,包括资源绑定、状态存储、API 配置等。

3.4 函数开发 SDK

WebAssembly 函数运行时通过拓展 Hostcall 接口的方式丰富了运行时能力,但这些接口并不适合直接向用户暴露。如果直接向用户提供 Hostcall 接口,一来给用户增加了额外的学习成本,难以上手,二来由于 WebAssembly Hostcall 接口只支持传递数字,会涉及大量的指针传递操作,使用起来非常复杂,开发体验极差。为了方便用户使用,ByteFaaS 为常见的开发语言提供了相应的 SDK,如下图所示,用户只需选择某个自己熟悉的语言,使用简单的编程接口编写自己的函数。目前 WebAssembly 函数已支持 5 种常用语言,Rust、Go、JavaScript、AssemblyScript、C++。

12-4.png

图 4. ByteFaaS WebAssembly Hostcall 交互流程

关于 SDK 的使用会在本文后续的开发者支持章节详细展开。

4. 精简架构

由于现有的经典 FaaS 架构相对比较复杂,调用链路长,不利于发挥轻量级函数运行时的优势,因此 ByteFaaS 设计了一套精简架构,以实现极致的启动速度和极低的资源开销,支持在中心机房、汇聚机房和边缘机房部署。

4.1 请求路径

经典 FaaS 的请求路径相对较复杂,请求可能需要经过多个系统组件处理,会增加额外的时间开销。如下图,一个典型的冷启动请求需要经历以下几个阶段:

  1. 请求到达 FaaS Gateway

  2. FaaS Gateway 向 Dispatcher 组件获取函数实例信息,Dispatcher 组件负责管理所有函数实例

  3. Dispatcher 组件发现函数实例不存在时,向 Worker Manager 组件获取新的函数实例

  4. Worker Manager 从资源池中获取空闲函数实例,触发代码下载,初始化函数实例

  5. 返回新启动函数实例信息给 Dispatcher,再返回给 Gateway

  6. Gateway 访问新启动的函数实例

12-5.png

图 5. 经典 FaaS 函数请求路径

而在精简架构中,由于数据面被大幅度简化,仅保留了关键组件,最大程度地降低架构复杂度,同时也能最大限度地减少数据面路径给请求带来的时间开销。如下图所示,在精简架构数据面中,用户请求首先到达 Gateway,Gateway负责请求调度并直接将请求发送给相应的函数运行时进程,触发相应函数的执行,整个函数调用几乎没有额外的系统内部开销。

12-6.png

图 6. FaaS Worker 函数请求路径

4.2 流量调度

常见的流量调度策略包括轮询调度(Round Robin Upstream)、随机调度(Random Upstream)、带附加权重(Weighted Round Robin)的轮询调度等。

通过设计合理的调度策略系统,可以实现在保证稳定性的同时提升资源利用率,具体如:

  • 在函数稳态的前提下尽可能复用已有实例,减少不必要的实例冷启;

  • 提升 Worker 实例资源利用率,在 Worker 实例健康的前提下尽可能多的承载函数请求;

  • 在 QPS 激增时能够快速扩容,减小/杜绝因此导致的时延上升、请求超时的问题;

FaaS Worker 整体采取了带闭环反馈的 Weighted Round Robin 策略,为每个租户函数维护加权路由表,并且根据系统链路监的指标实时调整加权路由表,从而较好地实现上述调度目标。

流量调度系统主要由三大组件组成:

  • Gateway 网关,也即策略调度执行器:将请求实时 Upstream 到合适的 Worker 中执行,同时也会统计上报各个函数端到端时延、并发数等指标;

  • 实例集群:除执行用户函数外也会实时上报资源负载等信息;

  • 反馈控制器:生成并实时根据反馈指标动态调整调度策略;

12-7.png

图 7. FaaS Worker 流量调度

除上述功能之外,对于用户隐私、数据安全、稳定性以及 SLA 有特殊要求的租户,整个流量调度系统也支持租户隔离资源池的功能。资源池支持为租户划分 Worker 实例资源池,并保证资源池内的 Worker 实例仅用于处理租户的函数,通过容器或者虚拟机级别的隔离满足相关需求。

4.3 冷启动优化

轻量级函数冷启动请求会经历如下图所示的几个阶段:

  1. 函数请求到达 Gateway

  2. Gateway 向控制面查询相对应的函数信息

  3. Gateway 转发函数请求到函数运行时实例

  4. 下载函数代码包,保存在本地

  5. 加载函数,初始化函数实例

  6. 执行函数

12-8.png

图 8. 优化前冷启动步骤

其中,第 2 步可以通过在 Gateway 中缓存函数元数据信息跳过,第 4 步可以通过函数代码包预分发跳过,通过这两步优化,去除了函数冷启动请求中对内外部服务或组件的依赖。

经过优化后的冷启动请求路径如下:

  1. 函数请求到达Gateway

  2. Gateway 查询内存中的函数元数据缓存,并转发函数请求到运行时实例

  3. 加载函数,初始化函数实例

  4. 执行函数

12-9.png

图 9. 优化后冷启动步骤

另外对于 WebAssembly 函数运行时而言,可以使用 WASM 的 AOT 模式,在函数发布阶段将 WebAssembly Module 进行预编译,在函数加载阶段就可以跳过 WebAssembly 的编译过程,加快函数 Module 的加载速度。

以 Hello World 函数为例,展示目前 WebAssembly 函数的冷启动开销。

➜ ~ curl -v https://function-id.fn.example.com
......

> GET / HTTP/2
> Host: function-id.fn.example.com
> user-agent: curl/7.77.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200
< server: nginx
< date: Tue, 04 Jan 2022 10:25:23 GMT
< content-type: text/plain
< content-length: 12
< x-bytefaas-cold-start-duration: 0.48
< x-bytefaas-execution-duration: 0.21
< x-bytefaas-gateway-duration: 1.35
< x-bytefaas-memory-usage: 1.57
< x-bytefaas-request-id: fa69dbeb-98c9-452e-bd7f-af32d2f3d652
< x-bytefaas-worker-duration: 0.77
< x-bytefaas-worker-schedule-duration: 0.49
< server-timing: inner; dur=2
< x-tt-trace-host: 01dfaefa3226038402f730246d8bfde8ea4f54b866cc1abf75297d5acf57eb8124fbbf7d545645b4572784277b1ae1db6d10b261545eba6885f64ac14b5b7a95243d7b3617915456fa1e170b7fe1b1498a
< x-tt-trace-tag: id=00;cdn-cache=miss
<
* Connection #0 to host function-id.fn.example.com left intact
  • Instance 创建时间 / 冷启动时间耗时:0.48ms

  • 函数执行阶段耗时:0.21ms

  • 函数调用总时间:1.35ms

4.4 高密度部署

轻量级函数运行时进程可创建和执行多个函数实例,由于单个函数实例的开销极小,可以实现函数高密度部署。

按照内存资源计算:

  • 假设采用 Amazon c5.metal 机型(96c192g)

  • 每台机器保留 8c16g 给管理组件

  • 单个运行时进程分配 8c16g

  • 每个运行时进程保留 1g 内存自用不分配给函数

  • 单个函数实例规格 32MB

以上条件下,计算得出,单机可运行 11 个函数运行时进程,承载 5280 个函数实例。

4.5 合并部署

FaaS 将服务部署抽象粒度提升到了函数级别,能够对服务的运行做到精细化控制,配合自动扩缩容能力,能够实现极致的成本优化,但也因此带来了新的问题。用户可将微服务在 FaaS 平台上做进一步拆分,造成了函数数量多,调用关系复杂的情况。由于 FaaS 函数与函数间的调用请求需要完整经过 FaaS 数据面组件,进行请求转发和请求调度等操作,会给函数之间的调用带来额外的请求时延,同时也会给 FaaS 系统带来额外的资源开销,如下图所示。

12-10.png

图 10. 优化前函数间调用链路

针对此问题,美研 FaaS Lab 团队在 ByteFaaS Worker 精简架构上设计并实现了合并部署方案。该方案能够让函数间调用绕过 Gateway,直接在同一个 WebAssembly 函数运行时进程内完成,或者在当前函数运行时进程资源不足的情况下,将请求直接发送至同物理节点的其他函数运行时进程上,如下图所示。

12-11.png

图 11. 优化后函数间调用链路

通过合并部署,我们可以将函数间的调用开销降到最低,同时此项调用优化对用户而言无感知,无需用户修改代码进行适配。

4.6 边缘计算支持

随着云计算的发展,更多的业务场景需要在能够支持一定算力的条件下,不断地缩短算力和客户端的距离,来减少客户端到服务端之间的网络时延。例如服务端渲染、VR 游戏、IOT 等场景,对时延和算力有着较高的要求。在此背景下,云计算不断向边缘扩散,搭载算力的设备也从海量的云主机,到千百级别机器的汇聚节点,再到几十台机器的边缘节点,边缘计算不断地贴近用户侧,以满足各种极致的性能需求。

ByteFaaS Worker 的轻量级运行时和轻量级架构,天生就非常适合部署在资源受限的边缘节点。除了资源开销足够小之外,ByteFaaS Worker 还对边缘场景做出了主要以下几个方面的优化:

  1. 云边元数据分发:与传统的云上相比,边缘机房网络的可靠性和稳定性都会有所下降,可以被认为是弱网环境。ByteFaaS 针对不可靠的网络环境,开发了一套元数据增量同步机制,既能元数据的正确分发,又能尽快将变更推送到各个边缘节点。

  2. 高可用:边缘机房的环境复杂,边缘机房的可用性和稳定性通常远不如云端机房。所以在前提下,ByteFaaS 会持续对所有边缘机房进行拨测,设立了两种策略,单机房内部健康检查策略、边缘机房到中心机房的心跳上报和熔断策略,保证单机房内部的局部不可用或者局部边缘机房不可用情况下,对服务整体的可用性不会产生不可控的影响。

  3. 流量接入:在边缘场景下,需要做到流量就近接入,以发挥边缘计算的最大价值。为此 ByteFaaS 团队联合云解析调度团队集成了全局流量管理平台,提供基于 DNS 解析的地理位置就近接入和全局流量负载均衡,以及基于健康检查的故障隔离和容灾能力。

  4. 云边通信:受限于边缘所在节点环境的安全问题,同时每个边缘机房都与云端建立专线的成本过于巨大,因此无法提供 IP 层的云边通信通道。所以 ByteFaaS 联合服务框架团队,推出 EdgeMesh 服务,作为云边通信的可靠信道,支持 HTTP 和 RPC 两种协议。

目前 ByteFaaS Worker 已通过 EdgeMS 边缘管理平台在国内部署了上百个边缘机房,包含数百个边缘节点。同城市同运营商的情况下,客户端到边缘的网络时延可降低至 10ms 内,与访问中心机房相比,平均访问时延可降低 75% 左右。

4.7 存储服务

除了计算能力外,ByteFaaS 还为 WebAssembly 函数运行时提供了两种存储服务,分别是 Global KV 和 Local Cache,它们既可以满足用户在边缘场景的存储诉求,又能方便用户在中心机房开发应用。

Global KV 是一个支持全球性同步的低延迟的最终一致性 KV 存储,基于 Abase 2.0 构建。Global KV 非常适合读多写少的使用场景,如渲染缓存、配置分发、身份验证等,在任意位置的数据变更都会异步同步到其他地区。对于函数而言,无论是写请求还是读请求,都与本地的 Global KV 服务进行交互,以实现低延迟访问。

Local Cache 是本地缓存存储,基于单机存储引擎实现,每台宿主机上部署一个服务实例提供服务,机器之间数据不会共享,数据只存储在本地。Local Cache 读写速度会比 Global KV 快,适合存储一些临时数据,例如页面渲染的中间产物。

用户可通过 WebAssembly 函数 SDK 方便快捷地访问以上两种存储服务,以 Golang 为例,引用服务的依赖包,创建对应存储空间的 Client,即可使用 Client 提供的存储方法读写数据。

一个简单的代码例子如下:

import (
    kv "example/internal/go/path/global_kv_service"
)

func demo() {
    // Create global kv namespace client.
    kvNS := kv.NewGlobalKV("demo-storage-namespace")

    // Put key value to global kv.
    err := kvNS.Put("tmp", []byte("test"), nil)
    fmt.Printf("Put error, %v.\n", err)

    // Read value from global kv.
    value, err := kvNS.Get("tmp")
    fmt.Printf("Get value, result: %v, error: %v.\n", value, err)

    // Delete value from global kv.
    err = kvNS.Delete("tmp")
    fmt.Printf("Delete error, %v.\n", err)
}

在真实的业务场景下,推荐用户使用多层缓存机制,满足业务对热数据的存储需求,解决在边缘场景下,避免由于拉取或生成数据所造成的请求时延抖动。

一个典型的多级缓存逻辑如下:

  1. 首先访问代码内存缓存

  2. 若步骤 1 未命中,访问 Local Cache,主要时延在跨进程通信,以及可能有小部分的本地磁盘 IO 时延

  3. 若步骤 2 未命中,访问 Global KV,主要时延为内网通信的网络时延

  4. 以上均未命中,需要业务拉取或生成数据,获取到数据后可先返回请求响应,再将数据写入缓存

5. 开发者支持

为了方便用户开发 WebAssembly 函数,ByteFaaS 为用户提供了多种常用语言的 SDK,降低接入成本,本章节以最常使用的 Golang 和 JavaScript 为例来说明如何快速开发部署 WebAssembly 函数。另外在开发体验上,轻量级函数与经典 FaaS 函数相比,最大的不同是用户无法直接接触到轻量级函数运行时,也无法在本地执行轻量级函数代码,给日常的开发调试带来很大不便。ByteFaaS 针对该问题,推出了在线预览工具和 CPU Profiling 工具来辅助用户的日常开发流程。

5.1 多语言 SDK

不同语言的 Guest SDK 的函数请求入口接口设计会尽量保持一致,由于 WebAssembly 函数支持 HTTP 和 TTHeader 两种协议,函数入口也分两种情况讨论。对于 HTTP 协议的函数,用户需要实现一个 Handler 函数,Handler 函数接受 HTTP Request 作为参数,返回 HTTP Response 作为函数执行输出。而对于 TTHeader 函数,函数入口则遵循各语言框架定义,如 Go 语言可使用 WebAssembly 版本的 KiteX 框架进行开发。

5.1.1 Golang

ByteFaaS 与服务框架团队合作,开发了 wago,即 Golang 语言的 WebAssembly SDK。

在 FaaS 的 WebAssembly 函数的执行环境下,已支持如下能力:

  • 接受 HTTP 请求

  • 接受 Timer 请求

  • 消费 MQ 触发器消息

  • 发送 HTTP 请求

  • Global KV

  • Local Cache

  • Metrics

  • Stream Log

  • Databus

  • Consul

  • TOS

  • KiteX Client

  • KiteX Server

以 Hello World 示例函数为例,说明 Go WASM 函数基本开发流程。

目录结构说明如下:

➜  bytefaas-wasm-golang git:(master) tree
.
├── build.sh      # 函数构建脚本
├── bytefaas.yml  # 函数元数据,bytefaas 命令行工具使用
├── go.mod
├── go.sum
├── handler.wasm  # 编译得到的 WebAssembly Module,用于发布的函数产物
└── main.go       # 函数 Go 代码

Go WASM 函数简单实例代码如下,整体和使用经典 FaaS 的 faas-go 类似。

main() 为整个函数入口,入口中调用 faas.Start() 执行 handler() 函数。

handler 函数参数为 HTTP 请求,返回值为 HTTP 响应。

package main

import (
    "example/internal/go/path/faas"
    "example/internal/go/path/faas/http"
)

func main() {
    faas.Start(handler)
}

func handler(_ *http.Request) (resp *http.Response) {
    return &http.Response{
        StatusCode: 200,
        Header: map[string][]string{
            "Content-Type": {"text/plain"},
        },
        Body: http.MustNewBody([]byte("Hello WASM from Golang!")),
    }
}

本地完成函数编辑后,使用 ByteFaaS 命令行工具即可发布上线。

➜ bytefaas release --rolling-step=100
FunctionRegion already created cn-north...
Uploading code...
Code file Uploaded
Building revision 0...
Building revision success, rev id: q6ysfraske
Releasing function in cn-north...
Function release request has been sent in cn-north
Status: rolling, q6ysfraske: 100%, yfb5z55hp8: 0%
Deployed in cn-north, status: rolling

5.1.2 JavaScript

ByteFaaS 与美研 FaaS Lab 和 ClientInfra VM 团队共建合作,探索实现将 SpiderMonkey 以及 Lepus (内部版 QuickJS) 编译成 WebAssembly 并运行在 FaaS Worker 环境。用户通过使用上层提供的 @byted/wajs sdk 可以轻松编写 JS WebAssembly 函数。

下面我们通过一个简单的示例函数 ,说明 JS WASM 函数基本开发流程。

目录结构说明如下:

❯ tree
.
├── README.md
├── build.sh             # 构建脚本
├── bytefaas.yml
├── handler.js           # 函数本地构建产物(JS)
├── handler.js.map       # 构建产物的SourceMap,用于本地调试
├── package-lock.json
├── package.json
├── src
│ └── index.ts         #  开发者编写的源代码入口
└── tsconfig.json

JS 函数入口与 ServiceWorker 类似,以 HTTP Server 为例,只需要实现一个 Web 标准的 Request Handler 即可:

import { run } from '@byted/wajs';

const handler = async (request: Request) => {
  return new Response("Hello World", {
    headers: {
      'content-type': 'text/plain'
    },
  });
}

run(handler)

完成编写源代码后,通过熟悉的构建工具(例如esbuild或swc)将源码打包构建到 <project_root>/handler.js 后直接发布即可。

JS WebAssembly 上手非常简单,本地开发不需要 WASM 编译工具,初学者甚至不需要了解 WebAssembly 相关背景知识就可以上手。

5.2 在线预览工具

为了提升开发体验,快速看到代码改动效果,ByteFaaS 针对 WebAssembly 函数提供了快速在线预览本地代码的功能,一条命令即可将本地项目部署上线并生成临时调用链接,同时还可实时输出函数日志。

预览工具使用非常简单,在函数项目目录下使用 ByteFaaS 命令行工具执行 bytefaas playground 子命令即可开启实时预览,并输出实时日志:

➜ mzpat91d bytefaas playground
-> Packing local code...
-> Sending to playground...
-> Generated playground invoke URL:

  https://d3d000cfccbc1401006d7a706174393164.fn.example.com

-> Starting local proxy server:

   http://127.0.0.1:8000

-> Connecting to realtime logs...

[2021-06-21 16:54:14 +0800 CST] STDOUT - [log] Hello Playground! (0236091a-f8de-4260-b75b-c16978c41868)

命令会输出预览调用 URL,每次预览时会生成不同的 URL 并启动一个本地代理,可根据需要选择访问预览调用 URL 或本地代理地址。

5.3 CPU Profiling 工具

除常见的开发支持外,在线上出现 CPU 热点故障 / 资源利用率异常等疑难杂症时,我们往往希望能够通过抓取 CPU Profile 的方式来排查代码中可能出现的性能问题。

ByteFaaS 与 ByteDog 团队合作,提供了 CPU Profiling 接口,支持按百分比分流部分小流量到开启了 WebAssembly JitDump 的特定的独占实例下,与此同时触发 ByteDog 对采样实例开启 CPU Profiling。

整个系统调用过程示意如下:

12-12.png

图 12. WASM CPU Profiling 过程

抓取完成之后可以在 ByteDog 平台看到包含 WebAssembly 调用栈的火焰图,由此可以方便分析性能损耗的位置。抓取的火焰图示意如下:

12-13.png

图 13. WASM CPU 火焰图

6. 总结

本文首先简要介绍了 ByteFaaS 函数计算平台,从经典 FaaS 的常见问题出发,提出轻量级函数的思路,引出了函数冷启动时间极短、资源开销极低的 ByteFaaS Worker 方案。随后从 WebAssembly 函数运行时、精简架构、开发者支持三个角度详细描述了 ByteFaaS Worker 方向。

  • WebAssembly 函数运行时章节,讲解了 ByteFaaS 如何基于 WebAssembly 打造出适合 FaaS 场景使用的函数运行时和配套的 SDK。

  • 精简架构章节,阐述了 ByteFaaS 在架构层面针对 WebAssembly 函数运行时所做出的各项开发和优化工作。

  • 开发者支持部分,通过最常用的 Golang 和 JavaScript 语言展示了如何快速开发 WebAssembly 函数,以及两个方便用户开发的辅助工具。

用户使用 WebAssembly 函数,可以获得以下收益:

  1. 得益于 WASM 函数的快速启动特性,函数实例能够在 <1ms 的时间内启动,从而能够在冷启动速度上获得相比经典容器 FaaS 百倍的提升。另外对于突发流量场景,由于函数实例启动速度足够快,不会出现由于函数预热池实例耗尽导致的请求时延波动。

下图是 FaaS 各运行时函数冷启动时间的对比,原生 Golang 函数冷启动时间约 90ms,而使用 Golang 语言编写的 WASM 函数可做到亚毫秒启动。

12-14.png

图 14. 各运行时冷启动时间对比

  1. 函数资源开销低,函数最小函数资源套餐可选择 16MB 内存,同时 WASM 函数的计费标准和经典容器 FaaS 保持一致,成本降低。

  2. 云边部署能力,支持用户将函数一键部署至中心机房和边缘机房,使用体验与 FaaS 多 Region 部署对齐。

  3. 支持全局流量调度,部署在边缘的服务支持就近访问等策略,最大程度降低访问时延。

  4. 云边协同能力,有架设在公网环境的云边通信通道,方便用户从边缘节点访问中心服务,反之亦然。

  5. 函数间调用时延降低,得益于合并部署能力的支持,函数之间调用在条件允许情况下直接在同一个进程内完成,极大降低了函数间调用开销。

通过本文详细的介绍,相信大家对 ByteFaaS 平台以及 FaaS Worker 已经有了一定的认知,最后,ByteFaaS 团队也欢迎大家试用 WebAssembly 函数,提出宝贵的意见和建议。

7. 参考文献

[1]. 《Serverless 核心技术和大规模实践》: item.jd.com/10067996365…


尾部关注.gif

扫码关注公众号 👆 追更不迷路