Manticoresearch - это Open-Source проект, форк проекта sphinxsearch от Андрея Аксенова и его команды. Проект позиционирует себя как открытое высокопроизводительное решение для полнотекствого поиска. Судя по бенчмаркам (правда они от самих создателей Мантикоры), "средняя по больнице" скорость превышает скорость популярного Elasticsearch.
В своей заметке я расскажу, как устроены индексы и как их можно потюнить с графиками и картинками на живом примере покажу что на что влияет.
Индексы
В manticore (сокращенно от manticoresearch - далее буду писать manticore для простоты) есть четыре типа индексов: plain, real-time, distributed и percolate. Plain индексы - это т.н. простые индексы, которые хранятся на диске, создаются один раз, поддерживают обновление атрибутов, но не полнотекстовых полей. Real-time индексы похожи на таблицы в базах данных, поддерживают полную замену документов через REPLACE, вставку, обновление в т.ч. и полнотекстовых полей в режиме "онлайн".
Также существует тип percolate индекс, основанный на Real-time-индексе. Этот тип не хранит данные, но хранит запросы.
Четвертый тип - это distributed индекс. Он ничего не хранит ни в каких файлах, это просто составной индекс который под собой содержит несколько plain или/и rt-индексов.
Plain и real-time индексы могут состоять из трех видов полей и атрибутов:
1) id - это идентификатор документа в индексе. Механизма автоинкремента для id в мантикоре не имеется, обеспечение уникальности id ложится на плечи программиста. Тип id всегда unsigned int64.
2) Полнотекстовые поля - хранят проиндексированный текст. Упрощенно полнотекстовые поля -- это структура инвертированного индекса: в памяти хранится словарь, на диске цепочки указателей на местоположение слова (терма) в документе. В настоящее время manticore не поддерживает хранение оригинального текста в полнотекстовых полях. В одном индексе может быть сразу несколько полнотекстовых полей, и разумеется, можно искать (сопостовлять, матчить) документы сразу по нескольким полнотекстовым полям. Именно по полнотекстовым полям manticore вычисляет релевантность - weight() для дальнейшего ранжирования результатов поиска. Для подробностей см. раздел searching
3) Атрибуты - дополнительные поля, по которым можно дофильтровать, сгруппировать или сортировать сматченные документы прежде чем отдать их клиенту. Атрибуты могут быть использованы в формуле ранжирования. Существует несколько типов поддерживаемых manticore атрибутов:
беззнаковый int32 и int64 со знаком. В manticore их типы обозначаются соответсвенно - uint и bigint.
32 битные числа с плавающей точкой одинарной точности (float)
unix-timestamps
булевые типы (bool)
строки (string)
JSON
multi-value - MVA, это типа массивов, которые могут содержать только лишь 32 битные целые без знака
Каждый определенный в конфигурации индекс состоит из нескольких файлов на диске. Некоторые файлы по умолчанию всегда лежат на диске и обращение к ним происходит через стандартные средства ОС обращения к диску. Некоторые файлы для ускорения обращения к ним отображаются в память, это словари, а также скалярные атрибуты. Ниже для удобства приведена таблица файлов индекса и как они отображаются или не отображаются в память:
Файл, расширение | Что хранится? | Как хранится по умолчанию |
spa | Скалярные атрибуты | Отображение в память через mmap() |
spd | Список документов | Читается с диска |
spi | Словарь | Всегда в памяти |
sph | Заголовок индекса(или блока) | Всегда в памяти |
spk | kill list | Загружается в память при запуске и выгружается после применения |
spl | lock файл | Всегда на диске |
spm | row map | mmap |
sphi | гистограммы | Всегда в памяти |
spt | структуры для обращения к docid | mmap |
spp | позиции ключевых слов | Читается с диска |
spb | Атрибуты - mva, строки и json | mmap |
Real-time индексы имеют дополнительные файлы:
Файл, расширение | Что хранится? | Как хранится по умолчанию |
kill | RT kill - документы, которые были заменены через REPLACE, прошли очистку и сброшеные как блок (чанк) | Всегда на диске |
meta | Заголовок rt-индекса | Всегда в памяти |
lock | RT lock-файл | Всегда на диске |
ram | Копия блока (чанка) из памяти - создается, когда блок из памяти сбрасывается на диск. | Всегда на диске |
Для чтения индексов мантикора использует два метода - seek+read и mmap.
В seek+read режиме (значение опций acess_* = file, об опциях доступа ниже) чтение выполняется через pread(2), то есть мантикора с настройками по умолчанию использует этот системный вызов для чтения файлов spd и spp с диска. Для оптимизации чтения на старте алоцируются буферы, размер которых можно подстроить через опции read_buffer_docs и read_buffer_hits для чтения spd (документы) и чтения spp (позици ключевых слов) соответственно. Важно знать также, что по умолчанию на старте мантикоры индексы еще не открыты, то есть вызов open() происходит при каждом обращении к файлам индекса. Это поведение регулируется опцией preopen. У меня на практике (довольно высокая нагрузка) эта опция всегда была выставлена в 1, это позволяет избежать вызовов open() на каждый запрос, однако в таком режиме мантикора создает 2 файловых дескриптора на каждый индекс. Плохо это или хорошо, зависит от ситуации, если много индексов у вас, и не такая большая нагрузка, то имеет смысл оставить по умолчанию.
В режиме mmap файлы отображаются в память системным вызовом mmap(2). Опции read_buffer_docs и read_buffer_hits не влияют на производительность никаким образом. Этот режим доступа может быть применен к файлам, хранящим скалярные атрибуты (spa), документам (spd), позициям ключевых слов (spp) и атрибутам переменной длины - json, строкам и mva (spb).
Есть еще один режим в котором можно запретить ОС свопить на диск кешированные данные индексов из памяти. Это осуществляется через системный вызов mlock(2), но чтобы воспользоваться им, мантикора должна быть запущена в привелигированном режиме (например, из под рута).
Теперь про настройки и выбор их значений в различных ситуациях.
Настройка | За что отвечает | По умолчанию |
access_plain_attrs | Определяет доступ к скалярным атрибутам (типы bigint, bool, float, timestamp, uint). | mmap_preread |
access_blob_attrs | Определяет доступ к blob-атрибутам - json, string, mva | mmap_preread |
access_doclists | Определяет доступ к документам | file |
access_hitlists | Определяет доступ к позициям | file |
Значения настроек доступа:
Значение | Краткое описание |
file | Буферизированное чтение с диска с вызовом pread(2). Размеры буферов регулируются read_buffer_docs и read_buffer_hits. |
mmap | Файлы индекса будут отображаться в память через системный вызов mmap(2), ОС будет кешировать все обращения к файлам в памяти. |
mmap_preread | Тоже самое что mmap, но файлы индекса будут прочитаны на старте мантикоры. Своего рода "прогрев" кеша. |
mlock | Файлы индекса будут отображены в память и через системный вызов mlock(2) данные будут закешированы ОС и заблокированы от сброса на диск. |
Я проводил небольшой эксперимент, подкрутил настройки access_doclists и access_hitlists выставив в значение mmap. Ниже дан график, наглядно демонстрирующий, что особо сильно это не дает прироста производительности. Настройки применены в 1:05PM.
Машина имела такую конфигурацию:
Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz 40 ядер
96Гб памяти
Рабочие виртуалки редиса и редиса под кеш портала
43 640 037 проиндексированных документов
16 Gb (spd) +9 Gb (spa) + 8Gb (spp) + 25 Gb (spb) ~ 60Gb монолитный plain-индекс + rt-индекс
Но! На нашей машине с мантикорой установлен SSD диск, что и не дает вау-эффекта. Если у вас HDD и памяти хватает, чтобы закешировать все индексы, то смело выставляйте access_doclists и access_hitlists в mmap, а access_plain_attrs и access_blob_attrs в mlock. Тогда это даст, на сколько я понял, максимальную производительность мантикоры.
В случае, если памяти не хватает, то не используйте mlock - "горячие" данные все равно будут в памяти, а редко используемые будут погружаться с диска ОСью.
Также порекомендую всегда запускать мантикору с опцией --force-preread
, это задержит мантикору на начальном этапе, так как она будет считывать сначала индексы и только потом начнет принимать запросы от клиентов. Каждая часть индекса будет прочитана для того, чтобы ОС закешировала обращения к диску, это как прогрев кеша. Зато после старта ваши запросы будут обрабатываться гораздо быстрее. Но это опять же, совет для тех, у кого HDD.
Concurrency
Теперь поговорим о том, как мантикора реализует конкуретное исполнение ваших запросов. В ранних версиях, когда мантикора была сфинксом, на каждое сетевое соединение порождался тред, в котором и происходила обработка запроса/запросов. Данный режим до сих пор остался в мантикоре по соображениям обратной совместимости. С версии 2.3.1-beta сфинкса был добавлен режим конкурентности thread pool. Этот режим является самым предпочтительным. Основные настройки конкурентности в мантикоре отвечают следующие основные опции (секция searchd):
Опция | Краткое описание |
workers | определяет режим конкурентости. По умолчанию - thread_pool. Не меняйте, особенно если у вас большая нагрузка и постоянный поток подключений от клиентов. |
queue_max_length | Размер очереди воркеров в режиме thread_pool. По умолчанию в мантикоре бесконечная очередь. |
max_children | Кол-во потоков запускаемых в параллель. В режиме thread_pool определяет размер пула потоков на старте. В режиме threads ограничивает максимальное кол-во параллельных воркеров. По умолчанию равняется нулю, что означает в thread_pool размер пула равен кол-ву ядер*1.5. |
dist_threads | Кол-во потоков для обработки внутри одного запроса. По умолчанию 0, что означает, параллельность внутри обработки одного запроса отключена. |
Далее будет подразумеваться, что в мантикоре выставлен рекомендуемый режим - пул потоков - workers = thread_pool.
Для начала давайте еще глубже спустимся и посмотрим по-диогонали, как вообще устроен пул потоков в мантикоре. Это поможет понять какие метрики надо отслеживать и какие настройки тюнить.
Давайте вспомним или поймем, что такое пул потоков или thread pool. Классический пул потоков представляет собой некую структуру, которая при запуске инициализирует определенное кол-во потоков. Каждый новый таск поступает на обработку в определенный уже запущенные поток исполнения. Таски или джобы прежде чем попасть в поток исполнения попадают в очередь. Очередь позволяет балансировать нагрузку на пул.
Взглянем на код
Пул потоков в мантикоре представлен классом CSphThdPool. Внутри как мы видим, все по классике: очередь представленна в виде связанного списка (указатели на голову и хвост m_pHead и m_pTail соответственно), вектор пула потоков - m_dWorkers. Чуть ниже - поля показывающие статистику пула потоков - m_tStatActiveWorkers - сколько воркеров сейчас исполняется (то есть сколько активных запросов или служебных задач сейчас в исполнении), и m_tStatQueuedJobs - кол-во джобов, ожидающих в очереди на исполнение.
Как же происходит обработка запроса? Взглянем на метод AddJob, который принимает указатель на джоб:
Пока что тут все ясно: джоб добавляется в связанный список, представляющий очередь пула потоков, и увеличивается счетчик m_iStatQueuedJobs. Именно этот счетчик ототображается в результате запроса SHOW STATUS;
в метрике work_queue_length:
Джоб еще не начал свое выполнение, он просто поставлен в очередь и ждет, когда наступит событие, по которому его "возьмут" на исполнение. В идеале, метрика work_queue_length должна быть всегда равна нулю. Если она начнет расти, тогда у меня плохие новости. Это говорит о том, что пул потоков иссяк, все потоки заняты и вновь приходящие запросы от клиентов будут "висеть".
За ограничение размера очереди отвечает настройка queue_max_length, по умолчанию она равна 0, что обозначает бесконечную очередь. Если задать эту настройку, то при превышении лимита, мантикора начнет отдавать вновь "прибывшим" запросам ответ maxed out в случае работы через старый бинарный протокол, либо too many requests в случае, если ваш драйвер работает по протоколу mysql. Эта ошибка возвращается со статусом "retry"
Ошибку клиент должен обработать и попытаться повторить запрос.
Далее рассмотрим "сердце" пула потоков, цикл обработки джоба в потоке:
Здесь все просто, на строках 1942-1950 мы выбираем джоб из очереди, далее уменьшаем счетчик m_iStatQueuedJobs (work_queue_length), увеличиваем счетчик активных джобов, запускаем джоб на исполнение. Как только джоб выполнился (повторюсь, это может быть запрос клиента или внутренний джоб мантикоры, например flush атрибутов на диск), уменьшаем счетчик m_tStatActiveWorkers - счетчик активных джобов. Этот счетчик отражается в метрике SHOW STATUS; под названием workers_active (см. рис. 4).
Итого, имеем три метрики, за которыми стоит следить:
workers_active - кол-во исполняемых в данный момент джобов
work_queue_length - кол-во джобов, "висящих" в очереди на исполнение
workers_total - текущий размер пула потоков, который задается на этапе запуска мантикоры и остается постоянным на всем протяжении работы. Этот размер задается настройкой max_children.
Еще одна интересная настройка dist_threads, по умолчанию она равна 0, что означает "каждый запрос будет обработан в один поток". Если у вас достаточно большой plain индекс и есть real-time индекс, то есть смысл plain-индекс разбить на несколько частей и создать ditributed-индекс, выставив dist_threads равное или чуть большее кол-ву частей distributed-индекса. На практике у меня на работе есть два rt-индекса и один plain. dist_threads выставлен в 3, то есть каждый запрос обрабатывается в три потока и каждый поток "смотрит" в plain либо в один из rt индексов. Прироста производительности не было замечено при выставлении dist_threads в "3", однако в нынешней ситуации роста кол-ва документов в plain индексе скорее всего придется разбивать plain индекс на две части и менять dist_threads соответственно. Время покажет.
Проблемы
Пока что есть довольно существенная проблема в мантикоре, это то, что real-time индекс фрагментируется, по мере роста кол-ва постоянных обновлений. У real-time индексов есть такая настройка rt_mem_limit которая задает лимит данных индекса, находящихся в памяти, по мере приближения к этому лимиту, мантикора начинает сбрасывать редко используемые данные на диск в виде блоков (chunk). Если измений с последнего сброса real-time индекса накопилось довольно много, то таких блоков становится много, более того, потребляемая процессом searchd память растет и может вырасти на десятки гигабайт.
Есть такая операция в мантикоре OPTIMIZE, она решает проблему фрагментированного rt-индекса, и позволяет "схлопнуть" все блоки в один, тем самым удерживая общую производительность в пределах нормы.
Существенная проблема мантикоры - это исчерпание пула потоков, если джобы "уперлись" в диск например. Тогда клиенту ничего не остается делать, как прервать запрос по таймауту и повторить позднее либо отправить запрос на другую ноду в реплике. По документации рекомендуется выставлять max_children в 1.5*кол-во ядер на машине, однако учтите также и создаваемую нагрузку на диск. Практически я пришел к выводу, что лучше все таки выставить явно max_children равное кол-ву ядер на хосте.
Заключение
Данная заметка далеко не полная и даже не является инструкцией к действию. Это просто сборник из моих записей и наблюдений в процессе эксплуатации sphinx и далее - manticoresearch. По моему скромному мнению, проект очень даже не плохой, если нужен полнотекстовый поиск на сайте, то это вполне рабочее решение. Возможно, если понравится читателям, в следующий раз я соберусь и опишу другие интересеные части manticoresearch.
Для подробностей обращайтесь к официальной документации.
mrBarabas
По тексту хдд и ссд местами перепутаны — это на хдд показатели будут ниже, а не на ссд