pudge — встраиваемая key/value база данных, написанная на стандартной библиотеке Go.

image

Остановлюсь на принципиальных отличиях от существующих решений.

Stateless

pudge.Set("../test/test", "Hello", "World")

Пудж автоматически создаст базу данных test, включая вложенные директории, либо откроет. Нет необходимости хранить состояние таблицы и можно безопасно сохранять значения в многопоточных приложениях. Пудж потокобезопасен.

Typefree

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

	type Point struct {
		X int
		Y int
	}
	for i := 100; i >= 0; i-- {
		p := &Point{X: i, Y: i}
		db.Set(i, p)
	}
	var point Point
	db.Get(8, &point)
	log.Println(point)

QuerySystem

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

   keys, _ := db.Keys(7, 2, 0, true)

Приведенный выше код, является аналогом SQL запроса:

select keys from db where key>7 order by keys asc limit 2 offset 0

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

Parallelism

Пудж, как и большинство современных баз данных использует модель неблокирующего чтения, однако запись в файл блокирует все операции. Но Вы можете создавать/открывать файлы «на лету», минимизируя количество блокировок. В пудже нет ошибки «database already opened». Пример использования в http роутере:

func write(c *gin.Context) {
	var err error
	group := c.Param("group")
	counter := c.Param("counter")
	db, err := pudge.Open(group, cfg)
	if err != nil {
		renderError(c, err)
		return
	}
	_, err = db.Counter(counter, 1)
	if err != nil {
		renderError(c, err)
		return
	}
	c.String(http.StatusOK, "%s", "ok")
}

Engines

Несмотря на небольшой размер, пудж поддерживает два режима хранения данных. В памяти и на диске. По умолчанию пудж хранит данные (значения) только на диске. Но при желании можно включить режим хранения данных в памяти. В этом случае сбрасываться на диск они будут по запросу, либо при закрытии базы.

Status

Пудж используется как в домашних проектах, так и в продакшен, на графике ниже — количество запросов к http серверу на базе пудж, и количество запросов дольше 20 ms



В данном случае пудж включен в режиме полной синхронизации, и, в момент fsync — случаются значительные (более 20 ms) задержки. Но к счастью, их не так много в процентном соотношении.

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

Speed

В репозитории с benchmark'ами можно сравнить пудж с другими базами данных:

Test 1


Number of keys: 1000000
Minimum key size: 16, maximum key size: 64
Minimum value size: 128, maximum value size: 512
Concurrency: 2
pogreb
goleveldb
bolt
badgerdb
pudge
slowpoke
pudge(mem)
1M (Put+Get), seconds
187
38
126
34
23
23
2
1M Put, ops/sec
5336
34743
8054
33539
47298
46789
439581
1M Get, ops/sec
1782423
98406
499871
220597
499172
445783
1652069
FileSize,Mb
568
357
552
487
358
358
358

Пудж очень хорошо сбалансирован по соотношению между скоростью записи и скоростью чтения. Те он не является узкоспециализированной базой данных оптимизированной для чтения или записи. При высокой скорости чтения — сохраняется довольно высокая скорость записи. Которая впрочем может быть еще увеличена за счет распараллеливания записи в разные файлы (как это сделано в LSM Tree движках).

Ссылки на бд, использованные в тесте:

  • pogreb Embedded key-value store for read-heavy workloads written in Go
  • goleveldb LevelDB key/value database in Go.
  • bolt An embedded key/value database for Go.
  • badgerdb Fast key-value DB in Go
  • slowpoke Low-level key/value store in pure Go (based on pudge)
  • pudge Fast and simple key/value store written using Go's standard library

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

Дальнейшее развитие

  • Транзакции. Было бы удобно объединять запросы на запись в пул, с автоматическим откатом в случае ошибки.
  • Возможность ограничения времени жизни ключа (как TTL в memcache/cassandra etc)
  • Отсутствие сервера. Удобно встраивать пудж в существующие микросервисы, но скорее всего появится отдельный сервер. В рамках отдельного проекта.
  • Мобильная версия. Для использования на Android, iOS и в виде плагина для Flutter.

Комментарии (30)


  1. valichek
    06.02.2019 19:44
    +1

    по префиксу умеет? было бы интересно как-то группировать.


    1. recompileme Автор
      06.02.2019 19:47
      +1

  1. tuxi
    06.02.2019 20:49

    Классная штука! Надо будет попробовать. Вопрос: а валидация бд при инициализации есть? На случай предыдущей аварийной остановки приложения?


    1. recompileme Автор
      06.02.2019 21:20

      Валидации нет, только бэкап: https://godoc.org/github.com/recoilme/pudge#BackupAll


      1. tuxi
        06.02.2019 21:34

        А планируется? Было бы здорово на самом деле. В плане бреда, хэш всех ключей записывать например и при старте сравнивать. Или что то в этом роде.


        1. recompileme Автор
          06.02.2019 21:48

          Нет, не планируется. Пудж это простой как гвоздь движок и я постараюсь его таковым оставить. Это скорее в рамках сервера должно быть реализовано. А сервера пока нет


          1. david_mz
            08.02.2019 15:43

            А как тогда понять при запуске, что база повреждена и пора использовать бэкап?


            1. recompileme Автор
              08.02.2019 15:49

              Ошибка прилетит.


              1. david_mz
                08.02.2019 15:50

                Ошибка в ответ на какой метод?


                1. recompileme Автор
                  08.02.2019 16:21

                  Все методы возвращают ошибку вторым параметром, так как прилететь она может откуда угодно. Все надо обрабатывать. В нормально работающей системе никаких ошибок не случается, с одной стороны. С другой стороны, файл может повредиться в результате аппаратного сбоя, или экскаватор перерубит кабель питания, или в дебиан случится кернел паник в конце концов диски «летят» — это случается. Можно писать в файл дважды, это просто, но это никак не поможет если полетел диск. А если он не полетел то и файл не повредится.
                  Итого, когда Вы покупаете сервер, обычно в «довесок» дарят небольшой ftp сервер для бэкапов, физически размещенный на другом сервере. И единственный путь минимизировать потери — это делать бэкапы, зиповать и отправлять их на этот другой сервер и периодически проверять. Либо писать «живую» реплику — если простой на время востановления бэкапа ведет к ощутимым убыткам/недопустим. Тема репликации немного выходит за рамки движка для хранения данных.


  1. taliban
    06.02.2019 21:14

    «Отсутствие сервера. Удобно встраивать пудж в существующие микросервисы, но скорее всего появится отдельный сервер. В рамках отдельного проекта.»
    Обьясните что это значит? Вроде ж бд встраиваемая, что значит «сервер»?


    1. recompileme Автор
      06.02.2019 21:36

      Сервер нужен для взаимодействия с базой по сети и из других ЯП. Да и в гоу иногда удобнее держать БД в виде отдельного сервиса
      Скорее всего сервер будет в виде отдельного проекта и будет поддерживать grps, http и, возможно, memcache протокол (текстовую версию)
      Ссылки ведут на примеры реализаций серверов, не на описания протоколов.


      1. taliban
        06.02.2019 21:44

        Ну это уже получается не встроенная бд а key-value хранилище :) как по мне, редис тут будет сложно переплюнуть, а вот встроенная бд очень даже круче смотрится для данных не очень чувствительных, кеша там или чего подобного.
        Или это планируется как ответвление для других языков, а текущий «встроенный» функционал оставить как есть?


        1. recompileme Автор
          06.02.2019 22:04

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

          Ну и кстати по скорости редис переплюнуть не так уж и сложно, он же однопоточный. Редис хитёр, но не так уж и быстр. Вот бенчмарк чтения/записи в мемкеш в 100 потоков (они примерно сопоставимы, где то мемкеш даже быстрее)
          А вот чтение/запись в 100 потоков пудж и это в режиме ежесекундного fsync в персистенс моде. В инмемори моде пудж намного быстрее. Другое дело что в редисе масса других ништяков и врятли они пересекаются с пуджем


  1. tyderh
    07.02.2019 00:31

    1) Сравнивать по-хорошему нужно с bbolt, а не bolt. Второй немного умер.

    2) Было бы интересно посмотреть на бенчмарки того самого префиксного сканирования.


    1. tyderh
      07.02.2019 01:08

      3) Немного не честно, что в pudge в принципе не делает fsync после каждой записи, а сравниваемый bolt запускается без NoSync. Я думаю, с этой опцией и аналогичным ручным Commit() производительность значительно вырастет.


      1. recompileme Автор
        07.02.2019 01:18
        +1

        У болта включён режим nosync.
        https://github.com/recoilme/pogreb-bench/blob/master/cmd/pogreb-bench/bolt.go#L19


        У пуджа, в той версии, по дефолту был включён принудительный fsync, ежесекундный


        Те все ровно наоборот)


        1. tyderh
          07.02.2019 01:20
          +1

          Ага, извиняюсь. Смотрел в тот файл и в упор не видел, бывает.


    1. recompileme Автор
      07.02.2019 01:12

      1) Болт коварный. Отлично спроектировано апи. Прекрасно работает на маленькой бд и плох, когда база перестаёт вмешаться в память целиком. Вставка деградирует особенно сильно. Кстати, проверить можете сами, элементарно заменив путь в пакете импорта. Там нет отличий. Для него нет бенчмарков на десятках миллионов, так как он очень медленный. Причём даже с выключенным fsync, на ssd. Бболт это тот же болт с мелкими фиксами. Я и код сравнивал и проверял. Постараюсь завтра прогнать хотя-бы десяток миллионов, если не верите, но боюсь это займёт более часа(
      2) Префикс скан требует сортировки. Сортировка миллиона 8 байтных ключей занимает до секунды. Скан в уже отсортированном массиве ключей около 10 ms
      Ps если вы относитесь скептически к пуджу, посмотрите лучше баджер. Я не юзал его в проде, но на бенчмарках он показал себя лучше всех. Только пухнет сильно, ну и учтите что будет копмпакшен, ибо лсм. Но все же думаю он лучше


      1. tyderh
        07.02.2019 01:25

        Хм, играюсь вот с такой базой и не замечал:


        Page count statistics
                Number of logical branch pages: 12306
                Number of physical branch overflow pages: 0
                Number of logical leaf pages: 1247368
                Number of physical leaf overflow pages: 431
        Tree statistics
                Number of keys/value pairs: 45381603
                Number of levels in B+tree: 4
        Page size utilization
                Bytes allocated for physical branch pages: 50405376
                Bytes actually used for branch data: 33244240 (65%)
                Bytes allocated for physical leaf pages: 5110984704
                Bytes actually used for leaf data: 3518247189 (68%)
        Bucket statistics
                Total number of buckets: 2
                Total number on inlined buckets: 0 (0%)
                Bytes used for inlined buckets: 0 (0%)

        SSD, оперативки 1gb на все, база весит уже 4gb. Пойду смотреть, сколько занимают времени вставки.


        1. recompileme Автор
          07.02.2019 01:43

          Поделитесь потом, интересно. Ну и бенчи я прогоню завтра, выложу. У меня когда последний раз смотрел была гиг 30. И тупил по несколько секунд на вставку. Правда ссд похоже криво замаунтился. На другом проекте, болт винт грузил на 100%. Болт плохой.



  1. selenite
    07.02.2019 01:21

    Использую похожую «базу» cznik/ql в одном из проектов, сверху прикручена обертка upper.io/db.v3/sqlite для вменяемого ORM. Лучше бы не пользовался..) но поскольку в gomobile не очень хорошо с sqlite (особенно при сборке через год не обновлявшийся образ докера для сборки AOSP), то приходится страдать.


    1. recompileme Автор
      07.02.2019 01:53

      На телефонах очень медленно в принципе. Даже родной sqlite превращается в жуткого тормоза. При том, что на десктоп это одна из самых быстрых бд. Скан папки Телеграм занимает минут 20. Я не копал в чем там дело, фс, драйвера или железо, но есть такая боль в АОСП


  1. ddidwyll
    07.02.2019 16:38

    Можете в сравнение добавить buntDB?


    1. recompileme Автор
      07.02.2019 17:49
      +1

      Добавил

      Vadims-MacBook-Pro:bin recoilme$ ./pogreb-bench -c 100 -d bench -e pudge -n 500000 -mink 8 -maxk 8 -minv 64 -maxv 64
      Number of keys: 500000
      Minimum key size: 8, maximum key size: 8
      Minimum value size: 64, maximum value size: 64
      Concurrency: 100
      Running pudge benchmark...
      Put: 12.707 sec, 39349 ops/sec
      Get: 0.140 sec, 3569360 ops/sec
      Put + Get time: 12.847 sec
      File size: 41.96MB
      Vadims-MacBook-Pro:bin recoilme$ ./pogreb-bench -c 100 -d bench -e buntdb -n 500000 -mink 8 -maxk 8 -minv 64 -maxv 64
      Number of keys: 500000
      Minimum key size: 8, maximum key size: 8
      Minimum value size: 64, maximum value size: 64
      Concurrency: 100
      Running buntdb benchmark...
      Put: 9.385 sec, 53273 ops/sec
      Get: 0.553 sec, 904339 ops/sec
      Put + Get time: 9.938 sec
      File size: 0.00B


      1. recompileme Автор
        07.02.2019 18:11

        скорость вставки кривая из-за персистенса. Ну точнее не кривая, а вместе с персистенсом (скорее всего бант персистит при закрытии в инмемори режиме, как и пудж, и это время включено в put — github.com/recoilme/pogreb-bench/blob/master/cmd/pogreb-bench/benchmark.go#L129) Поправьте сами если хотите померять чистый пут


        1. ddidwyll
          08.02.2019 16:25

          Спасибо, обязательно попробую pudge в своих проектах.


  1. rumkin
    07.02.2019 17:49

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


    1. recompileme Автор
      07.02.2019 17:56

      Значения хранятся без оверхеда. Можно например сохранить картинку или фильм в пудж — и она будет открываться. Например: pudge.Set(«img/image.jpg»,«imagekey»,[]byte(..imagedata..))
      Ключи хранятся в индексах, занимают размер ключа + 16 байт на ключ