Давеча у меня возникла необходимость пересмотреть работу полнотекстового поискового движка Sphinx, поскольку, некоторые нередкие запросы занимали секунды, а иные даже больше десяти. После поиска уязвимых мест и путей оптимизации нашел нехитрый способ повышения производительности — распараллеливание нагрузки на несколько потоков, в результате я получил неплохое сокращение времени запросов.
Одна из неприятных особенностей Sphinx'а — очень скудная информация на русском языке. Удивишись тем, что тема распределения нагрузки не была затронута, решил поделиться данным решением на Хабре.
Цель: повысить производительность Sphinx путём разделения нагрузки на несколько потоков.
Решение: разделить индексы и указать в конфигурации количество потоков.
Начнем с более простого — указания количества потоков выполнения. Предположим, что на нашем сервере четырехядерный процессор, поэтому оптимальнее всего будет использовать четыре потока. Для этого используем директиву dist_threads в разделе searchd конфигурационного файла.
Директива обозначает максимальное количество потоков для обработки запроса. Значение по умолчанию — 0, что подразумевает неиспользование распаралелливания.
Далее, разделим индексы, чтобы каждый поток обрабатывал свой интервал записей. Другими словами: допустим, в нашей таблице 1 000 000 записей и четыре потока. Необходимо, чтобы каждый поток обработал 1 000 000 / 4 = 250 000 записей, чтобы в дальнейшем Sphinx получил результаты работы этих потоков и выдал наиболее релевантный результат. Логично, что четыре потока обрабатывающие по 250 000 записей быстрее справятся с работой, чем один поток, обрабатывающий 1 000 000 записей почти в четыре раза.
Предположим, что у нас есть некоторый source и index:
Для примера оставим директиву min_infix_len.
Чтобы разделить индекс на четыре части, создадим четыре source с ограниченными интервалами записей и назначим им по index'у:
Самый простой способ разделить таблицу на приблизительно равные части — указать в запросе кратность id записи, однако это не является единственным способом. Как вариант, можно использовать директиву sql_query_range, однако в моем случае этот метод не подошел в связи с неоднородностью распределения id-шников записей по таблице.
Хорошей практикой будет указать прародителей index'ов и sourc'ов, чтобы наследоваться от них и вынести в них некоторые повторяющиеся директивы. В данном случае, я вынес в них директивы type и min_infix_len.
Чтобы иметь некоторый индекс, к которому можно было обратиться за результатами, мы создали индекс ind_books с типом distributed в директивах local которого указали имена индексов, результаты которых необходимо получить.
Если вы используете delta индекс то самым простым решением будет мержить его с одним из полученных индексов.
Однако, в этом случае, необходимо иметь ввиду, что если delta окажется слишком большой, то выбранный индекс, с которым мы мержим её, окажется заметно больше остальных, что может негативно сказаться на производительности. Чтобы предотвратить это, оптимальнее всего мержить её со всеми индексами поочередно.
В конечном итоге, использование этого метода позволило мне сократить время запросов от 2 до 10 раз.
Одна из неприятных особенностей Sphinx'а — очень скудная информация на русском языке. Удивишись тем, что тема распределения нагрузки не была затронута, решил поделиться данным решением на Хабре.
Цель: повысить производительность Sphinx путём разделения нагрузки на несколько потоков.
Решение: разделить индексы и указать в конфигурации количество потоков.
Потоки выполнения
Начнем с более простого — указания количества потоков выполнения. Предположим, что на нашем сервере четырехядерный процессор, поэтому оптимальнее всего будет использовать четыре потока. Для этого используем директиву dist_threads в разделе searchd конфигурационного файла.
searchd
{
...
dist_threads = 4
...
}
Директива обозначает максимальное количество потоков для обработки запроса. Значение по умолчанию — 0, что подразумевает неиспользование распаралелливания.
Разделение индексов
Далее, разделим индексы, чтобы каждый поток обрабатывал свой интервал записей. Другими словами: допустим, в нашей таблице 1 000 000 записей и четыре потока. Необходимо, чтобы каждый поток обработал 1 000 000 / 4 = 250 000 записей, чтобы в дальнейшем Sphinx получил результаты работы этих потоков и выдал наиболее релевантный результат. Логично, что четыре потока обрабатывающие по 250 000 записей быстрее справятся с работой, чем один поток, обрабатывающий 1 000 000 записей почти в четыре раза.
Предположим, что у нас есть некоторый source и index:
source books {
type = mysql
sql_query = SELECT id, name FROM tb_books
}
index books {
source = books
min_infix_len = 3
}
Для примера оставим директиву min_infix_len.
Чтобы разделить индекс на четыре части, создадим четыре source с ограниченными интервалами записей и назначим им по index'у:
source books_base {
type = mysql
}
source books0: books_base {
sql_query = SELECT id, name FROM tb_books WHERE id % 4 = 0
}
source books1: books_base {
sql_query = SELECT id, name FROM tb_books WHERE id % 4 = 1
}
source books2: books_base {
sql_query = SELECT id, name FROM tb_books WHERE id % 4 = 2
}
source books3: books_base {
sql_query = SELECT id, name FROM tb_books WHERE id % 4 = 3
}
index ind_books_base {
min_infix_len = 3
}
index ind_books0: ind_books_base {
source = books0
}
index ind_books1: ind_books_base {
source = books1
}
index ind_books2: ind_books_base {
source = books2
}
index ind_books3: ind_books_base {
source = books3
}
index ind_books {
type = distributed
local = ind_books0
local = ind_books1
local = ind_books2
local = ind_books3
}
Самый простой способ разделить таблицу на приблизительно равные части — указать в запросе кратность id записи, однако это не является единственным способом. Как вариант, можно использовать директиву sql_query_range, однако в моем случае этот метод не подошел в связи с неоднородностью распределения id-шников записей по таблице.
Хорошей практикой будет указать прародителей index'ов и sourc'ов, чтобы наследоваться от них и вынести в них некоторые повторяющиеся директивы. В данном случае, я вынес в них директивы type и min_infix_len.
Чтобы иметь некоторый индекс, к которому можно было обратиться за результатами, мы создали индекс ind_books с типом distributed в директивах local которого указали имена индексов, результаты которых необходимо получить.
Delta
Если вы используете delta индекс то самым простым решением будет мержить его с одним из полученных индексов.
Однако, в этом случае, необходимо иметь ввиду, что если delta окажется слишком большой, то выбранный индекс, с которым мы мержим её, окажется заметно больше остальных, что может негативно сказаться на производительности. Чтобы предотвратить это, оптимальнее всего мержить её со всеми индексами поочередно.
В конечном итоге, использование этого метода позволило мне сократить время запросов от 2 до 10 раз.
Комментарии (6)
Koc
30.07.2015 18:23Там есть определенные особенности с вызовом UDF насколько я помню. Типа на каждом индексе UDF будут вызываться отдельно. Поэтому могут возникнуть проблемы с сортировками.
SHSE
А чем ElasticSearch или Solr не устроил?
thunderspb
Так вопрос не в том «что выбрать», а как оптимизировать то, что уже стоит же. В корп сегменте миграция с одного продукта на другой дело далеко не тривиальное.
klirichek
Не обращайте внимания! Это просто такая традиция — в любой статье про Sphinx должен в числе первых быть комментарий с вопросом «а почему не Solr/Elastic?»
Исключений я вроде пока не видел.
SHSE
Так интересен же выбор автора, вдруг чего не знаю. Оказалось что банально legacy. Не жалею что спросил :)
Koc
Например, нужно что б срабатывали определенные сортировки товаров, но при этом товары не должны выводиться скопом по 100 от каждой компании, а должны как бы итерироваться по компаниям — товар от компании 1, товар от компании 2,… товар от компании 1. В сфинксе это просто реализовать при помощи UDF, написанной на си.
А еще коммуникация по mysql-протоколу проще и удобнее чем REST. Да и развернуть сфинкс на сервере проще, не нужно заморачиваться с явой.