Что такое сетевой сервис? Это программа, которая принимает входящие запросы по сети и обрабатывает их, возможно, возвращая ответы. Есть много аспектов, в которых сетевые сервисы отличаются друг от друга. В этой статье я акцентрирую внимание на способе обработки входящих запросов. Выбор способа обработки запросов имеет далеко идущие последствия. Как сделать чат-сервис, выдерживающий 100.000 одновременных соединений? Какой подход выбрать для извлечения данных из потока слабоструктурированных файлов? Неправильный выбор приведет к пустой трате сил и времени. В статье рассмотрены такие подходы как пул процессов/потоков, событийно-ориентированная обработка, half sync/half async паттерн и многие другие. Приводятся многочисленные примеры, рассматриваются плюсы и минусы подходов, их особенности и области применения. Тема способов обработки запросов не нова, смотрите, например: один, два. Однако большинство статей рассматривают её лишь частично. Данная статья призвана заполнить пробелы и привести последовательное изложение вопроса. Будут рассмотрены следующие подходы: Нужно обратить внимание, что сервис, обрабатывающий запросы — это не обязательно сетевой сервис. Это может быть сервис, который получает новые задачи из БД или очереди задач. В данной статье подразумеваются именно сетевые сервисы, но нужно понимать, что рассматриваемые подходы имеют более широкую область применения. В конце статьи приводится список с кратким описанием каждого из подходов. Приложение состоит из единственного потока в единственном процессе. Все запросы обрабатываются только последовательно. Параллелизма нет. Если к сервису приходит одновременно несколько запросов, один из них обрабатывается, остальные попадают в очередь. Плюс данного подхода в простоте реализации. Нет никаких блокировок и конкуренции за ресурсы. Очевидный минус — невозможность масштабироваться при большом количестве клиентов. Приложение состоит из основного процесса, который принимает входящие запросы, и рабочих процессов. На каждый новый запрос основной процесс создает рабочий процесс, который обрабатывает запрос. Масштабирование по количеству запросов простое: каждый запрос получает свой собственный процесс. В этой архитектуре тоже нет ничего сложного, но у неё есть проблемы ограничения: Эти проблемы отнюдь не являются стоповыми. Ниже будет показано, как они обходятся в РСУБД PostgeSQL. Плюсы этой архитектуры: Примеры: В целом нужно сказать, что этот подход имеет свои преимущества, которые обуславливают его область применения, но возможности масштабирования сильно ограничены. Этот подход во многом похож на предыдущий. Отличие в том, что вместо процессов используются потоки. Это позволяет использовать общую память "из коробки". Однако другие плюсы предыдущего подхода использовать уже не получится, в то время как потребление ресурсов так же будет высоким. Плюсы: Минусы: В качестве примера использования можно привести MySQL. Но нужно заметить, что в MySQL используется смешанный подход, поэтому этот пример будет рассмотрен в следующем разделе. Потоки (процессы) создавать дорого и долго. Чтобы не тратить ресурсы впустую, можно использовать один и тот же поток многократно. Ограничив дополнительно максимальное количество потоков, получим пул потоков (процессов). Теперь основной поток принимает входящие запросы и складывает их в очередь. Рабочие процессы берут запросы из очереди и обрабатывают. Этот подход можно воспринимать как естественное масштабирование последовательной обработки запросов: каждый рабочий поток может обрабатывать потоки только последовательно, объединение их в пул позволяет обрабатывать запросы параллельно. Если каждый поток может обрабатывать 1000 rps, то 5 потоков будут обрабатывать нагрузку близкую к 5000 rps (при условии минимальной конкуренции за общие ресурсы). Пул может быть создан заранее при старте сервиса или формироваться постепенно. Использование пула потоков более распространено, т.к. позволяет применять общую память. Размер пула потоков не обязательно должен быть ограничен. Сервис может использовать свободные потоки из пула, а если таких нет — создавать новый поток. После завершения обработки запроса поток присоединяется к пулу и ожидает следующего запроса. Данный вариант — комбинация подхода поток на запрос и пул потоков. Ниже будет приведен пример. Плюсы: Минусы: Примеры: Пожалуй, это один из наиболее распространенных подходов к построению сетевых сервисов, если не самый распространенный. Он позволяет хорошо масштабироваться, достигая больших rps. Основное ограничение подхода — количество одновременно обрабатываемых сетевых соединений. Фактически этот подход работает хорошо, только если запросы короткие или клиентов мало. Две парадигмы — синхронная и асинхронаая — вечные конкуренты друг друга. До сих пор речь шла только о синхронных подходах, но было бы неправильно игнорировать асинхронный подход. Событийно ориентированная или реактивная обработка запросов — это подход, в котором каждая IO операция выполняется ассинхронно, а по окончании выполнения операции вызывается обработчик (handler). Как правило, обработка каждого запроса состоит из множества асинхронных вызовов с последующим выполнением handler'ов. В каждый конкретный момент однопоточное приложение выполняет код только одного handler'а, но выполнение handler'ов различных запросов чередуется между собой, что позволяет одновременно (псевдопараллельно) обрабатывать множество параллельных запросов. Полное рассмотрение этого подхода выходит за рамки этой статьи. Для более глубокого ознакомления можно порекомендовать Reactor (Реактор), В чем секрет скорости NodeJS?, Inside NGINX. Здесь ограничимся только рассмотрением плюсов и минусов данного подхода. Плюсы: Минусы: Примеры: Название взято из книги POSA: Patterns for Concurrent and Networked Objects. В оригинале этот паттерн трактуется очень широко, однако для целей данной статьи я буду понимать этот паттерн несколько уже. Half sync/half async — это подход к обработке запросов, в котором для каждого запроса используется легковесный поток управления (зеленый поток). Программа состоит из одного или нескольких потоков уровня операционной системы, однако система исполнения программы поддерживает зеленые потоки, которые ОС не видит и которыми не может управлять. Несколько примеров, чтобы сделать рассмотрение конкретнее: В сущности этот подход призван совместить высокую производительность асинхронного подхода с простотой программирования синхронного кода. При использовании этого подхода, несмотря на иллюзию синхронности, программа будет работать асинхронно: система исполнения программы будет управлять event loop'ом, а каждая "синхронная" операция на самом деле будет асинхронной. При вызове такой операции система исполнения будет вызывать асинхронную операцию с помощью средств ОС и регистрировать handler завершения выполнения операции. Когда асинхронная операция будет завершена, система исполнения вызовет ранее зарегестрированный handler, который продолжит выполнение программы в точке вызова "синхронной" операции. В результате подход half sync/half async содержит в себе как некоторые плюсы, так и некоторые минусы асинхронного подхода. Объем статьи не позволяет рассмотреть этот подход во всех деталях. Интересующимся советую прочесть одноименную главу в книге POSA: Patterns for Concurrent and Networked Objects. Сам по себе подход half sync/half async вводит новую сущность "зеленый поток" — легковесный поток управления на уровне системы исполнения программы или библиотеки. Что делать с зелеными потоками — выбор программиста. Он может использовать пул зеленых потоков, может создавать новый зеленый поток на каждый новый запрос. Разница по сравнению с потоками/процессам ОС в том, что зеленые потоки намного дешевле: они расходуют гораздо меньше оперативной памяти и создаются намного быстрее. Это позволяет создавать огромное количество зеленых потоков, например, сотни тысяч в языке Go. Такое огромное количество делает оправданным использование подхода "зеленый поток на запрос". Плюсы: Минусы: В зависимости от реализации этот подход хорошо масштабируется по ядрам CPU (Golang) или не масштабируется вовсе (Python).
Этот подход так же, как и асинхронный, позволяет обрабатывать большое количество одновременных соединений. Но программировать сервис с использованием этого подхода проще, т.к. код пишется в синхронном стиле. Как можно догадаться из названия, в этом подходе запросы обрабатываются по конвейеру. Обрабатывающий процесс состоит из нескольких потоков ОС, выстроенных в цепочку. Каждый поток — это звено цепочки, он выполняет определенное подмножество операций, необходимых для обработки запроса. Каждый запрос последовательно проходит через все звенья цепочки, а разные звенья в каждый момент времени обрабатывают разные запросы. Плюсы: Минусы: Примеры: Конвейерная обработка широко используется, однако чаще всего звеньями являются отдельные компоненты в независимых процессах, которые обмениваются сообщениями, например, через очередь сообщений или БД. Краткая сводка рассмотренных подходов: Список выше не является исчерпывающим, но он содержит основные подходы к обработке запросов. Обращаюсь к читателю: какие подходы используете Вы? Какие плюсы и минусы, особенности их работы Вы узнали на собственном опыте? Разработчик высоконагруженных систем