A7. HTTP-сервер у трьох редакціях
Сервер на сотню з’єднань можна написати як завгодно. На десяти тисячах
модель вводу-виводу стає головним рішенням: потік на з’єднання
впирається в пам’ять стеків і перемикання контексту, цикл подій
на epoll віддає тисячі з’єднань одному потоку, io_uring прибирає
системний виклик на кожну операцію. Модуль 13 порівнює ці
механізми в таблиці, модуль 8 пояснює, чому потік
на кожного клієнта не масштабується. Тут ви напишете всі три сервери,
навантажите їх однаково і побачите на графіку, де ламається кожен.
Після цієї роботи ви зможете:
- написати HTTP-сервер у трьох моделях: потік на з’єднання, цикл подій
на
epollіio_uringчерезliburing; - порахувати, скільки пам’яті займуть стеки потоків на 10 000 з’єднань,
і перевірити це заміром
RSS; - пояснити, чому в циклі подій один блокуючий виклик зупиняє всіх клієнтів, і показати це перевіркою з повільним клієнтом;
- зняти під навантаженням запити за секунду, p50, p99,
RSSі кількість контекстних перемикань і відрізнити межу моделі від ліміту машини; - прочитати три графіки й сказати, на скількох з’єднаннях і чому зламалася кожна модель.
У роботі це розбір інциденту «сервер перестав відповідати під навантаженням, хоча процесор вільний» і вибір моделі для нового сервісу: після цієї роботи ви знатимете, у що впирається кожна.
Завдання
Section titled “Завдання”Написати три HTTP-сервери з однаковою поведінкою і звіт із замірами під однаковим навантаженням.
./srv-threads 8080./srv-epoll 8080./srv-uring 8080Що має вміти кожен із трьох:
- слухати порт із першого аргументу і на
GET /віддавати фіксовану відповідьHTTP/1.xзі статусом200; - обслуговувати 20 послідовних і 100 одночасних з’єднань без втрат;
- не падати на битих запитах: порожньому, на сміття замість HTTP, на рядку запиту в 40 000 байтів і на обриві посеред заголовків;
- збирати запит, який приходить частинами з паузами, до порожнього рядка;
- відповідати іншим клієнтам, поки один підключився і нічого не надсилає;
- закривати дескриптори: після 200 запитів їх кількість
у
/proc/<pid>/fdне зростає.
Етап 2 додає до першої моделі варіант із пулом потоків, його заміряють окремо.
Чого робити не треба. Розбирати HTTP цілком, віддавати файли,
підтримувати keep-alive, HTTPS чи інші методи не потрібно: відповідь
одна й та сама на будь-який коректний GET /, з’єднання після неї
можна закривати.
Обмеження. C і системні виклики: сокети, pthread, epoll,
liburing. Готові HTTP-бібліотеки не використовувати: модель
вводу-виводу має бути написана вами. Автоперевірці потрібен python3.
Що записати у звіт:
- розмір стека з
ulimit -s, помножений на 10 000 з’єднань, і фактичнийRSSпершого сервера на цій точці; ulimit -nіnet.ipv4.ip_local_port_range, які ви виставили для сервера і клієнта;- для кожної моделі на 100, 1 000, 5 000 і 10 000 з’єднань: запити
за секунду, p50, p99,
RSS,voluntary_ctxt_switchesіnonvoluntary_ctxt_switches; - три графіки: «з’єднання → запити/с», «з’єднання → p99», «з’єднання → пам’ять»;
- де зламалася кожна модель і чому; якщо клієнт працював на тій самій машині, що й сервер, це теж треба написати.
Готово, коли:
./check.sh ./srv-epoll 8080проходить усі дванадцять перевірок у семи групах, і те саме для./srv-threadsі./srv-uring;- заміри зняті на всіх чотирьох точках навантаження для кожної моделі;
- у звіті є п’ять пунктів вище.
Перед початком
Section titled “Перед початком”- Прочитайте в модулі 13 розділ «Блокуючий і неблокуючий ввід-вивід» разом з Aside «Неблокуючий не означає асинхронний», а в модулі 8 розділи «Конкурентність без потоків» і «Скільки потоків».
- Розпакуйте архів курсу: перевірка лежить
у
labs/a7-http-server/check.sh; без другого аргументу порт буде 18080. - Знадобляться
liburing-dev,abзapache2-utilsабоwrk, а також піднятийulimit -n:setup/provision.shвиставляє 65536, після чого треба перезайти в систему. Підійде будь-який Linux, зокрема контейнер і WSL2; для абсолютних чисел клієнт краще запускати на іншій машині.
-
Потік на з’єднання.
acceptу циклі, на кожне з’єднанняpthread_create. Найпростіший і найзрозуміліший варіант, тому з нього починаємо.Одразу порахуйте, скільки пам’яті займуть стеки на 10 000 з’єднань при вашому
ulimit -s. Це число знадобиться у звіті. -
Пул потоків.
Та сама модель, але потоки не створюються на кожне з’єднання. Заміряйте окремо: частина накладних витрат зникає, а обмеження лишається.
-
epoll.Один потік, неблокуючі сокети, цикл подій. Не забудьте, що
acceptтеж має бути неблокуючим і що дані можуть прийти частинами.Порівняйте з першою версією не лише за пропускною здатністю, а й за пам’яттю на з’єднання.
-
io_uring.Заявки на
accept,recvіsendподаються в кільце, результати забираються з другого. Використайтеliburing, писати сирі системні виклики не обов’язково. -
Вимірювання.
Навантаження створюйте
wrk,abабо власним клієнтом. Обов’язкові чотири точки: 100, 1 000, 5 000 і 10 000 одночасних з’єднань.Для кожної точки фіксуйте: запитів за секунду, затримку p50 і p99,
RSSсервера, кількість контекстних перемикань (/proc/<pid>/status, поляvoluntary_ctxt_switchesіnonvoluntary_ctxt_switches). -
Звіт.
Три графіки: «з’єднання → запити/с», «з’єднання → p99», «з’єднання → пам’ять». І текст: де зламалася кожна модель і чому.
Автоперевірка
Section titled “Автоперевірка”Перевірка лежить в архіві з файлами курсу, і команди нижче виконуються з розпакованого каталогу.
cd labs/a7-http-server./check.sh ./srv-epoll 8080Перевірка запускає сервер, надсилає коректні й навмисно биті запити, перевіряє відповіді, тримає повільне з’єднання і контролює, що інші клієнти при цьому обслуговуються.
Часті помилки
Section titled “Часті помилки”Читання одним read. TCP не зобов’язаний віддати запит цілком, тож
потрібен цикл до порожнього рядка.
Дескриптори течуть. Забули close на шляху помилки. Побачити можна
через ls /proc/<pid>/fd | wc -l під навантаженням.
EAGAIN вважають помилкою. На неблокуючому сокеті це нормальна
відповідь «зараз даних немає».
Один повільний клієнт зупиняє всіх. У версії з epoll це означає,
що десь усередині циклу подій ви зробили блокуючий виклик.
Заміри на тій самій машині. Клієнт конкуруватиме з сервером за процесор. Так робити можна, якщо чесно написати про це у звіті, але для абсолютних чисел цей спосіб не годиться.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”Додати SO_REUSEPORT і кілька процесів із власним epoll у кожному:
цю модель використовує nginx. Порівняти з одним циклом подій
на всіх ядрах.