美文网首页码农的世界
WebAssembly初探以及在Tengine中的应用

WebAssembly初探以及在Tengine中的应用

作者: 阿里云技术 | 来源:发表于2019-08-28 13:31 被阅读0次

    0x01 前言

    WebAssembly本质是一种二进制指令格式(Binary Instruction Format),即是一种编译目标,该技术成功地使得浏览器有办法将沉重且耗时的JS代码变成了拥有Native性能的二进制,毋庸置疑,它是成功的。

    当WebAssembly脱离于浏览器后,其沙盒、高效、规范化、可移植性 等特性使其成为独立VM变得可能,本文也将讨论起WebAssembly技术作为独立VM的优势以及其在Tengine中的应用。

    0x02 简介

    WebAssembly标准目前由Mozilla领导,Google、Microsoft、Apple等众多大公司参与制定,但是是何缘故能让这4大巨头站在一块、他们最原始的动机又是什么,或许我们已经不得而知,Brendan Eich(JS的发明者、WebAssembly的主要推动者)也承认了最开始使用了私有github来协调各大巨头共同确定目标并且推动他们为这项技术买单。

    普遍认为,这4大厂代表了4大浏览器平台,在面对日益增加的前端业务逻辑对计算机计算资源的消耗,迫切需要一种新的技术,该技术首先需要解决JS性能问题,即它是高效的;其次需要安全性,即它是沙盒的;接着是需要可移植性,即能被所有浏览器接受。

    0x03 例子

    首先我们先来通过一个例子来直观感受下WebAssembly

    机器环境准备

    • 升级工具链(仅集团的机器需要,因为他们的版本较低,使用OSX可以不需要这步)
    • emcc编译器

    准备源文件

    #include <stdio.h>
    int main()
    {
     printf("in wasm world\n");
     return 1;
    }
    
    

    编译

    <pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-variant-east-asian: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

    chenjiayuadeMacBook-Pro:emsdk mrpre$ emcc targt.c -o target.html
    chenjiayuadeMacBook-Pro:emsdk mrpre$
    chenjiayuadeMacBook-Pro:emsdk mrpre$ ls -l target*
    -rw-r--r-- 1 mrpre staff 80 8 13 10:21 target.c
    -rw-r--r-- 1 mrpre staff 102676 8 13 10:22 target.html
    -rw-r--r-- 1 mrpre staff 102935 8 13 10:22 target.js
    -rw-r--r-- 1 mrpre staff 42765 8 13 10:22 target.wasm
    
    

    我们看到,生成了3个文件,target.js 是胶水文件,用来调用target.wasm,其被本身用来被JSEngine加载。

    使用node运行

    chenjiayuadeMacBook-Pro:emsdk mrpre$ node target.js
    in wasm world
    chenjiayuadeMacBook-Pro:emsdk mrpre$
    
    

    貌似还不直观,那就使用浏览器运行

    先在当前目录中启动一个简单的httpserver用以浏览器访问

    python -m SimpleHTTPServer 9000
    
    

    浏览器访问http://127.0.0.1:9000/target.html

    展示页面的其他元素是emcc帮助生成的我们并不关心,这里我们关心浏览器控制台console输出的结果,console里面输出的正是我们C代码printf打印的数据,可以推测,printf的功能由console.log实现。

    0x04 安全性

    WebAssembly的安全性归根结底,是其沙盒的特性。

    文件安全

    你可以在 上面例子中 尝试 open一个xxx文件,你会发现,你没办法打开,除非编译时显示的加上"--embed-file xxx",该编译选项将文件内容全部放在target.js中,target.wasm在被执行时,所有的文件操作也均在内存上执行,在WebAssembly所有读写操作均在内存上进行。

    内存安全

    先来看下WebAssembly是如何管理内存的。

    通常,WebAssembly的内存需要Native环境申请且提供给.wasm,即Native在实例化.wasm文件时需要显示将一块属于Native环境的内存给.wasm使用,这带来2个好处。

    (1)Native和WebAssembly内存共享

    (2)防止内存泄露

    (1)内存共享

    先来看如何共享字符串。

    1、首先,有一块内存10字节的内存,由Native环境申请,且给WebAssembly使用,并且告诉WebAssembly地址是1423,WebAssembly会将其映射为010。

    2、WebAssembly将字符串"Hello"写入0~4

    3、WebAssembly告诉Native"Hello"的起始地址,当前例子是0。

    4、Native获得0,知道其映射在Native环境的地址是14,所有从14开始读取字符串

    乍一听,反倒觉得这不安全,该特性岂不会让WebAssembly内存的错误操作污染到Native的环境?

    实际上,当执行WebAssembly的VM/Engine尝试操作内存的时候,都会判断内存的边界,当Vm/Engine发现地址越界时会抛出异常:

    (2)防止内存泄露

    这个也很容易理解,对于Native环境,其传给WebAssembly的内存对于Native而言就是一个pool,当调用玩且释放WebAssembly时也可以随即释放该pool;对于有GC的语言,这个pool在Native环境被创建时就已被GC跟踪,会自动释放内存。

    0x05 性能

    WebAssembly通常被认为是高性能的,我们来看看究竟如何。

    WebAssembly VS JS

    JS执行流程

    WebAssembly执行流程(附带和上图的对比)

    总结WebAssembly比JS更快的原因

    (1)WebAssembly的变量类型不是JS的动态类型,所以编译器无需在运行时才编译。

    (3)因为WebAssembly变量是静态的,编译器无需生成多份代码。

    (4)LLVM已经在编译C文件时进行了优化。

    其实说到底,核心问题就是JS是动态类型语言。

    WebAssembly为什么快

    首先需要了解的是,编译器通常分为Front End 和 Back End,Front End 用于语法分析,然后生成IR(Intermediate representation),Front End用于生成IR对应的机器码。下图是WebAssembly从源文件到可执行机器码的整个流程。

    将其分为2部分,前半部分在Server端完成,编译成wasm二进制。后半部分由VM/Engine完成,将wasm二进制编译成对应机器的字节码。

    所以当一个浏览器或者VM/Engine获得WebAssembly二进制文件时,并非解析格式后直接执行,而是需要使用Back End将其编译成机器码。

    当有人都在说WebAssembly代码有Native运行速度时,往往都忽略了使用Back End编译成机器码的耗时。

    开源项目Wasmer项目使用了3个Back End,通常需要在编译耗时以及对应生成的机器码执行效率间进行取舍。

    另一个针对嵌入式环境的开源库Intel WAMR,它不包含Back End,直接人肉解析WebAssembly二进制的代码段中的指令,然后实现这些指令对应的功能,这种运行方式和解释型语言没多大区别,或许是因为嵌入式环境资源限制导致的无法加载通常体积较大的Back End。

    0x06 WASI

    从上面的介绍可以看到,所有的概念和例子的语境都没有离开浏览器,正在将WebAssembly技术带离浏览器奔向更大应用场景的是WASI规范,它定义了一系列底层(特别是和系统资源相关)的操作。

    上文提及,WebAssembly是沙盒的,这对于浏览器而言很关键,但是当它脱离浏览器后,作为独立VM,和Native环境打交道就不可避免。让这些个接口规范化程度直接决定了其跨平台性。

    先贴一张WASI的终极目标示意图

    用大家都熟悉的话总结就是 "Write One,Run Everywhere",同一个编译目标能在不同平台、机器上运行。

    和Emscripten的区别

    上文提到的emcc就是Emscripten项目的一员,从本文开头的例子中,貌似emcc也能执行open read等操作,那为何还需要定义WASI呢?一个新的规范的出现必定是为了解决当前的某些问题,首先来看下Emscripten的问题。

    read函数被emcc翻译成了__syscall3(3, args),即VM/Engine需要实现一个名字叫__syscall3的函数,且函数read的多个参数将被保存在WebAssembly线性空间中,集成在参数 args 中,这被认为type unsafe

    WASI将这些POSIX函数重新定义,如下图所示,无论哪个平台的VM/Engine,只需要实现和自身平台相关的__wasi_fd_read函数给WebAssembly用即可,这样在编写WebAssembly调用read函数时就无需关心自身将会运行在哪个平台。

    WASI例子

    这里使用的不再使用上文例子中的emcc,而是使用高版本Clang,为了避免系统环境无法支持高版本Clang的情况,这里使用官方推荐的wasi-sdk-6来编译生成WASI。

    下载工具链

    下载好wasi-sdk-6后解压,例如我的解压路径是./code/wasi-sdk-6.0,可以使用如下命令编译C源码

    编写C文件

    $cat 1.c 
    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    char buf[1024];
    int main()
    {
     int fd = open("1.c", O_RDONLY);
     if (fd < 0) {
     printf("can't load file 1.c\n");
     return -1;
     }
     read(fd, buf, sizeof(buf));
     printf("Read file:\n%s",buf);
     return 0;
    }
    
    

    编译

    ./code/wasi-sdk-6.0/opt/wasi-sdk/bin/clang --target=wasm32-wasi --sysroot=./code/wasi-sdk-6.0/opt/wasi-sdk/share/sysroot 1.c -o 1.wasm

    运行WASI

    这里我们使用Wasmer的CLI来运行WASI

    wasmer run 1.wasm --mapdir ./:./

    如果顺利的话,他将打印出当前目录下1.c文件的内容。

    0x07 Tengine+WebAssembly

    Tengine作为基于Nginx的Web服务器,开源至今已有8年,在集团内几乎所有应用前都部署了Tengine作为反向代理,同时Tengine本身作为集团统一接入产品,每天处理着集团大多数入口流量。

    Tengine在WebAssembly领域也做了些尝试,目前使用了Wasmer/Wavm 作为自己底层的runtime(编译Tengine时选择具体runtime)

    下图是Tengine使用WebAssembly的框架(蓝色是为了支持WebAssembly而新增加的功能)

    由于语言的限制,Wasmer c-api和Wavm c-api是分别针对两种不同的VM提供的C的api接口,虽然两个VM都自带c-api,但是其功能都无法满足Tengine需求,所以重新使用C++和Rust编写了各自的库对应的c-api。

    Tengine C-API 实现了 加载WebAssembly、实例化WebAssembly、调用WebAssembly函数等操作,对于熟悉编写Nginx C模块的人来说,可以在任意地方调用这些函数来加载和运行WebAssembly。

    同时在WebAssembly代码里可以调用诸如ngx_wasm_callhost_get_headers、ngx_wasm_callhost_get_var等函数获取当前HTTP请求的相关信息来给WebAssembly处理。

    Wasm util实际上是 类似 一些 Lua的指令,这里Tengine暂时实现了类似content_by_lua_file的content_by_wasm_file指令用于调试功能。

    体验

    虽王婆卖瓜,但童叟无欺。这里提供了一个HTTP接口(运气好的话当你看到这篇文章的时候这个接口还在),你可以POST一个WebAssembly文件上来,Tengine帮你运行且将结果作为HTTP response body反吐给你!

    (手下留情别传太大的,日常机器资源有限)

    准备C源码

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <malloc.h>
    #include <time.h>
    #ifdef __EMSCRIPTEN__
    #define __IMPORT(name)
    #else
    #define __IMPORT(name) __attribute__((__import_module__("env"), __import_name__(#name)))
    #endif
    int ngx_wasm_callhost_say(char *str, int len) __IMPORT(_ngx_wasm_callhost_say);
    int main(void) {
     int max, len;
     char time[65], *p;
     struct timespec tp;
     max = 100 + sizeof(time);
     p = malloc(max);
     if (!p) {
     ngx_wasm_callhost_say("malloc fail", sizeof("malloc fail") - 1);
     return 0;
     }
     clock_gettime(CLOCK_REALTIME, &tp);
     len = snprintf(p ,max, "in host time %ld", tp.tv_sec);
     ngx_wasm_callhost_say(p, len);
    
     free(p);
     return 0;
    }
    
    

    编译

    emcc 1.c -o 1.wasm -s ERROR_ON_UNDEFINED_SYMBOLS=0
    
    

    或者

    ./code/wasi-sdk-6.0/opt/wasi-sdk/bin/clang --target=wasm32-wasi --sysroot=./code/wasi-sdk-6.0/opt/wasi-sdk/share/sysroot 1.c -o 1.wasm -Wl,--allow-undefined
    
    

    发送至Tengine

    curl http://11.164.24.251:8088/runwasm -H "Transfer-Encoding: chunked" --data-binary "@./1.wasm"
    
    

    正常情况下返回的内容是WebAssembly代码中ngx_wasm_callhost_say传入的内容。

    这是我自己测试的结果

    [shangxu.cjy@tengine-daily011164024251.na62sqa /home/shangxu.cjy/code/wasmtengine/alibaba-tengine]
    $curl http://11.164.24.251:8088/runwasm -H "Transfer-Encoding: chunked" --data-binary "@./1.wasm"
    in host time 1565876453
    
    

    展望

    社区

    WebAssembly技术还处于初中期阶段,特别是脱离于浏览器环境后作为独立VM/Engine,相关的定义和规范缺失,各开源实现都未能跟上。

    在Tengine尝试加载这两个WebAssembly VM时,碰到了各种VM自身的问题,包括 当 WebAssembly异常(空指针等),整个VM也就crash了,同时 也出现过更新emcc编译器后,编译出来的wasm文件无法被VM运行的情况。 期望 WebAssembly 尽早被完善。

    Tengine + 多语言

    WebAssembly 是二进制的格式的规范,理论上只要能被编译成LLVM IR的语言都能被转换成WebAssembly ,这意味着理论上,Tengine可以使用WebAssembly 作为多语言的容器,无论哪种语言都能作为Tengine模块开发且运行速度接近Native。或许在不久得将来,Tengine能够作为底层的网络容器发挥其特有的异步优势,而上层业务代码可以使用Go/Rust/C/C++甚至是JAVA,发挥不同语言的特性。

    Tengine + 安全

    WebAssembly 是沙盒的,这意味着如果WebAssembly 文件内容出现异常不会连累宿主环境(Native),特别适合运行一些危险的逻辑。

    阅读原文
    本文为云栖社区原创内容,未经允许不得转载。

    相关文章

      网友评论

        本文标题:WebAssembly初探以及在Tengine中的应用

        本文链接:https://www.haomeiwen.com/subject/vwspectx.html