Що робити, коли сервер тобі не відповідає
Всім привіт! Давно я не писала статей на ДОУ. Якщо ви мене забули, я все ще Олена Шаровар, і останні 6 років тісно й глибоко працюю з усім, що пов’язано з платежами, переказами та білінгом, як Senior/Lead Engineer. Ця стаття є доповненням до статті Сергія Бабіча «Про три символи, на які може послати сервер», адже я хочу детальніше розповісти про ситуацію, коли сервер НЕ посилає вас на три символи.
Чому сервер вам не відповідає
Коли ви переходите з моноліту на мікросервіси, які спілкуються між собою через HTTP-протокол, у вас з’являється дуже цікава проблема — таймаути. Таймаут — це ситуація, коли ви відправили HTTP-запит, а сервер вам ніяк не відповів.
Погодьтеся, завжди набагато краще отримати конкретну відповідь — «так» чи «ні», ніж залишатися зовсім без відповіді. Мабуть, кожен із вас стикався з подібним у повсякденному житті: рекрутери, які не відповідають, люди, що ігнорують повідомлення, кандидати, які запитали вилку зарплат й зникли.
Так от, сервер НЕ завжди посилає вас на три символи. Іноді він просто... не відповідає!
Практичний приклад
Для прикладу візьмемо такий код:
try {
const res = await fetch('http://google.com')
console.log(`Response status: ${res.status}`)
} catch (err) {
console.log(`Error message: ${err.message}`)
console.log(`Error code: ${err.cause.code}`)
}
Виконавши цей код, ви побачите в консолі повідомлення «Response status: 200».
Якщо замінити URL на якийсь неіснуючий, наприклад, http://google.com/some-unknown-url, ви побачите повідомлення «Response status: 404», оскільки така сторінка на сервері google.com не знайдена, і Google послав вас на 404.
Але! Якщо замінити URL на якийсь неіснуючий домен, наприклад, https://some-unknown-domain, то у відповіді від сервера не буде ніякого тризначного коду. Натомість з’явиться «Error code: ENOTFOUND», адже сервер взагалі не знайдено, і послати нас на три символи просто не було кому.
Цей код — ENOTFOUND — генерується на клієнті, коли відповіді від сервера (або самого сервера) немає. Це не єдиний код, який ви можете отримати. Інші доволі популярні коди:
- ETIMEDOUT — виконання запиту не вклалося в заданий час.
- ECONNABORTED — клієнт закрив з’єднання.
- ECONNRESET — сервер закрив з’єднання.
- EPIPE — ви намагалися писати в сокет, який вже був закритий на іншому кінці.
- ERR_CANCELED — відміна запиту клієнтом за допомогою AbortController чи CancelToken.
- ERR_NETWORK — проблеми з мережею (наприклад Wi-Fi).
Більше кодів помилок дивіться в «корисних посиланнях» наприкінці статті, там же інші статті, які варто прочитати.
Не відповів сервер — хіба це велика проблема?
На перший погляд здається, що ця проблема невелика: якщо HTTP-запит до якогось із наших мікросервісів впав із таймаутом, можна просто вирішити, що запит був неуспішним, і повторити його, або показати користувачеві повідомлення про помилку.
Але тут є дуже підступний нюанс: виникнення таймауту не означає, що запит був неуспішним. Він міг обробитися успішно, а таймаут виник через проблеми в інтернет-мережі під час передачі тіла відповіді від сервера до клієнта.
Усе це відбувається тому, що HTTP-запит може обірватися на будь-якому із цих етапів:
- Резолвінг доменного імені (DNS lookup) — якщо браузер або сервер не може знайти IP-адресу домену.
- Встановлення з’єднання (TCP handshake) — клієнт і сервер мають узгодити з’єднання, і тут може виникнути таймаут.
- TLS-хендшейк (якщо використовується HTTPS) — встановлення захищеного з’єднання може затримуватися або провалитися.
- Передача тіла запиту — якщо запит містить дані (наприклад, у POST або PUT), вони можуть передаватися частково або зависнути.
- Очікування відповіді від сервера — сервер може зависнути на обробці запиту або просто не відповісти вчасно.
- Передача тіла відповіді — навіть якщо сервер вже сформував відповідь, передача може бути обірвана.
Якщо запит обірвався на стадії резолвінгу доменного імені, то, звісно, він не виконався. Але в ситуаціях 5 та 6 запит міг виконатися, просто відповідь не змогла успішно дійти до клієнта.
Приклади проблем, які виникають через таймаути
Часткове створення ресурсу
Уявіть, що користувач реєструється на сайті. Він заповнює всі поля, натискає кнопку «Зареєструватися»... і бачить «Server error, please try again». Нічого страшного, думає він, і пробує ще раз. Але тепер отримує «Користувач з таким e-mail вже існує».
Що сталося? Запит на сервер частково обробився: e-mail вже записаний у базу, але через таймаут відповідь так і не дійшла до клієнта. Користувач губиться, злітає з реєстраційного шляху, можливо, навіть піде геть — і все через хитромудрі механізми роботи мережі.
Подвійне зняття грошей
Інша ситуація ще гірша. Користувач купує квиток на літак. Вводить картку, тисне «Оплатити» — таймаут. Система каже «Server error, please try again». Що робить користувач? Очевидно, пробує ще раз. А через кілька хвилин отримує два електронних квитки замість одного.
І ось тепер починається справжній квест: дзвінки в авіакомпанію, повернення коштів, комісії за скасування... Хто хоч раз займався поверненням квитків, той знає, що це може стати грою нервів і терпіння.
Саме цікаве — люди реально не помічають, що в них вже знялися гроші з карточки, і реально роблять повторні спроби. Дуже наполегливі люди можуть і три рази заплатити.
Завислі транзакції в базі даних
Запит до БД відправлений, але через таймаут клієнт не отримав відповідь. Якщо транзакція не була правильно завершена (commit/rollback), вона може блокувати таблиці або ресурси, що призведе до дедлоків або погіршення продуктивності системи.
Наскільки часто це трапляється? Це залежить від налаштувань системи та серверів і в середньому таймаути трапляються нечасто (менше ніж у 0.1% запитів). Однак у разі спонтанного високого навантаження, збоїв у сторонніх API або проблем у cloud-провайдера кількість таймаутів може зрости.
Тому не варто закривати очі на цю проблему, сподіваючись, що з вами цього не станеться, особливо якщо йдеться про високонавантажені системи або якщо ви претендуєте на статус системи з високою надійністю.
Як розробляти систему, щоб мінімізувати проблеми через таймаути
Встановлення явних таймаутів
Кожного разу, коли ваш код робить HTTP-запит або звертається до бази даних, усвідомлено встановлюйте явне та адекватне саме для вашої системи значення для таймауту. Таймаут має базуватися на очікуваному часі відповіді сервера плюс буфер. Рекомендовані підходи:
- встановлювати таймаут принаймні у 3 рази більше середнього часу відповіді сервера.
- якщо сервер зазвичай відповідає за 5 секунд, встановлюйте таймаут 15 або навіть 30 секунд, щоб уникнути помилкових обривів.
Відрізняйте таймаути від помилок
Чітко розрізняйте «неуспішний запит» і «запит, що впав із таймаутом». Не відповідайте клієнту чи іншому мікросервісу повідомленням «запит неуспішний», якщо запит впав через таймаут, адже запит міг бути успішним. Продумайте логіку: що саме ви робитимете у разі неуспішного запиту, а що — у випадку таймауту.
- Для операцій, які можуть займати тривалий час (обробка платежів, генерація звітів), краще використовувати асинхронну обробку та повертати клієнту статус «у процесі».
- Багато платіжних систем (Stripe, PayPal та інші) зазвичай мають у своїй документації цілий розділ, що описує, як вам слід діяти у разі таймауту.
Збирайте статистику таймаутів
Щоб вчасно реагувати на проблеми, важливо збирати метрики та аналізувати причини таймаутів:
- Логування кожного таймауту із деталями:
- час запиту
- сервіс, який викликали
- фактичний час відповіді
- Аналіз середнього часу відповіді та виявлення аномалій.
- Автоматичні алерти, якщо рівень таймаутів перевищує певний поріг (наприклад, 5% від загальної кількості запитів).
Що робити, коли трапився таймаут
- Обов’язково записати в лог, що сталося: код помилки (ECONNRESET чи інший), куди саме та який робився запит, скільки часу операція виконувалася. Час виконання важливий, бо:
- Якщо таймаут трапляється через 0 мілісекунд після початку запиту, проблема, ймовірно, не в сервері, а, наприклад, у мережі або клієнтській логіці.
- Якщо час виконання майже дорівнює встановленому таймауту, можливо, варто збільшити ліміт.
- Визначити, чи варто автоматично повторювати запит. Це залежить від таких факторів:
- Чи була операція ідемпотентною — чи безпечно виконувати її кілька разів без побічних ефектів? Наприклад, GET зазвичай безпечний, а POST може створювати дублі.
- Чи є у вас час на повторні спроби? Користувач не буде чекати вічно, поки ваш код робить 5 спроб завершити один HTTP-запит.
- Якщо ви вирішили автоматично повторювати запити:
- Для початку перевірте, чи не робить повторні запити та бібліотека, яку ви використовуєте. Бо, наприклад, AWS SDK автоматично робить повторні спроби, і часто немає сенсу вам робити повтори, бо бібліотека вже й так зробила три спроби.
- Скільки разів будете пробувати? Найчастіше роблять
3–5 повторних спроб. - Який інтервал між спробами — фіксований (наприклад, 2 секунди) чи експоненційно зростаючий (наприклад, 1s → 2s → 4s → 8s) — щоб зменшити навантаження на сервер, якщо він і так перевантажений.
- Чи варто припиняти спроби, якщо почали приходити інші критичні помилки (наприклад, 500 Internal Server Error)?
- Якщо ви вирішили НЕ повторювати запити:
- Коректно завершити роботу: розблокувати ресурси, якщо щось було заблоковано (наприклад, зняти блокування з бази даних).
- Повідомити користувача: повернути правильний код помилки, якщо це API (504 Gateway Timeout, наприклад), або надати осмислене повідомлення користувачу: «Час очікування вичерпано. Спробуйте пізніше».
- Застосування підходу Circuit Breaker (захисний вимикач): якщо стороння система починає віддавати дуже багато таймаутів, розумно припинити запити до неї на певний час, щоб:
- Не створювати додаткове навантаження на неї.
- Дати їй час відновитися.
- Швидше повернути клієнтам осмислену помилку, а не змушувати їх чекати кілька повторних спроб.
Як платіжні системи зазвичай працюють з таймаутом
Процесори платежів (Stripe, PayPal, Visa) мають власні механізми обробки таймаутів, які зазвичай добре задокументовані. Найчастіше, якщо запит затримується:
- він може залишитися в статусі «pending» і обробитися пізніше.
- процесор може скасувати запит і повернути статус «failed».
- може використовуватися вебхук для сповіщення продавця, якщо платіж зрештою буде підтверджено.
Одна з систем, яку ми використовували, вимагала від нас надсилати запит на скасування (revert) останнього запиту, якщо ми отримували таймаут. Причому якщо запит на revert був теж неуспішним — у вимогах було прописано, що ми маємо спробувати зробити revert ще три рази з експоненційним проміжком часу.
Чи вирішує проблему перехід на асинхронну обробку
Використання меседж-брокерів та черг дещо зменшує проблему, але не нівелює її повністю. При асинхронному підході:
- Ви переносите виконання довготривалих операцій у фоновий режим.
- Вам зручніше робити повторні спроби, оскільки у вас є додатковий час, не обмежений терпінням користувача.
- Ви можете додавати додаткові воркери, якщо черга перевантажена.
- Ви можете розподілити запити в часі, щоб уникнути надмірного навантаження на API, яке ви використовуєте (неважливо, чи це ваше власне API, чи стороннє)
Однак залишаються проблеми:
- Блокування ресурсів у разі неправильної обробки обриву зв’язку.
- Подвійне створення ресурсу, якщо операція не є ідемпотентною.
Ми стикалися з багом, коли неправильно закрите з’єднання створювало зомбі-процеси, які блокували всю чергу, оскільки ці зомбі-процеси зайняли всіх воркерів.
Ще цікаве
Під час пошуків причини одного багу ми з’ясували, що axios неправильно обробляє деякі з видів таймаутів. А саме cитуації, коли обрив зв’язку трапляється вже під час передачі відповіді від сервера клієнту. І наскільки я бачу, в Axios 1.5.0 це все ще не виправлено (see this issue). Бо це, здається, «не баг, а фіча», і вони рекомендують використовувати AbortSignal, щоб цьому запобігти.
Пишіть, якщо ви стикалися в роботі з цікавими багами, пов’язаними з таймаутами.
Бажаю завжди отримувати відповіді! А якщо не отримаєте — ви тепер знаєте, що робити :)
Корисні лінки:
- All you need to know about timeouts
- Microservices Aren’t Magic: Handling Timeouts
- The Eight Fallacies of Distributed Computing
- Handling Timeouts in microservices
- Libuv error codes
- Error codes in Axios
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів