AshBlade
@AshBlade
Просто хочу быть счастливым

Как реализовать атомарное обновление 2 файлов?

Пишу приложение на .NET, которое хранит свои данные в файлах и обновление состояния происходит с помощью алгоритма Raft.
Есть 2 файла: снапшот и лог команд. При сохранении нового снапшота необходимо
1. Заменить содержимое старого снапшота, и
2. Обновить лог соответствующим образом (очистить до нужной команды).

Вопрос: как реализовать атомарное обновление этих 2 файлов, чтобы в случае сбоя смог откатиться к корректному состоянию?

На данный момент, в голову пришла такая идея:
1. Создаем временные файлы снапшота и лога и сохраняем в них новое состояние
2. Делаем запись в UNDO логе о том, что необходимо выполнить перемещение этих 2 файлов на место старых файлов снапшота и лога. Т.е. выполнить mv / File.Move
3. Выполняем перемещение
4. Коммитим изменения в UNDO логе (чтобы больше не выполняли операции)

Когда будем восстанавливаться, если какого-то файла (из указанных) уже нет, то считаем, что перемещение выполнено успешно и просто пропускаем эту операцию.
О подводных камнях такого подхода мне не известно - возможно операция обновления метаданных будет не атомарной, произойдет частичная перезапись файла или другие.
Есть какие идеи как лучше это реализовать?
Идея о том, чтобы записывать полное состояние файла в UNDO лог и дальнейшего выполнения мне не нравится, т.к. снапшот/лог может быть огромным
  • Вопрос задан
  • 1009 просмотров
Решения вопроса 2
@mvv-rus
Настоящий админ AD и ненастоящий программист
Движок ESE от Microsoft (его используют системные БД MS Windows и MS Exchange) на для уменьшения объема просматриваемого журнала транзакций ("лога", он там хранится не в одном большом файле а в куче мелких) при рестарте создает и обновляет отдельный файл контрольной точки, в котором сохраняется ссылка на транзакцию, до которой все точно зафиксировано. А при рестарте - запускает применение журнала транзакций с контрольной точки.
Попробуйте подумать в эту сторону - может, подойдет.
PS А еще в Windows, в файловой системе NTFS есть встроенная поддержка транзакционного обновления. Но, во-первых - это только в Windows, а во-вторых, я не помню, чтобы в .NET была библиотека для использования этой возможности, так что, не исключено, что доступаться до нее придется через P/Invoke
Ответ написан
Комментировать
AshBlade
@AshBlade Автор вопроса
Просто хочу быть счастливым
В итоге пришел к такому решению:
Моя основная проблема - не как атомарно обновить 2 файла, а создавать снапшот приложения и очищать лог (чтобы размер не становился огромным) так, чтобы согласованность не нарушалась.

Моя проблема заключалась в изначально неправильном проектировании файловой структуры - было только 2 файла: снапшот и лог.
Спустя время нашел следующее решение:
- Файл снапшота 1 - для его обновления создается временный файл снапшота, который атомарно переименовывается (системный вызов rename атомарный - так прописано в документации)
- Вместо единого файла лога используется сегментированный лог - весь лог хранится в нескольких меньших файлах. Т.е. теперь я работаю с логическим файлом лога, который состоит из нескольких физических сегментов. Сегменты лога не удаляются при создании снапшота - этим может заниматься фоновый поток, который удаляет те сегменты все записи которых по индексу меньше индекса примененной записи в снапшоте.

Так как я использую RAFT, то необходимость в UNDO логе при записи в лог отпадает:
- запись append only, т.е. закоммиченные записи перезаписываться не будут
- лог может быть в несогласованном состоянии только если не до конца будут перезаписаны незакоммиченные записи, но это можно понять по чек-суммам, которые идут после каждой записи (можем откатить лог)

Также нашел недостаток в моей реализации - я храню индекс последней закоммиченной записи на диске (хранил в логе). Этого можно не делать и спокойно хранить в памяти, инициализируя на старте в 0 (ничего не закоммичено). Такой подход я также нашел в реализации рафт от HashiCorp (точнее есть открытый issue). Этот индекс будет инициализирован либо существующим лидером, либо при старте он будет корректно выставлен согласно консенсусу.

В итоге работа со снапшотами будет примерно следующей:
- Добавляем (при необходимости перезаписываем) записи в лог
- Если размер сегмента превысил максимальный, то создаем новый и закрываем старый (работа будет вестить с новым файлом)
- Новый снапшот создаем во временном файле
- Когда снапшот готов, то атомарно вызовом rename перемещаем его в целевой файл (замена)
- Фоновый поток в один момент обнаружит, что есть файлы лога с неактуальными записями и тогда их (старые сегменты лога) удалит

Похожие идеи я нашел в:
- etcd - создание новых файлов сегментов через атомарный rename
- kafka - все записи хранит в xxxxxxx.log файле, где xxxxxxx означает офсет первой записи
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
mindtester
@mindtester
http://iczin.su/hexagram_48
у тебя ключевое слово - транзакция. медитируй над ним.
...
ну а так... на вскидку... ?
ну создаешь ты копии.. кстати, метка времени, хотя она и у файлов может быть адекватной..
ну допустим файлы-флаги-завершения этапа транзакции? на каждый этап!
... если все крешанулось, то по наличию одного или более флагов, уже строишь алгоритм полного или частичного отката

... я так думаю... раз уж ты не на бд, а на файлы оперся...

ps ну или временный лог транзакции... так сказать.. ну и все чистить по успешному завершению

pps MVV,
в файловой системе NTFS есть встроенная поддержка транзакционного обновления
еще бы нарыть инфу об этом )))
- это белее чем реалистично, современные ФС (а NTFS точно) по сути специализированные СУБД
- подписываюсь!
- если кто нароет хотя бы на уровне winapi, подписываюсь допилить до шарпа.. ну мало ли... ну или поделитесь!!! )))

... хотя на отдельных файлах флагах-этапов транзакции.. скорее топорно, но надежно... по тому что топорно ;)))
... а еще и переносимо между ОС ))
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы