LINUX.ORG.RU

транзакционная запись в файл

 


1

9

привет!

нужно объединить две операции записи в транзакцию. запись происходит в один и тот же файл, но в разные его участки(заголовок - начало файла, и, собственно, данные - конец файла).

делал ли кто-нить что-нить подобное? возможно ли это сделать в user-space? что почитать по сабжу?

спасибо.

★★★

возможно ли это сделать в user-space?

В общем случае это ни в каком -space сделать нельзя.

Если тебе нужно, чтобы для другого читающего процесса в файле по определённому пути были либо данные до твоей транзакции, либо после твоей транзакции, то это можно сделать так:

  1. Создаём новый файл в той же директории, что и существующий файл, который мы хотим изменить.
  2. Пишем в него все данные (копируем из существующего файла, как вариант).
  3. Изменяем этот новый файл, если нужно.
  4. Перемещаем новый файл на место старого при помощи функции rename(), про которую написано в мане:

    If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing.

Читающий процесс должен переоткрывать файл перед чтением, иначе он всегда будет видеть старый файл. Способ не быстрый, но проверенный и надёжный.

Если нужно что-то более сложное или быстрое, то лучше возьми готовую СУБД. Да хоть тот же sqlite.

Deleted
()
Последнее исправление: Deleted (всего исправлений: 1)
Ответ на: комментарий от Deleted

нечто подобное и мне пришло в голову. но, как мне кажется, - сильно оверхедное решение...

я думал, что возможно есть какое-то фс-специфичное АПИ, при помощи которого можно реализовать сабж...

вариант с sqlite тоже обдумывал, но тоже как-то сильно оверхедно..

спасибо, подожду еще вариантов.

niXman ★★★
() автор топика
Ответ на: комментарий от niXman

в оффтопике открытый на запись файл блокируется и не читается. Может в онтопике есть флаг это делающий?

mittorn ★★★★★
()

делал ли кто-нить что-нить подобное?

Не делал.

возможно ли это сделать в user-space? что почитать по сабжу?

Можно ли установить жесткую буферизацию например?

нужно объединить две операции записи в транзакцию.

А условия для прервания транзакции чисто технические или происходят из бизнес-логики?

KennyMinigun ★★★★★
()
Ответ на: комментарий от KennyMinigun

Можно ли установить жесткую буферизацию например?

поясните.

А условия для прервания транзакции чисто технические или происходят из бизнес-логики?

технические. на случай падения программы..

niXman ★★★
() автор топика
Ответ на: комментарий от anonymous

а fuse с одной нитью?

Это уже не «общий случай». И требует наворачивания нестандартного поведения поверх POSIX FS. И я вообще не уверен, что это будет работать...

Deleted
()
Ответ на: комментарий от niXman

вариант с sqlite тоже обдумывал, но тоже как-то сильно оверхедно..

если нужны транзакции, sqlite - это самое простое и легковесное, что можно взять; если не хочешь с нуля кусок sqlite написать сам

MyTrooName ★★★★★
()
Ответ на: комментарий от niXman

Можно выставить флаг доступа к файлу, но он не обязателен к выполнению. Т.е. это вариант если доступ только из твоих приложений.

four_str_sam
()
Ответ на: комментарий от four_str_sam

Можно выставить флаг доступа к файлу, но он не обязателен к выполнению. Т.е. это вариант если доступ только из твоих приложений.

А ещё это не поможет в случае креша приложения посередине «транзакции». И при kill -9. И при внезапном пропадании электричества.

Deleted
()
Ответ на: комментарий от niXman

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

но тут возникает другой вопрос: т.к. файл будет постояно расти, хотелось бы его усекать с головы =)

niXman ★★★
() автор топика
Ответ на: комментарий от niXman

А нельзя вместо модификаций делать добавление в хвост, append-only? Теоретически это могло бы упростить контроль за «атомарностью». Сначала дописываем, потом изменяем заголовок. Опционально к «транзакции» можно прикладывать хэш. Будет видно битые и незавершенные транзакции.

В sqlite кстати сделано очень заморочено, вот тут есть неполное описание https://www.sqlite.org/fileio.html

Deleted
()
Последнее исправление: Deleted (всего исправлений: 1)

Файл memeory mapped в обоих процессах. В начале файла мутекс. Изменения защищаются мутексом. В этом случае все будет консистентно.

vromanov ★★★
()

Есть, как водится, два подхода: лог и транзакционный.

1) Пишешь в лог всё что собираешься писать в данные (offset, size, data), затем fsync лога, затем пишешь собственно данные как собирался, затем fsync данных и можно удалять лог (или атомарно* помечать как чистый). При чтении, если встретил лог, накатываешь сначала все записи из него.

2) Дописываешь в данные, но никак не перезатирая живые. Т.е. либо дописываешь в конец файла, либо пишешь в неиспользуемые дырки. Подчёркиваю что так как ты не перезатирал живые данные, никаких ссылок на то что было записано в файле нет, т.е. пока эти данные «невидимы» для читающего кода - это мусор в конце файла или в неиспользуемых дырках. Затем делаешь fsync, затем атомарно* нужно сделать эти данные видимыми.

* Вот с атомарностью есть такой нюанс что гарантий атомарности записи > 1 байта в общем случае нет, поэтому ты должен делать это записью одного байта. С логом всё просто, а с транзакционный подход будет диктовать особую структуру файла:

[1 байт] флаг (A либо B)
[8 байт] offset суперблока A
[8 байт] offset суперблока B
         данные 0
         суперблок 0
         данные 1
         суперблок 1
         данные 2
         суперблок 2
         данные 3
         суперблок 3

Суперблок это структура описывающая актуальное состояние хранящихся данных. Может быть организован по-разному в зависимости от ситуации (удаляются ли данные, space/speed tradeoff и т.д.). Т.е. если данные только аппендятся, суперблок 3 может содержать таблицу offset'ов данных 0, 1, 2, 3, а может только offset данных 3 и оффсет предыдущего суперблока. В любом случае, из суперблока 3 ты можешь дойти до всех четырёх кусков данных.

Смещение актуального суперблока хранится либо в A (при этом флаг = A), либо в B (при этом флаг = B).

Запись будет выглядеть так:

  • Дописать данные 4
  • Дописать суперблок 4
  • Изменить offset суперблока во вторичном поле (т.е. B если флаг == A и наоборот)
  • f(data)sync (сейчас все данные на диске, но всё ещё невидимы)
  • Поменять флаг с A на B (или наоборот) (это коммит, новые данные видимы)
slovazap ★★★★★
()

лок на файл можно выставить. если это posix. там есть такая функция, что-то типа flock.

но rollback, очевидно, нужно будет костылить самостоятельно.

dzidzitop ★★
()
Последнее исправление: dzidzitop (всего исправлений: 1)
Ответ на: комментарий от MyTrooName

Самый верный вариант. А то понагородили какой-то ерунды в треде.

cnupm
()
Ответ на: комментарий от niXman

забыл указать, что читает и пишет файл один и тот же процесс.

По-моему мнению лучший вариант - sqlite. Он не только делает транзакции, но еще и следит за тем, чтобы они реально были транзакционными (суть в том, что одного sync не достаточно для сброса данных, нужно еще чтобы файловая система ничего не оптимизировала, кеш фс в ядре, диск..., вот sqlite учитывает многие такие особенности).

Другой вариант - собственный журнал. Пишешь данные в файл журнала (можно использовать простой формат записей, каждая запись из нескольких частей записанных в разные места файла, объединенных в транзакцию)

    size_t checksum;
    size_t record_length;
    size_t subrecord_count;
    size_t subrecord_offset;
    size_t subrecord_size;
    // offset и size по количеству subrecord
    // далее subrecord_data также по количеству subrecord_count 

Для журнала можно использовать файл размером больше одной записи (транзакции), или нескольких. Файлу делаешь mmap и при записи транзакции сначала копируешь данные в эту журналируемую mmap'ленную память, затем считаешь контрольную сумму, а затем пишешь данные в обычный файл данных. Журнал используется повторно (с начала), когда очередная запись не влезает в конец журнала. Если произойдет падение приложения mmap'ленные данные журнала будут записаны в файл ядром. При запуске программы сначала накатываешь журнал с транзакциями у которых совпадают контрольные суммы (если не совпадают - значит транзакция и не начиналась). Если тебя беспокоит то, что при выключении питания данные могут потерятся - да могут. Но чтобы не терялись, нужна распределенность, без нее даже батарейка на контроллере не поможет (лишь уменьшит вероятность). Для уменьшения вероятности потери данных можно делать msync части журнала с записанной транзакцией в режиме MS_ASYNC после каждой записи в журнал (или в режиме MS_SYNC, но тогда у тебя скорость записи упадет до максимального количества IOPS, которые потянет твой диск от 200 для SATA, для SSD сильно зависит от модели, но на порядок больше).

anonymous
()
Ответ на: комментарий от niXman

Может оказаться, что это сейчас sqlite выглядит оверхедом, а в перспективе наоборот ускорит программу. В sqlite настолько надёжная и заоптимизированная журналируемая запись в файл, что воспроизвести такую же самому не выйдет. Плюс для серии записей данных у sqlite есть транзакции, которые сильно ускорят задачу. Если записи редкие и объёмные, то лучше через запись в фоновый файл и rename

quiet_readonly ★★★★
()
Ответ на: комментарий от MyTrooName

sqlite транзакции умеет весьма коряво, конкурентное чтение-запись вообще печаль...

NiTr0 ★★★★★
()
Ответ на: комментарий от mittorn

это поведение по-умолчанию и в онтопике тоже

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.