3. Функциональные и нефункциональные требования
«Сначала заставьте программу работать, затем сделайте ее правильной, и лишь потом — быстрой. Но помните: если не заложить нефункциональные требования с самого начала, сделать систему быстрой или надежной без переписывания с нуля уже не удастся».
— Брайан Керниган
Любая программная система создается для решения конкретных задач бизнеса или конечных пользователей. В инженерной практике эти задачи формализуются в виде требований — явных спецификаций того, что именно система должна делать и какими свойствами она обязана обладать при эксплуатации.
Опытный системный архитектор знает: требования разделяются на два фундаментальных класса, и пренебрежение вторым классом гарантированно приводит к катастрофе в продакшене, даже если каждая отдельная строчка кода на Go написана безупречно.
В этой главе мы разберем:
- Принципиальное различие между функциональными и нефункциональными требованиями.
- Почему нефункциональные требования (NFR) являются главным архитектором системного дизайна.
- Как нефункциональные ограничения транслируются в физику рантайма Go — от сборщика мусора до сетевого поллера.
Функциональные требования (Functional Requirements)
Функциональные требования описывают, ЧТО система должна делать. Это конкретные бизнес-возможности, пользовательские сценарии (Use Cases) и поведение интерфейсов, видимое человеку или внешним сервисам.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Functional["Функциональные требования (ЧТО делает сервис)"]
F1["Регистрация пользователя"]:::entry
F2["Создание заказа"]:::process
F3["Списание баланса"]:::process
F4["Отправка email"]:::process
end
subgraph NonFunctional["Нефункциональные требования (КАК система работает)"]
NF1["Latency: P99 < 50ms"]:::process
NF2["Throughput: 10 000 RPS"]:::process
NF3["Availability: 99.95%"]:::process
NF4["Security: Zero Trust & mTLS"]:::process
end
Functional --> Service["Go Микросервис"]
NonFunctional --> Service
Примеры для типичного бэкенд-сервиса на Go:
- Пользователь может зарегистрироваться, указав email и пароль.
- Система должна отправлять email с подтверждением регистрации.
- Пользователь может создать заказ, содержащий список товаров.
- Администратор может просмотреть список всех заказов за период с фильтрацией по статусу.
- При создании заказа необходимо проверить наличие товара на складе через внешний API склада.
Функциональные требования обычно фиксируются в виде User Stories, Use Cases или спецификаций API. Они являются основой для написания кода и тестов.
С точки зрения Go-разработчика, реализация функциональных требований — это написание обработчиков HTTP, бизнес-логики в use case слое, запросов к базе данных и вызовов внешних сервисов. Это то, чем мы занимаемся каждый день.
Нефункциональные требования (Non-Functional Requirements, NFR)
Нефункциональные требования описывают, КАК система должна выполнять свои функции. Это качественные характеристики, системные инварианты, ограничения и атрибуты надежности. Они не видны внутри отдельной функции, но именно они решают судьбу продукта под боевой нагрузкой.
В мировой литературе нефункциональные требования называют Quality Attributes или системными «-ilities» (Scalability, Reliability, Maintainability, Observability, Security).
| Категория | Примеры метрик / требований | Как влияет на архитектуру Go-сервиса |
|---|---|---|
| Производительность | Latency (P95 < 100ms), Throughput (10k RPS) | Выбор между sync/async, настройка пулов соединений, профилирование аллокаций, тюнинг GC |
| Масштабируемость | Горизонтальное масштабирование до 100 инстансов, линейный рост пропускной способности | Stateless-дизайн, отсутствие локального состояния, правильное шардирование БД |
| Надёжность / Доступность | Uptime 99.95%, время восстановления < 5 мин | Graceful shutdown, health checks, retry, circuit breaker, репликация |
| Безопасность | Аутентификация по JWT, шифрование данных в покое, защита от OWASP Top 10 | Middleware аутентификации, валидация входных данных, использование crypto/rand |
| Сопровождаемость | Время добавления новой фичи < 2 дней, понятная структура кода | Clean Architecture, DI, модульные тесты, документация |
| Наблюдаемость | Все запросы трассируются, метрики по RED (Rate, Errors, Duration) | Интеграция OpenTelemetry, экспорт метрик в Prometheus, структурированное логирование |
| Стоимость | Бюджет на инфраструктуру, лимиты CPU/Memory на под | Оптимизация аллокаций, переиспользование объектов, эффективная сериализация |
Почему нефункциональные требования критичны для архитектуры
Представьте, что вы спроектировали систему, идеально реализующую все функциональные требования: пользователи могут регистрироваться, создавать заказы, просматривать историю. Вы написали чистый Go-код с интерфейсами и DI, покрыли тестами. Но когда сервис вышел в продакшен под нагрузкой, выяснилось:
- Обработка запроса занимает 2 секунды, потому что на каждый запрос сервис ходит во внешний API без кэша.
- При 100 одновременных запросах сервер падает с
out of memory, потому что каждая горутина аллоцирует большой буфер для JSON. - После перезапуска сервис теряет все заказы, созданные в памяти, но еще не сохраненные в БД.
Система функционально корректна, но нефункционально провальна. Пользователи не будут ждать 2 секунды, бизнес теряет деньги.
Архитектура — это в первую очередь дисциплина удовлетворения нефункциональных требований. Функциональные требования можно реализовать множеством способов, но именно NFR диктуют, какой из способов выбрать. Например, если требуется высокая пропускная способность, вы пожертвуете простотой синхронного REST в пользу асинхронной очереди (19. Синхронное vs асинхронное взаимодействие сервисов). Если требуется мгновенная консистентность данных, вы откажетесь от CQRS в пользу транзакционной записи в одну БД (23. CQRS. Разделение чтения и записи).
Go и нефункциональные требования: практические проекции
Go как язык и платформа предлагает уникальные возможности и накладывает определенные ограничения в контексте NFR.
Производительность и масштабируемость
Возможности:
- Легковесные горутины позволяют обрабатывать десятки тысяч конкурентных соединений на одном инстансе без значительных накладных расходов (в отличие от модели 1 поток = 1 соединение в Java или C#).
- Встроенный HTTP-сервер (
net/http) эффективно использует пул горутин и неблокирующий I/O черезepoll/kqueue. - Низкий порог входа в оптимизацию через встроенные инструменты
pprofиtrace(16. Профилирование, отладка и производительность).
Ограничения и ловушки:
- Garbage Collector — хотя GC в Go очень быстрый и имеет субмиллисекундные паузы, он всё равно может влиять на хвостовые задержки (tail latency) в системах с жёсткими требованиями к предсказуемости. Для таких случаев требуются ручное управление памятью через
sync.Poolи минимизация аллокаций. - Блокировка горутины на системном вызове не блокирует весь поток ОС (благодаря hand-off в планировщике), но создаёт дополнительную нагрузку на планировщик. При большом количестве заблокированных на I/O горутин может потребоваться увеличение
GOMAXPROCS.
package pool
import (
"net/http"
"sync"
)
// Пример: минимизация аллокаций через sync.Pool для высоконагруженного сервиса
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 4096)
},
}
func handler(w http.ResponseWriter, r *http.Request) {
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf[:0]) // возвращаем в пул, обнуляя длину
// использование buf для сериализации или чтения
// ...
w.WriteHeader(http.StatusOK)
}
Надёжность и отказоустойчивость
Инструменты Go:
- Контексты (
context.Context) — стандартный механизм передачи дедлайнов и отмены операций. Позволяет корректно обрывать цепочки вызовов, предотвращая утечки горутин и бесконечные ожидания. - Graceful Shutdown — возможность корректно завершить HTTP-сервер, дождавшись обработки текущих запросов.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func runServer() {
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
}
Паттерны устойчивости:
- Retry с exponential backoff — реализуется в несколько строк, но требует осторожности с идемпотентностью (36. Circuit Breaker, Retry, Timeout и Backoff).
- Circuit Breaker — библиотеки вроде
gobreakerилиsony/gobreakerпредотвращают каскадные отказы.
Безопасность
Go имеет богатую стандартную библиотеку crypto, которая позволяет реализовать большинство криптографических операций без внешних зависимостей. Однако важно помнить:
- Не используйте
math/randдля генерации токенов или ключей — толькоcrypto/rand. - При работе с JWT проверяйте алгоритм подписи (избегайте
alg: none). - Всегда валидируйте входные данные перед анмаршалингом в структуры.
Компромиссы между нефункциональными требованиями
Нефункциональные требования часто конфликтуют друг с другом. Нельзя одновременно максимизировать все показатели. Архитектор должен находить баланс.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
A["Высокая доступность<br>Availability"]:::process
B["Строгая консистентность<br>Consistency"]:::process
C["Низкая задержка<br>Low Latency"]:::process
D["Низкая стоимость<br>Low Cost"]:::process
A ---|"противоречит"| B
A ---|"противоречит"| D
B ---|"противоречит"| C
C ---|"противоречит"| D
- CAP-теорема (30. CAP теорема и реальные компромиссы): в распределённой системе при сетевом разделении вы должны выбрать между доступностью и консистентностью.
- Latency vs Consistency: репликация данных асинхронно (eventual consistency) даёт низкую задержку записи, но читатели могут видеть устаревшие данные. Синхронная репликация гарантирует свежесть, но увеличивает время ответа.
- Cost vs Performance: добавление индексов в БД ускоряет чтение, но замедляет запись и увеличивает объём хранилища. Добавление кэша Redis снижает нагрузку на БД, но увеличивает сложность и стоимость инфраструктуры.
Как Go помогает управлять компромиссами
- Явная обработка ошибок заставляет разработчика думать о том, что может пойти не так, и проектировать поведение системы при отказах. В языках с исключениями легко забыть обработать редкий случай.
- Контексты (
context.Context) делают таймауты и отмену первоклассными гражданами, а не дополнительной опцией. Это напрямую влияет на надёжность и предсказуемость. - Простота конкурентности через горутины и каналы позволяет строить высокопроизводительные системы без сложных абстракций вроде реактивных потоков, но требует дисциплины в управлении временем жизни горутин.
- Статическая компиляция упрощает деплой и контейнеризацию, снижая операционные риски (несоответствие версий библиотек, уязвимости в системных пакетах). Это напрямую влияет на сопровождаемость и безопасность.
Итог
- Функциональные требования отвечают на вопрос «Что система делает?». Они видимы пользователю и определяют продуктовую ценность.
- Нефункциональные требования отвечают на вопрос «Как система это делает?». Они определяют качество, надёжность, производительность и, в конечном счёте, удовлетворённость пользователя и стоимость владения.
- Архитектура системы на 80% определяется нефункциональными требованиями. Функциональные требования можно реализовать по-разному, но только NFR диктуют выбор конкретных паттернов и технологий.
- Go предоставляет отличный баланс между производительностью, простотой и надёжностью, но требует от разработчика понимания того, как написанный код транслируется в поведение системы под нагрузкой.
Теперь, когда мы понимаем разницу между функциональными и нефункциональными требованиями, мы готовы перейти к конкретным метрикам, с помощью которых бизнес и инженеры договариваются о качестве сервиса. В следующей статье мы разберем: 4. SLA, SLO, SLI и как они влияют на дизайн.