Атомарный фиксация в SQLite
Содержание
1. Введение
Важной особенностью транзакционных баз данных, таких как SQLite, является «атомарная фиксация». Атомарная фиксация означает, что все изменения в базе данных в рамках одной транзакции происходят или все, или ни одно. При атомарной фиксации кажется, что многочисленные записи в различные части файла базы данных происходят мгновенно и одновременно. Однако реальное оборудование последовательно выполняет запись на долговременное хранение, а запись одного сектора занимает определенное время. Поэтому невозможно по-настоящему записывать множество различных секторов файла базы данных одновременно и/или мгновенно. Однако логика атомарной фиксации в SQLite делает так, что кажется, что изменения транзакции записываются мгновенно и одновременно.
SQLite обладает важным свойством: транзакции кажутся атомарными даже если транзакция прерывается сбоем операционной системы или отключением питания.
В этой статье описываются методы, используемые SQLite для создания иллюзии атомарной фиксации.
Информация в этой статье применяется только в том случае, если SQLite работает в «режиме отката», то есть когда SQLite не использует журнал предварительной записи. SQLite все еще поддерживает атомарную фиксацию при включенном журнале предварительной записи, но для достижения атомарной фиксации используется другой механизм, чем описанный в этой статье. Более подробная информация о том, как SQLite поддерживает атомарную фиксацию в этом контексте, содержится в документации по журналу предварительной записи.
2. Предположения о работе оборудования
В этой статье мы будем называть устройство долговременного хранения «диском», даже если это может быть флеш-память.
Мы предполагаем, что диск записывается блоками, которые мы называем «сектором». Невозможно изменить какую-либо часть диска, меньшую, чем сектор. Чтобы изменить часть диска, меньшую, чем сектор, необходимо прочитать весь сектор, содержащий нужную часть, внести изменения и записать весь сектор заново.
На традиционном винчестере сектор является минимальной единицей передачи как для чтения, так и для записи. Однако на флеш-памяти минимальный размер чтения, как правило, значительно меньше минимального размера записи. SQLite интересует только минимальный размер записи, поэтому в этой статье, когда мы говорим «сектор», мы имеем в виду минимальное количество данных, которое можно записать на долговременное хранение за один раз.
До версии SQLite 3.3.14 предполагался размер сектора 512 байт во всех случаях. Существовала опция компиляции для изменения этого значения, но код никогда не тестировался с большим значением. Предположение о секторе размером 512 байт казалось разумным, поскольку до недавнего времени все жесткие диски использовали внутренний сектор размером 512 байт. Однако недавно наблюдается тенденция к увеличению размера сектора дисков до 4096 байт. Кроме того, размер сектора флеш-памяти обычно больше 512 байт. По этим причинам, начиная с версии 3.3.14, в SQLite имеется метод в слое интерфейса операционной системы, который запросом к файловой системе определяет фактический размер сектора. Как реализовано на данный момент (версия 3.5.0), этот метод все еще возвращает жестко закодированное значение 512 байт, так как нет стандартного способа определения фактического размера сектора ни в Unix, ни в Windows. Однако метод доступен производителям встраиваемых устройств для настройки в соответствии с их потребностями. И мы оставили возможность в будущем реализовать более информативный метод в Unix и Windows.
SQLite традиционно предполагает, что запись сектора не является атомарной. Однако SQLite всегда предполагает, что запись сектора линейна. Под «линейностью» мы понимаем, что SQLite предполагает, что при записи сектора оборудование начинает с одного конца данных и записывает байт за байтом до другого конца. Запись может выполняться с начала до конца или с конца до начала. Если во время записи сектора произойдет отключение питания, то возможно, часть сектора была изменена, а другая часть осталась неизменной. Основное предположение SQLite состоит в том, что если любая часть сектора изменена, то либо первый, либо последний байты будут изменены. Таким образом, оборудование никогда не начнет запись сектора посередине и не будет двигаться к концам.
В предыдущем абзаце указано, что SQLite не предполагает, что записи секторов являются атомарными. Это верно по умолчанию. Но начиная с версии SQLite 3.5.0, появился новый интерфейс под названием Виртуальная файловая система (VFS). VFS — единственный способ, которым SQLite взаимодействует с файловой системой. Поставляются предустановленные реализации VFS для Unix и Windows, а также есть механизм создания новых пользовательских реализаций VFS во время выполнения. В этом новом интерфейсе VFS имеется метод xDeviceCharacteristics. Этот метод запросом к файловой системе определяет различные свойства и характеристики, которые может демонстрировать файловая система. Метод xDeviceCharacteristics может указать, что записи секторов атомарны, и если это так, SQLite попытается воспользоваться этим фактом. Но стандартный метод xDeviceCharacteristics для Unix и Windows не указывает атомарные записи секторов, поэтому эти оптимизации обычно не применяются.
SQLite предполагает, что операционная система буферизует записи и что запрос на запись вернется, прежде чем данные будут фактически сохранены на долговременном хранилище. SQLite далее предполагает, что операционная система может переупорядочивать операции записи. По этой причине SQLite выполняет операцию «очистки» или «fsync» в ключевых точках. SQLite предполагает, что операция очистки или fsync не вернётся, пока все ожидающие операции записи для файла, который очищается, не завершатся. Нам сообщают, что примитивы очистки и fsync не работают должным образом на некоторых версиях Windows и Linux. К сожалению, это открывает SQLite возможность повреждения базы данных после сбоя питания в середине процесса фиксации. Однако SQLite ничего не может сделать для проверки или устранения такой ситуации. SQLite предполагает, что операционная система, на которой оно работает, функционирует согласно спецификации. Если это не совсем так, то надеемся, что у вас не слишком часто будет отключение питания.
SQLite предполагает, что когда файл увеличивается в длине, новое дисковое пространство первоначально содержит мусор, а затем заполняется данными, фактически записанными. Другими словами, SQLite предполагает, что размер файла обновляется до содержимого файла. Это пессимистическое предположение, и SQLite приходится выполнять дополнительные действия, чтобы убедиться, что это не приведет к повреждению базы данных, если произойдет отключение питания между временем увеличения размера файла и временем записи нового содержимого. Метод xDeviceCharacteristics объекта VFS может указывать, что файловая система всегда записывает данные перед обновлением размера файла. (Это свойство SQLITE_IOCAP_SAFE_APPEND для тех читателей, которые смотрят на код.) Когда метод xDeviceCharacteristics указывает, что содержимое файла записывается перед увеличением размера файла, SQLite может отказаться от некоторых своих педантичных шагов защиты базы данных и тем самым уменьшить количество дисковых операций ввода-вывода, необходимых для выполнения фиксации. Однако текущая реализация не делает таких предположений для стандартных VFS для Windows и Unix.
SQLite предполагает, что удаление файла атомарно с точки зрения процесса пользователя. Это означает, что если SQLite запросит удаление файла, и питание отключится во время операции удаления, после восстановления питания либо файл будет существовать полностью со всем исходным содержимым без изменений, либо файл вообще не будет виден в файловой системе. Если после восстановления питания файл будет удален только частично, если часть его данных будет изменена или удалена, или файл будет усечен, но не полностью удален, то, вероятно, произойдет повреждение базы данных.
SQLite предполагает, что обнаружение и/или исправление битовых ошибок, вызванных космическими лучами, тепловым шумом, квантовыми флуктуациями, ошибками драйверов устройств или другими механизмами, является обязанностью базового оборудования и операционной системы. SQLite не добавляет никакой избыточности в файл базы данных для обнаружения повреждений или ошибок ввода-вывода. SQLite предполагает, что данные, которые он считывает, являются точно теми же данными, которые он ранее записал.
По умолчанию SQLite предполагает, что вызов операционной системы для записи диапазона байтов не повредит и не изменит никакие байты за пределами этого диапазона, даже если во время этой записи произойдет отключение питания или сбой ОС. Мы называем это свойством «безопасная перезапись при отключении питания». До версии 3.7.9 (2011-11-01) SQLite не предполагал безопасной перезаписи при отключении питания. Но с увеличением стандартного размера сектора с 512 до 4096 байт на большинстве дисковых накопителей стало необходимо предполагать безопасную перезапись при отключении питания для поддержания исторических уровней производительности, поэтому безопасная перезапись при отключении питания предполагается по умолчанию в последних версиях SQLite. Предположение о свойстве безопасной перезаписи при отключении питания может быть отключено во время компиляции или во время выполнения, если это необходимо. См. документацию о безопасной перезаписи при отключении питания для получения дополнительной информации.
3. Фиксация транзакции для одного файла
Мы начинаем с обзора шагов, которые SQLite выполняет для выполнения атомарной фиксации транзакции для одного файла базы данных. Подробности используемых форматов файлов для защиты от повреждений при отключении питания и методы выполнения атомарной фиксации для нескольких баз данных обсуждаются в последующих разделах.
3.1. Начальное состояние
Состояние компьютера при первом открытии подключения к базе данных представлено на диаграмме справа. Область диаграммы в правой части (подписанная «Диск») представляет информацию, хранящуюся на устройстве массового хранения. Каждый прямоугольник — это сектор. Синий цвет обозначает, что секторы содержат исходные данные. Средняя область — это кэш диска операционной системы. В начале нашего примера кэш пустой, и это показано пустым прямоугольниками в кэше диска. Левая область диаграммы показывает содержимое памяти для процесса, использующего SQLite. Подключение к базе данных только что было открыто, и пока никакая информация не была прочитана, поэтому пользовательское пространство пусто.
3.2. Получение блокировки на чтение
Прежде чем SQLite сможет записать в базу данных, он должен сначала прочитать базу данных, чтобы увидеть, что там уже есть. Даже если это просто добавление новых данных, SQLite все равно должен прочитать схему базы данных из таблицы «sqlite_schema», чтобы знать, как парсить инструкции INSERT и определить, где в файле базы данных следует хранить новую информацию.
Первый шаг к чтению из файла базы данных — получение общей блокировки на файле базы данных. «Общее» блокирование позволяет двум или более подключениям к базе данных одновременно читать из файла базы данных. Но общая блокировка предотвращает запись в файл базы данных другому подключению к базе данных, пока мы его читаем. Это необходимо, потому что если другое подключение к базе данных записывало в файл базы данных в то же время, когда мы читали из файла базы данных, мы могли бы прочитать некоторые данные до изменения и другие данные после изменения. Это дало бы впечатление, что изменение, сделанное другим процессом, не атомарно.
Обратите внимание, что общая блокировка находится в кэше диска операционной системы, а не на самом диске. Файловые блокировки в основном представляют собой флаги в ядре операционной системы. (Подробности зависят от конкретного интерфейса слоя ОС.) Таким образом, блокировка мгновенно исчезнет, если операционная система потерпит сбой или произойдет отключение питания. Обычно блокировка также исчезает, если процесс, создавший блокировку, завершает работу.
3.3. Чтение информации из базы данных
После получения общей блокировки мы можем начать чтение информации из файла базы данных. В этом сценарии мы предполагаем холодный кэш, поэтому информация должна быть сначала прочитана с устройства массового хранения в кэш операционной системы, а затем передана из кэша операционной системы в пользовательское пространство. При последующих чтениях вся или часть информации может уже находиться в кэше операционной системы, и поэтому потребуется только передача в пользовательское пространство.
Обычно считываются только подмножество страниц в файле базы данных. В этом примере показано чтение трех страниц из восьми. В типичном приложении база данных будет содержать тысячи страниц, а запрос обычно затрагивает лишь небольшую часть этих страниц.
3.4. Получение резервной блокировки
Прежде чем вносить изменения в базу данных, SQLite сначала получает «резервную» блокировку на файле базы данных. Резервная блокировка аналогична общей блокировке в том, что и резервная, и общая блокировки позволяют другим процессам читать из файла базы данных. Одна резервная блокировка может существовать вместе с несколькими общими блокировками от других процессов. Однако на файле базы данных может быть только одна резервная блокировка. Следовательно, только один процесс может пытаться записать в базу данных одновременно.
Идея резервной блокировки заключается в том, что она сигнализирует о намерении процесса в ближайшем будущем изменить файл базы данных, но изменения еще не начаты. И поскольку изменения еще не начаты, другие процессы могут продолжать читать из базы данных. Однако ни один другой процесс не должен также начинать попытку записи в базу данных.
3.5. Создание файла журнала отката
Перед внесением каких-либо изменений в файл базы данных SQLite сначала создает отдельный файл журнала отката и записывает в этот журнал отката исходное содержимое страниц базы данных, которые будут изменены. Идея журнала отката заключается в том, что он содержит всю информацию, необходимую для восстановления состояния базы данных до исходного состояния.
Журнал отката содержит небольшой заголовок (показанный зеленым цветом на диаграмме), в котором записывается исходный размер файла базы данных. Таким образом, если изменение приведет к увеличению размера файла базы данных, мы все равно будем знать исходный размер базы данных. Номер страницы хранится вместе с каждой страницей базы данных, которая записывается в журнал отката.
При создании нового файла большинство настольных операционных систем (Windows, Linux, Mac OS X) фактически ничего не записывают на диск. Новый файл создается только в кэше диска операционной системы. Файл не создается на устройстве массового хранения до тех пор, пока операционная система не найдет свободного времени. Это создает у пользователей впечатление, что ввод-вывод происходит намного быстрее, чем при реальных дисковых операциях ввода-вывода. Мы иллюстрируем эту идею на диаграмме справа, показывая, что новый журнал отката появляется только в кэше диска операционной системы, а не на самом диске.
3.6. Изменение страниц базы данных в пользовательском пространстве
После того как исходное содержимое страницы было сохранено в журнале отката, страницы могут быть изменены в памяти пользователя. Каждое подключение к базе данных имеет свою собственную частную копию пользовательского пространства, поэтому изменения, внесенные в пользовательское пространство, видны только подключению к базе данных, которое их вносит. Другие подключения к базе данных по-прежнему видят информацию в буферах кэша диска операционной системы, которые еще не были изменены. Итак, даже если один процесс занят изменением базы данных, другие процессы могут продолжать читать свои собственные копии исходного содержимого базы данных.
3.7. Выгрузка файла журнала отката на устройство массового хранения
Следующий шаг — выгрузка содержимого файла журнала отката на энергонезависимое хранилище. Как мы увидим позже, это критически важный шаг для обеспечения того, что база данных сможет пережить непредвиденную потерю питания. Этот шаг также занимает много времени, поскольку запись на энергонезависимое хранилище обычно является медленной операцией.
Этот шаг обычно более сложен, чем просто выгрузка журнала отката на диск. На большинстве платформ требуются две отдельные операции выгрузки (или fsync()). Первая выгрузка записывает основное содержимое журнала отката. Затем заголовок журнала отката изменяется, чтобы показать количество страниц в журнале отката. Затем заголовок выгружается на диск. Подробности о том, почему мы выполняем это изменение заголовка и дополнительную выгрузку, приведены в последующем разделе этой статьи.
3.8. Получение эксклюзивной блокировки
Перед внесением изменений в сам файл базы данных мы должны получить эксклюзивную блокировку на файле базы данных. Получение эксклюзивной блокировки фактически происходит в два этапа. Сначала SQLite получает «ожидающую» блокировку. Затем она повышает ожидающую блокировку до эксклюзивной.
Блокировка ожидания позволяет другим процессам, которые уже имеют общую блокировку, продолжить чтение файла базы данных. Но она предотвращает создание новых общих блокировок. Идея блокировки ожидания заключается в предотвращении голодания писателей, вызванного большим пулом читателей. Может быть десятки, даже сотни других процессов, пытающихся прочитать файл базы данных. Каждый процесс приобретает общую блокировку перед началом чтения, считывает необходимые данные, а затем освобождает общую блокировку. Однако, если много разных процессов читают из одной базы данных, может случиться, что новый процесс всегда приобретает свою общую блокировку до того, как предыдущий процесс освободит свою общую блокировку. И поэтому никогда нет момента, когда на файле базы данных нет общих блокировок, и, следовательно, никогда нет возможности для писателя захватить эксклюзивную блокировку. Блокировка ожидания разработана для предотвращения этого цикла, позволяя существующим общим блокировкам продолжать работу, но блокируя создание новых общих блокировок. В конечном итоге все общие блокировки будут сняты, и блокировка ожидания сможет перейти в эксклюзивную блокировку.
3.9. Запись изменений в файл базы данных
После того, как эксклюзивная блокировка удерживается, мы знаем, что другие процессы не читают из файла базы данных, и безопасно записывать изменения в файл базы данных. Обычно эти изменения проходят только через кэш диска операционной системы и не доходят до постоянного хранилища.
3.10. 0 Сброс изменений в постоянное хранилище
Необходимо выполнить еще один сброс, чтобы убедиться, что все изменения в базе данных записаны в энергонезависимое хранилище. Это критический шаг для обеспечения того, что база данных переживет отключение электропитания без повреждений. Однако из-за присущей медленности записи на диск или флеш-память этот шаг, вместе со сбросом файла журнала отката в разделе 3.7 выше, занимает большую часть времени, необходимого для завершения подтверждения транзакции в SQLite.
3.11. 1 Удаление журнала отката
После того, как все изменения в базе данных безопасно записаны на постоянное устройство хранения, файл журнала отката удаляется. Это момент, когда транзакция подтверждается. Если произойдет отключение электропитания или сбой системы до этого момента, то процессы восстановления, которые будут описаны позже, покажут, что изменения никогда не вносились в файл базы данных. Если отключение электропитания или сбой системы произойдут после удаления журнала отката, то покажется, что все изменения были записаны на диск. Таким образом, SQLite имитирует, что не внесло никаких изменений в файл базы данных или внесло полный набор изменений в файл базы данных, в зависимости от того, существует ли файл журнала отката.
Удаление файла — это не атомарная операция, но с точки зрения пользовательского процесса она таковой представляется. Процесс всегда может спросить у операционной системы «существует ли этот файл?», и процесс получит ответ «да» или «нет». После отключения электропитания во время подтверждения транзакции SQLite спросит у операционной системы, существует ли файл журнала отката. Если ответ «да», значит, транзакция не завершена и отменяется. Если ответ «нет», значит, транзакция подтверждена.
Существование транзакции зависит от наличия файла журнала отката, а удаление файла кажется атомарной операцией с точки зрения процесса в пользовательском пространстве. Следовательно, транзакция кажется атомарной операцией.
Действие удаления файла является дорогостоящим на многих системах. В качестве оптимизации SQLite можно настроить на обнуление длины файла журнала до нуля байт или перезаписать заголовок файла журнала нулями. В любом случае, полученный файл журнала больше не способен отменить, и транзакция все равно подтверждается. Обнуление длины файла до нуля, подобно удалению файла, считается атомарной операцией с точки зрения пользовательского процесса. Перезапись заголовка журнала нулями не является атомарной, но если какая-либо часть заголовка повреждена, журнал не будет отменен. Поэтому можно сказать, что подтверждение происходит, как только заголовок достаточно изменится, чтобы сделать его недействительным. Как правило, это происходит, как только первый байт заголовка обнулится.
3.12. 2 Освобождение блокировки
Последним шагом в процессе подтверждения является освобождение эксклюзивной блокировки, чтобы другие процессы могли снова начать доступ к файлу базы данных.
На диаграмме справа показано, что информация, хранившаяся в пользовательском пространстве, очищается при освобождении блокировки. Раньше это было буквально верно для более старых версий SQLite. Но более новые версии SQLite хранят информацию в пользовательском пространстве в памяти, на случай, если она снова понадобится в начале следующей транзакции. Переиспользование информации, которая уже находится в локальной памяти, дешевле, чем передача информации обратно из кэша диска операционной системы или повторное чтение ее с диска. Перед повторным использованием информации в пользовательском пространстве необходимо сначала снова получить общую блокировку, а затем проверить, не изменил ли другой процесс файл базы данных, пока у нас не было блокировки. В первой странице базы данных есть счетчик, который инкрементируется каждый раз, когда файл базы данных изменяется. Мы можем узнать, модифицировал ли другой процесс базу данных, проверив этот счетчик. Если база данных была модифицирована, то кэш пользовательского пространства необходимо очистить и перечитать. Но часто бывает, что никаких изменений не произошло, и кэш пользовательского пространства можно повторно использовать для значительной экономии производительности.
4. Откат
Предполагается, что атомарное подтверждение происходит мгновенно. Но описанные выше процессы занимают конечное количество времени. Предположим, что питание компьютера было отключено во время выполнения описанной выше операции подтверждения. Чтобы сохранить иллюзию мгновенных изменений, мы должны «отменить» любые частичные изменения и восстановить базу данных в состояние, в котором она находилась перед началом транзакции.
4.1. При возникновении проблемы...
Предположим, что отключение питания произошло во время шага 3.10 выше, в то время как изменения в базе данных записывались на диск. После восстановления питания ситуация может быть такой, как показано справа. Мы пытались изменить три страницы файла базы данных, но успешно записали только одну. Другая страница была частично записана, а третья страница вообще не была записана.
Журнал отката полностью и без повреждений хранится на диске при восстановлении питания. Это ключевой момент. Причина операции сброса в шаге 3.7 заключается в том, чтобы абсолютно гарантировать, что весь журнал отката безопасно хранится в энергонезависимой памяти до внесения любых изменений в сам файл базы данных.
4.2. Журналы отката с активным состоянием
В первый раз, когда любой процесс SQLite пытается получить доступ к файлу базы данных, он получает общую блокировку, как описано в разделе 3.2 выше. Но затем он замечает, что присутствует файл журнала отката. SQLite проверяет, является ли журнал отката «активным». Активный журнал отката — это журнал отката, который необходимо воспроизвести, чтобы восстановить базу данных в согласованное состояние. Активный журнал существует только тогда, когда предыдущий процесс находился в середине подтверждения транзакции, когда произошел сбой или потеря питания.
Журнал отката является «активным», если все из перечисленного ниже верно:
- Журнал отката существует.
- Журнал отката не пустой файл.
- Нет резервной блокировки на основном файле базы данных.
- Заголовок журнала отката имеет правильный формат и, в частности, не был обнулен.
- Журнал отката не содержит имя файла супержурнала (см. раздел 5.5 ниже), или если он содержит имя супержурнала, то этот файл супержурнала существует.
Наличие активного журнала указывает на то, что предыдущий процесс пытался подтвердить транзакцию, но прервался по какой-то причине до завершения подтверждения. Активный журнал означает, что файл базы данных находится в несогласованном состоянии и требует восстановления (откатом) перед использованием.
4.3. Получение эксклюзивной блокировки на базе данных
Первый шаг в работе с активным журналом — получение эксклюзивной блокировки на файл базы данных. Это предотвращает одновременную попытку двух или более процессов отменить один и тот же активный журнал.
4.4. Отмена незавершенных изменений
После получения процесса эксклюзивной блокировки ему разрешается записывать в файл базы данных. Затем он продолжает читать исходное содержимое страниц из журнала отката и записывать это содержимое обратно в файл базы данных, откуда оно было взято. Отметим, что заголовок журнала отката записывает исходный размер файла базы данных до начала прерванной транзакции. SQLite использует эту информацию для сокращения файла базы данных до его первоначального размера в тех случаях, когда незавершенная транзакция привела к увеличению размера базы данных. По окончании этого шага размер и содержимое базы данных должны быть такими же, как и до начала прерванной транзакции.
4.5. Удаление активного журнала
После того, как вся информация из журнала отката была воспроизведена в файл базы данных (и сброшена на диск в случае возникновения еще одного сбоя электропитания), активный журнал отката можно удалить.
Как и в разделе 3.11, файл журнала можно укоротить до нулевой длины или перезаписать его заголовок нулями в качестве оптимизации на системах, где удаление файла является дорогостоящим. В любом случае, журнал больше не является активным после этого шага.
4.6. Продолжить, как будто незавершенные записи никогда не происходили
Заключительным этапом восстановления является уменьшение эксклюзивной блокировки до общей блокировки. После этого база данных возвращается в состояние, в котором она бы находилась, если бы прерванная транзакция никогда не начиналась. Поскольку вся эта деятельность по восстановлению происходит полностью автоматически и прозрачно, для программы, использующей SQLite, это выглядит так, как будто прерванная транзакция никогда не начиналась.
5. Подтверждение для нескольких файлов
SQLite позволяет одному подключению к базе данных одновременно работать с двумя или более файлами базы данных с помощью команды ATTACH DATABASE. При модификации нескольких файлов базы данных в рамках одной транзакции все файлы обновляются атомарно. Другими словами, либо все файлы базы данных обновляются, либо ни один из них не обновляется. Достижение атомарного подтверждения для нескольких файлов базы данных сложнее, чем для одного файла. В этом разделе описано, как SQLite выполняет эту магию.
5.1. Отдельные журналы отката для каждой базы данных
Когда в транзакции участвуют несколько файлов базы данных, каждая база данных имеет свой собственный журнал отката, и каждая база данных блокируется отдельно. Диаграмма справа демонстрирует сценарий, в котором в одной транзакции были изменены три разных файла базы данных. Ситуация на этом шаге аналогична сценарию транзакции с одним файлом на шаге 3.6. Каждый файл базы данных имеет зарезервированный замок. Для каждой базы данных исходное содержимое страниц, которые изменяются, записывается в журнал отката для этой базы данных, но содержимое журналов ещё не выведено на диск. Изменения ещё не внесены в сам файл базы данных, хотя предполагается, что изменения хранятся в памяти пользователя.
Для краткости диаграммы в этом разделе упрощены по сравнению с предыдущими. Синий цвет всё ещё обозначает исходное содержимое, а розовый — новое содержимое. Но отдельные страницы в журнале отката и файле базы данных не отображаются, и мы не различаем информацию в кэше операционной системы и информацию, которая находится на диске. Все эти факторы по-прежнему применимы в многофайловом сценарии подтверждения. Просто они занимают много места в диаграммах, и они не добавляют никакой новой информации, поэтому здесь они опущены.
5.2. Файл супержурнала
Следующим шагом в многофайловой транзакции является создание файла «супержурнала». Имя файла супержурнала совпадает с именем исходного файла базы данных (базы данных, которая была открыта с помощью интерфейса sqlite3_open(), а не одной из вспомогательных баз данных ATTACH) с добавленным текстом «-mjHHHHHHHH», где HHHHHHHH — случайное 32-битное шестнадцатеричное число. Случайный суффикс HHHHHHHH меняется для каждого нового супержурнала.
(Примечание: формула для вычисления имени файла супержурнала, приведенная в предыдущем абзаце, соответствует реализации версии SQLite 3.5.0. Но эта формула не является частью спецификации SQLite и может быть изменена в будущих выпусках.)
В отличие от журналов отката, супержурнал не содержит исходного содержимого страниц базы данных. Вместо этого супержурнал содержит полные пути к журналам отката для каждой базы данных, участвующей в транзакции.
После построения супержурнала его содержимое выводится на диск перед выполнением любых дальнейших действий. В Unix также синхронизируется каталог, содержащий супержурнал, чтобы гарантировать, что файл супержурнала появится в каталоге после сбоя питания.
Цель супержурнала — гарантировать атомарность многофайловых транзакций при отключении питания. Но если файлы базы данных имеют другие настройки, которые ухудшают целостность при отключении питания (например, PRAGMA synchronous=OFF или PRAGMA journal_mode=MEMORY), то создание супержурнала пропускается как оптимизация.
5.3. Обновление заголовков журнала отката
Следующим шагом является запись полного пути к файлу супержурнала в заголовок каждого журнала отката. Пространство для хранения имени файла супержурнала было зарезервировано в начале каждого журнала отката при создании журналов отката.
Содержимое каждого журнала отката выводится на диск как до, так и после записи имени файла супержурнала в заголовок журнала отката. Важно выполнить обе эти операции вывода на диск. К счастью, второй вывод на диск обычно недорог, так как обычно изменяется только одна страница файла журнала (первая страница).
Этот шаг аналогичен шагу 3.7 в сценарии подтверждения с одним файлом, описанном выше.
5.4. Обновление файлов базы данных
После вывода на диск всех файлов журналов отката можно безопасно начать обновление файлов базы данных. Мы должны получить эксклюзивную блокировку всех файлов базы данных перед записью изменений. После записи всех изменений важно вывести изменения на диск, чтобы они сохранились в случае сбоя питания или сбоя операционной системы.
Этот шаг соответствует шагам 3.8, 3.9 и 3.10 в сценарии подтверждения с одним файлом, описанном ранее.
5.5. Удаление файла супержурнала
Следующим шагом является удаление файла супержурнала. На этом этапе многофайловая транзакция подтверждается. Этот шаг соответствует шагу 3.11 в сценарии подтверждения с одним файлом, где удаляется журнал отката.
Если в этот момент произойдёт отключение питания или сбой операционной системы, транзакция не будет откатана при перезагрузке системы, даже если журналы отката присутствуют. Разница заключается в пути к супержурналу в заголовке журнала отката. При перезапуске SQLite рассматривает журнал как активный и будет воспроизводить его только если в заголовке нет имени файла супержурнала (что соответствует подтверждению с одним файлом) или если файл супержурнала всё ещё существует на диске.
5.6. Уборка журналов отката
Последним шагом в многофайловом подтверждении является удаление отдельных журналов отката и снятие эксклюзивных блокировок с файлов базы данных, чтобы другие процессы могли увидеть изменения. Это соответствует шагу 3.12 в последовательности подтверждения с одним файлом.
В этот момент транзакция уже подтверждена, поэтому временные параметры не являются критическими для удаления журналов отката. Текущая реализация удаляет один журнал отката, затем разблокирует соответствующий файл базы данных перед переходом к следующему журналу отката. Однако в будущем мы можем изменить это таким образом, чтобы все журналы отката были удалены перед разблокировкой любых файлов базы данных. До тех пор, пока журнал отката удаляется до разблокировки соответствующего файла базы данных, порядок удаления журналов отката или разблокировки файлов базы данных не имеет значения.
6. Дополнительные сведения о процессе подтверждения
Раздел 3.0 выше предоставляет обзор того, как работает атомарное подтверждение в SQLite. Но он опускает ряд важных деталей. Следующие подразделы попытаются заполнить пробелы.
6.1. Всегда журналировать полные сектора
Когда исходное содержимое страницы базы данных записывается в журнал отката (как показано в разделе 3.5), SQLite всегда записывает полный сектор данных, даже если размер страницы базы данных меньше размера сектора. Исторически размер сектора в SQLite был жёстко задан в 512 байт, и так как минимальный размер страницы также составляет 512 байт, это никогда не было проблемой. Но начиная с версии SQLite 3.3.14, SQLite может использовать устройства хранения с размером сектора больше 512 байт. Таким образом, начиная с версии 3.3.14, всякий раз, когда любая страница в секторе записывается в файл журнала, все страницы в том же секторе сохраняются с ней.
Важно сохранять все страницы сектора в журнале отката, чтобы предотвратить повреждение базы данных после сбоя питания во время записи сектора. Предположим, что страницы 1, 2, 3 и 4 хранятся в секторе 1, и страница 2 изменена. Чтобы записать изменения в страницу 2, основное оборудование также должно перезаписать содержимое страниц 1, 3 и 4, так как аппаратное обеспечение должно записать весь сектор. Если эта операция записи прервана отключением питания, одна или несколько страниц 1, 3 или 4 могут остаться с неверными данными. Следовательно, чтобы избежать постоянного повреждения базы данных, исходное содержимое всех этих страниц должно содержаться в журнале отката.
6.2. Обработка мусора, записанного в файлы журналов
Когда данные добавляются в конец журнала отката, SQLite обычно делает пессимистическое предположение, что файл сначала расширяется с помощью неверных данных «мусора», а затем правильные данные заменяют мусор. Другими словами, SQLite предполагает, что размер файла увеличивается сначала, а затем содержимое записывается в файл. Если отключение питания происходит после увеличения размера файла, но до записи содержимого файла, журнал отката может содержать мусорные данные. Если после восстановления питания другой процесс SQLite видит журнал отката, содержащий мусорные данные, и пытается откатить его в исходный файл базы данных, он может скопировать часть мусора в файл базы данных и, таким образом, повредить файл базы данных.
SQLite использует две защиты от этой проблемы. Во-первых, SQLite записывает количество страниц в журнале отката в заголовке журнала отката. Это число изначально равно нулю. Итак, во время попытки откатить неполный (и, возможно, повреждённый) журнал отката процесс, выполняющий откат, увидит, что журнал содержит ноль страниц, и, таким образом, не внесёт никаких изменений в базу данных. Перед подтверждением журнал отката выводится на диск, чтобы убедиться, что всё содержимое синхронизировано с диском и в файле не осталось «мусора», а только тогда счётчик страниц в заголовке меняется с нуля на истинное количество страниц в журнале отката. Заголовок журнала отката всегда хранится в отдельном секторе от любых данных страницы, чтобы он мог быть перезаписан и выведен на диск без риска повреждения страницы данных в случае отключения питания. Обратите внимание, что журнал отката выводится на диск дважды: один раз для записи содержимого страницы и второй раз для записи количества страниц в заголовке.
В предыдущем абзаце описывается то, что происходит, когда значение параметра synchronous равно «full».
PRAGMA synchronous=FULL;
Значение параметра synchronous по умолчанию равно full, поэтому обычно происходит именно это. Однако, если значение параметра synchronous уменьшено до «normal», SQLite выводит журнал отката только один раз, после записи количества страниц. Это несёт риск повреждения, поскольку может произойти так, что изменённое (не равное нулю) количество страниц достигнет поверхности диска раньше, чем все данные. Данные были записаны первыми, но SQLite предполагает, что основная файловая система может переупорядочивать запросы записи и что счётчик страниц может быть сохранён на носителе первым, даже если его запрос записи пришёл последним. Итак, как вторая линия защиты, SQLite также использует 32-битный контрольную сумму для каждой страницы данных в журнале отката. Эта контрольная сумма вычисляется для каждой страницы во время отката, как описано в разделе 4.4. Если обнаружена неверная контрольная сумма, откат отменяется. Обратите внимание, что контрольная сумма не гарантирует, что данные страницы верны, так как существует небольшая, но конечная вероятность того, что контрольная сумма может быть верной, даже если данные повреждены. Но контрольная сумма по крайней мере делает такую ошибку маловероятной.
Обратите внимание, что контрольные суммы в журнале отката не нужны, если значение синхронизации установлено в FULL. Мы полагаемся на контрольные суммы только тогда, когда синхронизация понижена до NORMAL. Тем не менее, контрольные суммы никогда не мешают, и поэтому они включаются в журнал отката независимо от настроек синхронизации.
6.3. Выполнение разлива кеша до подтверждения
Процесс подтверждения, показанный в разделе 3.0, предполагает, что все изменения в базе данных помещаются в память до момента подтверждения. Это обычный случай. Но иногда более крупное изменение переполняет кеш пользователя до подтверждения транзакции. В таких случаях кеш должен быть разлит в базу данных до завершения транзакции.
В начале разлива кеша состояние соединения с базой данных такое же, как показано в шаге 3.6. Исходное содержимое страницы сохраняется в журнале отката, а изменения страниц существуют в памяти пользователя. Для разлива кеша SQLite выполняет шаги 3.7 по 3.9. Другими словами, журнал отката записывается на диск, приобретается эксклюзивный замок, и изменения записываются в базу данных. Однако оставшиеся шаги откладываются до фактического подтверждения транзакции. Новый заголовок журнала добавляется в конец журнала отката (в собственном секторе), и эксклюзивный замок базы данных сохраняется, но в остальном обработка возвращается к шагу 3.6. При подтверждении транзакции или при возникновении другого разлива кеша шаги 3.7 и 3.9 повторяются. (Шаг 3.8 пропускается при втором и последующих проходах, поскольку эксклюзивный замок базы данных уже удерживается из-за первого прохода.)
Разлив кеша приводит к повышению блокировки файла базы данных с резервированной до эксклюзивной. Это снижает конкуретность. Разлив кеша также приводит к дополнительным операциям сброса или синхронизации диска, которые медленные, следовательно, разлив кеша может существенно снизить производительность. По этим причинам разлив кеша избегается всякий раз, когда это возможно.
7. Оптимизации
Профилирование показывает, что для большинства систем и в большинстве случаев SQLite тратит большую часть времени на ввод-вывод на диск. Следовательно, все, что мы можем сделать для уменьшения количества операций ввода-вывода на диск, вероятно, окажет большое положительное влияние на производительность SQLite. Этот раздел описывает некоторые методы, используемые SQLite, чтобы свести количество операций ввода-вывода на диск к минимуму, сохраняя при этом атомарное подтверждение.
7.1. Кеш, сохраненный между транзакциями
Шаг 3.12 процесса подтверждения показывает, что после освобождения общего замка все изображения кеша пользователя содержимого базы данных должны быть удалены. Это делается потому, что без общего замка другие процессы свободно могут изменять содержимое файла базы данных, и поэтому любое изображение содержимого в пространстве пользователя может стать устаревшим. Вследствие этого каждая новая транзакция начнётся с повторного чтения данных, которые ранее уже были прочитаны. Это не так плохо, как кажется на первый взгляд, так как данные, которые читаются, скорее всего, всё ещё находятся в кэше файлов операционной системы. Таким образом, «чтение» — это просто копирование данных из пространства ядра в пользовательское пространство. Но даже так, это всё равно занимает время.
Начиная с версии SQLite 3.3.14, был добавлен механизм, призванный уменьшить бесполезное перечитывание данных. В более новых версиях SQLite данные в кэше страниц пейджера пользовательского пространства сохраняются при освобождении блокировки файла базы данных. Позже, после получения общего замка в начале следующей транзакции, SQLite проверяет, не изменил ли какой-либо другой процесс файл базы данных. Если база данных была изменена каким-либо образом с момента последнего освобождения блокировки, кеш пользовательского пространства стирается в этот момент. Однако обычно файл базы данных не изменяется, и кеш пользовательского пространства можно сохранить, а некоторые ненужные операции чтения можно избежать.
Для определения того, изменился ли файл базы данных или нет, SQLite использует счётчик в заголовке базы данных (в байтах с 24 по 27), который увеличивается при каждой операции изменения. SQLite сохраняет копию этого счётчика перед освобождением блокировки базы данных. Затем, после получения следующей блокировки базы данных, он сравнивает сохранённое значение счётчика с текущим значением счётчика и стирает кеш, если значения отличаются, или повторно использует кеш, если они одинаковы.
7.2. Режим эксклюзивного доступа
Версия SQLite 3.3.14 добавляет понятие «Режим эксклюзивного доступа». В режиме эксклюзивного доступа SQLite сохраняет эксклюзивную блокировку базы данных по окончании каждой транзакции. Это предотвращает доступ других процессов к базе данных, но во многих развертываниях только один процесс использует базу данных, поэтому это не является серьёзной проблемой. Преимущество режима эксклюзивного доступа заключается в том, что количество операций ввода-вывода на диск можно уменьшить тремя способами:
Не нужно увеличивать счётчик изменений в заголовке базы данных для транзакций после первой транзакции. Это часто экономит запись первой страницы как в журнал отката, так и в основной файл базы данных.
Ни один другой процесс не может изменить базу данных, поэтому никогда не возникает необходимости проверять счётчик изменений и очищать кеш пользовательского пространства в начале транзакции.
Каждая транзакция может быть подтверждена путём перезаписи заголовка журнала отката нулями вместо удаления файла журнала. Это позволяет избежать необходимости изменения записи в каталоге для файла журнала и освобождения секторов диска, связанных с журналом. Кроме того, следующая транзакция перепишет существующее содержимое файла журнала вместо добавления нового содержимого, а на большинстве систем перезапись намного быстрее, чем добавление.
Третья оптимизация, обнуление заголовка файла журнала вместо удаления файла журнала отката, не зависит от удержания эксклюзивной блокировки в любое время. Эту оптимизацию можно настроить независимо от режима эксклюзивной блокировки с помощью параметра `journal_mode`, как описано в разделе 7.6 ниже.
7.3. Не регистрировать страницы свободного списка
При удалении информации из базы данных SQLite страницы, используемые для хранения удалённой информации, добавляются в «свободный список». Последующие вставки будут извлекать страницы из этого свободного списка, а не расширять файл базы данных.
Некоторые страницы свободного списка содержат критически важные данные; в частности, расположение других страниц свободного списка. Но большинство страниц свободного списка не содержат полезной информации. Эти страницы свободного списка называются «листовыми» страницами. Мы можем изменять содержимое страницы листового свободного списка в базе данных без изменения смысла базы данных каким-либо образом.
Поскольку содержимое страниц листового свободного списка не имеет значения, SQLite избегает сохранения содержимого страниц листового свободного списка в журнале отката в шаге 3.5 процесса подтверждения. Если страница листового свободного списка изменена, и это изменение не отменяется при восстановлении транзакции, база данных не пострадает от пропуска. Аналогичным образом, содержимое новой страницы свободного списка никогда не записывается обратно в базу данных в шаге 3.9 и не читается из базы данных в шаге 3.3. Эти оптимизации могут значительно уменьшить количество операций ввода-вывода при внесении изменений в файл базы данных, содержащий свободные места.
7.4. Обновления одной страницы и атомарные записи сектора
Начиная с версии SQLite 3.5.0, новый интерфейс виртуальной файловой системы (VFS) содержит метод xDeviceCharacteristics, который сообщает о специальных свойствах, которые может иметь базовое устройство хранения. Среди специальных свойств, которые может сообщить xDeviceCharacteristics, есть возможность выполнения атомарной записи сектора.
Напомним, что по умолчанию SQLite предполагает, что записи сектора линейные, но не атомарные. Линейная запись начинается с одного конца сектора и изменяет информацию по байтам, пока не дойдёт до другого конца сектора. Если во время линейной записи произойдёт отключение питания, то часть сектора может быть изменена, в то время как другая часть останется неизменённой. При атомарной записи сектора либо весь сектор перезаписывается, либо ничего в секторе не меняется.
Мы считаем, что большинство современных жёстких дисков реализуют атомарные записи сектора. При отключении питания диск использует энергию, хранящуюся в конденсаторах и/или угловой момент пластины диска, для обеспечения питания для завершения любой операции в процессе. Тем не менее, между системным вызовом записи и электроникой встроенного жёсткого диска существует множество слоёв, поэтому мы используем безопасный подход как в реализациях VFS для Unix, так и для w32 и предполагаем, что записи сектора не атомарны. С другой стороны, производители устройств, имеющие больший контроль над своими файловыми системами, могут рассмотреть возможность включения свойства атомарной записи xDeviceCharacteristics, если их оборудование действительно выполняет атомарные записи.
Когда записи сектора атомарны, размер страницы базы данных такой же, как размер сектора, и когда есть изменение в базе данных, затрагивающее только одну страницу базы данных, тогда SQLite пропускает весь процесс журналирования и синхронизации и просто записывает изменённую страницу непосредственно в файл базы данных. Счётчик изменений в первой странице файла базы данных изменяется отдельно, так как нет вреда, если питание отключится, прежде чем счётчик изменений сможет быть обновлён.
7.5. Файловые системы с безопасной семантикой добавления
Ещё одна оптимизация, введённая в SQLite версии 3.5.0, использует «безопасное добавление» поведения базового диска. Напомним, что SQLite предполагает, что при добавлении данных в файл (в частности, в журнал отката) сначала увеличивается размер файла, а затем записывается содержимое. Таким образом, если питание отключится после увеличения размера файла, но до записи содержимого, файл останется с неверными «мусорными» данными. Однако метод xDeviceCharacteristics VFS может указывать, что файловая система реализует «безопасную семантику добавления». Это означает, что содержимое записывается до увеличения размера файла, чтобы невозможно было внести мусорные данные в журнал отката из-за отключения питания или сбоя системы.
Когда для файловой системы указана безопасная семантика добавления, SQLite всегда сохраняет специальное значение -1 для количества страниц в заголовке журнала отката. Значение -1 для количества страниц сообщает любому процессу, пытающемуся отменить журнал, что количество страниц в журнале должно быть вычислено из размера журнала. Это значение -1 никогда не изменяется. Таким образом, при подтверждении мы сохраняем одну операцию сброса и запись одного сектора первой страницы файла журнала. Кроме того, при разливе кеша нам больше не нужно добавлять новый заголовок журнала в конец журнала; мы можем просто продолжить добавлять новые страницы в конец существующего журнала.
7.6. Постоянные журналы отката
Удаление файла — ресурсоёмкая операция на многих системах. Поэтому в целях оптимизации SQLite можно настроить так, чтобы избегать удаления журнала раздела 3.11. Вместо удаления файла журнала при фиксации транзакции файл либо обнуляется, либо его заголовок перезаписывается нулями. Обнуление файла до нулевой длины позволяет избежать модификаций каталога, в котором находится файл, поскольку файл не удаляется из каталога. Перезапись заголовка даёт дополнительную экономию, так как не нужно обновлять длину файла (в «inode» на многих системах) и не нужно иметь дело с освобождёнными секторами диска. Кроме того, при следующей транзакции журнал будет создан путём перезаписи существующего содержимого, а не добавлением нового содержимого в конец файла, а перезапись часто намного быстрее, чем добавление.
SQLite можно настроить на фиксацию транзакций путём перезаписи заголовка журнала нулями вместо удаления файла журнала, установив режим журнализации «PERSIST» с помощью PRAGMA journal_mode. Например:
PRAGMA journal_mode=PERSIST;
Использование режима постоянного журнала обеспечивает заметное улучшение производительности на многих системах. Конечно, недостатком является то, что файлы журнала остаются на диске, занимая дисковое пространство и засоряя каталоги, долгое время после фиксации транзакции. Единственный безопасный способ удалить файл постоянного журнала — зафиксировать транзакцию с режимом журнализации DELETE:
PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE; COMMIT;
Будьте осторожны, удаляя файлы постоянного журнала другими способами, так как файл журнала может быть активным, и в этом случае его удаление приведёт к повреждению соответствующего файла базы данных.
Начиная с версии SQLite 3.6.4 (2008-10-15), также поддерживается режим журнализации TRUNCATE:
PRAGMA journal_mode=TRUNCATE;
В режиме TRUNCATE транзакция фиксируется путём обнуления длины файла журнала до нуля, а не удалением файла журнала (как в режиме DELETE) или обнулением заголовка (как в режиме PERSIST). Режим TRUNCATE сохраняет преимущество режима PERSIST, что каталогу, содержащему файл журнала и базы данных, не нужно обновляться. Следовательно, обнуление файла часто происходит быстрее, чем его удаление. TRUNCATE имеет дополнительное преимущество — он не сопровождается системным вызовом (например, fsync()) для синхронизации изменений на диске. Возможно, это было бы безопаснее. Но на многих современных файловых системах операция обнуления является атомарной и синхронной, и поэтому мы считаем, что TRUNCATE обычно будет безопасным в случае сбоев питания. Если вы не уверены, является ли TRUNCATE синхронной и атомарной операцией на вашей файловой системе и для вас важно, чтобы ваша база данных пережила отключение питания или сбой операционной системы во время операции обнуления, то вы можете рассмотреть использование другого режима журнализации.
На встраиваемых системах со синхронными файловыми системами TRUNCATE приводит к более медленной работе, чем PERSIST. Операция фиксации имеет одинаковую скорость. Но последующие транзакции медленнее после TRUNCATE, потому что перезапись существующего содержимого быстрее, чем добавление в конец файла. Новые записи в журнале всегда будут добавляться после TRUNCATE, но обычно перезаписываются с PERSIST.
8. Тестирование атомарного поведения фиксации
Разработчики SQLite уверены в его надёжности в случае сбоев питания и системных сбоев, потому что автоматические тестовые процедуры выполняют обширные проверки возможности SQLite восстанавливаться после имитируемого отключения питания. Мы называем их «тестами на сбои».
Тесты на сбои в SQLite используют изменённый VFS, который может моделировать виды повреждений файловой системы, возникающие во время сбоя питания или сбоя операционной системы. VFS для тестов на сбои может имитировать неполные записи секторов, страницы, заполненные мусорными данными из-за того, что запись не завершилась, и записи в неправильном порядке, всё происходит в различные моменты тестового сценария. Тесты на сбои выполняют транзакции снова и снова, изменяя время, в которое происходит имитируемое отключение питания, и свойства причиняемых повреждений. Затем каждый тест повторно открывает базу данных после имитируемого сбоя и проверяет, произошла ли транзакция полностью или нет, и находится ли база данных в полностью согласованном состоянии.
Тесты на сбои в SQLite обнаружили ряд очень тонких ошибок (теперь исправленных) в механизме восстановления. Некоторые из этих ошибок были очень скрытыми и вряд ли были бы обнаружены только с помощью анализа и проверки кода. На основании этого опыта разработчики SQLite уверены, что любая другая система баз данных, не использующая аналогичную систему тестирования на сбои, вероятно, содержит необнаруженные ошибки, которые приведут к повреждению базы данных после сбоя системы или отключения питания.
9. Возможные проблемы
Механизм атомарной фиксации в SQLite оказался надёжным, но его можно обойти достаточно изобретательным противником или достаточно неисправной реализацией операционной системы. В этом разделе описаны некоторые из способов, которыми база данных SQLite может быть повреждена в результате сбоя питания или системного сбоя. (См. также: Как повредить файлы базы данных.)
9.1. Неисправные реализации блокировок
SQLite использует блокировки файловой системы, чтобы убедиться, что только один процесс и подключение к базе данных пытаются одновременно изменить базу данных. Механизм блокировки файловой системы реализован на уровне VFS и отличается для каждой операционной системы. SQLite полагается на правильность этой реализации. Если что-то пойдёт не так, и два или более процессов смогут одновременно записать один и тот же файл базы данных, это может привести к серьёзному повреждению.
Мы получили сообщения об ошибках реализации сетевых файловых систем Windows и NFS, в которых блокировки были некорректно реализованы. Мы не можем подтвердить эти сообщения, но, поскольку блокировки сложно реализовать правильно в сетевой файловой системе, у нас нет причин им не верить. Вам рекомендуется избегать использования SQLite в сетевой файловой системе, поскольку производительность будет низкой. Но если вам необходимо использовать сетевую файловую систему для хранения файлов базы данных SQLite, рассмотрите возможность использования дополнительного механизма блокировки, чтобы предотвратить одновременные записи в одну и ту же базу данных, даже если встроенный механизм блокировки файловой системы работает неправильно.
Версии SQLite, которые предварительно установлены на компьютерах Apple Mac OS X, содержат версию SQLite, расширенную для использования альтернативных стратегий блокировки, которые работают во всех сетевых файловых системах, поддерживаемых Apple. Эти расширения, используемые Apple, работают отлично, пока все процессы обращаются к файлу базы данных одинаковым образом. К сожалению, механизмы блокировки не исключают друг друга, поэтому, если один процесс обращается к файлу с использованием (например) блокировок AFP, а другой процесс (возможно, на другом компьютере) использует блокировки dot-файлов, два процесса могут столкнуться, потому что блокировки AFP не исключают блокировки dot-файлов или наоборот.
9.2. Неполные сбросы данных в буфер
SQLite использует системный вызов fsync() в Unix и вызов FlushFileBuffers() в w32, чтобы синхронизировать буферы файловой системы с диском, как показано в шаге 3.7 и шаге 3.10. К сожалению, мы получили сообщения о том, что ни один из этих интерфейсов не работает так, как рекламируется, на многих системах. Мы слышали, что FlushFileBuffers() может быть полностью отключен с помощью параметров реестра в некоторых версиях Windows. Некоторые старые версии Linux содержат версии fsync(), которые являются бесполезными операциями для некоторых файловых систем, как нам сообщают. Даже на системах, где FlushFileBuffers() и fsync() работают, часто контроллер диска IDE врёт и говорит, что данные достигли поверхности диска, в то время как они всё ещё находятся только в кэше управляющего устройства.
В Mac вы можете установить этот pragma:
PRAGMA fullfsync=ON;
Установка fullfsync на Mac гарантирует, что данные действительно будут отправлены на физические носители при сбросе. Но реализация fullfsync включает сброс параметров контроллера диска. И поэтому это не только крайне медленно, но также замедляет и другие операции ввода-вывода на диске. Поэтому его использование не рекомендуется.
9.3. Частичные удаления файлов
SQLite предполагает, что удаление файла является атомарной операцией с точки зрения процесса пользователя. Если произойдёт отключение питания во время удаления файла, то после восстановления питания SQLite ожидает увидеть либо весь файл со всеми его исходными данными, либо ожидает не найти его вообще. Транзакции могут не быть атомарными на системах, которые не работают таким образом.
9.4. Запись мусора в файлы
Файлы базы данных SQLite — обычные файлы на диске, которые могут быть открыты и записаны обычными процессами пользователя. Злонамеренный процесс может открыть базу данных SQLite и заполнить её повреждёнными данными. Повреждённые данные также могут быть введены в базу данных SQLite из-за ошибок в операционной системе или контроллере диска, особенно из-за ошибок, вызванных сбоем питания. SQLite ничего не может сделать для защиты от таких проблем.
9.5. Удаление или переименование активного журнала
Если произойдёт сбой или отключение питания, и активный журнал останется на диске, крайне важно, чтобы исходный файл базы данных и активный журнал остались на диске со своими исходными именами до тех пор, пока файл базы данных не будет открыт другим процессом SQLite и не будет отменён. Во время восстановления в шаге 4.2 SQLite ищет активный журнал, ища файл в той же папке, что и открываемая база данных, и чьё имя происходит из имени открываемого файла. Если либо исходный файл базы данных, либо активный журнал были перемещены или переименованы, активный журнал не будет найден, и база данных не будет отменена.
Мы подозреваем, что распространённый способ отказа при восстановлении SQLite происходит так: происходит отключение питания. После восстановления питания добросовестный пользователь или системный администратор начинает искать на диске повреждения. Они видят свой файл базы данных с именем «важный.данные». Этот файл, возможно, им знаком. Но после сбоя также есть активный журнал с именем «важный.данные-журнал». Затем пользователь удаляет активный журнал, считая, что помогает очистить систему. Нам не известно способа предотвратить это, кроме просвещения пользователей.
Если есть несколько (жёстких или символических) ссылок на файл базы данных, журнал будет создан с использованием имени ссылки, через которую был открыт файл. Если произойдёт сбой, и база данных будет открыта с помощью другой ссылки, активный журнал не будет найден, и отмена не произойдёт.
Иногда отключение электропитания приводит к повреждению файловой системы таким образом, что недавно изменённые имена файлов забываются, а файл перемещается в директорию «/lost+found». В таком случае горячий журнал не будет найден, и восстановление не произойдёт. SQLite пытается предотвратить это, открывая и синхронизируя директорию, содержащую журнал отката, одновременно с синхронизацией самого файла журнала. Однако перемещение файлов в /lost+found может быть вызвано не связанными процессами, создающими не связанные файлы в той же директории, что и основной файл базы данных. Поскольку это происходит вне контроля SQLite, SQLite ничего не может сделать, чтобы предотвратить это. Если вы работаете на системе, уязвимой к этому виду повреждения пространства имен файловой системы (большинство современных файловых систем с журналированием, по нашему мнению, устойчивы к этому), то вы можете рассмотреть возможность размещения каждого файла базы данных SQLite в своей собственной частной поддиректории.
10. Будущие направления и заключение
Время от времени кто-то обнаруживает новый режим сбоя механизма атомарного коммита в SQLite, и разработчикам приходится вносить исправление. Это происходит всё реже, и режимы сбоя становятся всё более и более сложными. Но всё равно было бы глупо предполагать, что логика атомарного коммита SQLite полностью лишена ошибок. Разработчики стремятся исправить эти ошибки так быстро, как только они будут обнаружены.
Разработчики также следят за новыми способами оптимизации механизма коммита. Текущие реализации VFS для Unix (Linux и Mac OS X) и Windows делают пессимистичные предположения о поведении этих систем. После консультации со специалистами по работе этих систем мы, возможно, сможем ослабить некоторые предположения относительно этих систем и позволить им работать быстрее. В частности, мы предполагаем, что большинство современных файловых систем обладают свойством безопасного добавления и что многие из них могут поддерживать атомарную запись секторов. Но пока это не известно наверняка, SQLite будет придерживаться консервативного подхода и исходить из худшего.
Эта страница была последний раз изменена 31 декабря 2022 г. в 21:51:03 UTC
SQLite is in the Public Domain.
https://sqlite.org/atomiccommit.html