В прошлой публикации мы обсудили механизм парсинга, оптимизации и исполнения запроса в PostgreSQL. В процессе обсуждения, был также затронут WAL (Write-Ahead Log). Давайте разберемся, что же это такое.
WAL, он же Write Ahead Log — бинарный лог, хранящий в бинарном виде непоcредственные результаты исполнения транзакций, модифицирующих текущее состояние данных. Речь идет о запросах INSERT, UPDATE и DELETE.
WAL обеспечивает Durability из ACID, т.е. сохранность данных в случае любых возможных сбоев. Тем не менее, ошибочно представлять себе WAL как бэкап данных. Смысл данного механизма не в хранении копии всех созданных и измененных данных с момента создания бд.
WAL используется для нескольких целей. В том числе — это основной механизм получения реплицируемых данных, будь то физическая или логическая репликация. Но об этом мы сейчас не будем говорить. В нашем примере речь идет о единственном инстансе PostgreSQL, запущенном на отдельной машине или в контейнере.
Синхронная фиксация отдельной транзакции
Любой механизм удобнее всего рассматривать на конкретном примере. Именно этим путем пойдем и мы:
-
Клиент отправляет UPDATE запрос: Клиент начинает транзакцию, в рамках которой будет производиться изменение данных в базе данных. Это может быть, например, запрос на изменение значений в таблице.
-
PostgreSQL выполняет проверки и запрос: PostgreSQL выполняет необходимые проверки, чтобы удостовериться, что изменения соответствуют правилам и ограничениям базы данных. Если все проверки проходят успешно, запрос выполняется и в оперативной памяти у нас появляются обновленные данные.
-
Запись данных в WAL буфер: обновленные данные записываются в WAL буфер, который является составной частью Shared буфера Postgres.
-
Запись данных в WAL сегмент на диске: Postgres использует fsync для переноса данных из буфера в основной памяти в файл сегмента на диске. Это одна из самых тяжелых операций, поэтому речь идет не о голом fsync, а с применением некоторых оптимизационных техник. По-умолчанию размер каждого файла сегмента WAL на диске (энергонезависимая память) 16mb. Как только исчерпывается лимит, создается новый файл сегмента.
-
Фиксация транзакции — COMMIT: в WAL записывается сообщение о фиксации данной транзакции. Запись происходит одновременно с записью на шаге 4, сразу за записанными данными (поскольку речь идет о синхронной фиксации).
-
Сохранение изменений на диске: обновленные данные записываются в файлы, представляющие собой физическое представление таблицы базы данных.
-
Snapshot Update: транзакции, которые стартовали до фиксации нашей текущей транзакции в WAL и выполняются в данный момент, видят старую версию измененных нами строк. Это та самая изоляция (I из ACID) и ее обсуждение выходит за рамки данной статьи. Тем не менее, для новых транзакций (дефолтный уровень изоляции Read Commited и выше), которые будут созданы с момента фиксации нашей транзакции, должна быть видна версия данных, включающая наши обновления. Т.е. происходит snapshot update и новые запросы клиентов читают уже данные в том виде, в котором они находятся после нашей транзакции. Подобная изоляция и обновление снэпшотов в Postgres обеспечиваются механизмом MVCC (Multi Version Concurency Control), который показывает разную версию одних и тех же строк таблицы в рамках разных транзакций для обеспечения согласованности данных.
-
VACUUM: по большому счету, этот шаг не является последовательным и не имеет непосредственного отношения к описанному нами процессу. Однако, есть один аспект, производный от наших действий, которые не хочется оставлять без внимания. А что же со старой версией данных? Наверное, если все транзакции, которым она была нужна (стартовавшие до COMMIT’а нашей), уже завершились, то эти данные помечаются как stale data. Время от времени в Postgres запускается процесс VACUUM, который выполняет роль, схожую со сборщиком мусора (Garbage Collector) в языках программирования. Он освобождает место на диске и в памяти от таких удаленных строк. Это не единственная задача процесса VACUUM, но другие его функции выходят за рамки статьи.
Заинтересованному в других функция процесса вакуума читателю предлагаю ознакомиться с термином «Transaction wraparound», в основе которого лежит 32-битная природа Txid (идентификатор транзакции) в Postgres, которая приводит к исчерпанию диапазона значений и необходимости переиспользования одних и тех же идентификаторов.
В рассмотренном примере мы исходили из того, что значение параметра synchronous_commit = on, поскольку речь шла о синхронной фиксации. Это параметр, значение которого определяет, в какой момент мы можем считать транзакцию успешной и вернуть сообщение об этом клиенту.
Прочие значения synchronous_commit и асинхронная фиксация
Существует 5 возможных значений данного параметра, но поскольку мы рассматриваем вариант со standalone Postgres, без репликации данных на другие машины, рассмотрим только некоторые из них:
-
off: значение является индикатором того, что транзакцию можно считать выполненной до того как данные будут зафиксированы на диске в WAL. Такую фиксацию называют асинхронной. Если произойдет физический сбой Postgres, несколько последних асинхронных комитов могут быть утеряны навсегда.
-
local: данные в WAL записаны и сброшены на локальный диск. В этом случае транзакция будет считаться зафиксированной. В нашем примере именно это и происходит
-
on: это значение по умолчанию. Но его смысл меняется в зависимости от того, является ли Postgres standalone (одна машина), как в нашем случае, либо кластером из нескольких реплицируемых машин. Если бы у нас была синхронная реплика, то транзакция считалась бы зафиксированной только тогда, когда аналогичные действия с WAL произошли бы и на реплике
Значение synchronous_commit можно задавать для инстанса, базы данных, пользователя бд, сессии, конкретной транзакции, передавая его тем или иным образом. Но это довольно специфические кейсы. В общем случае конфигурируется на уровне инстанса или базы данных.
Масштаб возможных потерь данных при асинхронной фиксации
Очевидно, что асинхронная фиксация даст серьезный прирост в производительности, потому что не придется ждать выполнения дорогой дисковой операции записи. Но вместе с этим возникает и вопрос — как много данных мы потеряем, если произойдет сбой, а данные в WAL еще не записались?
В общих чертах формула для случая с асинхронным коммитом звучит следующим образом:
Потери при асинхронной фиксации составят меньше двух интервалов wal_wirter_delay в стандартном случае. В худшем случае могут достичь трех.
Разберемся, что же из себя представляет такой параметр, как wal_writer_delay. По умолчанию его значение 200 милисекунд. Это значит, что каждые 200 милисекунд данные из WAL буфера в основной памяти будут сбрасываться в журнал фиксации на диске. Руководствуясь формулой выше мы можем прийти к пониманию, что в случае физического сбоя с конфигурацией асинхронного коммита мы потеряем данные за 400-600 милисекунд. В зависимости от количества транзакций в секунду и критичности самих данных, данные показатели могут быть как абсолютно нормальными, так и категорически неприемлемыми.
Но данный параметр не является единственным критерием, определяющим момент сброса данных на персистентный носитель. Данные также автоматически сохраняются в зависимости от достижения порога заполненных страниц в буфере WAL в основной памяти. Этот порог определяется значением параметра wal_writer_flush_after и при его достижении до наступления следующего тика wal_wirter_delay, данные сбрасываются на диск не дожидаясь наступления последнего.
Что же касается производительности, очевидно, что более строгие значения synchronous_commit ведут к меньшей производительности. Также немалое значение имеют такие показатели как задержка в работе fsync, сетевые задержки, задержки в случае синхронного комита с репликацией и ожиданием подтверждения от другой машины, производительность дисков и т.д. К этим вопросам мы вернемся в следующих публикациях.
Баланс между надежностью (reliability) и производительностью это сугубо индивидуальный вопрос, универсального ответа на который нет и не будет. Тюнинг тех или иных настроек должен происходить постепенно и будет меняться в зависимости от архитектуры хранимых данных и нагрузки.
Благодарю читателя за прочтение. Надеюсь изложение материала было доступным.
Предыдущая публикация: Немного о Durability в Postgres. Часть 1
ссылка на оригинал статьи https://habr.com/ru/articles/855910/
Добавить комментарий