Сегодня мы с нашими друзьями из Snaplet открываем исходники postgres-wasm — запускаемый в браузере сервер PostgreSQL с полным набором функционала, включая сохранение состояния в браузере, восстановление из pg_dump и логическую репликацию из удалённой базы данных. Впервые Postgres в браузере запустили в Crunchy Data, их потрясающая версия выложена на HN месяц назад. Вместе со Snaplet мы решили сделать версию с открытым кодом. Посмотрим, как она разрабатывается и какой функционал мы добавили. Подробности — к старту нашего флагманского курса по Data Science. Snaplet — это данные с продакшена и предварительные базы данных, с которыми разработчики могут писать код, сосредоточившись на переносимости. Посмотрите, как с помощью Snaplet клонировать среды Supabase. Прежде чем перейти к техническим деталям, попробуйте сами (внимание: загрузится ~30 Мб).  Запускаем локально: И вводим в браузере localhost:3000. В нашей демоверсии — приятные плюшки! Хороший вопрос. postgres-wasm пока весит около 30 Мб, поэтому для общих вариантов применения запуск Postgres в браузере сейчас не так хорош. Но потенциал у него большой. Вот идеи, над которыми мы будем работать в ближайшие месяцы: Мы разделили репозиторий на три папки: виртуальная машина, веб-приложение и сетевой прокси. Создаём встраиваемую ВМ при помощи Buildroot. Наша ВМ — это урезанная сборка Linux с установленным Postgres. Смотрите исходный код. Дальше запускаем её в браузере с помощью WASM и v86. Демоверсия приложения очень проста — это обычный HTML с простыми стилями. Смотрите исходный код. Запускать Postgres в браузере — это хорошо, а подключаться к Postgres с помощью pgAdmin — ещё лучше. К сожалению, в браузерах блокируется сетевой TCP-доступ к ВМ, поэтому проксируем трафик через веб-сокеты. Запускаем форк Websockproxy для взаимодействия эмулятора с интернетом, то есть преобразования данных, отправляемых через порт веб-сокета, в TCP-пакеты. С этим форком можно пробросить туннель на сервер Postgres. Смотрите исходный код. В ходе разработки postgres-wasm обнаружилось много проблем. Первая проблема: наша реализация — не чистый WASM. Мы пытались скомпилировать Postgres для WASM прямо из исходного кода, но это оказалось сложнее, чем мы ожидали. В статье Crunchy на HN намекали на применяемый в работе подход — виртуализацию машины в браузере. Мы следовали этой стратегии с v86: с его помощью в браузере эмулируются аппаратное обеспечение и x86-совместимый ЦП. PostgreSQL 13.3 с устаревшей версией Buildroot у нас запускалась быстро. А уже с версии PG14 не запускалась, и при инициализации выдавалась segfault. Мы пробовали: В итоге Фабиан, создатель v86, предложил отключить JIT-компиляцию для v86. Так решилась эта проблема. Он свёл её к багу в v86 и добавил устраняющее этот баг обновление. Проблема решилась и для текущего выпуска V86 — переключением управления памятью Postgres с posix на sysv. Запустив PG14 в эмуляторе, мы переключились на производительность. Размер образа эмулятора оказался слишком велик для браузерного инструмента. Как мы ни старались сжать снимок, загружать более 30 Мб, прежде чем увидеть какую-то интерактивность, — многовато. Мы решили эту проблему: запустили минимальный образ Linux, а после инициализации загрузили оставшуюся часть виртуальной машины динамически по HTTPS. Делается это монтированием в ВМ сжатой файловой системы 9P. В 9P есть Python-скрипт. Скрипт берёт папку файловой системы, переименовывает каждый файл 8-символьным именем, и выдаёт файл filesystem.json — структуру с файлами, исходными именами файлов, размерами и т. д. Затем этот сжатый вывод копируется в виртуальную машину. Чтобы загружать прямо из файловой системы 9P, мы модифицировали командную строку ядра и параметры загрузки v86. И даже поместили в 9P файл ядра. Все необязательные файлы при необходимости также загружаются в браузере асинхронно по HTTPS. Исходное состояние уменьшилось, но его размер оставался в пределах 15–20 Мб. Мы обсудили это с Фабианом, и он указал на параметр ядра page_poison=on, с помощью которого перед созданием снимков кеши очищаются, а в освобождённую память Linux вместо случайных байтов записываются произвольные. Поэтому неиспользуемая память сжимается намного эффективнее. Каков итог всех этих изменений? Размер сжатого файла исходного состояния составляет около 12 Мб, в том числе состояние с активной сетью и Postgres 14.4 с загруженным psql. Без запуска Postgres мы получили исходное состояние ещё меньше. Но инициализация Postgres обычно дольше загрузки дополнительных мегабайтов. Мы предпочли меньшее время запуска. Кроме того, без состояния выполнения каждое обновление страницы чревато снижением производительности, ведь Postgres нужно каждый раз инициализироваться. В нашей текущей версии, после того как исходное состояние загружено и кешировано браузером, ВМ обновляется почти мгновенно. Особенно трудно решалась проблема сети. По соображениям безопасности в браузерах поддерживается доступ к ограниченному набору портов. В нашем случае у браузеров порты 80/443, а у Postgres — 5432, а значит, возможности взаимодействовать с внешним миром не было. WASM здесь мало чем полезен. Запущенным в браузере модулем WebAssembly, как правило, никаких действий вне его не производится, кроме вызова предоставляемых из JavaScript функций. Однако в браузерах есть другой вариант подключения — веб-сокеты. Их преимущество заключается в персистентности, поэтому нужно было только понять, как проксировать трафик ВМ через подключение к веб-сокету. В ВМ есть эмулированная сетевая карта ne2k-pci и параметр запуска proxy_url, который указывает на внешний сервер, где проксирование выполняется через веб-сокет. При этом подключения устанавливаются через порт веб-сокета, а затем принимаются необработанные Ethernet-пакеты, которые преобразуются в TCP/IP-пакеты и маршрутизируются между интернетом и ВМ. После создания туннеля можно отправлять сетевой трафик во вне. Попробуем «запустить» сеть в ВМ, «выйти» из psql (cmd+D) и запустим ping 1.1.1.1.  В большинстве ВМ v86 используется открытый прокси-сервер wss://relay.widgetry.org/ на основе websockproxy. Благодаря ему возможны взаимодействие ВМ с внешним миром, отработка всех базовых сценариев, например загрузка данных из других БД PostgreSQL с помощью pg_dump и psql или работа в качестве реплики «только для чтения» другой БД. Но с ним невозможна маршрутизация входящего трафика в ВМ, запускаемую локально на ноутбуке. Например, нельзя подключить локальный PgAdmin4 к экземпляру PostgreSQL в браузере. Поэтому мы дублировали прокси-сервер и в качестве обратного прокси-сервера добавили nginx.  Обратным прокси-сервером прослушивается ряд портов, каждый из которых сопоставляется с ВМ в определённом, нужном нам приватном адресе.  Простое решение — взять последние два сегмента приватного IP-адреса и сопоставить с номером порта. Так, если приватный IP-адрес 10.5.6.123, подключаемся к прокси-серверу в порте 6123 и оттуда направляемся в PostgreSQL, запускаемую на порте 5432 с приватным IP 10.5.6.123. Сокращённые адреса дополняются, поэтому 10.5.6.2 сопоставляется с портом 6002, а 10.5.6.44 — с 6044.  После запуска эмулятор, по словам Фабиана, «выполняется как процесс типа «чёрный ящик», а единственный способ взаимодействия пользовательского интерфейса и запущенной в эмуляторе операционной системы — это serial0_send для отправки нажатий клавиш через эмулируемый последовательный порт. Либо функция create_file, которая загружает файл в запущенный эмулятор. Чтобы после восстановления файла состояния перезапустить сеть, нужно выполнить короткий скрипт для отмены привязки и повторной привязки эмулированной сетевой карты. Так она получает новый mac-адрес и, следовательно, уникальный приватный IP-адрес на прокси-сервере. И затем запустить стандартную команду перезапуска сети. Можно было бы запустить этот скрипт, отправив в эмулятор нажатия клавиш, но это громоздко. Если пользователь в командной строке psql, отправляем \! script_command. А если в командной строке ОС? Мы решили проблему созданием папки \inbox и добавлением в неё слушателя. При поступлении туда файла, который заканчивается на .sh (скрипт оболочки), применяем к файлу chmod +x, выполняем его, а затем удаляем. Так команды запускаются «в фоновом режиме», не затрагивая пользовательский интерфейс. Подключитесь к экземпляру Postgres, запускаемому в браузере другого пользователя: Репликация БД Ещё круче — реплицировать данные из онлайн — БД PostgreSQL (например, проекта Supabase) в PostgreSQL, запускаемую в браузере. И, наоборот, используя логическую репликацию Postgres. То же относится к любой другой БД Postgres — просто используйте в качестве цели URL-адрес прокси-сервера. Пока всё это очень экспериментально, но с большим потенциалом. Чтобы поучаствовать, свяжитесь с нами или командой Snaplet. Они проделывают невероятную работу, и мы получили огромное удовольствие от сотрудничества с ними.