试试将.NET7编译为WASM在Docker上运行

之前有听到说 Docker 支持 Wasmtime 了,刚好.NET7 也支持 WASM,就带大家来了解一下这个东西,顺便试试它怎么样。

因为WASM(WebAssembly) 一开始是一个给浏览器的技术,比起 JS 解释执行,WASM 能用于提升浏览器的用户体验,因为在一些场景中它有着比 JS 更好的性能。

大家可以将 WASM 理解为 C#的 MSIL 或者 Java 的字节码,它并不是二进制代码,还是会由 JIT 编译执行,JIT 有很多优化,另外大多数场景也只会 JIT 一次,加上省略了 JS 加载,语法分析各种的过程,才会有着比 JS 更好的性能。.

另外因为 WASM 是中间码的格式,所以理论上任何语言 C#、RUST、Java、Go 都可以将代码编译为 WASM,然后放到浏览器中执行。比如 C#火热的 Blazor 项目,就是将 C#编译为 WASM,然后使 C#代码能在浏览器中运行。

另外聊一聊WASI(WebAssembly System Interface),我们知道 WASM 有着不错的可移植性和安全性(目前浏览器运行都是沙箱运行,对于权限管控很严格),那么就有一群大佬就说,我们是不是能脱离浏览器单独运行 WASM 程序呢?于是就产生了一个标准的系统接口,大家都按照这样的方式来生成 WASM,调用系统 API,然后我们开发一个 Runtime,让大家的 WASM 程序都能在这上面运行。

举个不严谨的例子说明一下 WASI 就是比如:

  • C# => MSIL => CLR(Mono、CoreCLR)
  • Java => 字节码 => JVM(HotSpot VM、ZingVM) 而现在我们可以:
  • C# => WASM => WASI(wasmtime、wasmedge)。

各位应该就明白了,WASI 其实就是个运行时的规范,大家编译成 WASM 放上去就能跑。试试将.NET7编译为WASM在Docker上运行

所以现在对于它的观点就是,觉得它在 Server 后端领域目前来说不是一个很价值的东西,因为可移植性好的语言比比皆是,比如 C#、Java、Go 等等。

拿性能来说,对于这样的中间语言性能无关就是 JIT 和 GC,WASI 的 JIT 和 GC 能做的像 C#、Java 这样的 JIT、GC 性能那么好吗?这个目前来说是存在疑问的,至少在短时间内很难追平其它平台十多年的优化。

再说 WASM 的另一个优点,就是体积小和启动快,现在 C#支持 NativeAOT、Java 有 GraalVM、Go 和 Rust 之类的本身就是编译型语言,启动速度和体积都很不错,WASM 在这个方面其实不占优势。

.NET 编译为 WASM

好了,言归正传,我们来试试.NET7 上面的 WASM。.NET7 目前已经发布,我们需要使用最新的版本,如下图所示:试试将.NET7编译为WASM在Docker上运行

然后我们创建一个简单的控制台项目,用于输出斐波那契数列和执行耗时,代码如下所示 (这并不性能最优的实现,只是这样子实现简单)

using System.Diagnostics;

namespace PublishDotNetToWASM;

public static class Program
{
    public static void Main()
    {
        // warm
        ulong sum = 0;
        foreach (var i in Fibonacci().Take(1000))
        {
            sum += i;
        }

        // run
        sum = 0;
        var sw = Stopwatch.StartNew();
        foreach (var i in Fibonacci().Take(100000))
        {
            sum += i;
        }
        sw.Stop();
        Console.WriteLine($"Result:{sum}, Timespan:{sw.ElapsedTicks} Ticks");
    }

    private static IEnumerable<ulong> Fibonacci()
    {
        ulong current = 1, next = 1;

        while (true)
        {
            yield return current;
            next = current + (current = next);
        }
    }
}

接下来为了将.NET 程序发布成 WASM,我们需要安装Wasi.Sdk预览包,这个预览包是Steve Sanderson大佬做的支持,可以将.NET 程序编译为 WASM,截止至目前版本信息如下所示:

<PackageReference Include="Wasi.Sdk" Version="0.1.2-preview.10061" />

运行dotnet publish -c Release命令,将我们的应用程序发布为 WASM 格式,在发布过程中,需要下载MinGW作为编译器,网络环境不好的同学,需要想办法科学上网,稍微等待一会就顺利的发布成功了:试试将.NET7编译为WASM在Docker上运行

运行 WASM 程序

此时我们可以安装一下Wasmtime来执行我们的程序,通过https://wasmtime.dev/下载安装:试试将.NET7编译为WASM在Docker上运行

然后就可以直接使用wasmtime命令运行我们的程序,我分别使用wasmtimedotnet运行了我们的程序:试试将.NET7编译为WASM在Docker上运行

可见目前来说 WASM 的性能还是惨不忍睹的,等一等后续的优化吧。

将.NET 发布到 Docker WASI

再来看看我们的 Docker,对于 Docker 支持 WASI 我感到并不意外,因为 Docker 的容器化对于直接执行的 WASM 来说还是比较重,支持它是一个拓宽影响力的好事。具体的执行模型如下所示,对于 WASM 应用有着不同的执行方式。不再使用runc而是wasmedge

试试将.NET7编译为WASM在Docker上运行

wasmedge也是一个实现了 WASI 标准的 WASM 运行时,和上文提到的 wasmtime 一样。

要实现在 Docker 上运行 WASM 程序需要安装 Docker 的预览版,链接https://docs.docker.com/desktop/wasm/试试将.NET7编译为WASM在Docker上运行

然后我们整一个 Dockerfile,我们直接依赖 scratch 镜像即可,因为它不需要其它的基础镜像(暂时我没有使用.NET7 的多段构建镜像,听大佬说目前貌似有问题)。

FROM scratch
COPY ./bin/Release/net7.0/PublishDotNetToWASM.wasm /PublishDotNetToWASM.wasm
ENTRYPOINT [ "PublishDotNetToWASM.wasm" ]

再使用下面的命令构建 Docker 镜像,由于是 wasm 镜像,所以需要带额外的参数。

docker buildx build --platform wasi/wasm32 -t publishdotnettowasm .

可以看到打包出来的镜像是非常小的,只有 3.68MB。试试将.NET7编译为WASM在Docker上运行

运行的话也很简单,用下方的命令即可,需要指定 runtime 为io.containerd.wasmedge.v1,另外也需要指定 paltform。

docker run --rm --name=publishdotnettowasm --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm32 publishdotnettowasm

我把 dotnet 原生运行、wasmtime 运行、docker WASI 运行都跑了一下,可以发现目前来说是惨不忍睹。试试将.NET7编译为WASM在Docker上运行

总结

以上就是如何将.NET7 程序发布到 WASM,然后在 Docker 最新的 WASI 中运行的样例,目前来看基本的运行都已经 OK,不过正如我前面提到的,现在性能还是太受影响了。

这不仅仅是在.NET 平台上,其它语言 Rust、C、C++编译为 WASM 上都有明显的性能下降。试试将.NET7编译为WASM在Docker上运行

思来想去可能在一些插件化和不需要性能很好的场景 WASI 会比较用。不过这些都需要时间慢慢见证,毕竟存在即合理,像 JS 这样的语言不一样好好的?

我们可以拭目以待,看看 WASM/WASI 会不会给我们带来其它惊喜,期待后续 Steve Sanderson 大佬和 WASM 社区的相关优化。

源码链接

https://github.com/InCerryGit/PublishDotNetToWASM