如何使用 WebAssembly 扩展后端应用

news2024/12/22 20:26:40

1. WebAssembly 简介

随着互联网的发展,越来越多的应用借助 Javascript 转到了 Web 端,但人们也发现,随着移动互联网的兴起,需要把大量的应用迁移到手机端,随着手端的应用逻辑越来越复杂,Javascript 的解析、编译消耗了大量时间,导致页面加载慢,应用性能低下的很多问题。

为了解决这些问题,Mozilla 的工程师 Alon Zakai 在 2012 年提出了 Asm.js。之后,经过几年的发展,最终在 2015 年演变成了 WebAssembly。

WebAssembly(简写为 Wasm)是一种用于堆栈式虚拟机的二进制指令格式。它的设计目的,是成为其它编程语言的一个可移植的编译目标,以便在 Web 上布署客户端和服务端应用。

这是 WebAssembly 官网上的定义,从这个定义中我们可以知道,WebAssembly 是一种二进制指令格式。但在日常的讨论中,我们也常常把 WebAssembly Text Format 称为 WebAssembly,而这种文本格式实际上是一种编程语言。

正式发布后,WebAssembly 迎来了迅猛的发展,到 2017 年 11 月,Mozilla 宣布包括 Chrome、Firefox、Safari 等在内的所有主流浏览器都已经支持 WebAssembly,而根据 2021 年 7 月的数据,用户正在使用的浏览器中,已经有 94% 支持了 WebAssembly。

在得到了浏览器的广泛支持之后,一些重量级的应用也被逐渐被移植到了 Web 端,其中包括:

Google Earth — 一个3D的地图应用
AutoCAD — 一个工业制图的应用
Doom — 一个经典的第一人称的射击游戏
TensorFlow — Google 开源的机器学习框架

这些案例也说明 WebAssembly 达到了自己的设计目标——在 Web 上部署桌面上的原生应用。而 WebAssembly 能够获得如此快速的发展,得益于它的几个特点:

1.1 性能好

接近机器代码的运行速度,评测表明,WebAssembly 只比原生代码慢大约 10%。

1.2 体积小

加载速度快,WebAssembly 是一种紧凑的二进制格式,体积通常远小于完成同样功能的 Javascript 代码。

1.3 安全高

WebAssembly 代码运行在沙箱内,默认无法进行任何外部访问。

1.4 多语言

WebAssembly 并不限制用户使用何种语言开发,只要有相应的编译器,任何语言都可以被编译成 WebAssembly。

2. WebAssembly 在后端的应用

在 WebAssembly 的官方定义中,“用于堆栈式虚拟机的”这个定语也非常值得关注,因为它导致 WebAssembly 这项最初以 Web 端为应用场景,以至于名字中都包含“Web”这个词的技术,慢慢进入了后端应用的领域。

这是因为,从早期的 VMWare WorkStation、VirtualBox,到今天的 Docker,虚拟化技术一直是云计算的重要基础。所以,WebAssembly 作为一种具有很多特点的虚拟机代码格式,进入后端应用领域是必然趋势。Docker 的创始人 Solomon Hykes 在 2019 年说“如果 2008 年就有 WASM 和 WASI,我们就不用发明 Docker 了”,对其在后端应用的前景之看好,可见一斑。

当然,Solon Hykes 后来也说他的意思不是“WebAssembly 会取代 Docker”,这也是当今业界普遍的观点:WebAssembly 和 Docker 各有优势,互为补充。具体来说:

WebAssembly 程序的体积通常只有1M左右,而 Docker 镜像则动辄超过100M,所以 WebAssembly 具有快得多的加载速度。

WebAssembly 程序的冷启动速度大约是 Docker 容器的 100 倍。

WebAssembly 默认运行在沙箱内,任何与外部的交互,都只有获得明确的许可之后才能进行,具有极佳的安全性。

WebAssembly 模块仅仅是二进制的程序代码,不包含操作系统环境,所以不可能像在Docker 中那样简单的编译下就能执行。

下面,通过实际的示例介绍下如何在后端应用中使用 WebAssembly。

2.1. 在应用中嵌入 WebAssembly

如下图所示,不管是 Web 应用还是非 Web 应用,要使用 WebAssembly,都需要在宿主程序中嵌入 WebAssembly 运行时引擎,区别只在于,在 Web 应用中,这个宿主程序是浏览器,而在非 Web 应用中,宿主程序是我们自己开发的,具体到本文关注的后端应用,宿主程序则是我们的后端服务程序。
在这里插入图片描述

目前可选的 WebAssembly 运行时引擎有 Wasmtime, WasmEdge, WAVM, Wasmer等很多种,各有自己的优势和缺陷。本文将以 Wasmtime 为例,介绍如何在以 Go 语言开发的宿主程序中嵌入 WebAssembly。

嵌入 WebAssembly 运行时引擎并实例化 WebAssembly 模块本身非常简单,在省略错误处理的情况下,只要下面几行代码即可:

func createWasmVM(code []byte) {
engine := wasmtime.NewEngine()
module, _ := wasmtime.NewModule(engine, code)
store := wasmtime.NewStore(engine)
linker := wasmtime.NewLinker(engine)
inst, _ := linker.Instantiate(store, module)
_ = inst
}
其中涉及了实际开发中需要了解的几个重要概念,简单介绍如下:

引擎(engine):用于编译和管理模块的全局上下文。

模块(module):编译后的 WebAssembly 模块。

仓库(store):WebAssembly 程序和宿主之间的纽带,维护各种关联信息。

实例(instance):实例化的模块,是真正可以执行的程序。

链接器(linker):wasmtime 中的一个工具对象,用于实例化模块。

虽然上面的代码创建了 WebAssembly 模块的实例,并且根据 WebAssembly 的规范,模块实例化时就可以执行 WebAssembly 代码,但由于安全性的限制,其执行结果无法对外输出,所以这种“执行”毫无意义。因此,我们需要实现宿主程序和 WebAssembly 程序的互操作,为 WebAssembly 程序提供输入/输出接口。

2.2. 宿主调用 WebAssembly

假设我们的WebAssembly 程序中有一个名为 sum 的函数,接收两个整形变量作为参数,返回它们的和,则宿主程序可以使用下面的代码来调用这个函数:

fn := inst.GetExport(store, “sum”).Func()
r, _ := fn.Call(store, 1, 2)
fmt.Println(r.(int32))
虽然不同的宿主开发语言和 WebAssembly 运行时引擎具体的调用方式有区别,但运行时引擎的文档一般都有相关说明,所以这一步照着文档做就好,没有难度。

这里的难点在于,如何才能在 WebAssembly 程序中暴露出这个函数,以便宿主程序能找到并调用它。前面说过,只要有相应的编译器,各种语言都可以编译成 WebAssembly,但大多数语言设计时并没有考虑 WebAssembly 的需要,也就没有提供暴露函数的方法。所以这个问题只能通过特定编译器的非标准扩展来解决。也就是说,找到这个非标准扩展是解决问题最关键的一步。但也正由于“非标准”,所以相关的资料有时并不容易找到。

作为示例,下面给出的是使用 C/C++(编译器是 emscripten)和 AssemblyScript 时对外暴露函数的方法:

// C/C++
EMSCRIPTEN_KEEPALIVE int sum( int a, int b ) {
return a + b;
}
// AssemblyScript
export function sum( a: i32, b: i32 ): i32 {
return a + b
}

2.3. WebAssembly 调用宿主

与宿主调用 WebAssembly 类似,运行时引擎的文档中一般会介绍宿主端如何暴露一个函数给 WebAssembly 程序。

fn := func() {
fmt.Println(“hello wasm”)
}
linker.DefineFunc(store, “easegress”, “hello”, fn)
问题的难度同样在于 WebAssembly 程序中如何使用语言的非标准扩展来导入这个函数,下面是 C/C++ 和 AssemblyScript 中的具体方法:

// C/C++
attribute((import_module(“easegress”), import_name(“hello”))) void hello();
void call_hello() {
hello();
}
// AssemblyScript
@external(“easegress”, “hello”) declare function hello(): void
export function callHello(): void {
hello()
}

2.4. 复杂参数传递

宿主和 WebAssembly 程序相互调用对方的函数时,也需要传递参数和返回值,如果是整数等简单数据类型,直接传递即可。但当数据类型是字符串等复杂类型时,就会遇到新的问题,具体有两点:

宿主程序和 WebAssembly 程序的开发语言一般并不相同,所以复杂参数的内存布局也不同,直接传递的话,接收方根本理解不了,也就没有办法使用。

由于安全性设计,宿主程序和 WebAssembly 程序的内存是隔离开的,WebAssembly 程序访问不了宿主的内存。

因为宿主程序可以访问 WebAssembly 的内存,所以第二个问题的解决方法是 WebAssembly 程序暴露内存管理的相关函数,让宿主来操作 WebAssembly 的内存,例如:

// C/C++
EMSCRIPTEN_KEEPALIVE void* wasm_alloc( int size ) {
return malloc( size );
}

EMSCRIPTEN_KEEPALIVE void wasm_free( void* p ) {
free( p );
}
// AssemblyScript
export function wasm_alloc(size: i32): number {
let buf = new ArrayBuffer(size)
let ptr = changetype(buffer)
return __pin(ptr)
}

export function wasm_free(ptr: number): void {
__unpin(ptr)
}
之后,我们可以借助这些内存管理函数,并通过序列化/反序列化相关数据类型来传递它们,比如在下面 WebAssembly 调用宿主函数的代码中,参数和返回值本来各是一个字符串,但通过序列化/反序列化之后,只要传递它们在 WebAssembly 内存中的地址(整数)即可:

// Host: Go
func foo(addr int32) int32 {
// …
mem := inst.GetExport(store, “memory”).Memory().UnsafeData(store)
start := addr
for mem[addr] != 0 {
addr++
}
data := make([]byte, addr-start)
copy(data, mem[start:addr])
param := string(data)
// …
result := “result”
vaddr, _ := inst.GetExport(store, “wasm_alloc”).Func().Call(store, len(result)+1)
addr = vaddr.(in32)
copy(mem[addr:], []byte(result))
mem[addr + int32(len(result))] = 0
return addr
}
// WebAssembly: AssemblyScript
@external(“easegress”, “foo”) declare function foo(addr: number): number
export func callFoo(name: string): string {
let buf = String.UTF8.encode(name + ‘’)
let addr = changetype(buf)
addr = foo(addr)
buf = changetype(addr)
wasm_free(addr)
buf = buf.slice(0, buf.byteLength - 1)
return String.UTF8.decode(buf)
}

2.5. SDK

看了上面宿主程序和 WebAssembly 程序互操作的过程,相信大家已经发现它和 RPC 调用的过程非常像。不同的是,在 RPC 调用中,一系列非常繁琐的序列化/反序列化操作,都由工具自动生成代码实现,使用者根本无需关心。

而在 WebAssembly 应用的开发中,用户也不希望每次都处理那么多的细节。所以,作为宿主程序的开发者,我们需要为用户提供相关 SDK,屏蔽掉底层细节,让用户能够专注于业务逻辑的开发。

由于用户可以使用多种语言开发 WebAssembly 应用,所以我们需要提供针对不同语言的 SDK,或者至少也需要覆盖目标用户使用的主流语言。

2.6. 错误处理

像普通程序一样,WebAssembly 程序也会有各种错误,虽然作为宿主程序的开发者,我们无法预知具体的错误,但我们必须把这些错误的影响范围限制在 WebAssembly 虚拟机内部,不能让它们影响宿主程序的功能。

宿主程序要防范的第一类错误是 WebAssembly 程序中的死循环。实际场景中,其实宿主程序并没有办法判断是否真的出现了死循环,所以,折衷的解决办法是给 WebAssembly 程序设置一个最大运行时间,一旦超过了这个时间还没有结束,就认为里面出现了死循环,并终止它的执行。终止执行的示例代码如下:

ih, _ := store.InterruptHandle()
ih.Interrupt()

3. Easegress 中的 WebAssembly

Easegress 是 MegaEase 开发的下一代流量型网关,具有云原生、高可用、可观测、可扩展等特点。在 Easegress 之前,市场上已经有包括 nginx 在内的多个成熟网关产品。但 MegaEase 认为,流量调度型网关并不仅仅是一个反向代理,还需要能够动态的进行流量编排和调度。此外,在具体应用场景中,还涉及各种各样的业务逻辑侵入,因此,必须高度可扩展。

基于以上观点,Easegress 从诞生之日就将可扩展性放到了重要位置,并在多个层面进行了针对性的设计。

首先,在开发语言的选择上,我们有C/C++/Java/Rust/Go这些主流的静态语言的选项:

使用 C/C++ 或 Rust 肯定会给 Easegress 带来最好的性能,但这些语言门槛太高,一般用户很难掌握,尤其在写业务逻辑的代码上效率实在是太低。也就谈不上通过修改代码来扩展业务逻辑了

Java 易学易用,也非常适合写业务逻辑,但是体积大,而且性能不能满足要求;

相对而言,Go 简单易学,性能也比较好,特别是在 Easegress 所处的网络应用领域,由于语言本身的特殊设计,很多场景下与 C/C++ 的性能差距基本可以忽略。所以,Easegress 最终选择了 Go 作为开发语言。但不管使用什么语言,源码级的扩展都不可避免的将用户限制到一门特定的语言,而且会涉及重新编译、重新布署、重新启动,造成服务的中断。

第二,通过外部调用的方式扩展,如FaaS。通过云原生的 FaaS 方式的好处是不限制用户的开发语言,而且可以让整个架构具有很好的可伸缩性,但其缺点是需要引用 Kubernetes 等的比较重的外部依赖,导致整个运维非常复杂(注:Easegress 目前已经支持了 Knative)

第三,通过嵌入其它语言的解释器进行扩展。这方面,Lua 本身就是为嵌入其它程序而设计的,具有相应的优势。但我们认为 Lua 也有两个缺点,一是本身表现力不够,不适合写复杂的业务逻辑;二是太小众,不是主流语言,有相关经验的程序员太少。所以,经过权衡,Easegress 最终选择了嵌入 WebAssembly,主要基于两点考虑:一是接近于原生代码的高性能;二是不限制用户的开发语言,用户可以使用自己喜欢或熟悉的语言开发业务逻辑。

作为使用 WebAssembly 扩展业务逻辑的样例,我们之前已经发布了《使用 Easegress + WebAssembly 做秒杀》,欢迎大家阅读并向我们反馈更多的实际案例。

当然,选择了 WebAssembly 意味着我们需要开发多种语言的 SDK,目前已经完成了 AssemblyScript SDK 的开发,相信在 MegaEase 和整个开源社区的共同努力下,我们支持的语言会越来越多。欢迎大家关注我们的开源社区:https://github.com/megaease/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2263879.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python学习——洛谷P2010 [NOIP2016 普及组] 回文日期 三种方法

[NOIP2016 普及组] 回文日期 文章目录 [NOIP2016 普及组] 回文日期题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示方法一方法二方法三 题目背景 NOIP2016 普及组 T2 题目描述 在日常生活中,通过年、月、日这…

前端yarn工具打包时网络连接问题排查与解决

最近线上前端打包时提示 “There appears to be trouble with your network connection”,以此文档记录下排查过程。 前端打包方式 docker启动临时容器打包,命令如下 docker run --rm -w /app -v pwd:/app alpine-node-common:v16.20-pro sh -c "…

BenchmarkSQL使用教程

1. TPC-C介绍 Transaction Processing Performance Council (TPC) 事务处理性能委员会,是一家非盈利IT组织,他们的目的是定义数据库基准并且向产业界推广可验证的数据库性能测试。而TPC-C最后一个C代表的是压测模型的版本,在这之前还有TPC-A、…

Linux网络基础--传输层Tcp协议(上) (详细版)

目录 Tcp协议报头: 4位首部长度: 源端口号和目的端口号 32位序号和确认序号 标记位 超时重传机制: 两个问题 连接管理机制 三次握手,四次挥手 建立连接,为什么要有三次握手? 先科普一个概念&…

全志H618 Android12修改doucmentsui鼠标单击图片、文件夹选中区域

背景: 由于当前的文件管理器在我们的产品定义当中,某些界面有改动的需求,所以需要在Android12 rom中进行定制以符合当前产品定义。 需求: 在进入File文件管理器后,鼠标左击整个图片、整个文件夹可以选中该类型,进行操作,故代码分析以及客制化如下: 主要涉及的代码:…

Unity命令行传递自定义参数 命令行打包

命令行参数增加位置 -executeMethod 某脚本.某方法 参数1 参数2 参数3 ... 例如执行EditorTest.GetCommandLineArgs方法 增加两个命令行参数 Version=125 CDNVersion=100 -executeMethod EditorTest.GetCommandLineArgs Version=125 CDNVersion=100 Unity测试脚本 需要放在…

如何重新设置VSCode的密钥环密码?

故障现象: 忘记了Vscode的这个密码: Enter password to unlock An application wants access to the keyring “Default ke... Password: The unlock password was incorrect Cancel Unlock 解决办法: 1.任意terminal下,输入如下…

电子发票汇总改名,批量处理电子发票问题

今天给大家推荐一个财务方面工作的软件。可以帮你解决很多财务。发票方面的问题。 电子发票汇总改名 批量处理电子发票问题 这个软件安装之后。会在桌面上分成三个小软件,分别是修改单位信息、自定义命名规则和电子发票汇总改名。 你可以在这个软件里提取PDF或者of…

Linux——卷

Linux——卷 介绍 最近做的项目,涉及到对系统的一些维护,有些盘没有使用,需要创建逻辑盘并挂载到指定目录下。有些软件需要依赖空的逻辑盘(LVM)。 先简单介绍一下卷的一些概念,有分区、物理存储介质、物…

MySQL通用语法 -DDL、DML、DQL、DCL

SQL 全称 Structured Query Language,结构化查询语言。操作关系型数据库的编程语言,定义了 一套操作关系型数据库统一标准 。 SQL通用语法 MySQL语言的通用语法。 SQL语句可以单行或多行书写,以分号结尾。SQL语句可以使用空格/缩进来增强…

利用DnslogSqlinj工具DNSlog注入

工具下载链接 https://github.com/adooo/dnslogsqlinj 配置 将域名和API进行一个更改 之后再安装两个python2的库就可以使用dnslog进行自动化注入了 python2安装pip2 curl https://bootstrap.pypa.io/2.7/get-pip.py -o get-pip.py python2 get-pip.py库 pip2 install geven…

QT网络(一):主机信息查询

网络简介 在QT中进行网络通信可以使用QT提供的Qt Network模块,该模块提供了用于编写TCP/IP网络应用程序的各种类,如用于TCP通信的QTcpSocket和 QTcpServer,用于 UDP 通信的 QUdpSocket,还有用于网络承载管理的类,以及…

STM32-笔记5-按键点灯(中断方法)

1、复制03-流水灯项目,重命名06-按键点灯(中断法) 在\Drivers\BSP目录下创建一个文件夹exti,在该文件夹下,创建两个文件exti.c和exti.h文件,并且把这两个文件加载到项目中,打开项目工程文件 加载…

重塑医院挂号体验:SSM 与 Vue 搭建的预约系统设计与实现

4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式,是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示: 图4-1系统工作原理…

mysql的事务控制和数据库的备份和恢复

事务控制语句 行锁和死锁 行锁 两个客户端同时对同一索引行进行操作 客户端1正常运行 客户端2想修改,被锁行 除非将事务提交才能继续运行 死锁 客户端1删除第5行 客户端2设置第1行为排他锁 客户端1删除行1被锁 客户端2更新行5被锁 如何避免死锁 mysql的备份和还…

C# OpenCV机器视觉:尺寸测量

转眼就是星期一了,又到了阿强该工作的时候了!阿强走进了他作为机器视觉工程师的办公室,准备迎接新一天的挑战。随着周末的结束,他心中暗想:“如果我能让机器像我一样聪明,那就太好了!” 正当他…

四川托普信息技术职业学院教案1

四川托普信息技术职业学院教案 【计科系】 周次 第 1周,第1次课 备 注 章节名称 第1章 XML语言简介 引言 1.1 HTML与标记语言 1.2 XML的来源 1.3 XML的制定目标 1.4 XML概述 1.5 有了HTML了,为什么还要发展XML 1.5.1 HTML的缺点 1.5.2 XML的特点 1.6 X…

网络安全防范

实践内容 学习总结 PDR,$$P^2$$DR安全模型。 防火墙(Firewall): 网络访问控制机制,布置在网际间通信的唯一通道上。 不足:无法防护内部威胁,无法阻止非网络传播形式的病毒,安全策略…

GhostRace: Exploiting and Mitigating Speculative Race Conditions-记录

文章目录 论文背景Spectre-PHT(Transient Execution )Concurrency BugsSRC/SCUAF和实验条件 流程Creating an Unbounded UAF WindowCrafting Speculative Race ConditionsExploiting Speculative Race Conditions poc修复flush and reload 论文 https:/…

c4d动画怎么导出mp4视频,c4d动画视频格式设置

宝子们,今天来给大家讲讲 C4D 咋导出mp4视频的方法。通过用图文教程的形式给大家展示得明明白白的,让你能轻松理解和掌握,不管是理论基础,还是实际操作和技能技巧,都能学到,快速入门然后提升自己哦。 c4d动…