Что будет в этой статье?
Это заключительная статья в цикле о жизни разработчиков IDE для баз данных. В этой части я расскажу, о том, как внедрение продуктовой аналитики повлияло на некоторые аспекты разработки.
Для понимания этой статьи не обязательно читать все три предыдущие, но будет полезно прочитать несколько первых параграфов первой части, так как они дают представление о контексте. Впрочем, вот некоторые тезисы, если нет возможности ознакомится с первой статьей:
- Мы делаем линейку IDE для СУБД MySQL, SQL Server, Oracle, PostgreSQL
- Это настольное приложение на .NET стеке со всеми вытекающими
- Парсинг SQL это сложная задача в плане производительности и памяти. Постоянно приходится применять разные трюки для оптимизации
Ссылки на предыдущие статьи цикла:
Часть 1. Сложности парсинга. Истории о доработке ANTLR напильником
Часть 2. Оптимизация работы со строками и открытия файлов
Часть 3. Жизнь расширений для Visual Studio. Работа с IO. Необычное использование SQL
Часть 4. Работа с исключениями, влияние данных на процесс разработки. Использование ML.NET
Аналитика исключений
Задача сбора данных о пользовательской активности очень просто и круто решается в вебе. С аналитикой настольных приложений все значительно хуже. Не существует инструмента, что способен сразу дать такой невероятный набор метрик и средств визуализации как Google Analytics. Почему так вышло? Мне было бы крайне интересно прочитать ретроспективную статью о том почему так вышло. Пока выскажу несколько своих предположений:
- На протяжении большей части истории активного развития настольных приложений у них не было стабильного и постоянного доступа к Сети.
- Существует невероятное количество инструментов разработки настольных приложений. Невозможно построить универсальный инструмент сбора пользовательских данных для всех UI-фреймворков и технологий.
В итоге задачу продуктовой аналитики каждая компания для каждого продукта решала по-своему. По своему, решать ее пришлось и нам. Пару лет, с согласия пользователей, мы собираем небольшой набор данных о том какими функциями в инструментах линейки dbForge пользуются чаще.
Важным аспектом среди собираемых данных стала аналитика исключений. Мы забираем данные о крешах возникших на компьютерах пользователей. До этого пользователю приходилось писать на почту в службу поддержки самостоятельно, прикрепляя StackTrace ошибки скопированный из специального окна в приложении. Очевидно, что немногие проходили весь этот путь. Собираемые данные полностью обезличены, что, к сожалению, отнимает у нас возможность узнать шаги воспроизведения и какую-то другую информацию у пользователя. С другой стороны, данные об ошибках лежат в Postgres базе, а это открывает двери к мгновенной проверке десятков гипотез. Что общего у пользователей у которых возникла эта ошибка? Один билд ОС? Возможно одинаковые процессоры? Какими функциями пользовались все перед тем как у них возникла эта ошибка? Ответ на любой из этих вопросов можно получить мгновенно, лишь отправив соответствующий SQL запрос в базу. Часто по одному лишь стеку и типу исключений непонятно как вообще возникло такое исключение, потому вся эта информация критически важна, для исследования проблемы.
Кроме этого есть возможность проанализировать все собранные данные и найти самые проблемные модули и классы. На основе результатов анализа можно запланировать рефакторинги или дополнительное покрытие тестами этих частей программы.
Сервис расшифровки стеков
.NET сборки содержат IL код, который без труда может быть преобразован обратно в код на C#, с точностью до оператора при помощи ряда специальных программ. Один из способов защиты кода программы — его обфускация. Рынок предлагает ряд решений этой проблемы. Программы переименовывают методы, переменные и классы, заменяют сам код на эквивалентный, но крайне сложный для понимания. Если .NET используется в качестве серверного бекенда, то обычно, необходимости обфусцировать код нет. Вряд ли злоумышленник получит доступ к файловой системе сервера, а если получит, то вряд ли для извлечения исходных кодов, в таких случаях обычно воруют данные. Необходимость обфусцировать исходный код появляется, когда вы распространяете свой продукт таким образом, что пользователь получает сборки вашего приложения. Настольные приложения как раз такой случай, поэтому все билды, включая промежуточные для тестировщиков, тщательно обфусцируются. К сожалению, обратная сторона --зашифрованные стеки исключений, как на этапе тестирования, так и от пользователей. Отдел качества и поддержки были обучены пользоваться инструментами для расшифровки стеков от производителя обфускатора. Для расшифровки требовалось запустить программу, найти карты деобфускации опубликованные CI для конкретного билда и вставить стек исключения в поле ввода. Работа была неприятная и занимала по нескольку минут на один стек. Когда исключения генерировались лишь тестировщиками или приходили по почте от самых сознательных пользователей мы еще справлялись с потоком, но, когда зашифрованные исключения стали поступать еще и через сервис аналитики… мы стали захлебываться. Разные версии и редакции обфусцировались по-разному, что усложняло исследование проблемы разработчику или даже могло пустить его по ложному следу. Необходимость автоматизировать эту часть процесса стала очевидной. Формат карт деобфускации оказался не сложным. Мы без труда его распарсили и написали программу расшифровки стеков. Незадолго до этого был разработан web-ui для отображения исключений по версиям продукта и группировки их по стекам. Это был сайт на .NET Core с базой на SQLite. SQLite — великолепный инструмент для небольших решений. Карты деобфускации мы попробовали складывать туда же. Каждый билд генерировал примерно 500к пар зашифровка-расшифрока. SQLite просто не справлялся с такой агрессивной вставкой. Пока в базу вставлялись данные по одному билду, в очередь становилось еще два. Я почти уверен, что можно было прокачать SQLite для такой нагрузки, помимо упаковки в транзакции и различных режимов консистентности можно было бы попробовать как-то секционировать базу. Незадолго до возникновения этой проблемы я слушал доклад по Clickhouse, мне стало интересно попробовать его. Он показал себя отлично, вставка ускорилась более чем в 200 раз. Надо сказать, что расшифровка стеков(чтение из базы) замедлилось примерно в 50 раз, но поскольку на стек уходило менее 1мс было экономически не эффективно тратить время на исследование этой проблемы.
ML.NET для классификации исключений
Развивая тему автоматической обработки исключений было сделано еще несколько улучшений. У нас уже был Web-UI для удобного просмотра сгруппированных по стекам исключений, была grafana для high-level анализа стабильности продукта на уровне версий и продуктовых линеек, но на глаза вечно желающих что-то оптимизировать разработчиков попался другой аспект этого процесса. На команде поддержки лежала работа по просмотру ленты исключений и созданию из них задач в Jira. В результате был написан несложный сервис, что делал это автоматически. Чуть позже он был обучен проставлять и обновлять задачам приоритеты в зависимости от количества пользователей пострадавших от этой проблемы.
Исторически сложилось, что разработка продуктов линейки dbForge была разделена на 4 команды. За каждой был закреплен определенный функционал, пусть границы и не всегда четкие и очевидные. Команда технической поддержки на основе своего опыта читала стек и определяла его на ту или иную команду. Получалось это довольно неплохо, но в некоторых случаях все таки случались ошибки. Информация об ошибках из аналитики уже сама попадала в Jira, но команде поддержки все еще приходилось классифицировать задачи по командам. Microsoft, в то время, представили новую библиотеку — ML.NET, а у нас была задача классификации, параметры который мы и сами сформулировать-то не могли. То что нужно, для такого рода библиотеки. Были извлечены стеки всех исправленных исключений из Redmine, системы управления проектам, что использовалась ранее и Jira, что используется сейчас. Получился массив данных из примерно 5 тысяч пар Exception StackTrace — команда. 100 исключений было отложено для теста, а на остальных была обучена модель. Точность получилась около 75%, напоминаю, что команд было 4, а значит random или round-robin дали бы лишь 25%, это было чуть хуже чем то, что получалось у команды поддержки, но экономило им достаточно времени.
Думаю если существенно почистить входящий массив данных, тщательно изучить библиотеку ML.NET и теоретическую базу по механизмам machine learning в целом, то можно улучшить эти результаты. С другой стороны меня очень впечатлила простота этой библиотеки: без каких-либо знаний в области AI и ML менее чем за час получилось получить реальную экономическую выгоду.
Заключение
Уверен все кто дочитали до конца финальной статьи цикла явно в каждой части находили что-то интересное и/или полезное.
Надеюсь среди читателей есть и существующие пользователи продуктов речь о которых в статье и какие-то строки пролили свет на то почему тот или иной функционал был реализован именно так.
Я пишу эти строки еще до публикации даже первой статьи цикла, потому не знаю как сложилось, но уверен, что нашел для себя много полезного и интересного в комментариях.
Теперь перейду собственно к выводам.
- Лучше принимать решения на основе данных, а не догадок. Про это писал в прошлой статье на основе оптимизаций, там необходимые данные можно извлечь из профилировщика, в этой речь об аналитике и инсайтах, что можно получить из нее.
- Собрав массив данных об исключениях, можно получить данные о проблемных участках программы.
- Нужно постоянно инвестировать в инструменты. Если для этого нужно что-то разработать, то ничего страшного, за следующие несколько месяцев сэкономит кучу времени и избавит от рутины. Рутина кроме временных расходов демотивирует сама по себе.
- Разработка каких-то внутренних инструментов это отличный шанс опробовать новые технологии, что потом можно будет применить уже в продакшн решениях.
- Существует бесконечное множество инструментов для анализа данных, но всю действительно важную информацию получалось извлечь средствами SQL. Это самый мощный инструмент для формулирования вопросов к данным и получения ответа в структурированном виде.
Еще раз спасибо всем, кто прочитал и всем, кто помогал мне создавать эту статью!