Миллион запросов в секунду с Python

Возможно ли такое? До недавнего времени, это было невозможно. Крупные компании пытаются мигрировать на другие языки программирования для улучшения производительности и сокращения расходов на серверы, но в действительности этого делать не нужно. Python это хороший инструмент и в сообществе вокруг него в последнее время происходит много всего, касающегося улучшения производительности. В CPython 3.6 улучшена общая производительность интерпретатора благодаря новой реализации словарей, CPython 3.7 будет ещё производительнее благодаря введению более быстрых вызовов и кэшированию в словарях. Для некоторых задач вы можете использовать PyPy с его just-in-time компиляцией. С недавних пор он может запускать тестовый набор NumPy и кардинально улучшил общую совместимость с расширениями на C. Немного позже ожидается, что PyPy достигнет соответствия с Python 3.5.

Всё это вдохновило меня на инновацию в одной из сфер, где Python широко используется, в разработке web и micro-сервисов.

Встречайте Japronto!

Japronto это совершенно новый микро-фреймворк, созданный с учётом ваших нужд. Его главные цели это скорость, расширяемость и лёгкость. Он умеет синхронное и асинхронное программирование с asyncio, и он чертовски быстр. Даже быстрее NodeJS и Go.

Миллион запросов в секунду с Python
микро-фреймворки на Python (синие), Тёмная сторона силы (зелёные) и Japronto (фиолетовый)

Этот микро-бенчмарк был сделан с помощью минимальной программы типа “Hello world!”, но он хорошо демонстрирует серверную нагрузку некоторых решений. Эти результаты были получены на экземпляре AWS c4.2xlarge с 8 VCPU, запущенной в Сан-Пауло с ресурсами по-умолчанию, виртуализацией HVM и магнитным устройством хранения. На машине была установлена Ubuntu 16.04.1 LTS (Xenial Xerus) с ядром Linux 4.4.0–53-generic x86_64. В отчёте ОС было: Xeon® CPU E5–2666 v3 @ 2.90GHz CPU. Я использовал Python 3.6, скомпилированный из исходного кода. Чтобы быть справедливым ко всем участникам сравнения (включая Go), был запущен только один рабочий процесс. Для тестирования серверной загрузки была использована утилита wrk с 1 потоком, 100 соединениями и 24 одновременными (в конвейере) запросами на каждое соединение (всего 2400 запросов).

Миллион запросов в секунду с Python
HTTP pipelining (картинка из Wikipedia)

HTTP pipelining (переводится как Конвейерная обработка HTTP) — технология, которая позволяет передавать на сервер сразу несколько запросов в одном соединении, не ожидая соответствующих ответов. Конвейерная обработка одна из важнейших оптимизаций в данном тесте, так как используется Japronto при выполнении запросов.

Большинство серверов выполняет запросы от клиентов HTTP pipelining таким же образом, как от клиентов не имеющих конвейерной обработки. Они не пытаются оптимизировать их. (На самом деле Sanic и Meinheld также молча сбрасывают запросы от клиентов HTTP pipelining, что является нарушением HTTP 1.1 протокола.)

Говоря простым языком, HTTP-pipelining представляет собой метод, в котором клиент не должен ждать ответа перед отправкой новых запросов, в рамках одного TCP соединения. Для обеспечения целостности соединения, сервер отправляет обратно несколько ответов в порядке, в котором были получены запросы.

Подробности механизма оптимизации

Если клиент отправляет большое количество мелких GET запросов, объединенных конвейерным методом, существует высокая вероятность того, что они будут прибывать, на сервер, в одном TCP пакете (благодаря

алгоритму Нэйгла

) и будут считаны одним системным вызовом.

Выполнение системного вызова и перемещения данных из пространства ядра в

пользовательское пространство

является очень ресурсоёмкой операцией, по сравнению, например, с перемещением памяти внутри пространства процесса. Вот почему важно выполнять системные вызовы по мере необходимости (но не меньше).

Когда Japronto принимает данные и успешно обрабатывает несколько запросов из них, он пытается выполнить все запросы как можно быстрее. Склеивает ответы посылаемые обратно, и в правильном порядке записывает их в один системный вызов. На самом деле, благодаря методу

scatter/gather IO

, ядро может сильно помочь в части склеивания ответов, но Japronto еще не умеет использовать этот метод.

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

Миллион запросов в секунду с PythonJapronto позволяет обработать в среднем 1,214,440 запросов в секунду , эти данные рассчитаны методом интерполяции, исходя из того что 50{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} значений располагается ниже этого уровня.

Кроме задержки операций записи для конвейерных клиентов, существует несколько других методов, использующих код.
Japronto почти полностью написан на C. Парсер, протокол, установщик соединения, маршрутизатор, запрос и объекты ответа записываются в виде расширений C.

Japronto старается изо всех сил, задержать создание внутренних структур Python, пока нет запроса в явном виде. Например, заголовки словаря не будут созданы, пока нет запроса на отображение их. Все символические границы уже отмечены ранее, но нормализация ключей заголовка, а также создание нескольких строковых объектов делается, только при обращении к ним.

Japronto опирается на отличную C библиотеку picohttpparser для разбора строки состояния, заголовков и тела HTTP сообщения. Picohttpparser напрямую использует инструкции по обработке текста в современных процессорах с расширениями SSE4.2 (доступно в любом x86_64 процессоре), для быстрого определения границ HTTP токенов. Ввод/вывод обрабатывается потрясающим uvloop — альтернативная реализации цикла событий для asyncio, написанная Юрием Селивановым на Cython и построеная на базе libuv. Работает примерно в 2 раза быстрее Node.js и практически не уступает программам на Go. На низком уровне, это мост к системному вызову epoll, обеспечивающий асинхронные уведомления о готовности чтения-записи.

Миллион запросов в секунду с Python
Picohttpparser полагается на SSE4.2 и CMPESTRI x86_64

Python является языком, имеющим специальный процесс сбора мусора, периодически освобождающий память, удаляя объекты, которые уже не будут востребованы приложениями. Внутренняя реализация Japronto пытается избегать циклических ссылок и делать как можно меньше выделений/освобождений памяти. Это реализовано путем предварительного выделения некоторых объектов в так называемые арены. Также производится попытка повторного использования объектов Python для будущих запросов, если они больше не имеют циклических ссылок, вместо выбрасывания их.

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

Открытые источники, которые можно использовать

Я работал на Japronto непрерывно в течение последних 3-х месяцев — как в выходные дни, так и рабочие дни. Это стало возможным только из-за перерыва в моей основной работе и я смог вложить все свои усилия в развитие этого проекта.

В настоящее время в Japronto реализован довольно солидный набор функций:
  • HTTP 1.x реализация с поддержкой Chunked загрузки
  • полная поддержка HTTP pipelining
  • Keep-Alive соединения с настраиваемым установщиком соединений
  • Подержка синхронного и асинхронного ввода/вывода
  • модель Master-multiworker на основе разветвления
  • Поддержка перезагрузки кода при изменениях
  • Простая маршрутизация

Если вы хотите помочь мне, пожалуйста, свяжитесь со мной в Twitter. Проект расположен по адресу https://github.com/squeaky-pl/japronto .

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

Послесловие

Все методы, которые я упомянул здесь не очень специфичны для Python. Их можно было бы, использовать на других языках, таких как Ruby, JavaScript и даже PHP. Мне было бы очень интересно поработать над этим, но это, к сожалению, не произойдет, если конечно кто-то вдруг не захочет это профинансировать.

Я хочу поблагодарить Python сообщество за их неоценимый вклад в разработку направленную на улучшение производительности. В частности Виктора Стиннера @VictorStinner, INADA Naoki @methane, Юрия Селиванова @1st1 и всю команду PyPy.

Ради любви к Python.

Источник на английском языке: Paweł Piotr Przeradowski: Million requests per second with Python

Leave a Comment