Масштабируем Elasticsearch
оптимизация реального кластера с индексами в несколько терабайт
Медленные запросы в Elaticsearch
Итак, работая над поисковым движком по социальной информации, мы остановили свой выбор на Elasticsearch, так как по отзывам он был очень легок в настройке и использование, имел огромные поисковые возможности и, в целом, выглядел как манна небесная. Все в целом так и было до тех пор, пока наш индекс не вырос до более-менее приличных- миллиарда (1 млрд) документов.
Даже банальный Term query
мог занять десятки (!) секунд. Документации по ES не так много, как хотелось бы, а гуглинг данного вопроса выдавал результаты 2х-летней давности по совсем не актуальным версиям нашего поискового движка (мы работаем с 0.90.13 - что тоже не самый айс, но мы не можем позволить себе опустить весь кластер, обновить его, и запустить заново на текущий момент - только роллинг рестарты).
Низкая скорость индексации Elasticsearch
Вторая проблема - мы индексируем больше документов в секунду, чем Elasticsearch может обрабатывать. Тайм-ауты, огромная нагрузка на Write IO, очереди из процессов по 400 единиц. Все выглядит очень страшно, когда смотришь на это в Marvel.
Как масштабировать кластер Elasticsearch
Исходная ситуация:
- 5 data (!) nodes, http enabled:
- 100 GB RAM
- 16 cores
- 4 TB HDD (7200 RPM, seagate)
- Индексы:
- от 500 до 1 млрд документов, всего порядка 5 штук
- количество primary шардов от 50 до 400 (здесь мы тестировали разные стратегии индексирования - эта настройка очень важна)
- реплики - от 2 до 5
- размер индекса до 1,5 терабайт
Проблема: медленные запросы, и, самое печальное, мы индексируем больше, чем позволяют наши машины.
Увеличиваем скорость индексирования в Elasticsearch
Итак, эта проблема оказалось не такой сложной и информации в интернете по ней чуть больше.
Чеклист, который нужно проверить:
refresh_interval
- как часто обновляются данные для поиска, чем чаще, тем больше Write IO вам требуетсяindex.translog.flush_threshold_ops
- через сколько операций скидывать данные на дискindex.translog.flush_threshold_size
- сколько данных должны быть добавлены в индекс перед скидыванием на диск
Подробная документация здесь:
В первую очередь мы увеличили refresh_interval
до 30 секунд, и фактически увеличили пропускную способность практически до 5000 документов в секунду. Позже поставили flush_threshold_ops
в 5000 операций, а размер до 500 мб. Если хотите, то можно поиграться с количеством реплик, шардов и так далее, но это не будет давать настолько большой разницы.
Увеличиваем скорость запросов в Elasticsearch
Теперь переходим к сложной части. Зная размер нашего индекса и постоянные потребности в перезагрузке кластера, а также принимая во внимание посты вроде этого: мы решили, что размер шарда в нашем индексе не будет превышать 1-2 ГБ. С учетом RF3, наш индекс (мы рассчитываем на 1,5 млрд документов), учитывая что 0,5 млрд наших документов занимают порядка 300 ГБ без учета реплик, мы создали в индексе 400 (!) шардов и посчитали что все будет хорошо - скорость ребута будет достаточно высока (нам не нужно будет читать блоки данных по 50-60 ГБ и блокировать таким образом восстановление маленьких шардов), да и скорость поиска по маленьким шардам выше.
По началу, количество документов в индексе было небольшим (100-200 млн) и скорость запроса составляла всего 100-200 мс. Но как только практически все шарды были заполнены хоть каким-то количество документов, мы начали значительно терять в производительности запросов, комбинируя все это с высокой нагрузкой на IO из-за постоянной индексации, мы могли и вообще не выполнить запрос.
В данном случае мы совершили 2 ошибки:
- Создали очень много шардов (идеальная ситуация 1 ядро - 1 шард)
- Наши дата ноды были и нодами-балансерами с включенным http - сериализация и десериализация данных занимает достаточно много времени
Поэтому мы начали экспериментировать.
Добавялем ноды-балансировщики в Elaticsearch
Первым и очевидным шагом для нас было добавлением, так называемых, balancer nodes
в Elasticsearch. Они могут производить агрегирование результатов запросов по другим шардам, у них никогда не будет перегружен IO, так как они не выполняют чтения и записи на диск, и мы разгрузим наши data nodes
.
Для деплоя мы используем chef
и соответствующий elasticsearch cookbook, поэтому создав всего пару дополнительных ролей, со следующими настройками:
|
|
Мы благополучно запустили 4 балансировщика. Картина немного улучшилась, мы больше не наблюдали перегруженных нод с дымящимися жесткими дисками, но скорость запросов была все еще низка.
Увеличиваем количество data nodes в Elasticsearch
Теперь мы вспомнили, что количество шардов, которое было у нас (400) никоим образом не сказывается на улучшении производительности, а лишь усугубляет ее, так как слишком больше количество шардов находится на 1 машине. Проведя простые вычисления мы получаем, что 5 машин адекватно поддержат только 80 шардов. Учитывая количество реплик, то их у нас вообще 1200.
Так как наш общий парк машин позвляет добавление достаточно большого количества нод и основная проблема в них - это размер HDD (всего 128гб), то мы решили добавить сразу порядка 15 машин. Это позволит работать с еще 240 шардами более эффективно.
Помимо этого мы наткнулись на несколько любопытных настроек:
index.store.type
- по умолчанию ставится в niofs, а по бенчмаркам производительность ниже чем у mmapfs - мы переключили его на mmapfsindices.memory.index_buffer_size
- увеличили до 30%- А количество RAM под Java Heap наоборот уменьшили до 30 ГБ, так как с
mmapfs
нам нужно намного больше оперативки для кеша операционной системы
И конечно же, в нашем случае было обязательно включить настройку контроля за расположением шардов на основе свободного места:
|
|
После пары дней переноса шардов и перезапуска старых серверов с новыми настройками, мы провели тесты и не (!) кешированные запросы (Term Query, не фильтры) выполнялись не более 500 мс. Данная ситуация все еще не идеальна, но мы видим, что добавление дата нод и подгон количества ядер под количество шардов исправляет ситуацию.
Что еще следует учесть при масштабировании кластера
При роллинг рестарте кластера, обязательно выключайте возможность переноса шардов: cluster.routing.allocation.enable = none
, в старых версиях чуть другая настройка.
Если возникли вопросы во время прочтения - буду рад обсудить.