Комментарии 117
Согласен.
Прогнал тесты mysql с in-memory mysql (ENGINE=MEMORY) — пришлось оторвать полнотекстовый поиск, т.к. ENGINE=MEMORY его не умеет. Результаты такие:
mysql byid -> 12917.17
mysql 1cond -> 10845.16
mysql 2cond -> 10114.12
mysql update -> 8447.82 (вырос в 10 раз, скорее всего, за счет отказа от полнотекстового индекса)
Судя по всему, mongo умеет in-memory только в enterprise редакции. Поэтому удалось проверить только на tmpfs:
mongo byid -> 12808.25
mongo 1cond -> 11279.45
mongo 2cond -> 7895.26
mongo text -> 7258.60
mongo update -> 809.77
Итого, выигрыш получился не принципиальный...
Обычный дисковый. Похоже, у MongoDB In-memory движок есть только в Enterprise редакции, поэтому сходу его опробовать не удалось.
Так же, в комментарии выше написал результаты тестов с базой в tmpfs. Результаты принципиально не изменились.
Спасибо. Прогнал с ним тесты:
mongo byid -> 14748.92
mongo 1cond -> 12661.20
mongo 2cond -> 7703.52
mongo update -> 1158.17
mongo text -> 876.09
В целом стало на ~20% быстрее. Однако, полнотекстовый поиск — в 10 раз медленнее. Предполагаю, что это связано с версией монги — где то между 3.4 и 3.6 они его существенно разогнали.
Вероятно не «SQL БД», а «реляционная БД»?
</zanuda-mode>
Было бы идеально если бы вы избавились от С/С++ кода в библиотеке для golang и полностью её написали бы на Go.
Лично меня, с моим перфекционизмом, наличие cgo всегда напрягает, прежде чем использовать такое творение придётся смотреть количество вызовов переключения go -> c, c->go и только потом решать стоит ли…
Подумайте об этом, избавление от cgo добавит значительную часть производительности reindexer и снимет проблему переключения миров c <-> go!
Я ждал этого вопроса. Наверно стоит начать с того, что переключение между go и c++ действительно не бесплатно, но весьма легковесно. Например, у меня на ноутбуке это порядка 100нс. Это время сопоставимо со временем обращения к map, и на остальном фоне практически не заметно — возможно, что-то в районе <1% от общей загрузки.
В Reindexer количество переключений go — с минимально: 1-ин раз на Query.Exec(), 2-рой раз на iterator.Close().
Решение на C++ выбрано сознательно по двум основным причинам:
- Для эффективной реализации индексов активно используются "generics", которых в golang, увы нет. В golang для этого пришлось бы либо существенно раздувать кодовую базу, копипастой реализаций индексов под каждый тип, либо использовать рантайм interface{}, который бы существенно ударил по производительности.
- В golang есть GC. Если данные хранить в Go (например 5м записей, в каждой из которых есть по 10 строчек), это как минимум 50М объектов на куче. Расплатой за это станут заметные паузы на GC, и как следствие общее замедление работы. В C++ нет GC, и как следствие проблемы с GC нет, как класс.
Корректно ли говорить о цифрах, если тестирование велось в далёкой от prod среде?
Специально проверил. В данном случае корректно:
Если говорить о c <-> cgo, то вот цифры с прод сервера:
root@90b6ed8da107:/build/tst# go test -bench . -benchmem
goos: linux
goarch: amd64
BenchmarkCGO-24 20000000 109 ns/op 0 B/op 0 allocs/op
Если про остальные тесты, выборочно запустил несколько бенчмарков на prod сервере. Соотношение результатов ± аналогичное.
interface{}, который бы существенно ударил по производительности
Откуда информация?
Приведение interface{} к типу в Go вроде бы довольно быстро работает.
Согласен, приведение interface{} не очень затратная операция.
Однако, каждое приведение interface{} к конкретному типу, это как минимум лишнее ветвление, и дополнительные затраты по памяти на хранение информации о типе каждой переменной. Когда данных много, и индекс активно используется — даже незначительный оверхед на приведении типа заметно ударит по производительности.
Наверно стоит начать с того, что переключение между go и c++ действительно не бесплатно, но весьма легковесно. Например, у меня на ноутбуке это порядка 100нс.Я не очень знаком с компилятором Go, но может мне кто-нибудь объяснить, что вообще представляет собой природа этих задержек? Как я себе это представляю: есть загруженная в память программа на Go, есть загруженная ею в свою память библиотека на Си. Программа на Go кладет свой адрес в стек и передает управление какому-то адресу библиотеки, библиотека что-то считает, кладет сам результат или ссылку на него, например, в регистр, достает из стека адрес Go программы и возвращает ему управление. О каких переключениях идет речь? Тут разве что небольшой cache miss может быть, но это не страшно. Чем это отличается от обычного вызова процедуры?
Если коротко, то каждый вызов C из Go может приводить к созданию нового thread с копированием стэка соответственно, плюс нужно передавать данные между C и Go частями, для этого в Go есть отдельные типы данных и возможность преобразования стандартных типов в них и из них, что влечет и еще задержки.
Фактически есть две вселенные Go и C и они общаются мало и нехотя с существенными задержками, которые тем выше, чем более активно эти вселенные должны общаться.
Лучше избавиться от Go
Пробовали разное. Если мне не изменяет память, варьировали количество шард, использовали наиболее подходящие mappings. Делали разнообразные sysctl, увеличивали до беспредела размер памяти под JVM...
эластик, игнит, монго рассчитаны на большой объем данных, размазанных по куче нод.
в приведенном решении самих данных мало, они на одной ноде, но нужно очень быстро по ним выполнять обработку.
наверно, можно было бы сравнивать с тарантул, в случае нормализации данных. но как я понял автору важна денормализация и вложенные структуры.
Если честно, то пока руки не дошли. Да и Ignite в golang очень скудно представлен — нагуглиась только одна либа https://github.com/amsokol/go-ignite-client
У полнотекстового индекса есть морфология?
Есть аггрегации как у эластика? Уточню кейс — есть некий фильтр а-ля яндекс маркет. Списки галочек — свойств, по которым идет фильтрация, для галочки надо вывести число — количество элементов с этим свойством без учета галочек этой группы. Вот такое делается?
Может ли свойство иметь несколько значений? ({«year»: [2001, 2017]})?
Да уж!
У полнотекстового поиска морфология реализована на уровне поиска по корням слов и возможных опечаток. Например, если в документе есть слово "задачами", то документ найдется по запросам "задача", "зодачей" и даже "zadacha". Это в быстром движке. В продвинутом -триграммы, он допускает еще больший разброс словоформ.
Аггрегация тоже есть — вот описание
Свойства массивы — есть. Они нам потребовались в бизнеслогике с самого начала, и кстати сильно ограничили набор готовых решений, которые мы рассматривали.
Но у Tarantool есть быстрый старт после падения, не надо ожидать разогрев кэша.
Reindexer сразу после запуска считывает весь кэш с диска в память. Скорость загрузки мы специально не измеряли, но в среднем, база в ~800MB считывается в память где то за 5-7 секунд.
То есть, в нашем случае, через 7 секунд после запуска получаем полностью прогретый кэш.
Тогда я не понял, зачем для Elastic нужно 200-300 серверов. Поясните, пожалуйста.
800MB это компактный бинарный формат, сверху пожатый snappy. В эластике эти данные занимают на диске существенно больше (точной цифры сейчас уже не скажу, но кажется коэффициент быть 1:10). А по памяти, что бы с ними нормально работать эластику требовалось минимально 16GB RAM.
Но главная проблема все же не в объеме данных, а в правилах фильтрации. С одной машины с эластиком получали всего лишь сотни RPS, а на всю систему нужно 100к RPS
P.S. Почему не выложили Ваши красивые графики в документацию git-проекта?
Из Java проекта пока можно только по http подключиться, но с ним будет конечно, большой overhead. В не очень далеких планах есть реализация бинарного протокола, тогда и можно будет сделать хороший коннектор для Java
А эти графики с пылу-жару — для хабра сделали, еще не успели оформить и выложить в документации git проекта.
По отказо-устойчивости хранилища Reindexer зависит от storage backend-а, сейчас это leveldb со всеми ее плюсами и минусами. Если окажется, что leveldb не устраивает, легко можно перейти на любой другой. Но пока устраивает.
Сами данные хранятся с минимальными накладными расходами (+~32 байта на одну запись)
Потребление памяти сильнее всего зависит от количества и типов индексов, которые используются. Наиболее прожорливый — полнотекстовый индекс.
Если говорить в среднем, то потребление ОЗУ получается 2-3х от размера исходных данных (но зависит от большого количества факторов)
Reindexer — это NoSQL in-memory БД общего назначения.Не указали очень важный момент — «here is no standalone server mode. Only embeded (builtin) binding is supported for now.», отсюда и скорость. В добавок можете сравнить со встроеным map из Go для теста выборки по значению.
Отчего же, не указали. Еще как указали:
Сейчас доступно три варианта подключения Reindexer-а к проекту:
библиотекой к Golang
библиотекой к C++
standalone server, работающий по http протоколу
Конечно, сеть скорости не прибавляет. Но и подключение либой — не серебряная пуля.
К примеру, sqlite, подключается либой, без сети, однако цифры у нее — так себе.
Сеть заметно накидывает на Latency. Примерно ~30мкс на запрос, это правда. Но Latency мы в этой статье не сравниваем.
А на RPS, которые мы сравниваем — влияние не так велико. Точные цифры сказать сложно, но по ощущениям на Get By ID ~20%-30%, на остальных более тяжелых запросах — способ подключения базы влияет еще меньше.
Не пробовали сравнить по производительности конечного приложения tarantool+lua с go+reindexer?
В рамках этих тестов — не сравнивали, но раньше сталкивались.
Несколько месяцев назад проводился Mailru Highload Сup. https://highloadcup.ru/rating/. Пользуясь случаем, кстати, огромное спасибо организаторам :)
Решение но основе Reindexer/C++ прошло в финал, а решение на основе Tarantool+lua — нет.
В синхронном режиме Latency имеет решающее значение на RPS.
Тут нужно либо Reindexer поставить в те же условия (отдельным процессом запускать) либо для остальных использовать pipelining (https://redis.io/topics/pipelining) и тогда у вас Redis быстро уйдет за 1M RPS.
fuzzy, триграммный — … он в экспериментальном статусе
_
про полнотекстовый поиск понятно.
_
Что-то ещё по функционалу в планах есть?
Интересно узнать Ваше мнение чего не хватает. Разработка коллективная? Спасибо.
В планах — встроенный Web интерфейс для просмотра и редактирования данных в БД, а так же консольная утилиты для дампа/рестора БД.
Еще в обозримых планах бинарный протокол для сервера и коннекторы к другим ЯП.
Из функционала движка задумываемся об R-Tree, оптимизация операция записи, и еще некоторый ряд оптимизаций.
Начинал сам, а сейчас уже разработка коллективная — в проекте участвует несколько человек.
Какой объем данных был?
Каждый тест 10раз x 5 секунд.
Объем данных 100К записей x 0.5кб. Увеличение объема в 10 раз существенно результаты не меняло, однако со всеми запущенными базами контейнер переставал помещаться в память, что сильно усложняло тесты.
Rocksdb функционально ближе к Leveldb, от которой она и произошла. Добавить к ней функционал выборок по N произвольным индексам, Join и произвольные сортировки — задача возможно даже сложнее, чем написать с нуля, т.к. Архитектурно RocksDB это все же продвинутая дисковая K-V
Смотрел на нее, как на дисковый backend вместо leveldb, но большого профита по отношению к leveldb в этом разрезе не нашел.
Почему не хотите сделать gRPC вместо бинарного протокола?
Если данные не перестают влезать в память, то будет либо уход в swap либо отказ в операции с ошибкой, или даже OOM killer на уровне ядра. Зависит от настройки конкретной системы.
gRPC кажется тяжеловатым для нашей задачи. Нашел такие бенчмарки: 50мкс wall clock, 30мкс cpu clock — это очень медленно.
Выбор пал на Postgres, тут никаких откровений
10М пользователей может дать сотни тысяч RPS на всю систему.
Это означает, что запросы от клиентов и близко не стоит подпускать к реляционной SQL БД без кэширования, а между SQL БД и клиентами должен быть хороший кэш
Постгрес даже по записи держит 100 000 при включенной отложенной записи. Не то что по чтению.
Кэширование в любом случае применять стоит на всякий случай.
Но вот это ваше "нагрузку в 100к даже близко нельзя подпускать к Постгрес" — откровенно коробит и выдает в вас специалистов, не вникающих в инструменты с которыми работаете
Вау! Получили 15к RPS на том же железе, с теми же условиями, где Elastic давал 500.
Все тормоза Эластика — от автоматического распределения данных по кластеру.
Без этого — есть уже быстрое решение на C написанное. Sphinx называется.
На конференции Highload был доклад Ivi. Почему они перешли с Sphinx на ElasticShearch. Там рассказано что производительность у Elastic ниже чем у Сфинкса. Но они решели это уменьшением размера ответа — в терминах SQL это limit в запросе в 200 строк. При 1500 строках Сфинкс существенно шустрее Эластика
У Sphinx по состоянию на год назад не было хранилища и для него требовалось еще SQL хранилище рядом, как для индексации, так и для отдачи контента.
Сейчас, говорят, уже появилось. Но коннекторов Golang для Sphinx 3.x с поддержкой хранилища я еще не встречал.
На год назад Сфинкс 2 было актуальным. И с коннекторами под Go — порядок.
SQL хранилище там не требуется для отдачи вообще. Для индексации SQL хранилище опционально.
Ну то есть вам очень хотелось сделать свой велосипед, вы даже не вникнули в аналоги. Ни в Сфинкс ни в Эластик. Ограничились дефолтными настройками?
С коннекторами в гошке, к сожалению, у сфинкса — грусно.
Нативный не поддерживает многопотчку и падает при конкурентных запросах из нескольких потоков (казалось бы, что в 2017 году это базовый фунционал), не говоря уж об коннекшн пулинге…
Коннектор через протокол MySQL — просто отказался работать с ошибкой
С эластиком, как бы цифры бенчей (даже после тюнига коннектора и рекомендованных sysctl), уступающие на порядок и Reindexer и Tarantool, говорят сами за себя.
С коннекторами в гошке, к сожалению, у сфинкса — грусно.
Нативный не поддерживает многопотчку и падает при конкурентных запросах из нескольких потоков (казалось бы, что в 2017 году это базовый фунционал), не говоря уж об коннекшн пулинге…
Коннектор через протокол MySQL — просто отказался работать с ошибкой
БД можете сделать а коннектор починить нет?
И вместо этого соорудили новую БД, не разобравшишь ни со Сфинксом ни с Эластиком?
Как программист я вас понимаю.
Но менеджеру за вашу не эффективность я бы всыпал люлей
Это важные особенности — так как отсутствие слоя сети существенно увеличивает рпс быстрых однотипных операций (оптимизации компилятором всего бенчмарка, меньше вытеснений кешлайнов и тп), а асинхронная запись на диск не только ничего не блокирует, но еще и может приводить к потере данных. С этими уточнениями будет понятно откуда такой выигрыш в производительности и не будет нужды смотреть в код.
Слой сети существенно увеличивает latancy, однако на RPS он влияет не так существенно. Порядка 20-30% процентов.
В нашем случае — развернута линейная структура из нод, каждая из которых работает со своим инстансом кэша. Один сервер — одна нода. В этом случае сеть между Reindexer и Golang бэком технически избыточна и вносит дополнительный оверхед.
Если использовать pipeline запросы (сразу по 100 — 1000 штук), то Redis легко улетает за 1М RPS.
Latency влияет на RPS далеко не линейно: пока один процесс ждет сети — работает другой процесс и процессор не простаивает. Конечно, какой то, оверхед на context switch есть.
В тестах я привел бенчмарки методов, аналогичных реальной задаче: "в методе http API сходить в кэш -> сфорировать JSON -> отдать клиенту"
Pipelining, это конечно хорошо, но к данной, и что не маловажно весьма типовой задаче, он не применим.
2. нормально ли поддерживается кирилица?
3. есть ранжирование результатов?
4. активно занялся разработкой под Odoo, вопрос с эффективным поиском не решон до сих пор, тут случайно наткнулся на ваш пост, (elastic, solr колупал, но привести его до вменямеого состояния с анализом морфолии, транслитом и т.д. не удалось). Плачевность ситуации что в Odoo кроме как Postgres FTS больше ничего нет из коробки. Готов реализовать такой модуль с вашей разработкой (конечно под OpenSource), и вам хорошо и нам хорошо) Как вы на это смотрите? Или может кто предлагал или уже делает бинды для питона (хотя более нужнее всетаки Stand Alone)?
- standalone режим уже реализован, но пока поддерживается только http протокол.
- кириллица в utf8 поддерживается полностью, включая транслит и "неверную" раскладку клавиатуры. 8-ми битные кодировки типа koi-8r/win1251 — нет.
- ранжирование результатов полнотекстового поиска — есть по достаточно большому количеству критериев. Можно настроить через API.
- мы только за :) бинд для питона у нас есть в производственных планах, но пока не с самым большим приоритетом.
Есть ли планы по горизонтальному масштабированию для отказоустойчивости?
Спасибо. Очень актуальные вопросы.
У нас внутри развернута система CI с автотестами Reindexer, включая автотесты MR в Reindexer в составе нашего гошного бэкенда. Если честно, пока не знаю, как собрать конструкцию с разработкой на github и с автотестами, которым требуется доступ ко внутренним ресурсам.
Сейчас горизонтальное масштабирование реализовано уровнем выше. В системе есть входной балансировщик, который знает про статус нод и отправляет на клиентов на живые ноды. В случае аварии и потери данных в кэше, нода загружает данные из Постгресс.
Как реализовать горизонтальное масштабирование на уровне Reindexer думаем.
в gcc есть для этого библиотека и у LLVM
Вот не уловил мысль…
т.е. формировать код запроса на с++ и компилировать, скомпилированную функцию исполнять.
За jit компиляцией движков баз данных будущее. Это позволит значительно оптимизировать запросы на выборку данных. С удовольствием бы глянул на существующие проекты.
В Elastic, например, этот функционал есть.
У нас можно искать фразу, в том числе с учетом расстояния между словами, и полей в которых эти фразы встречаются и т.д.
примеры поисковых запросов
Удалять индексы без переиндексации всей таблички Reindexer тоже не умеет. Технически задача не сложная, но я, если честно, сходу не вижу практический кейс, в котором такой функционал был бы критичен.
Документация по HTTP API будет, но чуточку попозже.
А какой образ хотелось бы видеть на докерхабе? )
Хорошая идея, спасибо! Сделаем такой образ.
namespace — табличка.
Сейчас ограничение — 64 индекса на сущность.
Выложил образ на Dockerhub:
Запускать такой командой:
docker run -p9088:9088 -it reindexer/reindexer
Дальше, в браузере можно зайти на http://<ip докера>:9088/doc — откроется свагер дока REST API
Поддерживается ли ACID?
Что вообще происходит при конкурентной записи в таблицы? Поддерживается ли read consistency?
ACID только на уровне документа, насколько я понимаю примерно так-же, как у монги.
При записи происходит короткий lock всей таблицы.
Так же есть механизм Lock Free атомарного bulk обновления таблицы.
Тогда, боюсь, что это не ACID, а «Transactions at the single-document level are known as
atomic transactions». Есть еще термин BASE (Basically Available, Soft state, Eventual
consistency) в противовес ACID.
Проблему согласованности и атомарности данных Монга выносит на уровень приложения в виде «Two Phase Commits», как об этом говорит документация.
Блокировать всю таблицу ради atomic transactions не нужно, оптимистической блокировки более чем достаточно. Все-таки пессимистическая блокировка существенно влияет на уровень параллелизма.
Но если Вы замахнулись на поддержку JOIN, тогда ACID будет уместным. Но при этом вы поставите крест на возможностях шардинга (CAP-теорема). Есть небольшая книжечка, всего в 150 страниц, «NoSQL Distilled» by M.Fowler, которая кратко и очень доходчиво рассматривает все эти вопросы. Только не читайте русский перевод этой книги, он ужасен, и нередко искажает смысл оригинала.
Согласен, честный ACID нам будет дорого стоить. Возможно лучше сменим терминологию, и назовем функционал не Join, а например 'Nested queries', что бы не вводить людей в заблуждение )
Все-таки пессимистическая блокировка существенно влияет на уровень параллелизма.
Все так, но реализация индексов внутри не thread safe, и требует наличия блокировки на запись.
Что бы запустить запись во много потоков еще потребуется порефакторить индексы — они требуют блокировки. Прямо сейчас производительность на запись нас устраивает, если станет проблемой — то да, пойдем именно этим путем.
Ну, в этом и заключается смысл NoSQL хранилищ. Они ориентированны для работы в условиях шардинга (за исключением некоторых, например, графовых). А поскольку в условиях шардинга невозможно обеспечить ACID (в силу CAP-теоремы), то возник вопрос организации транзакций. Поэтому границами транзакций в NoSQL стали границы агрегата (композитной структуры объектов), что вписывается в распределенную модель хранения информации (и удовлетворяет DDD). Джойны по этой же причине обычно не поддерживаются (что компенсируется поддержкой вложенных объектов).
Кстати, я не заметил, как автор решает проблему параллельного доступа к данных (транзакций). Возможно я этот момент упустил, поэтому пробежался по статье повторно, но так и не нашел. А этот момент очень важный в условиях «100К RPS».
Поскольку в NoSQL границы транзакции совпадают с границами агрегата, там достаточно оптимистической блокировки. Совсем другое дело возникает при поддержке JOIN. В таком случае, следует как-то предотвратить чтение несогласованных данных. А способ реализации транзакций существенно влияет на уровень параллелизма (потому и существует четыре уровня ACID транзакций).
Я не хочу затрагивать вопрос о том, что это влечет за собой способ организации клиентского кода (двухфазные транзакции и т.д.).
Отдельно хочу затронуть тему самого термина NoSQL.
«The original call [NoSQL Meetup] for the meetup asked for “open-source,
distributed, nonrelational databases.» (NoSQL Distilled by M.Fowler)
Одним из критериев NoSQL является "Designed to run on large clusters".
Автор решал совсем другую задачу, нежели решают NoSQL. И хотя термин NoSQL использовать можно в порядке исключения, как это делают, например, графовые БД, но этот термин заметно искажает назначение БД. По этой причине, например, вы не встретите термина NoSQL в документации IndexedDB.
Интересно было бы услышать характеристики используемого диска. И, в целях чистоты эксперимента, было бы интересно рассмотреть вариант монтирования файловой системы тестируемых БД в RAM.
Кстати, я не заметил, как автор решает проблему параллельного доступа к данных (транзакций). Возможно я этот момент упустил, поэтому пробежался по статье повторно, но так и не нашел. А этот момент очень важный в условиях «100К RPS».
Реализовано на уровне rwlock табличек, с гарантией конситености на уровне документов.
Интересно было бы услышать характеристики используемого диска. И, в целях чистоты эксперимента, было бы интересно рассмотреть вариант монтирования файловой системы тестируемых БД в RAM.
Тесты запускались на MacBook Pro 15" 2016. Диск — штатный SSD
Выше в комментариях повторил тесты MySQL и Mongo в вариантах с монтированием файловой системы в tmpfs и там же привел цифры.
Пока http API в статусе драфта, и будет немного меняться. Как финализируется сделаем подробную документацию.
По просьбе в комментарии выше, выложил на docker hub образ, который можно запустить, и в браузере подергать методы API через Swagger UI.
Выглядит вот так:
При таком размере датасета для меня главный вопрос был бы не в том, какую внешнюю базу использовать, а в том использовать ли вообще какую-либо внешнуюю базу. Чтоб совладать с 50 Мб данных обычно бывает достаточно внутренних средств языка программирования, на котором пишу. Так всё будет в одной области памяти, минимальный overhead, нет переключений процессов, не нужно поддерживать инфраструктуру, зависимости и т.п.
А можете показать результаты тех же тестов, но при бОльших датасетах (до десятков млн. записей, до десятков Гб)?
Вообще наша исходная задача — данных до ~1GB, ~3-4М записей: уметь их быстро искать/фильтровать по сложным критериям. На мой взгляд, это достаточно типовая задача, для достаточно большого количества проектов.
Бесспорно, вручную совладать конечно можно, но вопрос на засыпку:
- как вручную сделать выборку из 100к записей по N критериям или с полнотекстовым поиском? Выносить всю логику выполнения запроса к БД на Application Level — не очень радостная перспектива то.
Если говорить про реальные тесты с бОльшим количеством данных — вот живой пример, участвовали с Reindexer в mail.ru highload cup — там было порядка 10м записей (если мне не изменяет память, общим объемом около 1GB и ограничение 4GB ОЗУ). В финале попал только Reindexer, остальные решения не прошли в финал — кто по скорости, кто в память не влез…
Но впрочем ради интереса, чуть позже как дойдут руки прогоню тесты из статьи на большем объеме данных — скажем 10-20м записей.
Да, предусмотрен. Работа с данными внутри Reindexera использует COW подход.
Более того, в сишном когде, в подавляющем большинстве случаев операция Select отрабатывает вообще в "zero-alloc" режиме.
Select возвращает что то типа COW shared_ptr на записи, находящиеся прямо в хранилище. Алокация и копирование произойдет только в случае, если будут конкурирующие Select и Update.
Просто раскидать данные в память и начинать в лоб искать — не панацея. Особенно когда есть числовые данные (year > 2001).
Если данных много (гигабайты, десятки гигабайт) — тут уж не обойтись без elasticsearch/clickhouse (второй особенно для данных, основанных на временных срезах).
К сожалению, с учетом ограничения у меня на ноутбуке в контейнере — 8GB RAM, десятки гигобайт данных не переварить, тем более с полнотекстовым поиском.
5М записей (исходных данных 2.5GB) полнотекст отключен:
reindex byid -> 161491.19
reindex 1cond -> 66582.29
reindex 2cond -> 55052.78
reindex update -> 20879.24
1М записей
reindex byid -> 168354.18
reindex 1cond -> 65383.95
reindex 2cond -> 51218.16
reindex update -> 22164.61
Тарантул и Redis такой датасет в том же окружении не переварили
Конечно есть.
При выполнении запроса строится план исполнения, с учетом селективности и свойств индексов, селективности выборок и условий выборок в запросе.
Развернутый ответ как работает исполнитель запроса — скорее предмет большой статьи, приведу несколько примеров оптимизации:
SELECT * FROM table WHERE A = 2010 AND B = 300 AND C > 100 AND C < 200
Если есть композитный(составной индекс) по A+B, то он будет автоматически подставлен вместо двух отдельных выборок в индексы A и B, и суммарная сложность выборки по A и B будет O(1)
- Если есть TREE индекс по C, то индекс C будет использован как основной и сложность выборки по C будет 2*O(log(N)), вместо сканирования со сравнением поля C
SELECT * FROM table WHERE A IN (1,2,3,4,5,6,7,8,9,10,...) AND B=200
- результат выборки A IN (1,2,3,4,5,6,7,8,9,10,...) с M-го запроса будет закэширован, и будет отдаваться из кэша, со сложностью O(1)
SELECT * FROM table WHERE A > 1 ORDER BY C ASC OFFSET 100000 LIMIT 10
- Если есть TREE индекс по C, то при первом запросе будут пред рассчитаны, и закэшированы позиции позиции всех элементов таблицы отсортированные по полю 'C', и при следующих запросах ресурсоемкая сортировка уже не потребуется
github.com/Restream/reindexer/blob/master/benchmarks/repo/item.go?
Хочу свой велосипед пробенчить и сравнить (больно схожие задачи).
Второй вопрос — сколько оперативной памяти занимает (включая всю обвязку, если говорим про HTTP сервер, включая активацию всех индексов и прогрев кешей) этот датасет?
Да датасет генерируется тут.
Однако, на этот датасете потребление памяти не очень показательно:
C включенным полнотекстовым индексом RSS — порядка 1 GB. С выключенным полнотекстом RSS ~ 300 MB.
Замерил цифры на датасете из 5М записей (~2.5гб исходных данных) с отключенным полнотекстом:
RSS всего процесса
- 3.9GB с отключенным кэшом десериализованных объектов на стороне Golang,
- 5.2GB с включенным кэшем в golang и прогретым.
Редис и тарантул к сожалению на датасете из 5М записей не смогли взлететь :( Возможно что-то делаю не так
2018/01/29 16:43:53 Seeding data to Redis
panic: MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
goroutine 1 [running]:
_/build/repo.(*RedisRepo).Seed(0xc42000e098, 0x4c4b40, 0xddfb3b)
/build/repo/redis.go:37 +0x4cc
_/build/repo.Start(0xc420016801, 0x7ffeca7ad94a, 0x5, 0x4c4b40)
/build/repo/repo.go:43 +0x171
main.main()
/build/main.go:19 +0x10e
2018/01/29 16:41:10 Seeding data to Tarantool
panic: Failed to allocate 546 bytes in slab allocator for memtx_tuple (0x2)
goroutine 1 [running]:
_/build/repo.(*TarantoolRepo).Seed(0xc42000e0a0, 0x4c4b40, 0xde360d)
/build/repo/tarantool.go:28 +0x414
_/build/repo.Start(0xc420016801, 0x7ffe2f1a0946, 0x9, 0x4c4b40)
/build/repo/repo.go:43 +0x171
main.main()
/build/main.go:19 +0x10e
Прошу прощения за резкий коммент..
инмемори эбеддед движок поиска сравнивать с полноценными (даже кластерными) субд в скорости работы - это как? Давайте тогда сравним возможности репликации или автоматического шардирования?)
Вопрос по делу: как-то гарантируется целостность данных в снэпшоте? если память не ecc что случится в худшем случае?
Как мы выбирали между Elastic и Tarantool, а сделали свою (самую быструю) in-memory БД. С Join и полнотекстовым поиском