Введение
Перколятные запросы также известны как постоянные запросы, перспективный поиск, маршрутизация документов, поиск в обратном направлении или обратный поиск.
Обычный способ выполнения поиска заключается в хранении документов, которые мы хотим искать, и выполнении запросов к ним. Однако бывают случаи, когда мы хотим применить запрос к поступающему новому документу, чтобы определить совпадение. Существуют сценарии, где это необходимо. Например, система мониторинга не просто собирает данные, но также должна уведомлять пользователя о различных событиях. Это может быть достижение некоторого порога метрики или появление определённого значения в мониторируемых данных. Другой похожий случай — агрегирование новостей. Вы можете уведомлять пользователя о любой свежей новости, но пользователь может захотеть получать уведомления только о определённых категориях или темах. Более того, их могут интересовать только определённые «ключевые слова».
Приложения, выполняющие перколятные запросы, часто работают с высокой нагрузкой: им необходимо обрабатывать тысячи документов в секунду и они могут иметь сотни тысяч запросов, с которыми проверяется каждый документ.
И Elasticsearch, и Manticore Search предоставляют перколятные запросы.
Хотя перколятные запросы присутствуют в Elasticsearch уже некоторое время, в Manticore они были добавлены в версии 2.6.0, и всё ещё отсутствуют в Sphinx. Сегодня я хотел бы провести измерения производительности, чтобы выяснить цифры, которые может предоставить каждая из технологий.
Тестовый случай
Многие компании, работающие в индустрии маркетинга в социальных сетях, часто интересуются анализом потока Twitter firehose или decahose, другими словами они хотят искать по всем или части входящих сообщений, которые люди публикуют на twitter.com по всему миру, чтобы понять, насколько аудитория интересуется брендами, продуктами или чем‑то ещё их клиентов. Чтобы сделать наш тест более приближённым к реальности, мы также будем использовать образцы твитов. Я нашёл хороший архив на archive.org здесь https://archive.org/details/tweets-20170508080715-crawl448 , который предоставил мне около 600 000 случайных твитов:
$ wc -l ~/twitter/text
607388 /home/snikolaev/twitter/text
Документы декодированы из JSON, но на всякий случай, если вы хотите получить представление о том, как они выглядят (да, представим, что сейчас всё ещё 2005 год, и никто не знает, как обычно выглядит сообщение в Twitter):
$ head text
"\u8266\u968a\u304c\u5e30\u6295\u3057\u305f\u3002"
"RT @BTS_jp_official: #BTS \u65e5\u672c7th\u30b7\u30f3\u30b0\u30eb\u300c\u8840\u3001\u6c57\u3001\u6d99 -Japanese ver.-\u300dMV\n\u21d2 https:\/\/t.co\/jQGltkBQhj \niTunes\uff1a https:\/\/t.co\/2OGIYrrkaE\n#\u9632\u5f3e\u5c11\u5e74\u56e3 #\u8840\u6c57\u6d99"
"RT @79rofylno: \uc6b0'3'\nhttps:\/\/t.co\/4nrRh4l8IG\n\n#\uc7ac\uc708 #jaewin https:\/\/t.co\/ZdLk5BiHDn"
"Done with taxation supporting doc but still so mannnyyy work tak siap lagi \ud83d\ude2d"
"RT @RenfuuP: \ud64d\uc900\ud45c \uc9c0\uc9c0\uc728 20%\uc778 \uac1d\uad00\uc801 \uc774\uc720 https:\/\/t.co\/rLC9JCZ9XO"
"RT @iniemailku83: Tidak butuh lelaki yg kuat. Tp butuh lelaki yg bisa membuat celana dalam basah. Haaa"
"5\/10 02\u6642\u66f4\u65b0\uff01BUMP OF CHICKEN(\u30d0\u30f3\u30d7)\u306e\u30c1\u30b1\u30c3\u30c8\u304c27\u679a\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u3002\u4eca\u3059\u3050\u30c1\u30a7\u30c3\u30af https:\/\/t.co\/acFozBWrcm #BUMPOFCHICKEN #BUMP #\u30d0\u30f3\u30d7"
"---https:\/\/t.co\/DLwBKzniz6"
"RT @KamalaHarris: Our kids deserve the truth. Their generation deserves access to clean air and clean water.\nhttps:\/\/t.co\/FgVHIx4FzY"
"#105 UNIVERSITY East Concourse Up Escalator is now Out of Service"
Итак, это будут документы, которые мы будем тестировать. Второе, что нам крайне необходимо, — это собственно перколятные запросы. Я решил сгенерировать 100 000 случайных запросов из топ‑1000 самых популярных слов твитов, делая каждый запрос содержащим 2 ключевых слова с оператором AND и 2 ключевых слова с оператором NOT. Вот что у меня получилось (синтаксис запросов немного отличается между Manticore Search и Elasticsearch, поэтому мне пришлось создать 2 набора похожих запросов):
$ head queries_100K_ms.txt queries_100K_es.txt
== queries_100K_ms.txt ==
ll 0553 -pop -crying
nuevo against -watch -has
strong trop -around -would
most mad -cosas -guess
heart estamos -is -esc2017
money tel -didn -suis
omg then -tel -get
eviniz yates -wait -mtvinstaglcabello
9 sa -ao -album
tout re -oi -trop
== queries_100K_es.txt ==
ll AND 0553 -pop -crying
nuevo AND against -watch -has
strong AND trop -around -would
most AND mad -cosas -guess
heart AND estamos -is -esc2017
money AND tel -didn -suis
omg AND then -tel -get
eviniz AND yates -wait -mtvinstaglcabello
9 AND sa -ao -album
tout AND re -oi -trop
Запросы бессмысленны, но для целей тестирования производительности они имеют смысл.
Вставка запросов
Следующий шаг — вставить запросы в Manticore Search и Elasticsearch. Это можно легко сделать так:
Manticore Search
$ IFS=$'\n'; time for n in `cat queries_100K_ms.txt`; do mysql -P9314 -h0 -e "insert into pq values('$n');"; done;
real 6m37.202s
user 0m20.512s
sys 1m38.764s
Elasticsearch
$ IFS=$'\n'; id=0; time for n in `cat queries_100K_es.txt`; do echo $n; curl -s -XPUT http://localhost:9200/pq/docs/$id?refresh -H 'Content-Type: application/json' -d "{\"query\": {\"query_string\": {\"query\": \"$n\" }}}" > /dev/null; id=$((id+1)); done;
...
mayo AND yourself -ils -tan
told AND should -man -go
well AND week -won -perfect
real 112m58.019s
user 11m4.168s
sys 6m45.024s
Удивительно, но вставка 600 000 запросов в Elasticsearch заняла в 17 раз дольше, чем в Manticore Search.
Посмотрим, какова производительность поиска. Чтобы не ошибиться в выводах, сначала согласуем несколько предположений:
- конфигурации как Manticore Search, так и Elasticsearch используются в том виде, в каком они поставляются из коробки или согласно базовой документации, т.е. в них не было внесено никаких оптимизаций
- но поскольку Elasticsearch по умолчанию многопоточный, а Manticore Search — нет, я добавил "dist_threads = 8" в конфигурацию Manticore Search, чтобы уравнять шансы
- один из тестов будет заключаться в полном использовании сервера для измерения максимальной пропускной способности. В этом тесте, если мы увидим, что встроенное многопоточность в любом из движков не обеспечивает максимальную пропускную способность, мы поможем ей, добавив многопроцессность к многопоточности.
- достаточный размер Java heap важен для Elasticsearch. Индекс занимает всего 23 МБ на диске, но мы позволим Elasticsearch использовать 32 ГБ ОЗУ на всякий случай
- Для тестов мы будем использовать этот инструмент для нагрузочного тестирования с открытым исходным кодом — https://github.com/Ivinco/stress-tester
- Оба тестовых плагина открывают соединение только один раз на дочерний процесс и извлекают все найденные документы, что включено в измерение задержки
Запуск документов
Elasticsearch
Итак, начнём, и прежде всего поймём, какую максимальную пропускную способность может дать Elasticsearch. Мы знаем, что обычно пропускная способность растёт при увеличении размера пакета, давайте измерим это при concurrency = 1 (-c=1) на основе первых 10000 документов (--limit=10000) из тех 600 000, скачанных с archive.org и использованных для заполнения корпуса запросов.
$ for batchSize in 1 4 5 6 10 20 50 100 200; do ./test.php --plugin=es_pq_twitter.php --data=/home/snikolaev/twitter/text -b=$batchSize -c=1 --limit=10000 --csv; done;
concurrency;batch size;total time;throughput;elements count;latencies count;avg latency, ms;median latency, ms;95p latency, ms;99p latency, ms
1;1;34.638;288;10000;10000;3.27;2.702;6.457;23.308
1;4;21.413;466;10000;10000;8.381;6.135;19.207;57.288
1;5;20.184;495;10000;10000;9.926;7.285;22.768;60.953
1;6;19.773;505;10000;10000;11.634;8.672;26.118;63.064
1;10;19.984;500;10000;10000;19.687;15.826;57.909;76.686
1;20;23.438;426;10000;10000;46.662;40.104;101.406;119.874
1;50;30.882;323;10000;10000;153.864;157.726;232.276;251.269
1;100;69.842;143;10000;10000;696.237;390.192;2410.755;2684.982
1;200;146.443;68;10000;10000;2927.343;3221.381;3433.848;3661.143
Действительно, пропускная способность растёт, но только до размеров пакета 6‑10, после чего ухудшается. Общая пропускная способность выглядит очень слабой. С другой стороны задержка выглядит очень хорошей, особенно для самых маленьких размеров пакетов — 3‑20 мс для пакетов от 1 до 10. При этом я вижу, что сервер использован лишь на 25 %, что хорошо для задержки, но нам также хочется увидеть максимальную пропускную способность, поэтому запустим тест снова с concurrency = 8:
$ for batchSize in 1 4 5 6 10 20 50 100 200; do ./test.php --plugin=es_pq_twitter.php --data=/home/snikolaev/twitter/text -b=$batchSize -c=8 --limit=10000 --csv; done;
8;1;12.87;777;10000;10000;8.771;4.864;50.212;63.838
8;4;7.98;1253;10000;10000;24.071;12.5;77.675;103.735
8;5;7.133;1401;10000;10000;27.538;15.42;79.169;99.058
8;6;7.04;1420;10000;10000;32.978;19.097;87.458;111.311
8;10;7.374;1356;10000;10000;57.576;51.933;117.053;172.985
8;20;8.642;1157;10000;10000;136.103;125.133;228.399;288.927
8;50;11.565;864;10000;10000;454.78;448.788;659.542;781.465
8;100;25.57;391;10000;10000;1976.077;1110.372;6744.786;7822.412
8;200;52.251;191;10000;10000;7957.451;8980.085;9773.551;10167.927
1;200;146.443;68;10000;10000;2927.343;3221.381;3433.848;3661.143
Гораздо лучше: сервер полностью загружен, и пропускная способность теперь значительно выше, как и задержка. И мы всё ещё видим, что Elasticsearch ухудшается после размера пакета 6‑10:
Остановимся на размере пакета = 6, кажется, это самое оптимальное значение для этого теста. Проведём более длительный тест и обработаем 100 000 документов (--limit=100000):
$ ./test.php --plugin=es_pq_twitter.php --data=/home/snikolaev/twitter/text -b=6 -c=8 --limit=100000
Time elapsed: 0 sec, throughput (curr / from start): 0 / 0 rps, 0 children running, 100000 elements left
Time elapsed: 1.001 sec, throughput (curr / from start): 1510 / 1510 rps, 8 children running, 98440 elements left
Time elapsed: 2.002 sec, throughput (curr / from start): 1330 / 1420 rps, 8 children running, 97108 elements left
Time elapsed: 3.002 sec, throughput (curr / from start): 1433 / 1424 rps, 8 children running, 95674 elements left
...
Time elapsed: 67.099 sec, throughput (curr / from start): 1336 / 1465 rps, 8 children running, 1618 elements left
Time elapsed: 68.1 sec, throughput (curr / from start): 1431 / 1465 rps, 8 children running, 184 elements left
FINISHED. Total time: 68.345 sec, throughput: 1463 rps
Latency stats:
count: 100000 latencies analyzed
avg: 31.993 ms
median: 19.667 ms
95p: 83.06 ms
99p: 102.186 ms
Plugin's output:
Total matches: 509883
Count: 100000
И у нас есть цифра — Elasticsearch смог обрабатывать 1463 документа в секунду со средней задержкой ~32 мс. Первые 100 000 документов против 100 000 запросов в индексе дали 509 883 совпадения. Нам нужна эта цифра, чтобы затем сравнить её с Manticore Search и убедиться, что они не сильно различаются, что означает корректное сравнение.
Manticore Search
Теперь посмотрим на Manticore Search:
$ for batchSize in 1 4 5 6 10 20 50 100 200 300; do ./test.php --plugin=pq_twitter.php --data=/home/snikolaev/twitter/text -b=$batchSize -c=1 --limit=10000 --csv; done;
concurrency;batch size;total time;throughput;elements count;latencies count;avg latency, ms;median latency, ms;95p latency, ms;99p latency, ms
1;1;54.343;184;10000;10000;5.219;5.207;5.964;6.378
1;4;14.197;704;10000;10000;5.441;5.416;6.117;6.596
1;5;11.405;876;10000;10000;5.455;5.444;6.041;6.513
1;6;9.467;1056;10000;10000;5.466;5.436;6.116;6.534
1;10;5.935;1684;10000;10000;5.679;5.636;6.364;6.845
1;20;3.283;3045;10000;10000;6.149;6.042;7.097;8.001
1;50;1.605;6230;10000;10000;7.458;7.468;8.368;8.825
1;100;1.124;8899;10000;10000;10.019;9.972;11.654;12.542
1;200;0.95;10530;10000;10000;17.369;17.154;20.985;23.355
1;300;1.071;9334;10000;10000;29.058;28.667;34.449;42.163
Пропускная способность Manticore Search продолжает расти до размера пакета 200, при этом задержка не увеличивается так сильно (от миллисекунд до секунд), как в Elasticsearch. Однако для размера пакета 1 она на 2 мс выше, чем у Elasticsearch.
Пропускная способность еще более отличается по сравнению с Elasticsearch. Мы видим, что она значительно ниже при размере пакета < 20, но после этого, когда Elasticsearch начинает деградировать, пропускная способность Manticore Search растет до 10 K документов в секунду.
Как и в первоначальном тесте Elastic, мы видим, что сервер не полностью загружен (80% в данном случае по сравнению с 25% у Elastic). Увеличим параллелизм с 1 до 2, чтобы лучше загрузить сервер:
$ for batchSize in 1 4 5 6 10 20 50 100 200 300; do ./test.php --plugin=pq_twitter.php --data=/home/snikolaev/twitter/text -b=$batchSize -c=2 --limit=10000 --csv; done;
concurrency;batch size;total time;throughput;elements count;latencies count;avg latency, ms;median latency, ms;95p latency, ms;99p latency, ms
2;1;44.715;223;10000;10000;8.445;8.017;12.682;15.386
2;4;11.852;843;10000;10000;8.906;8.493;13.411;16.085
2;5;9.468;1056;10000;10000;9.001;8.566;13.476;16.284
2;6;8.004;1249;10000;10000;9.073;8.626;13.647;15.848
2;10;4.931;2028;10000;10000;9.202;8.799;13.525;15.622
2;20;2.803;3567;10000;10000;9.924;9.352;15.322;18.252
2;50;1.352;7395;10000;10000;12.28;11.946;18.048;27.884
2;100;0.938;10659;10000;10000;17.098;16.832;22.914;26.719
2;200;0.76;13157;10000;10000;27.474;27.239;35.103;36.61
2;300;0.882;11337;10000;10000;47.747;47.611;63.327;70.952
Это увеличило пропускную способность при размере пакета 200 с 10K до 13157 документов в секунду, хотя задержка также возросла.
Оставим этот размер пакета и параллелизм и проведём расширенный тест с 100K документов:
$ ./test.php --plugin=pq_twitter.php --data=/home/snikolaev/twitter/text -b=200 -c=2 --limit=100000
Time elapsed: 0 sec, throughput (curr / from start): 0 / 0 rps, 0 children running, 100000 elements left
Time elapsed: 1.001 sec, throughput (curr / from start): 12587 / 12586 rps, 2 children running, 87000 elements left
Time elapsed: 2.002 sec, throughput (curr / from start): 12990 / 12787 rps, 2 children running, 73800 elements left
Time elapsed: 3.002 sec, throughput (curr / from start): 13397 / 12991 rps, 2 children running, 60600 elements left
Time elapsed: 4.003 sec, throughput (curr / from start): 12992 / 12991 rps, 2 children running, 47600 elements left
Time elapsed: 5.004 sec, throughput (curr / from start): 12984 / 12989 rps, 2 children running, 34600 elements left
Time elapsed: 6.005 sec, throughput (curr / from start): 12787 / 12956 rps, 2 children running, 21800 elements left
Time elapsed: 7.005 sec, throughput (curr / from start): 12999 / 12962 rps, 2 children running, 8800 elements left
FINISHED. Total time: 7.722 sec, throughput: 12949 rps
Latency stats:
count: 100000 latencies analyzed
avg: 28.631 ms
median: 28.111 ms
95p: 37.679 ms
99p: 44.615 ms
Plugin's output:
Total matches: 518874
Count: 100000
Мы видим, что:
- Общее количество совпадений почти не отличается (519K vs 510K в Elastic), что означает корректность сравнения; небольшая разница, вероятно, вызвана незначительными различиями в токенизации текста
- Пропускная способность составляет 12949 документов в секунду
- Средняя задержка ~29ms
Следующие 2 графика показывают сравнение между Elasticsearch и Manticore Search при максимальной пропускной способности.
Медианная (50‑й процентиль) задержка на несколько миллисекунд лучше в Elasticsearch, средняя немного лучше в Manticore Search. Задержки 95p и 99p сильно различаются: здесь выигрывает Elasticsearch.
Но когда речь идёт о пропускной способности, Manticore Search превосходит Elasticsearch более чем в 8 раз.
Выводы
- Если вам нужно обрабатывать тысячи документов в секунду и вы можете позволить себе задержку в десятки миллисекунд (например, 32ms), Manticore Search может обеспечить гораздо более высокую пропускную способность: ~13K rps vs 1463 rps с Elastic
- Если ваша цель — чрезвычайно низкая и стабильная задержка (несколько ms) и у вас нет большого количества документов или вы можете распределить нагрузку между множеством серверов (например, 8 серверов Elasticsearch вместо 1, работающего с Manticore Search), чтобы обеспечить требуемую задержку, Elasticsearch может достичь всего 3.27ms задержки при пропускной способности 288 документов в секунду. Manticore Search может дать лишь 5.2ms при 188 rps или 29ms при 13K rps.