В экосистеме PHP на данный момент существует два коннектора для работы с сервером Tarantool ― это официальное расширение PECL tarantool/tarantool-php, написанное на С, и tarantool-php/client, написанный на PHP. Я являюсь автором последнего.
В этой статье я хотел бы поделиться результатами тестирования производительности обеих библиотек и показать, как с помощью минимальных изменений в коде можно добиться 3-5 прироста производительности (на синтетический тестах!).
Что будем тестировать?
Будем тестировать упомянутые выше синхронные коннекторы, запущенные асинхронно, параллельно и асинхронно-параллельно. :) Также мы не хотим трогать код самих коннекторов. На данный момент доступно несколько расширений, позволяющих добиться желаемого:
- Swoole ― высокопроизводительный асинхронный фреймворк для PHP. Используется такими интернет-гигантами как Alibaba и Baidu. С версии 4.1.0 появился волшебный метод Swoole\Runtime::enableCoroutine(), позволяющий «одной строкой кода преобразовать синхронные сетевые библиотеки PHP в асинхронные».
- Async ― до недавнего времени весьма перспективное расширение для асинхронной работы в PHP. Почему до недавнего? К сожалению, по неизвестной мне причине, автор удалил репозиторий и дальнейшая судьба проекта туманна. Придется воспользоваться одним из форков. Как и Swoole, это расширение позволяет легким движением руки
превратить брюкивключить асинхронность заменой стандартной реализации TCP и TLS потоков их асинхронными версиями. Делается это через опцию "async.tcp = 1".
- Parallel ― довольно новое расширение от небезызвестного Joe Watkins, автора таких библиотек как phpdbg, apcu, pthreads, pcov, uopz. Расширение предоставляет API для многопоточной работы в PHP и позиционируется как замена pthreads. Существенным ограничением библиотеки является то, что она работает только с ZTS (Zend Thread Safe) версией PHP.
Как будем тестировать?
Запустим экземпляр Tarantool'а с отключенным журналом упреждающей записи (wal_mode = none) и увеличенным сетевым буфером (readahead = 1 * 1024 * 1024). Первая опция исключит работу с диском, вторая — даст возможность вычитывать больше запросов из буфера операционной системы и тем самым минимизировать количество системных вызовов.
Для бенчмарков, которые работают с данными (вставка, удаление, чтение и т.д.) перед стартом бенчмарка будет (пере)создаваться memtx-спейс, в котором значения первичного индекса создаются генератором упорядоченных значений целых чисел (sequence).
DDL спейса выглядит так:
space = box.schema.space.create(config.space_name, {id = config.space_id, temporary = true})
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}, sequence = true})
space:format({{name = 'id', type = 'unsigned'}, {name = 'name', type = 'string', is_nullable = false}})
По необходимости, перед запуском бенчмарка, спейс заполняется 10,000 кортежами вида
{id, "tuplе_<id>"}
Доступ к кортежам осуществляется по рандомному значению ключа.
Сам бенчмарк представляет собой единичный запрос к серверу, который выполняется 10,000 раз (революций), которые, в свою очередь, выполняются в итерациях. Итерации повторяются до тех пор, пока все отклонения во времени между 5 итерациями не окажутся в пределах допустимой погрешности в 3 %*. После этого берется усредненный результат. Между итерациями пауза в 1 секунду, чтобы не дать процессору уйти в throttling. Сборщик мусора Lua отключается перед каждой итерацией и принудительно запускается после ее завершения. PHP-процесс запускается только с необходимыми для бенчмарка расширениями, с включенной буферизацией вывода и выключенным сборщиком мусора.
* Количество революций, итераций и порог погрешности можно изменить в настройках бенчмарка.
Тестовое окружение
Опубликованные ниже результаты были сделаны на MacBookPro (2015), операционная система — Fedora 30 (версия ядра 5.3.8-200.fc30.x86_64). Tarantool запускался в докере в с параметром "
--network host"
.Версии пакетов:
Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (built: Oct 22 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ патч для 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3
* К сожалению, официальный коннектор не работает с версией PHP > 7.2. Чтобы скомпилировать и запустить расширение на PHP 7.3, пришлось воспользоваться патчем.
Результаты
Синхронный режим
Протокол Tarantool’а использует бинарный формат MessagePack для сериализации сообщений. В коннекторе PECL сериализация скрыта глубоко в недрах библиотеки и повлиять на процесс кодирования из userland-кода не представляется возможным. Коннектор на чистом PHP, напротив, предоставляет возможность кастомизации процесса кодирования расширением стандартного кодировщика либо возможностью использования своей реализации. Из коробки доступно два кодировщика, один основан на msgpack/msgpack-php (официальное расширение MessagePack PECL), другой — на rybakit/msgpack (на чистом PHP).
Перед сравнением коннекторов, измерим производительность MessagePack кодировщиков для PHP-коннектора и в дальнейших тестах будем использовать тот, который покажет лучший результат:
Хоть PHP версия (Pure) и уступает расширению PECL в скорости, в реальных проектах я бы все же рекомендовал использовать именно rybakit/msgpack, потому как в официальном расширении MessagePack спецификация формата реализована лишь частично (например, нет поддержки пользовательских типов данных, без которой вы не сможете использовать Decimal — новый тип данных, представленный в Tarantool 2.3) и имеет ряд других проблем (включая проблемы совместимости с PHP 7.4). Ну и в целом, проект выглядит заброшенным.
Итак, измерим производительность коннекторов в синхронном режиме:
Как видно из графика, коннектор PECL (Tarantool) показывает лучшую производительность по сравнению с коннектором на PHP (Client). Но это и не удивительно, учитывая, что последний, помимо того, что реализован на более медленном языке, выполняет, по сути, больше работы: при каждом вызове создается новый объект Request и Response (в случае Select ― еще и Criteria, а в случае Update/Upsert ― Operations), отдельные сущности Connection, Packer и Handler тоже добавляют оверхед. Очевидно, что за гибкость приходится платить. Однако, в целом, PHP-интерпретатор показывает хорошую производительность, хоть разница и есть, но она незначительна и, возможно, будет еще меньше при использовании preloading в PHP 7.4, не говоря уже о JIT в PHP 8.
Двигаемся дальше. В Tarantool 2.0 появилась поддержка SQL. Попробуем выполнить операции Select, Insert, Update и Delete используя SQL-протокол и сравнить результаты с noSQL (бинарными) эквивалентами:
Результаты SQL не сильно впечатляют (напомню, что мы все еще тестируем синхронный режим). Однако, я бы не стал расстраиваться по этому поводу раньше времени, поддержка SQL все еще находится в активной разработке (относительно недавно, например, добавилась поддержка prepared statements) и, судя по списку issues, движок SQL в дальнейшем ждет ряд оптимизаций.
Async
Ну что ж, посмотрим теперь, как расширение Async сможет помочь нам улучшить результаты выше. Для написания асинхронных программ расширение предоставляет API на основе корутин (coroutines), им и воспользуемся. Опытным путем выясняем, что оптимальное количество корутин для нашего окружения равно 25:
«Размазываем» 10,000 операций по 25 корутинам и смотрим, что получилось:
Количество операций в секунду выросло более чем в 3 раза для tarantool-php/client!
Печально, но коннектор PECL не запустился с ext-async.
А что c SQL?
Как видите, в асинхронном режиме разница между бинарным протоколом и SQL стала в пределах погрешности.
Swoole
Опять выясняем оптимальное количество корутин, теперь уже для Swoole:
Остановимся на 25. Повторим тот же трюк, что и с расширением Async ― распределим 10,000 операций между 25 корутинами. Помимо этого, добавим еще тест, в котором разделим всю работу на 2 два процесса (то есть каждый процесс будет выполнять 5,000 операций в 25 корутинах). Процессы будут создаваться при помощи Swoole\Process.
Результаты:
Swole показывает чуть более низкий результат по сравнению с Async при запуске в одном процессе, но с 2 процессами картина меняется кардинально (число 2 выбрано не случайно, на моей машине именно 2 процесса показали наилучший результат).
Кстати, в расширении Async тоже есть API для работы с процессами, однако там я не заметил какой-то разницы от запуска бенчмарков в одном или нескольких процессах (не исключено, что я где-то накосячил).
SQL vs бинарный протокол:
Так же как и с Async, разница между бинарными и SQL-операциями нивелируется в асинхронном режиме.
Parallel
Так как расширение Parallel не про корутины, но про потоки, измерим оптимальное количество параллельных потоков:
Оно равно 16 на моей машине. Запустим бенчмарки коннекторов на 16 параллельных потоках:
Как видите, результат даже лучше, чем с асинхронными расширениями (не считая Swoole запущенном на 2 процессах). Заметьте, что для коннектора PECL на месте Update и Upsert операций пусто. Связано это с тем, что данные операции вылетели с ошибкой ― не знаю, по вине ext-parallel, ext-tarantool или обоих.
Теперь сравним производительность SQL:
Заметили сходство с графиком для коннекторов, запущенных синхронно?
Всё вместе
Ну и напоследок, сведем все результаты в один график, чтобы увидеть общую картину для тестируемых расширений. Добавим на график лишь один новый тест, который мы еще не делали ― запустим корутины Async параллельно при помощи Parallel*. Идея интеграции вышеупомянутых расширений уже обсуждалась авторами, однако консенсус так и не был достигнут, придется делать это самим.
* Запустить корутины Swoole с Parallel не получилось, похоже эти расширения несовместимы.
Итак, финальные результаты:
Вместо заключения
По-моему, результаты получились весьма достойные, и я почему-то уверен, что это еще не предел! Нужно ли это вам в реальном проекте решать исключительно вам самим, скажу лишь, что для меня это был интересный эксперимент, позволяющий оценить, сколько можно «выжать» из синхронного TCP-коннектора с минимальными усилиями. Если у вас есть идеи по улучшению бенчмарков ― я с радостью рассмотрю ваш пул реквест. Весь код с инструкциями по запуску и результатами опубликован в отдельном репозитории.