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

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

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

Це найпереконливіша робота курсу, бо результат тут неможливо переказати, його видно на графіку. Одна й та сама задача, три моделі вводу-виводу, однакове навантаження і три різні точки, у яких усе ламається.

Три сервери з однаковою поведінкою: віддають фіксовану відповідь на GET /, слухають порт із аргументу.

Terminal window
./srv-threads 8080
./srv-epoll 8080
./srv-uring 8080

Плюс звіт із вимірами й поясненням.

  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. Порівняти з одним циклом подій на всіх ядрах.