Устранение барьеров на пути к масштабированию: как мы оптимизировали использование Elasticsearch в Intercom

Опубликовано: 2022-09-22

Elasticsearch — неотъемлемая часть Intercom.

Он поддерживает основные функции Intercom, такие как Inbox, Inbox Views, API, статьи, список пользователей, отчеты, Resolution Bot и наши внутренние системы ведения журналов. Наши кластеры Elasticsearch содержат более 350 ТБ данных клиентов, хранят более 300 миллиардов документов и обслуживают более 60 тысяч запросов в секунду в пиковые периоды.

По мере увеличения использования Intercom Elasticsearch нам необходимо обеспечить масштабирование наших систем, чтобы поддерживать наш постоянный рост. С недавним запуском Inbox нового поколения надежность Elasticsearch стала важнее, чем когда-либо.

Мы решили решить проблему с нашей настройкой Elasticsearch, которая создавала риск доступности и угрожала будущим простоем: неравномерное распределение трафика/работы между узлами в наших кластерах Elasticsearch.

Ранние признаки неэффективности: дисбаланс нагрузки

Elasticsearch позволяет масштабировать по горизонтали за счет увеличения количества узлов, в которых хранятся данные (узлов данных). Мы начали замечать дисбаланс нагрузки между этими узлами данных: некоторые из них находились под большей нагрузкой (или «горячее»), чем другие, из-за более высокой загрузки диска или ЦП.

Рис. 1 Дисбаланс в использовании ЦП
(Рис. 1) Несбалансированное использование ЦП: Два горячих узла с примерно на 20% более высокой загрузкой ЦП, чем в среднем.

Встроенная логика размещения сегментов Elasticsearch принимает решения на основе расчета, который приблизительно оценивает доступное дисковое пространство на каждом узле и количество сегментов индекса на каждом узле. Использование ресурсов сегментом не учитывается в этом расчете. В результате некоторые узлы могли получить больше ресурсоемких шардов и стать «горячими». Каждый поисковый запрос обрабатывается несколькими узлами данных. Горячий узел, который выходит за пределы своих возможностей во время пикового трафика, может привести к снижению производительности всего кластера.

Распространенной причиной горячих узлов является логика размещения сегментов, при которой большие сегменты (в зависимости от использования диска) назначаются кластерам, что снижает вероятность сбалансированного распределения. Как правило, узлу может быть назначено на один большой сегмент больше, чем другим, что увеличивает использование диска. Наличие больших сегментов также мешает нам постепенно масштабировать кластер, поскольку добавление узла данных не гарантирует снижения нагрузки со всех горячих узлов (рис. 2).

Рис. 4 Добавление узлов

(Рис. 2) Добавление узла данных не привело к снижению нагрузки на хост A. Добавление еще одного узла уменьшило бы нагрузку на хост A, но кластер по-прежнему будет иметь неравномерное распределение нагрузки.

Напротив, наличие меньших сегментов помогает снизить нагрузку на все узлы данных по мере масштабирования кластера, включая «горячие» узлы (рис. 3).

Рис. 3 Множество мелких осколков

(Рис. 3) Наличие множества небольших сегментов помогает снизить нагрузку на все узлы данных.

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

Дисбаланс нагрузки влияет не только на стабильность кластера, но и на нашу способность масштабироваться с минимальными затратами. Нам всегда придется добавлять больше мощности, чем необходимо, чтобы удерживать более горячие узлы ниже опасного уровня. Решение этой проблемы будет означать лучшую доступность и значительную экономию средств за счет более эффективного использования нашей инфраструктуры.

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

  • Больше осколков относительно количества узлов данных. Это гарантирует, что большинство узлов получат одинаковое количество осколков.
  • Меньшие осколки относительно размера узлов данных. Если бы некоторым узлам дали несколько дополнительных осколков, это не привело бы к значительному увеличению нагрузки на эти узлы.

Решение для кексов: меньше крупных узлов

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

Мы решили начать с кекса, чтобы проверить эту гипотезу. Мы перенесли несколько наших кластеров в более крупные и мощные экземпляры с меньшим количеством узлов, сохранив при этом общую емкость. Например, мы переместили кластер с 40 экземпляров 4xlarge на 10 экземпляров 16xlarge, уменьшив дисбаланс нагрузки за счет более равномерного распределения осколков.

Рис. 4. Лучшее распределение нагрузки между диском и ЦП

(Рис. 4) Лучшее распределение нагрузки между диском и ЦП за счет перехода к меньшему количеству более крупных узлов.

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

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

Проблема: превышение порога сжатых указателей на обычные объекты (ООП)

Переместиться на меньшее количество более крупных узлов было не так просто, как просто изменить размер экземпляра. Узким местом, с которым мы столкнулись, было сохранение общего доступного размера кучи (размер кучи на одном узле x общее количество узлов) при миграции.

Мы ограничили размер кучи в наших узлах данных до ~ 30,5 ГБ, как было предложено Elastic, чтобы убедиться, что мы остаемся ниже отсечки, чтобы JVM могла использовать сжатые ООП. Если бы мы ограничили размер кучи примерно до 30,5 ГБ после перехода к меньшему количеству узлов большего размера, мы бы уменьшили общую емкость кучи, поскольку нам пришлось бы работать с меньшим количеством узлов.

«Экземпляры, на которые мы мигрировали, были огромными, и мы хотели выделить большую часть оперативной памяти для кучи, чтобы у нас было место для указателей и достаточно места для кеша файловой системы».

Мы не смогли найти много советов о влиянии пересечения этого порога. Экземпляры, на которые мы мигрировали, были огромными, и мы хотели выделить большую часть ОЗУ для кучи, чтобы у нас было место для указателей и достаточно места для кеша файловой системы. Мы экспериментировали с несколькими пороговыми значениями, реплицируя наш производственный трафик на тестовые кластеры, и остановились на ~33-42% ОЗУ в качестве размера кучи для машин с более чем 200 ГБ ОЗУ.

Изменение размера кучи по-разному повлияло на разные кластеры. В то время как некоторые кластеры не показали изменений в таких показателях, как «% кучи JVM в использовании» или «Время сбора молодого GC», общая тенденция заключалась в увеличении. Несмотря на это, в целом это был положительный опыт, и наши кластеры работают уже более 9 месяцев с этой конфигурацией — без каких-либо проблем.

Долгосрочное исправление: множество мелких осколков

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

  • Миграция индекса, чтобы иметь больше первичных осколков: это распределяет данные в индексе по большему количеству осколков.
  • Разбивка индекса на более мелкие индексы (разделы): это распределяет данные в индексе среди большего количества индексов.

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

«Мы сосредоточились на том, чтобы упростить экспериментирование и исправление неоптимальных конфигураций в нашей системе, а не зацикливаться на «идеальной» конфигурации».

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

Чтобы сделать наши системы более отказоустойчивыми, мы сосредоточились на том, чтобы упростить экспериментирование и исправление неоптимальных конфигураций в нашей системе, а не зацикливаться на «идеальной» конфигурации.

Индексы секционирования

Увеличение количества первичных сегментов иногда может повлиять на производительность запросов, объединяющих данные, что мы испытали при переносе кластера, отвечающего за продукт Intercom Reporting. Напротив, разделение индекса на несколько индексов распределяет нагрузку на большее количество сегментов без снижения производительности запросов.

Intercom не требует совместного размещения данных для нескольких клиентов, поэтому мы выбрали разделение на основе уникальных идентификаторов клиентов. Это помогло нам быстрее получить выгоду за счет упрощения логики разбиения и сокращения объема необходимой настройки.

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

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

Наш режим восстановления после сбоя заключался в повторении запросов, поэтому мы внесли необходимые изменения там, где мы делали неидемпотентные запросы. В итоге мы добавили несколько линтеров, чтобы не поощрять использование таких API, как `update/delete_by_query`, поскольку они упрощают выполнение неидемпотентных запросов.

Мы создали две возможности, которые работали вместе, чтобы обеспечить полную функциональность:

  • Способ маршрутизации запросов из одного индекса в другой. Этот другой индекс может быть разделом или просто неразделенным индексом.
  • Способ двойной записи данных в несколько индексов. Это позволило нам синхронизировать разделы с переносимым индексом.

«Мы оптимизировали наши процессы, чтобы свести к минимуму радиус поражения любых инцидентов без ущерба для скорости»

В целом процесс переноса индекса на партиции выглядит так:

  1. Мы создаем новые разделы и включаем двойную запись, чтобы наши разделы оставались актуальными с исходным индексом.
  2. Мы запускаем обратную засыпку всех данных. Эти запросы обратной засыпки будут дважды записываться в новые разделы.
  3. Когда заполнение завершается, мы проверяем, что и старый, и новый индексы содержат одни и те же данные. Если все выглядит нормально, мы используем флаги функций, чтобы начать использовать разделы для нескольких клиентов и отслеживать результаты.
  4. Как только мы будем уверены, мы переместим всех наших клиентов в разделы, одновременно выполняя двойную запись как в старый индекс, так и в разделы.
  5. Когда мы уверены, что миграция прошла успешно, мы прекращаем двойную запись и удаляем старый индекс.

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

Получение преимуществ

Эта работа помогла нам улучшить баланс нагрузки в наших кластерах Elasticsearch. Что еще более важно, теперь мы можем улучшать распределение нагрузки каждый раз, когда оно становится неприемлемым, перенося индексы в разделы с меньшим количеством первичных сегментов, достигая лучшего из обоих миров: меньшего количества и меньших сегментов на индекс.

Применяя эти знания, мы смогли добиться значительного повышения производительности и экономии.

  • Мы сократили затраты на два наших кластера на 40% и 25% соответственно, а также получили значительную экономию на других кластерах.
  • Мы снизили среднюю загрузку ЦП для определенного кластера на 25 % и улучшили медианную задержку запросов на 100 %. Мы достигли этого за счет переноса индекса с высоким трафиком на разделы с меньшим количеством первичных сегментов на раздел по сравнению с исходным.
  • Общая возможность переноса индексов также позволяет нам изменять схему индекса, позволяя инженерам по продуктам создавать более удобные условия для наших клиентов, или повторно индексировать данные с помощью более новой версии Lucene, которая открывает возможность обновления до Elasticsearch 8.

Рис. 5. Улучшение дисбаланса нагрузки и загрузки ЦП

(Рис. 5) 50-процентное улучшение дисбаланса нагрузки и 25-процентное улучшение использования ЦП за счет переноса индекса с высоким трафиком на разделы с меньшим количеством основных сегментов на раздел.

Рис.6 Средняя задержка запроса

(Рис. 6) Медианная задержка запросов уменьшилась в среднем на 100 % за счет переноса индекса с высоким трафиком на разделы с меньшим количеством основных сегментов на раздел.

Что дальше?

Внедрение Elasticsearch для поддержки новых продуктов и функций должно быть простым. Наше видение состоит в том, чтобы нашим инженерам было так же просто взаимодействовать с Elasticsearch, как современные веб-фреймворки упрощают взаимодействие с реляционными базами данных. Команде должно быть легко создавать индекс, читать или записывать из индекса, вносить изменения в его схему и т. д., не беспокоясь о том, как обслуживаются запросы.

Вам интересно, как работает наша команда инженеров в Intercom? Узнайте больше и ознакомьтесь с нашими открытыми вакансиями здесь.

Карьера CTA - Инженерное дело (горизонтальное)