Хабрахабр

WAL в PostgreSQL: 4. Настройка журнала

Итак, мы познакомились с устройством буферного кеша и на его примере поняли, что когда при сбое пропадает содержимое оперативной памяти, для восстановления необходим журнал предзаписи. Размер необходимых файлов журнала и время восстановления ограничены благодаря периодически выполняемой контрольной точке.

В этой статье (последней в этом цикле) мы рассмотрим те вопросы настройки, которые еще не обсуждались: уровни журнала и их назначение, а также надежность и производительность журналирования. В предыдущих статьях мы уже посмотрели на довольно большое число важных настроек, так или иначе относящихся к журналу.

Основная задача журнала предзаписи — обеспечить возможность восстановления после сбоя. Но, если уж все равно приходится вести журнал, его можно приспособить и для других задач, добавив в него некоторое количество дополнительной информации. Есть несколько уровней журналирования. Они задаются параметром wal_level и организованы так, что журнал каждого следующего уровня включает в себя все, что попадает в журнал предыдущего уровня, плюс еще что-то новое.

Minimal

Минимально возможный уровень задается значением wal_level = minimal и гарантирует только восстановление после сбоя. Для экономии места операции, связанные с массовой обработкой данных (такие, как CREATE TABLE AS SELECT или CREATE INDEX), не записываются в журнал. Вместо этого необходимые данные сразу пишутся на диск, а новый объект добавляется в системный каталог и становится видимым при фиксации транзакции. Если сбой происходит в процессе выполнения операции, уже записанные данные остаются невидимыми и не нарушают согласованности. Если же сбой происходит после того, как операция завершилась, все необходимое уже попало на диск и не нуждается в журналировании.

Сначала установим необходимый уровень (для этого потребуется также изменить другой параметр — max_wal_senders). Посмотрим.

=> ALTER SYSTEM SET wal_level = minimal;
=> ALTER SYSTEM SET max_wal_senders = 0;

student$ sudo pg_ctlcluster 11 main restart

Обратите внимание, что изменение уровня требует перезапуска сервера.

Запомним текущую позицию в журнале:

=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/353927BC
(1 row)

Теперь выполним создание таблицы (CREATE TABLE AS SELECT) и снова запишем позицию в журнале. Объем данных, выбираемый оператором SELECT, в данном случае не играет роли, поэтому мы ограничимся одной строкой.

=> CREATE TABLE wallevel AS SELECT 1 AS n;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/353A7DFC
(1 row)

Уже знакомой утилитой pg_waldump посмотрим на журнальные записи.

postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353927BC -e 0/353A7DFC

Некоторые детали, конечно, могут отличаться от запуска к запуску, а в данном случае получилось вот что. Запись менеджера Heap2 относится к очистке, здесь это внутристраничная очистка одной из таблиц системного каталога (системные объекты легко отличаются невооруженным взглядом по «короткому» номеру в rel):

rmgr: Heap2 len (rec/tot): 59/ 7587, tx: 0, lsn: 0/353927BC, prev 0/35392788, desc: CLEAN remxid 101126, blkref #0: rel 1663/16386/1247 blk 8 FPW

Затем идет запись о получении очередного номера OID для таблицы, которую мы собираемся создавать:

rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/35394574, prev 0/353927BC, desc: NEXTOID 82295

Теперь собственно создание таблицы:

rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/35394594, prev 0/35394574, desc: CREATE base/16386/74103

Однако вставка данных в таблицу не журналируется. Дальше идут многочисленные записи о вставке строк в разные таблицы и индексы — это PostgreSQL прописывает созданную таблицу в системном каталоге (привожу в сокращенном виде):

rmgr: Heap len (rec/tot): 203/ 203, tx: 101127, lsn: 0/353945C0, prev 0/35394594, desc: INSERT off 71, blkref #0: rel 1663/16386/1247 blk 8
rmgr: Btree len (rec/tot): 53/ 685, tx: 101127, lsn: 0/3539468C, prev 0/353945C0, desc: INSERT_LEAF off 37, blkref #0: rel 1663/16386/2703 blk 2 FPW
...
rmgr: Btree len (rec/tot): 53/ 2393, tx: 101127, lsn: 0/353A747C, prev 0/353A6788, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW

Ну и наконец фиксация транзакции:

rmgr: Transaction len (rec/tot): 34/ 34, tx: 101127, lsn: 0/353A7DD8, prev 0/353A747C, desc: COMMIT 2019-07-23 18:59:34.923124 MSK

Replica

Когда мы восстанавливаем систему из резервной копии, мы начинаем с некоторого состояния файловой системы и постепенно доводим данные до целевой точки восстановления, проигрывая заархивированные журнальные записи. Количество таких записей может быть весьма велико (например, несколько дней), то есть период восстановления будет охватывать не одну контрольную точку, а множество. Поэтому понятно, что минимального уровня журнала недостаточно — если какая-то операция не журналируется, мы просто не узнаем, что ее надо повторить. Для восстановления из резервной копии в журнал должны попадать все операции.

Но, если мы хотим выполнять на реплике запросы, все еще усложняется. То же самое верно и для репликации — все, что не журналируется, не будет передано на реплику и не будет воспроизведено.

Такие блокировки записываются в журнал и применяются на реплике (от имени процесса startup). Во-первых, нам нужна информация об исключительных блокировках, возникающих на основном сервере, поскольку они могут конфликтовать с запросами на реплике.

В случае реплики речь идет не только о локальных транзакциях, но и о транзакциях на основном сервере. Во-вторых, нужно уметь строить снимки данных, а для этого, как мы помним, необходима информация о выполняющихся транзакциях. Единственный способ эту информацию передать — периодически записывать ее в журнал (это происходит раз в 15 секунд).

(До версии 9. Уровень журнала, гарантирующий как возможность восстановления из резервной копии, так и возможность физической репликации, задается значением wal_level = replica. 6 было два отдельных уровня archive и hot_standby, но потом их объединили в один общий.)

Поэтому просто сбросим параметры в умолчательные значения: Начиная с версии PostgreSQL 10 именно этот уровень установлен по умолчанию (а до этого был minimal).

=> ALTER SYSTEM RESET wal_level;
=> ALTER SYSTEM RESET max_wal_senders;

student$ sudo pg_ctlcluster 11 main restart

Удаляем таблицу и повторяем ровно ту же последовательность действий, что и в прошлый раз:

=> DROP TABLE wallevel;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/353AF21C
(1 row)

=> CREATE TABLE wallevel AS SELECT 1 AS n;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/353BE51C
(1 row)

Теперь проверим журнальные записи.

postgres$ /usr/lib/postgresql/11/bin/pg_waldump -p /var/lib/postgresql/11/main/pg_wal -s 0/353AF21C -e 0/353BE51C

Очистка, получение OID, создание таблицы и регистрация в системном каталоге — пока все как было:

rmgr: Heap2 len (rec/tot): 58/ 58, tx: 0, lsn: 0/353AF21C, prev 0/353AF044, desc: CLEAN remxid 101128, blkref #0: rel 1663/16386/1247 blk 8
rmgr: XLOG len (rec/tot): 30/ 30, tx: 0, lsn: 0/353AF258, prev 0/353AF21C, desc: NEXTOID 82298
rmgr: Storage len (rec/tot): 42/ 42, tx: 0, lsn: 0/353AF278, prev 0/353AF258, desc: CREATE base/16386/74106
rmgr: Heap len (rec/tot): 203/ 203, tx: 101129, lsn: 0/353AF2A4, prev 0/353AF278, desc: INSERT off 73, blkref #0: rel 1663/16386/1247 blk 8
rmgr: Btree len (rec/tot): 53/ 717, tx: 101129, lsn: 0/353AF370, prev 0/353AF2A4, …
rmgr: Btree len (rec/tot): 53/ 2413, tx: 101129, lsn: 0/353BD954, prev 0/353BCC44, desc: INSERT_LEAF off 10, blkref #0: rel 1664/0/1233 blk 1 FPW

А вот что-то новое. Запись об исключительной блокировке, относящаяся к менеджеру Standby — в данном случае это блокировка номера транзакции (зачем она нужна, мы подробно поговорим в следующем цикле статей):

rmgr: Standby len (rec/tot): 42/ 42, tx: 101129, lsn: 0/353BE2D8, prev 0/353BD954, desc: LOCK xid 101129 db 16386 rel 74106

А это — запись о вставке строк в нашу таблицу (сравните номер файла rel с тем, что указан выше в записи CREATE):

rmgr: Heap len (rec/tot): 59/ 59, tx: 101129, lsn: 0/353BE304, prev 0/353BE2D8, desc: INSERT+INIT off 1, blkref #0: rel 1663/16386/74106 blk 0

Запись о фиксации:

rmgr: Transaction len (rec/tot): 421/ 421, tx: 101129, lsn: 0/353BE340, prev 0/353BE304, desc: COMMIT 2019-07-23 18:59:37.870333 MSK; inval msgs: catcache 74 catcache 73 catcache 74 catcache 73 catcache 50 catcache 49 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 catcache 7 catcache 6 snapshot 2608 relcache 74106 snapshot 1214

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

rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/353BE4E8, prev 0/353BE340, desc: RUNNING_XACTS nextXid 101130 latestCompletedXid 101129 oldestRunningXid 101130

Logical

Наконец, последний уровень задается значением параметра wal_level = logical и обеспечивает возможность работы логического декодирования и логической репликации. Он должен быть включен на публикующем сервере.

В основном же логическое декодирование зависит от информации о выполняющихся транзакциях, поскольку требуется строить снимок данных для отслеживания изменений системного каталога. С точки зрения журнальных записей этот уровень практически не отличается от replica — добавляются записи, относящиеся к источникам репликации (replication origins), и произвольные логические записи, которые могут добавлять в журнал приложения.

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

Понятно, что механизм журналирования должен быть надежным и давать гарантии возможности восстановления в любых ситуациях (не связанных, конечно, с повреждением носителя данных). На надежность влияют много факторов, из которых мы рассмотрим кеширование, повреждение данных и атомарность записи.

Кеширование

На пути данных к энергонезависимому хранилищу (такому, как пластина жесткого диска), стоят многочисленные кеши.

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

Электроника накопителя тоже может отложить запись, например, собирая данные в группы, которые более выгодно записать одновременно. Когда ОС решает записать данные, они попадают в кеш накопителя (жесткого диска). А если используется RAID-контроллер, между ОС и диском появляется еще один уровень кеширования.

Обычно это и не важно, но есть критические места, в которых PostgreSQL должен быть уверен в том, что данные записаны надежно. Таким образом, если не предпринять специальных мер, совершенно непонятно, когда же данные действительно будут надежным образом сохранены. Но есть и другие ситуации, скажем, выполнение нежурналируемых операций на уровне minimal и др. В первую очередь это журналирование (если журнальная запись не дошла до диска, она пропадет вместе с остальным содержимым оперативной памяти) и контрольная точка (должна быть уверенность, что грязные страницы действительно записаны на диск).

Есть несколько вариантов, но они сводятся к двум основным: либо после записи дается команда синхронизации (fsync, fdatasync), либо при открытии файла (или записи в него) указывается специальный флаг необходимости синхронизации или даже прямой записи, минуя кеш ОС. Операционная система предоставляет средства, которые должны гарантировать немедленную запись данных в энергонезависимую память.

Обычные файлы всегда синхронизируется с помощью fsync. Что касается журнала, утилита pg_test_fsync позволяет выбрать способ, наиболее подходящий для конкретной ОС и конкретной файловой системы, а устанавливается он в конфигурационном параметре wal_sync_method.

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

Документация содержит много подробностей на эту тему.

В любом случае синхронизация стоит дорого и выполняется не чаще, чем абсолютно необходимо (к этому вопросу мы еще вернемся чуть ниже, когда будет говорить о производительности).

Отключая fsync, вы соглашаетесь с тем, что данные могут быть безвозвратно потеряны в любой момент. Вообще говоря, синхронизацию можно отключить (за это отвечает параметр fsync), но в этом случае про надежность хранения следует забыть. Наверное, единственный разумный вариант использования этого параметра — временное увеличение производительности, когда данные можно легко восстановить из другого источника (например, при начальной миграции).

Повреждение данных

Аппаратура несовершенна и данные могут быть повреждены на носителе, при передаче данных по интерфейсным кабелям и т. п. Часть таких ошибок обрабатывается на аппаратном уровне, но часть — нет.

Чтобы вовремя обнаружить возникшую проблему, журнальные записи всегда снабжаются контрольными суммами.

Пока это можно сделать только при инициализации кластера, но в версии PostgreSQL 12 их можно будет включать и выключать с помощью утилиты pg_checksums (правда, пока не «на лету», а только при остановленном сервере). Страницы данных также можно защитить контрольными суммами.

Это уменьшает вероятность того, что сбой не будет вовремя обнаружен. В производственной среде контрольные суммы обязательно должны быть включены, несмотря на накладные расходы на их вычисление и контроль.

Именно поэтому pg_probackup проверяет при резервном копировании контрольные суммы всех страниц кластера.
Во-вторых, страница, заполненная нулями, считается корректной — если файловая система по ошибке «занулит» файл, это может остаться незамеченным.
В-третьих, контрольные суммы защищают только основной слой файлов данных. Уменьшает, но не устраняет.
Во-первых, контрольные суммы проверяются только при обращении к странице — поэтому повреждение может остаться незамеченным до момента, когда оно попадет во все резервные копии. Остальные слои и остальные файлы (например, статусы транзакций XACT) ничем не защищены.
Увы.

Посмотрим, как это работает. Для начала убедимся, что контрольные суммы включены (имейте в виду, что при установке пакета в Debian-подобных системах по умолчанию это не так):

=> SHOW data_checksums;

data_checksums
---------------- on
(1 row)

Параметр data_checksums доступен только для чтения.

Вот в каком файле располагается наша таблица:

=> SELECT pg_relation_filepath('wallevel');

pg_relation_filepath
---------------------- base/16386/24890
(1 row)

Остановим сервер и поменяем несколько байтов в нулевой странице, например сотрем из заголовка LSN последней журнальной записи.

student$ sudo pg_ctlcluster 11 main stop

postgres$ dd if=/dev/zero of=/var/lib/postgresql/11/main/base/16386/24890 oflag=dsync conv=notrunc bs=1 count=8

8+0 records in
8+0 records out
8 bytes copied, 0,0083022 s, 1,0 kB/s

В принципе сервер можно было бы и не останавливать. Достаточно, чтобы страница записалась на диск и была вытеснена из кеша (иначе сервер так и будет работать со страницей из кеша). Но такой сценарий сложнее воспроизвести.

Теперь запускаем сервер и пробуем прочитать таблицу.

student$ sudo pg_ctlcluster 11 main start

=> SELECT * FROM wallevel;

WARNING: page verification failed, calculated checksum 23222 but expected 50884
ERROR: invalid page in block 0 of relation base/16386/24890

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

=> SET ignore_checksum_failure = on;
=> SELECT * FROM wallevel;

WARNING: page verification failed, calculated checksum 23222 but expected 50884 n
--- 1
(1 row)

Конечно, в данном случае все проходит успешно, потому что мы испортили только заголовок страницы, а не сами данные.

При включенных контрольных суммах в журнал записываются биты подсказок (мы рассматривали их ранее), поскольку изменение любого, даже несущественного, бита приводит и к изменению контрольной суммы. И еще один момент. При выключенных контрольных суммах за запись в журнал битов подсказок отвечает параметр wal_log_hints.

В этом случае имеет смысл включить сжатие полных образов с помощью параметра wal_compression (этот параметр появился в версии 9. Изменения битов подсказок всегда журналируется в виде полного образа страницы (FPI, full page image), что порядком увеличивает размер журнала. Чуть ниже мы посмотрим на конкретные цифры. 5).

Атомарность записи

Ну и наконец существует проблема атомарности записи. Страница базы данных занимает не менее 8 Кб (может быть 16 или 32 Кб), а на низком уровне запись происходит блоками, которые обычно имеют меньший размер (как правило 512 байт или 4 Кб). Поэтому при сбое питания страница данных может записаться частично. Понятно, что при восстановлении бессмысленно применять к такой странице обычные журнальные записи.

Этим управляет параметр full_page_writes, и он включен по умолчанию. Для защиты PostgreSQL позволяет записывать в журнал полный образ страницы при первом ее изменении после начала контрольной точки (такой же образ записывается и при изменении битов подсказок).

И уже к этому гарантированно корректному образу дальше применяются обычные журнальные записи. Если при восстановлении в журнале встречается образ страницы, он безусловно (без проверки LSN) записывается на диск: к нему больше доверия, поскольку, как и всякая журнальная запись, он защищен контрольной суммой.

Как уже говорилось, ситуацию можно улучшить за счет сжатия полных образов (параметр wal_compression). Хотя PostgreSQL исключает из полного образа страницы незанятое место (ранее мы рассматривали структуру блока), все же объем генерируемых журнальных записей существенно увеличивается.

Выполним инициализацию: Чтобы как-то почувствовать изменение размера журнала, проведем простой эксперимент с помощью утилиты pgbench.

student$ pgbench -i test

dropping old tables...
creating tables...
generating data...
100000 of 100000 tuples (100%) done (elapsed 0.15 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done.

Параметр full_page_writes включен:

=> SHOW full_page_writes;

full_page_writes
------------------ on
(1 row)

Выполним контрольную точку и сразу же запустим тест на 30 секунд.

=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/38E04A08
(1 row)

student$ pgbench -T 30 test

starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 30 s
number of transactions actually processed: 26851
latency average = 1.117 ms
tps = 895.006720 (including connections establishing)
tps = 895.095229 (excluding connections establishing)

=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/3A69C478
(1 row)

Размер журнальных записей:

=> SELECT pg_size_pretty('0/3A69C478'::pg_lsn - '0/38E04A08'::pg_lsn);

pg_size_pretty
---------------- 25 MB
(1 row)

Теперь выключим параметр full_page_writes:

=> ALTER SYSTEM SET full_page_writes = off;
=> SELECT pg_reload_conf();

И повторим эксперимент.

=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/3A69C530
(1 row)

student$ pgbench -T 30 test

starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 30 s
number of transactions actually processed: 27234
latency average = 1.102 ms
tps = 907.783080 (including connections establishing)
tps = 907.895326 (excluding connections establishing)

=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/3BE87658
(1 row)

Размер журнальных записей:

=> SELECT pg_size_pretty('0/3BE87658'::pg_lsn - '0/3A69C530'::pg_lsn);

pg_size_pretty
---------------- 24 MB
(1 row)

Да, размер уменьшился, но совсем не так существенно, как можно было бы ожидать.

Эти данные (в нашем случае) составляют примерно половину всего объема, в чем можно убедиться, посмотрев статистику: Причина в том, что кластер инициализирован с контрольными суммами в страницах данных, и поэтому в журнал все равно приходится записывать полные образы страниц при изменении битов подсказок.

postgres$ /usr/lib/postgresql/11/bin/pg_waldump --stats -p /var/lib/postgresql/11/main/pg_wal -s 0/3A69C530 -e 0/3BE87658

Type N (%) Record size (%) FPI size (%) ---- - --- ----------- --- -------- --- XLOG 1721 ( 1,03) 84329 ( 0,77) 13916104 (100,00) Transaction 27235 ( 16,32) 926070 ( 8,46) 0 ( 0,00) Storage 1 ( 0,00) 42 ( 0,00) 0 ( 0,00) CLOG 1 ( 0,00) 30 ( 0,00) 0 ( 0,00) Standby 4 ( 0,00) 240 ( 0,00) 0 ( 0,00) Heap2 27522 ( 16,49) 1726352 ( 15,76) 0 ( 0,00) Heap 109691 ( 65,71) 8169121 ( 74,59) 0 ( 0,00) Btree 756 ( 0,45) 45380 ( 0,41) 0 ( 0,00) -------- -------- -------- Total 166931 10951564 [44,04%] 13916104 [55,96%]

Из таблицы я для компактности убрал нулевые строки. Обратите внимание на итоговую строку (Total) и сравните размер полных образов (FPI size) с размером обычных записей (Record size).

Но, как мы видим, в этом нет большого резона (предполагая, что контрольные суммы включены). Параметр full_page_writes можно отключать только в том случае, если используемая файловая система и аппаратура сами по себе гарантируют атомарность записи.

Теперь давайте посмотрим, как помогает сжатие.

=> ALTER SYSTEM SET full_page_writes = on;
=> ALTER SYSTEM SET wal_compression = on;
=> SELECT pg_reload_conf();

Повторяем тот же эксперимент.

=> CHECKPOINT;
=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/3BE87710
(1 row)

student$ pgbench -T 30 test

starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 30 s
number of transactions actually processed: 26833
latency average = 1.118 ms
tps = 894.405027 (including connections establishing)
tps = 894.516845 (excluding connections establishing)

=> SELECT pg_current_wal_insert_lsn();

pg_current_wal_insert_lsn
--------------------------- 0/3CBD3EA8
(1 row)

Размер журнальных записей:

=> SELECT pg_size_pretty('0/3CBD3EA8'::pg_lsn - '0/3BE87710'::pg_lsn);

pg_size_pretty
---------------- 13 MB
(1 row)

Вывод: при наличии большого числа полных образов страниц (из-за контрольных сумм или full_page_writes, то есть почти всегда), скорее всего имеет смысл воспользоваться сжатием несмотря на то, что это нагружает процессор.
При обычной работе сервера происходит постоянная последовательная запись журнальных файлов. Поскольку отсутствует случайный доступ, с этой задачей справляются и обычные HDD-диски. Но такой характер нагрузки существенно отличается от того, как происходит доступ к файлам данных.

Вместо каталога $PGDATA/pg_wal нужно создать символьную ссылку на соответствующий каталог. Поэтому обычно выгодно размещать журнал на отдельном физическом диске (или дисковом массиве), примонтированных к файловой системе сервера.

Первая — понятный случай восстановления после сбоя. Есть пара ситуаций, при которой журнальные файлы необходимо не только писать, но и читать. Она возникает, если используется потоковая репликация, и реплика не успевает получать журнальные записи, пока они еще находятся в буферах оперативной памяти основного сервера. Вторая менее тривиальна. Подробней мы поговорим об этом, когда дойдем до репликации.
Тогда процессу walsender приходится читать нужные данные с диска.

Запись журнала происходит в одном из двух режимов:

  • синхронном — при фиксации транзакции продолжение работы невозможно до тех пор, пока все журнальные записи об этой транзакции не окажутся на диске;
  • асинхронном — транзакция завершается немедленно, а журнал записывается в фоновом режиме.

Синхронный режим определяется параметром synchronous_commit и включен по умолчанию.

Для этого обслуживающий процесс, завершающий транзакцию и записывающий журнал, делает небольшую паузу, определяемую параметром commit_delay. Поскольку синхронизация связана с реальным (то есть медленным) вводом-выводом, выгодно выполнять ее как можно реже. Ставка тут делается на то, что за время ожидания некоторые транзакции успеют завершиться и можно будет синхронизировать их записи за один прием. Но происходит это только в том случае, если в системе имеется не менее commit_siblings активных транзакций. Это похоже на то, как вы придерживаете двери лифта, чтобы кто-то успел заскочить в кабину.

Изменять commit_delay имеет смысл только в системах, выполняющих большое количество коротких OLTP-транзакций. По умолчанию параметр commit_siblings = 5, а commit_delay = 0, так что фактически ожидания не происходит.

После этого транзакция считается завершенной. Затем процесс выполняет сброс журнала на диск до необходимого LSN (или несколько больше, если за время ожидания добавились новые записи).

Обратная сторона состоит в том, что синхронная запись увеличивает время отклика (команда COMMIT не возвращает управление до окончания синхронизации) и уменьшает производительность системы. При синхронной записи гарантируется долговечность (буква D в аббревиатуре ACID) — если транзакция зафиксирована, то все ее журнальные записи уже на диске и не будут потеряны.

Асинхронную запись можно получить, установ synchronous_commit = off (или local).

При асинхронной записи сброс журнальных записей выполняет процесс wal writer, чередуя циклы работы с ожиданием (которое устанавливается параметром wal_writer_delay = 200ms по умолчанию).

Если появились, то процесс игнорирует текущую, недозаполненную, страницу, а записывает только полностью заполненные. Проснувшись после очередного ожидания, процесс проверяет, появились ли с прошлого раза полностью заполненные страницы WAL. (Правда, не всегда все сразу: запись останавливается, дойдя до конца кеша, и продолжается с начала кеша уже в следующий раз.)

Если же ни одна страница не заполнилась, то процесс записывает текущую (не полностью заполненную) страницу журнала — не зря же просыпался?

Этот алгоритм нацелен на то, чтобы по возможности не синхронизировать одну и ту же страницу несколько раз, что важно при большом потоке изменений.

Однако надежность уменьшается: зафиксированные данные могут пропасть в случае сбоя, если между фиксацией и сбоем прошло менее 3 × wal_writer_delay времени (что при настройке по умолчанию составляет чуть больше полсекунды). Асинхронная запись эффективнее синхронной — фиксация изменений не ждет записи.

Непростой выбор — эффективность или надежность — остается за администратором системы.

В случае сбоя система все равно восстановит согласованное состояние, но, возможно, часть последних транзакций будет в нем отсутствовать. Обратите внимание: в отличие от выключения синхронизации (fsync = off), асинхронный режим не приводит к невозможности восстановления.

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

Даже при синхронной фиксации журнальные записи долгой транзакции будут записываться асинхронно, чтобы освободить буферы WAL. В реальности оба этих режима работают совместно. А если при сбросе страницы из буферного кеша окажется, что соответствующая журнальная запись еще не на диске, она тут же будет сброшена в синхронном режиме.

Чтобы получить какое-то представление о том, какой выигрыш дает асинхронная фиксация, попробуем повторить в таком режиме тест pgbench.

=> ALTER SYSTEM SET synchronous_commit = off;
=> SELECT pg_reload_conf();

student$ pgbench -T 30 test

starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
duration: 30 s
number of transactions actually processed: 45439
latency average = 0.660 ms
tps = 1514.561710 (including connections establishing)
tps = 1514.710558 (excluding connections establishing)

При синхронной фиксации мы получали примерно 900 транзакций в секунду (tps), при асинхронной — 1500. Разумеется, в реальной системе под реальной нагрузкой соотношение будет другим, но видно, что при коротких транзакциях эффект может быть весьма значительным.

Если что-то важное осталось за кадром, не сочтите за труд написать в комментариях. На этом цикл статей про журналирование подошел к концу. Всем спасибо!

А дальше нас ждут увлекательные приключения в мире блокировок, но это уже другая история.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть