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

Есть две маленькие детали, которые вы, скорее всего, упустили:

  1. Есть огромный разрыв между приложением, готовым к первому деплою, и приложением, готовым к работе в продакшен-режиме.
  2. Когда с приложением начнут работать реальные пользователи, времени закрыть этот разрыв у вас уже нет, и это будет больно.

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

Поехали…

В общем случае всё это называется нефункциональными требованиями, 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: есть люди, которые не делают бэкапы, и люди, которые уже начали делать бэкапы. Если ни одна из описанных выше проблем никогда вас не задевала, значит, вы либо очень умный, либо просто удачливый. Спокойной вам ночи.