Русский
Русский
English
Статистика
Реклама

Краш-репорт

Crash-crash, baby. Автоматический мониторинг фатальных ошибок мобильных приложений

15.09.2020 12:21:41 | Автор: admin

Всем привет! Меня зовут Дмитрий, я релиз-инженер вкоманде CI/CD Speed Авито. Вот уже несколько лет мы сколлегами отвечаем за всё, что связано срелизами наших мобильных приложений и не только. Впрошлый раз я рассказывал онашей системе релизов мобильных приложений наоснове контракта. Сегодня речь пойдет отом, как мы автоматизировали сбор информации изFirebase оновых фатальных ошибках вмобильных приложениях.



Проблематика


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


Раньше, как и многие нарынке мобильных приложений, мы использовали Fabric, длякоторого vadimsmal и YourDestiny написали очень удобный клиент Fabricio. Набазе этого клиента унас была создана система мониторинга, которая заводила Jira-задачи нановые фатальные ошибки, искала ответственных поGit-Blame и сообщала обошибках вcпециальный слак-канал.


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


Система автоматического мониторинга крашей перестала работать, и нам пришлось возвращаться кпостоянному ручному мониторингу. Припервичном изучении документации выяснилось, что уFirebase нет публичного API дляполучения необходимой информации, и придётся искать обходные пути.


Получаем данные


Google Cloud Functions


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


Исследование документации Firebase привело нас кGoogle Cloud Functions или же облачным функциям. Это serverless FaaS отGoogle, который позволяет запускать ваш код воблачной инфраструктуре Google. УFirebase-Crashlytics есть встроенная интеграция соблачными функциями (намомент написания статьи данная функциональность помечена как deprecated). Вы можете написать call-back наодин изтрёх crashlytics-ивентов и дальше обрабатывать его как вашей душе угодно. Особенно нас интересуют два ивента onNew(новое событие crashlytics) и onVelocityAlert (резкий рост события crashlytics).



Вголове сразу же родилась схема. Настраиваем интеграцию Firebase-Google Cloud Functions, шлём оттуда все новые краши сразу всвой сервис, и там уже обрабатываем. Берём пример издокументации, вносим несколько доработок и получаем следующий код наJS который загружаем вGoogle Cloud:


const functions = require('firebase-functions');const rp = require('request-promise');function sendEvent(event) {    return rp({        method: 'POST',        uri: functions.config().crashlytics.crash_collector_url,        body: event,        json: true,    });}exports.NewIssueEvent = functions.crashlytics.issue().onNew(async (issue) => {    await processEvent(issue, 'NewIssueEvent')});exports.RegressedEvent = functions.crashlytics.issue().onRegressed(async (issue) => {await processEvent(issue, 'RegressedEvent')});exports.VelocityAlertEvent = functions.crashlytics.issue().onVelocityAlert(async (issue) => {await processEvent(issue, 'VelocityAlertEvent')});const processEvent = async (event, type) =>{    if (isActualEvent(event)) {        await sendEvent(event);        console.log(`Posted ${type} ${event.issueId} successfully to crash collector`);    }    else{        console.log(`It's old event or not Avito. Do nothing`);    }}const isActualEvent = (event) =>{    const {appInfo} = event;    const {appName, latestAppVersion} = appInfo;    const version = latestAppVersion &&  parseFloat(latestAppVersion.split(' ')[0]);    console.log(`Event appName: ${appName} version: ${version}`);    return appName === 'Avito' && version > 60.0}

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


Но втекущей реализации нам не хватает данных. ВFirebase-Crashlytics есть fatal события (собственно фатальные ошибки-краши) и non-fatal (остальные события которые по той или иной причине логируются в crashlytics). Все летящие кнам ивенты насобытие onNew не имеют признака фатальности, ктому же нам хотелось как-то фильтровать события поколичеству затронутых пользователей и частоте возникновения, но этой информации всобытиях нет.


BigQuery


Google позволяет экспортировать данные изFirebase вBigQuery. BigQuery облачное хранилище, предоставляющее удобную платформу дляхранения и обработки данных. На момент исследования всередине 2019года был доступен только один тип синхронизации cFirebase Batch Table.


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


  1. Синхронизация происходит раз всутки, приэтом нет гарантии, когда она будет завершена.
  2. Нельзя настроить тип экспортируемых событий экспортируется и fatal и non-fatal.
  3. Чем дольше живёт таблица, тем больше вней данных (ваш кэп) и тем дороже стоят услуги хранения.

Дорабатываем изначальную схему:



После получения ивента внашем сервисе идём вBigQuery и получаем недостающую информацию: признак фатальности, число задетых пользователей и так далее. При этом запросы кBigQuery отправляем не накаждый новый ивент, а периодически. Длянас оптимальная частота запросов раз вдень после 17:00, так как заэто время выгрузка данных изFirebase-Crashlytics вBigQuery успевала завершиться, и можно было получить информацию повсем необработанным ивентам простым запросом:


SELECT issue_id, is_fatal, COUNT(*) as crashes_counter, COUNT(DISTINCT installation_uuid) AS affected_users FROM `android.firebase_crashlytics.{table}` WHERE issue_id in ( {issues_id_string} ) GROUP BY issue_id, is_fatal LIMIT 1000

Внимательный читатель может заметить, что тут образовывается временной лаг между фактическим появлением краша и получением нами информации онём. Чтобы не пропускать редкие, но действительно важные краши, которые резко растут и задевают сразу много пользователей, унас по-прежнему оставалось событие onVelocityAlert вGoogle Cloud Function. Подокументации это событие вызывается исключительно нафатальные ошибки вработе приложения, если ошибка привела ксбою N сеансов пользователей запоследний час. Пофакту же onVelocityAlert не работало, мы зарепортили это вGoogle, нас внесли вовнутренний трекер, и наэтом всё.


Слак


Нас вполне устраивал временной лаг приполучении обычных фатальных ошибок, но совсем не хотелось пропустить пожар. Впоисках возможности получения VelocityAlert мы пришли кинтеграции Firebase-Crashlytics и слака. Вконсоли Firebase вы можете достаточно просто и быстро настроить отправку событий вопределённый слак-канал, втом числе событий VelocityAlert. Это было почти то, что мы искали. Интеграция позволяла оперативно получать информацию отом, что всё плохо, но приэтом нельзя было фильтровать события поверсии. Витоге канал больше напоминал свалку.


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


Новая схема выглядела так:



Обрабатываем данные


Систочником данных вроде определились. Теперь нужно эти данные обрабатывать.


Напомню, что наша старая система наFabric делала сданными окрашах:


  1. Искала ответственного поGit-Blame.
  2. Создавала задачу наисправление.
  3. Оповещала оновом событии в специальный слак-канал.

Первое отчего мы решили отказаться это автоматическое создание задачи и поиск ответственного поGit-Blame. Поопыту, автоматически созданные задачи отправлялись накладбище Jira, и кним редко кто возвращался, а поиск поGit-Blame иногда давал сбой, что ещё больше повышало шансы забыть задачу. А вот оповещения вслак мы решили развивать, этот канал коммуникации показал себя наиболее эффективным.


Обработку решили реализовать набазе сервиса мобильных релизов Nupokati. Он собирает информацию поновым крашам, раз вдень покрону запрашивает дополнительные данные изBigQuery, фильтрует краши пофатальности и частоте возникновения нас не интересуют единичные сбои и отправляет daily report вслак поактуальной версии приложения.



Пример daily report


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


Помимо daily report мы отлавливаем VelocityAlert дляактуальной версии и тут же репортим опожаре вслак-канал и ответственному законкретный релиз инженеру. Втреде определяется, насколько взрыв фатален, и что сним делать.



Google Cloud Functions всё


Около года мы успешно эксплуатировали новую систему автоматического сбора и алертинга фатальных ошибок вмобильных приложениях. Уже практически забыли, как заходить вFirebase и смотреть краши. Как вдруг было объявлено, что интеграция Firebase-crashlytics и Google Cloud Functions deprecated и её работа будет приостановлена 1октября 2020года. Нужно было оперативно дорабатывать решение и отказываться отоблачных функций. Приэтом хотелось обойтись минимальными изменениями вработающей системе.



Так мы просто убрали Cloud Functions и доработали запрос наполучения данных изBigQuery. Вся остальная система осталась прежней: daily report, velocityAlerts, фильтры поколичеству задетых пользователей и слак-каналы. Новый запрос получает сразу все уникальные краши понужной версии и отправляет их впоток обработки.


SELECT issue_id, issue_title, is_fatal, COUNT(issue_id) as crashes_counter, ARRAY_AGG (distinct application.display_version) AS versions, COUNT(DISTINCT installation_uuid) AS affected_users FROM `android.firebase_crashlytics.{table}`WHERE is_fatal=true GROUP BY issue_title, issue_id, is_fatal HAVING ARRAY_LENGTH(versions)=1 AND "{version}" in UNNEST(versions)ORDER BY crashes_counter DESC

Итоги


Система автоматической сборки крашей позволила нам снизить ручной труд и человеческий фактор. Больше не нужно заходить вконсоль Firebase и следить закрашами. Мы смогли построить процесс поисправлению фатальных ошибок набазе предоставляемых ей данных и улучшить качество наших мобильных приложений. Приэтом втекущей реализации сполучением данных изBigQuery напрямую мы можем легко расширять и дорабатывать её, если понадобится.


Несколько советов тем, кто захочет повторить наш путь:


  • Использование BigQuery платное, но есть песочница, вкоторой можно поэкспериментировать.
  • Оптимизируйте запросы кBigQuery. Процессинг данных не бесплатный, он впрямом смысле имеет денежное выражение согласно тарифам.
  • Дляоптимизации затрат нахранение данных вBigQuery уменьшайте время жизни таблиц, это есть внастройках. Длянас оптимальным отказался период жизни таблицы впять дней.
  • Уже после создания нашей системы появился BigQuery streaming. Нанём можно собрать аналогичную систему или даже лучше.
  • Внимательней читайте документацию кGoogle Cloud Platform. Это очень мощная платформа смножеством инструментов и возможностей.
Подробнее..

Ловим баги на клиенте как мы написали свою систему для сбора клиентских ошибок

08.10.2020 16:23:54 | Автор: admin

У нас в Badoo довольно много клиентских приложений. Помимо основных продуктов Badoo и Bumble, у которых есть как веб-версии (десктопная и мобильная), так и клиенты под нативные платформы (Android и iOS), ещё есть с десяток внутренних инструментов со своими UI. Для сбора клиентских ошибок мы используем собственную разработку под кодовым названием Gelatо. Последние два года я работал над её серверной частью и за это время открыл для себя много нового из мира разработки Error Tracking систем.

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


Что вас ждёт:

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

  • дам краткий обзор нашей системы, её архитектуры и технологического стека;

  • перечислю особенности, которые нужно учитывать, если вы решите разрабатывать свою систему сбора клиентских ошибок.

Как мы используем информацию об ошибках

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

Второе проводим анализ ошибок. Мы в Badoo релизимся довольно часто:

веб-приложения: один-два раза в день, включая серверную часть;

нативные приложения: раз в неделю (хотя многое зависит от того, как быстро билд примут в App Store и Google Play).

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

Третье. Информация об ошибках, собранная в одном месте, значительно упрощает работу разработчиков и QA-инженеров.

Что использовали раньше

Исторически для сбора клиентских ошибок в Badoo использовались две системы: HockeyApp для сбора краш-репортов из нативных приложений и самописная система для сбора JS-ошибок.

HockeyApp

HockeyApp полностью удовлетворяла наши потребности, до тех пор пока в 2014 году её не приобрела Microsoft и не начала менять политику использования, чтобы подтолкнуть людей к переходу на свою систему App Center. App Center на тот момент нашим требованиям не соответствовала: она находилась на стадии активной разработки, и часть необходимой нам функциональности отсутствовала, в частности деобфускация стек-трейсов Android-приложений с использованием DexGuard mapping-файлов, без которой невозможна группировка ошибок. О деобфускации я расскажу ниже; если вы слышите об этом впервые, значит, точно узнаете что-то новое.

Microsoft установила дедлайн 16 октября 2019 года, к этому дню все пользователи HockeyApp должны были мигрировать в App Center. К слову, поддержка DexGuard появилась в App Center лишь в конце декабря 2019 года, спустя несколько месяцев после официального закрытия HockeyApp.

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

Наша система для сбора JS-ошибок

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

Архитектура у неё была довольно простой:

  • данные хранились в MySQL (мы хранили информацию о последних 1020 релизах);

  • для каждой версии приложения была отдельная таблица;

  • работал поиск по фиксированному набору полей средствами MySQL, плюс мы использовали Sphinx для полнотекстового поиска.

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

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

Поэтому мы решили постепенно переходить на новую систему, которая бы покрывала весь существующий функционал и удовлетворяла всем нашим актуальным требованиям.

Требования к новой системе

  • Хранение всех клиентских ошибок в одном месте.

  • Хранение событий минимум за полгода без семплирования. При исследовании фантомного бага (бага, у которого нет чёткого сценария воспроизведения) возможность посмотреть ошибки для конкретного пользователя в определённый момент времени может значительно облегчить жизнь инженера. Поэтому чем больше информации мы можем хранить, тем лучше.

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

  • Отказоустойчивость чтобы минимизировать риск потери данных.

  • Способность давать ответ не только на вопрос Сколько произошло ошибок?, но и на вопрос Сколько пользователей это затронуло?.

  • Группировка похожих ошибок с сохранением мета-информации (время первой и последней ошибки в группе, в каком релизе появилась ошибка, статус ошибки и т. п.).

  • Гибкий поиск по любой комбинации полей с поддержкой полнотекстового поиска.

  • Разнообразная статистика: количество ошибок за период, количество ошибок в разбивке по релизам, по браузерам, по операционным системам и т. п.

  • Интеграция с Jira. Наша разработка ведётся с использованием Jira, поэтому нам необходима возможность создавать тикеты для определённых ошибок.

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

Почему мы не выбрали готовое решение

Конечно, прежде чем писать своё решение, мы проанализировали существующие на рынке системы.

Платные сервисы

Сбором клиентских ошибок занимаются многие SaaS-решения, и это неудивительно: быстрое обнаружение и исправление ошибок один из ключевых аспектов современной разработки. Среди самых популярных решений можно выделить Bugsnag, TrackJS, Raygun, Rollbar и Airbrake. Все они обладают богатым функционалом и в целом соответствуют нашим требованиям, но мы не рассматривали облачные решения: не было уверенности в том, что ценовая политика и политика использования со временем не изменятся, как это случилось с HockeyApp. А миграция на новое решение довольно сложная и длительная процедура.

Open-source-решения

С open-source-системами всё было не так радужно. Большинство из них либо перестали развиваться, либо так и не вышли из стадии разработки и были непригодны для использования в продакшене.

Фактически лишь Sentry продолжал активно развиваться и обладал функциональностью, покрывающей большую часть наших потребностей. Но на тот момент (начало 2018 года) восьмая версия сервиса не подошла нам по следующим причинам:

  • вместо хранения всех событий в ней использовалось семплирование;

  • в качестве базы данных использовалась PostgreSQL, с которой у нас не было опыта работы;

  • были сложности с добавлением новой функциональности, так как система написана на Python, а наши основные языки PHP и Go;

  • отсутствие части необходимой нам функциональности (например, интеграции с Jira).

В июле 2018 года вышла девятая версия Sentry, в которой появилась интеграция с issue-трекерами и был заложен фундамент для одного из ключевых, на мой взгляд, улучшений перехода на ClickHouse для хранения событий (рекомендую к прочтению серию статей об этом). Но к сожалению, на момент нашего ресёрча всего этого ещё не было даже в планах. Поэтому мы решили, что оптимальным вариантом в нашем случае будет реализация собственной системы, заточенной под наши процессы, которую легко интегрировать с другими внутренними продуктами.

Так на свет появилась система под кодовым названием Gelato (General Error Logs And The Others), о разработке которой и пойдёт речь дальше.

Краткий обзор системы

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

На главной странице находятся список приложений и общая статистика ошибок по заданному критерию.

Кликнув на то или иное приложение, мы попадём на страницу со статистикой по его релизам.

Кликнув на версию, мы попадём на страницу со списком ошибок.

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

Так эта страница выглядит для нативных приложений:

Кликнув на ошибку, мы попадём на страницу с детальной информацией о ней.

Здесь доступны общая информация об ошибке (1), график общего количества ошибок (2) и различная аналитика (3).

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

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

Выбираем версии для сравнения:

И попадаем на страницу со списком ошибок, которые есть в одной версии и которых нет в другой:

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

  • интеграция с нашим A/B-фреймворком для отслеживания ошибок, которые появились в том или ином сплит-тесте;

  • система уведомлений о новых ошибках;

  • расширенная аналитика (больше графиков и диаграмм);

  • email-дайджесты со статистикой по приложениям.

Общая схема работы

Теперь поговорим о том, как всё устроено под капотом. Схема довольно стандартная и состоит из трёх этапов:

  1. Сбор данных.

  2. Обработка данных.

  3. Хранение данных.

Схематически это можно изобразить следующим образом:

Давайте для начала разберёмся со сбором данных.

Сбор данных

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

Что входит в задачи API:

  • прочитать данные;

  • проверить данные на соответствие требуемому формату, чтобы сразу отсечь шум (чаще всего это запросы от мамкиных хакеров, которые натравили сканеры на наше API);

  • сохранить всё в промежуточную очередь.

Для чего нужна промежуточная очередь?

Если полагаться на то, что у нас достаточно низкий EPS (errors per second), и на то, что все части нашей системы будут работать стабильно всё время, то можно значительно упростить систему и сделать весь процесс синхронным.

Но мы-то с вами знаем, что в реальном мире так не бывает и на каждом из этапов в самый неподходящий момент может произойти нечто непредвиденное. И к этому наша система должна быть готова. Так, ошибка в одной из внешних зависимостей приложения приведёт к тому, что оно начнёт крашиться, что повлечёт за собой рост EPS (как это было с iOS Facebook SDK 10 июля 2020 года). Как следствие, значительно увеличится нагрузка на всю систему, а вместе с этим и время обработки одного запроса.

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

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

Что можно использовать в качестве очереди?

  1. Первое, что приходит в голову, популярные брокеры сообщений, например Redis или RabbitMQ.

  2. Также можно использовать Apache Kafka, которая хорошо подходит для случаев, когда требуется хранить хвост входящих данных за определённый период (например, для какой-то внутренней аналитики). Kafka используется в частности в последней (десятой) версии Sentry.

  3. Мы остановились на LSD (Live Streaming Daemon). По сути, это очередь на файлах. Система используется в Badoo довольно давно и хорошо себя зарекомендовала, плюс у нас в коде уже есть вся необходимая обвязка для работы с ней.

Хранение данных

Тут нужно ответить на два вопроса: Где хранить? (база данных) и Как хранить? (модель данных).

База данных

При реализации прототипа системы мы остановились на двух претендентах: Elasticsearch и ClickHouse.

Elasticsearch

Из очевидных плюсов данной системы можно выделить следующие:

  • горизонтальное масштабирование и репликация из коробки;

  • большой набор агрегаций, что удобно при реализации аналитической части нашей системы;

  • полнотекстовый поиск;

  • поддержка UPDATE по условию (процесс обработки данных у нас асинхронный, и нужна возможность выполнять некоторые шаги пайплайна повторно, а значит, и возможность обновлять определённые поля у конкретных событий);

  • поддержка DELETE по условию (мы храним данные за полгода, а значит, нам нужна возможность удалять устаревшие данные);

  • гибкая настройка через API, что позволяет разработчикам менять настройки индексов в зависимости от задач.

Конечно, как у любой системы, у Elasticsearch есть и недостатки:

  • сложный язык запросов, поэтому документация всегда должна быть под рукой (в последних версиях появилась поддержка синтаксиса SQL, но это доступно только в платной версии (X-Pack) либо при использовании Open Distro от Amazon);

  • JVM, которую нужно уметь готовить, в то время как наши основные языки это PHP и Go (например, для оптимизации сборщика мусора под определённый профиль нагрузки требуется глубокое понимание того, как всё работает под капотом; мы столкнулись с этой проблемой при обновлении с версии 6.8 до 7.5, благо тема не нова и есть довольно много статей в интернете (например, тут и тут));

  • плохое сжатие строк; мы планируем хранить довольно много данных и, хотя железо с каждым годом дешевеет, хотим использовать ресурсы максимально эффективно (конечно, можно использовать deflate-сжатие вместо LZ4, но это увеличит потребление CPU, что может негативно сказаться на производительности всего кластера).

ClickHouse

Плюсы данной базы:

  • отличная производительность на запись;

  • хорошее сжатие данных, особенно длинных строк;

  • MySQL-совместимый синтаксис запросов, что избавляет от необходимости учить новый язык запросов, как в случае с Elasticsearch;

  • горизонтальное масштабирование и репликация из коробки, хоть это и требует больше усилий по сравнению с Elasticsearch.

Но на начало 2018 года в ClickHouse отсутствовала часть необходимых нам функций:

  • Поддержка DELETE по условию (мы планировали хранить данные за полгода, поэтому нам была необходима возможность удаления устаревших данных; в ClickHouse удаление данных по произвольному критерию предусмотрено не было, а партиционирование по произвольному полю (в нашем случае по дате) на тот момент находилось на стадии экспериментальной фичи и не рекомендовалось к использованию в продакшене);

  • поддержка UPDATE по условию: ClickHouse заточена под неизменяемые данные, поэтому реализация обновления произвольных записей задача не из лёгких (этот вопрос не раз поднимался на GitHub и в конце 2018 года функцию всё же реализовали, но она не подходит для частых обновлений);

  • полнотекстовый поиск (была опция поиска по RegExp, но он требует сканирования всей таблицы (full scan), а это довольно медленная операция).

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

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

Модель данных

Теперь немного поговорим о том, как мы храним события.

Все данные у нас разбиты на несколько групп и хранятся в отдельных индексах:

  • мета-информация;

  • сырые события.

Данные изолированы в рамках конкретного приложения (отдельный индекс) это позволяет кастомизировать настройки индекса в зависимости от нагрузки. Например, для непопулярных приложений можно хранить данные на warm-нодах в кластере (мы используем hot-warm-cold-архитектуру).

Для того чтобы в одной системе можно было хранить и JS-ошибки, и краш-репорты нативных приложений, мы вынесли на верхний уровень всё, что используется для формирования общей статистики (время ошибки, в каком релизе она произошла, информация о пользователе, ключ группировки). А то, что уникально для каждого типа ошибки, хранится во вложенном поле attributes со своим маппингом.

Сама идея была позаимствована из Sentry и незначительно доработана в процессе эксплуатации. В Sentry у события есть базовые поля, есть поле tags для данных, по которым нужна возможность поиска, и поле extra для всех остальных специфических данных.

Обработка данных

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

Начнём со случая попроще.

Обработка краш-репортов из Android-приложений

Чтобы максимально уменьшить размер приложения, в Android-мире принято при сборке билда использовать специальные утилиты, которые:

  • удаляют весь неиспользуемый код (code shrinking сокращение);

  • оптимизируют всё, что осталось после первого этапа (optimization оптимизация);

  • переименовывают классы, методы и поля в соответствии со специальным форматом, что позволяет уменьшить размер кодовой базы, а также усложнить процесс реверс-инжиниринга приложения (obfuscation обфускация).

Подробнее про это можно узнать из официальной документации.

Сегодня есть несколько популярных утилит:

  • ProGuard (бесплатная версия);

  • DexGuard на базе ProGuard (платная версия с расширенным функционалом);

  • R8 от Google.

Если приложение собрано с режимом обфускации, то стек-трейс будет выглядеть примерно так:

o.imc: Error loading resources: Security check requiredat o.mef.b(:77)at o.mef.e(:23)at o.mef$a.d(:61)at o.mef$a.invoke(:23)at o.jij$c.a(:42)at o.jij$c.apply(Unknown Source:0)at o.wgv$c.a_(:81)at o.whb$e.a_(:64)at o.wgs$b$a.a_(:111)at o.wgy$b.run(:81)at o.vxu$e.run(:109)at android.os.Handler.handleCallback(Handler.java:790)at android.os.Handler.dispatchMessage(Handler.java:99)at android.os.Looper.loop(Looper.java:164)at android.app.ActivityThread.main(ActivityThread.java:6626)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:811)

Из него мало что можно понять, за исключением сообщения об ошибке. Чтобы извлечь полезную информацию из такого стек-трейса, его нужно расшифровать. Процесс дешифрования обфусцированных классов и методов называется деобфускацией, для этого необходим специальный файл mapping.txt, который генерируется в момент сборки билда. Вот фрагмент такого файла:

AllGoalsDialogFragment -> o.a:    java.util.LinkedHashMap goals -> c    kotlin.jvm.functions.Function1 onGoalSelected -> e    java.lang.String selectedId -> d    AllGoalsDialogFragment$Companion Companion -> a    54:73:android.view.View onCreateView(android.view.LayoutInflater,android.view.ViewGroup,android.os.Bundle) -> onCreateView    76:76:int getTheme() -> getTheme    79:85:android.app.Dialog onCreateDialog(android.os.Bundle) -> onCreateDialog    93:97:void onDestroyView() -> onDestroyView

Следовательно, нам нужен сервис, которому мы могли бы скормить обфусцированный стек трейс и mapping-файл и на выходе получить оригинальный стек-трейс.

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

На её базе наши Android-разработчики написали простенький сервис на Kotlin, который:

  • принимает на вход стек-трейс и версию приложения;

  • скачивает необходимый mapping-файл из Ceph (маппинги заливаются автоматически при сборке релиза в TeamCity);

  • деобфусцирует стек-трейс.

Обработка краш-репортов из iOS-приложений

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

Thread 0:0   libsystem_kernel.dylib              0x00000001bf3468b8 0x1bf321000 + 1537841   libobjc.A.dylib                     0x00000001bf289de0 0x1bf270000 + 1059522   Badoo                               0x0000000105c9c6f4 0x1047ec000 + 216941963   Badoo                               0x000000010657660c 0x1047ec000 + 309755004   Badoo                               0x0000000106524e04 0x1047ec000 + 306416685   Badoo                               0x000000010652b0f8 0x1047ec000 + 306670006   Badoo                               0x0000000105dce27c 0x1047ec000 + 229464287   Badoo                               0x0000000105dce3b4 0x1047ec000 + 229467408   Badoo                               0x0000000104d41340 0x1047ec000 + 5591872

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

Что можно использовать для символикации?

  • Можно написать свой сервис на базе консольных утилит, но, скорее всего, он подойдёт только для ручной символикации и будет доступен только на macOS. У нас же, например, всё работает на Linux.

  • Можно взять сервис Symbolicator от Sentry, который недавно был выложен в открытый доступ (рекомендую почитать статью о том, как он разрабатывался). Мы какое-то время экспериментировали с ним и пришли к выводу, что as is этот сервис будет сложно интегрировать в нашу схему: пришлось бы допиливать его под наши нужды, а опыта использования Rust у нас нет.

  • Можно написать свой сервис на базе библиотеки Symbolic от Sentry, которая, хоть и написана на Rust, но предоставляет C-ABI её можно использовать в языке с поддержкой FFI.

Мы остановили свой выбор на последнем варианте и написали сервис на Golang с учётом всех наших нюансов, который под капотом обращается к Symbolic через cgo.

Группировка ошибок

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

Человек, не знакомый с тем, как устроены системы обработки ошибок, может подумать, что в них используются какие-то сложные алгоритмы для определения схожести строк. Но в действительности все популярные системы используют группировку по ключу (fingerprint), потому что это просто в реализации и покрывает большинство кейсов. В самом элементарном случае это может быть хеш от текста ошибки и стек-трейса. Но это подходит не для всех типов ошибок, поэтому некоторые системы позволяют явно указывать, какие ключи вы хотите использовать для подсчёта ключа группировки (либо же вы можете передать ключ явно).

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

  • JS-ошибки группируются по полям message, type и origin;

  • Android краш-репорты группируются по первым трём строчкам стек-трейсов (плюс немного магии);

  • iOS краш-репорты группируются по первому несистемному фрейму из упавшего треда (тред, который отмечен как crashed в краш-репорте).

В качестве заключения

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

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

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

Если резюмировать, то:

  • храните как можно больше данных: это может сэкономить вам драгоценные часы и нервы, которые в противном случае придётся потратить на дебаг;

  • будьте готовы к резкому росту количества ошибок;

  • чем больше аналитики вы построите, тем лучше;

  • символикация iOS краш-репортов это сложно, присмотритесь к Symbolicator;

  • предусмотрите систему уведомления о новых ошибках: это даст вам больше контроля и возможность своевременно реагировать на резкий рост ошибок;

  • запаситесь терпением и приготовьтесь к увлекательному путешествию в мир разработки систем сбора ошибок.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru