我们在项目中引入了WebAssembly和WASI,设计了一套高可用高并发的系统接口架构。本文详细介绍了这套架构的设计思路,包括WebAssembly的优势、WASI系统接口的设计、运行时架构、高可用方案、高并发优化、以及实际的性能测试结果。如果你对WebAssembly和边缘计算感兴趣,希望这篇文章能给你一些参考。

一、背景

先说说我们为什么引入WebAssembly。

我们的业务是一个边缘计算平台,需要在边缘节点上运行用户上传的代码。最开始我们用的是Docker容器,但是Docker容器的启动速度比较慢(几百毫秒到几秒),内存占用也比较大(几十MB到几百MB)。在边缘节点资源有限的情况下,Docker的开销太大了。

我们需要一个更轻量、更快速的代码执行环境。后来我们了解到了WebAssembly(Wasm)和WASI(WebAssembly System Interface)。WebAssembly是一种二进制指令格式,可以在沙箱中安全地执行代码,启动速度快(微秒级),内存占用小(几MB),而且支持多种编程语言(C/C++、Rust、Go、AssemblyScript等)。

WASI是WebAssembly的系统接口标准,让WebAssembly模块可以访问系统资源,比如文件、网络、时钟等。有了WASI,WebAssembly就不只是能在浏览器里跑了,还能在服务端和边缘端跑。

我们决定用WebAssembly+WASI来替代Docker,设计一套高可用高并发的系统接口架构。本文就来分享我们的设计和实践。

二、WebAssembly的优势

先说说WebAssembly相比Docker的优势。

1. 启动速度快

WebAssembly模块的启动速度是微秒级的,比Docker容器的毫秒级快了几个数量级。这对于需要冷启动的场景(比如事件驱动、按需执行)非常重要。用户触发一个函数,WebAssembly实例可以在几十微秒内启动并执行,用户几乎感觉不到延迟。

2. 内存占用小

一个WebAssembly运行时的内存占用只有几MB,而Docker容器至少要几十MB。在资源有限的边缘节点上,可以同时运行更多的WebAssembly实例,提高资源利用率。

3. 安全性高

WebAssembly运行在沙箱中,默认不能访问系统资源,只能通过WASI接口访问授权的资源。这种沙箱机制比Docker的隔离更严格,安全性更高。即使WebAssembly模块有漏洞,也不会影响到宿主机和其他模块。

4. 跨平台

WebAssembly是平台无关的,编译一次就可以在任何支持WebAssembly的平台上运行,不管是x86还是ARM,不管是Linux还是Windows。这对于异构的边缘计算环境非常友好。

5. 多语言支持

WebAssembly支持多种编程语言,C/C++、Rust、Go、AssemblyScript、Python等都可以编译成WebAssembly。用户可以用自己熟悉的语言编写代码,不需要学习新的语言。

三、整体架构设计

我们的整体架构分为几层:接入层、调度层、运行时层、资源层。

1. 接入层

接入层负责接收用户的请求,包括HTTP请求、事件触发、API调用等。接入层做协议解析、鉴权、限流,然后把请求转发给调度层。

接入层用Nginx+Go实现,支持高并发。我们用了连接池和异步IO,单节点可以支持几万的并发连接。

2. 调度层

调度层负责把请求调度到合适的运行时节点上执行。调度层考虑的因素包括:节点的负载、模块的位置、数据的亲和性、用户的优先级等。

调度层用一致性哈希来分配请求,同一个用户的请求尽量调度到同一个节点,提高缓存命中率。同时也支持负载感知的调度,把请求调度到负载较低的节点。

调度层还做了故障转移,如果某个节点故障,自动把请求调度到其他节点,保证服务的可用性。

3. 运行时层

运行时层是核心,负责执行WebAssembly模块。每个运行时节点上运行多个WebAssembly实例,每个实例是一个独立的沙箱。

运行时用的是Wasmtime,这是Mozilla开发的WebAssembly运行时,支持WASI,性能好,安全性高。我们在Wasmtime的基础上做了一些定制,比如资源限制、监控、预热等。

运行时层做了实例池管理,提前创建好一些空闲的实例,请求来了直接用,不需要冷启动。实例用完之后放回池中,复用实例,减少创建和销毁的开销。

4. 资源层

资源层负责管理底层的资源,包括CPU、内存、存储、网络等。资源层给每个WebAssembly实例分配资源,限制CPU和内存的使用,防止某个模块占用太多资源影响其他模块。

资源层还做了资源监控和告警,当资源使用率过高的时候,自动扩容或者限流。

四、WASI系统接口设计

WASI是WebAssembly和系统之间的接口,我们在标准WASI的基础上做了一些扩展和定制。

1. 标准WASI接口

标准的WASI接口包括:

  • 文件系统操作:打开、读取、写入、删除文件
  • 网络操作:创建socket、连接、发送、接收数据
  • 时钟:获取当前时间、睡眠
  • 随机数:生成随机数
  • 进程:退出、获取环境变量

这些标准接口已经能满足大部分的需求。我们用的是WASI preview 1版本,这是目前最成熟的版本。

2. 自定义扩展接口

除了标准接口,我们还扩展了一些自定义接口,满足业务的特殊需求:

  • 缓存接口:访问分布式缓存,读写键值对
  • 消息队列接口:发送和接收消息
  • 数据库接口:执行SQL查询
  • 日志接口:输出结构化日志
  • 指标接口:上报监控指标

这些扩展接口通过WASI的custom section机制实现,在运行时中注册,WebAssembly模块可以通过导入函数来调用。

3. 接口的安全控制

WASI接口的安全控制很重要。我们对每个接口都做了权限控制,WebAssembly模块只能访问授权的资源。

比如文件系统,每个模块只能访问自己的工作目录,不能访问其他目录。网络访问也做了限制,只能连接白名单中的地址。数据库接口只能访问指定的数据库和表。

权限通过模块的配置文件来管理,部署的时候指定,运行时强制执行。这样即使模块有恶意代码,也不能越权访问资源。

五、高可用设计

高可用是我们架构设计的重点,从几个层面来保证。

1. 多节点部署

每个组件都部署多个节点,避免单点故障。接入层部署多个节点,前面用负载均衡器。调度层部署多个节点,用主从模式,主节点故障自动切换。运行时层部署多个节点,调度层自动故障转移。

2. 健康检查

每个节点都有健康检查,定期检查节点的状态。如果节点不健康,自动从负载均衡器或者调度列表中摘除,不再接收新的请求。节点恢复之后,自动加回来。

健康检查包括进程检查、端口检查、资源检查、功能检查等,确保节点是真正可用的。

3. 故障转移

如果运行时节点在执行请求的过程中故障了,调度层会把请求重新调度到其他节点执行。对于幂等的请求,直接重试就可以。对于非幂等的请求,我们用了请求去重机制,确保不会重复执行。

故障转移的时间在几百毫秒以内,用户几乎感觉不到。

4. 降级和限流

在系统负载过高的时候,我们有降级和限流机制。限流用令牌桶算法,限制每个用户和每个模块的请求速率。降级在系统过载的时候,优先保证核心功能,非核心功能暂时不可用。

降级和限流保证了系统在高负载下不会崩溃,核心功能依然可用。

5. 数据持久化

WebAssembly模块的状态和数据,我们做了持久化。模块执行过程中的重要数据,定期保存到分布式存储中。如果模块故障或者节点故障,可以从持久化的数据中恢复,不会丢失数据。

六、高并发优化

高并发是另一个重点,我们做了很多优化。

1. 实例池

前面提到过,我们用了实例池,提前创建好WebAssembly实例,请求来了直接用。实例池的大小根据负载动态调整,负载高的时候增加实例,负载低的时候减少实例。

实例池大大减少了冷启动的开销,提高了并发处理能力。

2. 异步执行

运行时用异步执行模型,一个线程可以同时处理多个请求。WebAssembly实例在等待IO的时候,线程可以去处理其他请求,不会阻塞。

我们用了tokio异步运行时,配合Wasmtime的异步支持,实现了高并发的执行。

3. 零拷贝

在数据传输的过程中,我们尽量用零拷贝,减少数据复制的开销。比如网络数据接收之后,直接传给WebAssembly实例,不需要在用户空间和内核空间之间复制。

零拷贝大大提高了数据处理的吞吐量,特别是对于大文件和大数据的场景。

4. 缓存

我们在多个层面做了缓存。接入层缓存静态资源,调度层缓存模块的位置,运行时层缓存编译好的WebAssembly模块和实例。

缓存减少了重复的计算和IO,提高了响应速度和吞吐量。

5. 资源隔离

每个WebAssembly实例都有独立的资源限制,CPU和内存都做了隔离。这样某个模块的高负载不会影响到其他模块,保证了整个系统的稳定性和公平性。

资源隔离用了cgroups和WebAssembly的沙箱机制,两层隔离,更加安全可靠。

七、性能测试

我们对这套架构做了性能测试,结果如下。

1. 冷启动时间

WebAssembly模块的冷启动时间平均在50微秒左右,最快的可以到10微秒。相比Docker容器的几百毫秒,快了几千倍。

用了实例池之后,热启动的时间几乎为0,请求来了直接执行。

2. 并发处理能力

单台运行时节点(4核8G)可以同时运行200个WebAssembly实例,处理每秒5000个请求。CPU使用率在70%左右,内存使用率在60%左右。

如果是简单的计算任务,单节点可以处理每秒10000个请求。

3. 内存占用

每个WebAssembly实例的平均内存占用是5MB左右,包括运行时和模块的内存。相比Docker容器的50MB,节省了90%的内存。

一台8G内存的节点,可以同时运行1000多个实例(当然CPU会成为瓶颈)。

4. 延迟

简单请求的平均延迟在5毫秒左右,P99延迟在20毫秒左右。复杂请求的延迟根据计算量不同,在几十到几百毫秒之间。

这个延迟对于大部分边缘计算场景来说都是可以接受的。

八、遇到的问题和解决方案

在开发和部署的过程中,我们也遇到了一些问题。

问题1:WASI的兼容性

不同的WebAssembly运行时对WASI的支持程度不一样,有些接口在某些运行时上不支持。我们的解决方案是统一用Wasmtime运行时,并且在编译模块的时候指定WASI版本,确保兼容性。

问题2:调试困难

WebAssembly模块的调试比原生代码困难,因为没有源码级的调试工具。我们的解决方案是在模块中加入详细的日志,通过日志来排查问题。同时也用了Wasmtime的调试支持,可以打印调用栈和变量信息。

问题3:GC语言的支持

对于有垃圾回收的语言(比如Go、Python),编译成WebAssembly之后性能不太好,而且内存占用比较大。我们的解决方案是推荐用户用Rust或者C/C++来写性能敏感的模块,Go和Python用来写逻辑简单的模块。

问题4:网络编程的复杂性

WASI的网络接口比较底层,用起来比较复杂。我们封装了一些高级的网络库,让用户可以更方便地进行网络编程。同时也提供了HTTP客户端和服务端的封装,简化了Web开发。

九、经验总结

总结一下我们的经验。

1. WebAssembly适合边缘计算

WebAssembly的轻量、快速、安全的特点,非常适合边缘计算场景。在资源有限的边缘节点上,WebAssembly比Docker更有优势。

2. WASI还在发展中

WASI目前还在发展中,preview 1版本已经比较稳定,但是还有一些功能不完善。preview 2版本正在开发中,会带来更多的功能和更好的性能。在生产环境中使用,要选择稳定的版本,并且做好测试。

3. 实例池是关键

对于高并发场景,实例池是关键。用实例池复用WebAssembly实例,可以大大减少冷启动的开销,提高吞吐量。

4. 安全不能忽视

WebAssembly的沙箱安全性很高,但是也不能忽视。要做好权限控制、资源隔离、安全审计,确保恶意模块不会影响到系统的安全。

5. 监控和可观测性很重要

WebAssembly模块运行在沙箱中,传统的监控工具可能用不了。要自己实现监控和可观测性,包括日志、指标、链路追踪等,方便排查问题和优化性能。

十、写在最后

WebAssembly和WASI是一个很有前景的技术,特别是在边缘计算和云原生领域。我们的实践证明,WebAssembly可以在生产环境中提供高可用高并发的服务,而且比传统的容器方案更轻量、更快速、更安全。

当然,WebAssembly还在发展中,还有一些不完善的地方,生态也不如Docker成熟。但是它的优势很明显,未来的发展空间很大。

如果你在做边缘计算、Serverless、或者其他需要轻量代码执行环境的场景,不妨考虑一下WebAssembly。它可能会给你带来惊喜。

最后用一句话结束本文:"技术的发展总是在不断突破边界,WebAssembly正在重新定义代码的执行方式。"愿每一个技术人都能拥抱新技术,在技术的浪潮中找到属于自己的机会。