最近 Docker 宣布与 WasmEdge 合作支持 WebAssembly。
本文将解释什么是 WebAssembly,为什么它与 Docker 生态系统相关,并提供一些实践案例供你尝试。我们假设你熟悉 Docker 工具。我们将使用我们在 PHP 的 WebAssembly 端口上的工作来演示如何构建 PHP 解释器,将其打包为 OCI 镜像的一部分并使用 Docker 运行它。
请注意,本文侧重于获得一些实践经验,而不是讨论技术细节。你可以重现下面的案例,也可以通读它们直到最后,因为我们还将提供输出。
这是一个非常基本的介绍。如果你已经熟悉该技术,则可以跳到下一节。
什么是 WebAssembly?
WebAssembly(或 Wasm)是一种二进制指令格式的开放标准,它允许从不同的源语言创建可移植的二进制可执行文件。
这些二进制文件可以在各种环境中运行。它起源于网络,并得到所有主要浏览器的支持。
Wasm 如何在浏览器中工作?
浏览器引擎集成了一个 Wasm 虚拟机,通常称为 Wasm 运行时,可以运行 Wasm 二进制指令。有编译器工具链(如 Emscripten)可以将源代码编译为 Wasm 目标。这允许将遗留应用程序移植到浏览器并直接与在客户端 Web 应用程序中运行的 JS 代码通信。
这些技术允许传统的桌面应用程序在浏览器中运行。现在它们可以在任何装有浏览器的设备上运行。著名的例子有 Google Earth[1] 和用于计算机视觉的 Open CV[2] 库。
Wasm 如何在服务器上运行?
有可以在浏览器之外运行的 Wasm 运行时,包括 Linux、Windows 和 macOS 等传统操作系统。因为他们不能依赖可用的 JavaScript 引擎,所以他们使用不同的接口与外界通信,例如 WASI,即 WebAssembly 系统接口。这些运行时允许 Wasm 应用程序以与 POSIX 类似(但不完全相同)的方式与其主机系统交互。WASI SDK 和 wasi-libc 等项目帮助人们将现有的 POSIX 兼容应用程序编译为 WebAssembly。
你只需一次将应用程序编译成 Wasm 模块,然后就可以在任何地方运行完全相同的二进制文件。
Wasm 有什么优势?
使 Wasm 在浏览器中表现出色的一些特性也使其对服务器端开发具有吸引力:
Open,这是业界广泛采用的标准。与过去的浏览器大战相反,各大公司正在合作实现 WASI 和 WebAssembly 应用程序的标准化。
快速,它可以通过大多数运行时的 JIT/AOT 功能提供类似原生的速度。与启动 VM 或启动容器不同,没有冷启动。
安全,默认情况下,Wasm 运行时是沙盒的,允许安全访问内存。基于能力的模型确保 Wasm 应用程序只能访问明确允许的内容。有更好的供应链安全。
可移植,在几个主要的运行时中,支持大多数 CPU(x86、ARM、RISC-V)和大多数操作系统。包括 Linux、Windows、macOS、Android、ESXi,甚至是非 Posix 操作系统。
高效,可以使 Wasm 应用程序以最小的内存占用和 CPU 要求运行。
Polyglot,40 多种语言可以编译成 Wasm,具有现代的、不断改进的工具链。
服务器平台发展的下一步是什么?
你可能已经看到 Solomon Hykes(Docker 的联合创始人之一)的这句话:
事实上,WASM+WASI 似乎确实是服务器端软件基础设施发展的下一步。
过去,我们有物理硬件可以使用。我们会在每个盒子上精心安装操作系统和应用程序,并一一维护。
然后随着 VMware 开创的 VM 的采用,事情变得更容易了。人们可以跨硬件盒复制、克隆和移动虚拟机。但这仍然需要在 VM 中安装操作系统和应用程序。
然后出现了由 Docker 推广的容器,它使得在简约的包装上下文中运行应用程序配置变得更加容易,而不会影响主机操作系统上的任何其他应用程序。但是,这仍然需要分发与其运行时和必要的库捆绑在一起的应用程序。安全边界由 Linux 内核提供。
我们现在有了 WebAssembly。它的技术特性和可移植性使得分发应用程序成为可能,无需运送操作系统级别的依赖项,并且可以在严格的安全约束下运行。
鉴于所有这些,开发人员通常将 WebAssembly 视为容器的“继承者”和基础设施部署的下一个合乎逻辑的步骤。
然而,另一种看待 WebAssembly 的方式是将其作为 Docker 工具的替代“后端”。你可以使用相同的命令行工具和工作流,但不是使用 Linux 容器,而是使用基于 WebAssembly 的容器等效项来实现。本文的其余部分探讨了这个概念,这就是我们标题所说的“没有容器的 Docker”。
Wasm 如何与 Docker 协同工作?
Docker Desktop 现在包括对 WebAssembly 的支持。它是通过一个 containerd shim 实现的,该 shim 可以使用一个名为 WasmEdge 的 Wasm 运行时来运行 Wasm 应用程序。这意味着,你现在可以在 WasmEdge 运行时中运行 Wasm 应用程序,而不是典型的 Windows 或 Linux 容器,它们会运行容器镜像中二进制文件的单独进程,模拟容器。
因此,容器镜像不需要包含正在运行的应用程序的操作系统或运行时上下文——单个 Wasm 二进制文件就足够了。
这在 Docker 的 Wasm 技术预览文章[3]中有详细解释。
什么是 WasmEdge?
WasmEdge 是一个高性能的 WebAssembly 运行时,它:
是开源的,是 CNCF 的一部分。
支持所有主要的 CPU 架构(x86、ARM、RISC-V)。
支持所有主要操作系统(Linux、Windows、macOS)以及其他操作系统,例如 seL4 RTOS、Android。
针对云原生和边缘应用程序进行了优化。
可扩展并支持标准和新兴技术:
使用 Tensorflow、OpenVINO、PyTorch 进行人工智能推理
与 Tokio 异步联网。支持微服务、数据库客户端、消息队列等。
与容器生态系统、Docker 和 Kubernetes 无缝集成(如本文所讲!)
解释型语言呢?
到目前为止,我们只提到了 C 和 Rust 等编译语言可以针对 WebAssembly。对于 Python、Ruby 和 PHP 等解释型语言,方法有所不同:它们的解释器是用 C 编写的,可以编译为 WebAssembly。然后这个解释编译成 Wasm 可以用来执行源代码文件,通常以 .py、.rb、.php 等结尾。一旦编译为 Wasm,任何具有 Wasm 运行时的平台都将能够运行这些解释语言,即使实际的解释器从未为该平台本地编译过。
让我们开始吧!在动手案例中,我们将使用编译为 Wasm 的 PHP 解释器。我们会:
构建一个 Wasm 容器
比较 Wasm 和本机二进制文件
比较传统容器和 Wasm 容器
展示 Wasm 的可移植性
如果你想在本地重现这些案例,你需要使用以下部分或全部内容来准备你的环境:
WASI SDK——从遗留 C 代码构建 WebAssembly 应用程序
PHP——为了比较而运行本机 PHP 二进制文件
WasmEdge 运行时——运行 WebAssembly 应用程序
Docker Desktop + Wasm(在撰写本文时,在 4.15 版中作为稳定测试版提供)能够运行 Wasm 容器
我们还利用 “Wasm Language Runtimes”[4] 存储库,它提供了将 PHP 解释器构建为 WebAssembly 应用程序的方法。
你可以像这样检查演示分支:
git clone --depth=1 -b php-wasmedge-demo \
https://github.com/vmware-labs/webassembly-language-runtimes.git wlr-demo
cd wlr-demo
作为第一个案例,我们将展示如何构建基于 C 的应用程序,例如 PHP 解释器。
该构建使用 WASI-SDK 工具集。它包括一个可以构建到 wasm32-wasi 目标的 clang 编译器,以及在 WASI 之上实现基本 POSIX 系统调用接口的 wasi-libc。使用 WASI SDK,我们可以从 PHP 的代码库中构建一个 Wasm 模块,用 C 编写。之后,我们需要一个非常简单的基于 scratch 的 Dockerfile 来制作一个可以使用 Docker + Wasm 运行的 OCI 镜像。
构建 WASM 二进制文件
假设你位于 wlr-demo 作为先决条件部分的一部分签出的文件夹中,你可以运行以下命令来构建 Wasm 二进制文件。
export WASI_SDK_ROOT=/opt/wasi-sdk/
export WASMLABS_RUNTIME=wasmedge./wl-make.sh php/php-7.4.32/ && tree build-output/php/php-7.4.32/bin/
... ( a few minutes and hundreds of build log lines)
build-output/php/php-7.4.32/bin/
├── php-cgi-wasmedge
└── php-wasmedge
PHP 是用 autoconf 和 make 构建的。因此,如果你查看 scripts/wl-build.sh 脚本,你会注意到我们设置了所有相关变量,如 CC、LD、CXX 等,以使用来自 WASI_SDK 的编译器。
export WASI_SYSROOT="${WASI_SDK_ROOT}/share/wasi-sysroot"
export CC=${WASI_SDK_ROOT}/bin/clang
export LD=${WASI_SDK_ROOT}/bin/wasm-ld
export CXX=${WASI_SDK_ROOT}/bin/clang++
export NM=${WASI_SDK_ROOT}/bin/llvm-nm
export AR=${WASI_SDK_ROOT}/bin/llvm-ar
export RANLIB=${WASI_SDK_ROOT}/bin/llvm-ranlib
./configure --host=wasm32-wasi host_alias=wasm32-musl-wasi \
--target=wasm32-wasi target_alias=wasm32-musl-wasi \
${PHP_CONFIGURE} || exit 1
...
make -j ${MAKE_TARGETS} || exit 1
我们在上面看到输出二进制文件转到 build-output/php/php-7.4.32。在下面的案例中,我们将使用 php-wasmedge 专门为 WasmEdge 构建的二进制文件,因为它提供服务器端套接字支持,这还不是 WASI 的一部分。
优化二进制文件
Wasm 是一个虚拟指令集,因此任何运行时的默认行为都是即时解释这些指令。当然,这在某些情况下可能会使事情变慢。因此,为了通过 WasmEdge 获得两全其美的效果,你可以创建一个 AOT(提前)优化的二进制文件,它可以在当前机器上本地运行,而且仍然可以在其他机器上进行解释。
要创建优化的二进制文件,请运行以下命令:
wasmedgec --enable-all --optimize 3 \
build-output/php/php-7.4.32/bin/php-wasmedge \
build-output/php/php-7.4.32/bin/php-wasmedge-aot
构建 OCI 镜像
现在我们有了一个二进制文件,我们可以将它包装在一个 OCI 镜像中。
让我们来看看 images/php/Dockerfile.cli。我们需要做的就是复制 Wasm 二进制文件并将其设置为 ENTRYPOINT。
FROM scratch
ARG PHP_TAG=php-7.4.32
ARG PHP_BINARY=php
COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasmENTRYPOINT [ "php.wasm" ]
FROM scratch
ARG PHP_TAG=php-7.4.32
ARG PHP_BINARY=php
COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasm
COPY images/php/docroot /docrootENTRYPOINT [ "php.wasm" , "-S", "0.0.0.0:8080", "-t", "/docroot"]
基于上述文件,我们可以轻松地在本地建立我们的 php-wasm 镜像。
docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot -f images/php/Dockerfile.cli .
docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-server-aot -f images/php/Dockerfile.server .
现在让我们将本机 PHP 二进制文件与 Wasm 二进制文件进行比较。在本地和 Docker 容器中。我们将使用相同的 index.php 文件并将运行它时获得的结果与以下内容进行比较:
php
php-wasmedge-aot
php 在传统容器中
php-wasmedge-aot 在 Wasm 容器中
我们将在下面所有的例子中使用相同的 images/php/docroot/index.php 文件,所以让我们看一下。简而言之,这个脚本将:
使用 phpversion 和 php_uname显示解释器版本和它运行的平台
打印脚本可以访问的所有环境变量的名称
打印一条包含当前时间和日期的问候消息
列出根文件夹的内容 /
<h2>List env variable names</h2>
<?php
$php_env_vars_count = count(getenv());
echo "Running with $php_env_vars_count environment variables:\n";
foreach (getenv() as $key => $value) {
echo $key . " ";
}
echo "\n";
?>
<h2>Hello</h2>
<?php
$date = getdate();
$message = "Today, " . $date['weekday'] . ", " . $date['year'] . "-" . $date['mon'] . "-" . $date['mday'];
$message .= ", at " . $date['hours'] . ":" . $date['minutes'] . ":" . $date['seconds'];
$message .= " we greet you with this message!\n";
echo $message;
?>
<h2>Contents of '/'</h2>
<?php
foreach (array_diff(scandir('/'), array('.', '..')) as $key => $value) {
echo $value . " ";
}
echo "\n";
?>
</body>
</html>
当我们使用本机 php 二进制文件时,我们会看到一个基于 Linux 的平台:
58个环境变量的列表,如果需要,脚本可以访问这些变量
`/``中所有文件和文件夹的列表,如果需要的话,脚本也可以访问这些文件和文件夹
$ php -f images/php/docroot/index.php
<html>
<body>
<h1>Hello from PHP 7.4.3 running on "Linux alexandrov-z01 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64"</h1><h2>List env variable names</h2>
Running with 58 environment variables:
SHELL NVM_INC WSL2_GUI_APPS_ENABLED rvm_prefix WSL_DISTRO_NAME TMUX rvm_stored_umask TMUX_PLUGIN_MANAGER_PATH MY_RUBY_HOME NAME RUBY_VERSION PWD NIX_PROFILES LOGNAME rvm_version rvm_user_install_flag MOTD_SHOWN HOME LANG WSL_INTEROP LS_COLORS WASMTIME_HOME WAYLAND_DISPLAY NIX_SSL_CERT_FILE PROMPT_COMMAND NVM_DIR rvm_bin_path GEM_PATH GEM_HOME LESSCLOSE TERM CPLUS_INCLUDE_PATH LESSOPEN USER TMUX_PANE LIBRARY_PATH rvm_loaded_flag DISPLAY SHLVL NVM_CD_FLAGS LD_LIBRARY_PATH XDG_RUNTIME_DIR PS1 WSLENV XDG_DATA_DIRS PATH DBUS_SESSION_BUS_ADDRESS C_INCLUDE_PATH NVM_BIN HOSTTYPE WASMER_CACHE_DIR IRBRC PULSE_SERVER rvm_path WASMER_DIR OLDPWD BASH_FUNC_cr-open%% _<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 12:0:36 we greet you with this message!<h2>Contents of '/'</h2>
apps bin boot dev docroot etc home init lib lib32 lib64 libx32 lost+found media mnt nix opt path proc root run sbin snap srv sys tmp usr var wsl.localhost</body>
</html>
当我们 php-aot-wasm 与 Wasmedge 一起使用时,我们会看到:
一个 wasi/wasm32 平台
没有环境变量,因为没有显式暴露给 Wasm 应用程序
Wasm 应用程序未获得显式访问权限,/ 因此尝试列出其内容失败并出现错误
当然,为了让 php-wasmedge-aot 有权限读取 index.php 文件,我们必须向 WasmEdge 明确说明,我们要预先打开 images/php/docroot,以便在 Wasm 应用程序的上下文中作为 /docroot 访问。
这很容易显示 Wasm 除了便携性之外的最大优势之一。我们获得了更好的安全性,因为除非明确说明,否则无法访问任何内容。
$ wasmedge --dir /docroot:$(pwd)/images/php/docroot \
build-output/php/php-7.4.32/bin/php-wasmedge-aot -f /docroot/index.php<html>
<body>
<h1>Hello from PHP 7.4.32 running on "wasi (none) 0.0.0 0.0.0 wasm32"</h1><h2>List env variable names</h2>
Running with 0 environment variables:<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 10:8:46 we greet you with this message!<h2>Contents of '/'</h2>
Warning: scandir(/): failed to open dir: Capabilities insufficient in /docroot/index.php on line 27
Warning: scandir(): (errno 76): Capabilities insufficient in /docroot/index.php on line 27
Warning: array_diff(): Expected parameter 1 to be an array, bool given in /docroot/index.php on line 27
Warning: Invalid argument supplied for foreach() in /docroot/index.php on line 27
</body>
</html>
当我们使用传统容器中的 php 时,我们会看到:
一个 wasi/wasm32 平台
没有环境变量,因为没有显式暴露给 Wasm 应用程序
Wasm 程序没有被赋予对 / 的明确访问权,因此试图列出其内容时出现了错误
php 与在主机上运行它相比,已经有了更好的区别。由于环境变量和内容 / 是“虚拟的”并且只存在于容器内。
docker run --rm \
-v $(pwd)/images/php/docroot:/docroot \
php:7.4.32-cli \
php -f /docroot/index.php<html>
<body>
<h1>Hello from PHP 7.4.32 running on "Linux 227b2bc2f611 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64"</h1><h2>List env variable names</h2>
Running with 14 environment variables:
HOSTNAME PHP_INI_DIR HOME PHP_LDFLAGS PHP_CFLAGS PHP_VERSION GPG_KEYS PHP_CPPFLAGS PHP_ASC_URL PHP_URL PATH PHPIZE_DEPS PWD PHP_SHA256<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 10:15:35 we greet you with this message!<h2>Contents of '/'</h2>
bin boot dev docroot etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var</body>
</html>
当我们 php-aot-wasm 与 Wasmedge 一起使用时,我们会看到:
一个 wasi/wasm32 平台
只有 2 个基础设施环境变量,使用在 containerd 中运行的 WasmEdge shim 预先设置
容器内所有文件和文件夹的列表,/明确地预先打开以供 Wasm 应用程序访问(WasmEdge shim 中的逻辑的一部分)
注意:如果你更细心,你会发现要从这个镜像运行一个容器,我们必须:
通过 --runtime=io.containerd.wasmedge.v1 明确说明运行时,直接向 php.wasm 传递命令行参数,而不包括二进制文件本身。往回滚动,我们可以明确地用传统的 PHP 容器写出完整的命令,包括 php 二进制文件(不是说一定要这样)。
最后一点,即使使用 Docker,Wasm 也加强了运行 index.php 的安全性,因为暴露给它的要少得多。
docker run --rm \
--runtime=io.containerd.wasmedge.v1 \
-v $(pwd)/images/php/docroot:/docroot \
ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot \
-f /docroot/index.php<html>
<body>
<h1>Hello from PHP 7.4.32 running on "wasi (none) 0.0.0 0.0.0 wasm32"</h1><h2>List env variable names</h2>
Running with 2 environment variables:
PATH HOSTNAME<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 11:33:10 we greet you with this message!<h2>Contents of '/'</h2>
docroot etc php.wasm</body>
</html>
我们设法构建并运行了一个 Wasm 二进制文件,并将其作为容器运行。我们看到了 Wasm 和传统容器之间的输出差异以及 Wasm 带来的高级“沙盒”。让我们来看看我们可以轻松看到的两种容器之间的其他差异。
首先,我们将运行两个守护进程容器,看看我们如何解释有关它们的一些统计信息。然后我们将检查容器镜像的差异。
容器统计
让我们运行两个守护进程容器——一个来自传统 php 镜像,另一个来自 php-wasm 镜像。
docker run --rm -d \
-p 8083:8080 -v $(pwd)/images/php/docroot:/docroot \
php:7.4.32-cli \
-S 0.0.0.0:8080 -t /docroot
docker run --rm -d \
--runtime=io.containerd.wasmedge.v1 \
-p 8082:8080 -v $(pwd)/images/php/docroot:/docroot \
ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot
-S 0.0.0.0:8080 -t /docroot
$ systemd-cgtop -kP --depth=10
Control Group Tasks %CPU Memory
podruntime 145 0.1 636.3M
podruntime/docker 145 0.1 636.3M
docker 2 0.0 39.7M
docker/ee444b... 1 0.0 6.7M
首先,探索镜像,我们看到 Wasm 容器镜像比传统镜像小得多。甚至 alpine 版本的 php 容器也比 Wasm 大。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
php 7.4.32-cli 680c4ba36f1b 2 hours ago 166MB
php 7.4.32-cli-alpine a785f7973660 2 minutes ago 30.1MB
ghcr.io/vmware-labs/php-wasm 7.4.32-cli-aot 63460740f6d5 44 minutes ago 5.35MB
这种大小差异对于第一次拉取镜像的速度以及镜像在本地存储库中占用的空间非常有益。
Wasm 最好的地方之一是它的可移植性。当人们想要一个可移植的应用程序时,Docker 已经使传统的容器成为一种方式。然而,在大的镜像尺寸之上,传统的容器也被束缚在它们所运行的平台的架构上。我们中的许多人都经历过不得不构建支持不同架构的软件版本,并为每个架构打包成不同的镜像。
WebAssembly 带来了真正的可移植性。你可以构建一次二进制文件并在任何地方运行它。作为这种可移植性的证明,我们准备了几个通过我们为 WebAssembly 构建的 PHP 解释器运行 WordPress 的案例。
当 PHP 作为独立的 Wasm 应用程序运行时,它会为 WordPress 提供服务。它也可以在 Docker+Wasm 容器中运行。此外,它可以在嵌入 Wasm 运行时的任何应用程序中运行。在我们的案例中,这是 apache httpd,它可以通过 mod_wasm 使用 Wasm 应用程序作为内容处理程序。最后,PHP.wasm 也可以在浏览器中运行。
我们为本次演示准备了一个紧凑的 WordPress+Sqlite 案例。由于是 ghcr.io/vmware-labs/php-wasm:7.4.32-server-wordpress 容器镜像的一部分,我们先下载到本地。
此命令将只创建一个临时容器(拉镜像),将 WordPress 文件复制到 /tmp/wp/docroot 中,然后删除容器。
container_id=$(docker create ghcr.io/vmware-labs/php-wasm:7.4.32-server-wordpress) && \
mkdir /tmp/wp && \
docker cp $container_id:/docroot /tmp/wp/ && \
docker rm $container_id
wasmedge --dir /docroot:/tmp/wp/docroot \
build-output/php/php-7.4.32/bin/php-wasmedge-aot \
-S 0.0.0.0:8085 -t /docroot
通过 Docker+Wasm 服务 WordPress
自然地,有了 Docker,事情就简单多了。
docker run --rm --runtime=io.containerd.wasmedge.v1 \
-p 8086:8080 -v /tmp/wp/docroot/:/docroot/ \
ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot
-S 0.0.0.0:8080 -t /docroot
在 Apache HTTPD 中通过 mod_wasm 服务 WordPress
Apache HTTPD 是使用最广泛的 HTTP 服务器之一。现在有了 mod_wasm,它还可以运行 WebAssembly 应用程序。为了避免在本地安装和配置它,我们准备了一个容器,其中包含 Apache HTTPD、mod_wasm 和 WordPress。
docker run -p 8087:8080 projects.registry.vmware.com/wasmlabs/containers/php-mod-wasm:wordpress
直接在浏览器中提供 WordPress
只需到 https://wordpress.wasmlabs.dev 看个例子。你将看到一个框架,其中 PHP Wasm 解释器正在现场渲染 WordPress。
感谢你阅读本文。需要消化的内容很多,但我们希望它有助于理解 WebAssembly 的功能以及它如何与你现有的代码库和工具(包括 Docker)一起工作。期待看到你使用 Wasm 构建的内容!
相关链接:
《Docker中Image、Container与Volume的迁移》
免责声明:本文内容来源于网络,所载内容仅供参考。转载仅为学习和交流之目的,如无意中侵犯您的合法权益,请及时联系Docker中文社区!