Typical web-application inner-architecture
Kirill YurkovThe First Nine Guide. Блок 3
Дисклеймер - в реальном мире архитектуры приложений крайне разнообразны, но есть достаточно универсальные слои, которые встречаются повсеместно. О них и поговорим.
В предыдущей серии мы говорили о том, как устроены рантаймы. Но рантайм сам по себе на хлеб не намажешь и сегодня попробуем понять - что нужно реализовать в выбранном нами рантайме, чтобы заслужить почетное звание веб-приложения.
Для знакомства с этим объектом я попытался найти какую-то готовую литературу, но ничего не нашел, что охватывало бы в достаточной степени веб-приложения вне зависимости от рантайма и языка.
Поэтому разработал пару своих интерпретаций, которыми хочу попробовать решить две проблемы:
- Многие инженеры (чаще, конечно, системные) не хотят погружаться в глубины работы приложения, думая что это невероятно сложно или просто не ясно как к этому подойти.
- С другой стороны нередко встречаю обратный взгляд - слишком упрощенный: "есть коробочка, вход, выход и бизнес логика по серединке". И чаще всего это размывает понимание того, какой тип ресурса требует та или иная функция внутри приложения.
Поехали - и ресурсы глянем и кишки посмотрим :)
Проще всего начать с понимания того через какие слои проходит HTTP запрос, для этого будем различать 4 основные слоя внутри нашего приложения:
NETWORK LAYER
Суть: превращает HTTP запрос в структурированные данные для бизнес-логики

Что тут происходит:
- Socket listen на порту (8080, 443)
- Accept connections - принимаем новые подключения
- TLS handshake - расшифровка HTTPS трафика
- HTTP parsing - разбор заголовков и тела запроса
Часто тут думают, что это только IO-bound. Собственно на больших телах с HTTPS болеть может начать именно CPU.
REQUEST PROCESSING
Суть: превращает HTTP запрос в вызов функции и маршрутизирует к нужному обработчику

Что тут происходит:
- Deserialize (JSON/XML/Proto) - разбор тела запроса в объекты
- Validation & Auth - проверка токенов, прав доступа, валидация данных
- Rate limiting - контроль частоты запросов от клиентов
- Request routing - определение какой handler вызвать по URL
Про рейт лимиты я недавно пост делал, поэтому важно сказать, что memory-bound, это чаще всего не распределенные рейт лимиты. Распределенные будут в редисе например и это уже IO.
BUSINESS LOGIC
Суть: выполняет основную логику приложения - то ради чего существует ваш сервис

Что тут происходит:
- Domain operations - основные алгоритмы и бизнес-правила
- Calculations - вычисления, обработка данных, математика
- External API calls - обращения к сторонним сервисам
- Database queries - запросы к PostgreSQL/MySQL/MongoDB
- Cache operations - чтение/запись Redis/Memcached
Этот слой получает приз SRE симпатий, как самый проблемный во веки веков. Тут у нас и медленные запросы SQL, и блокировки внутри, и внешние API вызовы без таймаутов, и даже тяжелые вычисления O(n!) например.
RESPONSE FORMATION
Суть: упаковывает результат обратно в HTTP ответ и отправляет клиенту

Что тут происходит:
- Serialize (JSON/XML/Proto) - превращение объектов в JSON/XML/Protobuf
- Compression (gzip/brotli) - сжатие больших ответов для экономии трафика
- Encryption - шифрование данных для HTTPS
- Send to socket - отправка байтов по сети клиенту
Те же самые проблемы что и у первого слоя.
Полная картина

Внутренняя архитектура
Теперь, когда у нас есть какое-то представление о том из каких объектов состоит типовое приложение, давайте попробуем нарисовать это в виде абстрактной архитектуры, как если бы у нас были микросервисы.

Знакомимся с объектами на схеме, их может быть и больше и меньше в реальности. Тут главное подход. Выделяем в отдельную сущность все, что имеет под собой независимый пул воркеров или любую работу, которая происходит за пределами нашего приложения.
Я выделил вот такой список:
- HTTP Ingress - входная точка для запросов, первичная маршрутизация, load balancing между инстансами.
- Dispatcher - распределяет запросы между воркерами, управляет очередями и приоритетами.
- Worker Execution - основное место выполнения бизнес-логики, здесь живут контроллеры и сервисы.
- Runtime Overhead - Garbage Collection, thread scheduling, планировщики задач. Архитектруно не очень верно говорить что компонент называется Overhead, это намеренно, чтобы подчеркнуть, что сам рантайм и его внутренние механизмы бывают сильно не бесплатны.
- Cache Access - in-memory кеши (Caffeine, Guava), внешние кеши (Redis, Memcached).
- Scripting & Serialization - JSON/XML/Protobuf сериализация, template engines, data transformation.
- Logging & Instrumentation - структурированное логирование и трейсинг.
- External Interaction - database connections, message queues, HTTP APIs и microservices.
Почему, на мой взгляд, понимание этой архитектуры важно? Рассмотрим на примере такой простой штуки как таймайут:
- Из пула воркеров в бизнес логике делаем SQL запрос в базу данных. Представим, что у базы отдельный пул, например (мой любимый) Hikari.
- Но пулы к базе данных заняты. Тут наш воркер тред висит и ждет, когда же там освободится уважаемая БД.
- В данном случае у нас или начнут копится горутины или мы забьем какой-нибудь воркер пул.
Пример довольно очевидный и его, справедливости ради, можно решить наличием хорошего таймаута внутри пула к самой БД. Но чем более реактивный и эфимерный рантайм используется, тем чаще встречаю ситуации, когда даже таймаут это не так очевидно.
Мешочек с советами
Работая с внутренней архитектурой гораздо проще соблюдать паттерны отказоустойчивости:
- таймауты и ttl на внутренних очередях
- bulkhead и разделение пулов внутри приложения, например отдельные для CPU и IO bound (хотя это не везде возможно)
- fallback механики, вплоть до внутренних circuit breaker
Про паттерны будет много отдельных статей.
На этом пока все!
В следующем выпуске мы закончим проектировать и писать наше идеальное приложение и пойдем его деплоить!
В предыдущей серии - разбирались с тем какие есть в мире рантаймы
Подписывайся на канал @r9yo11yp9e - будем искать девятки вместе :)