Проектирование под нефункциональные требования
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Проектирование под нефункциональные требования
Проектирование — это процесс поиска технических решений и создания подробного плана реализации системы. Результатом являются схемы, чертежи, спецификации и прототипы, по которым программисты пишут код.
Архитектура — это верхнеуровневый каркас системы, определяющий ее ключевые компоненты, их глобальные связи и жесткие правила взаимодействия. Изменить архитектуру в процессе разработки крайне дорого и сложно, поэтому её закладывают в самом начале.
Нефункциональные требования — это ограничения и стандарты качества, которые описывают условия работы системы, а не ее конкретные функции. Если функциональное требование — это кнопка «Оплатить», то нефункциональное — обязательство провести эту оплату быстрее чем за 2 секунды и в полной безопасности.
Архитектура начинается с вопроса: какие свойства системы нельзя нарушить при нагрузке, бюджете и сроках:
- Сначала формулируются нефункциональные требования (бизнес-цели и технические рамки качества).
- На их основе выстраивается архитектура системы — выбираются те технологии и паттерны, которые способны физически выдержать заданные ограничения (например, микросервисы для высокой масштабируемости).
- Внутри выбранной архитектуры начинается детальное проектирование конкретных модулей и функций, которые затем уходят непосредственно в разработку.
1. Что такое нефункциональные требования
Нефункциональные требования (Non-Functional Requirements / NFR) — это требования к свойствам и качествам системы, которые определяют, как именно она должна работать, а не что она должна делать.
Функциональные требования отвечают на вопрос "что система делает?" ("Пользователь может оформить заказ").
Нефункциональные — на вопрос "как хорошо она это делает?" ("Заказ оформляется за ≤ 2 секунды при 10 000 одновременных пользователей").
NFR часто формулируются расплывчато:
- "система должна быть надёжной",
- "интерфейс должен быть быстрым",
- "данные должны быть защищены".
Для проектирования такие формулировки бесполезны. Требуется количественная и измеримая спецификация:
| Категория | Плохая формулировка | Хорошая формулировка |
|---|---|---|
| Производительность | "Быстро" | "P95 latency для API /orders ≤ 300 мс при нагрузке 500 RPS" |
| Масштабируемость | "Масштабируется" | "Поддержка горизонтального масштабирования без downtime; добавление узла даёт ≥ 80% линейного роста throughput" |
| Отказоустойчивость | "Надёжная" | "Система сохраняет работоспособность при отказе одного узла в кластере; RTO ≤ 5 мин, RPO = 0" |
| Безопасность | "Защищена" | "Все внешние API требуют аутентификации по OAuth 2.0; чувствительные данные шифруются AES-256 at rest и TLS 1.3 in transit" |
| Сопровождаемость | "Легко поддерживать" | "Время локализации ошибки ≤ 15 мин по correlation ID; coverage unit-тестов ≥ 80%" |
Только такая формулировка позволяет сделать выбор: использовать кэширование или оптимизировать запрос? Внедрять репликацию или резервное копирование? Шифровать на уровне приложения или СУБД?
RPO задаёт, сколько данных допустимо потерять (частота бэкапов и архив журналов СУБД); RTO — за сколько сервис должен снова работать после аварии. Разбор для БД — восстановление после сбоя, практика DBA — администрирование РСУБД.
Если проигнорировать нефункциональные требования, программа может работать корректно на компьютере разработчика, но полностью отказать в реальной жизни: сайт «упадет» во время распродажи, данные пользователей украдут хакеры, а мобильное приложение будет разряжать батарею смартфона за полчаса.
Архитектурно значимые требования
Архитектурно значимые требования (Architecturally Significant Requirements / ASR) — это подмножество всех требований к системе, которое оказывает определяющее влияние на ее архитектуру и ключевые инженерные решения.
Если обычные требования можно реализовать на этапе детального проектирования или кодинга, то ASR требуют закладки фундамента. Изменение архитектурных решений, принятых на основе ASR, на поздних этапах разработки обходится дороже всего.
- Обычное требование: «Система должна отправлять пользователю email со ссылкой для сброса пароля». (Решается на уровне кода одной функцией).
- Архитектурно значимое требование (ASR): «Система должна бесперебойно отправлять до 500 000 уведомлений в минуту в периоды пиковых нагрузок». (Требует внедрения брокеров сообщений вроде Kafka/RabbitMQ и горизонтального масштабирования сервисов).
На практике держите в фокусе 5–7 атрибутов качества на проект — производительность, доступность (RTO/RPO), безопасность, сопровождаемость, интеграции, при необходимости — стоимость владения.
"Высокая доступность" без цифр ведёт к переплате за 99.99%. Согласуйте цену простоя с бизнесом — см. SLA (договор) и уровни доступности.
Архитектурно значимые требования формируются из четырех основных источников:
- Ключевые нефункциональные требования (NFR): ограничения по нагрузке, доступности (например, 99.99%), безопасности (соответствие PCI-DSS) и времени отклика.
- Главные функциональные требования (Core Business Features): базовые бизнес-процессы, ради которых создается система (например, механика транзакций в блокчейн-платформе).
- Технические ограничения (Constraints): внешние рамки, которые нельзя изменить (бюджет, стек технологий заказчика, интеграция со старой legacy-системой).
- Бизнес-цели (Business Goals): долгосрочные планы компании, например, «выход на международный рынок через год» (требует архитектурной поддержки мультиязычности, таймзон и разных валют).
2. Масштабируемость
Масштабируемость — это способность системы сохранять характеристики при увеличении нагрузки.
Она делится на два типа:
- Вертикальная (scale-up) — увеличение мощности одного узла (CPU, RAM, SSD).
- Горизонтальная (scale-out) — добавление новых узлов в кластер.
Проектирование под горизонтальную масштабируемость требует соблюдения нескольких принципов.
2.1. Отсутствие shared state
Отсутствие разделяемого состояния (No Shared State) — это фундаментальный архитектурный принцип, при котором отдельные компоненты системы (процессы, потоки, микросервисы или серверы) не имеют доступа к общей памяти или общим изменяемым данным.
Вместо совместного использования одних и тех же переменных или таблиц БД, компоненты взаимодействуют друг с другом исключительно путем передачи сообщений (Message Passing). Это ключевое условие для достижения высокой масштабируемости и отказоустойчивости.
Если два экземпляра сервиса обращаются к одной общей переменной в памяти — масштабирование невозможно.
Решение:
- Вынос состояния во внешнее хранилище (Redis, PostgreSQL),
- Использование stateless-сервисов (все данные — в запросе или в БД),
- Шардирование сессий (sticky sessions — антипаттерн, но допустим при переходе).
2.2. Идемпотентность и безопасность операций
Идемпотентность и безопасность (safety) — это фундаментальные свойства HTTP-методов и API-интерфейсов, которые определяют, как повторные запросы влияют на состояние системы и данные.
Безопасный метод — это операция, которая не изменяет состояние системы (является операцией «только для чтения»). Сколько бы раз вы ни вызвали безопасный метод, данные на сервере останутся нетронутыми. Безопасные методы могут изменять логи на сервере или счетчик просмотров страницы, но они не должны менять состояние бизнес-сущностей.
Например, вы открыли страницу товара в интернет-магазине. Вы можете обновлять её 100 раз — количество товара на складе не уменьшится, баланс вашей карты не изменится.
Идемпотентный метод — это операция, повторный вызов которой приводит к тому же результату, что и первый вызов, и не изменяет состояние системы дальше первого раза.
Первый запрос меняет систему. Второй и последующие запросы с теми же данными видят, что действие уже совершено, и ничего не меняют. Например, кнопка «Включить свет». Если свет выключен, первое нажатие его включит. Повторные нажатия оставят его включенным. Результат всегда один — свет горит.
Все безопасные методы автоматически являются идемпотентными. Но не все идемпотентные методы безопасны (например, DELETE меняет состояние системы, удаляя объект, но повторное удаление того же объекта систему уже не изменит).
Как обсуждалось ранее, идемпотентность (PUT, DELETE) позволяет безопасно повторять запросы при сбоях сети — что неизбежно в распределённой среде.
Безопасные операции (GET) можно кэшировать на любом уровне (CDN, reverse proxy, gateway).
2.3. Асинхронность и декомпозиция
Асинхронность и декомпозиция — это два мощных инструмента для борьбы со сложностью и высокими нагрузками в ИТ-системах. Они позволяют разбить монолитную задачу на независимые части и выполнять их параллельно, не блокируя работу всей системы.
Декомпозиция — это процесс разделения сложной системы, процесса или задачи на более мелкие, независимые и управляемые части (модули, сервисы, функции).
Асинхронность — это способ организации работы, при котором инициатор задачи отправляет запрос и сразу продолжает заниматься своими делами, не дожидаясь завершения этой задачи. Результат обработки приходит позже (через коллбэк, событие или опрос готовности).
Синхронные цепочки вызовов (A → B → C) создают "точки затора". При росте нагрузки задержка умножается.
Решение:
- Замена синхронных вызовов на события (event-driven architecture),
- Вынос долгих операций в фон (job queues — RabbitMQ, Kafka, SQS),
- Использование CQRS: запись — синхронно, чтение — из материализованных витрин.
Пример: оформление заказа.
- Синхронный вариант: UI → OrderService → InventoryService → PaymentService → EmailService
- Асинхронный: UI → OrderService (создаёт заказ, публикует
OrderCreated) → фоновые обработчики
Первый — проще для отладки, но хрупок. Второй — сложнее, но масштабируется линейно. Сравнение — Оценка архитектурных альтернатив.
Сама по себе декомпозиция просто делит систему на части. Но если эти части общаются друг с другом синхронно (например, через последовательные HTTP/REST-запросы), система остается хрупкой. Если один сервис зависнет, по «цепочке» зависнут и все остальные. Асинхронная декомпозиция решает эту проблему. Мы не просто делим систему на модули, мы делаем их взаимодействие независимым по времени с помощью брокеров сообщений (Kafka, RabbitMQ).
2.4. Локальность данных (data locality)
Локальность данных (Data Locality) — это архитектурный принцип, при котором вычислительные ресурсы (процессор, код, сервисы) размещаются как можно ближе к физическому месту хранения данных, которые они обрабатывают.
Главное правило локальности данных: «Перемещай код к данным, а не данные к коду» (Move code to data, not data to code). В классических трехзвенных архитектурах сервер приложения постоянно запрашивает данные из удаленной базы данных. При росте нагрузок и объемов данных (Big Data / Highload) этот подход упирается в физические ограничения - пропускная способность сети, задержка, энергопотребление и стоимость.
Операции над данными должны происходить там, где они находятся.
- Если данные шардированы по
user_id, логика, работающая с пользователем, должна выполняться на том же узле. - В распределённых БД (CockroachDB, Yugabyte) это достигается через коллокацию (placement rules).
- В микросервисах — через владение данными (database per service).
Нарушение локальности → сетевые вызовы → рост latency и снижение throughput. Но обеспечить локальность данных для пользователей из разных стран требует сложной и дорогой синхронизации баз данных между регионами.
3. Отказоустойчивость
Отказоустойчивость (Fault Tolerance) — это способность ИТ-системы продолжать корректную работу (возможно, с частичным снижением производительности или функциональности) при возникновении сбоев в одном или нескольких ее компонентах.
Отказоустойчивость — управление последствиями. Проектирование начинается с предположения: всё, что может сломаться — сломается. В коде сервиса — ошибка vs исключение; между сервисами — раздел ниже и инженерия устойчивости.
Высокая доступность (HA) минимизирует время простоя. Если сервер упал, система переключается на резервный. В этот момент пользователь может заметить секундное зависание или ошибку, но сайт быстро вернется к жизни.
Отказоустойчивость (FT) гарантирует непрерывность работы без видимых прерываний для пользователя и без потери данных. Это жесткое требование для критически важных систем (авиация, медицинское оборудование, банковский процессинг).
3.1. Принципы устойчивости
Принципы устойчивости (Resilience Principles) определяют способность ИТ-системы не просто пассивно сопротивляться сбоям, а активно адаптироваться к ним, выдерживать непредвиденное давление и восстанавливать работоспособность без катастрофических последствий.
Если отказоустойчивость (Fault Tolerance) нацелена на аппаратную избыточность, то устойчивость (Resilience) — это более широкая, «умная» стратегия выживания софта в динамичной среде. Для создания устойчивой архитектуры инженеры опираются на следующие руководящие принципы:
| Принцип | Описание | Реализация в коде/архитектуре |
|---|---|---|
| Изоляция (Bulkheads) | Не дать сбою в одном компоненте повлиять на другие | Разделение пулов соединений, отдельные очереди для критических и некритических задач |
| Отказоустойчивость по умолчанию (Fail-safe) | При сбое — перейти в безопасное состояние | Возврат кэшированных данных, переключение на упрощённый режим ("читалка работает, редактирование — нет") |
| Постепенное деградирование (Graceful degradation) | Сохранение частичной функциональности | Отключение аналитики при высокой нагрузке, отображение устаревших данных при недоступности БД |
| Самовосстановление (Self-healing) | Автоматическое восстановление без участия человека | Health-check + auto-restart, автоматическое переключение на реплику при таймауте |
3.2. Паттерны устойчивости
Паттерны устойчивости (Resilience Patterns) — это проверенные архитектурные шаблоны и программные механизмы, которые реализуют принципы устойчивости на практике. Они защищают систему от каскадных сбоев, перегрузок и нестабильности сети.
-
Circuit Breaker — "выключатель", который временно блокирует вызовы к нестабильному сервису после N сбоев. Позволяет избежать каскадного отказа.
Пример: библиотеки Polly (.NET), Resilience4j (Java), Hystrix (устаревает). -
Retry with Backoff — повтор с экспоненциальной задержкой. Важно: только для идемпотентных операций.
Правило — max 3 попытки, начальная задержка 100 мс, множитель 2. -
Timeout — явное ограничение времени ожидания. Без таймаута потоки "зависают", исчерпывая пул.
Рекомендация — таймаут вызова должен быть меньше, чем таймаут его клиента (правило 30-60-90 — клиент 90 мс, сервис 60 мс, БД 30 мс). -
Fallback — альтернативный путь при сбое.
Пример: при недоступности рекомендательного движка — показать "популярные товары" из кэша.
Сводка механизмов и ссылки на интеграции — 4.06 / отказоустойчивость.
3.3. Тестирование отказоустойчивости
Тестирование отказоустойчивости (Fault Tolerance Testing / Resilience Testing) — это вид тестирования надежности, который проверяет способность ИТ-системы продолжать корректную работу или быстро восстанавливаться при возникновении сбоев в ее компонентах (серверах, базах данных, сети, сторонних API).
Главная цель — убедиться, что защитные паттерны (Circuit Breaker, Bulkhead, Fallback) работают в реальности, а не только на бумаге.
Проектирование без проверки — теория. Необходимы:
- Chaos Engineering — намеренное введение сбоев (отключение узла, имитация latency) в staging/prod,
- Load + Failure Testing — нагрузка + одновременный сбой компонента,
- Game Days — симуляции инцидентов с участием команды.
Инструменты — Chaos Monkey, Gremlin, Toxiproxy.
4. Безопасность
Безопасность — встраивание контроля на каждом уровне. Cуть в том, что в системе не существует одной «серебряной пули» или идеального периметра. Безопасность достигается за счет создания множества независимых баров и рубежей контроля: если злоумышленник взломает один уровень, его остановит защита следующего.
Встраивание контроля на каждом уровне логически привело индустрию к парадигме Zero Trust. Её главный лозунг: «Никому не доверяй, всегда проверяй» (Never trust, always verify). В этой модели система относится к любому запросу — будь то внешний пользователь из интернета или внутренний микросервис в том же дата-центре — как к потенциальной угрозе. Каждый шаг, каждый вызов API и каждая транзакция должны быть изолированно аутентифицированы, авторизованы и залогированы.
4.1. Принципы безопасного проектирования
Принципы безопасного проектирования (Secure by Design) — это набор фундаментальных подходов к созданию архитектуры ИТ-систем, при которых безопасность закладывается в систему с самого первого дня, а не «навешивается» поверх готового кода перед релизом.
Исправление архитектурной уязвимости на поздних этапах обходится в сотни раз дороже, чем защита системы на этапе проектирования.
| Принцип | Описание | Пример в проектировании |
|---|---|---|
| Минимальные привилегии | Компонент имеет только те права, что необходимы | Микросервис OrderService имеет доступ только к таблице orders, не ко всей БД |
| Защита в глубину (Defense in depth) | Несколько независимых слоёв защиты | Валидация на gateway + на application layer + в домене + в БД (CHECK-ограничения) |
| Безопасность по умолчанию | Небезопасные настройки — запрещены | Все API закрыты по умолчанию; открытые — явно помечены [AllowAnonymous] |
| Аудит и отслеживаемость | Любое действие — логируется с контекстом | Запись в audit log: кто, что, когда, с каким correlation ID |
4.2. Контроль доступа — от RBAC к ABAC
RBAC (Role-Based Access Control) и ABAC (Attribute-Based Access Control) — это две наиболее популярные методологии управления доступом, которые определяют, как именно система решает, разрешить пользователю совершить действие или отказать. RBAC связывает права доступа с ролью, которую занимает пользователь в организации. ABAC — это более гибкий и современный подход, который принимает решение на основе атрибутов (характеристик) четырех сущностей: субъекта, ресурса, действия и окружения.
-
RBAC (Role-Based Access Control) — права назначаются ролям (
admin,user). Прост, но не гибок.
Пример:adminможет удалять заказы → но что, если только свои? -
ABAC (Attribute-Based Access Control) — права зависят от атрибутов:
- Пользователя (
department == "finance"), - Ресурса (
order.ownerId == userId), - Контекста (
time < 18:00).
Пример на псевдокоде:
- Пользователя (
if (user.role === 'manager' && order.status === 'draft' && order.createdBy === user.id) {
allow('delete');
}
ABAC сложнее, но соответствует реальным бизнес-правилам. Реализуется через политики (OPA — Open Policy Agent) или декларативные атрибуты.
4.3. Защита данных
Защита данных (Data Protection) — это комплекс архитектурных, программных и организационных мер, направленных на обеспечение конфиденциальности, целостности и доступности информации на всех этапах ее жизненного цикла.
В современном безопасном проектировании защита данных строится вокруг трех состояний информации.
- In transit — TLS 1.3, mutual TLS (mTLS) между сервисами. Это информация, которая передается по сети: от клиента к серверу, между микросервисами или при интеграции с внешними API партнеров.
- At rest — полное шифрование (TDE в СУБД) или на уровне приложения (шифрование полей
creditCardдо записи). Это информация, которая находится на постоянном хранении: в базах данных, облачных хранилищах (S3), на жестких дисках серверов или бэкапах. - In use — защита памяти (secure enclaves, Intel SGX — редко, но для регуляторных систем). Это информация, которая прямо сейчас находится в оперативной памяти (RAM) сервера, кэше или регистрах процессора во время выполнения кода.
Важно: ключи шифрования не хранятся в том же месте, что и данные. Используются KMS (Key Management Service) — HashiCorp Vault, AWS KMS, Azure Key Vault.
5. Сопровождаемость
Сопровождаемость (Maintainability / Поддерживаемость) — это характеристика качества программного обеспечения, которая определяет, насколько легко, быстро и безопасно разработчики могут изменять систему.
Система, которую невозможно понять, отладить или изменить — обречена. Сопровождаемость — это инвестиция в будущее. Изменения включают исправление багов, добавление новых функций, оптимизацию производительности и адаптацию ИТ-системы под новые версии библиотек или инфраструктуры. В среднем до 80% стоимости проекта тратится именно на этапе сопровождения, поэтому этот критерий напрямую влияет на выживание ИТ-продукта на рынке.
5.1. Наблюдаемость (Observability)
Наблюдаемость (Observability) — это свойство ИТ-системы, которое позволяет полностью понять ее внутреннее состояние, поведение и причины сбоев на основе анализа ее внешних выходных данных.
Если классический мониторинг просто отвечает на вопрос: «Работает ли система прямо сейчас?» (Да/Нет), то наблюдаемость отвечает на вопрос: «Почему именно система начала сбоить и где конкретно локализована проблема?». Это критически важно для распределенных систем и микросервисов, где один запрос пользователя проходит через десятки узлов.
Три столпа:
- Логи — структурированные (JSON), с
level,service,traceId,spanId. - Метрики — количественные показатели (latency, error rate, saturation).
- Трассировки — end-to-end цепочка вызовов через распределённую систему (OpenTelemetry).
Проектирование под наблюдаемость:
- Все входящие запросы получают
X-Request-ID, - Каждый лог содержит
correlationId, - Критические пути инструментированы
StartSpan()/EndSpan().
Раньше для сбора метрик, логов и трассировок приходилось ставить в код три разные библиотеки от разных вендоров, что сильно раздувало и усложняло приложение.Сегодня индустриальным стандартом стал проект OpenTelemetry (OTel) под эгидой Cloud Native Computing Foundation (CNCF). Это единый открытый SDK и набор API, который позволяет собирать все три типа данных (метрики, логи, трассы) в унифицированном формате и отправлять их в любую систему визуализации на ваш выбор (будь то Grafana, Datadog или Jaeger).
5.2. Тестируемость
Тестируемость (Testability) — это характеристика качества архитектуры и кода ИТ-системы, которая определяет, насколько легко, быстро и эффективно ее можно проверить на наличие ошибок.
Как обсуждалось ранее, модуль, который нельзя протестировать изолированно, — плохо спроектирован.
Признаки хорошей тестируемости:
- Зависимости инжектятся,
- Побочные эффекты вынесены (время, случайность, IO),
- Чистые функции выделены.
Высокая тестируемость означает, что вы можете покрыть систему автоматическими тестами с минимальными затратами усилий, а сами тесты будут выполняться быстро и давать предсказуемый результат.
5.3. Документированность по замыслу
Документированность по замыслу (Documentation by Design) — это подход к проектированию и разработке, при котором создание, актуализация и проверка технической документации встраиваются непосредственно в жизненный цикл ПО и код, а не выполняются вручную «постфактум».
Она должна рождаться естественным путем из самой архитектуры, автоматических тестов и кода. Это единственный способ избежать ситуации, когда система живет своей жизнью, а текстовые файлы в Wiki (Confluence/Notion) — своей, безнадежно устаревая через неделю после релиза.
Документация — продукт проектирования:
- Контракты API (OpenAPI/Swagger) генерируются из кода,
- Диаграммы (C4) обновляются при изменении архитектуры,
- Decision Log (ADR — Architectural Decision Record) фиксирует почему было выбрано решение.
5.4. SLO и эксплуатация
SLO (Service Level Objectives) и эксплуатация (Operations / SRE) — это концепции, которые связывают качество работы ИТ-системы с бизнес-целями и повседневной поддержкой инфраструктуры.
В современной эксплуатации надежность измеряется не абстрактным «все работает», а конкретными математическими метриками в рамках методологии SRE (Site Reliability Engineering) от Google. Чтобы эффективно эксплуатировать систему, нужно четко разделять три связанных понятия:
[ SLI ] (Метрика) ──► "Какое время отклика у API прямо сейчас?" (например, 150 мс)
[ SLO ] (Цель) ──► "99% запросов за месяц должны иметь отклик < 200 мс"
[ SLA ] (Договор) ──► "Если SLO нарушено, мы возвращаем клиентам 10% денег"
- SLI (Service Level Indicator) — индикатор уровня обслуживания. Это конкретная техническая метрика, измеряемая в реальном времени. (Пример: Доля успешных HTTP-ответов).
- SLO (Service Level Objective) — целевой уровень обслуживания. Это внутренний ориентир надежности, к которому стремится команда эксплуатации. (Пример: 99.9% всех запросов за месяц должны быть успешными).
- SLA (Service Level Agreement) — соглашение об уровне обслуживания. Это внешнее юридическое или финансовое обязательство перед клиентами. Нарушение SLA ведет к штрафам. SLA всегда мягче, чем SLO, чтобы у инженеров был запас прочности для исправления проблем до того, как пострадает бизнес.
NFR подтверждаются в проде — SLI (что меряем), SLO (цель), error budget (сколько "плохого" допустимо). Заложите trace_id, метрики на gateway, health checks — см. микросервисы — observability.
6. Мини-кейс — каталог под распродажу
Вход: 10× трафика, карточка товара p95 ≤ 200 мс.
- Доминирует чтение → read-модель + CDN.
- Цена может отставать на 30 с → кэш витрины.
- Падение рекомендаций → показываем каталог без блока "похожие".
Антирешение: резать на микросервисы за неделю без tracing.
Что запомнить
NFR в цифрах → приём в архитектуре → проверка метриками. Дальше: Вертикальное масштабирование · Надежность и доступность · Проектирование API и интеграций · итоги