Да, это модное слово, которое уже давно существует, реактивный. «Конечно, я знаю, что это значит, братан, я много лет занимаюсь разработкой веб-приложений!» ты, наверное, ответишь. Но так ли это на самом деле? Недавно я был честен с собой в отношении этой темы и обнаружил, что на самом деле не знаю, как это работает и какие преимущества предлагает.
Очень важно понимать последние волны этого меняющегося технического слова, потому что стек, который вы в настоящее время разрабатываете, может быстро устареть, и вы можете в конечном итоге стать старомодным, носить бороду, трансформирующую личность из кофе в код, окруженную одним и тем же порода среды обитания зоны комфорта.
Хотите лучшей участи? Бьюсь об заклад, вы! Я тоже, так что давай запрыгнем на этот реактивный поезд или даже на ракету, которая какое-то время существует.
4 типа обслуживания запросов
Чтобы понять, какие проблемы он нацелен на решение, и, таким образом, иметь возможность выбрать более мудрые технологии для вашего следующего проекта, давайте обсудим 4 существующие модели обработки запросов:
- Процесс - 1 процесс = 1 запрос
- Тема - 1 тема = 1 запрос.
- Реактивный - 1 процесс = X запросов
- Реактивные совместные подпрограммы - 1 поток = X запросов
1. Процесс
Это простейшая модель обслуживания параллельных запросов. Самая маленькая сущность здесь - это процесс.
Основы:
- Каждый запрос обслуживается в отдельном процессе
- Процесс заблокирован до завершения запроса
Плюсы:
- Простота настройки и ввода кода, так как память не используется совместно
Минусы:
- Нет общей памяти, для обмена данными требуются внешние инструменты
- В каждом процессе требуется копия кода фреймворка / пользователя
- Требуются отдельные сокеты ввода-вывода
- Накладные расходы на память, накладные расходы на создание процесса
- Дорогие переключатели контекста ЦП
Примеры технологий: Apache mod_php, PHP-FPM, Python uWCGI.
2. Резьба
Более легкий, где каждый запрос обслуживается отдельным потоком.
Основы:
- Каждый запрос обслуживается в отдельном потоке
- Поток заблокирован до завершения запроса
Плюсы:
- Общая память и сокеты ввода-вывода между серверами запросов
- Одна копия кода фреймворка / пользователя для всего сервера
- Меньший объем памяти для потоков
Примеры технологий: Java Tomcat
Минусы:
- Требуется синхронизация потоков в коде пользовательского пространства
- Все еще дорогие переключатели контекста ЦП
- По-прежнему теряется память для стека потоков
3. Реактивный
Здесь один процесс обслуживает множество запросов, «ожидая» завершения внешних операций ввода-вывода.
Основы:
- Каждый запрос обслуживается в отдельном процессе
- Процесс не блокируется до завершения запроса и может принимать новый, когда предыдущий бездействует из-за ввода-вывода.
Плюсы:
- Никаких переключателей контекста ЦП
- Отсутствие синхронизации потоков в хлопотах пользовательского пространства
- Малый объем памяти
- Возможность достижения более высокой производительности по сравнению с процессом / потоком в большинстве приложений.
Минусы:
- Требуется, чтобы весь код ввода-вывода языка, пользователя и библиотек был неблокирующим для достижения производительности
- Будет еще менее производительным, если много работы придется выполнять за пределами ввода-вывода.
- По-прежнему требуется запуск нескольких процессов / экземпляров для использования современных систем
- Адский обратный вызов / асинхронные проблемы
Примеры технологий: NodeJS, Python Tornado.
4. Реактивные совместные подпрограммы
Самый производительный, один поток, ожидая внешнего ввода-вывода, обслуживает другие запросы.
Основы:
- Каждый запрос обслуживается в отдельном потоке
- Поток не блокируется до завершения запроса и может принимать новый, когда предыдущий бездействует из-за ввода-вывода.
Плюсы:
- Общая память и сокеты ввода-вывода между серверами запросов
- Одна копия кода фреймворка / пользователя для всего сервера
- Малый объем памяти для потоков
- Меньше переключений контекста ЦП
- Не будет такого огромного снижения производительности, если какая-то часть кода не использует асинхронный ввод-вывод.
Минусы:
- По-прежнему требуется код синхронизации потоков
- Библиотеки, пользовательский код требуют поддержки асинхронного ввода-вывода в качестве реактивного для достижения максимальной производительности.
Примеры технологий: Golang, Java Spring WebFlux, Java Micronaut.
Что делает реактивный паттерн работоспособным?
Вообще говоря, принцип реактивного паттерна состоит в том, чтобы максимально избегать переключений контекста ЦП, точнее:
- меньше переключений контекста ЦП из-за небольшого количества потоков
- меньше переключений контекста ЦП из-за опроса сокетов ввода-вывода вместо аппаратных прерываний
- меньшее использование памяти из-за общей памяти между потоками
Хотя последнее знакомо большинству людей, которые разрабатывали в многопоточной среде, но вы говорите, переключатели контекста ЦП? Что за фигня?
Исторически это была эволюция от систем без многозадачности, таких как MS-DOS, к многозадачности, такой как Windows. По сути, операционная система устанавливает таймер для переключения с одного процесса / потока на другой. В этом процессе он сохраняет / восстанавливает в стеке состояние выполнения каждого процесса или потока, которые в данный момент загружены в память, плюс каждый раз, когда он это делает, он также имеет состояние выполнения ядра, которое также сохраняется / восстанавливается в стеке.
Как вы понимаете, этот процесс довольно дорогостоящий. Таким образом, минимизация переключений контекста повышает производительность.
И да, у нас есть первое предварительное условие для повышения производительности: меньше потоков. Здесь действует довольно простая формула: меньше процессов / потоков = меньше переключений контекста. Пример настройки Tomcat: ограничение в 100 потоков, размер пула 25 потоков. С реактивным шаблоном пример настройки будет: ядра ЦП * 2.
100 против 8 на экземпляре среднего размера. Требуется в 12 раз меньше переключений контекста ЦП. Чистая победа.
Вторым условием повышения производительности является опрос ввода-вывода и отсутствие аппаратных прерываний, следовательно, отсутствие переключений контекста ЦП для обслуживания этих прерываний.
Аппаратные прерывания функционируют таким образом, что после того, как данные становятся доступными для чтения в каком-либо сокете ввода-вывода, ЦП выдает прерывание, обрабатываемое ядром ОС, которое, используя ту же технику push / pop стека, сначала берет на себя управление, а затем передает управление процессу, ответственному за этот сокет. Это делается снова с помощью переключения контекста.
Говорят, аппаратные прерывания лучше. Фактически он был разработан с идеей замены традиционного опроса, поэтому он был разработан, чтобы навсегда заменить опрос. Почему тогда мы возвращаемся к голосованию? По-видимому, это хорошо для таких случаев, как ожидание ввода пользователя из канала STDIN, но не лучше для сред, в которых современные веб-серверы работают в коде, привязанном к вводу-выводу.
Веб-серверу приходится иметь дело с множеством параллельных сокетов ввода-вывода. Он читает запросы браузера из сети, записывает / читает в сокет базы данных, может быть распределенным кеширующим сокетом, записывает ответ обратно. А модель веб-опроса оказалась намного более производительной, потому что веб-сервер получает много состояний готовности ввода-вывода, поэтому стратегия немедленного обслуживания с ценой переключения контекста ЦП не так эффективна. С другой стороны, опрос происходит потому, что мы все еще придерживаемся концепции прерывания по таймеру и избегаем аппаратных прерываний.
Так какой же выбрать? Вы должны решить. Реактивные совместные подпрограммы лучше на бумаге, но в многопоточной среде всегда было труднее кодировать.
Мое эмпирическое правило - если ваше приложение требует высокой пропускной способности и высокой производительности, то ответом являются реактивные совместные подпрограммы, в противном случае я бы - ради простоты и скорости разработки - предпочел бы только реактивные .