Перейти к основному содержимому

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)

Три столпа:

  1. Логи — структурированные (JSON), с level, service, traceId, spanId.
  2. Метрики — количественные показатели (latency, error rate, saturation).
  3. Трассировки — 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) фиксирует почему было выбрано решение.