Хабрахабр

slowpoke — не самая быстрая база данных

Всем привет.

Slowpoke обладает минималистичным, удобным апи и подходит для решения довольно широкого круга задач. slowpoke это key/value хранилище данных, написанное на стандартной библиотеке golang.

Записать значение в slowpoke можно при помощи команды Set:

slowpoke.Set("db/some.db", []byte("foo"), []byte("bar"))

Единицей хранения данных в slowpoke является файл. В данном примере — будет создана директория «db», с файлом «some.db», в который будет помещено три байта («bar»).
Строго говоря каждый файл в slowpoke — является независимой базой данных. Каждая база данных обслуживается в отдельной goroutine. Открытие/чтение файла осуществляется автоматически. Т.е. если данная база данных существует — она будет открыта и прочитана. Если нет — создана. Данная особенность позволяет не думать о состоянии базы при работе с ней и использовать например в обработчиках http запросов или в других goroutine.

В примере выше в качестве ключа и значения по сути использованы строки. В качестве ключа и значения slowpoke принимает массив байт. Запишем в slowpoke числа:

for i := 0; i < 40; i++ { id := make([]byte, 4) binary.BigEndian.PutUint32(id, uint32(i)) slowpoke.Set("numeric.db", id, nil)
}

Мы конвертировали числа в формат BigEndian, что позволит корректно учитывать сортировку ключей при дальнейшей работе. Значение в данном примере не заданно, и будет создан только массив ключей (размер «numeric.db» будет 0 байт). Кстати о ключах, ключи в slowpoke — хранятся в памяти, но сохраняются на диск.

По этой причине, не рекомендуется использовать ключи большого размера. Это позволяет быстро оперировать с ключами с одной стороны, и закрывать таблицу в случае нехватки оперативной памяти с другой стороны. При превышении данного размера — лучше разбить на несколько таблиц базу данных. Предполагаю что оптимальным суммарным размером всех ключей для одной таблицы будет 10 Мегабайт. Суммарный размер таблицы значений не может превышать uint32. Значения же в памяти не хранятся и могут быть любого размера (картинки, фильмы). Некоторые преимущества данного подхода (раздельное хранение ключей и значений) описаны в статье www.usenix.org/system/files/conference/fast16/fast16-papers-lu.pdf (в данной статье есть акцент на ssd диски Lsm tree и прочие архитектурные изыски не использованные в slowpoke)

Помимо чисел, строк, файлов и тп, можно сохранять и объекты, сериализуя их одним из методов. Вернемся к slowpoke. Пример сериализации в json:

binary, _ := json.Marshal(post) slowpoke.Set("json",key,binary)

Или использовать встроенный в golang пакет gob: golang.org/pkg/encoding/gob

Дело в том, что современные файловые системы при записи в файл, пишут по сути в буфер. Все команды записи в slowpoke атомарны, и завершаются командой sync (синхронизация данных). Большинство баз данных имеет режим nosync (может называться по разному, но суть в том, что операция синхронизации очень медленная, особенно это заметно на старых винчестерах и для победы в бенчмарках и для ускорения записи используется данный режим). И в случае падения операционной системы буфер будет потерян. Хороший обзор на тему Crash Vulnerability.

В slowpoke нет режима «nosync», поэтому:

Для вставки нескольких записей в slowpoke используется команда Sets.

В этом случае данные будут записаны в файловый буфер и синхронизированы только один раз, по окончании записи. Данная команда рекомендуется для записи пакета ключей/значений. Пример использования команы Sets: Что драматически увеличивает скорость записи с одной стороны, позволяет не жертвовать сохранностью данных в целом, с другой стороны.

var pairs [][]byte for i := 0; i < 40; i++ b, _ := json.Marshal(post) pairs = append(pairs, id) pairs = append(pairs, b) } //store posts fast slowpoke.Sets(posts, pairs)

В примере выше, сначала формируется массив пар ключ/значение, затем осуществляется его запись.

Но прежде чем перейти к командам чтения, расмотрим команду для выборки ключей из slowpoke. Мы научились сохранять данные в slowpoke при помощи команд set и sets.

Для работы с ключами в slowpoke встроена команда Keys.

Ключи могут быть извлечены из slowpoke:

  • В прямом порядке
  • В обратном порядке
  • Со смещением
  • От определенного значения
  • Выбраны по префиксу

Пример команды:

//get last 2 post key with offset 2 limit := uint32(2) offset := uint32(2) order := false //descending keys, _ := slowpoke.Keys(posts, nil, limit, offset, order)

Для команды Keys действует ряд правил:

Если limit не задан будут возвращены все ключи.

Если задано поле from (не равно nil), будут возвращены ключи после данного значения (само значение включено не будет).

Если подходящих под условия ключей нет, будет возвращен пустой массив.

Соответственно, для получения корректно отсортированных чисел, например, необходимо конвертировать их в BigEndian. Ключи внутри представлены в виде массива байтов.

Внутри, в slowpoke, ключи продублированы в массивах (slice), каждый slice создается под таблицу и сортируется в момент вызова Keys (если необходимо). Команда Keys манипулирует данными в памяти, и не обращается к диску. Команда Keys предпочтительна по сравнению с Get/Gets по причинам изложенным выше. При выборке по префиксу/значению используется бинарный поиск (если возможно).

Например, в базе могут храниться теги tag:id или email:username ключи. Помимо возможностей паджинации, выборки следующего значения, команда Keys позволяет выбирать ключи по заданному префиксу. Также напомню о возможности создания ключей без значений, что может быть удобно для хранения индексов в памяти (даже если ключи без значений, они будут сохраняться на диске, для возможности восстановления в случае сбоя или закрытия таблицы). В данном случае необходимо передать в поле from значение с символом * на конце, например []byte(«sex:*»).

Для выборки значений, в паре с командой Keys — удобно использовать команду Gets.

результат команды Keys). Команда Gets получает на вход массив ключей (т.е. Пример:

keys, _ := slowpoke.Keys(posts, nil, limit, offset, order) //get key/ values res := slowpoke.Gets(posts, keys)

Результатом команды Gets будет массив пар ключ/значение. Обратите внимание, Gets:

— единственная команда не возвращающая ошибки
— если одного из ключей нет, он будет пропущен
— если нет ни одного из ключей — будет возвращен пустой массив

Что может быть использовано для сохранения данных в другой таблице, например. Также команда Sets — принимает на вход пары ключ/значение.

Keys — возвращает массив ключей
Gets — принимает массив ключей, возвращает массив пар
Sets — принимает массив пар

Наверное излишне говорить что фунукции для работы с группами значений — быстрее их аналогов для работы с одним значением.

Для выборки одного значения — используется команда Get.

res, err := slowpoke.Get(file, key)

В случае отсутствия значения Get вернет nil и ошибку ключ не найден.

Команда CloseAll() — закрывает все активные базы данных и должна быть вызвана по окончании работы, пример:

sigchan := make(chan os.Signal, 10) signal.Notify(sigchan, os.Interrupt) <-sigchan //закрытие всех баз данных в случае прерывания работы slowpoke.CloseAll()

Любо так: defer slowpoke.CloseAll()

Мы рассмотрели основные команды slowpoke, но есть еще продвинутые команды.

Команда Open — открывает базу данных и считывает ключи в память.

Команда Close — закрывает базу данных и выгружает ключи из памяти.

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

Пример 1: вы храните логи

При любом запросе к данной БД — она будет автоматически открыта и считана. Лог за интервал времени собирается в массив пар и вставляется при помощи команды Sets
И записывается, например в файл, соответствующий дате логов, например «logs/20170101.db»
По окончании работы с данным интервалом — база данных «logs/20170101.db» — может быть закрыта, и выгружена из памяти при помощи команды Сlose.

Пример 2: цепочка блокчейн-транзакций

Возможно будет уместно хранить каждый пул транзаций в отдельном файле, например 1000.db — соответствует транзакциям с 1 по 1000, и выгружать из памяти, по окончании записи, если обращение к данному блоку сравнительно редкое. Допустим, каждая транзакция имеет номер.

Пример 3: хостинг сайтов

Так как ключи в slowpoke хранятся отдельно от значений, чтение в память выгруженных ключей из файла — быстрая операция. можно хранить данные каждого сайта в отдельной папке, например sites/mysite.com.db
и выгружать из памяти после обновлений, если чтение из них сравнительно редкое.

также есть команда удаления базы данных DeleteFile()

Архитектура

Для хранения ключей используются map и slice. slowpoke написана на стандартной библиотеке golang. Масштабирование достигается запуском каждой базы данных в отдельной goroutine.

В проекте не использованы ни BTree, ни LSM Tree. Значения хранятся как есть, без оверхеда и только на диске, ссылки на адреса значений хранятся в памяти. Как показывает практика — чем более сложна архитектура базы данных, тем с бОльшим количеством проблем предстоит столкнуться с ростом базы. Также не используется технология mmap (и не планируется). Так как ничего на бывает бесплатно.

Обратной стороной простоты архитектуры является более низкое быстродействие slowpoke по сравнению с лидерами рынка, во всяком случае на типовых замерах:

//macbook 2017 slowpoke vs boltdb
//The 100 Set took 19.440075ms to run./19.272079ms
//The 100 Sets took 1.139579ms to run./?
//The 100 Get took 671.343µs to run./211.878µs
//The 100 Gets took 206.775µs to run./?
//The 100 Keys took 36.214µs to run./?
//The second 100 Keys took 5.632µs to run./?

Например, в базах данных использующих LSM Tree (rocksdb,leveldb,cassandra,badger) запись будет более быстрой.

В базах данных использующих mmap — (LMDB, Boltdb, SophiaDb) чтение значений будет более быстрым (slowpoke не кеширует значения)

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

Также потихоньку пилю более масштабный проект на этом движке. Для использования slowpoke на языках программирования отличных от golang написан пример небольшого сервера баз данных на http.

GRPC-сервер с бэкдором в виде REST API

Теоретически она хорошо вписалась бы в проекты предполагающие хранение больших объемов данных — ipfs.io или Ethereum как альтернатива более комплексным базам данных типа leveldb/badger. На данный момент slowpoke молода и мало распространена, используют в основном друзья, на своих личных пет — проектах (типа телеграм бота, сайта tggram.com, местами), как апи для сайта snatchnews.com (в мобильном приложении, через rest api).

Буду рад пул реквестам.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть