Случилось в конце концов довольно большое событие и я таки добрался до одного из модулей моего телеграммного бота. Это модуль с фразами - там всякие фразы про пятницу, фортунки, пословицы и так далее.
Собственно, идея переписать этот модуль с перла на гошку и тем самым сократить количество потребляемой памяти и заодно ускорить работу модуля.
Что не устраивало в оригинальном модуле, так это сам перл. Есть ряд смущающих меня особенностей работы этого самого перла. Да и сам язык медленно но верно отходит в историю, как таковой.
В оригинале модуль... плагин... сервис... вобщем, демночек, реализующий эту функциональность написан на перле, в качестве ивент-лупа, на котором построено приложение, запользован фреймворк Mojolicious (достойный фреймворк, надо сказать). Для интерконнекта задействован Mojo::Redis, который, в теории, позволяет использовать биндинги к либе hiredis через Protocol::Redis::XS, но этот модуль уже довольно приличное время не мэйнтэйнится и не собирается с современной версией lib hiredis. Соотвественно, Mojo::Redis работает на pure perl реализации протокола redis, что не супер быстро. Ну, и понятное дело, где-то надо хранить данные. Можно, конечно, хранить их в текстовых файлах или в json-чиках, но вычитывать из этого данные - процесс не быстрый, да и нагрузка на диск (относительно) велика. Я взял BerkeleyDB в лице CHI::Driver::BerkeleyDB для складывания кармы фраз. И SQLite_File для складывания статических данных в tied hash с последующим извлечением из этого хэша данных, как из ro-таблицы. Всё это в куче работает с удовлетворительной скоростью для бота. Тем более, что популярность генерируемого этим демоночком контента не то чтобы очень велика.
Технически я давно тому назад пробовал взять Tie::LevelDB и в принципе оно работает неплохо, но... это ещё один компонент в и без того сложном приложении (а в тот момент это был весь бот целиком в "одном флаконе", монолитное приложение).
Соответственно, при планировании приложения с аналогичным функциями, но на гошке, надо было оперировать тем, что есть для этого ЯП. Писать сервис 1 к 1 я изначально не планировал, т.к. это слишком затратно. Была идея воспользоваться тем, что есть.
После беглого анализа, я выяснил, что bdb в гошном исполнении нету, зато есть старые знакомые - плеяда различных вариаций на тему leveldb/rocksdb и их подражателей/продолжателей.
Оригинальная реализация leveldb - это у нас заброшенный проект биндинга к сишной либе.
Есть и другие биндинги к сишной LevelDB, например, Levigo. Понятное дело, что это сразу "нет", привязываться к сишке без особой надобности что-то не очень хочется.
Go-leveldb - это чисто гошная реализация leveldb. Более менее полная и вполне себе работающая.
Pebble - это вариация на тему RocksDB. Неполная реализация, нужная CockroachDB для работы.
GoRocksDB - это биндинг к сишной rocksdb. И там пацаны намекают, что у них всё может поменяться в любой момент. Типа, если нужна стабильность - вендорите либу.
Далее, есть в природе BadgerDB и BoltDB эти бд тоже навеяны идеями leveldb/RocksDB, но реализуют эти идеи в довольно витиеватом api. Работать с ними, скажем так - сложновато, городить специальные врапперы, упрощающие этот процесс, я смысла не увидел, особенно при наличии более простых альтернатив.
Также имеется бд Pogreb, в некотором роде напоминающая LevelDB. В ней есть всё, что надо для k-v базы: методы Get и Put и возможность последовательных bulk-операций. А также метод Has и Delete. Среди других методов имеется метод Count, который возвращает... почему-то uint32. Хотя значений в бд можно натолкать как будто uint64, а то и больше. Собственно, этот момент меня смутил.
Поныкавшись, помыкавшись, я пришёл к тому, что мне вполне подходит как leveldb, так и Pebble. И тут начинается интересное.
Я почему-то запомнил, что в перле я на каждый чих открывал и закрывал базу. И в этом подходе есть свой плюс: не надо корректно завершать работу с бд при выходе из программы. Просто сваливаешь и всё.
Что же я обнаружил в гошке при таком подходе? А при таком подходе, если писать в бд по одному значению или мелкими пачками, остаётся много файлов rdb или ldb, в зависимости от задействованной бд. То есть фактически немного неправильно отрабатывает компакция и при каждом сбрасывании wal-а в файлы (а такое происходит, если запись производить синхронно, что обычно и делают, если особо никуда не торопятся и не хотят в случае внезапного падения приложения или отключения сервера потерять данные) создаётся новый уровень/"бакет" бд. Системе "проще" создать ещё один файлик без индекса, чем создавать индекс и записывать значение в уже существующий. И таких файликов остаётся... тысячи, при соответсвующих объёмах вставок данных.
Понятное дело, что при чтении таких файлов мы не попадаем в индекс нормальным образом. Это тормозит процесс выборки. И в результате на параллельной выборке мы упираемся в то, что база уже открыта и мьютекс открытой базы не даёт другому читателю открыть базу на чтение и мы получаем ошибку конкурентного доступа к данным.
Понятное дело, что это некорректный способ работы с такими бд. Подразумевается, что при первом обращении БД "открывается", а закрывается она только при завершении работы приложения. Собственно, в перле так и происходило: на тестовых заливках баз я, при запуске программы, делал Tie(), заливал данные в tied hash и только перед выходом делал Untie(). И там я наблюдал прекрасную картину, когда десятки тысяч значений прекрасно ложились в пару файлов .ldb и всё в этом смысле было хорошо и быстро. А если издеваться над такой бд массовым чтением, то наткнуться на "занятую" базу и сломаться об это не получалось.
Таким образом, выходит, что мне, при запуске программы, нужно открывать все статичные базы. А динамические базы, к которым rw-доступ, надо открывать по мере обращения, а "дескрипторы" складывать в мапку. Но самое весёлое, что нам надо уметь корректно закрывать все открытые бд, а это значит ровно одно: надо написать свой обработчик сигналов, т.к. демоночек завершить работу корректным образом может и будет по сигналу супервизора процессов, то бишь init-а (в том или ином виде). Дело не сложное, но в простом случае оно означает, что мне понадобятся глобальные переменные, что осложняет создание тестов (если я вдруг сподвигнусь и решу оные написаь)... либо придётся везде носиться с дескрипторами баз, как со списанной торбой.
Сказано - сделано. Я начал открывать базки и удерживать дескрипторы открытыми на протяжении всего сеанса работы демоночка и это решило проблемы с "многофайловостью" баз. Другая проблема, которая меня заботила - это отсутствие метода Count() при обращении к бд. То есть, у нас имеется итератор, но количество ключей мы не знаем. А мне для некоторых кейсов эта инфа была бы очень полезна. Не итерироваться же "до упора", чтобы выяснить количество ключей! Пришлось соорудить костыль в форме отдельной базы count_db, которая хранит количество ключей для каждой бд, которой эта инфа нужна для работы.
Остаётся вопрос - почему pebbledb, а не goleveldb? Ответ простой, в своих тестах я сражался именно с pebbledb, на ней я более-менее отработал функциональность. Других причин нету. Для моих нужд обе бд подходят в равной степени удовлетворительно. Возможно, конкретно в случае с этим демоночком я всё же пересмотрю текущее положение дел и возьму другую бд, благо импортировать данные труда особого не составляет. И дополнительным моментом является то, что сейчас я пишу фронт-энд для протокола irc для этого самого бота и в нём надо где-то и как-то хранить настройки для канальчиков и это также задачка для k-v базы. И, если мне именно leveldb покажется более удачной в данном случае - это будет поводом навестить демоночек с фразами и заместить pebbledb на leveldb... или нет.