Я всех категорически приветствую. Долгое время я с нетерпением ждал появления на хабре статей об одном замечательном проекте - Database Lab Engine. Но время шло, статей всё не было, а терпение заканчивалось. Трудно держать в себе знание, которое может быть полезно многим. И моё терпение подошло к концу. Так что схватитесь покрепче за ручки кресел, чтобы вас не унесло потоком сакрального знания в метавселенную, и приступим. Database Lab Engine (далее DLE) - проект, призванный облегчить процесс тестирования изменений, связанных с БД (миграции, тесты, изменяющие данные и т.д.). Очень непросто провести полноценное тестирование без изменяющих действий. Кто-то обходится схемой БД с каким-то минимальным набором тестовых данных. Но таким образом не всегда полноценно можно проверить те или иные аспекты функционирования ПО. Какие-то ошибки можно выловить только на продуктивном наборе данных или максимально приближенном к нему. И в этом случае DLE может сильно облегчить процесс подготовки БД для предпродуктивного тестирования. Работает это следующим образом: Примечание: Далее речь будет идти о "физическом" режиме работы DLE + резервная копия с помощью инструмента WAL-G. DLE также позволяет работать с логическими резервными копиями, используя несколько пулов ZFS для ротации снимков. Подробнее можно почитать в глубинах документации. Подготавливается резервная копия БД с помощью WAL-G или PGBACKREST  резервная копия размещается на S3-совместимом хранилище Подготавливается сервер, на котором можно запускать docker-контейнеры. На сервере должна быть установлена ZFS и создан хотя бы один пул ZFS. В конфигурационном файле DLE прописываются параметры доступа к S3, где находится резервная копия и точка монтирования ZFS пула.  Желательно обращаться в S3 под разными учётными записями. Размещать резервную копию под учётной записью имеющей доступ на запись, а читать резервную копию с другой учётной записью, имеющей доступ только на чтение Запускается контейнер DLE, который разворачивает на ZFS пуле резервную копию и начинает поддерживать её в актуальном состоянии, накатывая WAL-журналы из S3. Таким образом фактически получается hot-standby сервер PostgreSQL. Назовём этот "набор данных" (а фактически это инстанс PostgreSQL) "эталонным" или просто "эталон". Раз в сутки (расписание настраивается в файле конфигурации DLE) с эталона делается снимок (snaphot в терминах ZFS). После создания снимка выполняется его подготовка к последующему использованию. В этот момент можно применить к данным в снимке свои изменения. В определённом каталоге размещаются .sql файлы, которые применяются к снимку. Порядок применения регулируется именем файла - как получили список файлов в каталоге, так и применили. Далее на основе созданных снимков становится возможным создавать "клоны" - изолированные инстансы PostgreSQL. Вся прелесть в том, что "долгая" работа выполняется при создании снимка, а клон (тоже snapshot ZFS) уже создаётся за секунды. Клон использует блоки данных снимка и сразу после создания практически не занимает места на диске. Но любые изменения в клоне уже создают новые блоки данных и занимают место. Совокупность того, что ZFS использует механизм CoW (Copy-On-Write), а также хорошо ужимает данные и позволяет называть клоны "тонкими" Закрепим терминологию: Снимок - предварительно подготовленная копия производственного инстанса PostgreSQL, к которой были применены, например, маскирование чувствительных данных или переименование/изменение/удаление какой-либо сущности и т.п.. Поэтому в клонах будут присутствовать все БД инстанса PostgreSQL, если, конечно, они не были удалены на этапе подготовки снимка Клон - новый инстанс PostgreSQL, который создан на основе какого-либо снимка. Клон использует блоки данных снимка, поэтому при создании не занимает места. Но любые изменения в клоне, приводящие к изменению данных, увеличивают "размер" клона. Каждый клон имеет свой собственный уникальный порт. Допускается наличие множества снимков, фактически являющихся срезами данных за разные периоды времени и множества клонов (читай: запущенных инстансов PostgreSQL), созданных на основе любых наличных снимков. Это позволяет, например, делать сравнение данных за разные периоды. Любая модификация данных в одном из клонов не затрагивает другие клоны. Расплатой за все эти приятности является довольно существенные ресурсы, которые нужно выделять на сервер DLE. Также стоит отметить, что ZFS довольно сильно ужимает данные (в 3-5 раз), что позволяет сильно экономить на дисках. Но диски лучше использовать SSD, т.к. несколько одновременно запущенных клонов, в которых есть существенная активность, быстро отправят обычный HDD в кому. Для демонстрации одной из возможных схем работы мы создадим стенд, состоящий из следующих компонентов: MinIO - S3-совместимое хранилище для хранения резервных копий и журналов WAL PostgreSQL - СУБД Keycloak - Identity Provider (IdP). Аутентификация и авторизация пользователей. DLE - движок для создания тонких клонов DLE GUI - графическая оболочка с аутентификацией и авторизацией через OIDC и возможностью разделения прав для разных пользователей DLE предоставляет CLI и API, позволяющее им управлять. С версии 2.5 также поставляется GUI в виде отдельного контейнера. GUI является кусочком SaaS и предоставляет минимальные функции по управлению клонами. Но всё это заточено либо на автоматизированное управление через системы CI, либо на одиночного пользователя, т.к. доступ осуществляется по единому токену, имеющему полный доступ. Никакого разделения прав нет. В компаниях, где пользователей много и они ведутся централизованно (например, AD, LDAP и т.п.) будет намного удобнее использовать существующие учётные записи для доступа к DLE. Поэтому в стенде мы будем использовать IdP Keycloak, позволяющий интегрироваться с различными учётными системами и проводить аутентификацию пользователя по OIDC, а также стороннюю графическую оболочку DLE GUI, позволяющую производить аутентификацию и авторизацию по OIDC, а также имеющую встроенные возможности для разграничения доступа к создаваемым клонам. Подробнее про DLE GUI можно почитать тут. Для хранения резервных копий БД и WAL мы будем использовать MinIO. В MinIO будет настроена корзина (bucket) для хранения, а также 2 учётных записи (УЗ): postgres-backup-rw - УЗ с правами "чтение и запись", для размещения резервных копий и журналов postgres-backup-ro - УЗ с правами "только чтение", для DLE Для стенда используется заранее подготовленная конфигурация MinIO, в которой содержатся необходимые пользователи и роли. В Keycloak будет 5 учётных записей, представляющих типичные роли для работы в DLE: dg-admin - роль "Администратор" dg-operator - роль "Оператор" dg-user1 - роль "Пользователь" dg-user2 - роль "Пользователь" dg-viewer - роль "Наблюдатель" Пароль УЗ в Keycloak такой же, как и имя пользователя. Описания ролей: Администратор (admin) Просмотр списка всех клонов Создание нового клона Сброс любого клона Удаление любого клона Установка/снятие защиты для любого клона Оператор (operator) Просмотр списка всех клонов Создание нового клона Сброс любого клона Удаление клона, созданного этим пользователем Установка/снятие защиты для клонов, созданных этим пользователем Пользователь (user) Просмотр списка клонов, созданных этим пользователем Создание нового клона Сброс клона, созданного этим пользователем Удаление клона, созданного этим пользователем Установка/снятие защиты для клонов, созданных этим пользователем Наблюдатель (viewer) Просмотр списка всех клонов На практике пользователям можно выдавать помимо роли "Пользователь", роль "Наблюдатель", чтобы был виден список клонов, созданных другими пользователями и можно было подобрать уникальное имя для нового клона (при создании клона с уже существующем именем/идентификатором произойдёт ошибка и клон не будет создан). Для стенда используется заранее подготовленная конфигурация Keycloak, в которой содержатся все необходимые пользователи и группы. Для стенда используется демонстрационная БД от PostgresPRO - https://postgrespro.ru/education/demodb Я буду запускать стенд на Macbook Pro M1, имеющем архитектуру arm64, в связи с чем будут определённые особенности. В частности, из-за невозможности в Docker Desktop подключать каталоги в режиме rshared, я буду использовать виртуальную машину, а также буду пересобирать некоторые контейнеры, т.к. они не имеют сборок для архитектур, отличных от amd64. Если вы будете запускать стенд на сервере с архитектурой amd64, то некоторые из действий можно будет пропустить или изменить, о чём будет отдельная ремарка. Для начала установим ZFS 2.x, чтобы в дальнейшем не переделывать. На Ubuntu 20.04 в составе дистрибутива устаревшая версия 0.8.4 и будут проблемы. Документация по установке OpenZFS находится тут. У меня установлено ядро 5.8.0-63 с заголовками. Для своих систем внесите соответствующие изменения. Также на момент выполнения кода из статье версия OpenZFS в репозитории может отличаться Далее скачаем все необходимые конфигурационные файлы для стенда Далее нужно скопировать шаблон с настройками и внести изменения, в соответствии с вашим окружением Содержимое файла В моём случае DLE будет работать в виртуальной машине (ВМ) с Debian Linux, имеющей адрес 192.168.144.138, поэтому я указываю его в DBLAB_URL. Локальный адрес хоста - 192.168.144.114. На этом адресе будут на разных портах доступны сервисы MinIO, Keycloak, DLE GUI. Этот адрес доступен также и с ВМ. Локальный адрес нужно прописать в переменной LOCAL_IP. Если вы разворачиваете стенд на одном или нескольких серверах, доступных по DNS-именам, то можно указать их. Импортируем настройки Скачаем и распакуем демонстрационную БД Скачаем DLE и GUI для DLE Создадим сеть docker, в которой будут работать контейнеры docker-compose должен быть версии >= 2.12 Соберём образы для запуска и скачаем те, что не нужно собирать. Сборка образов требуется для: Keyclok - чтобы включить поддержку БД PostgreSQL и добавить возможности отдавать метрики в формате Prometheus PostgreSQL - чтобы добавить в контейнер WAL-G и установить кодировку контейнера в ru_RU.UTF-8. Будет эмулировать сервер-источник БД. DLE GUI - тут нет готовых контейнеров, только собирать DLE - не имеется сборок для arm64, поэтому в моём случае только собирать самостоятельно Extended PostgreSQL - для добавления в контейнер, в котором будут запускаться клоны, необходимых кодировок и модулей Для работы DLE необходимо, чтобы логи писались на английском, а также кодировка в контейнере была C или en_US.UTF-8. Это нужно для корректного определения даты последнего checkpoint-а и определения активности в клоне, коя определяется, в том числе, по файлам логов, где производится поиск определённой подстроки. Пример как определяется Time of latest checkpoint У меня на сервере-источнике была кодировка ru_RU.UTF-8 и БД была создана в ней. Для того чтобы extended-postgres запустился, мне потребовалось пересобрать контейнер, добавив в контейнер кодировку ru_RU.UTF-8. Заодно я сделал её кодировкой по-умолчанию, что привело к проблемам. Обратите на это внимание. В контейнере должна присутствовать кодировка, в которой была создана БД Основная кодировка контейнера должна быть  C или en_US.UTF-8   Запустим и настроим MinIO Теперь нужно дождаться разворота демо базы. Должно появится сообщение "database system is ready to accept connections" или "система БД готова принимать подключения", в зависимости от кодировки контейнера с PostgreSQL-источником (задаётся переменными окружения LANG и LC_ALL в файле docker-compose.yml). В контейнер с PostgreSQL, в каталог /docker-entrypoint-initdb.d/ монтируются файлы 01-init.sql и demo-small-20170815.sql. Все файлы, находящиеся в этом каталоге выполняются по очереди после первичной инициализации БД. Это делается один раз, если БД не существует. Содержимое 01-init.sql Скриптом 01-init.sql мы создаём необходимые БД в инстансе, а потом выполняем наполнение БД demo. БД keycloak и dlegui будут использоваться для соответствующих сервисов. Обратите внимание, чтобы пароль для пользователя/роли keycloak совпадал с тем, что был задан в файле .env Чтобы не настраивать Keycloak вручную, загрузим в него заранее подготовленные настройки и удалим импорт-контейнер. Из настроек было сделано: Создание реалма dlegui Создание клиента dlegui и его настройка Создание групп Admins, Operators, Users, Viewers Создание пользователей dg-admin, dg-operator, dg-user1, dg-user2, dg-viewer и назначение им соответствующих групп После того, как в логе появится строка "INFO  [io.quarkus] (main) Keycloak stopped in 0.087s" можно удалять контейнер импорта Запустим рабочий экземпляр Keycloak Если разворот происходит где-то в облаках, на адресе не из серых зон, то нужно сделать так, чтобы keycloak работал без HTTPS Создадим полную резервную копию инстанса PostgreSQL, чтобы было что восстанавливать в DLE Проверить, что резервная копия была создана можно командой Эта операция не единоразовая. Если вы собираетесь использовать WAL-G и далее, нужно добавить в планировщик 2 задачи, выполняющиеся раз в сутки Старый libpq не умеет работать с паролями в scram-sha-256, только в md5, поэтому нужно сменить пароль для пользователя postgres, чтобы DLE GUI мог подключиться. Хоть при запуске и была указана опция "password_encryption=md5", но пароль пользователя postgres в 14 PostgreSQL создаётся, видимо, до активации данной опции. И чтобы всё нормально работало нужно заново установить пароль. Итак, у нас запущены следующие сервисы: S3 (MinIO). Порты 9000-9001. Зайти проверить можно наhttp://${LOCAL_IP}:9000 с логином ${ADMIN_USER} и паролем${ADMIN_PASSWORD} PostgreSQL, сделана резервная копия, настроена отправка журналов в S3. Работает на http://${LOCAL_IP}:5432. Логин posgtres, пароль ${ADMIN_PASSWORD} IdP Keycloak. Работает на http://${LOCAL_IP}:8080. Логин ${ADMIN_USER} и пароль ${ADMIN_PASSWORD} DLE GUI. Работает на http://${LOCAL_IP}:8008. На текущий момент DLE GUI не работоспособен, до момента запуска DLE, но при открытии адреса уже переадресует на Keycloak. Дальше нужно запустить DLE. В моём случае я переключаюсь на ВМ (в вашем может быть тот же или отдельный хост), клонирую туда репозиторий https://gitlab.com/ssi444/dblab-article и выполняю сборку контейнеров extended-postres и DLE. Основа для extended-postres находится тут. Это контейнер с PostgreSQL, WAL-G и тем набором расширений, что используются в БД-источнике. Мажорная версия PostgreSQL должны быть точно такая же, как и на сервере источнике, т.к. под капотом у WAL-G используется pg_basebackup, который архивирует БД в двоичном формате. Расширение Citus для Postgres, используемое в примере, имеет сборки только для архитектуры amd64. К тому же для демо это расширение не используется, поэтому оно было удалено из образа extended-postgres. Если оно вам необходимо, добавьте его самостоятельно. Создаём пул ZFS. В файле конфигурации DLE уже прописаны некоторые настройки, поэтому создаём всё как описано Вместо /dev/sdb нужно указать выделенный диск, на котором будет создан пул ZFS. Если выделенного диска нет, то делаем так: ВНИМАНИЕ! В контейнерах используется ZFS версии 2.0. Если на сервере, где будет запускаться DLE установлена версия 0.8, то могут быть проблемы. Нужно обновить версию ZFS сервера до 2. для amd64 для arm64 В файле conf/dle/test1/engine/configs/server.yml нужно прописать IP-адрес сервера MinIO (${LOCAL_IP}) в переменной AWS_ENDPOINT (в двух местах) Немного прокомментирую конфиг: С более подробными пояснениями для параметров от разработчиков можно ознакомиться в файле конфигурации. Хочу обратить внимание вот на какой момент: чтобы план запроса показывал, с большой вероятностью, те же значения, что и на продуктиве, нужно настроить параметры для инстансов-клонов как на продуктиве. Нужно выполнить такой запрос: Результат этого запроса прописать в файле конфигурации в секцию Подробнее можно почитать в официальной документации. Есть разные подходы к работе с клонами. Например, создавать клон на каждый запрос для какой-то проверки. Примерно так работает JoeBot в SaaS сервисе postgres.ai. В этом случае для maxIdleMinutes нужно выставлять небольшие значения, чтобы клоны быстрее зачищались (очистка запускается каждые 15 минут, если я не ошибаюсь). Мы в компании используем такой поход: при заливке кода в ветку staging происходит сборка ПО, создание нового клона с определённым шаблоном в имени, запускается собранное ПО, которому в качестве параметров передаётся только что созданный клон. Клону при создании устанавливается защита от удаления. При следующей сборке ПО, со всех клонов, с определённым шаблоном в имени, снимается защита от удаления и создаётся новый защищённый клон. Далее уже автоочистка самостоятельно подчистит ставшие ненужными клоны (в которых не было активности maxIdleMinutes минут). В дополнение к CI несколько аналитиков создают себе клоны для своих целей. Защита на них не ставится и через какое-то время, после того как аналитик перестал работать с клоном, он удаляется автоочисткой. В нашем случае оптимальным значением maxIdleMinutes является 1440 минут (сутки). Посмотреть пример скрипта можно тут Ну а теперь пришло время запустить вишенку на нашем торте - DLE После запуска у нас появятся контейнеры dletest-dle-1 и dblab_phr_ccq5nieficds73d508u0, потом dblab_phr_ccq5nieficds73d508u0 сменится dblab_promote_ccq5nieficds73d508u0 и останется только  dblab_sync_ccq5nieficds73d508u0 и dletest-dle-1. Вместоccq5nieficds73d508u0 у каждого будет свой уникальный идентификатор инстанса. Это нормальная работа. DLE вытащил резервную копию, запустил на её основе новый инстанс, сделал его репликой и стал применять новые журналы из S3. Если посмотреть лог DLE (docker-compose --env-file=.env -p dletest -f docker-compose.yml logs -f dle), то там будет видно, как применяется файл conf/dle/test1/engine/configs/preprocessing/01.sql, в котором прописано переименование БД demo в demo_renamed. Если произошли какие-то ошибки, то в логе можно увидеть что именно произошло. Теперь заходим на http://${LOCAL_IP}:8008. Произойдёт редирект на Keycloak (http://${LOCAL_IP}:8080). Вводим логин и пароль (dg-admin/dg-admin). Снова редирект на DLE GUI. Мы должны увидеть такую картину Теперь можно создать пару клонов под пользователями dg-user1 и dg-user2, сделать в одном изменения, убедиться, что на другом клоне это никак не отразилось. Также видно, что у каждого пользователя в списке только свои клоны, чужих он не видит. Зайдём в Keycloak (http://${LOCAL_IP}:8080), авторизуемся под ${ADMIN_USER} и ${ADMIN_PASSWORD} и добавим пользователя dg-user1 в группу Viewers  После этого dg-user1 должен увидеть в списке клоны, созданные пользователем dg-user2. Если же попробовать выполнить какие-либо операции над чужими клонами, кто произойдёт ошибка. До версии 2.5, при перезапуске или обновлении DLE, удалялись все клоны, что было очень неудобно, если клоны должны быть долгоживущими. После 2.5 состояния и настройки клонов стали сохраняться, что позволяет обновлять DLE без потери созданных клонов. Если нужно изменить настройки DLE, то после изменения файла конфигурации нужно либо перезапустить контейнер, либо выполнить команду Это заставит DLE перечитать конфигурацию. После выхода 3-й версии стало возможным не удаляя контейнер изменить снимок. Фактически создаётся новый контейнер, но со старыми настройками. Если какое-то ПО настроено на определённый клон, таким образом можно обновлять данные в БД, не теряя настроек. Если, конечно, приложение сможет пережить смену БД или её структуры. Чтобы продемонстрировать, как обновляются данные в DLE, зайдём в один из созданных клонов и посмотрим какие БД там есть. В моём примере клон имеет запущен по порту 6003. Обратите на это внимание при подключении. У нас пока есть только 1 снимок и клон создан на его основе. Внесём изменения в БД-источник. Обратите внимание на параметры подключения. 192.168.144.114 - это ${LOCAL_IP}. Мы создали новую БД demo2. Создадим в ней одну таблицу и заполним её сотней строк. Когда появятся строки новый снимок готов. Создаём клон на основе нового снимка и смотрим что внутри Новая база с таблицей и данными есть. В старых же клонах всё осталось как было. Если создать новый клон на первом снимке, то там так же не будет БД demo2. Таким образом можно безопасно проводить любые тесты и эксперименты с БД. Есть ещё один подводный камень, с которым мы столкнулись не так давно, переведя управление PotgreSQL на Patroni. При смене мастера меняется timeline в БД и эталон не всегда корректно обрабатывал эту ситуацию. В какой-то момент, когда timeline сменился, эталон перестал подтягивать новые журналы из S3 и выдал ошибку. После разбора полётов оказалось, что в образе Extended PostgreSQL, который собирался достаточно давно, был зашит WAL-G версии 0.2.19, а резервная копия на сервере-источнике делалась версией WAL-G 2.0.0. WAL-G 0.2.19 корректно восстанавливал полную резервную копию от WAL-G версии 2.0.0, а вот на дифференциальной уже выдавал ошибку. И с восстановлением журналов, по всей видимости, тоже было не всё гладко по этой же причине. После обновления WAL-G в образе Extended PostgreSQL мы пробовали менять timeline на источнике, но эталон больше не ругался. Также хочу добавить, что в версии DLE 4 должен появиться функционал создания снимков по требованию, что позволит не ждать очередной итерации планировщика, а сразу начать работу с самыми свежими данными. Надеюсь статья будет полезна коллегам по цеху. Те, кто не знал про DLE - начнут его использовать, те же, кто знает и использует, возможно, почерпнут для себя что-то новое.