Pull to refresh

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

Reading time 7 min
Views 11K
Всем привет.

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

Записать значение в 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++ {
		id := make([]byte, 4)
		binary.BigEndian.PutUint32(id, uint32(i))
		post := &Post{Id: i, Content: "Content:" + strconv.Itoa(i), Category: "Category:" + strconv.Itoa(i/10)}
		b, _ := json.Marshal(post)
		pairs = append(pairs, id)
		pairs = append(pairs, b)
	}
	//store posts fast
        slowpoke.Sets(posts, pairs)

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

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

Для работы с ключами в 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.

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

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

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

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

	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: хостинг сайтов

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

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

Архитектура

slowpoke написана на стандартной библиотеке golang. Для хранения ключей используются map и slice. Масштабирование достигается запуском каждой базы данных в отдельной 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

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

Буду рад пул реквестам.
Tags:
Hubs:
+21
Comments 39
Comments Comments 39

Articles