Давеча у меня возникла необходимость пересмотреть работу полнотекстового поискового движка Sphinx, поскольку, некоторые нередкие запросы занимали секунды, а иные даже больше десяти. После поиска уязвимых мест и путей оптимизации нашел нехитрый способ повышения производительности — распараллеливание нагрузки на несколько потоков, в результате я получил неплохое сокращение времени запросов.

Одна из неприятных особенностей 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)


  1. SHSE
    30.07.2015 14:48
    -1

    А чем ElasticSearch или Solr не устроил?


    1. thunderspb
      30.07.2015 14:52
      +2

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


      1. klirichek
        30.07.2015 15:33
        +2

        Не обращайте внимания! Это просто такая традиция — в любой статье про Sphinx должен в числе первых быть комментарий с вопросом «а почему не Solr/Elastic?»
        Исключений я вроде пока не видел.


        1. SHSE
          30.07.2015 17:06
          +2

          Так интересен же выбор автора, вдруг чего не знаю. Оказалось что банально legacy. Не жалею что спросил :)


          1. Koc
            30.07.2015 18:30
            +1

            Например, нужно что б срабатывали определенные сортировки товаров, но при этом товары не должны выводиться скопом по 100 от каждой компании, а должны как бы итерироваться по компаниям — товар от компании 1, товар от компании 2,… товар от компании 1. В сфинксе это просто реализовать при помощи UDF, написанной на си.

            А еще коммуникация по mysql-протоколу проще и удобнее чем REST. Да и развернуть сфинкс на сервере проще, не нужно заморачиваться с явой.


  1. Koc
    30.07.2015 18:23

    Там есть определенные особенности с вызовом UDF насколько я помню. Типа на каждом индексе UDF будут вызываться отдельно. Поэтому могут возникнуть проблемы с сортировками.