Наверное не секрет, что одна из причин популярности nginx - это развитая экосистема сторонних модулей. Модули позволяют не просто настраивать какие-то детали обработки запроса, но и глубоко изменять поведение сервера.
Помимо модулей, которые решают конкретные задачи, существуют модули, которые добавляют поддержку расширений на различных языках программирования: perl, lua, javascript и других.
Теперь в этот набор добавляется ещё и модуль WASM, который мы разработали для нашего веб-сервера Angie (здесь и далее ссылки на нашу документацию). Зачем понадобился WASM на сервере, чем нас не устраивают существующие методы расширения и что в итоге получилось?
Зачем нам WASM? Все просто — мы хотим, чтобы у разработчков были возможности писать приложения под Angie. В nginx такой возможности толком реализовано не было, а все попытки подружить его с WASM закончились на проекте UNIT. Сейчас он, в свою очередь, свое развитие прекратил.
Ну а данный текст был подготовлен для вас ведущим разработчиком Angie Владимиром Хомутовым. Ранее Владимир уже выступал с докладом про WASM в Angie на конференции Highload++. Владимира «загнать» на Хабр не удалось, поэтому выкладываем текст от гендиректора Angie Software. То есть от моего аккаунта.
Видео снято при помощи компании Evrone Development
Недостатки существующей системы расширений
Как известно, модули для nginx (и, соответственно, Angie) принято писать на C. Это не значит, что вы не сможете написать его на C++ или чём-то ещё, но в любом случае у вас на выходе будет динамическая библиотека, которой придётся взаимодействовать с ядром сервера при помощи существующих C-интерфейсов.
И здесь-то и кроется основная проблема - эти интерфейсы достаточно низкоуровневые, слабо документированы и предоставляют множество способов выстрелить себе в ногу. Для того, чтобы написать нетривиальный модуль нужно хорошо понимать работу ядра сервера, хорошо знать C, и быть готовым следить за всеми изменениями в кодовой базе сервера.
В чём выражаются такие проблемы на практике? Если в проекте используется множество модулей, то начинаются проблемы со стабильностью. Сервер может начать падать, а выяснение того, какой модуль всё испортил - может затянуться. Ведь адресное пространство - общее, и любой из модулей мог испортить память - либо из-за ошибки в самом модуле, либо из-за какой-то несовместимости.

Модули необходимо пересобирать с выходом каждой новой версии, и проверять на работоспособность, а тесты к ним есть не всегда. У администратора вечная головная боль: один из модулей надо обновить, так как там починили критическую ошибку, но только для свежей версии. Значит надо обновлять сервер, но другие используемые модули ещё не обновили, и так по-кругу.
Возможность писать модули на интерпретируемых языках программирования несколько смягчает проблему, но в целом её не решает. Сам интерпретатор по-прежнему крутится в том же адресном пространстве, что и сервер, а достаточно сложный код всё-равно пытается звать C-функции через какой-то аналог FFI, поскольку нуждается в сторонних библиотеках, которые не всегда доступны в выбранном окружении. Да и предоставляемые для интерпретаторов интерфейсы достаточно ограничены и не дают в полной мере реализовать желаемые возможности. Конечно, это компромисс - меньше возможностей, но также и меньше шансов сделать ошибку.
Кроме того, при работе с интерпретируемыми языками обычно возникают вопросы по потреблению ресурсов и скорости выполнения. Конечно, не всегда, но если вспомнить, что вы решили написать модуль для веб-сервера, то это означает, что серьёзные требования к производительности и потреблению ресурсов у вас в какой-то момент появились. В противном случае можно было бы просто использовать отдельный сервис написанный на любой популярной платформе.
Найти хорошую библиотеку - ещё одна проблема при создании модулей для асинхронного веб-сервера. Большинство популярных библиотек для работы с различными форматами и протоколами просто не умеют работать в таком режиме. Зачастую, они хотят сами работать с соединением или вызывают блокирующиеся системные вызовы в процессе работы. Поэтому авторам модулей приходится зачастую самим реализовать необходимый функционал, что ведёт к изобретению велосипедов, увеличению количества кода, а значит и поверхности атаки.
Ну и наконец - вопросы безопасности. Даже при отсутствии плохих намерений ошибка в C модуле - это возможный инцидент безопасности. При достаточно длинной цепочке зависимостей, кто поручится за то, что установленный вами с гитхаба модуль не украдёт секретных ключей или не будет майнить биткойн? Проводите ли вы тщательный аудит всех зависимостей рекурсивно при добавлении в проект и при каждом обновлении? Вопрос риторический.
Отсутствие хорошо определённого стабильного интерфейса для модулей ограничивает не только пользователей, но и в равной мере разработчиков. Невозможно поменять какую-то внутреннюю реализацию только потому, что её частью могут пользоваться неизвестное количество сторонних модулей, которые уже могут даже не поддерживаться. Пользователи не очень любят, когда какой-то работавший годами модуль вдруг ломается из-за того, что в ядре сервера произошли какие-то несущественные с их точки зрения перестановки, а поправить - некому.
Итак, основная проблема с модулями - это отсутствие изоляции выполняемого стороннего кода и отсутствие стабильных интерфейсов для взаимодействия с ним.
А если разговор идёт про изоляцию и запуск стороннего кода, то WASM выглядит здесь уместным решением.
Достоинства WASM как платформы для создания расширений
Что такое WASM с точки зрения разработчика? Это виртуальная машина, которая может исполнять загруженный в неё код. Причём эта виртуальная машина обладает множеством подходящих свойств:
Предоставляет изоляцию исполняемого кода
Код на этапе компиляции собирается под архитектуру WASM-машины и выполняется в своём адресном пространстве. Код общается с внешним миром путём вызова набора функций, ограниченных запускающим хостом, и никак иначе.
Соответственно, модуль может обрабатывать только те данные, которые были ему переданы разработчиком явным образом.
Нет привязки к конкретному языку
Каждая команда разработки может использовать знакомые инструменты с понятной инфраструктурой и жизненным циклом.
Для разработчиков расширяемого продукта это резко упрощает жизнь: теперь вместо поддержки N языков можно сосредоточиться на создании единого стабильного интерфейса для WASM.
Платформонезависимый
WASM-код избавляет от зависимости от конкретной версии сервера, архитектуры операционной системы. Для сборки не нужны исходники сервера, достаточно спецификации интерфейсов.
Стандартизован: множество реализаций
Для WASM существует стандарт. Бинарный формат портабелен, существует множество реализаций, можно выбрать любую подходящую под наши критерии — маленькую, быструю, функциональную, безопасную и т.д.
Быстрый: компактный и оптимизированный
Бинарное представление WASM достаточно эффективно и поддаётся оптимизации. При запуске кода не нужно парсить мегабайты условного javascript, потому что всё это делается на этапе компиляции — с использованием современного оптимизирующего компилятора.
WASM-машина — это достаточно продвинутая среда и с производительностью у неё всё достаточно хорошо. Как минимум, производительность не хуже интерпретаторов, а как максимум — сравнима с машинным кодом.
Обратная сторона изоляции: расходы на коммуникацию
Естественно, ничто не даётся даром: за изоляцию придётся платить производительностью. Необходимо копировать данные, передавая их в виртуальную машину, и забирая их обратно. Существуют накладные расходы на выполнение WASM-кода, на запуск экземпляров виртуальной машины и затраты памяти на сопутствующие нужды.
Системное окружение ещё не финализировано
Поддержка среди языков разнится. Поддержка в LLVM и наличие clang позволяет легко собирать C и C++, всё хорошо у Rust, а вот с интерпретаторами всё несколько сложнее.
Поскольку окружение WASM-программы — это нечто отличное от того, к чему привыкли обычные разработчики, то придётся как-то адаптировать старые программы для него или разбираться, как писать новые. И здесь есть определённые проблемы, о которых чуть позже.
По большому счёту, всё это очень напоминает JVM, но отвязанную от Java и Oracle и спроектированную с другими акцентами и с учётом опыта последних десятилетий. Сейчас это вполне самостоятельная платформа для разработки приложений с фокусом на интеграцию в существующие системы, их расширение и организацию взаимодействия между ними.
Системное окружение для WASM-программ
Итак, код модуля переезжает в виртуальную машину. И сразу возникает вопрос: а как это вообще работает?
Ведь обычный модуль запускается в контексте процесса сервера, ему доступны как внутренние API, так и внешние библиотеки, он может обратиться к операционной системе (в которой запущен сервер) или даже выполнить ассемблерный код.
При этом операционная система и библиотеки предоставляют нам доступ к обширнейшему функционалу — хранение данных, сеть, процессы и т.д. и т.п.
Поскольку WASM-код будет запускаться в виртуальной машине, то подразумевается ли, что там доступна операционная система, библиотеки, «железо» наконец?

И здесь мы подходим к важной особенности WASM-машины: она минималистична и не занимается эмуляцией железа (дисков, сетевых карт, таймеров и т.п.). Всё, что там есть, — это процессор и память. То есть это такой чёрный ящик. Но поскольку вещь в себе по Канту была бы несколько непрактичной, то общение с внешним миром всё-таки возможно. Единственный предусмотренный для этого механизм — вызов функций. Причём эти функции принимают только численные (i32, i64, f32, f64) аргументы.
Функции эти либо экспортируются (тогда их можно позвать снаружи), либо импортируются (тогда они должны быть предоставлены хост-окружением). В последнем случае хост-функции служат аналогом системных вызовов операционной системы. Если WASM-программа использует какую-то функцию, то она должна быть определена либо в ней самой, либо в библиотеке, либо быть предоставленной хост-окружением. Получается, что наш код запускается в так называемом free-standing окружении без операционной системы, что накладывает на программу известные ограничения.
Значит ли это, что существующие программы запустить не получится? Очевидно, что любую произвольную программу — нет, но если хост-окружение предоставит достаточное количество системообразующих функций, то мы сможем запускать имеющийся софт. Остаётся только понять, какие функции нам нужны от системы. Хорошо бы иметь какой-то стандарт на системное окружение! В мире операционных систем такой стандарт давно придуман и называется POSIX. Авторы WASM решили особо не мудрствовать, взяли его подмножество и назвали его WASI.
WASI = WASM System Interface (preview 1, 2019)
Что же попало в это подмножество и какого рода программы возможно запускать без модификаций?
Есть стандартная библиотека С, файлы и базовое окружение с stdin/stdout/stderr
Наличие стандартной C-библиотеки и POSIX-функций, гарантирующих её работу и немного вокруг, даёт возможность смело собирать простые POSIX-программы под WASM. Классический "hello, world" будет работать. Можно писать фильтры, читающие со stdin и пишущие на stdout.
Отсутствует поддержка нелокальных переходов
Так как в WASM-машине нет инструкции JMP (намеренно, в целях упрощения как самой машины, так и генерируемого кода и его анализа), то это приводит к тому, что программы, нуждающиеся в таком функционале, имеют проблемы, либо не могут быть собраны. Например, вместо исключения в C++ вы получаете просто остановку программы.
нет setjmp()/longjmp()
нет поддержки исключений и раскрутки стека из C++
нет тредов
Комитет работает над тем, как добавить многопоточность, но на сегодняшний день это экспериментальные возможности конкретных реализаций WASM-машин.
Нет сокетов
Нет процессов, fork/exec и связанных вещей: сигналов и т.п.
Традиционные POSIX-функции для работы с процессами отсутствуют, поскольку их добавление потребовало бы собственно операционной системы: ведь процесс — это уже мощная абстракция со своей непреложной семантикой, которую нельзя было бы игнорировать.
По тем же причинам не были добавлены и сокеты: сетевой стек — это уже довольно серьёзная привязка к операционной системе (или специфичному сетевому стеку), которая может сильно усложнить реализацию WASM-машины.
После относительно быстрого создания WASIp1 перед авторами спецификации сразу же возник тупик: движение по пути воссоздания POSIX приведёт к повторению всех исторических ошибок и просто ещё одной виртуальной машине, в которой можно запускать привычную операционную систему. А это совсем не та цель, к которой стремились люди, стоящие за развитием WASM.
Фундаментально проблема заключается в модели безопасности: традиционный POSIX предполагает изоляцию на уровне процесса и различение между ядром и пользовательскими программами. Такое различение слишком широкое и не отвечает требованиям реалий современности — когда приложение состоит из десятков модулей от независимых разработчиков, причём код не является доверенным. Целью на уровне дизайна платформы является возможность ограничить доступ на уровне компонента и дать гарантии, что компонент может обрабатывать только переданные ему данные.
Именно поэтому со стороны казалось, что развитие WASM остановилось – новых функций не появлялось, и авторам программ приходилось самим решать проблемы, связанные с отсутствием необходимого функционала.
Наконец, в 2024 была представлена следующая версия WASI.
Компонентная модель: WASI preview 2 (2024)
Модель окружения WASM-программ была кардинально пересмотрена, и на свет явился WASI preview2 aka Wasm Component Model. Каковы её ключевые особенности?
Interface types — язык описания интерфейсов
Во-первых, поскольку компоненты пишутся на разных языках, необходим ещё один язык, который будет описывать эти интерфейсы, чтобы никому не было обидно. Ниже приведён пример такого описания.
package "docs:calculator@0.1.0"; interface calculate { enum op { add, } eval-expression: func(op: op, x: u32, y: u32) -> u32; } world calculator { export calculate; import docs:adder/add@0.1.0; } world app { import calculate; }
Использовать в целевом языке это предполагается через кодогенераторы.
Canonical ABI – спецификация представления этих интерфейсов
Во-вторых, для API естественно требуется ABI, т.к. необходимо компоновать программы, написанные на разных языках. Соответственно, был создан подробный документ, описывающий, как все эти типы представляются в памяти, как передаются аргументы и подобные вопросы.
Composition — механизм динамического связывания модулей в компоненты
Ну и наконец, был представлен механизм связывания модулей в компоненты. Он определяет, как разрешаются зависимости, добавляет метаданных про импортируемые и экспортируемые функции и гарантирует, что модуль имеет доступ только к тому функционалу, что прописан в его интерфейсе.
Каковы же были практические последствия для разработчиков, которые за эти годы научились как-то жить в парадигме WASIp1 и использовать WASM-модули?
Бинарный формат, отличный от простых WASM-модулей
Компонент — это другой тип файла, отличный от модуля. По факту он включает, собственно, обычный модуль плюс набор метаданных. Нельзя просто так взять и заменить существующий модуль на компонент, даже если там всё те же функции.
POSIX API объявлены устаревшими
Они больше не в моде. Новые правильные API для всего-всего будут разработаны с нуля: от командной строки до HTTP и тредов, от XML до GRPC.
К счастью, существует набор так называемых "адаптеров", которые предоставляют доступ к интерфейсам wasip1 в рамках wasip2, так что существующие программы продолжат работать (по крайней мере, какое-то время).
Долгострой: большинство анонсированных API на нынешний день находятся на стадии разработки. Вот список того, что доехало до стадии реализации.
Очевидно, столь радикальные изменения требуют ресурсов на их воплощение, поэтому нынешний WASM являет собой некоторое промежуточное состояние: множество API находятся в состоянии разработки или долгого рассмотрения комитетом и внесения правок. Реально платформа поддерживается только в референсном wasmtime движке.
Таким образом, если вы пишете сегодня программу под WASM, вы можете рассчитывать на простое подмножество POSIX (доступное в WASIp2 через адаптеры), доступные WASIp2 API (долгострой), и тот API, который предоставляет ваш хост. Насколько хороши эти интерфейсы — время покажет, но в любом случае это нечто новое, что не позволит использовать их со старыми программами.
В целом, API, предоставляемое хостом, и является наиболее интересным для того, кто пишет расширение на WASM. Рассмотрим, как wasm-машина коммуницирует с хостом.
Встраивание WASM-машины в веб-сервер на примере Angie
Не будем пытаться написать свой уникальный WASM-движок, а воспользуемся существующими, которые имеют SDK для встраивания и предоставляют C-интерфейс. Сейчас мы поддерживаем референсный wasmtime и micro-wasm-runtime.
Высокоуровнево, встраивание WASM-машины в веб-сервер выглядит так:

Библиотека WASM рантайма загружается в рабочий процесс и инициализируется. Это происходит на старте. Теперь сервер может загружать WASM-код и порождать виртуальные машины используя интерфейс, который абстрагирует конкретные реализации WASM-машин.
Указанные пользователем в конфигурации модули загружаются. При старте они валидируются, а это значит, что они вызывают только доступные (в нашем хосте) функции. Если модуль вызывает какие-то сторонние функции, то такой модуль не будет загружен.
Загруженный модуль используется для обработки каких-то данных, например, HTTP-запросов. Для этого нужно породить экземпляр виртуальной машины. Это можно сделать либо на старте, либо по требованию. Тут всё зависит от намерений разработчика и сущности выполняемого кода.
По указанному в конфиге имени необходимо найти функцию в загруженном модуле и передать ей управление. При этом надо предоставить ей каким-то образом доступ к данным, которые она должна обработать, и получить их обратно. После чего можно уничтожить виртуальную машину либо оставить её для последующего использования. В последнем случае надо быть аккуратным с использованием памяти внутри машины и следить за ресурсами на хосте.
Отдельно отметим, что отсутствие поддержки тредов в WASM-машинах не является для нас проблемой. Дело в том, что парадигма обработки запросов в веб-сервере — это создание неблокирующихся однопоточных обработчиков. Все расширения работают в рабочем процессе, а эффективное использование нескольких CPU обеспечивается тем, что самих рабочих процессов запущено несколько. WASM-обработчик всегда обрабатывает только один запрос и не запускает никаких тредов. Он может породить асинхронную операцию, и в этом случае он может передать управление обратно хосту. После наступления нужного события хост вновь позовёт эту виртуальную машину и код продолжит своё выполнение. Попытки увеличить параллельность внутри WASM-машины скорее навредят производительности. В будущем мы рассмотрим возможность интеграции WASM и существующих thread pools.
Теперь рассмотрим более подробно, как код из виртуальной машины будет взаимодействовать с хостом:

Хост представляет собой много сложных и ссылающихся друг на друга объектов в памяти — открытые файлы, запросы, соединения...
В загруженном WASM-модуле существует функция, которая должна как-то манипулировать этими объектами. Однако сделать это сложно: нельзя передать объект по-указателю: указатели с хоста не имеют смысла для гостя, это разные адресные пространства; нельзя передать сложный объект по-значению: функции принимают только численные аргументы. Получается, 2 плюс 2 сложить можно, а что делать с каким-нибудь списком строк?
WASM-машина: отдельная память с неизвестным содержимым
Хост имеет доступ к памяти WASM-машины (поскольку это просто выделенная под её цели область памяти), но содержимое её ему непонятно: внутри виртуальной машины идёт своя жизнь, и мы не можем просто взять и написать по какому-то адресу, не учитывая внутреннего состояния рантайма машины (про который мы ничего не знаем, ведь модуль мог быть написан на любом языке).
В целом, вывод отсюда простой — содержимым памяти машины управляет гость. Поэтому и выделять её тоже должен он. В принципе, возможно использовать определённые возможности WASM-движка и выделять память на хосте, но это делает наш код более зависимым от реализации и делает управление памятью менее явным. Также надо учитывать, что разные языки относятся к выделению памяти совершенно по-разному.
Данные необходимо копировать
Отдельно заметим, что никак не выйдет избавиться от копирования данных в память машины и обратно.
WASM-машина не имеет доступа к памяти хоста
Хост не знает о структуре памяти WASM-машины
В целом, ситуация с асимметричными правами доступа к памяти слишком напоминает отношения ядра и пользовательской программы, чтобы её игнорировать. Для того, чтобы организовать взаимодействие двух компонентов, достаточно предоставить давно знакомый интерфейс с дескрипторами объектов и функциями для чтения/записи, то есть UNIX-подобный API.
Архитектура WASM-расширений
Как же будет выглядеть такой интерфейс для пользователя?
В целом, мы хотим, чтобы интерфейс между сервером и WASM-гостем был максимально простым и понятным. Это позволит достичь нашей цели — сделать стабильные интерфейсы для модулей расширения.
Мы хотели бы по минимуму зависеть от возможностей WASM-движка и не изобретать нового там, где это возможно. Поэтому мы оставляем всё управление памятью гостю, и ограничиваемся лишь вызовами функций, чтобы передать гостю его контекст.
Таким образом, у нас появляется достаточно низкоуровневый системный интерфейс — UNIX-подобный интерфейс, который позволяет передавать данные между объектами в ядре сервера и пользовательскими программами. Этот интерфейс реализуется хост-модулем непосредственно в ядре веб-сервера.
Все интересующие нас объекты внутри ядра веб-сервера для пользователя идентифицируются при помощи дескрипторов. Набор дескрипторов управляется хост-модулем, который предоставляет функции open()
и close()
, и позволяет идентифицировать объекты по имени.
Для передачи данных между ядром и программой используем аналоги read()
, write()
и ioctl()
.
/* обращаемся к объекту по имени и получаем дескриптор */
int fd = open("http.request");
/* получаем данные от объекта */
read(fd, buf, size);
/* пересылаем данные в объект */
write(fd, buf, size);
/* завершаем работу */
close(fd);
Семантика и способы работы c таким интерфейсом широко известны и не вызывают проблем у разработчиков. Интерфейс хорошо ложится на ограничения WASM — нам не нужны сложные типы. Состояние программы определяется набором открытых дескрипторов и их свойствами. Однако для создания непосредственно пользовательских расширений такой интерфейс не подходит:
низкоуровневый: примитивные типы данных, много лишних деталей
зависит от формата сериализации и набора внутренних объектов ядра
Высокоуровневый пользовательский интерфейс реализован на стороне гостя и представляет собой библиотеку функций предметной области. Это даёт следующие преимущества:
развязывает внутреннее представление вещей и внешние интерфейсы
позволяет не усложнять системный интерфейс деталями
возможность использовать язык высокого уровня
возможность обновляться независимо от сервера
Таким образом, в дополнение к системному интерфейсу появляется стандартная библиотека, скрывающая низкоуровневые подробности реализации. Пользовательские модули могут обращаться как к библиотеке, так и к системному интерфейсу по необходимости.

Немаловажно отметить, что все интерфейсы описаны при помощи WIT и используют Canonical ABI. Это позволяет иметь их документированное описание в виде интерфейсных файлов и позволяет не зависеть от языка и метода их реализации.
Рассмотрим на примере, как выглядит использование API для работы с объектом на хосте и при помощи каких модулей оно реализуется. Допустим, модуль обращается к стандартной библиотеке с целью как-то взаимодействовать с сервером. Это обычный вызов функции в любом языке программирования. Внутри библиотеки происходит обращение к системному интерфейсу — вызов функции, определённой на хосте. Что же происходит дальше?
Первичная обработка на хосте
В момент вызова open()/read()/write()/close() пересекается граница между WASM-гостем и хостом. WASM-адреса необходимо транслировать в адреса на хосте и проверить, что предоставленные буферы не выходят за границы памяти, выделенной WASM-машине.
Обработка этих вызовов происходит в модуле, который реализует поддержку конкретного WASM-движка. Этот модуль непосредственно реализует набор функций, составляющих системный интерфейс.
Системный интерфейс является минималистичным и лишь предоставляет абстракцию для обмена данными между ядром сервера и пользовательскими программами. Поэтому после первичной обработки данные передаются в хост-модуль, который отвечает за работу с объектами.
Выбор API в хост-модуле
Каждый запуск виртуальной машины происходит в каком-то конкретном окружении, например блок HTTP или Stream. Для каждого такого окружения существует отдельный модуль, который предоставляет набор доступных API.
Например, HTTP-модуль будет предоставлять работу с запросами.
При обращении к какому-либо API будет проверено его наличие и права доступа модуля: разрешил ли пользователь в конфигурации данному модулю доступ к данному API.
Если всё хорошо, то для объекта создаётся дескриптор и вызывается модуль, реализующий непосредственно работу с интерфейсом.
Реализация функционала в API-модуле — определяет семантику обмена данными
Модуль, который реализует API, проводит необходимые действия с объектом; например, откроет файл или соединение с удалённым хостом, или просто инициализирует какой-то счётчик.
Таким образом, у нас есть неограниченное количество интерфейсов, которые могут быть реализованы разными модулями и которые могут быть скомбинированы при создании хост-окружения для конкретного экземпляра WASM-машины.

Практический пример: вычисляем переменные
Рассмотрим модуль, который позволяет создавать переменные запроса при помощи WASM-обработчика. Такой обработчик получает на вход строки (которые могут быть вычислены из других переменных), и на выходе у него опять же строка. Это не самый сложный модуль, но даже в таком простом случае использование WASM может быть полезно и существенно упростить жизнь сопровождающим такого модуля.
В данном примере обработчик переменной будет трактовать свои аргументы как SQL-запрос с параметрами. Итак, вот наш конфиг:
load_module "modules/ngx_wasmtime_module.so";
wasm_modules {
load "ngx_http_vars.wasm"
id=http_vars
fs=/home/demo/tmp:/data;
}
http {
wasm_var http_vars # module ID
"ngx:wasi/var-utils#sqlite" # function to call
$db_res # new variable name
# function args:
"/data/demo.db" # 0: database
"SELECT name FROM people
WHERE id>?1 AND id"
$arg_id $arg_id2; # 2+: query arguments
}
http {
server {
listen 127.0.0.1:8080;
location / {
return 200 "sql( $arg_id .. $arg_id2 )= $db_res \n";
}
}
}
Прокомментируем конфигурацию:
1 — загружаем обычный модуль — поддержку среды исполнения wasmtime
5 — загружаем скомпилированный WASM-модуль с диска
6 — даём ему id, чтобы обращаться к нему из других мест конфига
7 — разрешаем ему доступ к одной директории на файловой системе под именем /data
11 — в директиве wasm_var мы указываем ID загруженного модуля
12 — вызываемая WASM-функция в указанном модуле
13 — имя результирующей переменной
16 — имя файла базы данных на диске (доступ к которой мы явно разрешили).
18-19 — SQL-запрос
21 — аргументы запроса к базе берутся из аргументов HTTP-запроса
28 — возвращаем клиенту ответ с использованием результата вычислений
Теперь запустим сервер и попробуем к нему обратиться:
$ curl "http://127.0.0.1:8080?id=1&id2=5"
sql(1..5)=foo2,foo3,foo4
$ ls -lh "conf/ngx_http_vars.wasm"
-rwxr-xr-x 1 demo demo 1.3M Nov 2 11:34 conf/ngx_http_vars.wasm
Аргументы HTTP-запроса были подставлены в SQL-запрос, и наш ответ содержит релевантную выборку из базы данных.
Остаётся прояснить вопрос — откуда в модуле взялся SQLite и что происходит внутри WASM-модуля. А внутри WASM-модуля находится обычная POSIX-программа на языке C, слинкованная статически с SQLite. Весь SQLite целиком был собран под WASM и добавлен в модуль.
Отметим несколько особенностей этого демо:
вот таким нехитрым образом мы фактически получили HTTP-интерфейс к SQLite базе
ни строчки кода не запатчено, ни в коде веб-сервера, ни в SQLite
мы явно разрешили доступ только к одной директории на файловой системе
мы не добавили никаких внешних зависимостей, только самодостаточный файл модуля; его можно без изменений использовать как на ARM, так и на x86
Чем ещё может быть полезен WASM в веб-сервере?
Помимо написания расширений с использованием предоставленных интерфейсов, наличие WASM-машины предоставляет возможность изолировать части существующих модулей, которые обрабатывают потенциально опасные данные. Прелесть такого подхода в том, что не придётся существенно переписывать код: достаточно организовать ввод-вывод данных и пересобрать существующий код под WASM. При этом вы получите гарантии, что в этом коде не вызывается ничего лишнего — иначе такой код просто не соберётся компилятором.
Например, в качестве эксперимента мы взяли MP4-модуль, который подарил нам несколько CVE, и вынесли парсинг MP4 в WASM. Оригинальный код парсинга на C был практически без изменений собран под WASM и интегрирован обратно в модуль. Ну и конечно, можно создавать свои API, доступные для ваших WASM-модулей.
Обзор реализованного в экосистеме Angie функционала
Базовые модули для работы с WASM:
Создание и работа с WASM-машиной и загрузка модулей
Создание HTTP-расширений:
Обработчики переменных на WASM
Обработчики фаз обработки запроса
Фильтры тела запроса и ответа
SDK:
Все доступные интерфейсы, предоставляемые в SDK, описаны в WIT-файлах.
Например, работа с HTTP запросом, логгирование, доступ к параметрам конфигурации, сетевым соединениям и другие.
Примеры доступны в отдельном репозитории.
Планы развития WASM в Angie
Интегрировать поддержку WASM в базу веб-сервера по умолчанию
Расширение количества доступных интерфейсов и стандартной библиотеки, а также улучшение и отладка существующих модулей
Переход на использование компонентной модели при появлении её поддержки в C-интерфейсах WASM-движков. Это сразу упростит процесс линковки при использовании отличных от C языков программирования.
На сегодня это главный блокирующий фактор, не позволяющий с лёгкостью интегрировать библиотеки с пользовательским кодом.
Увеличение числа мест, где возможно добавить пользовательскую логику, в том числе на WASM.
Исторически в nginx и Angie механизм обеспечения динамики в конфигурации – это переменные. Однако обработчики переменных — синхронные, и переменную нельзя вычислить поэтапно, перемежая это с обработкой других данных.
Если сделать возможной асинхронную обработку переменных, то это позволит кардинально расширить возможности конфигурации: например, обработчик переменной сможет сходить в сеть или провернуть ещё какие-то сложные вещи без изменения кода, который эту переменную использует.
Обратная связь
Мы очень заинтересованы в обратной связи, поэтому если вы — автор расширения, или активный пользователь каких-то сторонних модулей и у вас есть, что сказать по теме — пишите нам. Нам было бы интересно знать:
Какие сторонние модули вы используете и для чего?
Какие проблемы с модулями у вас существуют?
Какие языки вам интересны для написания модулей?
Какие нужны API в первую очередь?
Какие библиотеки необходимы?