Синхронна й асинхронна комунікація між фронтендом та бекендом: яку архітектуру обрати
Всім привіт! Я — Ігор Закутинський, Head of Engineering FORMA в групі компаній Universe Group з екосистеми Genesis. Це SaaS-платформа, яка займається конвертацією, редагуванням та роботою з PDF-документами.
Маю загалом понад 10 років у розробці. Основний фокус — побудова масштабованих та розподілених систем. Також я сертифікований архітектор AWS, маю PhD і викладаю в університеті деякі Computer Science дисципліни. Маю чимало хобі, але в останні роки займаюсь технологічним волонтерством — розробляю дрони, системи РЕБ та захищеного зв’язку, які допомагають нашим військовим наближати перемогу.
У цьому тексті я розповім про синхронну та асинхронну комунікацію у вебі, як обрати протокол та архітектуру відповідно до типу проєкту та його темпу зростання. Поділюся нашим підходом до тестування ефективності протоколів та деякими практичними порадами.
Протоколи синхронної комунікації
Одне з моїх улюблених питань на співбесідах із фулстек- та фронтенд-девелоперами: «Ваш основний інструмент — це браузер. Які протоколи він підтримує?». Відповіді зазвичай зводяться до впевненого «HTTP/HTTPS», але рідко кандидати називають більше ніж три протоколи. Водночас їх набагато більше. Наприклад, у браузерах на основі Chromium доступні такі: HTTP/HTTPS, WebSockets, Server-Sent Events (SSE), WebRTC, FTP, QUIC.
Згідно з даними W3C, 2023 року обсяг глобального інтернет-трафіку склав понад 4 зетабайти (4 трильйони гігабайтів). У середньому кожен користувач інтернету завантажує близько 1,7 гігабайта даних на день. Понад 60% трафіку генерується браузерами, левова частка якого припадає на протокол HTTP/HTTPS (70%). Близько 15% та 10% — на WebSockets і WebRTC, і зовсім небагато — на SSE.
Далі поговоримо про деякі з них, порівняємо ефективність та розглянемо способи балансування навантаження.
Синхронна комунікація — стандартна, зрозуміла, можна сказати, древня форма взаємодії між браузером та сервером, коли ми відправляємо запит і чекаємо на відповідь. Зазвичай вона використовується для CRUD-операцій (створення, читання, оновлення, видалення даних), а також у сервісах, де важлива передбачуваність та послідовність (банківські операції, онлайн-покупки).
- Переваги: простота реалізації, передбачуваність.
- Недоліки: затримки в часі очікування відповіді, блокування ресурсів.
- Приклади: HTTP-запити, REST API.
HTTP
Основним протоколом для синхронної комунікації є HTTP. Цей підхід покриває більшість сучасного вебу, і його цілком достатньо для більшості задач. На сьогодні існують три його основні версії. HTTP/1 з’явилася ще 1989 року. Друга версія (HTTP/2) була прийнята 2015 року і принесла фундаментальні зміни: можливість виконувати декілька одночасних запитів у межах одного TCP-з’єднання. І нарешті, HTTP/3 — офіційно прийнята 2020 року. Вона побудована на базі протоколу від Google — QUIC (Quick UDP Internet Connections).
Я працював з цим протоколом, і це дійсно крута річ. YouTube використовує QUIC для буферизації відео, що забезпечує до +40% продуктивності. Проте, на жаль, наразі небагато браузерів підтримують його нативно.
REST
На базі HTTP виникло безліч архітектурних підходів, серед яких найбільш популярним став REST. Він базується на таких принципах:
- Stateless — кожен запит від клієнта до сервера повинен містити всю необхідну інформацію для обробки цього запиту. Це означає, що сервер не зберігає стану клієнта між запитами.
- Клієнт-серверна архітектура — REST чітко розділяє клієнтську і серверну частини застосунку.
- REST дозволяє використовувати проміжні сервери, як-то кеші або балансувальники навантаження.
CoAP
Під час стандартизації REST розробники зрозуміли, що HTTP — занадто громіздкий для певних пристроїв. У результаті зʼявилася ідея протоколу CoAP для IoT пристроїв — стиснутої версії HTTP з меншим функціоналом, але кращими результатами у тестах з продуктивності. Його семантика подібна до HTTP: використовує методи GET, POST, PUT, DELETE. Цей протокол також має чіткий поділ між клієнтом та сервером та дає можливість використовувати проміжні сервери. Наприклад, кеші, балансувальники навантаження.
GraphQL
Ще один популярний інструмент, про який всі точно чули й, імовірно, використовували, — це GraphQL. Він побудований на основі HTTP і частково на WebSockets, коли використовуються сабскрипшени. Дозволяє через один запит отримати дані з декількох джерел, знизити загальну кількість запитів, оптимізувати передачу даних та уникнути надмірних даних. Проте є і недоліки — потрібен додатковий рівень абстракції як на фронтенді, так і на бекенді.
У фоновому режимі: протоколи асинхронної комунікації
При асинхронній комунікації клієнт відправляє запит та може продовжувати роботу, не чекаючи негайної відповіді. Основні переваги — низька затримка та можливість обробки великих обсягів даних в реальному часі. Водночас цей вид комунікації складніший у реалізації та дебагу.
Приклади: WebSockets, Server-Sent Events (SSE), AMQP, MQTT.
WebSockets
Це повнодуплексний протокол, який працює поверх TCP. Він дає змогу клієнту і серверу одночасно обмінюватися пакетами після встановлення realtime-з’єднання. Це зручно, але є нюанси: налаштування та масштабування можуть бути проблемними. Про це поговоримо детальніше далі.
SSE
Ще один цікавий інструмент — це SSE (Server-Sent Events). Дехто називає його «обрізаною версією WebSockets», але я б сказав, що це прокачана версія HTTP. SSE працює поверх HTTP: один раз встановлюється з’єднання з сервером, який може надсилати події клієнту в будь-який момент. Простий у реалізації, економить трафік, порівняно з WebSockets, підходить для стрімінгу даних (оновлення новин, фінансові дані).
Ідеальний юзкейс: підписка на нотифікації.
Якщо ви використовуєте NestJS, інтеграція SSE не вимагає налаштування нового сервера чи створення нових кодових абстракцій. Ви використовуєте той самий HTTP-контролер і методи. Тому SSE може бути дуже зручним, якщо він підходить вашій логіці.
Можливі нюанси: балансування навантаження, реконекти.
Як порівняти ефективність протоколів
Отже, у нас є три основні протоколи для реалізації більшості необхідних юзкейсів: HTTP, WebSockets і SSE. Коли ми проводимо рефакторинг або запускаємо нову систему, виникає питання: що вибрати? Відповім неофіційним лозунгом Universe Group — «Треба тестити».
На перших етапах запуску продукту команда не переймалася оптимізацією і реалізувала все через HTTP Short Polling. Логіка була проста: клієнт ініціює завдання на бекенді, наприклад, «зробити конвертацію файла», і кожні 5 секунд пінгує бекенд, щоб дізнатися статус. Мінуси такого підходу очевидні: по суті фронтенд «дедосить» бекенд. Ми розуміли, що потрібне real-time рішення, але не були впевнені, яке саме — SSE, WebSockets чи, можливо, TRPC? Тоді ми провели експеримент і порівняли ефективність основних протоколів на прикладі отримання сповіщень із бекенду.
Сценарій:
- Фронтенд відправляє запит та тригерить виконання задачі на бекенді.
- Час виконання задачі варіюється від 10 мілісекунд до 20 секунд.
- Після виконання задачі бекенд відправляє сповіщення на фронтенд — через SSE, WS. Або фронтенд забирає його через HTTP.
Для подібних експериментів раджу створити просте середовище, яке хоча б частково імітує ваш продакшн. Так ми підняли емулятор, який генерував завдання, подібні до тих, що проходять між фронтендом і бекендом, а також їхнє виконання, яке могло тривати від 0 до 30 секунд.
Ми провели тести для HTTP Long Polling, HTTP Short Polling (3s Interval), Server-Sent Events (SSE), WebSockets. Порівняти результати допомогли чотири показники:
- Latency — час від моменту завершення задачі до отримання сповіщення (в мілісекундах);
- Server Load — ресурси сервера, необхідні для підтримки з’єднання. Вираховуємо коефіцієнт
[0-5] на основні CPU та Memory utilization; - Complexity — суб’єктивна оцінка того, наскільки ускладнилася кодова база;
- Bandwidth Usage — використання пропускної здатності. Це відносна величина, яку рахуємо на основі Network Utilization.
Тоді нам було набагато легше прийняти обґрунтоване рішення. Ми зрозуміли, чому настав час відмовитися від Short Polling на користь WebSockets та SSE.
Балансування навантаження
Вибір протоколу за результатами експерименту — це лише початок. Кількість користувачів зростає, система ускладнюється, і потрібно продумати, як це рішення працюватиме в умовах збільшення навантаження.
HTTP/HTTPS
З HTTP все доволі просто: вистачить класичного load-balancer на Nginx, HAProxy чи іншій платформі. Дехто навіть підіймає PM2 в кластері та вважає це достатнім балансуванням.
Як це працює: коли надходить запит, ми за певним алгоритмом розподіляємо його на відповідний бекенд. Це легко і зручно дебажити.
Методи: Round Robin, Least Connections, IP Hash, Weighted Round Robin
Realtime протоколи
З балансуванням SSE або WebSockets виникають нюанси. У класичному варіанті цей механізм схожий на HTTP: клієнт ініціює з’єднання з сервером через load-balancer, запит проходить довгий-довгий шлях і закріпляється, — як ЗСУ в Курській області.
Методи: Round Robin, Least Connections, IP Hash.
Нюанси:
- потрібно тримати постійне з’єднання від сервера до клієнта;
- високе навантаження на бекенд через постійний стрімінг даних;
- масштабування для великої кількості одночасних клієнтів.
Уявімо ситуацію: клієнт ініціює завдання на конвертацію файлу з PNG в JPEG, і load-balancer прив’язує з’єднання до сервера 1, який виконує конвертацію через сторонній сервіс. Після завершення цей сервіс відправляє результат через вебхук на load-balancer, з якого він перекидається на випадковий сервер. У нього вже немає з’єднання з клієнтом. У такому випадку потрібно ускладнювати архітектуру, додаючи точку для синхронізації таких подій — умовний Message Broker, який відповідатиме за маршрутизацію потрібних повідомлень.
Коли ми вже розуміємо, які протоколи використовуємо для яких даних, наступний крок — обрати загальну архітектуру фронтенда і бекенда. Популярне рішення — API Gateway. Це доволі зручний інструмент, де в одній точці зібраний чималий функціонал, легко налаштувати моніторинг, є можливість об’єднання синхронних та асинхронних запитів. Водночас є потенційний bottleneck системи. Якщо ваші мікросервіси починають масштабуватися, вам також потрібно масштабувати Gateway, у такий спосіб штучно утворюючи певний моноліт. Для більшої кількості випадків це не буде проблемою, але буде неоптимальним рішенням.
Event-Driven Architecture
Альтернативний підхід — подієво-орієнтована архітектура, адептами якої є Uber та Netflix.
Це асинхронна модель, заснована на подіях. Спрощено це працює так: фронтенд через real-time з’єднання відправляє івенти на бекенд, де вони через черги розкидаються по мікросервісах. Основні її переваги — масштабованість та швидкість обробки. Цей підхід виглядає цікаво, але досить складний в реалізації.
Вибір оптимальної архітектури. На що варто звертати увагу:
- Тип проєкту (real-time, CRUD, аналітика тощо). Визначте, які дані будуть в системі і як вони повинні надходити. Наприклад, для статичних лендінгів це може бути одноразове завантаження даних під час білду. Але для динамічних даних, таких як курси валют, повідомлення чи нотифікації, необхідна підтримка у реальному часі.
- Вимоги до продуктивності та масштабованості. Подумайте про те, як ваш проєкт буде зростати, в яких обсягах, як підготувати інфраструктуру.
- Командні ресурси та експертиза. Оцініть командні ресурси: чи є у вас експерти з потрібними навичками? Адже немає сенсу проєктувати суперскладну архітектуру, яка просто на кубах буде скейлитись за три мілісекунди та розганятись за допомогою Karpenter до неможливого, якщо у команді переважно джуни. Тоді, можливо, краще все-таки залишити Short Polling, який буде кожні 5 секунд ходити на сервер, забирати дані, і збільшити кількість серверів у кластері. Це може бути доцільнішим на поточному етапі розвитку вашого продукту.
17 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів