Elasticsearch

Оптимизация производительности кластера Elasticsearch

"Каждая мелочь имеет значение"
Александр Романюк
автор, инженер-проектировщик систем мониторинга
Elasticsearch — это поисковая и аналитическая система с открытым исходным кодом, основанная на Apache Lucene, которая позволяет пользователям хранить, искать и анализировать данные практически в реальном времени. Хотя Elasticsearch предназначен для быстрых запросов, производительность во многом зависит от сценариев, применимых к приложению, объема индексируемых данных и скорости, с которой приложения и пользователи запрашивают эти самые данные.

В этой статье описаны проблемы производительности кластера Elasticsearch, а также инструменты, которые можно использовать для устранения этих проблем. Ключевыми проблемами, возникающими в процессе эксплуатации кластера Elasticsearch можно назвать падение производительности при возрастании нагрузки, увеличение задержки выполнения поисковых запросов, неоптимальные настройки индексов. А эффективными решениями, названных выше проблем, можно назвать оптимальный сайзинг, оптимальный дизайн индекса, тюнинг производительности индекса и тюнинг производительности поиска. Эти вещи мы разберем в статье.
Оптимизация дизайна индекса
Давайте дважды подумаем, прежде чем начинать принимать данные и выполнять запросы. Что обозначает индекс? Официальный ответ Elastic — «набор документов, которые имеют схожие характеристики». Следующий вопрос: «Какие характеристики следует использовать для группировки данных? Должен ли я помещать все документы в один индекс или в несколько индексов?» Ответ заключается в том, что это зависит от запроса, который вы использовали. Ниже приведены некоторые рекомендации по организации индексов в соответствии с наиболее часто используемыми запросами.

  • Разделите данные на несколько индексов, если в запросе есть поле фильтра и его значение перечислимо. Например, у вас есть много глобальной информации о продуктах, загруженной в Elasticsearch, в большинстве ваших запросов есть фильтр «регион», и низкая вероятность того, что будут выполняться межрегиональные запросы. Тело запроса можно оптимизировать:

{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "title": "${title}"
                }
            },
            "filter": {
                "term": {
                    "region": "US"
                }
            }
        }
    }
}
В этом сценарии мы можем получить лучшую производительность, если индекс будет разделен на несколько более мелких индексов в зависимости от региона, таких как Россия, Казахстан и другие. Затем фильтр может быть удален из запроса. Если нужно выполнить межрегиональный запрос, можно просто передать несколько индексов или подстановочных знаков в Elasticsearch.

  • Используйте маршрутизацию, если в запросе есть поле фильтра и его значение не перечислимо. Можно разделить индекс на несколько шардов, используя значение поля фильтра в качестве ключа маршрутизации и удалив фильтр.

    Например, в Elasticsearch поступают миллионы заказов, и для большинства запросов необходимо запрашивать заказы по идентификатору покупателя. Невозможно создать индекс для каждого покупателя, поэтому мы не можем разделить данные на несколько индексов по идентификатору покупателя. Правильное решение — использовать маршрутизацию, чтобы поместить все заказы с одним и тем же идентификатором покупателя в один шард. Тогда почти все ваши запросы могут быть выполнены в шарде, соответствующем ключу маршрутизации.
  • Организуйте данные по дате, если в вашем запросе есть фильтр диапазона дат. Это работает для большинства сценариев логирования или мониторинга. Можно организовать индексы по дням, неделям или месяцам, а затем получать список индексов по указанному диапазону дат. Elasticsearch будет нужно запрашивать меньший набор данных, а не весь набор. Кроме того, было бы легко сжать/удалить старые индексы, когда срок актуальности данных истек.
  • Задайте маппинг явно. Elasticsearch может создавать динамически маппинг индекса, но это бывает не всегда оптимально. Например, маппинг строковых полей может быть по умолчанию с типом text вместо keyword. Это излишне во многих сценариях.
  • Избегайте несбалансированного шардирования, если документы индексируются с помощью определяемого пользователем идентификатора или маршрутизации. Elasticsearch использует генератор случайных идентификаторов и алгоритм хеширования, чтобы обеспечить равномерное распределение документов по сегментам. Когда вы используете определяемый пользователем идентификатор или маршрутизацию, идентификатор или ключ маршрутизации могут быть недостаточно случайными, а некоторые сегменты могут быть явно больше других. В этом сценарии операция чтения/записи на этом сегменте будет намного медленнее, чем на других. Можно оптимизировать ключ ID/маршрутизации или использовать index.routing_partition_size.
  • Распределите шарды равномерно по узлам. Если один узел имеет больше шардов, чем другие, он будет испытывать больше нагрузки, что может стать узким местом всей системы. Кластер Elasticsearch работает со скоростью самого медленного узла.
Оптимизация производительности индекса
Для тяжелых сценариев индексирования, таких как логировзззхание и мониторинг, ключевым показателем является производительность индексирования. Вот несколько рекомендаций.

  • Используйте bulk-запросы.
  • Используйте несколько потоков для отправки запросов.
  • Увеличьте интервал обновления. Каждый раз, когда происходит событие обновления, Elasticsearch создает новый сегмент Lucene и позже объединяет их. Увеличение интервала обновления уменьшит стоимость создания/объединения сегментов. Обратите внимание, что документы будут доступны для поиска только после обновления.
Из приведенного выше графика видно, что пропускная способность увеличилась, а время отклика уменьшилось по мере увеличения интервала обновления. Можно использовать приведенный ниже запрос, чтобы проверить, сколько сегментов есть и сколько времени тратится на обновление и слияние.

Index/_stats?filter_path= indices.**.refresh,indices.**.segments,indices.**.merges
  • Уменьшите количество реплик. Elasticsearch необходимо записывать документы в основной шард и все реплики для каждого запроса на индексирование. Очевидно, что большое количество реплик замедлит скорость индексации, но, с другой стороны, улучшит производительность поиска. Мы поговорим об этом позже в этой статье.
На приведенном выше графике видно, что пропускная способность уменьшилась, а время отклика увеличилось по мере увеличения количества реплик.

  • Используйте автоматически сгенерированные идентификаторы документов, если это возможно. Автоматически сгенерированный идентификатор Elasticsearch гарантированно будет уникальным, чтобы избегать в дальнейшем поиска нужной версии документа. Если пользователю действительно нужно использовать собственный идентификатор, предлагаем выбрать идентификатор, удобный для Lucene, например последовательные идентификаторы с нулевым дополнением, UUID-1 или nano time. Эти идентификаторы имеют согласованные последовательные шаблоны, которые хорошо сжимаются. Напротив, идентификаторы, такие как UUID-4, по существу являются случайными, что обеспечивает плохое сжатие и замедляет работу Lucene.
Оптимизация производительности поиска
Основная причина использования Elasticsearch — поддержка поиска по данным. Пользователи должны иметь возможность быстро находить нужную им информацию. Эффективность поиска зависит от нескольких факторов.

  • Используйте контекст фильтра вместо контекста запроса. Контекст запроса используется для ответа на вопрос «Насколько хорошо этот документ соответствует этому выражению?» Контекст фильтра используется для ответа на вопрос «Соответствует ли этот документ этому выражению?» Elasticsearch нужно только ответить «Да» или «Нет». Не нужно вычислять показатель релевантности для предложения фильтра, а результаты фильтрации можно кэшировать.
  • Увеличьте интервал обновления. Как упоминалось в разделе о производительности индекса, Elasticsearch создает новый сегмент каждый раз, когда происходит обновление. Увеличение интервала обновления поможет уменьшить количество сегментов и снизить затраты на ввод-вывод для поиска. Кэш будет неактуальным после обновления и изменения данных. Увеличение интервала обновления может заставить Elasticsearch более эффективно использовать кеш.
  • Увеличить количество реплик. Elasticsearch может выполнять поиск как в основном шарде, так и в реплике. Чем больше реплик у вас есть, тем больше узлов может быть задействовано в вашем поиске.
На приведенном выше графике видно, что пропускная способность поиска почти линейна по количеству реплик. Обратите внимание, что в этом тесте в тестовом кластере достаточно узлов данных, чтобы каждый шард имел эксклюзивный узел. Если это условие не может быть выполнено, производительность поиска будет не такой хорошей.

  • Попробуйте разное количество шардов. К сожалению, нет правильного количества для всех сценариев. Это зависит от конкретного случая.

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

    По опыту, если размер индекса меньше 1 ГБ, можно установить количество шардов равным 1. Количество шардов нельзя изменить после создания индекса, но можно создать новый индекс и использовать API переиндексации для перемещения данных.
На приведенном выше графике видно, что оптимальное количество шардов равно 11. Пропускная способность поиска увеличилась (уменьшение времени отклика) в начале, но снизилась (увеличение времени отклика) по мере увеличения количества шардов.
Обратите внимание, что в этом тесте, как и в тесте количества реплик, каждый шард располагается на отдельном узле. Если это условие не может быть выполнено, производительность поиска не будет такой хорошей, как на этой диаграмме.

  • Кэш запросов узла. Кэш запросов узла кэширует только те запросы, которые используются в контексте фильтра. В отличие от контекста запроса, контекст фильтра представляет собой вопрос «Да» или «Нет». Обратите внимание, что только шарды, которые содержат более 10 000 документов (или 3 % от общего числа документов, в зависимости от того, что больше), включают кеширование запросов.

    Можно использовать следующий запрос, чтобы проверить, влияет ли кэш запросов узла на производительность.

GET index_name/_stats?filter_path=indices.**.query_cache
{
  "indices": {
    "index_name": {
      "primaries": {
        "query_cache": {
          "memory_size_in_bytes": 46004616,
          "total_count": 1588886,
          "hit_count": 515001,
          "miss_count": 1073885,
          "cache_size": 630,
          "cache_count": 630,
          "evictions": 0
        }
      },
      "total": {
        "query_cache": {
          "memory_size_in_bytes": 46004616,
          "total_count": 1588886,
          "hit_count": 515001,
          "miss_count": 1073885,
          "cache_size": 630,
          "cache_count": 630,
          "evictions": 0
        }
      }
    }
  }
}
  • Кэш запросов уровня шарда. Если большинство запросов являются агрегированными, следует обратить внимание на кэш запросов шарда, который может кэшировать агрегированные результаты, чтобы Elasticsearch обслуживал запрос напрямую с небольшими затратами. Есть несколько вещей, о которых нужно позаботиться:

    Установите «размер»: 0. Кэш сегментированных запросов кэширует только совокупные результаты и предложения (suggestions). Он не кэширует совпадения, поэтому, если вы установите ненулевой размер, вы не сможете извлечь выгоду из кэширования.

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

    Округлите время даты. Не используйте такие переменные, как Date.now, в своем запросе напрямую. Округлите его. В противном случае у вас будет разное тело для каждого запроса, что сделает кеш всегда недействительным. Предлагаем округлить дату и время до часа или дня, чтобы более эффективно использовать кеш.

    Можно использовать приведенный ниже запрос, чтобы проверить, влияет ли кэш запросов уровня шарда.

GET index_name/_stats?filter_path=indices.**.request_cache
{
  "indices": {
    "index_name": {
      "primaries": {
        "request_cache": {
          "memory_size_in_bytes": 0,
          "evictions": 0,
          "hit_count": 541,
          "miss_count": 514098
        }
      },
      "total": {
        "request_cache": {
          "memory_size_in_bytes": 0,
          "evictions": 0,
          "hit_count": 982,
          "miss_count": 947321
        }
      }
    }
  }
}
  • Получайте только необходимые поля. Если документы большие и вам нужно всего несколько полей, используйте stored_fields для извлечения нужных полей, а не всех полей.
  • Избегайте поиска стоп-слов. Стоп-слова, такие как «и» и «в», могут привести к резкому увеличению числа результатов запроса. Представьте, что у вас есть миллион документов. Поиск «fox» может дать десятки совпадений, но поиск «the fox» может вернуть все документы в индексе, поскольку «the» появляется почти во всех документах. Elasticsearch должен подсчитывать и сортировать все результаты поиска, поэтому такой запрос, как «the fox», замедлит работу всей системы. Вы можете использовать фильтр токенов, чтобы удалить стоп-слова, или использовать оператор «и», чтобы изменить запрос с «the fox» на «the AND fox», чтобы получить более точный результат поиска.

    Если некоторые слова часто используются в вашем датасете, но не входят в список стоп-слов по умолчанию, вы можете использовать частоту отсечения для их динамической обработки.
  • Сортируйте по _doc, если вас не волнует порядок, в котором возвращаются документы. Elasticsearch по умолчанию использует поле «_score» для сортировки по количеству баллов. Если вас не волнует порядок, вы можете использовать «sort»: «_doc», чтобы позволить Elasticsearch возвращать совпадения по порядку индекса.
  • Избегайте использования скриптового запроса для расчета совпадений на лету. Сохраняйте вычисляемые поля при индексации. Например, у нас есть индекс с большим количеством информации о пользователях, и нам нужно запросить всех пользователей, чей номер начинается с «1234». Возможно, вы захотите запустить запрос, например «source»: «doc['num'].value.startsWith('1234')». Этот запрос действительно ресурсоемкий и замедляет работу всей системы. Рассмотрите возможность добавления поля с именем "num_prefix" при индексировании. Затем мы можем просто запросить «name_prefix»: «1234».
  • Избегайте запросов с маской (wildcard).
Производительность Elasticsearch зависит от множества факторов, включая структуру документа, размер документа, настройки/маппинг индекса, частота запросов, размер набора данных, количество обращений к индексу и так далее. Рекомендация для одного сценария не обязательно работает для другого. Важно тщательно протестировать производительность, собрать данные телеметрии, настроить конфигурацию в соответствии с рабочими нагрузками и оптимизировать настройку кластеров по производительности.
Что дальше

Приглашаем на семинар-инструктаж по Elastic Stack 8

25-27 марта 2024
Другие наши статьи об Elastic
Статья на Хабре
Статья на Хабре
Статья на Хабре
Статья на Хабре
Есть вопросы или предложения?
Вы можете написать здесь и при необходимости приложить файлы.
Нажимая на кнопку, вы даете согласие на обработку персональных данных и соглашаетесь c политикой конфиденциальности.