Полгода назад Docker объявил о поддержке WebAssembly на базе WasmEdge. В этой статье расскажем, что такое WebAssembly, почему эта технология актуальна для экосистемы Docker, а также приведем несколько практических примеров. Предполагается, что вы знакомы с инструментарием Docker. Чтобы продемонстрировать, как создать интерпретатор PHP, упаковать его как часть OCI-образа и запустить с помощью Docker, воспользуемся нашим WebAssembly-портом PHP. Обратите внимание: эта статья скорее направлена на получение практического опыта, нежели на обсуждение технических тонкостей. Вы можете воспроизвести приведенные ниже примеры сами или довериться нам — результат выполнения всех команд мы публикуем. В этом разделе приводятся базовые сведения. Его можно пропустить, если вы уже знакомы с технологией. Открытый стандарт WebAssembly (или Wasm) определяет формат двоичных инструкций, который позволяет создавать портируемые исполняемые файлы из исходников, написанных на различных языках программирования. Эти исполняемые файлы могут работать в различных окружениях. Формат поддерживают все основные браузеры. В движки браузеров интегрирована виртуальная машина Wasm (обычно называемая «средой исполнения Wasm»), которая и отвечает за выполнение Wasm-инструкций. Специализированные компиляторы (например, Emscripten) могут компилировать исходный код в Wasm. Технология позволяет переносить legacy-приложения в браузер и обеспечивает их прямое взаимодействие с JS-кодом в веб-приложениях на стороне клиента. Таким образом, традиционные настольные приложения теперь успешно работают в браузере, то есть их можно запускать на любом устройстве, на котором есть браузер с поддержкой Wasm. Среди примечательных примеров — Google Earth и библиотека компьютерного зрения OpenCV. Существуют рантаймы Wasm, которые могут работать вне браузера, в том числе в классических операционных системах, таких как Linux, Windows и macOS. Поскольку в данном случае на движок Javascript рассчитывать не приходится, коммуникация с внешним миром идет через различные интерфейсы — например, WASI (WebAssembly System Interface). Такие рантаймы позволяют Wasm-приложениям взаимодействовать с хост-системой по аналогии с POSIX (хотя есть и отличия). Проекты вроде WASI SDK и wasi-libc помогают компилировать существующие POSIX-совместимые приложения в WebAssembly. При этом достаточно скомпилировать приложение в модуль Wasm только один раз, и его можно будет запускать где угодно. Некоторые из особенностей, благодаря которым Wasm отлично работает в браузерах, делают его привлекательным и для server-side-разработки: 🌐 Открытость. Стандарт широко распространен в отрасли. Браузерные войны остались в прошлом: крупные игроки активно сотрудничают в области стандартизации WASI и WebAssembly-приложений. 🚀 Быстрота. Почти нативная скорость благодаря JIT/AOT-возможностям большинства рантаймов. Никаких холодных стартов — в отличие от виртуальных машин или контейнеров. 🔒 Безопасность. Рантайм Wasm по умолчанию работает в режиме песочницы и следит за безопасным доступом к памяти. Модель, основанная на возможностях (capabilities-based), гарантирует, что Wasm-приложение имеет доступ только к тому, к чему ему явно разрешено. Повышается безопасность цепи поставок за счет виртуализации внешних зависимостей способом, с которым не нужно постоянно обновлять уязвимые компоненты приложения. 💼 Портируемость. Популярные рантаймы поддерживают большинство процессоров (x86, ARM, RISC-V) и большинство ОС, включая Linux, Windows, macOS, Android, ESXi и даже non-Posix-системы. 🔋 Эффективность. Можно сделать так, чтобы Wasm-приложение работало с минимальным объемом памяти и минимальными требованиями к процессору. 🗣 Полиглотизм. В Wasm можно скомпилировать исходники на 40+ языках. Инструменты для этого активно разрабатываются и совершенствуются. Возможно, вы видели этот твит Соломона Хайкса (одного из соучредителей корпорации Docker и автора Docker Open Source Initiative): Если бы WASM+WASI существовали в 2008 году, нам бы не пришлось создавать Docker. Вот насколько они важны. WebAssembly на сервере — будущее компьютерных технологий. В самом деле, WASM+WASI выглядят как следующий шаг в эволюции программной server-side-инфраструктуры. Раньше нам приходилось работать с реальным оборудованием, кропотливо устанавливать ОС и приложения на каждое устройство и по очереди их обслуживать. Все упростилось с внедрением виртуальных машин (здесь первопроходцем стала VMware). Их можно было копировать, клонировать и перемещать между устройствами. Однако все равно приходилось устанавливать ОС и приложения в виртуальные машины. Затем появились контейнеры, ставшие популярными благодаря Docker. С их помощью стало возможно запускать конфигурации приложений в минималистском оберточном контексте, при этом не влияя на другие приложения на ОС хоста. Но контейнеры все равно приходилось дополнять средой исполнения и необходимыми библиотеками, а за границами безопасности следило ядро Linux. И вот, наконец, появилась технология WebAssembly. Ее технические характеристики и портируемость позволяют распространять приложения, не «нагружая» их зависимостями системного уровня. Плюс они работают в строгих границах безопасности. Учитывая все это, WebAssembly обычно рассматривается как «преемник» контейнеров и следующий логический шаг в развертывании инфраструктуры. Но есть и другой взгляд на WebAssembly — как на альтернативный «бэкэнд» для Docker-инструментария. Можно применять те же инструменты командной строки и рабочие процессы, но вместо Linux-контейнеров запускать их WebAssembly-эквиваленты. В остальной части статьи рассматривается именно эта концепция — то, что мы называем «Docker без контейнеров». Docker Desktop теперь работает с WebAssembly. Поддержка реализована с помощью containerd shim'а, способного запускать Wasm-приложения в среде исполнения WasmEdge. То есть вместо обычных Windows- или Linux-контейнеров, которые запускают отдельный процесс из исполняемого файла в образе, под видом контейнера можно запустить Wasm-приложение в рантайме WasmEdge. Такому «контейнеру» не нужны ОС или контекст рантайма — достаточно исполняемого Wasm-файла. Подробнее об этом рассказывается в статье Introducing the Docker+Wasm Technical Preview (примечание редакции: это статья на английском языке). WasmEdge — это высокопроизводительный рантайм для WebAssembly, который: является проектом с открытым исходным кодом и входит в состав CNCF; поддерживает все основные архитектуры процессоров (x86, ARM, RISC-V); поддерживает все основные операционные системы (Linux, Windows, macOS) и некоторые другие, такие как seL4 RTOS, Android; оптимизирован для cloud-native и Edge-приложений; расширяется и поддерживает связанные с ИИ технологии, такие как Tensorflow, OpenVINO, PyTorch; поддерживает асинхронную работу с сетью с помощью Tokio. А также микросервисы, клиенты баз данных, очереди сообщений; позволяет настроить интеграцию с экосистемой контейнеров Docker и Kubernetes (как показано в этой статье). До сих пор мы говорили только о превращении в WebAssembly-программы исходников, написанных на компилируемых языках, таких как C и Rust. В случае интерпретируемых языков, таких как Python, Ruby и PHP, подход иной: их интерпретаторы написаны на C и могут быть скомпилированы в WebAssembly. После этого Wasm-интерпретатор cможет работать с исходниками в соответствующем формате (.py, .rb, .php и т. п.) После компиляции в Wasm любая платформа с Wasm-рантаймом сможет запускать программы, написанные на интерпретируемых языках, даже если интерпретатор изначально не предназначался для нее. Давайте перейдем к практике. В примерах будем использовать интерпретатор PHP, скомпилированный в Wasm. Мы: соберем Wasm-контейнер; сравним Wasm и нативные исполняемые файлы; сравним классические контейнеры и контейнеры Wasm; продемонстрируем портируемость Wasm. Чтобы воспроизвести примеры локально, необходимо подготовить окружение. Для этого понадобятся следующие компоненты: WASI SDK для сборки WebAssembly-приложений из исходников на C; PHP для запуска нативного PHP-файла для сравнения; рантайм WasmEdge для запуска WebAssembly-приложений; Docker Desktop + Wasm (на момент написания этой статьи доступен как стабильная бета в версии 4.15) для запуска Wasm-контейнеров. В репозитории Wasm Language Runtimes содержится все необходимое для сборки интерпретатора PHP в виде WebAssembly-приложения. Начать можно с клонирования демо-ветки: В качестве первого примера рассмотрим, как собрать приложение на языке C — скажем, интерпретатор PHP. Для этого нам понадобится набор инструментов WASI-SDK. Он включает в себя компилятор clang, способный собирать wasm32-wasi-модули, а также библиотеку wasi-libc, отвечающую за реализацию базовых интерфейсов системных вызовов POSIX поверх WASI. WASI SDK позволяет из кодовой базы PHP собрать модуль Wasm, написанный на C. После этого достаточно создать простой Docker-файл для сборки OCI-образа, который будет запускаться с помощью Docker+Wasm. Находясь в директории wlr-demo, которую вы клонировали ранее, выполните следующие действия для сборки исполняемого Wasm-файла: PHP собирается с autoconf и make. Изучив скрипт scripts/wl-build.sh, вы увидите, что были установлены все нужные компилятору WASI_SDK переменные (CC, LD, CXX и т.д.). Углубившись в php/php-7.4.32/wl-build.sh, можно увидеть, что используется обычный процесс сборки с autoconf. Работа над WASI еще не завершена, и многие POSIX-вызовы пока не поддерживаются. Поэтому для сборки PHP пришлось наложить несколько патчей на исходную кодовую базу. Выше мы видели, что готовые исполняемые файлы сохраняются в build-output/php/php-7.4.32. В последующих примерах будем использовать бинарник php-wasmedge, специально собранный для WasmEdge. Его особенность — поддержка сокетов на стороне сервера (она пока не является частью WASI). Wasm — виртуальный набор инструкций, поэтому по умолчанию любой рантайм будет интерпретировать их «на лету». В некоторых случаях это грозит замедлением работы. WasmEdge позволяет создать AOT-оптимизированный (ahead-of-time) исполняемый файл, который нативно работает на текущей машине и может интерпретироваться на других. Для этого нужно выполнить следующую команду: В примерах ниже мы будем использовать именно этот бинарник (build-output/php/php-7.4.32/bin/php-wasmedge-aot). Здесь можно получить дополнительную информацию об исполняемых файлах, оптимизированных для WasmEdge AOT. Теперь исполняемый файл можно обернуть в OCI-образ. Давайте посмотрим на images/php/Dockerfile.cli. Нужно лишь скопировать исполняемый Wasm-файл и установить его в качестве ENTRYPOINT. Также в образ можно положить некоторые вспомогательные данные, которые потребуются Wasm-бинарнику, когда его запустит Docker. Например, в images/php/Dockerfile.server добавляется некоторое содержимое docroot, которое php.wasm будет передавать при запуске контейнера. Используя вышеуказанные файлы, можно легко собирать образы php-wasm локально. Теперь давайте сравним исполняемый PHP-файл с Wasm-файлом как локально, так и в Docker-контейнере. Для этого возьмем один и тот же index.php и изучим результаты его запуска с помощью: php; php-wasmedge-aot; php в классическом контейнере; php-wasmedge-aot в Wasm-контейнере. Во всех примерах ниже используется один и тот же файл images/php/docroot/index.php. Вот его содержимое: Этот скрипт: с помощью phpversion и php_uname выводит версию интерпретатора и платформы, на которой запущен; выводит имена всех переменных окружения, к которым у скрипта есть доступ; выводит приветствие с указанием текущего времени и даты; выводит список содержимого корневой папки /. При запуске нативного PHP-файла видим: платформу под управлением Linux, на которой он выполняется; список из 58 переменных окружения, к которым скрипт при необходимости может получить доступ; список всех файлов и поддиректорий в /, к которым опять же скрипт при необходимости может получить доступ. При запуске с помощью php-aot-wasm и Wasmedge видим: платформу wasi/wasm32, на которой выполняется скрипт; пустой список переменных окружения, поскольку Wasm-приложению не был явно предоставлен доступ ни к одной из них; попытка вывести содержимое / закончилась ошибкой, поскольку приложению Wasm не был предоставлен явный доступ к корневой директории. Чтобы у php-wasmedge-aot был доступ на чтение файла index.php, нужно явно указать WasmEdge, что директория images/php/docroot должна быть открыта для доступа как /docroot в контексте Wasm-приложения. Этот момент отлично демонстрирует одно из главных преимуществ Wasm (помимо портируемости): более высокий уровень безопасности, доступ по умолчанию закрыт, если явно не указано иное. При запуске в классическом контейнере видим: платформу под управлением Linux, на которой выполняется скрипт; список из 14 переменных окружения, к которым у скрипта есть доступ; приветствие с указанием текущего времени и даты; список содержимого корневой папки /. Уже заметна разница в лучшую сторону по сравнению с запуском на хост-машине: переменные окружения и содержимое / являются «виртуальными» и существуют только внутри контейнера. При запуске с помощью php-aot-wasm и WasmEdge видим: платформу wasi/wasm32, на которой выполняется скрипт; всего 2 инфраструктурные переменные окружения, предварительно настроенные с помощью WasmEdge shim, который работает внутри containerd; список всех файлов и директорий в / внутри контейнера, который явно предварительно открыт для доступа приложением Wasm (часть логики в WasmEdge shim). Примечание: если внимательно посмотреть, можно увидеть, что для запуска контейнера из этого образа необходимо: явно указать рантайм с помощью аргумента командной строки --runtime=io.containerd.wasmedge.v1pass, передав его напрямую php.wasm, не включая сам исполняемый файл. Вернитесь выше: можно было бы явно написать полную команду с обычным контейнером PHP и включить в нее исполняемый файл php (но это не является необходимостью). И последнее замечание: даже при запуске в Docker-контейнере Wasm ужесточил границы безопасности, сократив число параметров, к которым у index.php есть доступ: Мы не только собрали/запустили исполняемый Wasm-файл, но и протестировали его работу в виде контейнера. Заметна разница в выводе Wasm и традиционного контейнера, а также расширенный «песочный» режим, который привносит Wasm. Давайте посмотрим, какие еще различия существуют между двумя типами контейнеров. Сначала мы запустим два контейнера-демона и посмотрим, как можно интерпретировать их статистику. Затем мы рассмотрим различия в образах контейнеров. Запустим два контейнера-демона — один из обычного образа php, другой — из php-wasm. docker stats пока выдает статистику только для обычного контейнера. Это может измениться со временем, поскольку Docker+Wasm находятся в стадии beta. Чтобы понять, что происходит, понаблюдаем за контрольными группами. Каждый классический контейнер получает собственную контрольную группу (например, docker/ee44). С другой стороны, Wasm-контейнеры включены как часть контрольной группы podruntime/docker, и можно косвенно наблюдать за потреблением ими ресурсов CPU или памяти. Видно, что образы Wasm-контейнеров намного меньше обычных. Даже alpine-версия контейнера php больше, чем Wasm. Это ожидаемо, поскольку в Wasm достаточно положить исполняемый файл внутрь контейнера. А в обычные контейнеры приходится добавлять некоторые базовые библиотеки и файлы из ОС, под которой этот исполняемый файл будет работать. Такая разница в размерах может благоприятно сказаться на скорости извлечения образа в первый раз, а также на месте, которое занимают образы в локальном хранилище. Портируемость — одно из главных преимуществ Wasm. Благодаря Docker'у, как только речь заходит о портируемости, сразу вспоминаются классические контейнеры. Помимо большого размера образа, такие контейнеры также привязаны к архитектуре платформы, на которой они запускаются. Многие из нас сталкивались с необходимостью собирать версии программного обеспечения под разные архитектуры и упаковывать их в отдельные образы для каждой архитектуры. WebAssembly привносит истинную портируемость. Исполняемый файл достаточно собрать один раз, после чего его можно будет запускать везде. Чтобы наглядно это продемонстрировать, мы подготовили несколько примеров запуска WordPress'а в интерпретаторе PHP, который собрали под WebAssembly. WordPress будем запускать как отдельное Wasm-приложение и как контейнер 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, а затем удалит контейнер. Теперь у нас есть WordPress. Давайте его запустим: Можно перейти на http://localhost:8085 и проверить работу WordPress'а под интерпретатором PHP Wasm. С Docker'ом все намного проще, что ожидаемо: Можно перейти на http://localhost:8086 и убедиться, что WordPress работает под интерпретатором PHP Wasm прямо из Docker-контейнера. Apache HTTPD — один из наиболее широко используемых HTTP-серверов. А теперь с помощью mod_wasm можно запускать и приложения WebAssembly. Чтобы не устанавливать и не настраивать его локально, мы подготовили контейнер, в котором находятся Apache HTTPD, mod_wasm и WordPress. Можно перейти на http://localhost:8087 и посмотреть, как WordPress работает с интерпретатором PHP Wasm и модулем mod_wasm в Apache HTTPD. Для примера зайдите на сайт https://wordpress.wasmlabs.dev. Увидите фрейм, в котором интерпретатор PHP Wasm рендерит WordPress в реальном времени. Спасибо, что дочитали до конца. Информации было много. Надеемся, она помогла оценить возможности WebAssembly и разобраться, как эта технология работает с существующими кодовыми базами и инструментами, в том числе с Docker'ом.  А какой у вас опыт работы с Wasm? Делитесь в комментариях! Читайте также в нашем блоге: «Тренды Kubernetes и контейнеризации в 2023 году» «Сравнение TCP и QUIC» «Миграция приложения из OpenShift в “ванильный” Kubernetes»