PostgreSQL. Тестируй то, тестируй это, тестируй не боясь

e2b4e41ab5d9b69ffae46b8468560310.png

Вступление

Я всех категорически приветствую. Долгое время я с нетерпением ждал появления на хабре статей об одном замечательном проекте — Database Lab Engine. Но время шло, статей всё не было, а терпение заканчивалось. Трудно держать в себе знание,  которое может быть полезно многим. И моё терпение подошло к концу. Так что схватитесь покрепче за ручки кресел, чтобы вас не унесло потоком сакрального знания в метавселенную, и приступим.

Вводная часть

Database Lab Engine (далее DLE) — проект, призванный облегчить процесс тестирования изменений, связанных с БД (миграции, тесты, изменяющие данные и т.д.). Очень непросто провести полноценное тестирование без изменяющих действий. Кто-то обходится схемой БД с каким-то минимальным набором тестовых данных. Но таким образом не всегда полноценно можно проверить те или иные аспекты функционирования ПО. Какие-то ошибки можно выловить только на продуктивном наборе данных или максимально приближенном к нему. И в этом случае DLE может сильно облегчить процесс подготовки БД для предпродуктивного тестирования.

Работает это следующим образом:

Примечание: Далее речь будет идти о «физическом» режиме работы DLE + резервная копия с помощью инструмента WAL-G. DLE также позволяет работать с логическими резервными копиями, используя несколько пулов ZFS для ротации снимков. Подробнее можно почитать в глубинах документации.

  1. Подготавливается резервная копия БД с помощью WAL-G или PGBACKREST

  2. Подготавливается сервер, на котором можно запускать docker-контейнеры. На сервере должна быть установлена ZFS и создан хотя бы один пул ZFS.

  3. В конфигурационном файле DLE прописываются параметры доступа к S3, где находится резервная копия и точка монтирования ZFS пула.

    Желательно обращаться в S3 под разными учётными записями. Размещать резервную копию под учётной записью имеющей доступ на запись, а читать резервную копию с другой учётной записью, имеющей доступ только на чтение

  4. Запускается контейнер DLE, который разворачивает на ZFS пуле резервную копию и начинает поддерживать её в актуальном состоянии, накатывая WAL-журналы из S3. Таким образом фактически получается hot-standby сервер PostgreSQL. Назовём этот «набор данных» (а фактически это инстанс PostgreSQL) «эталонным» или просто «эталон».

  5. Раз в сутки (расписание настраивается в файле конфигурации DLE) с эталона делается снимок (snaphot в терминах ZFS). После создания снимка выполняется его подготовка к последующему использованию. В этот момент можно применить к данным в снимке свои изменения. В определённом каталоге размещаются .sql файлы, которые применяются к снимку. Порядок применения регулируется именем файла — как получили список файлов в каталоге, так и применили.

  6. Далее на основе созданных снимков становится возможным создавать «клоны» — изолированные инстансы PostgreSQL. Вся прелесть в том, что «долгая» работа выполняется при создании снимка, а клон (тоже snapshot ZFS) уже создаётся за секунды. Клон использует блоки данных снимка и сразу после создания практически не занимает места на диске. Но любые изменения в клоне уже создают новые блоки данных и занимают место.

Совокупность того, что ZFS использует механизм CoW (Copy-On-Write), а также хорошо ужимает данные и позволяет называть клоны «тонкими»

Закрепим терминологию:
Снимок — предварительно подготовленная копия производственного инстанса PostgreSQL, к которой были применены, например, маскирование чувствительных данных или переименование/изменение/удаление какой-либо сущности и т.п. Поэтому в клонах будут присутствовать все БД инстанса PostgreSQL, если, конечно, они не были удалены на этапе подготовки снимка

Клон — новый инстанс PostgreSQL, который создан на основе какого-либо снимка. Клон использует блоки данных снимка, поэтому при создании не занимает места. Но любые изменения в клоне, приводящие к изменению данных, увеличивают «размер» клона. Каждый клон имеет свой собственный уникальный порт.

Допускается наличие множества снимков, фактически являющихся срезами данных за разные периоды времени и множества клонов (читай: запущенных инстансов PostgreSQL), созданных на основе любых наличных снимков. Это позволяет, например, делать сравнение данных за разные периоды. Любая модификация данных в одном из клонов не затрагивает другие клоны. Расплатой за все эти приятности является довольно существенные ресурсы, которые нужно выделять на сервер DLE. Также стоит отметить, что ZFS довольно сильно ужимает данные (в 3–5 раз), что позволяет сильно экономить на дисках. Но диски лучше использовать SSD, т.к. несколько одновременно запущенных клонов, в которых есть существенная активность, быстро отправят обычный HDD в кому.

Описание демонстрационного стенда

Для демонстрации одной из возможных схем работы мы создадим стенд, состоящий из следующих компонентов:

  1. MinIO — S3-совместимое хранилище для хранения резервных копий и журналов WAL

  2. PostgreSQL — СУБД

  3. Keycloak — Identity Provider (IdP). Аутентификация и авторизация пользователей.

  4. DLE — движок для создания тонких клонов

  5. 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 учётных записи (УЗ):

  1. postgres-backup-rw — УЗ с правами «чтение и запись», для размещения резервных копий и журналов

  2. postgres-backup-ro — УЗ с правами «только чтение», для DLE

Для стенда используется заранее подготовленная конфигурация MinIO, в которой содержатся необходимые пользователи и роли.

В Keycloak будет 5 учётных записей, представляющих типичные роли для работы в DLE:

  1. dg-admin — роль «Администратор»

  2. dg-operator — роль «Оператор»

  3. dg-user1 — роль «Пользователь»

  4. dg-user2 — роль «Пользователь»

  5. dg-viewer — роль «Наблюдатель»

Пароль УЗ в Keycloak такой же, как и имя пользователя.

Описания ролей:

  1. Администратор (admin)

  • Просмотр списка всех клонов

  • Создание нового клона

  • Сброс любого клона

  • Удаление любого клона

  • Установка/снятие защиты для любого клона

  1. Оператор (operator)

  • Просмотр списка всех клонов

  • Создание нового клона

  • Сброс любого клона

  • Удаление клона, созданного этим пользователем

  • Установка/снятие защиты для клонов, созданных этим пользователем

  1. Пользователь (user)

  • Просмотр списка клонов, созданных этим пользователем

  • Создание нового клона

  • Сброс клона, созданного этим пользователем

  • Удаление клона, созданного этим пользователем

  • Установка/снятие защиты для клонов, созданных этим пользователем

  1. Наблюдатель (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 в репозитории может отличаться

sudo apt install build-essential autoconf automake libtool gawk alien fakeroot dkms libblkid-dev uuid-dev libudev-dev libssl-dev zlib1g-dev libaio-dev libattr1-dev libelf-dev linux-headers-generic python3 python3-dev python3-setuptools python3-cffi libffi-dev python3-packaging git libcurl4-openssl-dev
git clone https://github.com/openzfs/zfs
cd ./zfs
git checkout master
sh autogen.sh
./configure
./configure --with-linux=/usr/src/linux-headers-5.8.0-63-generic --with-linux-obj=/usr/src/linux-headers-5.8.0-63-generic
make -s -j$(nproc) deb
sudo dpkg -i kmod-zfs-5.8.0-63-generic_2.1.99-1566_amd64.deb libzfs5_2.1.99-1566_amd64.deb libzpool5_2.1.99-1566_amd64.deb zfs_2.1.99-1566_amd64.deb
echo "search extra updates ubuntu built-in" | sudo tee /etc/depmod.d/ubuntu.conf
sudo ldconfig; sudo depmod; sudo modprobe zfs
zfs version

Далее скачаем все необходимые конфигурационные файлы для стенда

git clone https://gitlab.com/ssi444/dblab-article.git
cd dblab-article

Далее нужно скопировать шаблон с настройками и внести изменения, в соответствии с вашим окружением

cp .env-template .env

Содержимое файла

# Логин пользователя и пароль для тех подсистем где его потребуется создать (MinIO, Keycloak)
# В PostgreSQL пользователь будет postgres, а пароль ${ADMIN_PASSWORD}
ADMIN_USER=admin
ADMIN_PASSWORD=password

# Адрес и токен сервера DLE
DBLAB_URL=http://192.168.144.138:2345
DBLAB_TOKEN=super-secret

# Локальный адрес компьютера. Используется для переадресации из keycloak во время аутентификации в DLE GUI
LOCAL_IP=192.168.144.114

В моём случае DLE будет работать в виртуальной машине (ВМ) с Debian Linux, имеющей адрес 192.168.144.138, поэтому я указываю его в DBLAB_URL. Локальный адрес хоста — 192.168.144.114. На этом адресе будут на разных портах доступны сервисы MinIO, Keycloak, DLE GUI. Этот адрес доступен также и с ВМ. Локальный адрес нужно прописать в переменной LOCAL_IP. Если вы разворачиваете стенд на одном или нескольких серверах, доступных по DNS-именам, то можно указать их.

Импортируем настройки

source .env

Демо-база

Скачаем и распакуем демонстрационную БД

wget https://edu.postgrespro.ru/demo-small.zip -O ./demo-small.zip && unzip demo-small.zip

Загрузка конфигов

Скачаем DLE и GUI для DLE

git clone https://gitlab.com/postgres-ai/database-lab -b v3.2.0 ./dblab
git clone https://gitlab.com/ssi444/dle-gui

Создадим сеть docker, в которой будут работать контейнеры

docker network create dletest

Загрузка и сборка контейнеров

docker-compose должен быть версии >= 2.12

Соберём образы для запуска и скачаем те, что не нужно собирать.

docker-compose --env-file=.env -p dletest -f docker-compose.yml build postgres
docker-compose --env-file=.env -p dletest -f docker-compose.yml build keycloak
docker-compose --env-file=.env -p dletest -f docker-compose.yml build keycloak-config-import
docker-compose --env-file=.env -p dletest -f docker-compose.yml build dlegui
docker-compose --env-file=.env -p dletest -f docker-compose.yml pull
docker pull minio/mc

Сборка образов требуется для:

  • 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

$ /usr/lib/postgresql/14/bin/pg_controldata -D /db/data
pg_control version number:            1100
Catalog version number:               201809051
Database system identifier:           6706409297065165594
Database cluster state:               in production
pg_control last modified:             Tue 27 Sep 2022 04:29:39 PM UTC
Latest checkpoint location:           4600/4B000028
Latest checkpoint's REDO location:    4600/4B000028
Latest checkpoint's REDO WAL file:    00000009000046000000004B
Latest checkpoint's TimeLineID:       9
Latest checkpoint's PrevTimeLineID:   9
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID:          0:2120415942
Latest checkpoint's NextOID:          202362995
Latest checkpoint's NextMultiXactId:  37450860
Latest checkpoint's NextMultiOffset:  94046646
Latest checkpoint's oldestXID:        1976274274
Latest checkpoint's oldestXID's DB:   1
Latest checkpoint's oldestActiveXID:  0
...
**Time of latest checkpoint:            Tue 27 Sep 2022 04:29:39 PM UTC**
... 

У меня на сервере-источнике была кодировка ru_RU.UTF-8 и БД была создана в ней. Для того чтобы extended-postgres запустился, мне потребовалось пересобрать контейнер, добавив в контейнер кодировку ru_RU.UTF-8. Заодно я сделал её кодировкой по-умолчанию, что привело к проблемам. Обратите на это внимание.

В контейнере должна присутствовать кодировка, в которой была создана БД Основная кодировка контейнера должна быть C или en_US.UTF-8

Настройка MinIO

Запустим и настроим MinIO

docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d minio
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc alias set s3 "http://${LOCAL_IP}:9000" ${ADMIN_USER} ${ADMIN_PASSWORD}
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc mb s3/postgres-backup
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc admin user add s3 postgres-backup-ro postgres-backup-ro
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc admin user add s3 postgres-backup-rw postgres-backup-rw
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc -v $(pwd)/conf/minio:/conf minio/mc admin policy add s3 postgres-backup-ro /conf/role_postgres-backup_ro.json
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc -v $(pwd)/conf/minio:/conf minio/mc admin policy add s3 postgres-backup-rw /conf/role_postgres-backup_rw.json
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc admin policy set s3 postgres-backup-ro user=postgres-backup-ro
docker run --rm --name minio-client -v $(pwd)/data/minio-client:/root/.mc minio/mc admin policy set s3 postgres-backup-rw user=postgres-backup-rw

Запуск и настройка БД

docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d postgres
docker-compose --env-file=.env -p dletest -f docker-compose.yml logs -f postgres

Теперь нужно дождаться разворота демо базы. Должно появится сообщение «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

SELECT 'CREATE DATABASE demo' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'demo')\gexec
SELECT 'CREATE DATABASE keycloak' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'keycloak')\gexec
SELECT 'CREATE DATABASE dlegui' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'dlegui')\gexec
SELECT 'CREATE ROLE keycloak WITH LOGIN SUPERUSER PASSWORD ''password''' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname='keycloak')\gexec

Скриптом 01-init.sql мы создаём необходимые БД в инстансе, а потом выполняем наполнение БД demo. БД keycloak и dlegui будут использоваться для соответствующих сервисов.

Обратите внимание, чтобы пароль для пользователя/роли keycloak совпадал с тем, что был задан в файле .env

Запуск и настройка Keycloak

Чтобы не настраивать Keycloak вручную, загрузим в него заранее подготовленные настройки и удалим импорт-контейнер. Из
настроек было сделано:

  1. Создание реалма dlegui

  2. Создание клиента dlegui и его настройка

  3. Создание групп Admins, Operators, Users, Viewers

  4. Создание пользователей dg-admin, dg-operator, dg-user1, dg-user2, dg-viewer и назначение им соответствующих групп

docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d keycloak-config-import
docker-compose --env-file=.env -p dletest -f docker-compose.yml logs -f keycloak-config-import

После того, как в логе появится строка «INFO [io.quarkus] (main) Keycloak stopped in 0.087s» можно удалять контейнер импорта

docker rm -f dletest-keycloak-config-import-1

Запустим рабочий экземпляр Keycloak

docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d keycloak

Если разворот происходит где-то в облаках, на адресе не из серых зон, то нужно сделать так, чтобы keycloak работал без HTTPS

docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres psql -U postgres -d keycloak -c "update REALM set ssl_required='none'"

Резервная копия БД

Создадим полную резервную копию инстанса PostgreSQL, чтобы было что восстанавливать в DLE

docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres su -l postgres -c '/bin/wal-g --config /wal-g/.wal-g.json backup-push /var/lib/postgresql/data'

Проверить, что резервная копия была создана можно командой

docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres su -l postgres -c '/bin/wal-g --config /wal-g/.wal-g.json backup-list'

Эта операция не единоразовая. Если вы собираетесь использовать WAL-G и далее, нужно добавить в планировщик 2 задачи, выполняющиеся раз в сутки

# Создание полной или инкрементальной резервной копии
docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres su -l postgres -c '/bin/wal-g --config /wal-g/.wal-g.json backup-push /var/lib/postgresql/data'
# Удаление старых резервных копий
docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres su -l postgres -c '/bin/wal-g --config /wal-g/.wal-g.json delete --confirm retain FULL 4'

Запуск DLE GUI

Старый libpq не умеет работать с паролями в scram-sha-256, только в md5, поэтому нужно сменить пароль для пользователя postgres, чтобы DLE GUI мог подключиться. Хоть при запуске и была указана опция «password_encryption=md5», но пароль пользователя postgres в 14 PostgreSQL создаётся, видимо, до активации данной опции. И чтобы всё нормально работало нужно заново установить пароль.

docker-compose --env-file=.env -p dletest -f docker-compose.yml exec postgres psql -U postgres -c "alter role postgres password '${ADMIN_PASSWORD}';"
docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d dlegui

Итак, у нас запущены следующие сервисы:

  1. S3 (MinIO). Порты 9000-9001. Зайти проверить можно наhttp://${LOCAL_IP}:9000 с логином ${ADMIN_USER} и паролем${ADMIN_PASSWORD}

  2. PostgreSQL, сделана резервная копия, настроена отправка журналов в S3. Работает на http://${LOCAL_IP}:5432. Логин posgtres, пароль ${ADMIN_PASSWORD}

  3. IdP Keycloak. Работает на http://${LOCAL_IP}:8080. Логин ${ADMIN_USER} и пароль ${ADMIN_PASSWORD}

  4. DLE GUI. Работает на http://${LOCAL_IP}:8008.

На текущий момент DLE GUI не работоспособен, до момента запуска DLE, но при открытии адреса уже переадресует на Keycloak.

Сборка и запуск DLE

Дальше нужно запустить 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 уже прописаны некоторые настройки, поэтому создаём всё как описано

mkdir -p /var/lib/dblab/pools/test1
mkdir -p /data/dblab/test1
cp -r conf/dle/test1/* /data/dblab/test1/
sudo zpool create -f   -O compression=on   -O atime=off   -O recordsize=128k   -O logbias=throughput   -m /var/lib/dblab/pools/test1/dblab_pool_test1   dblab_pool_test1  /dev/sdb

Вместо /dev/sdb нужно указать выделенный диск, на котором будет создан пул ZFS. Если выделенного диска нет, то делаем так:

# делаем пустой файл на 10Гб
sudo mkdir -p /zfs
sudo dd if=/dev/zero of=/zfs/file.img bs=1G count=10
# создаём в нём пул ZFS
sudo zpool create -f   -O compression=on   -O atime=off   -O recordsize=128k   -O logbias=throughput   -m /var/lib/dblab/pools/test1/dblab_pool_test1   dblab_pool_test1  /zfs/file.img
# проверим что пул есть
sudo zpool list

ВНИМАНИЕ! В контейнерах используется ZFS версии 2.0. Если на сервере, где будет запускаться DLE установлена версия 0.8, то могут быть проблемы. Нужно обновить версию ZFS сервера до 2.

для amd64

docker build -t dblab-ext-pg:v1 -f Dockerfile.dblab-ext-pg .

для arm64

docker build --build-arg ARCH=arm64 -t dblab-ext-pg:v1 -f Dockerfile.dblab-ext-pg .
sed -i 's/GOARCH = amd64/GOARCH = arm64/; s/ARCHITECTURES=amd64/ARCHITECTURES=arm64/' dblab/engine/Makefile
# в macos sed немного по-другому работает и команда будет выглядеть так
# sed -i '' 's/GOARCH = amd64/GOARCH = arm64/; s/ARCHITECTURES=amd64/ARCHITECTURES=arm64/' dblab/engine/Makefile
docker-compose --env-file=.env -p dletest -f docker-compose.yml build dle

В файле conf/dle/test1/engine/configs/server.yml нужно прописать IP-адрес сервера MinIO (${LOCAL_IP}) в переменной AWS_ENDPOINT (в двух местах)

Немного прокомментирую конфиг:

server:
  # Токен, с которым нужно обращаться к API DLE 
  verificationToken: "super-secret"
  host: ""
  port: 2345
embeddedUI:
  # GUI, поставляемое с DLE. 
  # сборки для arm64 нет, поэтому у меня отключено, т.к. я использую другой GUI 
  enabled: false
  dockerImage: "postgresai/ce-ui:latest"
  host: ""
  port: 3345
global:
  # Другие движки, кроме postgres, пока не поддерживаются. Оставляем как есть 
  engine: postgres
  # ВАЖНО! Лучше всегда устанавливать этот параметр в `true`, чтобы в случае ошибок легче было найти концы.
  debug: true
  database:
    username: postgres
    dbname: postgres
  telemetry:
    # Тут по желанию можно включить
    enabled: false
    url: "https://postgres.ai/api/general"
poolManager:
  # Точка монтирования пула ZFS. Лучше передавать в контейнер тот же самый каталог по тем же путям, 
  # как и на хост-системе.
  # Внутри mountDir могут быть либо пулы, либо Dataset-ы одного пула. 
  mountDir: /var/lib/dblab/pools/test1
  dataSubDir: data
  clonesMountSubDir: clones
  socketSubDir: sockets
  observerSubDir: observer
  preSnapshotSuffix: "_pre"
  # Если в mountDir, несколько пулов/Dataset-ов, то тут можно указать нужный.
  # Остальные пулы/Dataset-ы не рассматриваются, соответственно, ротация отключена
  selectedPool: ""
databaseContainer: &db_container
  # Контейнер, на основе которого будет запускаться восстановление БД из архива, 
  # синхронизация эталона с архивом, клоны. 
  dockerImage: "dblab-ext-pg:v1"
  # Дополнительные параметры, которые можно задать для контейнера с БД.
  # Это параметры именно контейнера, а не PostgreSQL
  containerConfig:
    "shm-size": "2gb"
databaseConfigs: &db_configs
  # Параметры, которые можно передать запускаемым экземплярам PostgreSQL.
  # В архив, создаваемый WAL-G, попадает полная конфигурация PostgreSQL с сервера-источника.
  # Тут можно переопределить некоторые из параметров PostgreSQL, если есть необходимость.
  # Как узнать какие параметры нужно прописывать можно почитать тут:
  # https://postgres.ai/docs/how-to-guides/administration/postgresql-configuration#postgresql-configuration-in-clones
  configs:
    shared_buffers: 2GB
    shared_preload_libraries: "pg_stat_statements, auto_explain, logerrors"
    work_mem: "32MB"
    max_connections: 500
    lc_messages: "en_US.UTF-8"
    lc_monetary: "ru_RU.UTF-8"
    lc_numeric: "ru_RU.UTF-8"
    lc_time: "ru_RU.UTF-8"
    datestyle: "iso, dmy"
    timezone: "W-SU"
provision:
  <<: *db_container
  # Диапазон портов, на которых будут доступны клоны для данного инстанса DLE.
  # Если планируется одновременный запуск более 100 клонов, то нужно увеличить диапазон.
  portPool:
    from: 6001
    to: 6100
  useSudo: false
  # По умолчанию при создании клона там создаётся новая учётная запись с правами суперпользователя,
  # а всем существующим УЗ сбрасываются пароли на случайные. Если нужна возможность подключаться к БД
  # под УЗ существующих пользователей, например, чтобы протестировать правильность выдачи прав на объекты БД, 
  # то нужно значение этого параметра установить в `true` 
  keepUserPasswords: true
retrieval:
  jobs:
     # Какие задачи и в какой последовательности будут выполняться.
     # Сначала запускается контейнер (имеет в своём названии _phr_), вытягивающий архив из S3. 
     # После окончания разворачивания архива он завершается. 
     # После него запускается контейнер (имеет в своём названии _sync_), который мониторит S3 на наличия новых журналов
     # и применяет их к эталону.
    - physicalRestore
    # После того как развернётся БД из архива (а также по расписанию, указанному ниже), 
    # запускается задача создания снимка 
    - physicalSnapshot
  spec:
    # Настройки задач, указанных выше.
    physicalRestore:
      options:
        <<: *db_container
        tool: walg
        sync:
          enabled: true
          healthCheck:
            interval: 5
            maxRetries: 200
          configs:
            shared_buffers: 2GB
          recovery:
        envs:
          # Параметры доступа к S3 для WAL-G 
          AWS_ENDPOINT: "http://192.168.144.114:9000"
          AWS_S3_FORCE_PATH_STYLE: "true"
          AWS_ACCESS_KEY_ID: "postgres-backup-ro"
          AWS_SECRET_ACCESS_KEY: "postgres-backup-ro"
          WALG_S3_PREFIX: "s3://postgres-backup"
          # Кодировка для "системы" контейнера. Чтобы DLE работал корректно, можно явно задать эти настройки контейнеру
          LANG: "en_US.UTF-8"
          LC_ALL: "en_US.UTF-8"
        walg:
          backupName: LATEST
    physicalSnapshot:
      options:
        skipStartSnapshot: false
        <<: *db_configs
        promotion:
          <<: *db_container
          enabled: true
          healthCheck:
            interval: 5
            maxRetries: 200
          queryPreprocessing:
            # Каталог, в котором находятся .sql-файлы, применяемые в каждому создаваемому снимку
            queryPath: "/home/dblab/configs/preprocessing/"
            maxParallelWorkers: 2
          configs:
            shared_buffers: 2GB
          recovery:
        # Скрипт, выполняемый после создания снимка.
        # К примеру, можно удалить логи, доставшиеся клону от эталона
        preprocessingScript: "/home/dblab/configs/scripts/clear_old_log.sh"
        scheduler:
          # Расписание создания новых снимков
          snapshot:
             # 04:15 ежедневно
             timetable: "15 4 * * *"
          # Расписание удаления старых снимков
          retention:
            # 05:25 ежедневно
            timetable: "25 5 * * *"
            # Сколько снимков нужно оставить. Оставляет N-последних снимков + те снимки, 
            # на основе которых созданы клоны
            limit: 3
        envs:
          # Параметры доступа к S3 для WAL-G
          AWS_ENDPOINT: "http://192.168.144.114:9000"
          AWS_S3_FORCE_PATH_STYLE: "true"
          AWS_ACCESS_KEY_ID: "postgres-backup-ro"
          AWS_SECRET_ACCESS_KEY: "postgres-backup-ro"
          WALG_S3_PREFIX: "s3://postgres-backup"
          # Кодировка для "системы" контейнера. Чтобы DLE работал корректно, можно явно задать эти настройки контейнеру
          LANG: "en_US.UTF-8"
          LC_ALL: "en_US.UTF-8"
cloning:
  accessHost: "192.168.144.138"
  # Через сколько нужно удалять клоны, если в них не было активности.
  # Активность, в том числе, определяется по логам PostgreSQL и датам транзакций.
  # Удаляются только те клоны, на которые не установлена защита от удаления.
  maxIdleMinutes: 120

С более подробными пояснениями для параметров от разработчиков можно ознакомиться в файле конфигурации.

Есть разные подходы к работе с клонами. Например, создавать клон на каждый запрос для какой-то проверки. Примерно так работает JoeBot в SaaS сервисе postgres.ai. В этом случае для maxIdleMinutes нужно выставлять небольшие значения, чтобы клоны быстрее зачищались (очистка запускается каждые 15 минут, если я не ошибаюсь).

Мы в компании используем такой поход: при заливке кода в ветку staging происходит сборка ПО, создание нового клона с определённым шаблоном в имени, запускается собранное ПО, которому в качестве параметров передаётся только что созданный клон. Клону при создании устанавливается защита от удаления. При следующей сборке ПО, со всех клонов, с определённым шаблоном в имени, снимается защита от удаления и создаётся новый защищённый клон. Далее уже автоочистка самостоятельно подчистит ставшие ненужными клоны (в которых не было активности maxIdleMinutes минут).

В дополнение к CI несколько аналитиков создают себе клоны для своих целей. Защита на них не ставится и через какое-то время, после того как аналитик перестал работать с клоном, он удаляется автоочисткой. В нашем случае оптимальным значением maxIdleMinutes является 1440 минут (сутки).

Посмотреть пример скрипта можно тут

Ну, а теперь пришло время запустить вишенку на нашем торте — DLE

docker-compose --env-file=.env -p dletest -f docker-compose.yml build dle
docker-compose --env-file=.env -p dletest -f docker-compose.yml up -d dle
docker-compose --env-file=.env -p dletest -f docker-compose.yml logs -f 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.

dletest-dle-1  | 2022/09/28 14:36:08 queryPreprocessor.go:138: [INFO]   Run psql command [psql -U postgres -d postgres --file /home/dblab/configs/preprocessing/01.sql]
dletest-dle-1  | 2022/09/28 14:36:08 queryPreprocessor.go:61: [INFO]   Run SQL: /home/dblab/configs/preprocessing/01.sql
dletest-dle-1  |  ALTER DATABASE
dletest-dle-1  | 2022/09/28 14:36:08 tools.go:278: [INFO]   Run checkpoint command [psql -U postgres -d postgres -XAtc checkpoint]
dletest-dle-1  | 2022/09/28 14:36:08 tools.go:290: [INFO]   Checkpoint result:  CHECKPOINT

Если произошли какие-то ошибки, то в логе можно увидеть что именно произошло.

Теперь заходим на http://${LOCAL_IP}:8008. Произойдёт редирект на Keycloak (http://${LOCAL_IP}:8080). Вводим логин и пароль (dg-admin/dg-admin). Снова редирект на DLE GUI. Мы должны увидеть такую картину

0aad2e16aa2bbf3389cca3a8d33e8c83.png

Теперь можно создать пару клонов под пользователями dg-user1 и dg-user2, сделать в одном изменения, убедиться, что на другом клоне это никак не отразилось. Также видно, что у каждого пользователя в списке только свои клоны, чужих он не видит.

Зайдём в Keycloak (http://${LOCAL_IP}:8080), авторизуемся под ${ADMIN_USER} и ${ADMIN_PASSWORD} и добавим пользователя dg-user1 в группу Viewers

1c82dbc365196817ac56e35366d9e5cb.png

После этого dg-user1 должен увидеть в списке клоны, созданные пользователем dg-user2. Если же попробовать выполнить какие-либо операции над чужими клонами, кто произойдёт ошибка.

До версии 2.5, при перезапуске или обновлении DLE, удалялись все клоны, что было очень неудобно, если клоны должны быть долгоживущими. После 2.5 состояния и настройки клонов стали сохраняться, что позволяет обновлять DLE без потери созданных клонов. Если нужно изменить настройки DLE, то после изменения файла конфигурации нужно либо перезапустить контейнер, либо выполнить команду

docker-compose --env-file=.env -p dletest -f docker-compose.yml exec dle kill -SIGHUP 1

Это заставит DLE перечитать конфигурацию.

После выхода 3-й версии стало возможным не удаляя контейнер изменить снимок. Фактически создаётся новый контейнер, но со старыми настройками. Если какое-то ПО настроено на определённый клон, таким образом можно обновлять данные в БД, не теряя настроек. Если, конечно, приложение сможет пережить смену БД или её структуры.

Чтобы продемонстрировать, как обновляются данные в DLE, зайдём в один из созданных клонов и посмотрим какие БД там есть. В моём примере клон имеет запущен по порту 6003. Обратите на это внимание при подключении.

$ docker run --rm --name pg-tmp -it postgres:14.5 psql "host=192.168.144.138 port=6003 user=admin dbname=postgres"
Password for user admin: 
psql (14.5)
Type "help" for help.

postgres=# \l+
                                                                     List of databases
     Name     |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   |  Size   | Tablespace |                Description                 
--------------+----------+----------+-------------+-------------+-----------------------+---------+------------+--------------------------------------------
 demo_renamed | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 |                       | 281 MB  | pg_default | 
 dlegui       | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 |                       | 9609 kB | pg_default | 
 keycloak     | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 |                       | 13 MB   | pg_default | 
 postgres     | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 |                       | 8553 kB | pg_default | default administrative connection database
 template0    | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +| 8401 kB | pg_default | unmodifiable empty database
              |          |          |             |             | postgres=CTc/postgres |         |            | 
 template1    | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +| 8401 kB | pg_default | default template for new databases
              |          |          |             |             | postgres=CTc/postgres |         |            | 
(6 rows)

postgres=# 

У нас пока есть только 1 снимок и клон создан на его основе. Внесём изменения в БД-источник. Обратите внимание на параметры подключения. 192.168.144.114 — это ${LOCAL_IP}.

$ docker run --rm --name pg-tmp -it postgres:14.5 psql "host=192.168.144.114 port=5432 user=postgres dbname=postgres"
Password for user postgres: 
psql (14.5)
Type "help" for help.

postgres=# CREATE DATABASE demo2 OWNER keycloak;
CREATE DATABASE
postgres=# \l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 demo      | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 demo2     | keycloak | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 dlegui    | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 keycloak  | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 postgres  | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 template0 | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(7 rows)

postgres=# \c demo2
You are now connected to database "demo2" as user "postgres".

Мы создали новую БД demo2. Создадим в ней одну таблицу и заполним её сотней строк.

CREATE TABLE DOCUMENT_TEMPLATE(
   ID INTEGER NOT NULL,
   NAME TEXT,
   SHORT_DESCRIPTION TEXT,
   AUTHOR TEXT,
   DESCRIPTION TEXT,
   CONTENT TEXT,
   LAST_UPDATED DATE,
   CREATED DATE
);

INSERT INTO DOCUMENT_TEMPLATE(id,name, short_description, author,
                              description,content, last_updated,created)
SELECT id, 'name', md5(random()::text), 'name2'
      ,md5(random()::text),md5(random()::text)
      ,NOW() - '1 day'::INTERVAL * (RANDOM()::int * 100)
      ,NOW() - '1 day'::INTERVAL * (RANDOM()::int * 100 + 100)
FROM generate_series(1,100) id;
demo2=# select count(*) from DOCUMENT_TEMPLATE;
 count 
-------
   100
(1 row)

demo2=# 
docker-compose --env-file=.env -p dletest -f docker-compose.yml restart dle
docker-compose --env-file=.env -p dletest -f docker-compose.yml logs -f dle

Когда появятся строки

dletest-dle-1  | 2022/09/28 16:14:02 runners.go:106: [DEBUG]  Run(Local): "zfs snapshot -r dblab_pool_test1/clone_pre_20220928160300@snapshot_20220928161352"
dletest-dle-1  | 2022/09/28 16:14:02 runners.go:151: [DEBUG]  Run(Local): output ""
dletest-dle-1  | 2022/09/28 16:14:02 runners.go:106: [DEBUG]  Run(Local): "zfs set dblab:datastateat="20220928161352" dblab_pool_test1/clone_pre_20220928160300@snapshot_20220928161352"
dletest-dle-1  | 2022/09/28 16:14:02 runners.go:151: [DEBUG]  Run(Local): output ""
dletest-dle-1  | 2022/09/28 16:14:02 zfs.go:335: [DEBUG]  New snapshot: {dblab_pool_test1/clone_pre_20220928160300@snapshot_20220928161352 2022-09-28 16:14:02.234017346 +0000 UTC m=+661.978728658 2022-09-28 16:13:52 +0000 UTC 0 0 dblab_pool_test1}
dletest-dle-1  | 2022/09/28 16:14:02 physical.go:448: [INFO]   Snapshot scheduler has been started

новый снимок готов. Создаём клон на основе нового снимка и смотрим что внутри

$ docker run --rm --name pg-tmp -it postgres:14.5 psql "host=192.168.144.138 port=6004 user=admin dbname=postgres"
Password for user admin: 
psql (14.5)
Type "help" for help.

postgres=# \l
                                   List of databases
     Name     |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
--------------+----------+----------+-------------+-------------+-----------------------
 demo2        | keycloak | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 demo_renamed | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 dlegui       | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 keycloak     | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 postgres     | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | 
 template0    | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +
              |          |          |             |             | postgres=CTc/postgres
 template1    | postgres | UTF8     | ru_RU.UTF-8 | ru_RU.UTF-8 | =c/postgres          +
              |          |          |             |             | postgres=CTc/postgres
(7 rows)

postgres=# \c demo2
You are now connected to database "demo2" as user "admin".             
demo2=# select count(*) from document_template;
 count 
-------
   100
(1 row)

Новая база с таблицей и данными есть. В старых же клонах всё осталось как было. Если создать новый клон на первом снимке, то там так же не будет БД demo2.

© Habrahabr.ru