postgreSQL checkpoint logic

postgreSQL checkpoint logic


Попробую суммировать понимание. Покритикуйте.

Многие простые in-memory системы хранения key=value (или чего-то другого) работают с диском так: все изменения пишут в WAL, а всю свою память периодически (раз в час...день) пишут в новый спепшот (бекап, снимок). То есть, периодически создают новый файл и сбрасывают туда всю свою память в каком-то им понятном формате. Админы для страховки могут хранить несколько снепшотов, а совсем старые убивать. В каждом снепшоте запомнена позиция WAL, указывающая на место в WAL после последней (на помент начала создания снепшота) записи в WAL. При подъёме нам интересен только самый свежий не-битый, дописанный до конца, снимок и весь журнал (WAL) после позиции, указанной в снимке. Мы зачитываем весь свежайший снепшот в память и дальше накатываем все изменения из WAL, начиная с позиции, указанной в этом снепшоте, т.е. произошедшие после начала записи этого снепшота. Обычно снепшот пишут в fork(), имея таким образом замороженную, не изменяющуюся память процесса и не потребляя в 2 раза больше памяти (виртуальная память, ссылаемся физически на те же блоки памяти, а главный процесс сделает copy on write если надо).

В Postgres имеются похожие концепции. Снепшот (бекап, снимок) в postgres тоже существует - это набор файлов таблиц и индексов, по-сути по одному файлу на каждое B-Tree участвующее в жизни таблицы. И тоже существует идея о том, что периодически нужно сбрасывать состояние системы из ОЗУ в эти снепшоты, только не перезаписывая их и не формируя заново в новыйх файлах, а прямо в эти же существующие снепшоты, оперируя блоками (перезаписывая отдельные блоки B-Tree) в разных местах "снепшота" ("файлы таблицы"). Почему периодически, а не записывать блок сразу как он меняется - потому, что если некий блок B-Tree меняется 50K раз в секунду, то дешевле проделать много операций в ОЗУ, а потом когда-нибудь сбросить.

Далее как это происходит в Postgres.

Раз в час (например) наступает момент сброса состояния системы из ОЗУ в снепшот. Запускается process checkpointer.

1. В WAL пишется XLOG (элементарная запись WAL) с типом checkpoint. Она хранит позицию в WAL, которая была позицией записи на момент генерации данного XLOG. То есть, хранит адрес самой себя. Адресом в WAL можно назвать смещение в байтах от нулевого байта WAL (если представить WAL как бесконечно растущий от 0 файл, хотя на деле он сегментирован на куски для ротации). Номер XLOG - это LSN (Log Sequence Number).


2. Изменения (транзакции) не останавливаются. Любая транзакция, меняющая блок B-Tree, порождает XLOG запись в WAL соответствующего типа (insert/update/delete), но если после нашего XLOG checkpoint данный блок B-Tree меняется впервые, то вместо, например, XLOG insert пишется XLOG с полной копией этого блока, на которой (копии) уже выполнен этот insert. Повторная операция insert на данном блоке порождает обычный маленький XLOG insert. Сейчас не будем различать запись XLOG commit от XLOG rollback или XLOG insert, это не так важно для понимания - можно вообще считать, что у нас тупая key=value хранилка, куда приходят только 2 вида операций: set и delete без всяких там коммитов. Записи бывают только видов XLOG insert и XLOG delete, которые навсегда атомарно меняют данный блок B-Tree и являются коммитами в себе.


3. Фоновый процесс "process checkpointer" лениво (чтобы не убивать диск в полку) пишет по одному грязному блоку в старый "снепшот" на те места, откуда Postgres их взял чистыми. Никакого там shadow buffer как в InnoDB (или там его уже нет?) - пишем один раз сразу туда, куда надо. Пишутся только те блоки, которые были грязными на момент запуска "process checkpointer". Появившиеся новые грязные за время работы "process checkpointer" пока игнорируются -- прямо как делает тот fork()-нутый процесс в нашей примитивной системе, пишущий снепшот каждый раз в новый файл.


4. Не забываем, что параллельно WAL продолжает наполняться записями как описано в пункте (2).


5. Когда фоновый "process checkpointer" дописал все блоки, которые хотел (список "хотел" не меняется после начала его работы) (и, например сделал fsync()), то позиция последнего XLOG checkpoint пишется в файл "pg_control file". Логически это же можно было реализовать записью в тот же WAL пециальной записи вида "процедура checkpoint ОСИЛИЛА дойти до конца".



Теперь где умеет падать и подниматься.


1. При подъёме находим последний XLOG checkpoint. При этом точно известно, что "process checkpoint", записавший этот XLOG checkpoint успешно дошёл до конца, то надо спокойно начать накатывать изменения из WAL начиная с этого XLOG checkpoint. Почему это безопасно: рассмотрим блок 1, который на момент начала "process checkpoint" был уже грязный, но далее не менялся. Значит после данного XLOG checkpoint в WAL нет никаких записей про блок 1, в том числе и его бекапов. Но так как "process checkpoint" дошёл до конца, этот блок записан на своё место в снепшоте (ведь он был грязным на момент начала "process checkpoint"). Если всё же наш "блок 1" после XLOG checkpoint менялся, то в момент его первого (после начала "process checkpoint") изменения в WAL лёг полный бекап этого блока с произведённым изменением, которую мы оттуда и поднимем в память. В "файле-снепшоте" тоже есть копия "блока 1" - она туда была записана этим "process checkpoint", но записана в своём старом виде - тоже грязном, но без изменений, случившихся с ним после начала "process checkpoint", но поднимать нужно из WAL.


2. При подъёме находим последний XLOG checkpoint, но есть инфа, что "process checkpoint", который сделал эту запись не дошёл до конца. Это значит, что блоки, которые "process checkpoint" на момент своего старта взял как грязные и собирался записывать на свои места в снепшот (делать "чистыми"), записались туда частично, а какой-то записался наполовину (стал битым в снепшоте). Тогда, если начать накатывать изменения с последнего XLOG checkpoint, то для каких-то блоков, бывших грязными на момент записи этого XLOG checkpoint, но не получавших никаких изменений после него, у нас в снепшоте вероятно лежит мусор - их нельзя поднимать из снепшота. Контрольной суммой "блока 1" в снепшоте проблема не решается - она может быть хорошей, ведь "process checkpoint" мог помереть раньше, чем начал его трогать (перезаписывать) и там остался старый, но хороший блок. Нас не интересует старый хороший блок (т.е. "чистый"), ведь утеряны изменения, благодаря которым он считался грязным на момент появления в WAL нашей последней XLOG checkpoint. В общем, последний XLOG checkpoint бесполезен, нужно искать предыдущий. Только где взять информацию о том, закончился ли тот предыдущий...


3. Есть идея воспринимать каждый XLOG checkpoint как сигнал об успешном завершении предыдущего "process checkpoint", то есть синонимом XLOG-записи типа "я смог", но это неверно.


Report Page