ZFS: Copy-on-Write filesystem
2011-10-05 11:08 pm1. What's the prob, bro?
Обеспечение целостности данных в современных реляционных СУБД сделано, на самом деле, достаточно по-идиотски.
Когда работают транзакции и накапливаются изменения в страницах, СУБД не пишет сразу в мастер-файл, а накапливает в журнале, опережающем запись (WAL - write ahead log). Периодически файловой системе даётся команда этот журнал слить на диск, и когда в нём накопится достаточно много записей, начинается их перенос в мастер-файл, а затем для мастер-файла делается полный флаш. Таким образом, данные на диск фактически пишутся дважды, и дважды делается полный флаш. Иногда СУБД настраивается так, чтобы рядом идущие транзакции "слипались". В этом случае из ACID выпадает буква D. Сути это впрочем не меняет: данные всё равно пишутся дважды.
Далее в игру вступает файловая система. Обычно журналируемая. Чаще всего журналируются только метаданные, но бывает что админы включают журналирование и для данных - не всякая СУБД обрадуется, увидев в своём журнале непонятный мусор. В любом случае, в файловой системе тоже есть свой WAL и она тоже ждёт полного его слива на диск, прежде чем начать обновлять собственно данные и метаданные. Ну и после обновления последних тоже без флаша дело не обходится. Отдельно доставляет реализация линуксовой ext3, которая сливает на диск метаданные всех файлов, даже если вы делаете fsync(2) на каком-то одном файловом fd.
Если всё просуммировать, получается, что при коммите данных на диск полный флаш делается четыре раза.
Подытожим:
- IOPS жёсткого диска ограничен физикой на цифре порядка сотни
- главным тормозным фактором в СУБД является IOPS жёсткого диска
- не у всех есть SSD или RAID-контроллер с battery backed cache, чтобы умножить IOPS на сто, так что приходится жертвовать D в ACID
- с такой практикой журналирования, и так небольшой IOPS делится на четыре
- четырёхкратная запись вместо однократной срок службы SSD тоже не увеличивает
- по сути, это форменный идиотизм
2. Как можно избежать четырёхкратной записи и не угробить данные?
Когда-то была такая перспективная файловая система UDF. Она и сейчас используется для оптических дисков. Нет, я не предлагаю использовать её вместо базы данных. Просто в ней впервые была реализована идея пакетной записи изменений: все изменения пишутся непрерывным куском, а в этом куске можно найти новый суперблок, новый корневой каталог, изменившиеся каталоги и изменившиеся части файлов. Те части, что не изменились, хранятся в предыдущих сессиях и на них идут ссылки из новых сессий.
Примерно то же самое предлагает нам Sun/Oracle в ZFS (не считая встроенного LVM и RAID'а): copy-on-write filesystem.

Данные какое-то время накапливаются в кэше в ОЗУ и пишутся на диск в фоновом режиме без особой спешки. Последним пишется суперблок, после чего файловая система как бы атомарно заменяется на новую, и старые блоки можно трактовать как свободные.
Ещё в zfs есть снэпшоты и клоны. Они отличаются тем, что снэпшот - это такой снимок файловой системы, который можно только читать, а в клон можно ещё и делать запись. Суть одна и та же: аналог fork(2) для файловой системы.
Снапшоты работают действительно быстро. Вы и сами можете видеть из рисунка выше, что дешевле делать снэпшот, чем его не делать - в этом случае экономится время на освобождение блоков со старой копией изменённых данных.
А ведь СУБД могла бы не писать журналы по три раза при коммите, как сейчас, а действовать намного проще:
1) сделать снэпшот ФС
2) по всем кэш-промахам операций чтения читать снэпшот
3) накопить пачку изменённых страниц для записи, передать их в пишущий поток
4) в пишущем потоке не спеша записать изменённые страницы на диск и сделать флаш ФС
5) пишущий поток делает новый снэпшот ФС
6) читающий поток переключается на чтение нового снэпшота и помечает изменившиеся, но недавно записанные на диск страницы как безопасные для вытеснения из кэша
7) пишущий поток окончательно удаляет старый снэпшот ФС
8) повтор с п.3
Очень просто. Почти как fork(2) и фоновая запись на диск in-memory database в Redis.
Но для работы такой СУБД нужна особая файловая система - COW filesystem - ZFS, btrfs или NILFS2.
Это новый класс файловых систем, который расширяет философию системного вызова fork(2) на файловые системы и позволяет отказаться от журналов.
На самом деле, ZFS всё ещё содержит журнал - ZIL (ZFS Intent Log) - он нужен для POSIX-совместимости - глюпый POSIX-код может дрючить fsync(2) по сто раз в секунду, и чтобы постоянно не перезаписывать корень и все каталоги по пути, куча мелких изменений сливаются воедино, пока драйвер ФС не решит, что пора обновляться "по крупному". Обычно он это делает раз в 5-30 секунд. ZIL можно отключить, но драйвер ФС всё равно не будет сохранять данные на диск до самого корня на каждом fsync(2). Таким образом, если пропадёт питание, вы можете потерять последние изменения, но целостность файловой системы всё равно будет не нарушена.
А вот если вы ещё и ответвили снэпшот в подходящий момент прямо перед потерей питания, тогда и на прикладном уровне с целостностью данных будет полный порядок. Изучение исходников ZFS показывает, что когда делается снэпшот, принудительно коммитится транзакционная группа (TXG) аж до самого суперблока и производится ожидание подтверждения записи от дискового пула.
Таким образом, сделать флаш всей ФС из юзерспейса достаточно просто - надо только отправить IOCTL-запрос на изготовление нового снэпшота. По возврату из IOCTL-вызова ZFS_IOC_SNAPSHOT в юзерспейс, все страницы снэпшота будут закоммичены на диск. ZIL при этом тоже пишется на диск, так что его в этой модели использования ФС лучше отключить.
Так работает файловая система XXI века. Без журнала, без флаша через fsync(2), и без fsck. Вместо всего этого у нас COW и снэпшоты.
Обеспечение целостности данных в современных реляционных СУБД сделано, на самом деле, достаточно по-идиотски.
Когда работают транзакции и накапливаются изменения в страницах, СУБД не пишет сразу в мастер-файл, а накапливает в журнале, опережающем запись (WAL - write ahead log). Периодически файловой системе даётся команда этот журнал слить на диск, и когда в нём накопится достаточно много записей, начинается их перенос в мастер-файл, а затем для мастер-файла делается полный флаш. Таким образом, данные на диск фактически пишутся дважды, и дважды делается полный флаш. Иногда СУБД настраивается так, чтобы рядом идущие транзакции "слипались". В этом случае из ACID выпадает буква D. Сути это впрочем не меняет: данные всё равно пишутся дважды.
Далее в игру вступает файловая система. Обычно журналируемая. Чаще всего журналируются только метаданные, но бывает что админы включают журналирование и для данных - не всякая СУБД обрадуется, увидев в своём журнале непонятный мусор. В любом случае, в файловой системе тоже есть свой WAL и она тоже ждёт полного его слива на диск, прежде чем начать обновлять собственно данные и метаданные. Ну и после обновления последних тоже без флаша дело не обходится. Отдельно доставляет реализация линуксовой ext3, которая сливает на диск метаданные всех файлов, даже если вы делаете fsync(2) на каком-то одном файловом fd.
Если всё просуммировать, получается, что при коммите данных на диск полный флаш делается четыре раза.
Подытожим:
- IOPS жёсткого диска ограничен физикой на цифре порядка сотни
- главным тормозным фактором в СУБД является IOPS жёсткого диска
- не у всех есть SSD или RAID-контроллер с battery backed cache, чтобы умножить IOPS на сто, так что приходится жертвовать D в ACID
- с такой практикой журналирования, и так небольшой IOPS делится на четыре
- четырёхкратная запись вместо однократной срок службы SSD тоже не увеличивает
- по сути, это форменный идиотизм
2. Как можно избежать четырёхкратной записи и не угробить данные?
Когда-то была такая перспективная файловая система UDF. Она и сейчас используется для оптических дисков. Нет, я не предлагаю использовать её вместо базы данных. Просто в ней впервые была реализована идея пакетной записи изменений: все изменения пишутся непрерывным куском, а в этом куске можно найти новый суперблок, новый корневой каталог, изменившиеся каталоги и изменившиеся части файлов. Те части, что не изменились, хранятся в предыдущих сессиях и на них идут ссылки из новых сессий.
Примерно то же самое предлагает нам Sun/Oracle в ZFS (не считая встроенного LVM и RAID'а): copy-on-write filesystem.

Данные какое-то время накапливаются в кэше в ОЗУ и пишутся на диск в фоновом режиме без особой спешки. Последним пишется суперблок, после чего файловая система как бы атомарно заменяется на новую, и старые блоки можно трактовать как свободные.
Ещё в zfs есть снэпшоты и клоны. Они отличаются тем, что снэпшот - это такой снимок файловой системы, который можно только читать, а в клон можно ещё и делать запись. Суть одна и та же: аналог fork(2) для файловой системы.
Снапшоты работают действительно быстро. Вы и сами можете видеть из рисунка выше, что дешевле делать снэпшот, чем его не делать - в этом случае экономится время на освобождение блоков со старой копией изменённых данных.
А ведь СУБД могла бы не писать журналы по три раза при коммите, как сейчас, а действовать намного проще:
1) сделать снэпшот ФС
2) по всем кэш-промахам операций чтения читать снэпшот
3) накопить пачку изменённых страниц для записи, передать их в пишущий поток
4) в пишущем потоке не спеша записать изменённые страницы на диск и сделать флаш ФС
5) пишущий поток делает новый снэпшот ФС
6) читающий поток переключается на чтение нового снэпшота и помечает изменившиеся, но недавно записанные на диск страницы как безопасные для вытеснения из кэша
7) пишущий поток окончательно удаляет старый снэпшот ФС
8) повтор с п.3
Очень просто. Почти как fork(2) и фоновая запись на диск in-memory database в Redis.
Но для работы такой СУБД нужна особая файловая система - COW filesystem - ZFS, btrfs или NILFS2.
Это новый класс файловых систем, который расширяет философию системного вызова fork(2) на файловые системы и позволяет отказаться от журналов.
На самом деле, ZFS всё ещё содержит журнал - ZIL (ZFS Intent Log) - он нужен для POSIX-совместимости - глюпый POSIX-код может дрючить fsync(2) по сто раз в секунду, и чтобы постоянно не перезаписывать корень и все каталоги по пути, куча мелких изменений сливаются воедино, пока драйвер ФС не решит, что пора обновляться "по крупному". Обычно он это делает раз в 5-30 секунд. ZIL можно отключить, но драйвер ФС всё равно не будет сохранять данные на диск до самого корня на каждом fsync(2). Таким образом, если пропадёт питание, вы можете потерять последние изменения, но целостность файловой системы всё равно будет не нарушена.
А вот если вы ещё и ответвили снэпшот в подходящий момент прямо перед потерей питания, тогда и на прикладном уровне с целостностью данных будет полный порядок. Изучение исходников ZFS показывает, что когда делается снэпшот, принудительно коммитится транзакционная группа (TXG) аж до самого суперблока и производится ожидание подтверждения записи от дискового пула.
Таким образом, сделать флаш всей ФС из юзерспейса достаточно просто - надо только отправить IOCTL-запрос на изготовление нового снэпшота. По возврату из IOCTL-вызова ZFS_IOC_SNAPSHOT в юзерспейс, все страницы снэпшота будут закоммичены на диск. ZIL при этом тоже пишется на диск, так что его в этой модели использования ФС лучше отключить.
Так работает файловая система XXI века. Без журнала, без флаша через fsync(2), и без fsck. Вместо всего этого у нас COW и снэпшоты.