A6. Зворотний проксі й балансувальник
Перед кожним помітним сайтом стоїть зворотний проксі (reverse proxy): nginx, HAProxy, Envoy або хмарний балансувальник. Він приймає клієнтські з’єднання, вибирає бекенд, пересилає запит і повертає відповідь. Поки всі бекенди живі, завдання здається тривіальним. Справжня робота починається, коли один бекенд убито, інший завис, а третій відповідає, але віддає лише помилки. Модуль 14 пояснює, як HTTP/1.1 розмежовує запити й відповіді у відкритому з’єднанні (і чому без цього проксі не обійтися), а модуль 11 пояснює, що бачить TCP, коли процес на іншому кінці помер.
Після цієї роботи ви зможете:
- написати HTTP/1.1-проксі, що правильно відділяє запити один від одного
через
Content-Lengthі тримає з’єднання з клієнтом відкритим; - реалізувати round-robin і активну перевірку здоров’я, що виключає бекенди, які лежать чи віддають помилки, і повертає їх після одужання;
- відрізнити, коли запит безпечно повторити на іншому бекенді (
GETдо першого байта відповіді), а коли ні (POST); - поставити таймаути так, щоб завислий бекенд давав клієнтові
504, а не вічне очікування, і щоб повільний клієнт не зупиняв інших; - пояснити різницю між
502,503і504.
Ці самі рішення приймає кожен, хто налаштовує upstream у nginx або
backend-сервіс в HAProxy, а помилки в них тягнуть за собою інцидент
виду «один вузол упав і поклав усе».
Завдання
Section titled “Завдання”Написати програму lb: на C або на Python, за вибором. Чекер запускає
виконуваний файл, тому для Python потрібен shebang і chmod +x.
Готові проксі й HTTP-бібліотеки рівня сервера й клієнта, що роблять
усю роботу за вас (http.server як основа проксі, requests, aiohttp,
libcurl), заборонені: пересилання, розбір заголовків і таймаути мають
бути вашими. Сокети, select, poll, epoll, потоки й розбір рядків
дозволені.
./lb 8080 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003Перший аргумент — порт, що слухає балансувальник, решта — бекенди
у вигляді адреса:порт.
Що має вміти:
- Проксі. Приймати HTTP/1.1-запити (тіло запиту — лише з
Content-Length;Transfer-Encodingу запиті можна відхилити з501), пересилати їх бекенду й повертати відповідь клієнтові байт у байт. Відповіді бекенда можуть матиContent-Length, бутиchunkedабо закінчуватися закриттям з’єднання; 1 МіБ тіла відповіді й 300 КБ тіла запиту мають пройти цілими. - Заголовки. Не пересилати заголовки «від вузла до вузла»
(
Connection,Keep-Alive,Proxy-Connection,TE,Trailer,Transfer-Encoding,Upgrade). ДодаватиX-Forwarded-Forз адресою клієнта (дописувати в кінець, якщо він уже є).Hostлишати таким, яким його прислав клієнт. - З’єднання з клієнтом. Тримати його відкритим між запитами
(keep-alive), закривати після відповіді, коли клієнт просив
Connection: closeабо надіслав HTTP/1.0. - Розподіл. Round-robin по справних бекендах: 30 запитів на трьох бекендах дають по 10, строго по колу.
- Здоров’я. Щонайменше раз на секунду на кожен бекенд
GET /health; відповідь200означає «справний», усе інше (інший статус, відмова в з’єднанні, відсутність відповіді за секунду) означає «несправний». Бекенд, що лежить або нездоровий, має випасти з ротації за 4 с, а одужавши, повернутися за 5 с. Перемикання не повинно вимагати перезапуску балансувальника. - Повтор. Якщо до початку відповіді бекенд недоступний (відмова
в з’єднанні, обрив),
GETбез тіла повторюється на наступному справному бекенді, і клієнт не бачить помилки. Запит із тілом (POST) автоматично не повторюється: клієнт отримує502. Бекенд, з яким не вдалося з’єднатися або який обірвав з’єднання, одразу позначається несправним, не чекаючи наступної перевірки. - Таймаути. Бекенд, що не надіслав заголовків відповіді за 3 секунди,
дає клієнтові
504(допускається502), а інші клієнти тим часом обслуговуються. Клієнт, що надіслав половину запиту й замовк, не блокує нікого. - Немає жодного справного бекенда.
503за обмежений час, без падіння; коли бекенди повернулися, балансувальник сам відновлює роботу. - Стійкість. Сміття замість запиту, заголовок на 100 КБ, клієнт, що розірвав з’єднання посеред відповіді, не валять процес.
Чого робити не треба. HTTPS, HTTP/2, WebSocket, кешування, вагу бекендів,
липкі сесії, пересилання chunked-запитів, конфігураційні файли.
Підтримку keep-alive між проксі й бекендом теж не вимагає чекер:
це завдання для заміру на останньому етапі.
Готово, коли:
./check.sh ./lbпроходить усі перевірки;- у звіті є відповіді на запитання з етапів 3, 5 і 6 і порівняння пропускної здатності з прямим бекендом та через балансувальник.
Перед початком
Section titled “Перед початком”- Прочитайте в модулі 14, як HTTP/1.1 визначає межі
повідомлення (
Content-Length,chunked, закриття з’єднання), що такеkeep-aliveі які заголовки не передаються через проксі. У модулі 11 подивіться, як поводиться TCP, коли інший кінець не відповідає (таймаут) і коли відмовляє (RST). - Розпакуйте архів курсу: чекер лежить у
labs/a6-load-balancer/check.sh, тестовий бекенд уbackend.py. - Знадобляться
gccчиpython3, для вимірюваньcurlтаabабоwrk. Права root не потрібні, усе відбувається на127.0.0.1.
-
Проксі для одного бекенда.
Поки без round-robin і здоров’я. Прочитайте запит до порожнього рядка, розберіть рядок запиту й заголовки, з’єднайтеся з бекендом, перешліть запит, прочитайте заголовки відповіді й перешліть разом із тілом. Для власних дослідів запустіть тестові бекенди:
Terminal window python3 backend.py 9001 a &python3 backend.py 9002 b &curl -si localhost:9001/ # прямоcurl -si localhost:8080/ # через ваш проксіБекенд віддає
/(ім’я в тілі й в заголовкуX-Backend),/health,/hang(мовчить 30 с),/big(1 МіБ),/headers(показує, що дійшло) іPOST /upload(довжина йsha256тіла). -
Межі повідомлень.
Тіло запиту з
Content-Lengthпересилайте рівно стільки байтів. Тіло відповіді: заContent-Length, заchunkedабо до закриття. Рядок запиту може прийти частинами, а разом із заголовками в буфері опиниться і початок тіла: його не можна загубити. -
Keep-alive з клієнтом.
Після відповіді чекайте наступного запиту в тому самому з’єднанні. Запишіть у звіт: що станеться, якщо проксі відповість без
Content-Length, не закриваючи з’єднання, і як клієнт дізнається, що відповідь закінчилась. -
Round-robin.
Індекс наступного бекенда, який рухається по колу по справних. Перевірте під одночасними запитами: лічильник, що рухається без синхронізації, перекошує розподіл. Порахуйте, скільки запитів отримав кожен бекенд.
-
Здоров’я й повтор.
Окремий потік або таймер: кожні півсекунди
GET /healthіз таймаутом. Дві невдачі підряд — бекенд поза ротацією; одна вдача — повертається. Окремо: якщоconnectдо бекенда не вдався, а запит безпечний, позначте його несправним і спробуйте наступного.Запишіть у звіт: чому одного лише повтору без перевірки здоров’я мало (подумайте про бекенд, який відповідає
500на кожен запит) і чому одна лише перевірка без повтору теж дає помилки клієнтам (скільки запитів пролетить між падінням бекенда й наступною перевіркою). -
Таймаути.
Таймаут на з’єднання з бекендом (секунда) і на очікування заголовків відповіді (три секунди). Завислий
/hangмає дати504, а інші запити тим часом мають проходити. Повільний клієнт не повинен займати ні потік, який нікому не дістанеться, ні цикл подій.Запишіть у звіт: які таймаути ви поставили й чому саме такі, і що буде з бекендом, якщо ставити їх у десять разів більшими.
-
Заміри (за бажанням).
Порівняйте запити за секунду напряму до бекенда й через балансувальник (
ab -n 5000 -c 50 http://127.0.0.1:8080/), а потім те саме з повторним використанням з’єднань з бекендами (keep-alive між проксі й бекендом): скільки затримки забирає кожне нове з’єднання.
Перевірка
Section titled “Перевірка”cd labs/a6-load-balancer./check.sh ./lbЧекер запускає три тестові бекенди backend.py на вільних портах і ваш
балансувальник перед ними, а потім проходить сценарій. Повний прогін
триває близько хвилини й робить такі перевірки:
- проксі:
GET,POSTна 300 КБ, відповідь на 1 МіБ (заsha256),X-Forwarded-ForіHost, три запити в одному з’єднанні,Connection: close; - розподіл: 30 послідовних запитів по колу, 300 у десять потоків у межах ±20%;
- повільний клієнт із половиною запиту;
/hangі504за 3 с при тому, що решта запитів проходить; - падіння бекенда (
SIGKILL): перші 12 запитів після цього без жодної помилки, розподіл між двома живими; - «хворий» бекенд, що живий, але
/віддає500, а/health—503: виключення з ротації за 4 с; одужання й повернення за 5 с; перезапуск убитого; - усі бекенди вбито:
503і живий процес; відновлення після повернення; - сміття замість запиту, заголовок на 100 КБ, клієнт, що пішов посеред відповіді.
Чекер не залежить від мови, моделі вводу-виводу (потоки, epoll,
корутини), від того, як часто ви перевіряєте здоров’я (у межах указаних
часів), від кількості з’єднань з бекендами і від того, як ви
розбираєте заголовки.
Часті помилки
Section titled “Часті помилки”Запит розбирається одним recv. Рядок запиту чи тіло приходять
частинами. Читайте до \r\n\r\n, а що лишилося в буфері після заголовків,
належить тілу або наступному запитові.
Пересилаються заголовки Connection і Transfer-Encoding. Вони описують
одне з’єднання, а не запит. Якщо переслати Connection: close від
клієнта на бекенд, бекенд закриє з’єднання, а проксі подумає, що відповідь
закінчилась достроково.
Content-Length не збігається з тілом. Проксі віддає клієнту лише
частину тіла, а решту клієнт прочитає як початок наступної відповіді.
Повторюється POST. Якщо бекенд встиг обробити запит і впав, не
надіславши відповіді, повтор створить замовлення вдруге. Повторювати
можна тільки те, що безпечно.
Здоров’я лише за TCP-з’єднанням. Бекенд може приймати з’єднання, але
віддавати 500. Перевіряйте відповідь, а не сам факт з’єднання.
Вузол лишається «несправним» назавжди. Після виключення перевірка має продовжувати його опитувати, інакше одужати він не зможе.
Таймаут на весь запит замість таймауту на мовчання. Завантаження
великого файла за три секунди теж закінчиться 504. Рахуйте час
очікування заголовків і паузи між шматками, а не сумарну тривалість.
Один потік на все і блокуючий connect. Недоступний бекенд, що не
відповідає на SYN, зупинить усіх клієнтів на час таймауту з’єднання.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Додати keep-alive між проксі й бекендом (пул з’єднань, повтор, коли
з’єднання з пулу виявилось мертвим), а також ваги бекендів і алгоритм
«найменше активних запитів». Порівняти з
nginx у ролі балансувальника і побачити, яких можливостей йому бракує
без платної версії (активна перевірка здоров’я). Додати Retry-After
на 503, обмеження кількості запитів з однієї адреси й метрики у
вигляді простої сторінки стану.