Experiments
Любому программисту приходилось что-то менять в программном продукте. Ладно если просто новый типовой модуль добавить по образу и подобию. А иногда делаешь что-то принципиально новое и совершенно не понимаешь как оно будет работать. Вот об этом и поговорим. Если менее загадочно - про эксперименты, бенчмарки и с более-менее общими выводами, чтобы их можно было использовать применимо к маркетингу и гипотезам.
Нужен пример
Суть вносимых изменений в целом не очень важна, но нужен какой-то более-менее реалистичный пример чтобы не уйти совсем уж в абстракции. Например - хотим сделать несетевой кэш для записей одной таблицы, для которой характерны очень редкие изменения, который будет использоватьсян независимыми веб-воркерами распределённой системы.
Ну то есть это мы сейчас знаем, что кэш будет независимым, что только для одной таблицы, что это вообще будет именно кэш, а не другое решение проблемы. А в моменте проблема выглядит следующим образом:
Наш сервис при пиковых нагрузках создаёт немаленькую нагрузку на СУБД. СУБД нарастить можно, но сперва стоит немного подумать. Больше всего запросов к сервису происходит от одного и того же метода. В этом методе выполняется несколько SQL-запросов. В целом приложение сервиса асинхронное, запросы в БД асинхронные. Бутылочное горлышко не в процессоре. Проблема метода в том, что два SQL-запроса зависят друг от друга и поэтому в рамках запроса они таки выполняются синхронно, один дожидается выполнения другого.
Здесь напрашиваются два выхода:
- Добавить ещё один JOIN и объедини таким образом два запроса в один. Но в таком случае терялась возможность отделить отсутствие данных в одной таблице от отсутствия данных в другой при отправке ответов с ошибками. А эта семантика очень важна и сильно упрощает поддержку сервиса.
- Табличка маленькая, влезает в память целиком, меняется редко. Ответ напрашивается - кэшировать.
На второй ответ в ответ возникает череда вопросов: что, где, когда, чем и как кэшировать. Согласно какому-то классическому афоризму, в разработке есть две проблемы:
- Именование
- Инвалидация кэша
Вариантов на самом деле много было. По причинам не связанным с производительностью, был выбран собственный велосипед (не, ну серьёзно, что нам стоит кэш построить на голом питоне) и гнуть его под себя так. как нам самим удобно? Вышло всего-то около ста строчек.
Benchmark-first
Если задача не стоит как “сделать для галочки и пофиг что будет хуже” или “делать откровенное вредительство”, значит качество имеет значение. Так-как вопрос касается производительности, нужно сперва решить вопрос с метриками производительности и только потом приступать к её изменению.
Здесь возникает проблема определиться, что измеряем, насколько приближённо к реальности и как измеряем. Можно считать что у нас есть три среды:
Среда | Плюсы | Минусы |
---|---|---|
Машина разработчика | можно делать всё что угодно, быстро получать обратную связь | крайне далека от реальности, всю инфраструктуру облака не воспроизвести |
Сервер на тестовом слое | Близок к реальному окружению | Тем не менее профиль нагрузки сильно отличается от прода |
Сервер на продакшне | Является реальным окружением | Ломать жалко, даже временно |
На разных окружениях мы можем проводить измерения разного характера. На машине разработчика мы имеем полный доступ ко всей вспомогательной инфраструктуре, можем измерять всё что нам захочется, ситуации с тем что “это наше облако, сервис мы вам предоставляем, графики в графане предоставляем, а дальше доступ не дадим” не будет.
Изоляция эксперимента
С одной стороны кажется, что это плохо, а с другой это позволяет проводить измерения с разной степенью изоляции.
На локальной машине мы можем, используя моки и что-то вроде автотестов вызывать в больших количествах только нужную нам функцию. А это замечательно, потому что любая оптимизация начинается с профилирования. А профилировать очень удобно на локальной машине. По-другому тоже можно, но обычно это схоже с гинекологической гимнастикой, а в некоторых окружениях и вовсе не получится.
Если эксперимент потенциально разрушителен и его можно ограничить виртуалкой или контейнером - волшебно. Дешевизна проб и ошибок - это всегда хорошо.
Большое количество вызовов сгладит оверхед на запуск. Регулирование числа запусков или объёмов обрабатываемых данных
Метрики
Что собирать? Что приложение делает, то и собирать. А приложение состоит из:
- Сетевое I/O. Оно долгое. В вопросе пропускной способности и %wa в загрузке процессора асинхронные подходы спасают ситуацию, но это мало влияет на задержку между получением запроса и отдачей ответа. Обычно объёмы сетевого I/O +/- корреллируют с числом сетевых запросов, можно не заморачиваться и измерять только объём. Можно измерять число соединений. Можно число и размеры пакетов (вдруг у вас не влезало буквально 2-3 байта в jumbo-frame и теперь приложение стало обходиться меньшим числом пакетов для своей работы?). Всяко можно. Можно всё сразу, главное не запутаться потом.
- Дисковое I/O. Не такое долгое, но всё же. Если ваш сервис работает с СУБД, измерять надо дисковое I/O СУБД. Можно залезть во внутренние метрики СУБД и собирать и их в том числе.
- Потраченные такты CPU.
- Максимальный размер резидентной памяти. Здесь есть нюанс - смотреть надо снаружи, например с помощью утилиты
/usr/bin/time
. Внутри самого приложения это сделать сложнее, через всякие тамresource.getrusage()
можно получить потребление в моменте выполнения самогоresource.getrusage()
. То есть либо городить явно отдельный поток, в котором эта функция по кругу будет крутиться, либо получать неточные данные. Почему неточные? Конструкцияstart = getrusage(); func(); end = getrusage(); diff = end - start
на самом деле не покажет сколько памяти отъедает выполнениеfunc()
. Эта конструкция покажет сколько памяти потребляет ваша программа до запуска func и после запуска func с учётом работы garbage collector в процессе. Можно отключить garbage collector, но это отдалит нас от окружения продакшна ещё дальше. - Объём I/O в оперативной памяти. Число malloc’ов
- Context switching можно оставить совсем уж системным программистам.
Статистика
Окей мы насобирали сырых данных. На что смотреть?
В целом смотреть надо на распределение. Но сравнивать распределения сложно. Числа-то сравнивать сложно, люди постоянно графики всякие строят. Поэтому можно обойтись набором:
- min - max
- avg - mean
- 95 percentile