Ефективна імплементація SSE. Переваги та виклики
Усім привіт! Мене звати Марія Образцова, я Front-end розробниця в бізнесі FORMA продуктової IT-компанії Universe Group, що створює мобільні застосунки та вебплатформи для підвищення якості життя людей. FORMA — це бізнес, який створює SaaS для роботи з документами. Свій професійний шлях я розпочала три роки тому. За цей час здобула чималий досвід в розробці та власні вдалі кейси.
Протягом усього життя нашого продукту ми встигли імплементувати різні підходи до комунікації клієнта з сервером: Long-polling, Short-polling та Server Side Events (SSE). Під час нещодавньої реалізації останнього з них — SSE, я не знайшла предметного порівняння усіх трьох методів. Є багато статей, де описуються їхні плюси та мінуси на теоретичному рівні, але мені не траплялись практичні результати порівнянь — час отримання клієнтом інформації з сервера, підводні камені імплементацій тощо. Тому вирішила виправити це своїм текстом.
У статті буде трішки теорії про кожен з методів (плюси, мінуси та підводні камені з досвіду нашого продукту), безпосередньо практична частина — результати кожного методу у графіках, а також короткі підсумки.
Матеріал буде корисним для розробників та розробниць, що у себе на проєкті так чи інакше забирають інформацію з сервера (або саме ухвалюють рішення, яким чином це робити). А також для тих, хто хоче дізнатись трошки більше про комунікації клієнт-сервер для загального розвитку.
Історія версій, або Наші висновки на шляху розв’язання проблеми
Одна з основних функціональностей продукту — конвертація файлів з PDF в інші формати та навпаки. Безпосередньо конвертація виконується стороннім сервісом, з яким комунікує Back-end.
Отже, основне завдання Front-end частини — дочекатися завершення конвертації (тобто відповіді від Back-end), забрати готовий файл та передати його користувачеві якомога швидше.
Далі перейдемо до методів, які ми випробували для цього.
Long-polling
Першою ітерацією був Long-polling — метод, де клієнт відправляє звичайний HTTP-запит, а сервер тримає цей запит відкритим, поки у нього не зʼявиться відповідь (у нашому випадку це закінчення конвертації) або не витече час зʼєднання (timeout). Якщо клієнт не отримав успішну відповідь, то відразу після закриття старого запиту він кидає наступний.
Схематичне зображення Long-polling
Спрощена імплементація на нашому продукті:
/** * @description Attempts to retrieve a converted file from the backend * 1. Send request to backend to get the converted file with the provided fileId and format 2. If the conversion process has failed, throw an error 3. If the file is successfully converted, return the result 4. If a timeout error occurs and attempt count is less than 2, increment attempt count and retry 5. If a timeout error occurs and attempt count is 2 or more, reset attempt count to 0 and throw the error 6. If any other error occurs, throw the error */ const waitForFileConvertLongPolling = async ( fileId: string, format: InternalFileType, attempt = 0 ) => { try { const result = API.files.getConvertedFile(fileId, format) if (result.processing_status === 'FAILED') { throw new Error() } return result } catch (e) { if (axios.isAxiosError(e) && e.message.includes("timeout")) { if (attempt < 2) { attempt++; return waitForFileConvertLongPolling(fileId, format, attempt); } else { attempt = 0; throw e; } } throw e; } };
Плюси такого підходу:
- Легка імплементація. Це прості HTTP-запити, які відправляються з певним інтервалом.
- Майже real-time отримання інформації. Оскільки сервер тримає зʼєднання відкритим, клієнт миттєво отримує інформацію. Єдиний виняток — якщо потрібна інформація (у нашому випадку результат конвертації) надходить в момент встановлення підключення з сервером. Тоді буде несуттєва затримка.
- Через те, що це прості HTTP-запити, цей метод підтримується всіма браузерами.
Мінуси методу:
- Ресурсозатратно. Метод вимагає від сервера тримати підключення для кожного клієнта відкритим протягом достатньо довгого часу, що потребує великих ресурсів.
- Високий ризик краша сервера. З попереднього пункту зрозуміло, що занадто стрімке масштабування кількості зʼєднань та/або поганий попередній розрахунок необхідних ресурсів можуть призвести до падіння сервера та, отже, всього вебзастосунку.
- Неоптимальне використання ресурсів. Зʼєднання підтримуються відкритими та споживають ресурс, навіть якщо фактичного обміну інформацією немає.
Чому відмовились? Через регулярні падіння сервера.
На початкових етапах запуску сайту відбувалось швидке масштабування маркетингу. Відповідно, дуже швидко, але при цьому неодноразово, зростало навантаження на сайт в цілому, включаючи кількість конверсій файлів. У таких умовах передбачити потрібну кількість ресурсів для сервера було складно.
Також не забуваємо, що сам по собі Long-polling не зовсім оптимально використовує ресурси.
Ці проблеми (і суботні повідомлення про те, що сайт разом з Back-end знову лежать) змусили нас відмовитись від такого методу та перейти до наступного етапу для продукту — Short-polling.
Short-polling
Наступним проміжним етапом був Short-polling. Його відмінність від попереднього методу полягає в тому, що сервер одразу віддає клієнту відповідь — готовий файл до завантаження чи ще ні. Клієнт, своєю чергою, відправляє наступний запит із затримкою (на відміну від Long-polling, де наступний запит летить одразу), щоб уникнути надмірного навантаження на Back-end — вірогідність того, що файл конвертується за одну мілісекунду, дуже мала.
Схематичне зображення Short-polling
Спрощена імплементація на нашому продукті:
/** * @description wait for the file (with the provided fileId) to convert 1. Send request to backend to know if the file is already converted 2. If file is already converted, return result 3. If not, wait 3 sec and send request again 4. Try to get success status for 30 attempts 5. If all 30 tries failed, throw error and set attempt to 0 */ const waitForFileConvertShortPolling = async ( fileId: string, format: InternalFileType, attempt = 0 ) => { try { const result = await API.files.getConvertedFile(fileId, format) if (result.processing_status === 'FAILED') { throw new Error('converting failed') } return result } catch (e) { //@ts-ignore if (e.message === "converting failed") { throw e; } if (attempt < 30) { attempt++; await wait(3000); return waitForFileConvertShortPolling(fileId, format, attempt); } attempt = 0; throw e; } };
Плюси такого підходу:
- Легка імплементація. Аналогічно Long-polling, це також прості HTTP-запити, які відправляються клієнтом з незначним інтервалом.
- Через свою простоту цей метод підтримується усіма браузерами.
- Менш ресурсомісткий у порівнянні з Long-polling. Серверу потрібно відразу повернути відповідь на запит, а не тримати відкрите зʼєднання протягом певного часу, що зменшує навантаження на нього.
Мінуси методу:
- Може призвести до перевантаження Back-end постійними запитами. Запити, у порівнянні з запитами Long-polling, не забирають багато ресурсів сервера. Однак їхня кількість при необережному поводженні (наприклад, відсутність затримки між запитами) та великій кількості користувачів може бути подібна до DDoS-атаки на сайт.
- Можлива затримка інформації. Запити відправляються з деяким інтервалом (на нашому продукті, наприклад, це було 3 секунди). Тому результат конвертації може досягти клієнта на 3 секунди (в гіршому випадку) пізніше, ніж цей результат буде готовий для відправлення сервером. Затримка також може зростати, особливо якщо Back-end перевантажений через вищезазначений мінус.
Чому відмовились? Через неоптимальну кількість часу, потрібну для отримання файлу. Часто траплялись випадки, коли файл конвертувався швидше, ніж Front-end був готовий його забрати. Тобто Back-end був готовий повернути файл раніше, ніж клієнт встиг відправити повторний запит через ці 3 секунди.
Тож настав час переходити до наступного етапу життя продукту — Server Side Events (SSE).
Але перед цим дам відповідь на питання — чому не WebSocket?
Задачу миттєво забрати готовий файл з сервера після закінчення конвертації можна було б також вирішити за допомогою WebSocket.
WebSocket — це двосторонній канал комунікації сервер-клієнт, що дозволяє обмінюватись інформацією у реальному часі.
Одна з ключових відмінностей від SSE полягає в підтримці двосторонньої комунікації, що означає: і сервер, і клієнт можуть відправляти інформацію (на відміну від SSE, де клієнт лише приймає її). Також важливо врахувати, що для управління вебсокетами потрібно мати додатковий сервер, бо ця технологія не використовує звичайні HTTP-протоколи.
Оскільки клієнту потрібно лише забирати інформацію про файл, тобто тільки слухати сервер, WebSocket у нашому випадку перетворився б на overengineering та залучення великих, але зайвих ресурсів.
Схематичне зображення WebSockets
Server Side Events
Фінальною стадією отримання файлу з сервера на нашому продукті став SSE.
Server Side Events (SSE) — це технологія, яка дозволяє серверу надсилати до клієнта повідомлення в режимі реального часу. На відміну від Long- та Short-polling, клієнт та сервер одноразово встановлюють зʼєднання (часто це відбувається під час ініціалізації застосунку), і клієнт підписується на оновлення з цього каналу. Після цього сервер може відправляти оновлення без зайвих запитів з боку клієнта.
Тобто, як тільки наш файл конвертується, сервер відправляє сповіщення з інформацією про файл, і клієнт в той самий момент отримує це сповіщення та запускає відповідну обробку інформації (наприклад, звантаження файлу користувачу).
Схематичне зображення SSE
Спрощена імплементація на нашому продукті:
// this code is running while app is initializing const eventSourceRef = useRef<EventSource>(); React.useEffect(() => { if (!eventSourceRef.current) { eventSourceRef.current = new EventSource( "absolute-url-for-sse" ); } }, []); const waitForFileConvertSSE = (): Promise<ConvertedFile> => { return new Promise((resolve, reject) => { if (eventSourceRef.current) { eventSourceRef.current.onmessage = (event) => { // handle downloading file }; eventSourceRef.current.onerror = (error) => { // handle error (for example, show fail-popup) }; } }) };
Плюси такого підходу:
- Відсутність додаткових запитів на сервер. Після встановлення зʼєднання клієнт просто очікує на повідомлення від сервера.
- Відсутність необхідності регулярно перевстановлювати зʼєднання, як це було з Long-polling. SSE не має timeout, які потрібно обробляти й, знову ж таки, кидати нові запити.
- Одна з головних переваг цього методу полягає у real-time отриманні інформації з сервера — ніяких затримок при повторних відсиланнях запитів.
Мінуси:
- Потребує додаткової імплементації на стороні Back-end. На відміну від двох попередніх методів, які по суті є простими HTTP-запитами, які клієнт відправляв з певною затримкою, SSE вимагає імплементації як зі сторони Front-end, так і зі сторони Back-end. Варто звернути увагу, що додаткова імплементація на сервері — це не означає підняти додатковий сервер, на відміну від WebSocket.
Розгляньмо також і підводні камені.
Ускладнена логіка балансування навантаження. Після завершення конвертації готовий файл зберігається у базі даних. При використанні HTTP-запитів (наприклад, Short-polling) ми звертаємось до Back-end з запитом «Чи готовий конвертований файл?». Back-end відправляє запит до бази даних для пошуку готового файлу і повертає відповідь. Тобто запит і отримання відповіді відбувається на одному і тому ж Back-end.
У випадку SSE Front-end-1 підтримує постійне з’єднання з Back-end-1, але повідомлення про готовність файлу може прийти до Back-end-2. Відповідно, повідомлення ніколи не буде доставлено до Front-end-1, оскільки Back-end-1 не отримає його, а з Back-end-2 з’єднання з Front-end-1 відсутнє. Тому для SSE потрібно передбачити «прошарок» на рівні Back-end та інфраструктури (у нашому випадку це Redis), який буде відповідальний за розсилання сповіщень всім Back-ends про готовність файлу.
Верифікація refresh token. Оскільки SSE встановлює зʼєднання один раз і довго його утримує, можливо, з’явиться потреба оновлення refresh token протягом цього часу. Конкретний план дій може варіюватись залежно від імплементації авторизації в кожному окремому продукті, проте важливо не забувати про цей аспект.
А тепер — до результатів практики
Основне завдання, яке я ставила перед собою в цій статті: забрати результат конвертації з Back-end якомога швидше. Виміряти успіх цієї задачі дуже просто за допомогою графіків. Для порівняння швидкості цих підходів я провела невеликий експеримент — взяла результати конвертації 500 файлів для усіх трьох підходів.
Тож представляю вам графік, де синя крива — це результати Short-polling, червона крива — результати Long-polling, а зелена — результати SSE. Вісь Y — це час конвертації у секундах, вісь Х — порядковий номер файлу (від 1 до 500).
Графік 1
Як явно бачимо з графіка вище, SSE швидше повертає результат.
Якщо дивитись за середнім часом конвертації, для SSE він становить 15,63 секунди, для Short-polling — 33,17 секунди, для Long-polling — 32,9 секунди.
Графік 2: Середній час конвертації для Server Side Events (червона лінія) — 15,63 секунди
Графік 3: Середній час конвертації для Short-polling (червона лінія) — 33,17 секунди
Графік 4: Середній час конвертації для Long-polling (помаранчева лінія) — 32,9 секунди
Підсумуємо
Long-polling легкий в імплементації та віддає інформацію майже в real-time, якщо тільки сервер не отримав інформації під час реконекта до клієнта. При цьому він дуже ресурсомісткий та має високий ризик краша застосунку, якщо необережно поставитись до навантаження (наприклад, при швидкому масштабуванні кількості юзерів на сайті).
Short-polling також дуже легкий в імплементації та менш ресурсомісткий у порівнянні з long-polling, оскільки браузеру не треба тримати зʼєднання довго. Але існує великий шанс затримки інформації через відправлення запитів з інтервалом. Також цей метод збільшує навантаження на Back-end, бо ми відправляємо велику кількість запитів, що може призвести до краша застосунку або до збільшення ресурсоспоживання.
Server Side Events мають велику перевагу у швидкості, адже інформація відправляється з сервера до клієнта без жодних затримок. Це легко побачити в результатах експерименту — середній час конвертації для SSE удвічі менший, ніж для двох попередніх методів. Серед переваг методу можна назвати відсутність додаткових запитів та часу на реконект. До мінусів та підводних каменів належать додаткова імплементація на частині Back-end, а також ускладнена логіка балансування навантаження з погляду інфраструктури.
Тож, якщо порівнювати усі три технології за швидкістю роботи, SSE, вочевидь, перемагає. Та це не означає, що усім проєктам треба терміново запровадити SSE замість Long- та Short-polling.
Наприклад, якщо продукт знаходиться на етапі стартапу, в ньому є критичні баги, а інформацію з сервера треба забирати вже, то вам може цілком підійти й швидкість звичайних polling-методів.
Також раджу замислитись, чи вартий цього SSE, якщо на продукті взагалі не відіграє ролі швидкість отримання інформації.
Сподіваюся, мій досвід роботи над нашим продуктом допоможе вам покращити роботу або ухвалити оптимальні рішення щодо проєкту. Або ж просто розширити вашу ерудицію як розробника чи розробниці.
36 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів