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.
Для подробностей обращайтесь к официальной документации.
ссылка на оригинал статьи https://habr.com/ru/post/562448/
Добавить комментарий