使用 Docker + WasmEdge 執行 WordPress | WebAssembly:無需容器的 Docker (下)

語言: CN / TW / HK

本文翻譯自 Wasm Labs @ VMware OCTO 的 blog: WebAssembly: Docker without container。這是 Wasm Labs 在 2022 年 12 月 15 日在冬季Docker Community All Hands 7 的關於 Docker+WebAssembly 的演講的文字版。

作者:Asen Alexandrov,Wasm Labs 工程師。文中的我們均指作者或 Wasm Labs。本篇文章將更具實踐性,將以 PHP 為例帶領大家實踐 Docker + Wasm。

上篇文章我們瞭解了服務端 Wasm 為什麼有著重要的作用、什麼是 WasmEdge 以及如何讓解釋型語言編寫的程式在 Wasm 裡執行,這篇文章,我們將通過動手示例瞭解在 Docker + Wasm 背景下的 Wasm container 有什麼好處以及如何執行一個服務 WordPress 的 php.wasm 映象。

動手示例

讓我們開始吧! 在動手示例中,我們將使用編譯為 Wasm 的 PHP 直譯器。 我們會:

  • 構建一個 Wasm 容器。
  • 比較 Wasm 和原生二進位制檔案。
  • 比較傳統容器和 Wasm 容器。
  • 展示 Wasm 的可移植性

前期準備

如果想在本地重現這些示例,你需要使用以下部分或全部內容來準備你的環境:

  • WASI SDK - 從構建 C 程式碼構建 WebAssembly 應用程式
  • PHP - 為了比較而執行本機 PHP 二進位制檔案
  • WasmEdge Runtime - 執行 WebAssembly 應用程式
  • Docker Desktop + Wasm (本文寫作時,作為穩定 beta 版在 Docker Desktop4.15版可用) - 能夠執行 Wasm 容器

我們還充分運用 webassembly-language-runtimes repo,它提供了將 PHP 直譯器構建為 WebAssembly 應用程式的方法。 可以像這樣檢視 demo 分支:

git clone --depth=1 -b php-wasmedge-demo \
   https://github.com/vmware-labs/webassembly-language-runtimes.git wlr-demo
cd wlr-demo

構建一個 Wasm 容器

第一個示例,我們將展示如何構建基於 C 的應用程式,例如 PHP 直譯器。

該構建使用 WASI-SDK 工具集。 它包括一個可以構建到 wasm32-wasi 目標的 clang 編譯器,以及在 WASI 之上實現基本 POSIX 系統呼叫介面的 wasi-libc。 使用 WASI SDK,我們可以從 PHP 的程式碼庫中構建一個用 C 編寫的 Wasm 模組,。之後,我們需要一個非常簡單的基於 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 是用 autoconfmake 構建的。 所以如果你看一眼指令碼 scripts/wl-build.sh ,你會注意到我們設定了所有相關變數,如 CCLDCXX 等,以使用來自 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

然後,進一步深入檢視 php/php-7.4.32/wl-build.sh,可以看到像通常一樣,我們使用 autoconf 構建過程。

./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

WASI 是一項正在進行的工作,許多 POSIX 呼叫仍然不能在它之上實現。 因此,要構建 PHP,我們必須在原始程式碼庫之上應用多個補丁。

我們在上面看到輸出二進位制檔案會轉到 build-output/php/php-7.4.32。 在下面的示例中,我們將使用專門為 WasmEdge 構建的 php-wasmedge 二進位制檔案,因為它提供服務端 socket 支援,服務端 socket 支援還不是 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

我們在下面的例子中使用這個 build-output/php/php-7.4.32/bin/php-wasmedge-aot 二進位制碼。要了解有關 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.wasm

ENTRYPOINT [ "php.wasm" ]

我們還可以在映象新增更多內容,當 Docker 執行它時,Wasm 二進位制檔案可以訪問這些內容。 例如,在 images/php/Dockerfile.server 中,我們還添加了一些 docroot 內容,在容器啟動時由 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 /docroot

ENTRYPOINT [ "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 .

原生 vs Wasm

現在讓我們將原生 PHP 二進位制檔案與 Wasm 二進位制檔案在本地和 Docker 容器中分別進行比較。 我們將使用相同的 index.php 檔案並將執行它時得到的結果與以下內容進行比較:

  • php
  • php-wasmedge-aot
  • 在傳統容器中執行的 php
  • 在 Wasm 容器中執行的 php-wasmedge-aot

在下面所有的示例中,我們使用同樣的 images/php/docroot/index.php 檔案,讓我們來看一下。簡而言之,該指令碼將:

  • 使用 phpversionphp_uname 展示直譯器版本和它執行的平臺
  • 列印指令碼可以訪問的所有環境變數的名稱
  • 列印一條包含當前時間和日期的問候訊息
  • 列出根資料夾的內容 /
<html>
<body>
<h1>Hello from PHP <?php echo phpversion() ?> running on "<?php echo php_uname()?>"</h1>

<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>


Native PHP 執行 index.js

我們使用本地 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 執行 index.js

如果我們在 WasmEdge 使用 php-aot-wasm 我們看到

  • 一個 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 執行 index.js

當我們從一個傳統的容器中使用 php 時我們看到

  • 基於 Linux 的平臺
  • 指令碼有權訪問的 14 個環境變數的列表
  • 帶有當前時間和日期的問候訊息
  • 包含根資料夾內容的列表 /

與在主機上使用 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 在一個容器中執行 index.js

如果我們在 WasmEdge 使用 php-aot-wasm 我們看到

  • 一個 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>

傳統容器 vs Wasm 容器

我們構建並運行了一個 Wasm 二進位制檔案,並將其作為容器執行。 我們看到了 Wasm 和傳統容器之間的輸出差異以及 Wasm 帶來的高階“沙箱隔離”。我們可以輕鬆看到的兩種容器之間的其他差異。

首先,我們將執行兩個 daemon 容器,看看我們如何解釋有關它們的一些統計資訊。 然後我們將檢查容器映象的差異。

容器資料

讓我們執行兩個 daemon 容器 - 一個是從傳統的 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

但是如果我們看 docker stats,我們只看到傳統容器的資料。這之後可能會變化,因為 Docker+Wasm 現在是 beta 版特性。 所以,如果真的想看看發生了什麼,可以改為監視對照組。 每個傳統容器都有自己的控制組,如 docker/ee44...。另一方面,Wasm 容器作為 podruntime/docker 控制組的一部分包含在內,可以間接觀察它們的 CPU 或記憶體消耗。

$ 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,我們只需要在容器內新增可執行二進位制檔案,而對於傳統容器,我們仍然需要來自執行二進位制檔案的作業系統的一些基本庫和檔案。這種大小差異對於第一次拉取映象的速度以及進行在本地儲存庫中佔用的空間非常有幫助。

Wasm 可移植性

Wasm 最大優勢之一就是它的可移植性。 當人們想要一個可移植的應用程式時,Docker 已經提供了傳統的容器作為一種選擇。 然而,除了映象特別大之外,傳統容器還繫結到它們執行的平臺架構。 作為程式設計師,相比許多人都經歷過這種坎坷:針對不同的架構,必須構建支援的軟體版本,併為每種架構打包對應映象。

WebAssembly 帶來了真正的可移植性。 構建一次二進位制檔案,就能在任何地方執行它。 作為這種可移植性的證明,我們準備了幾個通過我們為 WebAssembly 構建的 PHP 直譯器執行 WordPress 的示例。

當 PHP 作為獨立的 Wasm 應用程式執行時,它會為 WordPress 提供服務。 它也可以在 Docker+Wasm 容器中執行。 此外,它還能在嵌入 Wasm 執行時的任何應用程式中執行。 在我們的示例中,這是 apache httpd,它可以通過 mod_wasm 使用 Wasm 應用程式作為內容處理程式。 最後,PHP.wasm 也可以在瀏覽器中執行。

通過 WasmEdge 服務 WordPress

我們為本次演示準備了一個緊湊的 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

現在我們有了 WordPress,讓我們新增伺服器:

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

可以訪問 http://localhost:8085 ,使用由 PHP Wasm 直譯器服務的 WordPress。

通過 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

可以訪問 http://localhost:8086 並使用由 PHP Wasm 直譯器服務的 WordPress,這回是在 Docker 容器中執行。

通過 mod_wasm in Apache HTTPD 服務 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

可以訪問 http://localhost:8087 並使用由 PHP Wasm 直譯器服務的 WordPress,它由 Apache HTTPD 中的 mod_wasm 載入。

直接在瀏覽器中服務 WordPress

訪問 https://wordpress.wasmlabs.dev 獲得示例。 你將看到一個框架,其中 PHP Wasm 直譯器會現場渲染 WordPress。

結論

感謝閱讀本文。 需要消化的內容很多,但我們希望本文有助於理解 WebAssembly 的能力以及它如何與你現有的程式碼庫和工具(包括 Docker)結合執行。 期待看到你使用 Wasm 程式設計!

如果你覺得 WasmEdge 不錯,不要忘了給我們點個贊!

https://github.com/WasmEdge/WasmEdge