Как медленные запросы влияют на пропускную способность

Сейчас наш сервер обрабатывает каждый запрос по очереди. Это работает для систем с небольшой загрузкой (которая получает не очень много запросов), но как только приложения становятся более сложными, такая реализация уже не будет оптимальной.

Поскольку наша текущая программа последовательно обрабатывает соединения, она не будет обработать второе соединение, пока оно не завершит обработку первого. Если мы получить один запрос, который требует много времени для обработки, запросам, поступающие во время обработки придется подождать, пока длинный запрос не будет завершен, даже если новый запрос может быть обработан быстро. Давайте посмотрим на это в действии.

Имитация медленного запроса в реализации текущего сервера

Давайте посмотрим на эффект от запроса, который требует много времени для обработки. В коде 20-10 показано, пример симуляции медленной обработки запроса. Код при ответе на запрос /sleep, сервер "заснёт" на пять секунд.

Filename: src/main.rs


# #![allow(unused_variables)]
#fn main() {
use std::thread;
use std::time::Duration;
# use std::io::prelude::*;
# use std::net::TcpStream;
# use std::fs::File;
// ...snip...

fn handle_connection(mut stream: TcpStream) {
#     let mut buffer = [0; 512];
#     stream.read(&mut buffer).unwrap();
    // ...snip...

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

    // ...snip...
}
#}

код 20-10: симуляция обработки медленного запроса

Мы создали специальный запрос sleep. При выполнении данного запроса будет 5-секундная задержка, перед тем, как отобразиться содержимое файла "hello.html".

Вы можете увидеть в реальном времени, насколько прост наш сервер. В реальных проекта может происходить и более длинная задержка.

Запустите программу командой cargo run, а затем в окне браузеры запросите данные по адресам http://localhost:8080/ и http://localhost:8080/sleep. Если вы запросите данные и строка запроса будет начинаться с / даже несколько раз - вы получите быстрый ответ. Но если вы запросите /sleep и затем попробуете ещё раз получить данные стартовой страницы - вы будете ожидать пока sleep код функции не закончит ожидания и не приступит к дальнейшей работе.

Существует несколько способов изменить работу нашего веб-сервера, чтобы избежать повторного запроса всех запросов следовавших за медленным запросом. Тот, что мы собираемся реализовать называется пулом потоков.

Улучшение пропускной способности пула потоков

Пул потоков - группа порожденных потоков, которые готовы обрабатывать некоторые задача. Когда программа получает новую задачу, один из потоков в пуле будет назначен выполнять эту задачу. Остальные потоки в пуле доступны для обработки любых других задач, которые могут возникнуть во врем работы занятого потока.

Пул потоков позволит нам одновременно обрабатывать соединения: мы можем начать обработку нового соединения до завершения старого соединения. Это увеличит пропускную способность нашего сервера.

Итак, вот что мы собираемся реализовать: вместо ожидания каждого запроса перед тем, как начать с следующей, мы отправим обработку каждого соединение с другой поток. Потоки будут поступать из пула, который мы будем создавать после запуска программы на выполнение. Причина, по которой мы ограничиваем число потоков на небольшое число (четыре) - потому, что если бы мы создавали бы новый поток для каждого запроса, то ресурсы системы были бы быстро израсходованы при увеличении количества запросов.

Вместо того, чтобы создавать неограниченное количество потоков, у нас будет фиксированное их количество в пуле. По мере поступления запросов мы будем отправлять запросы в пул для обработки. Пул будет поддерживать очередь входящих запросов. Каждый из потоков в пуле получает запрос из этой очереди, обрабатывает его, а затем запрашивает следующий. С таким дизайном мы можем обрабатывает N запросы одновременно, где N - количество потоков. Эта все равно означает, что длительные запросы N могут привести к резервному копированию запросов в очереди, но мы увеличили количество длительных запросов, которые мы можем обрабатывать до этого момента от одного до N.

Такое решение является одним из способов повысить пропускную способность нашего веб-сервера. Однако, это книга не о веб-серверах, поэтому мы не будем углубляться в проблемы реализаций. Скажем только, что способами увеличения пропускной способности является модель fork/join и модель однопоточного асинхронного ввода-выводы. Если вас интересует эта тема, вы можете больше узнать о ней и попытаться реализовать их в Rust. Rust является языком низкого уровня и может реализовать все эти модели.