Перейти до вмісту

A7. HTTP-сервер у трьох редакціях

просунутийспирається на модуль 13, модуль 8

Сервер на сотню з’єднань можна написати як завгодно. На десяти тисячах модель вводу-виводу стає головним рішенням: потік на з’єднання впирається в пам’ять стеків і перемикання контексту, цикл подій на epoll віддає тисячі з’єднань одному потоку, io_uring прибирає системний виклик на кожну операцію. Модуль 13 порівнює ці механізми в таблиці, модуль 8 пояснює, чому потік на кожного клієнта не масштабується. Тут ви напишете всі три сервери, навантажите їх однаково і побачите на графіку, де ламається кожен.

Після цієї роботи ви зможете:

  • написати HTTP-сервер у трьох моделях: потік на з’єднання, цикл подій на epoll і io_uring через liburing;
  • порахувати, скільки пам’яті займуть стеки потоків на 10 000 з’єднань, і перевірити це заміром RSS;
  • пояснити, чому в циклі подій один блокуючий виклик зупиняє всіх клієнтів, і показати це перевіркою з повільним клієнтом;
  • зняти під навантаженням запити за секунду, p50, p99, RSS і кількість контекстних перемикань і відрізнити межу моделі від ліміту машини;
  • прочитати три графіки й сказати, на скількох з’єднаннях і чому зламалася кожна модель.

У роботі це розбір інциденту «сервер перестав відповідати під навантаженням, хоча процесор вільний» і вибір моделі для нового сервісу: після цієї роботи ви знатимете, у що впирається кожна.

Написати три HTTP-сервери з однаковою поведінкою і звіт із замірами під однаковим навантаженням.

Terminal window
./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;
  • заміри зняті на всіх чотирьох точках навантаження для кожної моделі;
  • у звіті є п’ять пунктів вище.
  • Прочитайте в модулі 13 розділ «Блокуючий і неблокуючий ввід-вивід» разом з Aside «Неблокуючий не означає асинхронний», а в модулі 8 розділи «Конкурентність без потоків» і «Скільки потоків».
  • Розпакуйте архів курсу: перевірка лежить у labs/a7-http-server/check.sh; без другого аргументу порт буде 18080.
  • Знадобляться liburing-dev, ab з apache2-utils або wrk, а також піднятий ulimit -n: setup/provision.sh виставляє 65536, після чого треба перезайти в систему. Підійде будь-який Linux, зокрема контейнер і WSL2; для абсолютних чисел клієнт краще запускати на іншій машині.
  1. Потік на з’єднання.

    accept у циклі, на кожне з’єднання pthread_create. Найпростіший і найзрозуміліший варіант, тому з нього починаємо.

    Одразу порахуйте, скільки пам’яті займуть стеки на 10 000 з’єднань при вашому ulimit -s. Це число знадобиться у звіті.

  2. Пул потоків.

    Та сама модель, але потоки не створюються на кожне з’єднання. Заміряйте окремо: частина накладних витрат зникає, а обмеження лишається.

  3. epoll.

    Один потік, неблокуючі сокети, цикл подій. Не забудьте, що accept теж має бути неблокуючим і що дані можуть прийти частинами.

    Порівняйте з першою версією не лише за пропускною здатністю, а й за пам’яттю на з’єднання.

  4. io_uring.

    Заявки на accept, recv і send подаються в кільце, результати забираються з другого. Використайте liburing, писати сирі системні виклики не обов’язково.

  5. Вимірювання.

    Навантаження створюйте wrk, ab або власним клієнтом. Обов’язкові чотири точки: 100, 1 000, 5 000 і 10 000 одночасних з’єднань.

    Для кожної точки фіксуйте: запитів за секунду, затримку p50 і p99, RSS сервера, кількість контекстних перемикань (/proc/<pid>/status, поля voluntary_ctxt_switches і nonvoluntary_ctxt_switches).

  6. Звіт.

    Три графіки: «з’єднання → запити/с», «з’єднання → p99», «з’єднання → пам’ять». І текст: де зламалася кожна модель і чому.

Перевірка лежить в архіві з файлами курсу, і команди нижче виконуються з розпакованого каталогу.

Terminal window
cd labs/a7-http-server
./check.sh ./srv-epoll 8080

Перевірка запускає сервер, надсилає коректні й навмисно биті запити, перевіряє відповіді, тримає повільне з’єднання і контролює, що інші клієнти при цьому обслуговуються.

Читання одним read. TCP не зобов’язаний віддати запит цілком, тож потрібен цикл до порожнього рядка.

Дескриптори течуть. Забули close на шляху помилки. Побачити можна через ls /proc/<pid>/fd | wc -l під навантаженням.

EAGAIN вважають помилкою. На неблокуючому сокеті це нормальна відповідь «зараз даних немає».

Один повільний клієнт зупиняє всіх. У версії з epoll це означає, що десь усередині циклу подій ви зробили блокуючий виклик.

Заміри на тій самій машині. Клієнт конкуруватиме з сервером за процесор. Так робити можна, якщо чесно написати про це у звіті, але для абсолютних чисел цей спосіб не годиться.

Додати SO_REUSEPORT і кілька процесів із власним epoll у кожному: цю модель використовує nginx. Порівняти з одним циклом подій на всіх ядрах.