В прошлой публикации мы обсудили механизм парсинга, оптимизации и исполнения запроса в PostgreSQL. В процессе обсуждения, был также затронут WAL (Write-Ahead Log). Давайте разберемся, что же это такое.

WAL, он же Write Ahead Log - бинарный лог, хранящий в бинарном виде непоcредственные результаты исполнения транзакций, модифицирующих текущее состояние данных. Речь идет о запросах INSERT, UPDATE и DELETE.

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

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

Синхронная фиксация отдельной транзакции

Любой механизм удобнее всего рассматривать на конкретном примере. Именно этим путем пойдем и мы:

  1. Клиент отправляет UPDATE запрос: Клиент начинает транзакцию, в рамках которой будет производиться изменение данных в базе данных. Это может быть, например, запрос на изменение значений в таблице.

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

  3. Запись данных в WAL буфер: обновленные данные записываются в WAL буфер, который является составной частью Shared буфера Postgres.

  4. Запись данных в WAL сегмент на диске: Postgres использует fsync для переноса данных из буфера в основной памяти в файл сегмента на диске. Это одна из самых тяжелых операций, поэтому речь идет не о голом fsync, а с применением некоторых оптимизационных техник. По-умолчанию размер каждого файла сегмента WAL на диске (энергонезависимая память) 16mb. Как только исчерпывается лимит, создается новый файл сегмента.

    Иллюстрация шагов 3 и 4. Первые два шага были рассмотрены в предыдущей публикации и нам сейчас не сильно интересны
    Иллюстрация шагов 3 и 4. Первые два шага были рассмотрены в предыдущей публикации и нам сейчас не сильно интересны
  5. Фиксация транзакции - COMMIT: в WAL записывается сообщение о фиксации данной транзакции. Запись происходит одновременно с записью на шаге 4, сразу за записанными данными (поскольку речь идет о синхронной фиксации).

  6. Сохранение изменений на диске: обновленные данные записываются в файлы, представляющие собой физическое представление таблицы базы данных. 

    Пример wal сегментов по 16мб каждый, лежащих по пути /var/lib/postgresql/data/pb_wal
    Пример wal сегментов по 16мб каждый, лежащих по пути /var/lib/postgresql/data/pb_wal
  7. Snapshot Update: транзакции, которые стартовали до фиксации нашей текущей транзакции в WAL и выполняются в данный момент, видят старую версию измененных нами строк. Это та самая изоляция (I из ACID) и ее обсуждение выходит за рамки данной статьи. Тем не менее, для новых транзакций (дефолтный уровень изоляции Read Commited и выше), которые будут созданы с момента фиксации нашей транзакции, должна быть видна версия данных, включающая наши обновления. Т.е. происходит snapshot update и новые запросы клиентов читают уже данные в том виде, в котором они находятся после нашей транзакции. Подобная изоляция и обновление снэпшотов в  Postgres обеспечиваются механизмом MVCC (Multi Version Concurency Control), который показывает разную версию одних и тех же строк таблицы в рамках разных транзакций для обеспечения согласованности данных.

  8. VACUUM: по большому счету, этот шаг не является последовательным и не имеет непосредственного отношения к описанному нами процессу. Однако, есть один аспект, производный от наших действий, которые не хочется оставлять без внимания. А что же со старой версией данных? Наверное, если все транзакции, которым она была нужна (стартовавшие до COMMIT'а нашей), уже завершились, то эти данные помечаются как stale data. Время от времени в Postgres запускается процесс VACUUM, который выполняет роль, схожую со сборщиком мусора (Garbage Collector) в языках программирования. Он освобождает место на диске и в памяти от таких удаленных строк. Это не единственная задача процесса VACUUM, но другие его функции выходят за рамки статьи.

    Заинтересованному в других функция процесса вакуума читателю предлагаю ознакомиться с термином "Transaction wraparound", в основе которого лежит 32-битная природа Txid (идентификатор транзакции) в Postgres, которая приводит к исчерпанию диапазона значений и необходимости переиспользования одних и тех же идентификаторов.

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

Прочие значения synchronous_commit и асинхронная фиксация

Существует 5 возможных значений данного параметра, но поскольку мы рассматриваем вариант со standalone Postgres, без репликации данных на другие машины,  рассмотрим только некоторые из них:

  1. off: значение является индикатором того, что транзакцию можно считать выполненной до того как данные будут зафиксированы на диске в WAL. Такую фиксацию называют асинхронной. Если произойдет физический сбой Postgres, несколько последних асинхронных комитов могут быть утеряны навсегда.

  2. local: данные в WAL записаны и сброшены на локальный диск. В этом случае транзакция будет считаться зафиксированной. В нашем примере именно это и происходит

  3. 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

Комментарии (0)