diff --git a/DBA/pg_basebackup_progress_bar.sql b/DBA/pg_basebackup_progress_bar.sql index 995b9e08..3a8cabab 100644 --- a/DBA/pg_basebackup_progress_bar.sql +++ b/DBA/pg_basebackup_progress_bar.sql @@ -7,17 +7,17 @@ select pg_size_pretty(b.backup_streamed) as pretty_backup_streamed, pg_size_pretty(b.backup_total) as pretty_backup_total, - a.query_start, + a.query_start::timestamp(0) as query_start, e.duration, round(e.progress_percent, 4) as progress_percent, bytes_per_second, - (e2.estimated_duration || 'sec')::interval as estimated_duration, - a.query_start + (e2.estimated_duration || 'sec')::interval as estimated_query_end + (e2.estimated_duration || 'sec')::interval(0) as estimated_duration, + a.query_start + (e2.estimated_duration || 'sec')::interval(0) as estimated_query_end from pg_stat_progress_basebackup as b inner join pg_stat_activity as a on a.pid = b.pid cross join lateral ( select - NOW() - a.query_start as duration, + (NOW() - a.query_start)::interval(0) as duration, b.backup_streamed * 100.0 / b.backup_total as progress_percent ) as e cross join lateral ( diff --git a/LINKS.md b/LINKS.md index 3cd4bd43..6c0e77db 100644 --- a/LINKS.md +++ b/LINKS.md @@ -42,6 +42,7 @@ 1. https://github.com/supabase/pg_jsonschema - PostgreSQL extension providing JSON Schema validation 1. https://github.com/tembo-io/pgmq - Postgres Message Queue (PGMQ) 1. https://github.com/benbjohnson/litestream - standalone disaster recovery tool for SQLite + 1. https://github.com/ossc-db/pg_store_plans - Store execution plans like pg_stat_statements does for queries # StackOverflow 1. https://stackoverflow.com/questions/7923237/return-pre-update-column-values-using-sql-only diff --git a/README.md b/README.md index d00024e6..c91b9820 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ **[Модификация пользовательских данных (DML)](#модификация-пользовательских-данных-dml)** 1. [Как добавить или обновить записи одним запросом (UPSERT)?](#как-добавить-или-обновить-записи-одним-запросом-upsert) 1. [Как сделать `INSERT ... ON CONFLICT ...` без увеличения последовательности для дубликатов?](#как-сделать-insert--on-conflict--без-увеличения-последовательности-для-дубликатов) + 1. [Как ускорить добавление строк через `INSERT ... VALUES ...`?](как-ускорить-добавление-строк-через-insert-values) 1. [Как модифицировать данные в нескольких таблицах и вернуть id затронутых записей в одном запросе?](#как-модифицировать-данные-в-нескольких-таблицах-и-вернуть-id-затронутых-записей-в-одном-запросе) 1. [Как модифицировать данные в связанных таблицах одним запросом?](#как-модифицировать-данные-в-связанных-таблицах-одним-запросом) 1. [Как добавить запись с id, значение которого нужно сохранить ещё в другом поле в том же INSERT запросе?](#как-добавить-запись-с-id-значение-которого-нужно-сохранить-ещё-в-другом-поле-в-том-же-insert-запросе) @@ -167,7 +168,7 @@ WHERE email IS NOT NULL -- skip NULL ##### ИНН Домены: [`inn.sql`](domains/inn.sql), [`inn10.sql`](domains/inn10.sql), [`inn12.sql`](domains/inn12.sql). -Функции: [`is_inn.sql`](functions/is/is_inn.sql), [`is_inn10.sql`](functions/is/is_inn10.sql), [`is_inn12.sql`](functions/is/is_inn12.sql). +Функции: [`is_inn10.sql`](functions/is/is_inn10.sql), [`is_inn12.sql`](functions/is/is_inn12.sql). ##### КПП Домен: [`kpp.sql`](domains/kpp.sql). @@ -1177,6 +1178,18 @@ select max(x), count(x) from t ``` +[Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_sequence) numbers with recursive in PostgreSQL +```sql +with recursive r(a, b) as ( + select 0::int, 1::int + union all + select b, a + b + from r + where b < 1000 +) +select a from r; +``` + ## Модификация пользовательских данных (DML) ### Как добавить или обновить записи одним запросом (UPSERT)? @@ -1230,6 +1243,36 @@ returning id; table t1_id_seq; -- "last_value" is 3 ``` +### Как ускорить добавление строк через `INSERT ... VALUES ...`? + +Вместо запроса типа + +```sql +INSERT INTO t1 (col1, col2, col3) +VALUES + (1,ドル 2,ドル 3ドル), + (4,ドル 5,ドル 6ドル), + ..., + (2998,ドル 2999,ドル 3000ドル); +``` + +используйте запрос +```sql +INSERT INTO t1 (col1, col2, col3) + SELECT * + FROM unnest( + 1ドル::timestamptz[], + 2ドル::text[], + 3ドル::float8[] +) +``` + +Трюк в том, что на `INSERT VALUES` тратится много времени на планирование запроса (обрабатывается каждое значение), а на `INSERT UNNEST` нет. + +Детальная информация: +* https://www.timescale.com/blog/boosting-postgres-insert-performance +* https://www.timescale.com/blog/benchmarking-postgresql-batch-ingest + ### Как модифицировать данные в нескольких таблицах и вернуть id затронутых записей в одном запросе? ```sql diff --git a/TEMP.md b/TEMP.md deleted file mode 100644 index 2248b30d..00000000 --- a/TEMP.md +++ /dev/null @@ -1 +0,0 @@ -`test ! -f "$FILE_SRC" && echo "Error: file '$FILE_SRC' does not exist!">&2 && exit 1` diff --git a/bashrc/README.md b/bashrc/README.md index c7853f07..3debf7de 100644 --- a/bashrc/README.md +++ b/bashrc/README.md @@ -9,6 +9,7 @@ 1. дата и время с часовой зоной 1. пользователь (`root` отображается красным цветом) 1. хост +1. IP адреса 1. путь и каталог Часть приглашения на второй строке: @@ -24,6 +25,9 @@ ```bash # Last version and documentation: https://github.com/rin-nas/postgresql-patterns-library/tree/master/bashrc +# https://patroni.readthedocs.io/ +alias patronictl='patronictl -c /etc/patroni/patroni.yml' + export EDITOR=nano export HISTFILESIZE=5000 export HISTCONTROL="ignoredups" @@ -65,7 +69,7 @@ __prompt_command() { PS1+="${Cyan}@" PS1+="${Orange}\h" #host - PS1+="${Green}[$(hostname -I | xargs)] " # IP list + PS1+="${Green}[$(hostname -I | xargs)] " # IP list (useful for showing Virtual IP) PS1+="${Blue}\w" #directory PS1+="\n" diff --git a/db_copy/README.md b/db_copy/README.md index b0b01b5b..4c134f0b 100644 --- a/db_copy/README.md +++ b/db_copy/README.md @@ -15,3 +15,7 @@ nano ~/db_restore.sh && chmod +x ~/db_restore.sh ``` Файл [`db_restore.sh`](db_restore.sh) + +## Ссылки по теме + +* https://github.com/dimitri/pgcopydb diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 00000000..348df101 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,3 @@ +# In development + +В этой папке находятся файлы в разработке, идеи, черновые наброски и т.д. diff --git a/TODO.md b/dev/TODO.md similarity index 99% rename from TODO.md rename to dev/TODO.md index 871fea0d..6b060237 100644 --- a/TODO.md +++ b/dev/TODO.md @@ -733,7 +733,9 @@ FROM WHERE tbl.ctid = lc.ctid; -- поиск по физической позиции записи ``` -https://habr.com/ru/companies/tensor/articles/567514/ +https://habr.com/ru/companies/tensor/articles/567514/ - Борем deadlock при пакетных UPDATE +https://dba.stackexchange.com/questions/323040/avoiding-deadlocks-when-locking-multiple-rows-without-using-nowait +https://stackoverflow.com/questions/22775150/how-to-simulate-deadlock-in-postgresql # Finding skewed data in Postgres diff --git a/dev/role_model.sql b/dev/role_model.sql new file mode 100644 index 00000000..e71e22b3 --- /dev/null +++ b/dev/role_model.sql @@ -0,0 +1,89 @@ +-- группы для членства ТУЗ и ПУЗ +CREATE ROLE group_personal; COMMENT ON ROLE group_personal IS 'Группа для персональных УЗ (сотрудников)'; +CREATE ROLE group_patroni; COMMENT ON ROLE group_patroni IS 'Группа для технических УЗ Patroni'; +CREATE ROLE group_application; COMMENT ON ROLE group_application IS 'Группа для технических УЗ приложения'; + +-- группы для наделения их привилегиями +CREATE ROLE group_read; -- SELECT +CREATE ROLE group_write; -- DML (INSERT, UPDATE, DELETE, TRUNCATE, MERGE), TCL (COMMIT, ROLLBACK, SAVEPOINT) +CREATE ROLE group_read_write; -- group_read + group_write +CREATE ROLE group_deploy; -- DDL (CREATE, ALTER, DROP) + group_read_write +CREATE ROLE group_permission; -- DCL (REVOKE, GRANT) +CREATE ROLE group_admin; -- администрирование СУБД без SELECT, DML, TCL, DCL +CREATE ROLE group_audit; -- чтение всех настроек СУБД + +-- группы с наследованием привилегий +GRANT group_read, group_write TO group_read_write /*WITH SET FALSE*/; +GRANT group_read_write TO group_deploy /*WITH SET FALSE*/; + +--GRANT pg_read_all_data TO group_read; +--GRANT pg_write_all_data TO group_write; + +-- пользователи ТУЗ +CREATE USER app_read; +CREATE USER app_write; +CREATE USER app_read_write; +CREATE USER app_deploy; + +-- пользователи ПУЗ +CREATE USER dba_rhmukhtarov WITH SUPERUSER; +COMMENT ON ROLE dba_rhmukhtarov IS 'DBA'; + +CREATE USER sup_petrov; +COMMENT ON ROLE sup_petrov IS 'Прикладное сопровождение АС (support)'; + +-- объединяем ТУЗ приложения в группу +GRANT group_application TO app_read; +GRANT group_application TO app_write; +GRANT group_application TO app_read_write; +GRANT group_application TO app_deploy; + +-- объединяем ПУЗ в группу +GRANT group_personal TO dba_rhmukhtarov; +GRANT group_personal TO sup_petrov; + +-- раздаём привилегии ТУЗ +GRANT group_read TO app_read; +GRANT group_write TO app_write; +GRANT group_read_write TO app_read_write; +GRANT group_deploy TO app_deploy; +alter +--ALTER USER dba_rhmukhtarov SET statement_timeout = '6h'; +--ALTER USER dba_rhmukhtarov SET log_min_duration_statement = 0; +--ALTER USER dba_rhmukhtarov set log_duration = 1; + +CREATE DATABASE app_db WITH OWNER app_deploy; + +\connect app_db + +-- посмотреть список привилегий для текущей БД +--select * FROM information_schema.table_privileges + +-- отнимаем привилегии у роли PUBLIC для уже созданных объектов в текущей БД +REVOKE ALL /*CREATE, CONNECT, TEMPORARY*/ ON DATABASE app_db FROM PUBLIC; +REVOKE ALL /*CREATE, USAGE*/ ON SCHEMA public FROM PUBLIC; +REVOKE ALL /*CREATE, USAGE*/ ON SCHEMA pg_catalog FROM PUBLIC; +REVOKE ALL /*CREATE, USAGE*/ ON SCHEMA information_schema FROM PUBLIC; + +-- отнимаем привилегии у роли PUBLIC для будущих создаваемых объектов в текущей БД +ALTER DEFAULT PRIVILEGES REVOKE ALL ON tables FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE ALL ON sequences FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE ALL ON routines FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE ALL ON types FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE ALL ON schemas FROM PUBLIC; + +GRANT CONNECT ON DATABASE app_db TO group_application; --не работает для группы + +/* + Для смены пароля нельзя использовать команду ALTER USER user_name WITH PASSWORD 'new_password'; + Т.к. пароль сохранится в журнале на сервере СУБД. Правильные клиенты передают хеш пароля. + Поменять пароль можно в любом GUI клиенте, например в DBeaver. + Для psql подключитесь к СУБД, затем введите команду \password +*/ +\password app_read; +\password app_write; +\password app_read_write; +\password app_deploy; +\password dba_rhmukhtarov; +\password sup_petrov; + diff --git a/experiments/delta_encode.md b/experiments/delta_encode.md index dacefaf0..72825c7e 100644 --- a/experiments/delta_encode.md +++ b/experiments/delta_encode.md @@ -1,5 +1,8 @@ # Эксперимент по дельта-кодированию числовых массивов в PostgreSQL +> [!NOTE] +> Актуальность — ноябрь 2023. + ```sql drop table if exists test.delta; @@ -84,4 +87,4 @@ from test.delta; | jsonb\_int\_array | 2130 | 2044 | 5490 | 4496 | Отсортированный список чисел (например список идентификаторов) с дельта + Фибоначчи кодированием -и хранением в `bytea` занимает почти 2 раза меньше места, чем в обычном массиве `int[]`. \ No newline at end of file +и хранением в `bytea` занимает почти 2 раза меньше места, чем в обычном массиве `int[]`. diff --git a/experiments/pg_dump_restore.md b/experiments/pg_dump_restore.md index c0ff88f8..095b3720 100644 --- a/experiments/pg_dump_restore.md +++ b/experiments/pg_dump_restore.md @@ -1,8 +1,17 @@ -# Эксперимент по созданию и восстановлению дампов БД в разных форматах с разным сжатием +# Эксперимент по созданию дампов БД и воссозданию БД из дампов + +> [!NOTE] +> Актуальность — ноябрь 2023. ## Введение -Здесь рассматривается логическая, а не физическая резервная копия БД +Рассматривается логическая, а не физическая резервная копия (бекап) БД. + +Логическое бекапирование +* использует транзакцию с уровнем изоляции Repeatable Read и удерживает снимок СУБД (snapshot) +* выгружает из СУБД схему и данные в виде SQL команд в текстовые файлы (дамп) + +Оцениваются разные форматы дампов, разные архиваторы, степени сжатия, однопоточное и многопоточное сжатие. ## Размер БД diff --git a/functions/bit/links.md b/functions/bit/LINKS.md similarity index 56% rename from functions/bit/links.md rename to functions/bit/LINKS.md index e8f2b300..2c258fe5 100644 --- a/functions/bit/links.md +++ b/functions/bit/LINKS.md @@ -1,2 +1,2 @@ * http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit -* https://github.com/sean-/postgresql-varint \ No newline at end of file +* https://github.com/sean-/postgresql-varint diff --git a/functions/fibonacci/links.md b/functions/fibonacci/LINKS.md similarity index 100% rename from functions/fibonacci/links.md rename to functions/fibonacci/LINKS.md diff --git a/functions/json/LINKS.md b/functions/json/LINKS.md new file mode 100644 index 00000000..4eedb8fe --- /dev/null +++ b/functions/json/LINKS.md @@ -0,0 +1 @@ +* [Интересные случаи использования JSON (Иван Панченко, Postgres Professional)](https://pgconf.ru/media/2020/02/04/%D0%98%D0%BD%D1%82%D0%B5%D1%80%D0%B5%D1%81%D0%BD%D1%8B%D0%B5%20%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B8%20JSON.pdf) diff --git a/functions/unicode_unescape.sql b/functions/unicode_unescape.sql index 620da843..1fee57d0 100644 --- a/functions/unicode_unescape.sql +++ b/functions/unicode_unescape.sql @@ -7,6 +7,8 @@ create or replace function public.unicode_unescape(text) set search_path = '' as $func$ + -- input string - only as \uXXXX sequence + -- TODO validate format by regexp and return NULL for invalid strings? select concat('"', 1,ドル '"')::jsonb->>0; $func$; diff --git a/pg_archive_log/README.md b/pg_archive_log/README.md index e9a3f547..51ad5e46 100644 --- a/pg_archive_log/README.md +++ b/pg_archive_log/README.md @@ -1,13 +1,20 @@ -# Инсталляция сервиса архивирования log файлов PostgreSQL +# 🍁 Инсталляция сервиса архивирования log файлов PostgreSQL ## Описание -Systemd сервис, который запускается 1 раз в сутки. +[Systemd](https://en.wikipedia.org/wiki/Systemd) сервис, который запускается 1 раз в сутки: 1. удаляет файлы старше N дней -2. удаляет файлы нулевого размера старше K дней -3. архивирует несжатые файлы старше М дней в формат `zstd`, если размер файла> S килобайт +1. удаляет файлы нулевого размера старше K дней +1. архивирует несжатые файлы старше М дней + +Команда для архивации (сжатия) файлов выполняется в один поток с самым низким приоритетом (для минимизации рисков влияния на работающую СУБД). + +## Требования + +Место на диске обычно ограничено и не бесплатное. За длительный период хранения файлов их необходимо сжимать как можно сильнее, но не потребляя слишком много ресурсов (CPU, память). В данном случае степень сжатия важнее скорости сжатия и распаковки (можно подождать). ## Предусловия + ```ini log_destination = 'csvlog' #опционально log_directory = '/var/log/postgresql/16' #для надёжности, папку /var/log лучше сделать в отдельном разделе ФС с квотой свободного места @@ -17,6 +24,9 @@ log_filename = 'postgresql-%Y-%m-%d.log' ## Инсталляция и настройка ```bash +# устанавливаем архиваторы +sudo dnf -y install zstd xz bzip3 + # создаём файлы sudo nano /etc/systemd/system/pg_archive_log.timer && \ sudo nano /etc/systemd/system/pg_archive_log.service @@ -34,10 +44,55 @@ sudo systemctl start pg_archive_log sudo systemctl status pg_archive_log.timer && \ sudo systemctl status pg_archive_log -# получаем список активных таймеров, для pg_archive_log.timer д.б. указана дата-время следующего запуска! -systemctl list-timers +# получаем список активных таймеров, д.б. указана дата-время следующего запуска! +systemctl list-timers | grep -P 'NEXT|pg_archive_log' ``` **Файлы** 1. [`/etc/systemd/system/pg_archive_log.timer`](pg_archive_log.timer) 2. [`/etc/systemd/system/pg_archive_log.service`](pg_archive_log.service) + +## Тестирование сжатия и распаковки + +Для замеров длительности выполнения и потребления памяти использовалась команда `/usr/bin/time -v COMMAND`.\ +Сжатие и распаковка в один поток. Распаковка в `/dev/null`. + +Хост `db-te12`, тестовый файл: `postgresql-2025年09月14日.csv` **186,311,281 байт**. + +| Program and compression level | Compression size (bytes) | Compression size (%) | Compression duration (s) | Compression memory (KB) | Decompression duration (s) | Decompression memory (KB) | Rating place | +| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| `gzip -9` | $\color{#f00}{3,453,449}$ | $\color{#f00}{144\\%}$ | $\color{#090}{1.24}$ | $\color{#090}{2,416}$ | $\color{#090}{0.46}$ | $\color{#090}{2,408}$ | — | +| `zstd -9` | $\color{#f00}{2,381,965}$ | 100% | $\color{#090}{1.55}$ | $\color{#090}{41,580}$ | $\color{#090}{0.04}$ | $\color{#090}{4,220}$ | 5 | +| `zstd -14` | $\color{#f00}{2,449,812}$ | $\color{#f00}{103\\%}$ | $\color{#090}{2.61}$ | 117,560 | $\color{#090}{0.04}$ | $\color{#090}{6,436}$ | — | +| `zstd -19` | 1,760,829 | 74% | $\color{#f00}{77.40}$ | $\color{#f00}{216,512}$ | $\color{#090}{0.08}$ | $\color{#090}{10,444}$ | — | +| `bzip2 -9` | 2,138,384 | 90% | $\color{#f00}{37.70}$ | $\color{#090}{7,944}$ | $\color{#f00}{3.34}$ | $\color{#090}{5,032}$ | — | +| `bzip3 -b8` | $\color{#090}{1,509,780}$ | $\color{#090}{63\\%}$ | $\color{#090}{2.76}$ | $\color{#090}{21,212}$ | 1.99 | $\color{#090}{52,428}$ | 1 | +| `bzip3 -b16` | $\color{#090}{1,471,514}$ | $\color{#090}{62\\%}$ | $\color{#090}{2.71}$ | $\color{#090}{39,424}$ | 1.91 | 101,636 | 2 | +| `bzip3 -b64` | $\color{#090}{1,412,929}$ | $\color{#090}{59\\%}$ | $\color{#090}{2.98}$ | $\color{#f00}{149,040}$ | 2.12 | $\color{#f00}{396,484}$ | — | +| `xz -2` | 2,085,424 | 85% | $\color{#090}{2.6}$ | $\color{#090}{17,684}$ | $\color{#090}{0.34}$ | $\color{#090}{4,720}$ | 3 | +| `xz -4` | 2,233,640 | 91% | $\color{#f00}{7.20}$ | $\color{#090}{46,392}$ | $\color{#090}{0.33}$ | $\color{#090}{6,584}$ | 4 | +| `xz -9` | 2,057,736 | 86% | $\color{#f00}{18.34}$ | $\color{#f00}{629,832}$ | $\color{#090}{0.38}$ | $\color{#090}{16,510}$ | — | + +Хост `db-pr33`, тестовый файл: `postgresql-2025年09月16日.csv` **12,670,480 байт**. + +| Program and compression level | Compression size (bytes) | Compression size (%) | Compression duration (s) | Compression memory (KB) | Decompression duration (s) | Decompression memory (KB) | Rating place | +| :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| `zstd -9` | 1,535,399 | 100% | $\color{#090}{0.31}$ | 22,576 | 0.01 | 4,600 | 5 | +| `zstd -14` | 1,486,106 | 98% | 1.02 | 65,632 | 0.02 | 6,656 | — | +| `zstd -19` | $\color{#090}{1,121,678}$ | 73% | $\color{#f00}{6.25}$ | 98,844 | 0.02 | 10,768 | — | +| `bzip3 -b8` | 1,421,130 | 93% | 1.00 | 35,944 | 0.94 | 52,200 | 1 | +| `bzip3 -b16` | 1,368,821 | 89% | 1.08 | 54,104 | 0.84 | 92,912 | 2 | +| `bzip3 -b32` | 1,368,821 | 89% | 1.07 | 53,340 | 0.85 | $\color{#f00}{158,436}$ | — | +| `xz -2` | 1,456,844 | 95% | 0.92 | 18,696 | 0.13 | 4,468 | 3 | +| `xz -3` | 1,438,932 | 94% | 1.66 | 34,164 | 0.11 | 6,544 | — | +| `xz -4` | $\color{#090}{1,096,736}$ | 71% | $\color{#f00}{2.75}$ | 50,624 | 0.13 | 6,556 | 4 | +| `xz -9` | $\color{#090}{924,236}$ | 60% | $\color{#f00}{4.24}$ | $\color{#f00}{163,292}$ | 0.11 | 14,776 | — | + +### Версии ПО + +| Program | Version | Для каких целей лучше всего подходит (PostgreSQL) | Почему лучше всего подходит | +| :--- | :--- | :--- | :--- | +| [zstd](https://github.com/facebook/zstd) | 1.4.4 | Сжатие и распаковка резервных копий и WAL файлов | Важна скорость сжатия, особенно на больших размерах СУБД. Ещё важнее скорость распаковки для минимизации RTO. | +| bzip2 | 1.0.6 | — | | +| [bzip3](https://github.com/iczelia/bzip3) | 1.3.1 | Сжатие и распаковка log файлов | Файлы за каждые сутки относительно небольшие, примерно одинакового размера. Степень сжатия немного важнее, чем скорость сжатия и распаковки. | +| xz, liblzma | 5.2.4 | — | | diff --git a/pg_archive_log/pg_archive_log.service b/pg_archive_log/pg_archive_log.service index d3a577bd..ca21aa36 100644 --- a/pg_archive_log/pg_archive_log.service +++ b/pg_archive_log/pg_archive_log.service @@ -9,17 +9,22 @@ Environment="LOG_DIR=/var/log/postgresql/16" # создаём папки, если их ещё не было ExecStartPre=mkdir -p ${LOG_DIR} -ExecStartPre=chmod 700 ${LOG_DIR} +ExecStartPre=chmod 750 ${LOG_DIR} -# удаляем файлы старше N дней -ExecStartPre=find ${LOG_DIR} -maxdepth 1 -type f -mtime +30 -delete - -# удаляем файлы нулевой длины старше K дней -ExecStartPre=find ${LOG_DIR} -maxdepth 1 -type f -mtime +2 -size 0 -delete +# удаляем файлы старше N дней (в промышленной среде поставьте 180 или 90, в тестовой 7) +ExecStartPre=find ${LOG_DIR} -maxdepth 1 -type f -mtime +90 -delete -# архивируем несжатые файлы старше M дней (достаточно одного потока zstd, т.к. файлы относительно небольшие) -# не ставьте большой уровень компрессии, это приводит к большому потреблению CPU, а экономия на размере файла несущественная -ExecStart=find ${LOG_DIR} -maxdepth 1 -type f -mtime +1 -size +100k ! -name "*.zst" -exec ionice -c2 -n7 nice -n19 zstd -9 -q --rm {} \; +# удаляем файлы нулевого размера старше K дней +ExecStartPre=find ${LOG_DIR} -maxdepth 1 -type f -mtime +1 -size 0 -delete + +# архивируем несжатые файлы старше M дней (достаточно одного потока, т.к. файлы относительно небольшие) +# выбирайте, что для вас больше подходит (zstd, xz, bzip3): +# ExecStart=find ${LOG_DIR} -maxdepth 1 -type f -mtime +1 ! -size 0 ! -name '*.zst' ! -name '*.xz' ! -name '*.bz3' -exec ionice -c2 -n7 -- nice -n19 -- zstd -9 -q --rm '{}' \; +# ExecStart=find ${LOG_DIR} -maxdepth 1 -type f -mtime +0 ! -size 0 ! -name '*.zst' ! -name '*.xz' ! -name '*.bz3' -exec ionice -c2 -n7 -- nice -n19 -- xz -2 '{}' \; +# bzip3 ≥ v1.5.0: +# ExecStart=find ${LOG_DIR} -maxdepth 1 -type f -mtime +1 ! -size 0 ! -name '*.zst' ! -name '*.xz' ! -name '*.bz3' -exec ionice -c2 -n7 -- nice -n19 -- bzip3 -b16 --rm '{}' \; +# bzip3 < v1.5.0: +ExecStart=find ${LOG_DIR} -maxdepth 1 -type f -mtime +1 ! -size 0 ! -name '*.zst' ! -name '*.xz' ! -name '*.bz3' -exec ionice -c2 -n7 -- nice -n19 -- bzip3 -b16 '{}' \; -exec rm -f '{}' \; [Install] WantedBy=multi-user.target diff --git a/pg_archive_log/pg_archive_log.timer b/pg_archive_log/pg_archive_log.timer index 0749cafc..ee900e64 100644 --- a/pg_archive_log/pg_archive_log.timer +++ b/pg_archive_log/pg_archive_log.timer @@ -3,7 +3,8 @@ Description=PostgreSQL archive log timer [Timer] Unit=pg_archive_log.service -OnCalendar=*-*-* 05:15:00 +OnCalendar=*-*-* 06:00:00 +RandomizedDelaySec=3600 Persistent=true [Install] diff --git a/pg_backup/README.md b/pg_backup/README.md index 6055a926..549ed3dd 100644 --- a/pg_backup/README.md +++ b/pg_backup/README.md @@ -1,59 +1,161 @@ -# Инсталляция сервиса резервного копирования PostgreSQL +# 🌳 Инсталляция сервиса резервного копирования PostgreSQL -## Как это работает? +## Функциональность +1. Создание полной резервной копии СУБД +1. Удаление старых резервных копий и WAL файлов из архива +1. Валидация корректности и восстанавливаемости резервной копии СУБД +1. Проверка необходимости запуска команд (п. 1-3) с текущего сервера кластера СУБД +1. Восстановление резервной копии СУБД -На каждом сервере СУБД по расписанию (обычно 1 раз в сутки) запускается сервис для создания резервных копий СУБД. +## Требования +* ОС: GNU/Linux (протестировано на RHEL 8.10) +* PostgreSQL ≥ v12: psql, pg_basebackup, pg_verifybackup, pg_checksums, pg_ctl, pg_amcheck (опционально) +* Шифрование/дешифрование (опционально): gpg +* Сжатие: zstd, pigz; распаковка: zstd, pigz, lz4 +* Прочее: bash ≥ v4.4, pv; опционально: patronictl, jq -Резервные копии создаются только с мастер СУБД, а реплики игнорируются. +## Как это работает? -Если рез. копии создавать с одной из реплик, то есть риск значительного отставания (часы и дни). Это можно упустить (человеческий фактор) или не сразу возможно ликвидировать отставание и тогда будет создана неактуальная резервная копия! +На каждом сервере СУБД по расписанию запускаются [systemd](https://en.wikipedia.org/wiki/Systemd) сервисы: +1. Создание резервной копии СУБД (обычно 1 раз в сутки) +1. Валидация резервной копий СУБД (обычно 1 раз в неделю) -i Резервная копия сжимается в формат `zstd` (16–25% от исходного размера файлов СУБД). Это позволяет экономить место на сетевом диске и уменьшить нагрузку на ввод-вывод. +Условие запуска сервисов: +* Если [Patroni](https://patroni.readthedocs.io/en/latest/) или [jq](https://jqlang.org/) не инсталлирован, то только с сервера СУБД мастер. +* Иначе только с одного сервера СУБД в каждом ЦОДе. Приоритет выбора сервера: синхронная реплика, мастер, асинхронная реплика (с отставанием не более 1000 МБ). -⚠ Внимание! -WAL файлы в резервную копию не копируются. -Для возможности восстановления СУБД из резервной копии должно быть настроено [непрерывное архивирование WAL файлов](https://postgrespro.ru/docs/postgresql/16/continuous-archiving) -через [archive_command](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND) -или [pg_receivewal](https://postgrespro.ru/docs/postgresql/16/app-pgreceivewal). +> [!NOTE] +> Резервная копия сжимается (10–30% от исходного размера файлов СУБД) и шифруется. Это позволяет экономить место на сетевом диске, уменьшить нагрузку на ввод-вывод, увеличить безопасность. -## Настройка создания резервных копий СУБД +> [!CAUTION] +> Внимание! +> 1. Наличие WAL файлов в резервной копии зависит от текущего дня (по умолчанию каждый 5-й день), настройки [`archive_mode`](https://postgrespro.ru/docs/postgresql/17/runtime-config-wal#GUC-ARCHIVE-MODE) и текущей роли сервера (мастер, реплика). +> 1. Для возможности восстановления СУБД из резервной копии, созданной без WAL файлов, должно быть настроено [непрерывное архивирование WAL файлов](https://postgrespro.ru/docs/postgresql/16/continuous-archiving) +через [`archive_command`](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND) +или [`pg_receivewal`](https://postgrespro.ru/docs/postgresql/16/app-pgreceivewal). +> 1. Следует учесть [ограничения создания резервной копии с реплики](https://postgrespro.ru/docs/postgresql/16/app-pgbasebackup)! -**Инсталляция сервиса** -```bash -# создаём файлы -sudo su - postgres -c "nano ~/.pgpass && chmod 600 ~/.pgpass" # в файле нужно сохранить пароль для пользователя bkp_replicator -sudo su - postgres -c "nano ~/pg_backup.sh && chmod 700 ~/pg_backup.sh && bash -n ~/pg_backup.sh" -sudo su - postgres -c "nano ~/pg_backup.conf && chmod 600 ~/pg_backup.conf && bash -n ~/pg_backup.conf" +Валидация — это выполнение команд с самым низким приоритетом (для минимизации рисков влияния на работающую СУБД) и только на реплике (при её наличии): +1. дешифрование (опционально) и распаковка архива с бекапом во временную папку +1. проверка файлов СУБД через pg_verifybackup +1. запуск СУБД (на отдельном порту) +1. проверка СУБД через pg_amcheck (опционально) +1. остановка СУБД +1. проверка СУБД через pg_checksums +1. сохранение артефактов рядом с бекапом и удаление временной папки -sudo nano /etc/systemd/system/pg_backup.service && \ -sudo nano /etc/systemd/system/pg_backup.timer +## Инсталляция -# активируем и добавляем в автозагрузку -sudo systemctl daemon-reload && \ -sudo systemctl enable pg_backup.timer && \ -sudo systemctl enable pg_backup +**Шаг 1. Выполнить на терминальном сервере Windows (PowerShell)** +```powershell +# создаём папку и переходим в неё +$path = "$home\pg_install"; mkdir -force $path; cd $path + +# создаём файлы +# ВНИМАНИЕ! кодировка файлов должна быть UTF8 без BOM, переносы строк в формате Unix (LF) +C:\"Program Files"\Notepad++\notepad++.exe ` + pg_backup.sh pg_backup.conf ` + pg_backup.timer pg_backup.service ` + pg_backup_validate.timer pg_backup_validate.service ` + archive_command.sh restore_command.sh + +# замените xx на код АС (или ФП?), yy на код среды (ps, nt, if, te), NN на порядковый номер +$db_hosts='sp-xx-db-yyNN', 'sp-xx-db-yyNN', 'sc-xx-db-yyNN', 'sc-xx-db-yyNN' + +# копируем локальную папку с файлами на удалённые серверы СУБД в домашнюю папку (Windows -> Linux) +foreach ($db_host in $db_hosts) { + Write-Host "`n$db_host" -ForegroundColor white -BackgroundColor blue + pscp -r $path ${env:username}@${db_host}: +} +``` -# проверяем работоспособность (отладка) -# time sudo su - postgres -c "~/pg_backup.sh" # сделает резервную копию СУБД, выведет сообщения на экран +**Шаг 2. Выполнить на каждом сервере СУБД Linux (Bash) - создаём файлы** +```bash +sudo -i + +AUTH_USER=$(who -m | cut -f1 -d' ') && \ +HOME_DIR=$(eval echo ~$AUTH_USER) && \ +cd ~postgres + +# создаём файлы (1) +nano -c .pgpass # в файле нужно сохранить пароль для пользователя bkp_replicator +(cp --update --backup $HOME_DIR/pg_install/pg_backup.sh . || nano -c pg_backup.sh) && \ +(cp --update --backup $HOME_DIR/pg_install/pg_backup.conf . || nano -c pg_backup.conf) && \ +(cp --update --backup $HOME_DIR/pg_install/archive_command.sh . || nano -c archive_command.sh) && \ +(cp --update --backup $HOME_DIR/pg_install/restore_command.sh . || nano -c restore_command.sh) +# выставляем нужные права и владельца +chmod 600 .pgpass pg_backup.conf && \ +chmod 700 {pg_backup,{archive,restore}_command}.sh && \ +chown postgres:postgres .pgpass {pg_backup,{archive,restore}_command}.sh pg_backup.conf + +# проверяем работоспособность (отладка), выводим сообщения на экран +sudo -i -u postgres -- ./pg_backup.sh ExecCondition # будем ли создавать или проверять резервную копию с текущего сервера СУБД (см. код возврата)? +sudo -i -u postgres -- ./pg_backup.sh create # создаст резервную копию текущего сервера СУБД +sudo -i -u postgres -- ./pg_backup.sh validate # проверит корректность и восстанавливаемость резервной копии СУБД +sudo -i -u postgres -- ./pg_backup.sh restore SOURCE_BACKUP_FILE_OR_DIR TARGET_PG_DATA_DIR # восстановит резервную копию СУБД + +# создаём файлы (2) +(cp --update --backup $HOME_DIR/pg_install/pg_backup.timer /etc/systemd/system || nano -c /etc/systemd/system/pg_backup.timer) && \ +(cp --update --backup $HOME_DIR/pg_install/pg_backup.service /etc/systemd/system || nano -c /etc/systemd/system/pg_backup.service) && \ +(cp --update --backup $HOME_DIR/pg_install/pg_backup_validate.timer /etc/systemd/system || nano -c /etc/systemd/system/pg_backup_validate.timer) && \ +(cp --update --backup $HOME_DIR/pg_install/pg_backup_validate.service /etc/systemd/system || nano -c /etc/systemd/system/pg_backup_validate.service) && \ +systemctl daemon-reload # активируем +``` -# запускаем -sudo systemctl start pg_backup.timer && \ -sudo systemctl start pg_backup # сделает резервную копию СУБД только на мастере, НЕ выведет сообщения на экран +**Шаг 3. Выполнить на каждом сервере СУБД Linux (Bash) - запускаем сервис создания бекапов** +```bash +# добавляем в автозагрузку +systemctl enable pg_backup.timer && \ +systemctl enable pg_backup.service + +# запускаем; сделает резервную копию СУБД, если условие ExecCondition выполнится +systemctl start pg_backup.timer && \ +systemctl start pg_backup.service + +# проверяем статус +systemctl status pg_backup.timer && \ +systemctl status pg_backup.service + +# получаем список активных таймеров, д.б. указана дата-время следующего запуска! +systemctl list-timers | grep -P 'NEXT|pg_backup' +``` +**Шаг 4. Выполнить на каждом сервере СУБД Linux (Bash) - запускаем сервис валидации бекапов** +```bash +# добавляем в автозагрузку +systemctl enable pg_backup_validate.timer && \ +systemctl enable pg_backup_validate.service + +# запускаем; проверит корректность и восстанавливаемость резервной копии СУБД, если условие ExecCondition выполнится +systemctl start pg_backup_validate.timer && \ +systemctl start pg_backup_validate.service + # проверяем статус -sudo systemctl status pg_backup.timer && \ -sudo systemctl status pg_backup +systemctl status pg_backup_validate.timer && \ +systemctl status pg_backup_validate.service -# получаем список активных таймеров, для pg_backup.timer д.б. указана дата-время следующего запуска! -systemctl list-timers +# получаем список активных таймеров, д.б. указана дата-время следующего запуска! +systemctl list-timers | grep -P 'NEXT|pg_backup' ``` Файлы -* [`/etc/systemd/system/pg_backup.service`](pg_backup.service) -* [`/etc/systemd/system/pg_backup.timer`](pg_backup.timer) -* [`/var/lib/pgsql/pg_backup.sh`](pg_backup.sh) -* [`/var/lib/pgsql/pg_backup.conf`](pg_backup.conf) +1. [`/etc/systemd/system/pg_backup.timer`](pg_backup.timer) +1. [`/etc/systemd/system/pg_backup.service`](pg_backup.service) +1. [`/etc/systemd/system/pg_backup_validate.timer`](pg_backup_validate.timer) +1. [`/etc/systemd/system/pg_backup_validate.service`](pg_backup_validate.service) +1. [`/var/lib/pgsql/pg_backup.sh`](pg_backup.sh) +1. [`/var/lib/pgsql/pg_backup_test.sh`](pg_backup_test.sh) +1. [`/var/lib/pgsql/pg_backup.conf`](pg_backup.conf) +1. [`/var/lib/pgsql/archive_command.sh`](archive_command.sh) +1. [`/var/lib/pgsql/restore_command.sh`](restore_command.sh) ## Ссылки по теме -* [PostgreSQL: архивирование WAL файлов (archive_command)](archive_command.md) -* [PostgreSQL: восстановление WAL файлов (restore_command)](restore_command.md) +* [Монтирование сетевой папки /mnt/backup_db (на примере)](mount_example.md) +* [PostgreSQL: копирование WAL файлов в архив (archive_command)](archive_command.md) +* [PostgreSQL: восстановление WAL файлов из архива (restore_command)](restore_command.md) +* Systemd + * https://systemd-by-example.com/ + * Google: [crontab+vs+systemd+timer](https://www.google.com/search?q=crontab+vs+systemd+timer) + * [Stop using cron! Systemd Timers Explained](https://coady.tech/systemd-timer-vs-cron/) +* https://www.dmosk.ru/miniinstruktions.php?mini=linux-cifs +* https://chmod-calculator.com/ diff --git a/pg_backup/TODO.md b/pg_backup/TODO.md new file mode 100644 index 00000000..344507d4 --- /dev/null +++ b/pg_backup/TODO.md @@ -0,0 +1,17 @@ +# TODO + +1. Сделать проверку pg_checksums опциональной (добавить настройку в конфигурационный файл?), потому что pg_basebackup при создании рез. копий уже проверяет контрольные суммы, если они включены. + В gpg в командной строке вместо пароля (--passphrase) использовать чтение из файла (--passphrase_file) pg_gpg_passphrase. + Вместо команды `ionice -c2 -n7 -- nice -n19 --` использовать соответствующие настройки Systemd? \ + https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html \ + https://unix.stackexchange.com/questions/788554/set-highest-cpu-and-io-priority-for-a-systemd-service (пример) +1. В функции валидации корректности восстанавливаемости СУБД из рез. копии после выполнения pg_controldata добавить проверку, что СУБД корректно завершила работу + ``` + pg_controldata | grep state + Database cluster state: shut down + ``` +1. Подумать над приоритетом выбора сервера, с которого делать бекап из-за этого комментария Димы Бородина: \ + https://habr.com/ru/articles/506610/#comment_21736832 \ + Вот есть у нас кластер, типа patroni. В нём есть primary, и какие-то реплики. + Сейчас у нас в питонячем скрипте реплики координируются через ЗК чтобы выбрать какой из узлов снимает бекап: в последнюю очередь с primary, но лучше с реплики. + Из реплик лучше выбирать не ту что в syncronous_standby_names. Среди прочих нужно выбрать реплику с максимальным LSN. Нетривиально, да? diff --git a/pg_backup/archive_command.md b/pg_backup/archive_command.md index 2a995bd6..bee4802e 100644 --- a/pg_backup/archive_command.md +++ b/pg_backup/archive_command.md @@ -1,10 +1,10 @@ -# PostgreSQL: архивирование WAL файлов (archive_command) +# PostgreSQL: копирование WAL файлов в архив (archive_command) ## Введение [Документация](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#RUNTIME-CONFIG-WAL-ARCHIVING) -i При архивировании WAL файлы сжимаются в формат `zstd` (52–62% от исходного размера, даже если включен параметр wal_compression). Это позволяет экономить место на сетевом диске и уменьшить нагрузку на ввод-вывод. +i При архивировании [WAL файлы](https://postgrespro.ru/docs/postgresql/16/continuous-archiving) сжимаются в формат `zstd` (52–62% от исходного размера, даже если включен параметр [wal_compression](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-WAL-COMPRESSION)). Это позволяет экономить место на сетевом диске и уменьшить нагрузку на ввод-вывод. ⚠ Удаление неактуальных WAL файлов сделано в сервисе резервного копирования, см. "[Инсталляция сервиса резервного копирования PostgreSQL](README.md)" @@ -15,13 +15,13 @@ # создайте файл archive_command.sh sudo mkdir -p /mnt/backup_db/ && sudo chown postgres:postgres /mnt/backup_db/ \ && sudo su - postgres -c "mkdir -p /mnt/backup_db/archive_wal/cluster/ && chmod 700 /mnt/backup_db/archive_wal/{,cluster/}" \ - && sudo su - postgres -c "nano ~/archive_command.sh && chmod 700 ~/archive_command.sh && bash -n ~/archive_command.sh" \ - && sudo su - postgres -c "nano \$PGDATA/postgresql.conf" - + && sudo su - postgres -c "nano -c ~/archive_command.sh && chmod 700 ~/archive_command.sh && bash -n ~/archive_command.sh" \ + && sudo su - postgres -c "nano -c \$PGDATA/postgresql.conf" + # pg_hba.conf and postgresql.conf syntax check test -z "$(psql --user=postgres --quiet --no-psqlrc --pset=null=¤ --tuples-only --no-align \ --command='select * from pg_hba_file_rules where error is not null; select * from pg_file_settings where error is not null')" - + sudo systemctl restart postgresql-16 sudo systemctl status postgresql-16 ``` diff --git a/pg_backup/archive_command.sh b/pg_backup/archive_command.sh index 9de1fb1f..4a293187 100644 --- a/pg_backup/archive_command.sh +++ b/pg_backup/archive_command.sh @@ -1,29 +1,78 @@ #!/bin/bash - -# заглушка, т.к. для archive_mode = 'on' требуется перезагрузка СУБД + +# заглушка, т.к. при изменении значения параметра archive_mode требуется перезагрузка СУБД # exit 0 - -# проверяем, скрипт должен запускаться с двумя параметрами + +function log() { + # echo $(date --rfc-3339=ns) $(hostname -s) $(basename "$SRC_FILE") "1ドル" &>> "$WAL_DIR/archive_wal.log" + return 0 +} + +WAL_DIR="/mnt/backup_db/archive_wal/cluster" +SRC_FILE="$(pwd)/2ドル" # откуда будем читать WAL файл +DST_FILE="$WAL_DIR/1ドル.zst" # куда будем сохранять WAL файл +LOCK_FILE="$WAL_DIR/1ドル.lock" # этот файл создаётся перед архиваций WAL файла и удаляется после + +log "check" + +# скрипт должен запускаться с двумя параметрами test "$#" -ne 2 && echo "Error: 2 number of parameters expected, $# given">&2 && exit 2 - -FILE_SRC="2ドル" -FILE_DST="/mnt/backup_db/archive_wal/cluster/1ドル.zst" - -test ! -f "$FILE_SRC" && echo "Error: file '$FILE_SRC' does not exist!">&2 && exit 1 -test -f "$FILE_DST" && exit - +test ! -f "$SRC_FILE" && echo "Error: WAL file '$SRC_FILE' does not exist!">&2 && exit 1 +test -f "$DST_FILE" && test ! -f "$LOCK_FILE" && log "exists" && exit 0 + +log "does not exist" + +: <<'comment' + Для надёжности архивирование WAL файлов могут настроить на мастере и репликах через параметр archive_mode=always. + Запрещаем от разных экземпляров СУБД конкурентную запись WAL файлов в общие сетевые папки. + Например, в основном и резервном ЦОДе могут быть разные сетевые папки. + Дополнительные файлы и проверки нужны для решения проблемы переполнения диска или недоступности сетевого соединения, + когда WAL файл может не записаться совсем или записаться только частично. +COMMENT + +SRV_FILE="$WAL_DIR/archive_server.$(hostname -s)" # сервер, на котором планируется архивирование WAL файла +touch -m "$SRV_FILE" || exit # создаём файл или обновляем дату модификации файла, если файл существует + +# разрешаем архивацию WAL файла только при наличии жёстких связанных ссылок (единый inode) между файлами $SRV_FILE и $LOCK_FILE +# для отладки и просмотра кол-ва жёстких ссылок на файл используйте утилиту stat (не используйте ls, она кеширует информацию) +if ln -T "$SRV_FILE" "$LOCK_FILE" &> /dev/null; then + # если удалось создать файл $LOCK_FILE, то только этот (основной) процесс будет архивировать WAL файл (первая попытка) + log "locked now" +elif test "$SRV_FILE" -ef "$LOCK_FILE"; then + # если файлы имеют одинаковый inode, то только этот (основной) процесс будет архивировать WAL файл (повторная попытка) + log "locked already" +else + # иначе архивировать WAL файл пытается конкурирующий процесс, ждём несколько секунд завершения работы основного процесса + for i in {1..75}; do + log "waiting i=$i" + sleep 0.2 + test -f "$DST_FILE" && test ! -f "$LOCK_FILE" && log "appeared" && exit 0 + done + # не дождались, значит основной процесс сломался, WAL файл мог сохраниться только частично + # передаём управление другому конкурирующему процессу, который станет основным (при повторном вызове этого скрипта) + rm -f "$DST_FILE" && rm -f "$LOCK_FILE" # очерёдность удаления файлов важна + MESSAGE="waiting timeout, deleted, unlocked" + log "$MESSAGE" + echo "Error: WAL file '$DST_FILE' $MESSAGE">&2 + exit 1 +fi + # кол-во потоков сжатия ZSTD_THREADS=$(echo "$(nproc) / 4 + 1" | bc) - -# кол-во файлов в очереди на архивирование -ARCHIVE_STATUS_DIR=$(dirname $FILE_SRC)/archive_status + +# подсчитываем кол-во WAL файлов в очереди на архивирование +ARCHIVE_STATUS_DIR=$(dirname "$SRC_FILE")/archive_status WAL_FILES_QUEUE=$(find "$ARCHIVE_STATUS_DIR" -maxdepth 1 -type f -name "*.ready" -printf "." | wc --bytes) - -# чем больше файлов в очереди, тем меньше степень сжатия (но больше скорость сжатия и размер сжатого файла) + STEP=3 -# не ставьте большой уровень компрессии, это приводит к большому потреблению CPU, а экономия на размере файла несущественная +# чем больше WAL файлов в очереди, тем меньше степень сжатия (но больше скорость сжатия и размер сжатого файла) +# не ставьте большой уровень компрессии, это приводит к большому потреблению CPU и памяти, а экономия на размере файла несущественная ZSTD_LEVEL=$(echo "(9 * ${STEP} - ${WAL_FILES_QUEUE}) / ${STEP}" | bc) test "$ZSTD_LEVEL" -lt 1 && ZSTD_LEVEL=1 - -# архивируем файл (без -B1M используются не все ядра из-за небольшого размера файла) -ionice -c2 -n7 nice -n19 zstd -q -f -${ZSTD_LEVEL} -T${ZSTD_THREADS} -B1M "$FILE_SRC" -o "$FILE_DST" + +# архивируем WAL файл +# в zstd без флага -B1M используются не все ядра (из-за небольшого размера файла?) +COMMAND="zstd -q -f -${ZSTD_LEVEL} -T${ZSTD_THREADS} -B1M $SRC_FILE -o $DST_FILE" +test $ZSTD_LEVEL -gt 1 && COMMAND="ionice -c2 -n7 -- nice -n19 -- $COMMAND" +log "command: $COMMAND" +$COMMAND && rm -f "$LOCK_FILE" && log "saved, unlocked" diff --git a/pg_backup/mount_example.md b/pg_backup/mount_example.md new file mode 100644 index 00000000..5f0fd501 --- /dev/null +++ b/pg_backup/mount_example.md @@ -0,0 +1,55 @@ +# Монтирование сетевой папки /mnt/backup_db (на примере) + +> [!NOTE] +> Сетевую папку обычно монтируют системные администраторы, а не DBA. + +```bash +# хотим на сервере srv2 сделать папку, как на сервере srv3 +root@srv3 ~ $ df -H +... +//srv-bkp/Backup_DB/EK 80T 24T 56T 31% /mnt/backup_db +... + +# копируем содержимое файла в буфер обмена 1 +root@srv3 ~ $ cat ~/.smbclient +username=srv_bpk_ek +password=*censored* +domain=some.com + +# копируем строку из файла в буфер обмена 2 +root@srv3 ~ $ cat /etc/fstab +... +# backup DB +//srv-bkp/Backup_DB/EK /mnt/backup_db cifs user,rw,credentials=/root/.smbclient,dir_mode=0750,file_mode=0640,uid=postgres,gid=postgres,nofail 0 0 +... + +#---------------------------------------------------------------------------------------------------------------------------- + +# инсталлируем +root@srv2 ~ $ dnf -y install cifs-utils + +# создаём папку +root@srv2 ~ $ mkdir -p /mnt/backup_db && chmod 770 /mnt/backup_db && chown postgres:postgres /mnt/backup_db + +# создаём файл из буфера обмена 1 +root@srv2 ~ $ nano -c ~/.smbclient && chmod 600 ~/.smbclient + +# добавляем строку из буфера обмена 2 +root@srv2 ~ $ nano -c /etc/fstab + +# автоматическое монтирование +root@srv2 ~ $ mount -a && systemctl daemon-reload + +# ручное монтирование без /etc/fstab (при необходимости) +# root@srv2 ~ $ mount.cifs //srv-bkp/Backup_DB/EK /mnt/backup_db -o user,rw,credentials=/root/.smbclient,dir_mode=0750,file_mode=0640,uid=postgres,gid=postgres,nofail + +# если будет ошибка, то смотрим системный журнал +root@srv2 ~ $ tail -n20 /var/log/messages + +# если нет доступов, проверяем доступность портов +# root@srv2 ~ $ nmap sp-bkp-bpr-pr03 -p 139 # DEPRECATED +root@srv2 ~ $ nmap sp-bkp-bpr-pr03 -p 445 + +# как отмонтировать, при необходимости +root@srv2 ~ $ umount /mnt/backup_db +``` diff --git a/pg_backup/pg_backup.conf b/pg_backup/pg_backup.conf index 78b19e30..89f8c972 100644 --- a/pg_backup/pg_backup.conf +++ b/pg_backup/pg_backup.conf @@ -1,17 +1,39 @@ # имя пользователя для подключения к СУБД PG_USERNAME="bkp_replicator" - -# номер основной версии СУБД -PG_MAJOR_VERSION=16 - + +# путь к исполняемым файлам утилит СУБД +PG_BIN_DIR="/usr/pgsql-16/bin" + +# https://postgrespro.ru/docs/postgresql/16/libpq-pgpass +PG_PASS_FILE="/var/lib/pgsql/.pgpass" + +# https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND +PG_ARCHIVE_COMMAND_FILE="/var/lib/pgsql/archive_command.sh" + +# https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-RESTORE-COMMAND +PG_RESTORE_COMMAND_FILE="/var/lib/pgsql/restore_command.sh" + # папка для хранения файлов с резервными копиями # для корректной работы системы резервного копирования внутри папок /mnt/backup_db/{active_full,archive_wal}/ должна быть папка с названием БД # но в PostgreSQL можно сделать физическую резервную копию только для всего кластера, поэтому папка называется cluster BACKUP_DIR="/mnt/backup_db/active_full/cluster" - + # папка для хранения WAL файлов WAL_DIR="/mnt/backup_db/archive_wal/cluster" - + +# как часто делать резервные копии с WAL файлами? +# текущий день с начала года должен делиться на BACKUP_WAL_DOY_DIVIDER без остатка (1 - ежедневно, 2 - каждый второй день и т.д.) +BACKUP_WAL_DOY_DIVIDER=5 + +# при валидации СУБД запускать программу pg_amcheck (1 - да, 0 - нет) +PG_AMCHECK_VALIDATE=1 + +# шифровать файлы (1 - да, 0 - нет) +GPG_ENCRYPT=1 + +# пароль для шифрования/дешифрования файлов резервных копий +GPG_PASSPHRASE="*censored*" + # сколько времени хранить резервные копии СУБД и WAL файлы (старые файлы будут автоматически удаляться) # для тестовой среды (ПСИ, НТ, ИФТ) установить в 2 BACKUP_AGE_DAYS=14 diff --git a/pg_backup/pg_backup.service b/pg_backup/pg_backup.service index 0cf7a8cc..130f815e 100644 --- a/pg_backup/pg_backup.service +++ b/pg_backup/pg_backup.service @@ -1,13 +1,12 @@ [Unit] Description=PostgreSQL backup service - + [Service] User=postgres Group=postgres - -ExecCondition=echo "pg_backup: check if PostgreSQL is primary" -ExecCondition=/bin/bash -c "test f = $(psql --user=bkp_replicator --no-password --dbname=postgres --quiet --no-psqlrc --pset=null=¤ --tuples-only --no-align --command='select pg_is_in_recovery()')" -ExecStart=/bin/bash /var/lib/pgsql/pg_backup.sh - + +ExecCondition=/bin/bash /var/lib/pgsql/pg_backup.sh ExecCondition +ExecStart=/bin/bash /var/lib/pgsql/pg_backup.sh create + [Install] WantedBy=multi-user.target diff --git a/pg_backup/pg_backup.sh b/pg_backup/pg_backup.sh index 7ef7cf26..1917bd09 100644 --- a/pg_backup/pg_backup.sh +++ b/pg_backup/pg_backup.sh @@ -1,20 +1,19 @@ #!/bin/bash # https://habr.com/ru/company/ruvds/blog/325522/ - Bash documentation - + # https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html # set -e - прекращает выполнение скрипта, если команда завершилась ошибкой # set -u - прекращает выполнение скрипта, если встретилась несуществующая переменная # set -x - выводит выполняемые команды в stdout перед выполнением (только для отладки, а то замусоривает журнал!) # set -o pipefail - прекращает выполнение скрипта, даже если одна из частей пайпа завершилась ошибкой set -euo pipefail - + SCRIPT_FILE=$(readlink -f "0ドル") SCRIPT_DIR=$(dirname "$SCRIPT_FILE") - -# Check syntax this file -bash -n "${SCRIPT_FILE}" || exit - -# Colors + +bash -n "$SCRIPT_FILE" || exit # check syntax this file + +# colors Red='\e[1;31m' Green='\e[0;32m' Yellow='\e[38;5;220m' @@ -25,16 +24,18 @@ Cyan='\e[0;36m' Gray='\e[0;37m' White='\e[1;37m' Reset='\e[0m' - -# Colored messages -echoerr() { echo -e "${Red}$@${Reset}" 1>&2; } -echowarn() { echo -e "${Yellow}$@${Reset}" 1>&2; } -echoinfo() { echo -e "${White}$@${Reset}" ; } -echosucc() { echo -e "${Green}$@${Reset}" ; } - + +# colored messages +echoerr() { echo -e "${Red}$@${Reset}" 1>&2; } # ошибки +echowarn() { echo -e "${Yellow}$@${Reset}" 1>&2; } # предупреждения +echohead() { echo -e "${Blue}$@${Reset}" ; } # заголовок или этап +echoinfo() { echo -e "${White}$@${Reset}" ; } # важные сообщения +echosucc() { echo -e "${Green}$@${Reset}" ; } # сообщения об успехе + +# функция подсчитывает длительность (day:hh:mm:ss) между временными метками в Unixtime elapsed() { - local time_start=1ドル #time_start=$(date +%s) - local time_end=2ドル #time_end=$(date +%s) + local time_start=1ドル # time_start=$(date +%s) + local time_end=2ドル # time_end=$(date +%s) local dt=$(echo "$time_end - $time_start" | bc) local dd=$(echo "$dt/86400" | bc) local dt2=$(echo "$dt-86400*$dd" | bc) @@ -42,66 +43,455 @@ elapsed() { local dt3=$(echo "$dt2-3600*$dh" | bc) local dm=$(echo "$dt3/60" | bc) local ds=$(echo "$dt3-60*$dm" | bc) - printf '%dd:%02d:%02d:%02d' $dd $dh $dm $ds #day:hh:mm:ss + printf '%dd:%02d:%02d:%02d' $dd $dh $dm $ds } - -# include -source "$SCRIPT_DIR/pg_backup.conf" - -# calculated variables -PG_BIN_DIR="/usr/pgsql-${PG_MAJOR_VERSION}/bin" -FILENAME=${BACKUP_DIR}/$(date +%Y-%m-%d.%H-%M-%S).$(hostname) - -if test $(whoami) != "postgres"; then - echoerr "PostgreSQL backup: run script as user postgres, not $(whoami)!" + +# для минимизации рисков влияния на работающую СУБД меняем приоритет этого процесса ($$ - это его pid) на минимальный +# (дочерние процессы наследуют значение приоритета родительского процесса) +ionice -c 2 -n 7 -p $$ +renice -n 19 -p $$ + +source "$SCRIPT_DIR/pg_backup.conf" # include +TIME_START=$(date +%s) # время в Unixtime + +# разные обязательные общие проверки при запуске скрипта +if test "$(whoami)" != "postgres"; then + echoerr "pg_backup: run script as user 'postgres', not '$(whoami)'" + exit 1 +elif ! grep -q -w "$PG_USERNAME" "$PG_PASS_FILE"; then + echoerr "pg_backup: file '$PG_PASS_FILE' must contain record for user '$PG_USERNAME'" + exit 1 +elif ! (echo "$PG_AMCHECK_VALIDATE" | grep -qoP '^[01]$'); then + echoerr "pg_backup: '$SCRIPT_DIR/pg_backup.conf': incorrect value of PG_AMCHECK_VALIDATE, expected 0 or 1" + exit 1 +elif ! (echo "$GPG_ENCRYPT" | grep -qoP '^[01]$'); then + echoerr "pg_backup: '$SCRIPT_DIR/pg_backup.conf': incorrect value of GPG_PASSPHRASE, expected 0 or 1" + exit 1 +elif test "$GPG_ENCRYPT" = 1 && test "$GPG_PASSPHRASE" = "*censored*"; then + echoerr "pg_backup: '$SCRIPT_DIR/pg_backup.conf': change default value of GPG_PASSPHRASE" + exit 1 +elif test ! -d "$BACKUP_DIR"; then + echoerr "pg_backup: directory '$BACKUP_DIR' does not exist" + exit 1 +elif test ! -d "$WAL_DIR"; then + echoerr "pg_backup: directory '$WAL_DIR' does not exist" + exit 1 +elif test ! -x "$PG_ARCHIVE_COMMAND_FILE"; then + echoerr "pg_backup: file '$PG_ARCHIVE_COMMAND_FILE' does not exist or user has not execute access" + exit 1 +elif test ! -x "$PG_RESTORE_COMMAND_FILE"; then + echoerr "pg_backup: file '$PG_RESTORE_COMMAND_FILE' does not exist or user has not execute access" + exit 1 +fi + +# вычисляем, с какого сервера СУБД будем создавать или проверять резервную копию (Systemd service ExecCondition) +if test "${1:-}" = "ExecCondition"; then + if ! (command -v patronictl &> /dev/null && command -v jq &> /dev/null); then + # test ! -f "$PGDATA/standby.signal" # deprecated + PG_ROLE=$(psql --username=$PG_USERNAME --no-password --dbname=postgres --quiet --no-psqlrc --pset=null=¤ --tuples-only --no-align \ + --command="select case when pg_is_in_recovery() then 'standby' else 'primary' end") + echo "pg_backup: candidate role is $PG_ROLE (checked by psql)" + test ${2:-primary} = "$PG_ROLE" + exit + fi + + echo 'pg_backup: check candidate role is "Sync Standby", "Leader", "Replica" (in order of priority)' + # https://jqlang.org/manual/ + # https://stackoverflow.com/questions/46070012/how-to-filter-an-array-of-json-objects-with-jq + # https://stackoverflow.com/questions/76476166/jq-sorting-by-value + # https://stackoverflow.com/questions/35540294/sort-descending-by-multiple-keys-in-jq + # https://stackoverflow.com/questions/1952404/linux-bash-multiple-variable-assignment + DC=$(hostname | grep -oP '^\w+') # текущий ЦОД + IFS=',' read -r MEMBER HOST ROLE <<< $(patronictl -c /etc/patroni/patroni.yml list --format=json \ + | jq -Mcr --arg DC $DC ' + map(select( + (.Member | startswith($DC + "-")) and + .State == ("streaming", "running") and + ."Lag in MB" < 1000 + )) + | sort_by(.Role != ("Sync Standby", "Leader", "Replica"), .Host) + | .[0] # LIMIT 1 + | [.Member, .Host, .Role] + | join(",") + ') + test -z "$HOST" && echoerr "pg_backup: no candidate found" && exit 1 + echo "pg_backup: perform will be from '$MEMBER' [$HOST] ($ROLE)" + test $(hostname) = "$MEMBER" + exit + +# восстанавливаем PostgreSQL из резервной копии +# на экране будет отображаться прогресс работы в процентах, скорость работы в мегабайтах/секунду, текущая и оставшаяся длительность работы +elif test "${1:-}" = "restore"; then + # скрипт должен запускаться с тремя параметрами + test "$#" -ne 3 && echoinfo "Usage: 0ドル restore SOURCE_BACKUP_FILE_OR_DIR TARGET_PG_DATA_DIR" && exit 2 + + BACKUP_FILE_OR_DIR="2ドル" + if test -f "$BACKUP_FILE_OR_DIR"; then + BACKUP_FILE="$BACKUP_FILE_OR_DIR" + elif test -d "$BACKUP_FILE_OR_DIR"; then + BACKUP_FILE=$(find $BACKUP_FILE_OR_DIR -maxdepth 1 -type f -name "base.tar.*" ! -name "*.log" -printf "%p") + test ! -f "$BACKUP_FILE" \ + && echoerr "pg_backup restore: source backup archive file '$BACKUP_FILE_OR_DIR/base.tar.*' does not exist" && exit 1 + else + echoerr "pg_backup restore: source backup archive file/directory '$BACKUP_FILE_OR_DIR' does not exist" exit 1 -elif ! grep -q -P "\b${PG_USERNAME}\b" /var/lib/pgsql/.pgpass; then - echoerr "File /var/lib/pgsql/.pgpass must contain rule for user '${PG_USERNAME}'" + fi + + PG_DATA_DIR="3ドル" + test ! -d "$PG_DATA_DIR" && echoerr "pg_backup restore: target directory '$PG_DATA_DIR' does not exist" && exit 1 + + # определяем архиватор по расширению файла + BACKUP_FILE_EXT=$(basename "$BACKUP_FILE" | grep -oP '\.\Ktar\..+$') + ARCHIVE_TYPE=$(echo "$BACKUP_FILE_EXT" | cut -d. -f2) + COMPRESS_PROGRAM=$(echo "zst:unzstd,lz4:unlz4,gz:unpigz" | grep -oP "\b${ARCHIVE_TYPE}:\K[^,]+") + if test -z "$ARCHIVE_TYPE" || test -z "$COMPRESS_PROGRAM"; then + echoerr "pg_backup validate: no compress program found" exit 1 + fi + + if test "$GPG_ENCRYPT" = 0; then + echo "Распаковываем архив '$BACKUP_FILE' в папку '$PG_DATA_DIR'" + GPG_COMMAND="cat" + else + echo "Расшифровываем и распаковываем архив '$BACKUP_FILE' в папку '$PG_DATA_DIR'" + GPG_COMMAND="gpg --decrypt --passphrase=$GPG_PASSPHRASE --batch" + fi + # посмотреть прогресс выполнения процесса pv: sudo pv -d PID + pv -trebp $BACKUP_FILE \ + | $GPG_COMMAND \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_DIR + + if test -d "$BACKUP_FILE_OR_DIR"; then + WAL_FILE="$BACKUP_FILE_OR_DIR/pg_wal.$BACKUP_FILE_EXT" + test ! -f "$WAL_FILE" && echoerr "Файл '$WAL_FILE' не найден" && exit 1 + if test "$GPG_ENCRYPT" = 0; then + echo "Распаковываем архив '$WAL_FILE' в папку '$PG_DATA_DIR/pg_wal'" + else + echo "Расшифровываем и распаковываем архив '$WAL_FILE' в папку '$PG_DATA_DIR/pg_wal'" + fi + pv -trebp $WAL_FILE \ + | $GPG_COMMAND \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_DIR/pg_wal + fi + + echo "Удаляем старые и ненужные файлы (информация об удалённых файлах будет выведена)" + # https://www.google.com/search?q=Linux+curly+brace+expansion+documentation + rm -f -r -v $PG_DATA_DIR/{*.{signal,{backup,old}{,.*}},log/*,*~} + + TIME_END=$(date +%s) # время в Unixtime + TIME_ELAPSED=$(elapsed $TIME_START $TIME_END) + echosucc "pg_backup restore: success, duration: $TIME_ELAPSED (day:hh:mm:ss)" + + cd $PG_DATA_DIR + read -p "Укажите роль создаваемого сервера (primary/standby): " PG_ROLE + if test "$PG_ROLE" = "primary"; then + touch recovery.signal && echo "Создан файл recovery.signal" + elif test "$PG_ROLE" = "standby"; then + touch standby.signal && echo "Создан файл standby.signal" + else + echoerr "Роль указана неверно, ожидается primary/standby" + exit 1 + fi + echowarn "Донастройте postgresql.conf и запустите кластер СУБД!" + echowarn "После старта СУБД дождитесь наката всех WAL файлов. Проверить завершение можно запросом select pg_is_in_recovery()" + exit 0 + +# проверяем корректность и восстанавливаемость PostgreSQL из резервной копии +elif test "${1:-}" = "validate"; then + echo "Получаем название предпоследнего или последнего файла с архивом резервной копии (сортировка по дате модификации)" + BACKUP_FILE=$(find $BACKUP_DIR -maxdepth 2 -type f \( -name "*.pg_backup.tar.*" -o -path "*.pg_backup/base.tar.*" \) \ + ! -name "*.log" -printf "%T@ %p\n" | sort -n | tail -2 | head -1 | cut -d" " -f2) + test -z "$BACKUP_FILE" && echoerr "pg_backup validate: no backup archive file found in directory '$BACKUP_DIR'" && exit 1 + echo "pg_backup validate: archive file '$BACKUP_FILE' selected" + + # определяем архиватор по расширению файла + BACKUP_FILE_EXT=$(basename "$BACKUP_FILE" | grep -oP '\.\Ktar\..+$') + ARCHIVE_TYPE=$(echo "$BACKUP_FILE_EXT" | cut -d. -f2) + COMPRESS_PROGRAM=$(echo "zst:unzstd,lz4:unlz4,gz:unpigz" | grep -oP "\b${ARCHIVE_TYPE}:\K[^,]+") + if test -z "$ARCHIVE_TYPE" || test -z "$COMPRESS_PROGRAM"; then + echoerr "pg_backup validate: no compress program found" + exit 1 + fi + + LOG_FILE_PREFIX=$(dirname $BACKUP_FILE)/$(basename $BACKUP_FILE .$BACKUP_FILE_EXT) + touch $LOG_FILE_PREFIX.validate-selected.log + + PG_DATA_TEST_DIR=$(dirname $(dirname $BACKUP_DIR))/pgdata_validate + echo "Создаём тестовую папку '$PG_DATA_TEST_DIR' для данных СУБД, удаляем старые данные (защита от предыдущего неудачного запуска скрипта)" + test -d "$PG_DATA_TEST_DIR" && rm -r $PG_DATA_TEST_DIR && echo "pg_backup validate: old temporary directory '$PG_DATA_TEST_DIR' deleted" + mkdir $PG_DATA_TEST_DIR + echo "pg_backup validate: temporary directory '$PG_DATA_TEST_DIR' created" + + echo "Проверяем, что у папки '$PG_DATA_TEST_DIR' права доступа 750 или 700, иначе PostgreSQL не запустится" + chmod 750 $PG_DATA_TEST_DIR + # chmod не гарантирует изменение прав доступа на SMB/CIFS + stat -c "%a" $PG_DATA_TEST_DIR | grep -qP '^7[05]0$' \ + || (echoerr "pg_backup validate: directory '$PG_DATA_TEST_DIR' permission must be 750 or 700" && exit 1) + + if test "$GPG_ENCRYPT" = 0; then + echo "Распаковываем архив '$BACKUP_FILE' в папку '$PG_DATA_TEST_DIR'" + # посмотреть прогресс выполнения процесса pv: sudo pv -d PID + pv $BACKUP_FILE \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_TEST_DIR 2> $LOG_FILE_PREFIX.tar.stderr.log + else + echo "Расшифровываем и распаковываем архив '$BACKUP_FILE' в папку '$PG_DATA_TEST_DIR'" + # посмотреть прогресс выполнения процесса pv: sudo pv -d PID + pv $BACKUP_FILE \ + | gpg --decrypt --passphrase=$GPG_PASSPHRASE --batch 2> $LOG_FILE_PREFIX.gpg.stderr.log \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_TEST_DIR 2> $LOG_FILE_PREFIX.tar.stderr.log + fi + + BACKUP_BASE_DIR=$(echo "$BACKUP_FILE" | grep -qP '\.pg_backup/base\.tar\.' && dirname "$BACKUP_FILE" || true) + if test ! -z "$BACKUP_BASE_DIR"; then + echo "Копируем '$BACKUP_BASE_DIR/backup_manifest' в папку '$PG_DATA_TEST_DIR'" + cp $BACKUP_BASE_DIR/backup_manifest $PG_DATA_TEST_DIR + + WAL_FILE="$BACKUP_BASE_DIR/pg_wal.$BACKUP_FILE_EXT" + test ! -f "$WAL_FILE" && echoerr "Файл '$WAL_FILE' не найден" && exit 1 + if test "$GPG_ENCRYPT" = 0; then + echo "Распаковываем архив '$WAL_FILE' в папку '$PG_DATA_TEST_DIR/pg_wal'" + pv $WAL_FILE \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_TEST_DIR/pg_wal \ + 2> $LOG_FILE_PREFIX.tar.stderr.log + else + echo "Расшифровываем и распаковываем архив '$WAL_FILE' в папку '$PG_DATA_TEST_DIR/pg_wal'" + pv $WAL_FILE \ + | gpg --decrypt --passphrase=$GPG_PASSPHRASE --batch \ + 2> $LOG_FILE_PREFIX.gpg.stderr.log \ + | tar -xf - --use-compress-program="$COMPRESS_PROGRAM" --directory=$PG_DATA_TEST_DIR/pg_wal \ + 2> $LOG_FILE_PREFIX.tar.stderr.log + fi + fi + + DIR_SIZE=$(du -sh "$PG_DATA_TEST_DIR" | grep -oP '^\S+') + echo "pg_backup validate: archive file extracted to directory '$PG_DATA_TEST_DIR' (total size: $DIR_SIZE)" + + echo "Проверяем целостность копии кластера СУБД, сделанной программой pg_basebackup, по манифесту backup_manifest" + $PG_BIN_DIR/pg_verifybackup --no-parse-wal --exit-on-error --quiet $PG_DATA_TEST_DIR \ + 1> $LOG_FILE_PREFIX.pg_verifybackup.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_verifybackup.stderr.log + echo "pg_backup validate: '$PG_DATA_TEST_DIR' backup verify OK" + + echo "Удаляем старые и ненужные файлы (информация об удалённых файлах будет выведена)" + # https://www.google.com/search?q=Linux+curly+brace+expansion+documentation + rm -f -r -v $PG_DATA_TEST_DIR/{*.{signal,{backup,old}{,.*}},log/*,*~} + + echo "Разрешаем локальному пользователю postgres аутентифицироваться методом peer" + sed -i '1i local all postgres peer' $PG_DATA_TEST_DIR/pg_hba.conf # добавляем строчку в начало файла + + echo "(Ре)стартуем сервер СУБД в роли мастер (рестарт - это защита от предыдущего неудачного запуска скрипта)" + touch $PG_DATA_TEST_DIR/recovery.signal + PG_PORT=55432 + $PG_BIN_DIR/pg_ctl restart --pgdata=$PG_DATA_TEST_DIR \ + --options="-p $PG_PORT -B 128MB --cluster_name=BACKUP_VALIDATE --archive_mode=off --log_directory=$PG_DATA_TEST_DIR/log" \ + --options="--hba_file=$PG_DATA_TEST_DIR/pg_hba.conf --ident-file=$PG_DATA_TEST_DIR/pg_ident.conf" \ + --options="--restore_command='$PG_RESTORE_COMMAND_FILE %f %p'" \ + 1> $LOG_FILE_PREFIX.pg_ctl.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_ctl.stderr.log + echoinfo "pg_backup validate: server started (port $PG_PORT)" + + # ВНИМАНИЕ! После старта тестовой СУБД завершать работу скрипта с ошибкой нельзя до остановки СУБД! + + echo "Ждём, пока накатятся WAL файлы" + while true; do + PG_IS_IN_RECOVERY=$(psql --port=$PG_PORT --username=postgres --no-password --dbname=postgres --quiet --no-psqlrc \ + --pset=null=¤ --tuples-only --no-align --command="select pg_is_in_recovery()") + test -z "$PG_IS_IN_RECOVERY" && echowarn "pg_backup validate: get in recovery status error" && break + test "$PG_IS_IN_RECOVERY" = "f" && echo && break + sleep 1 + # echo -n "." # debug only + done + + echo "Проверяем количество ошибок в контрольных суммах" + CHECKSUM_FAILURES=$(psql --port=$PG_PORT --username=postgres --no-password --dbname=postgres --quiet --no-psqlrc \ + --pset=null=¤ --tuples-only --no-align --command='select sum(checksum_failures) from pg_stat_database' \ + 2> $LOG_FILE_PREFIX.psql.stderr.log) || true + if test -z "$CHECKSUM_FAILURES"; then + echowarn "pg_backup validate: connection ERROR" + elif test "$CHECKSUM_FAILURES" = "¤"; then + echowarn "pg_backup validate: data checksums disabled" + elif test "$CHECKSUM_FAILURES" -gt 0; then + echowarn "pg_backup validate: data checksum failures: $CHECKSUM_FAILURES" + else + echo "pg_backup validate: data checksum failures: 0" + fi + + if test "$PG_AMCHECK_VALIDATE" = 0; then + echowarn "Проверка логической целостности таблиц и индексов (amcheck) отключена" + else + echo "Проверяем логическую целостность таблиц и индексов (amcheck)" + if $PG_BIN_DIR/pg_amcheck --port=$PG_PORT --username=postgres --no-password --database=* --rootdescend --on-error-stop \ + 1> $LOG_FILE_PREFIX.pg_amcheck.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_amcheck.stderr.log ; then + echo "pg_backup validate: amcheck OK" + else + echowarn "pg_backup validate: amcheck ERROR" + fi + fi + + echo "Останавливаем сервер СУБД" + $PG_BIN_DIR/pg_ctl stop --pgdata=$PG_DATA_TEST_DIR \ + 1> $LOG_FILE_PREFIX.pg_ctl.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_ctl.stderr.log + echoinfo "pg_backup validate: server stopped (port $PG_PORT)" + + BAD_WORDS_RE="\b(WARNING|ERROR|FATAL|PANIC|ignored?|(fail|(? $LOG_FILE_PREFIX.pg_checksums.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_checksums.stderr.log + echo "pg_backup validate: '$PG_DATA_TEST_DIR' checksums OK" + fi + + echo "Сохраняем управляющую информацию кластера СУБД" + $PG_BIN_DIR/pg_controldata --pgdata=$PG_DATA_TEST_DIR \ + 1> $LOG_FILE_PREFIX.pg_controldata.stdout.log \ + 2> $LOG_FILE_PREFIX.pg_controldata.stderr.log + + echo "Удаляем папку '$PG_DATA_TEST_DIR', она больше не нужна" + rm -r $PG_DATA_TEST_DIR + echo "pg_backup validate: temporary directory '$PG_DATA_TEST_DIR' deleted" + + TIME_END=$(date +%s) # время в Unixtime + TIME_ELAPSED=$(elapsed $TIME_START $TIME_END) + LOG_FILE=$LOG_FILE_PREFIX.validate-success.log + echo "Total size: $DIR_SIZE"> $LOG_FILE + echo "Validate duration: $TIME_ELAPSED (day:hh:mm:ss)">> $LOG_FILE + echosucc "pg_backup validate: success, duration: $TIME_ELAPSED (day:hh:mm:ss)" + exit 0 + +elif test "${1:-}" != "create"; then + echoinfo "Usage: 0ドル COMMAND" + echo "COMMAND - one of: create, validate, restore" + exit 2 +fi + +# ----------------------------------------------------------------------------------------------------------------------- +echoinfo "pg_backup: creating started" +BASE_NAME=${BACKUP_DIR}/$(date +%Y-%m-%d.%H%M%S).$(hostname).pg_backup +COMPRESS_THREADS=$(echo "$(nproc) / 2.5 + 1" | bc) + +# для многопоточного режима используется максимальная степень сжатия 5, которая была получена опытным путём +# это баланс между потреблением CPU и памяти, размером сжатого файла, скоростью записи на сетевой диск, с учётом нагрузки другими процессами +COMPRESS_LEVEL=$COMPRESS_THREADS +test "$COMPRESS_LEVEL" -gt 5 && COMPRESS_LEVEL=5 + +echo 'Проверяем необходимость бекапирования WAL файлов' +# зависит от текущего дня, настройки параметра archive_mode и роли СУБД primary/standby +IS_BACKUP_WAL=$(psql --username=$PG_USERNAME --no-password --dbname=postgres --quiet --no-psqlrc --pset=null=¤ --tuples-only --no-align \ + --command="select extract(doy from now())::int % ${BACKUP_WAL_DOY_DIVIDER} = 0 --cast to int for Postgres v12 + or setting = 'off' or (pg_is_in_recovery() and setting = 'on') + from pg_settings where name = 'archive_mode'") + +if test "$IS_BACKUP_WAL" = "f"; then + echo 'Создаём физическую резервную копию (без WAL файлов)' + COMMAND="${PG_BIN_DIR}/pg_basebackup --username=${PG_USERNAME} --no-password --wal-method=none --checkpoint=fast --format=tar --pgdata=-" + if test "$GPG_ENCRYPT" = 0; then + FILE="${BASE_NAME}.tar.zst" + ($COMMAND | zstd -q -T${COMPRESS_THREADS} -${COMPRESS_LEVEL} -o $FILE) 2> $BASE_NAME.stderr.log + else + FILE="${BASE_NAME}.tar.zst.gpg" + ($COMMAND | zstd -q -T${COMPRESS_THREADS} -${COMPRESS_LEVEL} \ + | gpg -c --passphrase=${GPG_PASSPHRASE} --batch --compress-algo=none -o $FILE) 2> $BASE_NAME.stderr.log + fi + SIZE=$(du -sh "$FILE" | grep -oP '^\S+') + echoinfo "Создан файл '$FILE' (size: $SIZE)" +else + echo 'Создаём физическую резервную копию (с WAL файлами)' + PG_MAJOR_VER=$(echo "$PG_BIN_DIR" | grep -oP '\-\K\d+(?=/)') + OPT_COMPRESS=1 # gzip support only + if test "$PG_MAJOR_VER" -ge 15; then + # в библиотеке libzstd многопоточность поддерживается с версии 1.5.0 + LIBZSTD_VER=$(rpm -q libzstd | grep -oP '^libzstd-\K\d+\.\d+') + test -z "$LIBZSTD_VER" && echoerr "pg_backup: cannot get libzstd version, it is installed?" && exit 1 + OPT_COMPRESS="server-zstd:level=1" + test $(echo "$LIBZSTD_VER>= 1.5" | bc -l) = 1 && OPT_COMPRESS="server-zstd:level=${COMPRESS_LEVEL},workers=${COMPRESS_THREADS}" + fi + mkdir -p ${BASE_NAME} + ${PG_BIN_DIR}/pg_basebackup --username=${PG_USERNAME} --no-password --compress=${OPT_COMPRESS} --checkpoint=fast --format=tar \ + --pgdata=${BASE_NAME} \ + 2> $BASE_NAME.stderr.log + + FILES="${BASE_NAME}/base.tar ${BASE_NAME}/pg_wal.tar" + for FILE in $FILES; do + if test -f "$FILE"; then + if test "$GPG_ENCRYPT" = 0; then + echo "Сжимаем '$FILE'" + if test "$PG_MAJOR_VER" -ge 15; then + zstd -c -q -T${COMPRESS_THREADS} -${COMPRESS_LEVEL} $FILE -o $FILE.zst 2> $BASE_NAME.stderr.log + else + pigz -c -q -p ${COMPRESS_THREADS} -${COMPRESS_LEVEL} -o $FILE.gz 2> $BASE_NAME.stderr.log + fi + else + echo "Сжимаем и шифруем '$FILE'" + if test "$PG_MAJOR_VER" -ge 15; then + (zstd -c -q -T${COMPRESS_THREADS} -${COMPRESS_LEVEL} $FILE \ + | gpg -c --passphrase=${GPG_PASSPHRASE} --batch --compress-algo=none -o $FILE.zst.gpg) 2> $BASE_NAME.stderr.log + else + (pigz -c -q -p ${COMPRESS_THREADS} -${COMPRESS_LEVEL} $FILE \ + | gpg -c --passphrase=${GPG_PASSPHRASE} --batch --compress-algo=none -o $FILE.gz.gpg) 2> $BASE_NAME.stderr.log + fi + fi + rm -f $FILE + elif test -f "$FILE.zst"; then + if test "$GPG_ENCRYPT" = 1; then + echo "Шифруем '$FILE.zst'" + gpg -c --passphrase=${GPG_PASSPHRASE} --batch --compress-algo=none $FILE.zst 2> $BASE_NAME.stderr.log + rm -f $FILE.zst + fi + elif test -f "$FILE.gz"; then + if test "$GPG_ENCRYPT" = 1; then + echo "Шифруем '$FILE.gz'" + gpg -c --passphrase=${GPG_PASSPHRASE} --batch --compress-algo=none $FILE.gz 2> $BASE_NAME.stderr.log + rm -f $FILE.gz + fi + else + echoerr "Файл '$FILE' или '$FILE.zst' или '$FILE.gz' не найден" && exit 1 + fi + done + SIZE=$(du -sh "$BASE_NAME" | grep -oP '^\S+') + echoinfo "Создана папка '$BASE_NAME' (total size: $SIZE)" fi - -echoinfo "PostgreSQL backup: creating started" -TIME_START=$(date +%s) #время в Unixtime - -# создаём директории, если их ещё нет -mkdir -p ${BACKUP_DIR} ${WAL_DIR} - -# создаём физический бэкап -# zstd adapt compression level depending on I/O conditions, mainly how fast it can write the output -# В zstd v1.4.4 плохо работает адаптивный режим с кол-вом потоков> 1 (--adapt -T), постепенно увеличивается степень сжатия и длительность работы. -# Для многопоточного режима лучше явно поставить степень сжатия. -ZSTD_THREADS=$(echo "$(nproc) / 2.5 + 1" | bc) -${PG_BIN_DIR}/pg_basebackup --username=${PG_USERNAME} --no-password --wal-method=none --checkpoint=fast --format=tar --pgdata=- \ - | ionice -c2 -n7 nice -n19 zstd -q -T${ZSTD_THREADS} -5 -o ${FILENAME}.pg_basebackup.tar.zst - -# создаём логический бэкап (deprecated) -# ${PG_BIN_DIR}/pg_dumpall --username=${PG_USERNAME} --no-password | zstd -q -T${ZSTD_THREADS} -5 -o ${FILENAME}.sql.zst - -TIME_END=$(date +%s) #время в Unixtime + +# создаём логическую резервную копию (deprecated) +# ${PG_BIN_DIR}/pg_dumpall --username=${PG_USERNAME} --no-password | zstd -q -T${COMPRESS_THREADS} -${COMPRESS_LEVEL} -o ${BASE_NAME}.sql.zst + +TIME_END=$(date +%s) # время в Unixtime TIME_ELAPSED=$(elapsed $TIME_START $TIME_END) - -echosucc "PostgreSQL backup: created successfully, duration: $TIME_ELAPSED (day:hh:mm:ss)" - + +echosucc "pg_backup: created, duration: $TIME_ELAPSED (day:hh:mm:ss)" + +# ----------------------------------------------------------------------------------------------------------------------- # удаляем архивные резервные копии старше N дней (папки и файлы рекурсивно) -echo "PostgreSQL backup: deleting backup files older than ${BACKUP_AGE_DAYS} days" +echo "pg_backup: deleting backup files older than ${BACKUP_AGE_DAYS} days" find ${BACKUP_DIR} -mindepth 1 -mtime +${BACKUP_AGE_DAYS} -delete - -# удаляем архивные WAL файлы старше N дней -echo "PostgreSQL backup: detect oldest kept WAL file for ${BACKUP_AGE_DAYS} days" -WAL_OLD_FILE=$(find ${WAL_DIR} -maxdepth 1 -mtime +${BACKUP_AGE_DAYS} -type f -printf "%C@ %f\n" | sort -n | tail -n 1 | cut -d" " -f2) +echosucc "pg_backup: old backup files deleted" + +# ----------------------------------------------------------------------------------------------------------------------- +# удаляем архивные WAL файлы старше N дней (сортировка по дате модификации) +echo "pg_backup: detect oldest kept WAL file for ${BACKUP_AGE_DAYS} days" +WAL_OLD_FILE=$(find ${WAL_DIR} -maxdepth 1 -mtime +${BACKUP_AGE_DAYS} -type f ! -size 0 \ + ! -name "*.history" ! -name "*.history.*" -printf "%T@ %f\n" \ + | sort -n | tail -1 | cut -d" " -f2) if test -z "${WAL_OLD_FILE}"; then - echo "PostgreSQL backup: WAL old file is not found" + echowarn "pg_backup: old WAL file is not found" else - echo "PostgreSQL backup: WAL old file is ${WAL_OLD_FILE}" - - WAL_DIR_SIZE=$(du -sh "${WAL_DIR}" | grep -oP "^\S+") - echo "PostgreSQL backup: Before cleanup WAL dir size: ${WAL_DIR_SIZE}" + echo "pg_backup: WAL old file is ${WAL_OLD_FILE}" + WAL_OLD_FILE_EXT=$(echo "${WAL_OLD_FILE}" | grep -oP '\.[^.]+$') # compressed files support (.gz, .zst, .lz4) - WAL_OLD_FILE_EXT=$(echo "${WAL_OLD_FILE}" | grep -oP "\.[a-z\d]+$") # compressed files support (.gz, .zst, .lz4) - ${PG_BIN_DIR}/pg_archivecleanup -x "${WAL_OLD_FILE_EXT}" "${WAL_DIR}" "${WAL_OLD_FILE}" + BEFORE_WAL_DIR_SIZE=$(du -sh "${WAL_DIR}" | grep -oP '^\S+') + ${PG_BIN_DIR}/pg_archivecleanup -x "${WAL_OLD_FILE_EXT}" "${WAL_DIR}" "${WAL_OLD_FILE}" 2> ${WAL_DIR}/pg_archivecleanup.stderr.log - WAL_DIR_SIZE=$(du -sh "${WAL_DIR}" | grep -oP "^\S+") - echo "PostgreSQL backup: After cleanup WAL dir size: ${WAL_DIR_SIZE}" + AFTER_WAL_DIR_SIZE=$(du -sh "${WAL_DIR}" | grep -oP '^\S+') + echo "pg_backup: WAL dir size reducing: ${BEFORE_WAL_DIR_SIZE} (before cleanup) -> ${AFTER_WAL_DIR_SIZE} (after cleanup)" + echosucc "pg_backup: old WAL files deleted" fi - -echosucc "PostgreSQL backup: done" + +exit 0 diff --git a/pg_backup/pg_backup.timer b/pg_backup/pg_backup.timer index 89df9188..a9ce9a02 100644 --- a/pg_backup/pg_backup.timer +++ b/pg_backup/pg_backup.timer @@ -1,10 +1,11 @@ [Unit] Description=PostgreSQL backup timer - + [Timer] Unit=pg_backup.service -OnCalendar=*-*-* 23:55:00 +OnCalendar=*-*-* 00:00:00 +RandomizedDelaySec=7200 Persistent=true - + [Install] WantedBy=timers.target diff --git a/pg_backup/pg_backup_test.sh b/pg_backup/pg_backup_test.sh new file mode 100644 index 00000000..24854eec --- /dev/null +++ b/pg_backup/pg_backup_test.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# https://habr.com/ru/company/ruvds/blog/325522/ - Bash documentation + +# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html +# set -e - прекращает выполнение скрипта, если команда завершилась ошибкой +# set -u - прекращает выполнение скрипта, если встретилась несуществующая переменная +# set -x - выводит выполняемые команды в stdout перед выполнением (только для отладки, а то замусоривает журнал!) +# set -o pipefail - прекращает выполнение скрипта, даже если одна из частей пайпа завершилась ошибкой +set -euo pipefail + +SCRIPT_FILE=$(readlink -f "0ドル") +SCRIPT_DIR=$(dirname "$SCRIPT_FILE") + +bash -n "$SCRIPT_FILE" || exit # check syntax this file + +# colors +Red='\e[1;31m' +Green='\e[0;32m' +Yellow='\e[38;5;220m' +Blue='\e[38;5;39m' +Orange='\e[38;5;214m' +Magenta='\e[0;35m' +Cyan='\e[0;36m' +Gray='\e[0;37m' +White='\e[1;37m' +Reset='\e[0m' + +# colored messages +echoerr() { echo -e "${Red}$@${Reset}" 1>&2; } # ошибки +echowarn() { echo -e "${Yellow}$@${Reset}" 1>&2; } # предупреждения +echohead() { echo -e "${Blue}$@${Reset}" ; } # заголовок или этап +echoinfo() { echo -e "${White}$@${Reset}" ; } # важные сообщения +echosucc() { echo -e "${Green}$@${Reset}" ; } # сообщения об успехе + +# функция подсчитывает длительность (day:hh:mm:ss) между временными метками в Unixtime +elapsed() { + local time_start=1ドル # time_start=$(date +%s) + local time_end=2ドル # time_end=$(date +%s) + local dt=$(echo "$time_end - $time_start" | bc) + local dd=$(echo "$dt/86400" | bc) + local dt2=$(echo "$dt-86400*$dd" | bc) + local dh=$(echo "$dt2/3600" | bc) + local dt3=$(echo "$dt2-3600*$dh" | bc) + local dm=$(echo "$dt3/60" | bc) + local ds=$(echo "$dt3-60*$dm" | bc) + printf '%dd:%02d:%02d:%02d' $dd $dh $dm $ds +} + +read -p "Запустить тестирование скрипта pg_backup.sh на тестовой СУБД? (yes/no): " RUN_FLAG +if test "$RUN_FLAG" != "yes"; then + echowarn "Запуск отменён" + exit 1 +fi + +echohead "Test started" +TIME_START=$(date +%s) # время в Unixtime + +BACKUP_DIR="/mnt/backup_db/active_full/cluster" +TEMP_DIR="$BACKUP_DIR/pg_backup_test" +CONF_FILE="$SCRIPT_DIR/pg_backup.conf" + +echohead "Запускаем pg_backup.sh без параметров" +sudo -i -u postgres -- ./pg_backup.sh || true + +for BACKUP_WAL_DOY_DIVIDER in 1 999; do + for FLAG in 0 1; do + + echohead "Корректируем '$CONF_FILE': BACKUP_WAL_DOY_DIVIDER=$BACKUP_WAL_DOY_DIVIDER, GPG_ENCRYPT=$FLAG, PG_AMCHECK_VALIDATE=$FLAG" + sed -E -e "s/(BACKUP_WAL_DOY_DIVIDER)=[0-9]+/1円=${BACKUP_WAL_DOY_DIVIDER}/" \ + -e "s/(GPG_ENCRYPT)=[0-9]+/1円=${FLAG}/" \ + -e "s/(PG_AMCHECK_VALIDATE)=[0-9]+/1円=${FLAG}/" \ + -i $CONF_FILE + + echohead "Удаляем все файлы в папке '$BACKUP_DIR'" + find $BACKUP_DIR -mindepth 1 -delete + + echohead "Тестируем ExecCondition" + sudo -i -u postgres -- ./pg_backup.sh ExecCondition + + echohead "Тестируем create" + sudo -i -u postgres -- ./pg_backup.sh create + + echohead "Тестируем validate" + sudo -i -u postgres -- ./pg_backup.sh validate + + echohead "Получаем название папки/файла бекапа" + BACKUP_FILE_OR_DIR=$(find $BACKUP_DIR -maxdepth 1 -name "*.pg_backup*" ! -name "*.log" -printf "%p") + test -z "$BACKUP_FILE_OR_DIR" && echoerr "no backup archive file/directory found in directory '$BACKUP_DIR'" && exit 1 + echo "Название папки/файла бекапа: '$BACKUP_FILE_OR_DIR'" + + echohead "Создаём временную папку '$TEMP_DIR'" + sudo -i -u postgres -- mkdir -p $TEMP_DIR + + echohead "Тестируем restore" + printf 'primary\n' | sudo -i -u postgres -- ./pg_backup.sh restore $BACKUP_FILE_OR_DIR $TEMP_DIR + + echohead "Удаляем временную папку '$TEMP_DIR'" + sudo -i -u postgres -- rm -r $TEMP_DIR + done +done + +TIME_END=$(date +%s) # время в Unixtime +TIME_ELAPSED=$(elapsed $TIME_START $TIME_END) +echosucc "Test finished successfully, duration: $TIME_ELAPSED (day:hh:mm:ss)" diff --git a/pg_backup/pg_backup_validate.service b/pg_backup/pg_backup_validate.service new file mode 100644 index 00000000..8c3956c1 --- /dev/null +++ b/pg_backup/pg_backup_validate.service @@ -0,0 +1,12 @@ +[Unit] +Description=PostgreSQL backup validate service + +[Service] +User=postgres +Group=postgres + +ExecCondition=/bin/bash /var/lib/pgsql/pg_backup.sh ExecCondition standby +ExecStart=/bin/bash /var/lib/pgsql/pg_backup.sh validate + +[Install] +WantedBy=multi-user.target diff --git a/pg_backup/pg_backup_validate.timer b/pg_backup/pg_backup_validate.timer new file mode 100644 index 00000000..44fcf420 --- /dev/null +++ b/pg_backup/pg_backup_validate.timer @@ -0,0 +1,11 @@ +[Unit] +Description=PostgreSQL backup validate timer + +[Timer] +Unit=pg_backup_validate.service +OnCalendar=Wed *-*-* 03:00:00 +RandomizedDelaySec=10800 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/pg_backup/restore_command.md b/pg_backup/restore_command.md index bf0a352d..233e4bf1 100644 --- a/pg_backup/restore_command.md +++ b/pg_backup/restore_command.md @@ -1,4 +1,4 @@ -# PostgreSQL: восстановление WAL файлов (restore_command) +# PostgreSQL: восстановление WAL файлов из архива (restore_command) ## Введение diff --git a/pg_backup/restore_command.sh b/pg_backup/restore_command.sh index 97319f97..00b62365 100644 --- a/pg_backup/restore_command.sh +++ b/pg_backup/restore_command.sh @@ -1,25 +1,25 @@ #!/bin/bash - + # проверяем, скрипт должен запускаться с двумя параметрами test "$#" -ne 2 && echo "Error: 2 number of parameters expected, $# given">&2 && exit 2 - + FILE_SRC="/mnt/backup_db/archive_wal/cluster/1ドル" FILE_DST="2ドル" - -test -f "$FILE_SRC" && cp "$FILE_SRC" "$FILE_DST" && exit -test -f "$FILE_SRC.partial" && cp "$FILE_SRC.partial" "$FILE_DST.partial" && exit - -test -f "$FILE_SRC.lz4" && lz4 -dkf "$FILE_SRC.lz4" "$FILE_DST" && exit -test -f "$FILE_SRC.partial.lz4" && lz4 -dkf "$FILE_SRC.partial.lz4" "$FILE_DST.partial" && exit - -test -f "$FILE_SRC.zst" && zstd -dkf "$FILE_SRC.zst" -o "$FILE_DST" && exit -test -f "$FILE_SRC.partial.zst" && zstd -dkf "$FILE_SRC.partial.zst" -o "$FILE_DST.partial" && exit - + +test -f "$FILE_SRC" && (cp "$FILE_SRC" "$FILE_DST" ; exit) +test -f "$FILE_SRC.partial" && (cp "$FILE_SRC.partial" "$FILE_DST.partial" ; exit) + +test -f "$FILE_SRC.lz4" && (lz4 -dkf "$FILE_SRC.lz4" "$FILE_DST" ; exit) +test -f "$FILE_SRC.partial.lz4" && (lz4 -dkf "$FILE_SRC.partial.lz4" "$FILE_DST.partial" ; exit) + +test -f "$FILE_SRC.zst" && (zstd -dkf "$FILE_SRC.zst" -o "$FILE_DST" ; exit) +test -f "$FILE_SRC.partial.zst" && (zstd -dkf "$FILE_SRC.partial.zst" -o "$FILE_DST.partial" ; exit) + # gzip DEPRECATED -test -f "$FILE_SRC.gz" && gzip -dkc "$FILE_SRC.gz"> "$FILE_DST" && exit -test -f "$FILE_SRC.partial.gz" && gzip -dkc "$FILE_SRC.partial.gz"> "$FILE_DST.partial" && exit - +test -f "$FILE_SRC.gz" && (gzip -dkc "$FILE_SRC.gz"> "$FILE_DST" ; exit) +test -f "$FILE_SRC.partial.gz" && (gzip -dkc "$FILE_SRC.partial.gz"> "$FILE_DST.partial" ; exit) + # pg_receivewal support, https://www.postgresql.org/docs/current/app-pgreceivewal.html -test -f "$FILE_SRC.gz.partial" && gzip -dkc "$FILE_SRC.gz.partial"> "$FILE_DST.partial" && exit -test -f "$FILE_SRC.lz4.partial" && lz4 -dkf "$FILE_SRC.lz4.partial" "$FILE_DST.partial" && exit -test -f "$FILE_SRC.zst.partial" && zstd -dkf "$FILE_SRC.zst.partial" -o "$FILE_DST.partial" && exit +test -f "$FILE_SRC.gz.partial" && (gzip -dkc "$FILE_SRC.gz.partial"> "$FILE_DST.partial" ; exit) +test -f "$FILE_SRC.lz4.partial" && (lz4 -dkf "$FILE_SRC.lz4.partial" "$FILE_DST.partial" ; exit) +test -f "$FILE_SRC.zst.partial" && (zstd -dkf "$FILE_SRC.zst.partial" -o "$FILE_DST.partial" ; exit) diff --git a/pg_receivewal/README.md b/pg_receivewal/README.md index 33f04211..d6f417ad 100644 --- a/pg_receivewal/README.md +++ b/pg_receivewal/README.md @@ -1,16 +1,19 @@ -# Инсталляция сервиса архивирования WAL файлов PostgreSQL +# 🍃 Инсталляция сервиса архивирования WAL файлов PostgreSQL ## Введение -⚠ Для кластеров СУБД используется не этот сервис, а штатная функциональность [archive_command](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND), чтобы везде было единообразно, это упрощает сопровождение. Обычно, при наличии синхронной реплики, требования по архивированию WAL файлов в реальном времени нет. - Для непрерывного архивирования [WAL файлов](https://postgrespro.ru/docs/postgresql/16/continuous-archiving) **в реальном времени** применяется [pg_receivewal](https://postgrespro.ru/docs/postgresql/16/app-pgreceivewal), а не [archive_command](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND). +> [!CAUTION] +> При наличии синхронной реплики архивировать WAL файлы в реальном времени не нужно. Для кластеров СУБД используется не этот сервис, а штатная функциональность [archive_command](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-ARCHIVE-COMMAND), чтобы везде было единообразно, это упрощает сопровождение. + Сервис работает только с СУБД мастером, использует отдельный слот репликации и выглядит как ещё одна постоянно отстающая асинхронная реплика. -i При архивировании WAL файлы сжимаются в формат `gzip` (≈ 66% от исходного размера, даже если включен параметр [wal_compression](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-WAL-COMPRESSION)). Это позволяет экономить место на сетевом диске и уменьшить нагрузку на ввод-вывод. +> [!NOTE] +> При архивировании WAL файлы сжимаются в формат `gzip` (≈ 66% от исходного размера, даже если включен параметр [wal_compression](https://postgrespro.ru/docs/postgresql/16/runtime-config-wal#GUC-WAL-COMPRESSION)). Это позволяет экономить место на сетевом диске и уменьшить нагрузку на ввод-вывод. -⚠ Удаление неактуальных WAL файлов сделано в сервисе резервного копирования! +> [!WARNING] +> Удаление неактуальных WAL файлов сделано в [сервисе резервного копирования](../pg_backup)! Преимущества сервиса: 1. Архивирование WAL файлов в реальном времени. Гарантируется, что ни одна транзакция не будет потеряна. @@ -26,8 +29,8 @@ **Инсталляция сервиса** ```bash # создаём файлы -sudo su - postgres -c "nano ~/.pgpass && chmod 600 ~/.pgpass" # в файле нужно сохранить пароль для пользователя bkp_replicator -sudo nano /etc/systemd/system/pg_receivewal@.service +sudo su - postgres -c "nano -c ~/.pgpass && chmod 600 ~/.pgpass" # в файле нужно сохранить пароль для пользователя bkp_replicator +sudo nano -c /etc/systemd/system/pg_receivewal@.service # PostgreSQL v14 sudo systemctl daemon-reload \ @@ -46,7 +49,7 @@ sudo systemctl status pg_receivewal@16 **Интеграция с Patroni** ```bash # разрешаем перезапускать сервис под пользователем postgres без пароля -sudo nano /etc/sudoers.d/permit_pgreceivewal +sudo nano -c /etc/sudoers.d/permit_pgreceivewal sudo su postgres -c "sudo /bin/systemctl restart pg_receivewal@14" # тестируем перезапуск # редактируем конфигурацию Patroni @@ -60,7 +63,7 @@ postgresql: #on_stop: /bin/bash -c 'sudo /bin/systemctl stop pg_receivewal@14' # закомментировано, т.к. это сделано в настройках pg_receivewal@.service через PartOf= ``` -Файлы +**Файлы** * [`/etc/systemd/system/pg_receivewal@.service`](pg_receivewal@.service) * [`/etc/sudoers.d/permit_pgreceivewal`](permit_pgreceivewal) @@ -69,6 +72,16 @@ postgresql: * interprets several `%` prefixes as specifiers (escape `%` with `%%`) * parses `\` before some characters (escape `\` with `\\`) +## Вопросы и ответы + +### Сервис был временно остановлен. После его запуска продолжит ли он копирование WAL файлов с того места, где остановился? +Да, если на сервере СУБД хватит WAL файлов для исключения «разрыва цепочки». + +Иначе нужно сделать так: +1. в архивной папке удалить все WAL файлы +1. запустить сервис +1. сделать полную резервную копию СУБД + ## Что осталось доделать в сервисе? 1. Протестировать, что сервис перезагружается при перезагрузке Patroni / PostgreSQL. @@ -79,6 +92,7 @@ postgresql: 1. https://www.cybertec-postgresql.com/en/never-lose-a-postgresql-transaction-with-pg_receivewal/ 1. SystemD + 1. https://systemd-by-example.com/ 1. https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html 1. https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html 1. https://www.youtube.com/watch?v=4s3mi-16vgI diff --git a/psqlrc/TODO.md b/psqlrc/TODO.md index dc97169f..82da75cc 100644 --- a/psqlrc/TODO.md +++ b/psqlrc/TODO.md @@ -1,5 +1,7 @@ # TODO list +Ошмётки в консоли https://bolknote.ru/all/oshmyotki-v-konsoli/ + Get some ideas from * https://github.com/zalando/pg_view * https://github.com/lesovsky/pgcenter/tree/master/internal/query ; https://habr.com/ru/articles/494162/

AltStyle によって変換されたページ (->オリジナル) /