Как я добавила групповой коммит в свою LSM-базу на Go и не пожалела

от автора

Введение

Синхронный WAL очень частое явление в базах данных, делая их durability максимальной. При таком исходе каждый батч записи это вызов fsync, и это дало мне 956k opr/s на 16кб значениях , звучит хорошо, но на самом деле: скорость записи упала в 5 раз.

Поскольку в планах создать конкурентноспособное хранилище, я смотрела на то как справлялись с этим замедлением в RocksDB. И, если вы знаете хранилища на Go, где есть групповой коммит, то поделитесь пожалуйста, потому что я не смогла найти такого.

В этой статье я расскажу:

Что такое групповой коммит на пальцах

Почему групповой коммит не для финтеха

Как это реализовано у меня

Как изменились цифры до и после внедрения

Во сколько раз ScoriaDB с group commit быстрее BadgerDB и Pebble.

Если вы пишете хранилище, логгер, кэш или просто любите копаться в LSM‑движках — добро пожаловать на борт, нас ждет короткое путешествие.

Что такое групповой коммит

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

Вам нужно скинуть другу их 10 штук, приходится отправлять поштучно, а каждая фотография это отдельное сообщение и уведомление, что весьма неудобно и времязатратно.

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

Групповой коммит это почти тоже самое, что отправка фоток одной пачкой. Вместо того, чтобы писать все записи по отдельности и после каждой делать запрос на диск, fsync делается только на пачку.

Почему почти ?

Групповой коммит работает по таймеру, в течение которого накапливаются данные. Не по количеству, как с фотками ( напомню, что чаще всего одним сообщением 10 фоток) потому что идея считать сколько записей не очень производительна.

И этот интервал времени, по мере которого данные будут принадлежать одной пачке, необходимо задать — в ScoriaDB это 10 мс.

В ScoriaDB этот режим включён по умолчанию. Для финансовых задач не рекомендуется групповой коммит.


2. Почему групповой коммит не для финтеха

Потому что там недопустима потеря записи.

Если питание отключится за эти 10 мс — пачка транзакций исчезнет безвозвратно.

Чтобы не произошло катастрофы там используют синхронный режим: fsync после каждой записи.

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

Как отключить?

При открытии базы передайте параметр GroupCommitEnabled: false через внутренний API. (В след релизе появится простой флаг в публичном конструкторе)

Когда точно можно:

  • Логи, метрики, трейсы. Если при краше вы потеряете последние 10 мс логов — мир навряд ли рухнет.

  • Разработка и тестирование. Когда база гоняется локально и накапливает тонны данных — скорость важнее, чем мифический краш посередине теста.

  • Любая аналитика, где важна пропускная способность, а не каждая запись. Например, видеонаблюдение.

  • Кэш, сессии, временные данные. Они и так живут в памяти, а на диске, как подстраховка от перезапуска.

  • Большие пакетные вставки данных (ETL, миграции, бэкапы). Там важна итоговая сумма, а не каждая промежуточная запись.

Когда можно, но с оговорками:

  • IoT-сенсоры, показания счётчиков. Если потеряется — ничего страшного, следующий цикл перезапишет. Но строго нет, если это датчик критичный или повлечет денежные убытки, даже за 10мс.

  • Игровая аналитика (действия игроков). Потеря нескольких кликов за 10 мс незаметна на фоне общего трафика. Но покупки внутри игры — уже финансы, там отдельно.


3. Как это реализовано у меня

У него есть три простых вещи:

  1. Буфер — обычная переменная в памяти. Вы кидаете туда свои записи, они лежат и ждут своей очереди.

  2. Интервал — 10 мс.

  3. Канал для сигнала «Сбросить на диск сейчас» — при закрытии базы можно сбросить все, даже если таймер еще не прошел.

Как работает этот цикл:

  • По истечению таймера, или сигналу из канала, он вызывается.

  • Смотрит буфер: если там ничего нет, спим дальше.

  • Что-то пришло? — тогда :

    1. Берёт всё из буфера

    2. Записывает в файл WAL

    3. Просит диск это зафиксировать через fsync

    4. Очищает буфер

А как же Deadlock?

Для этого продуман мьютекс. Один заходит — закрывает, кладёт запись, выходит — открывает. Остальные ждут своей очереди.

Где лежит код?
Вот ссылка на тот файл в репозитории:


4. Как изменились цифры до и после внедрения

Железо: Intel Core i3-1215U (8 потоков), NVMe SSD

Среда: Linux, Btrfs (тоже имеет значение)

Синхронный режим

Размер

ops/s

Задержка

16 байт

956 000

1 126 нс

4 КБ

215 564

4 639 нс

Групповой коммит

Размер

ops/s

Задержка

Прирост

16 байт

1 438 000

695 нс

+62%

4 КБ

224 300

4 458 нс

+4%

Для маленьких значений прирост значительный – в 1,62 раза. Для 4 КБ разница скромнее, ведь узкое место уже не WAL, а копирование данных в Value Log и работа с диском (проблема, которую я решу в след. релизе).

И, короночка, как изменилась производительность WAL

Групповой коммит сильнее всего ускорил сам журнал предзаписи

Режим WAL

ops/s

Задержка (нс/оп)

Прирост

Синхронный (fsync на запись)

2 392 000

418

Групповой коммит (таймер 10 мс)

10 537 000

94,9

+340%

Конкурентная запись (8 потоков)

Режим

ops/s

Задержка (нс/оп)

Прирост

Синхронный

1 432 000

698

Групповой коммит

5 917 000

169

+313%

Масштабируемость на 8 потоках улучшилась в 4,1 раза – групповой коммит снимает конкуренцию за мьютекс WAL.

Также он никак не влияет на чтение – MVCC гарантирует, что читатели не блокируются писателями.

P.S. Как повторить мои бенчмарки

git clone https://github.com/f4ga/ScoriaDB.gitcd ScoriaDBgo test -bench='BenchmarkPut' -benchtime=5s -count=5 -benchmem ./pkg/scoria/

Цифры будут зависеть от вашего железа. У меня проц слабоват и, кажется, эти бенчи кратно сократили жизнь ноуту… С современным диском и >8 ядер, может быть ещё быстрее. Будет интересно, что выйдет на HDD.


5. Во сколько раз ScoriaDB с group commit быстрее BadgerDB и Pebble

Все бенчмарки запущены на одном железе

База данных

Режим записи

Durability

ops/s (16 байт)

ScoriaDB

group commit (таймер 10 мс)

Подтверждение диском через 5 мс (в среднем), max 10 мс

1 438 000

Pebble

sync per op (fsync на каждую запись)

Каждая операция гарантированно на диске после возврата

472 000

BadgerDB

sync per op (fsync на каждую запись)

Каждая операция гарантированно на диске после возврата

171 000

Для справки: ScoriaDB в режиме batch sync (fsync на каждый батч, без группового коммита) даёт 956 000 ops/s – это тоже выше, чем у конкурентов

ScoriaDB с групповым коммитом даёт в 3 раза больше операций в секунду, чем Pebble, и в 8,4 раза больше, чем BadgerDB.

Таблица иллюстрирует, какой выигрыш даёт переход к групповому коммиту. Но открою секрет, она и без него была в пару раз быстрее.

Контракты надежности принципиально разные, но Групповой коммит в ScoriaDB можно отключить.


7. Итог

Групповой коммит — это честный инженерный компромисс.

  • Плюс: 1,44 млн операций в секунду (прирост в 1,6×).

  • Минус: задержка до 10 мс и потеря последней группы при краше.

ScoriaDB все же отличается своей сборкой решений. Здесь есть MVCC (многоверсионность), Column Families (отдельные LSM-деревья), встроенный gRPC/REST-сервер, и все не на плюсах, а на гошке.

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

Исходный код

README на двух языках, дока только на английском.


Спасибо, что дочитали до конца. Буду рада поддержать обсуждение и ответить на вопросы, надеюсь удастся прочитать ваши issues или PR.

ссылка на оригинал статьи https://habr.com/ru/articles/1043820/