Представьте, что вы начинаете новое приложение. Или, в лучшем случае, уже его написали и прямо сейчас первый раз разворачиваете в продакшен.
Есть две маленькие детали, которые вы, скорее всего, упустили:
- Есть огромный разрыв между приложением, готовым к первому деплою, и приложением, готовым к работе в продакшен-режиме.
- Когда с приложением начнут работать реальные пользователи, времени закрыть этот разрыв у вас уже нет, и это будет больно.
Ниже пройдёмся по вещам, которые стоит продумать, когда вы только подходите к архитектуре нового приложения для продакшена. Если вы уже в продакшене и что-то забыли, то лучше поздно, чем никогда. Всё, о чём пойдёт речь, кажется очевидным, но по разным причинам регулярно упускается. Это не полный список — просто несколько самых болезненных пунктов. Цена пропуска обычно предсказуема: аварийные релизы, бессонные ночи и овертайм.
Поехали…
В общем случае всё это называется нефункциональными требованиями, aka NFR (или качественными атрибутами). Или коротко: какой система должна быть: «нефункциональные требования задают критерии, по которым можно оценивать работу системы, а не её конкретное поведение».
Сегодня мы ограничимся NFR, связанными со временем исполнения системы и поддерживающими её операциями. Есть и много других нефункциональных требований — про эволюцию систем, тестируемость, удобство использования, безопасность и т.д. — их разберём в следующий раз. Инструменты для поддержки конкретных NFR тоже не будем трогать: выбор зависит от слишком многих факторов.
Также я предполагаю, что система полностью покрывает функциональные требования.
Темы на сегодня:
- Performance
- Availability and reliability
- Supportability
- Monitoring
Performance
Кажется, про важность требований к производительности каждый инженер узнал ещё в детском саду. Но, видимо, забыл в первом же проекте.
Многие ли могут сказать, на сколько пользователей спланировано их приложение? Сколько операций они будут выполнять? Вы в этом уверены? Вы точно знаете, что приложение с этим справится? Есть пара аспектов производительности, которые легко измеряются:
- Response time — как быстро клиент получит ответ на запрос
- Throughput — сколько запросов успешно обрабатывается за единицу времени
Для обоих нужно знать, что ожидается от системы. Response time касается не только UI-взаимодействий, но и фоновой или асинхронной обработки — процессов, которые возвращают результат клиенту позже. Вот что сильно влияет на оба аспекта.
Первое, о чём обычно забывают, — это объём данных. Один думает, что данных будет много, и пригоняет грузовой корабль вместо тележки. Это потом аукнется стоимостью владения и поддержки. Другой вообще не задумывается. В итоге, когда приложение разогревается до реальных объёмов, в какой-то момент вы обнаруживаете, что главный дашборд грузится от одной до пяти минут, а чтобы обработать все асинхронные сообщения за сутки, системе нужно два с половиной дня непрерывной работы на 100% CPU. Если повезло — нагрузка растёт плавно, и есть время переделать. Если не повезло — спайк 10x за сутки и русская рулетка.
Вместо этой рулетки сделайте несколько простых шагов:
- Посчитайте максимальные объёмы данных по основным сущностям системы.
- Сгенерируйте нужные данные в системе (не говорите, пожалуйста, что сеньор-разработчик не умеет создавать миллион случайных записей в БД).
- Проверьте, как система работает с этими данными в основных сценариях. Количество пользователей может заметно влиять на объём данных, поэтому нужно понимать, сколько людей ожидается и сейчас, и в ближайшем будущем.
Дальше — где всё это будет работать: сеть, железо, виртуализация. Не попадайте в ситуацию, когда ваш разработческий десктоп мощнее, чем ожидаемый продакшен.
И последнее, но не по важности — для продвинутых команд: соедините данные, среду, похожую на настоящую, и ожидаемую нагрузку, запустите нагрузочные тесты и убедитесь, что response time вас устраивает. Автоматизация здесь помогает не потерять тайминги после изменений.
Не думаю, что надо отдельно писать: если система не справляется с ожидаемой нагрузкой, критично начать искать решение до того, как появятся реальные пользователи.
Availability and Reliability
Эта часть — про то, чтобы система работала, когда она нужна, и про управление рисками отказа.
Главный вопрос здесь: когда система должна быть доступна? У него две части: время и уровень. У сложных систем у разных частей/компонентов могут быть разные метрики.
Первая часть — время, или «рабочие часы», то есть когда пользователи работают с системой. Для онлайн-системы это 24/7. Для системы, которая поддерживает функции какого-то отдела, — рабочие часы этого отдела.
Вторая часть — насколько критичен отказ. Я разбиваю на три уровня:
- Critical — если компонент лежит, это серьёзно бьёт по всему бизнесу (например, покупки, которые клиент не получит). Обычно такое влияние можно напрямую перевести в потерянные деньги.
- Important — если компонент лежит X часов, это затрагивает важные бизнес-сценарии, но без прямой потери денег (например, у пользователя нет полнотекстового поиска, но основные функции работают).
- Support — нет прямого влияния на бизнес (например, внутренняя админка или система управления пользователями).
Градации эмпирические — какой-то компонент можно отнести в critical, потому что простой бьёт по репутации или ещё чему-то.
Главная цель — чтобы критичных частей было как можно меньше. Дальше — обеспечить, чтобы критичные части были защищены и доступны в рабочие часы. Для важных компонентов нужны процедуры, гарантирующие восстановление доступности за X часов (до того как это начнёт бить по бизнесу).
Дальше нужно понять, что может пойти не так и как это повлияет на компоненты (это особенно критично). Типовые вещи: отказ железа/сети, недоступность внешних систем, неожиданная нагрузка. Не забывайте: всё, что может сломаться, — сломается. Хост БД ляжет, внешний веб-сервис ответит за 30 секунд вместо 100 мс, кто-нибудь напишет клиент к вашему API, который из-за человеческой ошибки в коде делает 1000 лишних запросов.
Для критичных частей нужна избыточность (минимум два узла) с репликацией данных — защита от аппаратных сбоев. Отказ целого дата-центра — редкость, поэтому обычная практика — иметь процедуру восстановления системы в другом ДЦ за разумное время.
Ещё один часто упускаемый момент — что делать, если приходит больше запросов, чем вы способны обработать (при условии, что система уже справляется с нормальной нагрузкой). Хорошие паттерны — настроенный автоскейлинг компонента и throttling запросов от конкретного клиента. Стратегия троттлинга бывает разной, но обычно что-то вроде: «Если клиент прислал больше 1000 запросов за последние 5 минут — возвращаем ошибку вместо обработки.»
Ещё нужно быть готовым к похожему поведению от внешних систем. Здесь подходит CircuitBreaker — проактивно управлять соединениями с внешними системами, обрабатывать их простои вместо случайных падений и возвращаться в нормальный режим, когда внешняя система ожила.
В общем, будьте паранойяльны в оценке возможных проблем — но только по настоящему критичным компонентам. Не стоит вкладываться в надёжность того, что используется два раза в месяц.
Supportability
Когда система уже умеет держать ожидаемую нагрузку и отказы вне вашего кода — пора подумать, как разбираться с проблемами уже в собственном продакшен-коде и чинить их легко. Я понимаю, вы пишете идеальный код без дефектов, но иногда кто-то из команды может ошибиться.
Первый уровень помощи — хорошие логи. Нужны лог-файлы с информацией обо всех значимых операциях, взаимодействиях с внешними системами, старте и завершении фоновых операций и любом неожиданном поведении — cache miss, переполнение внутренней очереди, — всём, что показывает, что происходит в системе.
Хорошая идея — у каждой бизнес-транзакции иметь уникальный ID и логин. Это быстро позволяет отделить взаимодействия одного пользователя от другого, и один клик в UI от другого. ID пользователя и ID ключевых сущностей — тоже хорошие кандидаты для логирования.
Одна фича, которая экономит кучу времени, — audit tracking. Многие «дефекты» — это просто то, что пользователь сам сделал или поменял.
Следующий уровень — возможность воспроизвести неожиданное поведение на реальных пользовательских данных. Это нужно аккуратно сочетать с требованиями безопасности — вы же не хотите провоцировать утечку пользовательских данных, — но в сложных сценариях это очень помогает. И никогда не знаешь, когда эта функциональность понадобится.
Один вариант — экспортировать нужный набор данных из продакшена в QA-окружение и разбираться там. Придётся поддерживать экспорт/импорт нужных данных между окружениями.
Другой вариант — уметь заходить в систему от имени конкретного пользователя. Это очень помогает саппорту, когда пользователь пытается объяснить, что, как ему кажется, сломалось. Проблему можно воспроизвести без копии данных и в реальной среде. Тут важно не светить персональные данные сотрудникам саппорта и логировать все действия, выполненные через эту фичу (с ID пользователя и ID саппортера). Иначе открываете огромную дыру в безопасности для любого, у кого есть доступ.
Следующий продвинутый уровень — специализированные support-функции приложения: возможность залезть в работающую систему (дампить пользовательскую сессию), посмотреть, что лежит в кэшах, перезапустить конкретную фоновую задачу — возможно, с изменёнными параметрами.
Monitoring
Мониторинг — сквозная тема для всех предыдущих. Вы знаете, что происходит с приложением прямо сейчас? Сколько у вас пользователей? Операций в секунду? Что было root cause отказа?
Первое, что нужно — набор метрик для приложения. Каждый важный с точки зрения производительности и эксплуатации аспект должен мониториться. Нужна статистика изменения метрики во времени. Речь не только об инфраструктурных метриках, про которые все знают, но и о прикладных. Примеры: число залогиненных пользователей, количество запросов конкретного типа, время ответа на запрос, время обработки джобы. С хорошей метрик-системой можно делать бизнес-метрики вроде заказов или покупок в день.
Дальше — нужно настроить алерты на метрики, чтобы ответственные люди получали нотификации, когда что-то идёт не так. Здесь полезно трекать все исключения в системе и получать по ним уведомления (простой вариант — настроить логгер слать email в рассылку команды разработки на каждое исключение).
После настройки нужно протестировать конфигурацию: убедиться, что алерт действительно срабатывает, когда что-то ломается, что люди его получают и знают, как реагировать. И, отдельно, убедиться, что люди не получают лишних алертов, иначе они начнут их игнорировать — и в какой-то момент пропустят действительно важный. Один из худших сценариев — есть настроенный мониторинг, но никто не реагирует на алерты.
Итог
Если совсем коротко:
- Поймите, что действительно ожидается от приложения, и имейте план, как это доставить.
- Не усложняйте.
- Держитесь как можно ближе к реальной среде.
- Не доверяйте своему опыту и интуиции — запускайте тесты.
- Мониторьте и алертите каждую важную метрику, чтобы узнавать об отказе до того, как вам скажет пользователь.
И ещё одна вещь про IT: есть люди, которые не делают бэкапы, и люди, которые уже начали делать бэкапы. Если ни одна из описанных выше проблем никогда вас не задевала, значит, вы либо очень умный, либо просто удачливый. Спокойной вам ночи.