Виртуальная файловая система SQLite: как устроен VFS

Если вы когда-нибудь задавались вопросом, как SQLite умудряется одинаково работать на Linux, Windows и экзотических встраиваемых платформах без единого #ifdef в бизнес-логике — ответ в VFS. Virtual Filesystem — это нижний уровень стека SQLite, который берёт на себя всё взаимодействие с операционной системой и позволяет остальным модулям об этом не думать.

Вся рубрика SQLite: уроки, инструменты и примеры

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


Если вы когда-нибудь задавались вопросом, как SQLite умудряется одинаково работать на Linux, Windows и экзотических встраиваемых платформах без единого #ifdef в бизнес-логике — ответ в VFS. Virtual Filesystem — это нижний уровень стека SQLite, который берёт на себя всё взаимодействие с операционной системой и позволяет остальным модулям об этом не думать

Схема стека SQLite VFS: SQL parser, planner и pager обращаются к SQLite VFS, а VFS вызывает OS API open read write fsync locks
VFS — нижний слой SQLite: верхние модули работают с общей абстракцией, а детали open, read, write, fsync и locks уходят в OS-specific код

VFS в контексте остальных компонентов SQLite

SQLite организован в виде стека модулей, где компоненты Tokenizer, Parser и Code Generator обрабатывают SQL-операторы и преобразуют их в исполняемые программы на языке виртуальной машины, известные как байткод. Эти три верхних уровня реализуют функцию sqlite3_prepare_v2() и возвращают подготовленный запрос

Модуль виртуальной машины выполняет байткод SQL-операторов. Модуль B-Tree организует файл базы данных в хранилище ключ/значение, обеспечивая упорядоченные ключи и логарифмическую производительность. Модуль Pager управляет загрузкой страниц файла базы данных в память и осуществляет транзакции, а также создаёт журналы для предотвращения повреждений при сбоях

Интерфейс ОС (OS Interface) предоставляет общий набор процедур, позволяющих адаптировать SQLite для работы в разных операционных системах

VFS взаимодействует с модулями SQLite, вызывая специфичный для ОС код для выполнения запросов, что обеспечивает его работу в разных средах

По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы

Несколько VFS в одном процессе

Стандартное дерево исходного кода SQLite включает как встроенные, так и альтернативные VFS, которые могут быть зарегистрированы во время запуска или выполнения с помощью sqlite3_vfs_register()

Несколько VFS могут быть зарегистрированы одновременно, каждый из которых имеет уникальное имя. Разные соединения в одном процессе могут использовать различные VFS, а при использовании команды ATTACH каждое подключение может иметь свой VFS

Схема выбора VFS в SQLite: sqlite3_open_v2, URI vfs и ATTACH выбирают зарегистрированные VFS unix, unix-excl или unix-none для файла demo.db
В одном процессе может быть несколько VFS, но одна активная база должна использовать совместимую locking-модель

Стандартные Unix VFS

Unix-сборки имеют несколько встроенных VFS, с VFS по умолчанию, названным «unix», который используется в большинстве приложений. Также могут быть другие VFS, в зависимости от параметров компиляции

  • unix-dotfile — использует блокировку через dot-файлы вместо консультативных блокировок POSIX
  • unix-excl — получает и удерживает эксклюзивную блокировку файлов базы данных, предотвращая доступ других процессов к базе данных; также хранит wal-index в куче, а не в общей памяти
  • unix-none — все операции блокировки файлов являются пустыми операциями (no-ops)
  • unix-namedsem — использует именованные семафоры для блокировки файлов; только для VXWorks

VFS в Unix имеют различные механизмы блокировки файлов, используя общую реализацию в файле os_unix.c. Если два процесса обращаются к одной базе данных с разными VFS, это может привести к повреждению данных, особенно при использовании 'unix-none', который вообще не выполняет блокировок

Стандартные Windows VFS

Windows-сборки также поставляются с несколькими встроенными VFS, с VFS по умолчанию, названным «win32», который используется в большинстве приложений, и другими VFS

  • win32-longpath — аналогичен «win32», за исключением того, что длина путей может достигать 65534 байт, тогда как в «win32» максимальная длина пути составляет 1040 байт
  • win32-none — все операции блокировки файлов являются пустыми операциями (no-ops)
  • win32-longpath-none — комбинация «win32-longpath» и «win32-none»: поддерживаются длинные пути, и все операции блокировки являются пустыми операциями

Как и в случае с unix, большая часть кода для различных Windows VFS является общей

Указание используемого VFS

В системах всегда есть VFS по умолчанию: «unix» для Unix и «win32» для Windows. Если не зарегистрировать другой VFS, новые соединения будут использовать VFS по умолчанию

VFS по умолчанию можно изменить, зарегистрировав его заново через sqlite3_vfs_register(), установив второй параметр в 1. Например, можно использовать 'unix-nolock' вместо 'unix'

sqlite3_vfs_register(sqlite3_vfs_find("unix-nolock"), 1);

Альтернативный VFS также можно указать в качестве 4-го параметра функции sqlite3_open_v2(). Например:

int rc = sqlite3_open_v2("demo.db", &db, SQLITE_OPEN_READWRITE, "unix-nolock");

Если используются URI-имена файлов, альтернативный VFS можно указать через параметр vfs= в URI. Этот способ работает с функциями sqlite3_open(), sqlite3_open16(), sqlite3_open_v2() и при добавлении базы данных с помощью ATTACH

ATTACH 'file:demo2.db?vfs=unix-none' AS demo2;

VFS, указанный в URI, имеет наивысший приоритет. Далее следует VFS, указанный в качестве четвёртого аргумента sqlite3_open_v2(). VFS по умолчанию используется, если VFS не указан иным способом

VFS-прослойки

С точки зрения верхних уровней стека SQLite, каждый открытый файл базы данных использует ровно один VFS. Но на практике конкретный VFS может быть лишь тонкой обёрткой вокруг другого VFS, выполняющего реальную работу. Такой VFS-обёртку принято называть «прослойкой» (shim)

Простым примером прослойки является VFS «vfstrace». Это VFS (реализованный в файле исходного кода vfstrace.c), который записывает сообщение, связанное с каждым вызовом метода VFS, в файл журнала, а затем передаёт управление другому VFS для выполнения фактической работы

Схема VFS shim в SQLite: SQLite вызывает vfstrace, прослойка логирует xOpen и xRead и делегирует вызовы реальному unix VFS
VFS-прослойка выглядит для SQLite как обычный VFS, но внутри добавляет свою логику и передаёт работу реальному нижнему VFS

Примеры VFS в исходном коде SQLite

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

  • appendvfs.c — этот VFS позволяет добавлять базу данных SQLite в конец другого файла. Это можно использовать, например, для добавления базы данных SQLite в конец исполняемого файла таким образом, чтобы при запуске он мог легко найти добавленную базу данных. Командная оболочка будет использовать этот VFS при запуске с параметром --append, а её команда .archive будет использовать его при указании флага --append.
  • test_demovfs.c — этот файл реализует очень простой VFS с именем «demo», использующий функции POSIX, такие как open(), read(), write(), fsync(), close(), sleep(), time() и так далее. Этот VFS работает только в системах unix, однако не предназначен для замены стандартного VFS «unix». VFS «demo» намеренно сделан очень простым, чтобы его можно было использовать в качестве учебного пособия или шаблона для создания других VFS или переноса SQLite на новые операционные системы.
  • test_quota.c — этот файл реализует прослойку под названием «quota», которая применяет ограничения на совокупный размер файлов для набора файлов базы данных. Вспомогательный интерфейс используется для определения «групп квот» — наборов файлов, имена которых соответствуют шаблону GLOB. Отслеживается сумма размеров всех файлов в каждой группе квот, и если эта сумма превышает порог, определённый для группы, вызывается функция обратного вызова. Эта функция обратного вызова может либо увеличить порог, либо вызвать сбой операции с ошибкой SQLITE_FULL. Одним из применений этой прослойки является применение ограничений ресурсов для баз данных приложений в Firefox.
  • test_multiplex.c — этот файл реализует прослойку, позволяющую файлам базы данных превышать максимальный размер файла базовой файловой системы. Прослойка создаёт видимость использования очень больших файлов, тогда как в действительности каждый такой большой файл разбивается на множество меньших файлов в базовой системе. Эта прослойка использовалась, например, для того, чтобы базы данных могли превышать 2 гибибайта на файловых системах FAT16.
  • test_onefile.c — этот файл реализует демонстрационный VFS с именем «fs», показывающий, как SQLite можно использовать на встраиваемом устройстве, лишённом файловой системы. Содержимое записывается непосредственно на базовый носитель. VFS, созданный на основе этого демонстрационного кода, может использоваться устройством с ограниченным объёмом флэш-памяти, чтобы SQLite выступал в роли файловой системы для флэш-памяти устройства.
  • test_journal.c — этот файл реализует прослойку, используемую при тестировании SQLite, которая проверяет, что база данных и журнал отката записываются в правильном порядке и «синхронизируются» в соответствующее время, чтобы гарантировать возможность восстановления базы данных после потери питания или жёсткого сброса в любой момент. Прослойка проверяет несколько инвариантов в работе баз данных и журналов отката и генерирует исключения при нарушении любого из этих инвариантов. Запуск большого набора тестовых случаев с использованием этой прослойки обеспечивает дополнительную уверенность в том, что базы данных SQLite не будут повреждены из-за неожиданных сбоев питания или сброса устройства.
  • test_vfs.c — этот файл реализует прослойку, которую можно использовать для симуляции сбоев файловой системы. Эта прослойка используется при тестировании для проверки того, что SQLite адекватно реагирует на неисправности оборудования или другие условия ошибок, такие как нехватка места в файловой системе, которые сложно воспроизвести на реальной системе.

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

Реализация нового VFS

Новый VFS реализуется путём создания подклассов трёх объектов: sqlite3_vfs, sqlite3_io_methods и sqlite3_file. Мой совет — начинать с изучения test_demovfs.c: это намеренно упрощённая реализация, которая хорошо показывает минимально необходимый каркас

Объект sqlite3_vfs определяет имя VFS и основные методы, реализующие интерфейс с операционной системой: проверку существования файлов, удаление файлов, создание файлов и их открытие для чтения и/или записи, преобразование имён файлов в их каноническую форму. Объект sqlite3_vfs также содержит методы для получения случайных данных от операционной системы, для приостановки процесса (сна) и для определения текущей даты и времени

Объект sqlite3_file представляет открытый файл. Метод xOpen объекта sqlite3_vfs создаёт объект sqlite3_file при открытии файла. Объект sqlite3_file отслеживает состояние файла, пока он открыт

Объект sqlite3_io_methods содержит методы, используемые для взаимодействия с открытым файлом. Каждый sqlite3_file содержит указатель на объект sqlite3_io_methods, соответствующий представляемому им файлу

Объект sqlite3_io_methods содержит методы для выполнения таких операций, как чтение и запись файла, усечение файла, сброс изменений в постоянное хранилище, определение размера файла, блокировка и разблокировка файла, а также закрытие файла и уничтожение объекта sqlite3_file

Написание кода для нового VFS включает создание подкласса объекта sqlite3_vfs и последующую регистрацию этого объекта VFS с помощью вызова sqlite3_vfs_register(). Реализация VFS также предоставляет подклассы для sqlite3_file и sqlite3_io_methods, однако эти объекты не регистрируются в SQLite напрямую

Вместо этого объект sqlite3_file возвращается из метода xOpen объекта sqlite3_vfs, а объект sqlite3_file указывает на экземпляр объекта sqlite3_io_methods

Схема реализации собственного VFS SQLite: sqlite3_vfs, sqlite3_file и sqlite3_io_methods с методами xOpen, xRead, xWrite, xSync и xLock
Минимальный каркас собственного VFS держится на трёх объектах: sqlite3_vfs, sqlite3_file и sqlite3_io_methods

Типичные ошибки при работе с VFS

На практике я замечал несколько повторяющихся проблем, с которыми сталкиваются разработчики при работе с VFS

Карта типичных ошибок SQLite VFS: mixing VFS, unix-none, URI priority, bad xOpen и early register
Главные ошибки VFS почти всегда связаны с locking, приоритетом выбора VFS и неправильной связкой sqlite3_file с io_methods

Смешивание несовместимых VFS. Если два процесса обращаются к одной и той же базе данных, используя разные unix VFS (например, один через «unix», другой через «unix-dotfile»), они могут не видеть блокировки друг друга. Результатом становится повреждение базы данных. Единственная безопасная пара для совместного использования — «unix» и «unix-excl»

Использование unix-none в многопроцессной среде. VFS «unix-none» полностью отключает блокировки. Это допустимо только тогда, когда гарантирован единственный пользователь базы данных. В любой другой ситуации это прямой путь к повреждению данных

Неправильный порядок приоритетов при выборе VFS. Разработчики иногда удивляются, почему VFS, переданный в sqlite3_open_v2(), не применяется, хотя в URI уже указан другой VFS. Правило простое: URI имеет наивысший приоритет, затем идёт четвёртый аргумент sqlite3_open_v2(), и только потом — VFS по умолчанию

Регистрация VFS без учёта порядка инициализации. Если VFS регистрируется до инициализации SQLite, это может привести к непредсказуемому поведению. Регистрацию через sqlite3_vfs_register() следует выполнять после того, как библиотека полностью инициализирована

Неверная реализация метода xOpen. При создании собственного VFS метод xOpen должен корректно возвращать объект sqlite3_file с заполненным указателем на sqlite3_io_methods. Если этот указатель не установлен или указывает на неполную структуру, SQLite будет вызывать неинициализированные функции, что приведёт к аварийному завершению

Часто задаваемые вопросы о VFS

Что такое VFS в SQLite и зачем он нужен? VFS (Virtual Filesystem) — это нижний уровень стека SQLite, который абстрагирует все операции с операционной системой: работу с файлами, блокировки, получение времени и случайных данных. Благодаря VFS SQLite можно перенести на новую платформу, написав только этот один уровень

Можно ли использовать несколько VFS одновременно в одном приложении? Да. В рамках одного процесса можно зарегистрировать несколько VFS с разными именами. Разные соединения с базой данных могут использовать разные VFS, а при использовании ATTACH каждая подключённая база данных может работать через свой VFS

Как указать конкретный VFS при открытии базы данных? Есть три способа: передать имя VFS четвёртым аргументом в sqlite3_open_v2(), указать параметр vfs= в URI-имени файла, либо сделать нужный VFS дефолтным через sqlite3_vfs_register() со вторым параметром равным 1. URI имеет наивысший приоритет

Что такое VFS-прослойка (shim) и чем она отличается от обычного VFS? Прослойка — это VFS, который не выполняет работу самостоятельно, а делегирует её другому VFS, добавляя при этом дополнительную логику: трассировку вызовов, квоты на размер файлов, мультиплексирование или симуляцию сбоев. С точки зрения верхних уровней SQLite прослойка неотличима от обычного VFS

Какие три объекта нужно реализовать для создания собственного VFS? sqlite3_vfs — определяет имя VFS и методы взаимодействия с ОС. sqlite3_file — представляет открытый файл и отслеживает его состояние. sqlite3_io_methods — содержит методы для операций с открытым файлом: чтение, запись, блокировка, усечение и закрытие. Объект sqlite3_vfs регистрируется явно через sqlite3_vfs_register(), остальные два возвращаются через метод xOpen

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x