Находите важные ошибки JavaScript еще быстрее

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

Агрегации: GROUPBY в Elasticsearch

Когда вы посещаете страницу «Ошибки» в TrackJS, мы показываем вам разбитый на страницы список сообщений об ошибках и количество появлений каждого из них. Вы можете сортировать по последним обнаруженным ошибкам, общему количеству ошибок или количеству затронутых уникальных пользователей. По сути, мы группируем ошибки по их сообщениям. В реляционной базе данных это можно сделать с помощью предложения GROUP BY. В Elasticseach это называется Terms Aggregation. По сути, мы делаем одно и то же — группируем кучу вещей по одному полю (в данном случае сообщение об ошибке).

Но мы можем пойти дальше в Elasticsearch и выполнять вложенные агрегаты (группировки). Например, для каждой группы сообщений об ошибках мы также можем получить количество уникальных затронутых браузеров, URL-адресов и пользователей, а также затронутые версии вашего кода и удобную гистограмму появления этой группы с течением времени. Мы можем получить все это для каждого элемента в нашем сгруппированном списке! Это было бы очень сложно сделать с помощью SQL в традиционной базе данных.

Группировка по полям высокой кардинальности

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

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

Агрегация быстрее

Итак, наша цель — быстрая группировка сообщений об ошибках. Проблема в том, что сообщения об ошибках JavaScript могут иметь переменную длину, а иногда и тысячи символов. В Elasticsearch агрегирование длинных строк выполняется медленнее, чем агрегирование числовых значений. Другими словами, быстрее сгруппировать по long значениям, чем по string значениям. Было бы здорово, если бы мы могли выполнять агрегацию по числовому полю, а не по строковому полю? Но как преобразовать сообщение об ошибке в число?

Мы хешируем это!

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

Проблема была в том, что мы уже использовали этот трюк для выполнения агрегатов, но все еще наблюдали медлительность. И что еще более важно, мы наблюдали огромное увеличение объема оперативной памяти, необходимой для обработки каждого запроса. Что еще происходило?

Виновник: вложенные агрегаты

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

Представьте, что у вас есть 1 000 000 ошибок с 1 000 различных сообщений об ошибках между ними. Вы хотите сгруппировать все экземпляры ошибок по сообщению, а затем для каждой группы сообщений также найти некоторые подагрегаты (количество пользователей для этого сообщения, браузеры, URL-адреса и т. д.), а затем упорядочить их по наибольшему количеству вхождений.

Затем вы хотите вернуть только первую страницу результатов, может быть, 20–100 из них.

Если вы попросите Elasticsearch сделать это в одном запросе, вот что вы получите:

  1. Сгруппируйте все сообщения об ошибках, чтобы у вас был список из 1000 сообщений в памяти. (Пока что это не так уж плохо, так как мы агрегируем значение поля хэша)
  2. Затем для каждой группы сообщений об ошибках сделайте подгруппы. Это потребует множества проходов по различным индексам полей 1M ошибок. Это также увеличит объем памяти, необходимый для удовлетворения запроса, так как все это остается резидентным в памяти.
  3. Как только все группы и подгруппы будут рассчитаны, упорядочите их по количеству сообщений. (опять же, для сортировки нам нужен весь список из 1000 групп в памяти)
  4. Верните 20 лучших, отбросив остальные 980 агрегатов.

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

Два запроса лучше, чем один

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

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

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

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

Первоначально опубликовано на https://trackjs.com.