6.11. Проектирование под нефункциональные требования
Проектирование под нефункциональные требования
1. Что такое нефункциональные требования
Функциональные требования отвечают на вопрос «что система делает?» («Пользователь может оформить заказ»).
Нефункциональные — на вопрос «как хорошо она это делает?» («Заказ оформляется за ≤ 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%» |
Только такая формулировка позволяет сделать выбор: использовать кэширование или оптимизировать запрос? Внедрять репликацию или резервное копирование? Шифровать на уровне приложения или СУБД?
2. Масштабируемость
Масштабируемость — это способность системы сохранять характеристики при увеличении нагрузки. Она делится на два типа:
- Вертикальная (scale-up) — увеличение мощности одного узла (CPU, RAM, SSD).
- Горизонтальная (scale-out) — добавление новых узлов в кластер.
Проектирование под горизонтальную масштабируемость требует соблюдения нескольких принципов.
2.1. Отсутствие shared state
Если два экземпляра сервиса обращаются к одной общей переменной в памяти — масштабирование невозможно.
Решение:
- Вынос состояния во внешнее хранилище (Redis, PostgreSQL),
- Использование stateless-сервисов (все данные — в запросе или в БД),
- Шардирование сессий (sticky sessions — антипаттерн, но допустим при переходе).
2.2. Идемпотентность и безопасность операций
Как обсуждалось ранее, идемпотентность (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) → фоновые обработчики
Первый — проще для отладки, но хрупок. Второй — сложнее, но масштабируется линейно.
2.4. Локальность данных (data locality)
Операции над данными должны происходить там, где они находятся.
- Если данные шардированы по
user_id, логика, работающая с пользователем, должна выполняться на том же узле. - В распределённых БД (CockroachDB, Yugabyte) это достигается через коллокацию (placement rules).
- В микросервисах — через владение данными (service owns its data).
Нарушение локальности → сетевые вызовы → рост latency и снижение throughput.
3. Отказоустойчивость
Отказоустойчивость — управление последствиями. Проектирование начинается с предположения: всё, что может сломаться — сломается.
3.1. Принципы устойчивости
| Принцип | Описание | Реализация в коде/архитектуре |
|---|---|---|
| Изоляция (Bulkheads) | Не дать сбою в одном компоненте повлиять на другие | Разделение пулов соединений, отдельные очереди для критических и некритических задач |
| Отказоустойчивость по умолчанию (Fail-safe) | При сбое — перейти в безопасное состояние | Возврат кэшированных данных, переключение на упрощённый режим («читалка работает, редактирование — нет») |
| Постепенное деградирование (Graceful degradation) | Сохранение частичной функциональности | Отключение аналитики при высокой нагрузке, отображение устаревших данных при недоступности БД |
| Самовосстановление (Self-healing) | Автоматическое восстановление без участия человека | Health-check + auto-restart, автоматическое переключение на реплику при таймауте |
3.2. Паттерны устойчивости
-
Circuit Breaker — «выключатель», который временно блокирует вызовы к нестабильному сервису после N сбоев. Позволяет избежать каскадного отказа.
Пример: библиотеки Polly (.NET), Resilience4j (Java), Hystrix (устаревает). -
Retry with Backoff — повтор с экспоненциальной задержкой. Важно: только для идемпотентных операций.
Правило: max 3 попытки, начальная задержка 100 мс, множитель 2. -
Timeout — явное ограничение времени ожидания. Без таймаута потоки «зависают», исчерпывая пул.
Рекомендация: таймаут вызова должен быть меньше, чем таймаут его клиента (правило 30-60-90: клиент 90 мс, сервис 60 мс, БД 30 мс). -
Fallback — альтернативный путь при сбое.
Пример: при недоступности рекомендательного движка — показать «популярные товары» из кэша.
3.3. Тестирование отказоустойчивости
Проектирование без проверки — теория. Необходимы:
- Chaos Engineering — намеренное введение сбоев (отключение узла, имитация latency) в staging/prod,
- Load + Failure Testing — нагрузка + одновременный сбой компонента,
- Game Days — симуляции инцидентов с участием команды.
Инструменты: Chaos Monkey, Gremlin, Toxiproxy.
4. Безопасность
Безопасность — встраивание контроля на каждом уровне.
4.1. Принципы безопасного проектирования
| Принцип | Описание | Пример в проектировании |
|---|---|---|
| Минимальные привилегии | Компонент имеет только те права, что необходимы | Микросервис OrderService имеет доступ только к таблице orders, не ко всей БД |
| Защита в глубину (Defense in depth) | Несколько независимых слоёв защиты | Валидация на gateway + на application layer + в домене + в БД (CHECK-ограничения) |
| Безопасность по умолчанию | Небезопасные настройки — запрещены | Все API закрыты по умолчанию; открытые — явно помечены [AllowAnonymous] |
| Аудит и отслеживаемость | Любое действие — логируется с контекстом | Запись в audit log: кто, что, когда, с каким correlation ID |
4.2. Контроль доступа: от 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. Защита данных
- In transit — TLS 1.3, mutual TLS (mTLS) между сервисами.
- At rest — полное шифрование (TDE в СУБД) или на уровне приложения (шифрование полей
creditCardдо записи). - In use — защита памяти (secure enclaves, Intel SGX — редко, но для регуляторных систем).
Важно: ключи шифрования не хранятся в том же месте, что и данные. Используются KMS (Key Management Service): HashiCorp Vault, AWS KMS, Azure Key Vault.
5. Сопровождаемость
Система, которую невозможно понять, отладить или изменить — обречена. Сопровождаемость — это инвестиция в будущее.
5.1. Наблюдаемость (Observability)
Три столпа:
- Логи — структурированные (JSON), с
level,service,traceId,spanId. - Метрики — количественные показатели (latency, error rate, saturation).
- Трассировки — end-to-end цепочка вызовов через распределённую систему (OpenTelemetry).
Проектирование под наблюдаемость:
- Все входящие запросы получают
X-Request-ID, - Каждый лог содержит
correlationId, - Критические пути инструментированы
StartSpan()/EndSpan().
5.2. Тестируемость
Как обсуждалось ранее, модуль, который нельзя протестировать изолированно, — плохо спроектирован.
Признаки хорошей тестируемости:
- Зависимости инжектятся,
- Побочные эффекты вынесены (время, случайность, IO),
- Чистые функции выделены.
5.3. Документированность по замыслу
Документация — продукт проектирования:
- Контракты API (OpenAPI/Swagger) генерируются из кода,
- Диаграммы (C4) обновляются при изменении архитектуры,
- Decision Log (ADR — Architectural Decision Record) фиксирует почему было выбрано решение.