Да, это модное слово, которое уже давно существует, реактивный. «Конечно, я знаю, что это значит, братан, я много лет занимаюсь разработкой веб-приложений!» ты, наверное, ответишь. Но так ли это на самом деле? Недавно я был честен с собой в отношении этой темы и обнаружил, что на самом деле не знаю, как это работает и какие преимущества предлагает.

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

Хотите лучшей участи? Бьюсь об заклад, вы! Я тоже, так что давай запрыгнем на этот реактивный поезд или даже на ракету, которая какое-то время существует.

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, но не лучше для сред, в которых современные веб-серверы работают в коде, привязанном к вводу-выводу.

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

Так какой же выбрать? Вы должны решить. Реактивные совместные подпрограммы лучше на бумаге, но в многопоточной среде всегда было труднее кодировать.

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