Масштабируем Elasticsearch

оптимизация реального кластера с индексами в несколько терабайт

| Категории: 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 ядро - 1 шард)
  2. Наши дата ноды были и нодами-балансерами с включенным http - сериализация и десериализация данных занимает достаточно много времени

Поэтому мы начали экспериментировать.

Добавялем ноды-балансировщики в Elaticsearch

Первым и очевидным шагом для нас было добавлением, так называемых, balancer nodes в Elasticsearch. Они могут производить агрегирование результатов запросов по другим шардам, у них никогда не будет перегружен IO, так как они не выполняют чтения и записи на диск, и мы разгрузим наши data nodes.

Для деплоя мы используем chef и соответствующий elasticsearch cookbook, поэтому создав всего пару дополнительных ролей, со следующими настройками:

1
2
3
4
5
6
7
8
9
10
11
12
13
name "elasticsearch-balancer"
description "Installs and launches elasticsearch"
default_attributes(
"elasticsearch" => {
"node" => {
"master" => false,
"data" => false
}
}
)
run_list("services::elasticsearch")

Мы благополучно запустили 4 балансировщика. Картина немного улучшилась, мы больше не наблюдали перегруженных нод с дымящимися жесткими дисками, но скорость запросов была все еще низка.

Увеличиваем количество data nodes в Elasticsearch

Теперь мы вспомнили, что количество шардов, которое было у нас (400) никоим образом не сказывается на улучшении производительности, а лишь усугубляет ее, так как слишком больше количество шардов находится на 1 машине. Проведя простые вычисления мы получаем, что 5 машин адекватно поддержат только 80 шардов. Учитывая количество реплик, то их у нас вообще 1200.

Так как наш общий парк машин позвляет добавление достаточно большого количества нод и основная проблема в них - это размер HDD (всего 128гб), то мы решили добавить сразу порядка 15 машин. Это позволит работать с еще 240 шардами более эффективно.

Помимо этого мы наткнулись на несколько любопытных настроек:

  • index.store.type - по умолчанию ставится в niofs, а по бенчмаркам производительность ниже чем у mmapfs - мы переключили его на mmapfs
  • indices.memory.index_buffer_size - увеличили до 30%
  • А количество RAM под Java Heap наоборот уменьшили до 30 ГБ, так как с mmapfs нам нужно намного больше оперативки для кеша операционной системы

И конечно же, в нашем случае было обязательно включить настройку контроля за расположением шардов на основе свободного места:

1
2
3
4
5
curl -XPUT localhost:9200/_cluster/settings -d '{
"transient" : {
"cluster.routing.allocation.disk.threshold_enabled" : true
}
}'

После пары дней переноса шардов и перезапуска старых серверов с новыми настройками, мы провели тесты и не (!) кешированные запросы (Term Query, не фильтры) выполнялись не более 500 мс. Данная ситуация все еще не идеальна, но мы видим, что добавление дата нод и подгон количества ядер под количество шардов исправляет ситуацию.

Что еще следует учесть при масштабировании кластера

При роллинг рестарте кластера, обязательно выключайте возможность переноса шардов: cluster.routing.allocation.enable = none, в старых версиях чуть другая настройка.

Если возникли вопросы во время прочтения - буду рад обсудить.