Как стать автором
Обновить

Комментарии 81

А не надо ему верить. RESP не проприетарный, и, как весь остальной Redis, идет под BSD. Вот описание протокола прямо на сайте: https://redis.io/topics/protocol

Спасибо, я доверился db-engine и не проверил.
Пофиксил.

Боже мой.
Ваши тарантуло — редисы — эластксерчи и прочее НЕ ЯВЛЯЮТСЯ БД и/или СУБД это key/val хранилища данных. Что бы там они не умели они не удовлетворяют acid критериям.
OLTP — относится к реляционным субд. Ключевое слово Transaction. Какие, боже упаси транзакции в редисе? Те костыли, которые клепаются не являются полноценными транзакциями.
Что это за понятие такое "первичные" и "втоичные" индексы? Первичными бывают потенциальные ключи. Ключ — это не индекс. Ключ это ограничение целостности которое может быть связано с индексом.
Вторичных, третичных, четвертичных ключей и индексов не бывает.
Бывают кластерные, плотные и разряженные индексы. Быают первичные ключи, внешние ключи.
Это, если в терминах рсубд.


Поддержка lua это еще не хранимая процедура. В терминах рсубд. Это просто скрипт. Он отсутствует в метаданных и запускается как простой скрипт.


Итого по п 1
Вот это вы "разобрались"… Намешали в кучу всего что можно и нельзя.


п 2 тест…
Этапять… Вы попробуйте не циферки загружать, а нлрмальные объемы текстовых данных. И пусть тарантуло по ним ещё ваши индексы построит.


Репликация это не "если упал один узел"… Это называется stand-by в полноценных системах
Репликация это к распределенности системы.
Повышение стабильности и независимость узлов субд.
Что такое шардирование, почему и от чего — разбирайтесь сами, говорит нам автор.


Автор. Вторичных ключей, как и индексов тоже не бывает.


Redis и Tarantool — однопоточные базы данных, что не позволяет распараллелить аналитические запросы.
Полный привет. Каким боком вы определяете многопоточность с аналитическими запросами?


Итого. Что редис что тараниул это хранилища с натягиванием совы на глобус в виде неких транзакций.
Никаким боком они не могут заменить и быть конкурентом любой рсубд.
Однопоточность — основная причина быстродействия и невохможности обработки множества параллельных запросов.
Если начнут в многопоточность — превратятся в тыкву.
А уж in-memory таблицы есть у всех практически рсубд.
Как инструмент вспомогательный — удобно.
Как основа для хранения… боже уапаси.

Я только про одно утверждение спрошу: откуда такая категоричность, что "Вторичных… индексов не бывает."?
Кажется, вполне взрослый и понятный термин, разве нет? Я сперва хотел отправить в гугл с фразами "вторичный индекс" и "secondary index", но решил просто спросить: можно какие-нить ссылки, где про это же мнение (что их не бывает) почитать можно? Мне правда интересно.

Ответьте для себя на вопрос.
Что такое индекс. Каков его физический смысл.
И что такое "потенциальный ключ".
И вы поймёте, почему он не можеи быть первичным, а ключ — может.
А вообще в комменте моем написано отчего и почему
Если что то гуглится, то это не означает, что информация верна.

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


Если что то гуглится, то это не означает, что информация верна.

Может быть. Но может и не быть.

Какие ссылки? Открываем книгу. Напимер, Дейта — введение в Системы баз данных или Ульмана и вперед.
Параллельно изучаем документацию на уже существующие.
Послушайте. Позиции других комментаторов заключается в следующем: вот, я нагуглил, что кто-то где-то упомянул. Значит я прав. "
Простая копипаста. Это считается сейчас "весомым аргументом".
По тексту уже видно смешение терминов, которые нельзя заменять…
То индекс первичный, то ключ… Это вообще разные по смыслу, стрктуре и назначению вещи.
Такое их употребление говорит о непонимании этого. И так — многое.

Ваши тарантуло — редисы — эластксерчи и прочее НЕ ЯВЛЯЮТСЯ БД и/или СУБД

Даже Эксель вполне себе может являться СУБД, а экселевский файл — БД. Система алфавитного хранения книжек в школьной библиотеке — СУБД. Вы путаете СУБД и РСУБД.

Вторичных, третичных, четвертичных ключей и индексов не бывает.

dev.mysql.com/doc/refman/5.6/en/innodb-index-types.html
Вторичный индекс — вполне себе нормальный термин. Если вы привыкли к терминологии кластерных / некластерный индекс, то это вообще говоря ваша проблема.

Не может.
Изучите вопрос, что такое субд. Из чего состоит и как хранит информацию о БД.
К сожалению. Это проблема не моя. Это проблема того, кто не понимает физического смысла индексной структуры.
Так расскажите. Чем по вашему отличается "первичный" индекс от "вторичного".
Мне прямо интересно послушать именно вашу интерпретацию. Не ссылки на сайты, а ваше мнение.

dev.mysql.com/doc/refman/5.6/en/innodb-index-types.html
Вторичный индекс — вполне себе нормальный термин. Если вы привыкли к терминологии кластерных / некластерный индекс, то это вообще говоря ваша проблема.

Вообще, печально, что понятия смазываются и перепутываются, лет 15-20 назад были четкие и однозначные понятия и за такое запутывание, как в статье, автора бы долго били канделябром по голове. Решил погуглить, так в рунете теперь даже вторичные ключи с foreign ключами путают, которые, кстати, могут быть по полям вообще без индексов. В общем грустно ото всего этого.
По хорошему, индексы и ключи — это разные сущности, хотя да, как правило ключ это поле, или поля по которым построен индекс. Однако у них разные области применения.
И праймари кей не всегда обязан быть кластерным, например в Оракле его можно сделать некластерным, правда кластерный индекс все равно будет построен.

Индекс это отдельная структура, построенная по данным из таблицы. Индексов можно нафигачить сколько угодно, больше чем полей у таблицы и все они будут просто индексами.
Однако для того, чтоб мы могли однозначно идентифицировать запись в таблице, мы выбираем какое-то поле, или группу полей таблицы и говорим — это первичный ключ. В таком случае индекс по этому полю, или по этой группе полей, обязан содержать уникальные значения. То есть ключ это не физическая сущность, не отдельная структура, а больше логическая сущность, метка в нашей таблице. Вторичный ключ, это какое-то другое поле, или группа полей, которая тоже может использоваться для однозначного идентифицирования записей. Например, автоинкрекментное поле-идентификатор в таблице товаров это первичный ключ, а наименование товара — вторичный, у нас не должны быть разные товары с одинаковыми названиями.
А foreign ключи, это связи нашей таблицы с какими-то первичными ключами другой таблицы и в нашей таблице по полю foreign ключа может и не быть индекса вовсе.

Зачем путать понятия ключей и индексов, какой в этом смысл?
Ой, прошу прощения, ничего в статье не путается, просто называются все некластерные индексы вторичными индексами, вот и все. Никакой путаницы с ключами там нет, ключи отдельно, индексы отдельно. Жаль нельзя больше редактировать первое сообщение, а то накатал полотенце не по делу. :(

Вы все правильно написали.
Ммешались в кучу кони, люди…
"Redis поддерживает первичные индексы, не поддерживает вторичные"
И ниже
"В Tarantool можно строить произвольное количество вторичных индексов для данных:


Вторичные ключи могут состоять из нескольких полей."
И ниже
"Вывод


Вторичные ключи и удобные итераторы позволяют строить в Tarantool реляционные модели хранения данных. В Redis такую модель построить невозможно."


Я уж не говорю о" реляционной модели" без fk и каскадных операций.


Да там вообще все прекрасно

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

Про терминологию наверно ответа нет: и те и другие термины гугляться, у некоторых терминов контекст пересекается.


А уж in-memory таблицы есть у всех практически рсубд.
Как инструмент вспомогательный — удобно.
Как основа для хранения… боже уапаси.

Вот здесь, мне кажется, вы себя ограничиваете. И в redis и в tarantool выполнено надежное хранение in-memory таблиц, в redis чуть похуже архитектурно, в tarantool получше.

Вы не понимаете, почему их нельзя назвать субд?
Все дело не в какой то там "надежности" хранения в ОЗУ. Данные там живут ровно до момента выключения питания.
На этом вся "надежность" заканчивается. Чтобы обеспечить восстанавливаемость, нужно прибегать к чему? Правильно. К некоему энергонезависимому хранилищу.
Дамп же!
В субд для этого используется транзакционный лог. Чувствуете разницу?
Надежность в рамках субд это непротиворечивость данных. Понимаете? Каждый момент вркмени, сложно организованные данные в БД находятся в непротиворечивом сострянии.
Для этого и был создан механизм двухфазной фиксации транзакций.
Который умеет обрабатывать параллельно многие тысячи запросов в секунду… Разруливая все изменения, сохраняя целостность данных.
Что же могу предложить редис с тарантулом? Дамп? Если его сохранять часто при больших объемах данных и однопоточной архитектуре, то от производительности останется пшик. Так как дисковая система — та же, что и для обычной субд. Все запросы выстроятся в очередь, пока база скинет дамп… При большой входящей нагрузке и низких таймингах дампа кратно повышается риск потери целого или части данных. Исходя из практик эмуляции структур. В одном месте поменяли в 3 других не успели. Все. Превед медвед. Образовался артефакт. Данные находятся в противоречивом состоянии. Это я не говорю о том, что никаких ограничений на изменение данных в любом виде нет. От слова вообще. Что такое типы данных… Одна лишь строка.
Попробуй добавь кортеж в отношение произвольными данными в субд. Получится? Нет. А тут — на раз два.
Так что. Key value
В принципе — ничего нового. Именно это же есть в ораклине точно.
Кэш индексных структур? Он и так есть в любой субд. Или вы считаете, что каждый раз субд читает файлы?
Кэш таблиц в память? Тоже есть.
Брать терминологию рсубд и пытаться выдать связанные списки за полноценную субд — глупость.
Это очень нишевые инструменты. Да, они хорошо заточены для определенных целей. Но только для них.
Пытаться микроскопом гвозди забивать — глупо.

Спасибо за разъяснения, но их сложно соотнести конкретно с Redis и Tarantool.


Данные там живут ровно до момента выключения питания.

  • И Redis и Tarantool имеют механизм записи всех транзакций в журнал прежде чем сгенерировать ответ клиентскому приложению.

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

  • И Redis и Tarantool периодически неблокирующе сохраняют снапшот in-memory данных. Redis с помощью fork, Tarantool с помощью read view.

in-memory архитектура хранит (не кеширует) сразу все данные и индексы в памяти, тем самым избавляясь от механизма кеширования/вытеснения страниц данных с диска/на диск. Какие именно данные уже зависит от архитектуры хранения конкретной БД.


Например, вы можете установить Tarantool и воспользоваться SQL, и соответствующими структурами как в традиционных РСУБД.


https://www.tarantool.io/en/doc/latest/reference/reference_sql/sql_beginners_guide/

Вы старательно обошли основную проблему — неротиворечивость данных.
Без ее решения все, о чем вы говорите не имеет весомого значения.
Хоть сколько сохраняйте дампы, если семантика данных нарушена, то считай данных и нет.
К слову. Как реализована модель бэкапа и можно ли восстановить данные на некий определенный момент.
Эх, жаль потерли эпичный тред на sql.ru про FVMas… Прям повеяло…

Redis fork механизм выполняет консистентный снапшот, потому что используется механизм OS copy-on-write.
Tarantool механизм также выполняет консистентный снапшот, потому что используется внутренний механизм read view.

При чем тут снапшот? Вопрос не о механической целостности, а о том, что в одинаковых структурах могут хранится разные по смыслу данные… Какой механизм предотвращения случайного или специального нарушения правил хранения?
В обычных, реляционных бд это само опреденюление отношения с предикатами и доменами, + ограничения целостности с внешними ключами, каскадированием и тд и тп

Такие механизмы в redis, tarantool решаются способами: или денормализацией, или логикой на аппсервере, или хранимыми процедурами

Вы опять говорите о другом.
Денормализация (опять какой то ацкий термин). Прямо как в школе. Честное слово.
Существует термин "нормалищация" это набор рекомендаций и правил позволяющий редуцировать объем данных и привести их в состояние логического непротиворечия (устранить неконтролируемую избыточность).
Для этого используется "декомпозиция" с участием семантического моделирования. Выделяются сущьности и связи. Определяются первичные и внешние ключи и прочие ограничения целостности, формируются домены и тд и тп.
Итогом этого процесса является набор отношений (сущностей) носителей строго определенных данных и никаких других с исключением артефактов (дублирований, отсутствия связанных значений и тд и тп).
И в идеале, при выполнени естесственного соединения (по естественным первисным ключам… о таком в ваших структурах даже и не мечтать… там все они суррогатные) получается исходный набор данных.
Так как этих механизмов в редисе и тарантуле и эластиксерче и тд нет…
То там просто нечего нормализовывать.
Все данные являются строками. Возможно в тарантуле встроена некое подобие хранения структур, но обеспечения их логической защишенности нет.
Вы поймите. Выполнение проверок внешней логикой это пролная провал. Субд отличается от этого тем, что хранит все металанные о базах данных в себе. Хранимые процедуры и функции, ограничения, сбор статистики, определение доменов, последовательностей, представления, триггеры описание отношений, самое важное — права доступа к операциям мутатораи и DDL… тд и тп…
В предлагаемых системах любой сторонний клиент моджет модифицировать любой блок данны как ему будет угодно и ни одна внешняя процедура не узнает об этом
А потом вы успешно сохраните ваш дамп. Ведь физически то он цел.

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

Думаем насчёт поддержки WSL2. Не буду прям обещать когда будет, но думаем.

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

НЛО прилетело и опубликовало эту надпись здесь
Разработчики у нас некоторые на WSL вполне успешно уже существуют. А вот насчет поддержки для продакшна на виндовых серверах — сильно сомневаюсь, максимум «на свой страх и риск»
Нет ли в тарантуле инструмента миграций? Например, есть спейс с заданным форматом, необходимо его обновить. Или удалить, добавить индекс. Правильно понимаю, что сейчас это делается руками через консоль?

Можно использовать чужие решения, например https://github.com/igorcoding/tarantool-spacer как наиболее проработанное из мне известных.

Как уже ответили: https://github.com/igorcoding/tarantool-spacer
И мигратор с интеграцией с Tarantool Cartridge со схожей семантикой: https://github.com/tarantool/migrations


В ентпрз решении используется авросхема с автоматической миграцией соответственно изменениям.

Если новый формат спэйса совместим с текущим, то его можно применить при инициализации приложения в space:format(). Например, добавить nullable поле в конец или поменять тип поля на совместимый.


Вторичные индексы можно добавлять так же при инициализации приложения с space::create_index(name, {if_not_exists = true}). Создание индекса не блокирует процесс, т.ч. под нагрузкой должно быть ок. Удалять индексы можно там же с index = space.index[name]; if index then index:drop() end. Вместо изменения индекса можно добавить новый, а старый удалить.

Было бы интересно также услышать мнение автора о key/value базе Memcached в сравнении с представленными в статье базами.

Надо подумать. Если я правильно помню Memcached с точки зрения k-v совсем скальпель: положил/забрал данные по ключу и всё. А вот сейчас заглянув в архитектурные доки увидел там мультипоточность и аллокаторы. Архитектурные сравнения это посложнее.

Начнём с того, что мемкэш — это кэш. А тарантул — надёжная персистентная бд

История тарантула как раз и начиналась с Memcached. Первым подходом было прикрутить к мемкешеду снапшоты, чтобы не стартовать с пустым кэшем. Потом был сильвербокс — уже полностью свое решение с транзакциями, снапшотами и wal. И уже на замену ему вырос тарантул.

Интересно было бы сравнить с производительностью ArangoDB на том же железе.
Дело в том что в ArangoDB первых версий были коллекции in memory от которых они в более поздних версиях отказались после очередной смены файлового хранилища так как обеспечивалась высокая скорость с исковой персистентностью.

Поскольку arangodb отказалась от хранения в памяти то хотелось бы сравнить то что в памяти с у тарантула и релиса с тем что в файлах у arangodb. В известном смысле у редиса ведь тоже файловый стор есть. Но у него нет 100% надежности. Я даже столкнулся с тем что файловый стор редиса привел к отказам от обслуживания именно по диску. Так как в режиме были заданы умолчательные значения на сохранение на диск после обновления определенного количества ключей и какой то процесс начал слишком часто писать в редис. В результате сброс памяти в файл проходил несколько ко раз в секунду чем гьушил и процессор и диск.
У и arangodb в свою очередь память также существенного скол зуется так как есть отдельная операция загрузитььколлекцию в память. И до недавнего времени до версии 3.7 кажется при превышении коллекцией объема доступной памяти сервер просто останавливался

Ну и современного сравнения с Аэроспайк точно не помешало бы)

Вот жалко что не пришло в голову проверить сколько каждая из баз занимает в памяти пустая. Или запихать в каждую из баз по одинаковому набору данных и посмотреть какая сколько оперативки ест.
Или поставить базы на одинаковые машины с одинаковым количеством оперативки и начать пихать одинаковые данные и посмотреть в какую больше запихается.
Константин Осипов заверял что «отпечаток» в памяти крайне мал.
youtu.be/yrTF3qH8ey8
Как можно просто реализовать на REDIS контроль активности IP? Что имею в виду — закидываем в память ip адрес со сроком хранения, скажем 60 сек. Сколько раз пришел этот IP, столько раз закинули. Как можно в REDIS каким то простым способом, в любой произвольный момент времени, подсчитать количество обращений конкретного ip? Почему то не нашел такой возможности — есть либо подсчет количества, либо срок жизни TTL. Либо я что то упустил из вида. Может кто то с таким вопросом сталкивался ранее

выглядит так, что можно сделать:


set ip_str 0
expire ip_str 60

потом на каждый приход ип адреса:


incr ip_str

количество обращений:


get ip_str
Спасибо, но это не то решение — у вас простой подсчет действий за период от начал подсчета, а нужно считать непрерывно за последние N-сек. Непрерывный подсчет активности с удалением устаревших данных. Отдельные части необходимого есть, но нет ни чего, чтобы в комплексе использовать средствами Redis
Вау, а вот это как раз то, что нужно! Спасибо!
Руками поверх Редиса это просто пишется. Чтобы модулей не ставить.

Прямо как там в лоб и реализуем. Пишум в ключ ip+toSecond(время) плюс один при каждом обращении. ттл минута + небольшой запас на всякий случай.
Читаем все 60 ключей начиная от текущего времени. Суммируем и получаем результат.

Оптимизация: уменьшение детализации. Можно писать блоками секунд по пять десять.
В Redis используется однозадачность: задачи выполняются по одной и целиком.

Не совсем так, для IO другие треды юзаются:
Redis is, mostly, a single-threaded server from the POV of commands execution (actually modern versions of Redis use threads for different things)
redis.io/topics/benchmarks

Да, это так. Я в статье не погружался в эти детали, но и Redis и Tarantool, для выполнения io операций используют отдельные потоки.
Тут фраза именно о том, что пользовательские задачи в БД не распараллеливаются.

И Redis, и Tarantool поддерживают асинхронную репликацию. Только Tarantool умеет в синхронную репликацию.
Разве команда WAIT в Redis не является способом достижения синхронной репликации?

Претензия только в том что способ репликации определяется на стороне клиента, а не в конфигах сервера?

Команда wait в Redis и аналог в Tarantool — это «частично» синхронная репликация.
Представим:


  • транзакция пришла
  • лидер сохранил
  • лидер ждёт реплику
  • в это время кто-то пришел и уже транзакцию на лидере увидел

В этой ситуации у нас появляются грязные чтения. Синхронная репликация в этом плане работает лучше.


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


https://habr.com/ru/company/mailru/blog/540446/

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

Кстати да.
Но есть ещё ограничение: нет ролбека в случае, если реплик мы не дождались. Ролбек можно конечно делать на апп уровне, но тогда может быть уже проще искать хранилище с полностью синхронной репликацией.


http://antirez.com/news/66

А для чего вообще ролбэк? Дальше, если реплики не доступные мы уже работать не можем, нужно вмешательство админа для разбора текущей ситуации. А если мы предполагаем, что все же можем работать далее через таймаут, то для чего нам синхронная репликация, RPO=0 значит не нужно, используем асинхронную репликацию и слежение за отставанием реплик.

Ролбек нужен, чтобы откатить данные, которые на лидере закомитились.

ИМХО, нужно восстанавливать репликацию (восстанавливать хост, сеть, или что там ещё мешает получить ответ лидеру), если нам нужно RPO=0. В момент не получения ответа от реплики нам не известно изменения применились ли на реплике, или применились, но ответ мы не получили. Данные между репликами становятся не согласованы, требуется полное восстановление данных с лидера на реплику если мы воспользуемся ролбэком, вместо ожидания подтверждения транзакции на реплике устраняя причину задержки.

Не совсем понимаю как эти действия применить к redis и wait и не получить грязные чтения

Очень упрощенно написано про Redis.
1. Key-value — можно использовать и так, но гораздо чаще key используется для доступа к более сложным структурам данных внутри value, про которых тут написано мало: стеки, хэши, хэш + индекс, stream,… Redis действительно хранилище структур, а не значений.
2. Забыт stream — крайне полезная и эффективная структура, может т.ч. использоваться для логов и гарантированной доставки сообщений — готовая основа для оркестровки групп воркеров с балансировкой нагрузки из коробки, например.
3. Несколько open source плагинов от той же команды, фактически части Redis:
Search — вторичное индексирование и мощный поиск по содержимому хэшей а-ля эластик — c агрегацией, лексикой и т.д.
JSON — хранит бинарно, поддерживает Path и вторичное индексирование с поиском по множеству ключей.
И ещё time series, графы, гео,…

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

Кстати, ещё момент — все 3 модуля, указанные в статье как Redis Enterprise, доступны и в виде open source — собираются и подключаются.
Единственный минус тут — нет готового докера со всеми батарейками сразу, или качаешь докер с редисом плюс один из них, либо голый редис и собирать и включать плагины.

F_AV_N верно упомянул streams. Фраза
Tarantool поддерживает: Создание брокера очередей

не очень корректно используется, посколько Redis тоже поддерживает как минимум 3 механизма (pub/sub, streams, sorted set (zadd/zpopmin) или rpoplpush)
в тарантул завезли для сшарпа строготипизированую библиотеку?

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

Что Тарантул, что КокроучДБ — пример крайне неудачных с точки зрения маркетинга названий, которые не дают проектам нормально развиваться во всем остальном мире, к сожалению, как бы круты они ни были. Увы. Но ничего уже не сделать. Как бы ни была смешна эта причина, по моим наблюдениям, она работает очень сильно.

Более того (оффтопик), есть мнение, что Микрософт не смогла полностью захватить серверный мир и отъесть у Линукса бОльшую долю тоже во многом из-за названия своей ОС (Windows Server). Ну какие на сервере окна? Это же для бородатых сисадминов, которые господствовали в 90-х и 2000-х (помните, в те времена серверные ребята еще тоже назывались сисадминами, а не DevOps, как сейчас модно?) — так вот, это для них было как соль на рану: «я человек-командная-строка, мне окна противны». «Как вы яхту назовете, так она и поплывет», на удивление сильный стереотип.

Согласен, хотя facebook выстрелил (но оно скорее нейтральное).
У Тарантула есть «Cartridge» для построение приложений. Это название может оказаться более подходящим.


А Микрософт исправлились и выпустили XboX )

100%. Другой пример: мне кажется, существенная причина, по которая сейчас очень популярна TimescaleDB — название. Тут тебе и тайм, тут тебе скейл. Звучит круто. "Еще никого не уволили за то что потянул таймскейл в продакшен".

Я в таких случаях всегда вспоминаю Elba, менеджер пакетов для Idris


Гугл по "idris elba" не оставляет никаких шансов)

Посоветуйте в чью сторону лучше посмотреть в такой задаче.
Есть пара миллиардов уникальных хэшей как первичный ключ, значения могут быть от 1 до 1000 других интовых id. Процессы — искать нужный хэш (тут нет проблем), добавлять id к существующей записи. Вот тут уже хочется волшебства, ибо доставать запись, если есть — добавлять новый id, обратно записывать — так себе КПД.
Без вытеснения, in-memory и с записью на диск.

И Redis и Tarantool такое умеют из коробки. Синтаксис Redis попроще, Tarantool более общий


redis-server # запустить

tarantool # запустить и сконфигурировать

  • tarantool>


    box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024}
    box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
    box.schema.space.create('kv', {if_not_exists=true,})
    box.space.kv:create_index('pkey', {type='TREE', parts={{field=1, type='str'}},
                                    if_not_exists=true,})

  • redis_test.go


    package main
    
    import (
        "context"
        "fmt"
        "log"
        "math/rand"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func BenchmarkSetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                value := rand.Int31()
                _, err := client.RPush(context.Background(), key, value, 0).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • tarantool_test.go


    package main
    
    import (
        "fmt"
        "math/rand"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                value := rand.Int31()
                _, err = pconn.Upsert("kv", []interface{}{key, value}, []interface{}{[]interface{}{"!", 2, value}})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • Запуск на 10 секунд на 12 потоков


    go test -cpu 12 -test.bench . -test.benchtime 10s

  • Результаты примерно равны


    goos: darwin
    goarch: amd64
    BenchmarkSetRandomRedisParallel-12        965875         14028 ns/op
    BenchmarkSetRandomTntParallel-12          955849         13928 ns/op
    PASS

  • У Redis для того, чтобы не доставать запись, существуют разные операции для таких вот структур данных


  • У Tarantool, чтобы не доставать данные, есть операции update,upsert. А если их не хватит то хранимки на LuaJIT.


Спасибо за шикарный ответ!
Добавим хотелок по полной, а если надо хранить два инта
hash-a={111{11}},{222{22}},{333{33}}
hash-b={111{44}},{222{55}}
Первый int неуникальный, хочется иметь на него индекс и искать по нему. Это возможно?

Второй вопрос — в редисе/тарантуле можно сделать а ля мускуль select * если вдруг надо будет пройтись по всем записям?
В тарантуле можно сделать индекс неуникальным, если при его создании указать параметр unique = false. www.tarantool.io/ru/doc/latest/reference/reference_lua/box_space/create_index
В тарантуле для прохода по всем записям можно сделать
box.space.space_name:pairs()
— это вернет итератор по всем записям, потом с ним можно будет работать с помощью luafun.github.io. Не используйте
box.space.space_name:select()
или
box.space.space_name:pairs():totable()
без ограничения на количество элементов в
select
или без вызова
fiber.yield()
через N итераций — это гарантированный способ уронить себе прод, особенно если у вас большой объем данных.

Такой вид задачи и в Redis и в Tarantool удобнее решать через две таблицы:


  • первая hash-a={ids}
  • вторая id={values}

То есть суть сведётся к предыдущему тесту

Может я не совсем правильно сформулировал вопрос, мне нужен вторичный индекс чтобы быстро узнать в каких хэшах есть id=111

В тарантуле есть multikey индексы — одна запись может быть проиндексирована по нескольким значениям. Документации не могу точной найти, но вот тут что-то есть https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/create_index/#box-space-path-multikey. Судя по примеру, можно указать сразу json-path до поля c несколькими значениями.

Я в лоб накидал схему на Redis и Tarantool:


  • Два insert-а в Redis


  • Один insert в Tarantool


  • redis_test.go


    package main
    
    import (
        "context"
        "fmt"
        "log"
        "math/rand"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func BenchmarkSetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                id := rand.Int31()
                value := rand.Int31()
                idstr := fmt.Sprintf("%d", id)
                valstr := fmt.Sprintf("%d", value)
    
                // INSERT
                _, err := client.HSet(context.Background(), key, idstr, valstr).Result()
                if err != nil {
                    b.Fatal(err)
                }
    
                _, err = client.SAdd(context.Background(), idstr, key, 0).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    
    func BenchmarkGetRandomRedisParallel(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                id := rand.Int31()
                idstr := fmt.Sprintf("%d", id)
    
                _, err := client.SMembers(context.Background(), idstr).Result()
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }

  • tarantool


    • tarantool>
      box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024,
      readahead=1024*1024*16}
      box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
      box.schema.space.create('kv', {if_not_exists=true,})
      box.space.kv:create_index('pkey', 
      {type='TREE', 
      parts={{field=1, type='str'}, {field=2, type='unsigned'}},
      if_not_exists=true,})
      box.space.kv:create_index('skey', 
      {type='TREE',
      parts={{field=2, type='unsigned'}},
      unique=false,
      if_not_exists=true,})

  • tarantool_test.go


    package main
    
    import (
        "fmt"
        "math/rand"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                key := fmt.Sprintf("bench-%d", rand.Int31n(1000))
                id := rand.Int31()
                value := rand.Int31()
    
                _, err = pconn.Replace("kv", []interface{}{key, id, value})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    
    func BenchmarkGetRandomTntParallel(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
        b.RunParallel(func(pb *testing.PB) {
            for pb.Next() {
                id := rand.Int31()
    
                _, err = pconn.Select("kv", "skey", 0, 10000, tarantool.IterEq, []interface{}{id})
                if err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
    

  • Тесты



go test -cpu 12 -test.bench . -test.benchtime 10s


goos: darwin
goarch: amd64
BenchmarkSetRandomRedisParallel-12        409393         39018 ns/op
BenchmarkGetRandomRedisParallel-12        768824         15063 ns/op
BenchmarkSetRandomTntParallel-12          781621         16820 ns/op
BenchmarkGetRandomTntParallel-12          858226         13462 ns/op
PASS
ok      _/Users/michael.filonenko/course/tmp    53.643s

Вывод:
В Redis приходится делать два инсерта.
В Tarantool один, и Tarantool поэтому быстрее.

Спасибо за еще один шикарный ответ, все становится намного яснее.
А еще такой вопрос, а какой оверхед у них на хранение? Если в моем случае будет 2 миллиарда хешей по 8 байт и внутри, ну пусть по 10 интов, сколько все это будет реально занимать памяти?
  • 1млн хешей (миллиарды долго ждать)


  • redis_test.go


    package main
    
    import (
        "context"
        "log"
        "math/rand"
        "strconv"
        "testing"
    
        "github.com/go-redis/redis"
    )
    
    func lpad(s string, pad string, plength int) string {
        for i := len(s); i < plength; i++ {
            s = pad + s
        }
        return s
    }
    
    func BenchmarkSetRandomRedis(b *testing.B) {
        client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})
        if _, err := client.Ping(context.Background()).Result(); err != nil {
            log.Fatal(err)
        }
    
        for n := uint64(1); n < 1e6; n++ {
            key := strconv.FormatUint(n, 16)
            key = lpad(key, "0", 8)
    
            pipe := client.Pipeline()
    
            for i := 1; i < 11; i++ {
                id := rand.Int31()
                pipe.LPush(context.Background(), key, id)
            }
    
            _, err := pipe.Exec(context.Background())
            if err != nil {
                log.Fatal(err)
            }
        }
    }

  • tarantool


    • tarantool>
      box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024,
      readahead=1024*1024*16}
      box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})
      box.schema.space.create('kv', {if_not_exists=true,})
      box.space.kv:create_index('pkey', 
      {type='TREE', 
      parts={{field=1, type='str'}, {field=2, type='unsigned'}},
      if_not_exists=true,})
      box.space.kv:create_index('skey', 
      {type='TREE',
      parts={{field=2, type='unsigned'}},
      unique=false,
      if_not_exists=true,})

  • tarantool_test.go


    package main
    
    import (
        "math/rand"
        "strconv"
        "testing"
    
        "github.com/tarantool/go-tarantool"
    )
    
    func BenchmarkSetRandomTarantool(b *testing.B) {
        opts := tarantool.Opts{
            User: "guest",
        }
        pconn, err := tarantool.Connect("127.0.0.1:3301", opts)
        if err != nil {
            b.Fatal(err)
        }
    
        for n := uint64(1); n < 1e6; n++ {
            key := strconv.FormatUint(n, 16)
            key = lpad(key, "0", 8)
    
            for i := 1; i < 11; i++ {
                id := rand.Int31()
                _, err = pconn.Replace("kv", []interface{}{key, id})
                if err != nil {
                    b.Fatal(err)
                }
            }
        }
    }

  • Тесты



go test -cpu 12 -test.bench .


  • redis


    • info
      # Memory
      used_memory:233454512
      used_memory_human:222.64M
      used_memory_rss:169279488
      used_memory_rss_human:161.44M
      used_memory_peak:233469968
      used_memory_peak_human:222.65M
      used_memory_peak_perc:99.99%
      used_memory_overhead:49407608
      used_memory_startup:1001600
      used_memory_dataset:184046904
      used_memory_dataset_perc:79.18%

  • tarantool вместе со вторичными индексами


    • box.slab.info()
      items_size: 357184240
      items_used_ratio: 99.86%
      quota_size: 2147483648
      quota_used_ratio: 35.16%
      arena_used_ratio: 96.9%
      items_used: 356685848
      quota_used: 754974720
      arena_size: 754974720
      arena_used: 731944984
      ...
    • box.slab.info().arena_used/1024/1024
      782

  • Вывод:


    • redis used_memory_human:222.64M
    • tarantool: 782
    • Tarantool занимает побольше в том числе за счёт вторичного индекса
    • В Tarantool можно (нужно) отключить индекс хинты, которые снизят потребление памяти

Зарегистрируйтесь на Хабре, чтобы оставить комментарий