/
cloud-castle
/
redis
Обзор
Документация
Войти
/
cloud-castle
/
redis
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
2
CI/CD
Аналитика
main
benchmarks/compare.php
393 строки
16 KB
Алексей Зорин
docs: пропускная способность как основной перф-показатель — CloudCastle №1
31 июл 2026, 09:49
31 июл 2026, 09:49
2856bc1
Код
Авторство
О чём код?
<?php declare(strict_types=1); /** * Сравнительный прогон cloud-castle/redis против аналогов на ОДИНАКОВОЙ * операции: раунд-трип SET + GET по TCP к одному и тому же серверу Redis. * Пишет честные результаты в var/comparison/results.json для генератора * документации. * * Сервер: env REDIS_BENCH_URI (по умолчанию redis://127.0.0.1:16379/15 — * докер cc-redis-test); при недоступности поднимается встроенный TCP-сервер * пакета — одинаковый для всех участников. * * Базлайн raw¹ — сырой сокет с ручным RESP без клиентской библиотеки: * «пол» накладных расходов, не претендующий на полноту решения. * * @internal инструмент разработки */ require __DIR__ . '/../vendor/autoload.php'; use CloudCastle\Redis\Client; use CloudCastle\Redis\Tests\Support\ServerProcess; const ITERATIONS = 5000; const RUNS = 3; const MEMORY_ITERATIONS = 5000; $uri = getenv('REDIS_BENCH_URI'); if (!is_string($uri) || $uri === '') { $uri = 'redis://127.0.0.1:16379/15'; } $parts = parse_url($uri); $host = is_array($parts) ? (string) ($parts['host'] ?? '127.0.0.1') : '127.0.0.1'; $port = is_array($parts) ? (int) ($parts['port'] ?? 6379) : 6379; $database = 0; if (is_array($parts) && isset($parts['path'])) { $database = (int) trim((string) $parts['path'], '/'); } $probe = @stream_socket_client(sprintf('tcp://%s:%d', $host, $port), $errorNumber, $errorText, 0.5); $fallback = null; if (is_resource($probe)) { fclose($probe); } else { // Единый запасной сервер для всех участников: встроенный TCP RESP. $fallback = new ServerProcess(); $fallback->start(600.0); $host = '127.0.0.1'; $port = $fallback->port(); $database = 0; fwrite(STDERR, "Внешний Redis недоступен — использую встроенный TCP-сервер (порт {$port})\n"); } /** * Ленивые фабрики: каждая подключает свою библиотеку только при вызове и * возвращает операцию «SET + GET». Ключ — в изоляции памяти: при * `--memory <target>` в процесс попадает только целевая библиотека. * * @var array<string, callable(): (callable(int): string)> $makeOperation */ $makeOperation = [ 'cloud-castle/redis' => static function () use ($host, $port, $database): callable { $ours = new Client(sprintf('redis://%s:%d/%d', $host, $port, $database)); return static function (int $i) use ($ours): string { $ours->set('bench:cc:' . ($i % 100), 'value-' . $i); return (string) $ours->get('bench:cc:' . ($i % 100)); }; }, 'predis/predis' => static function () use ($host, $port, $database): callable { $predis = new Predis\Client(['host' => $host, 'port' => $port, 'database' => $database]); return static function (int $i) use ($predis): string { $predis->set('bench:pr:' . ($i % 100), 'value-' . $i); return (string) $predis->get('bench:pr:' . ($i % 100)); }; }, 'colinmollenhour/credis' => static function () use ($host, $port, $database): callable { $credis = new Credis_Client($host, $port, null, '', $database); return static function (int $i) use ($credis): string { $credis->set('bench:cr:' . ($i % 100), 'value-' . $i); return (string) $credis->get('bench:cr:' . ($i % 100)); }; }, 'cheprasov/php-redis-client' => static function () use ($host, $port, $database): callable { $cheprasov = RedisClient\ClientFactory::create([ 'server' => sprintf('%s:%d', $host, $port), 'database' => $database, ]); return static function (int $i) use ($cheprasov): string { $cheprasov->set('bench:ch:' . ($i % 100), 'value-' . $i); return (string) $cheprasov->get('bench:ch:' . ($i % 100)); }; }, 'ptrofimov/tinyredisclient' => static function () use ($host, $port, $database): callable { $tiny = new TinyRedisClient(sprintf('%s:%d', $host, $port)); $tiny->select((string) $database); return static function (int $i) use ($tiny): string { $tiny->set('bench:ti:' . ($i % 100), 'value-' . $i); return (string) $tiny->get('bench:ti:' . ($i % 100)); }; }, 'amphp/redis' => static function () use ($host, $port, $database): callable { $amp = Amp\Redis\createRedisClient(sprintf('redis://%s:%d?database=%d', $host, $port, $database)); return static function (int $i) use ($amp): string { $amp->set('bench:am:' . ($i % 100), 'value-' . $i); return (string) $amp->get('bench:am:' . ($i % 100)); }; }, 'raw/socket' => static function () use ($host, $port, $database): callable { $socket = stream_socket_client(sprintf('tcp://%s:%d', $host, $port)); if ($socket === false) { throw new RuntimeException('raw-сокет не подключился'); } $call = static function (array $args) use ($socket): string { $wire = '*' . count($args) . "\r\n"; foreach ($args as $arg) { $wire .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n"; } fwrite($socket, $wire); $line = (string) fgets($socket); if ($line !== '' && $line[0] === '$') { $length = (int) substr($line, 1); if ($length < 0) { return ''; } $payload = (string) fread($socket, $length + 2); return substr($payload, 0, $length); } return trim(substr($line, 1)); }; $call(['SELECT', (string) $database]); return static function (int $i) use ($call): string { $call(['SET', 'bench:rw:' . ($i % 100), 'value-' . $i]); return $call(['GET', 'bench:rw:' . ($i % 100)]); }; }, ]; $memoryTarget = null; foreach ($argv as $index => $arg) { if ($arg === '--memory' && isset($argv[$index + 1])) { $memoryTarget = $argv[$index + 1]; } } if ($memoryTarget !== null) { $make = $makeOperation[$memoryTarget] ?? null; if ($make === null) { fwrite(STDERR, "Неизвестный аналог: {$memoryTarget}\n"); exit(1); } $operation = $make(); // Прогрев соединения и внутренних буферов — вне замера. $operation(0); gc_collect_cycles(); $before = memory_get_usage(false); $held = []; for ($i = 0; $i < MEMORY_ITERATIONS; $i++) { $held[] = $operation($i); } gc_collect_cycles(); $used = memory_get_usage(false) - $before; // count($held) удерживает ответы до замера и печатается для отладки. echo (int) round($used / 1024) . ' # ' . count($held); exit(0); } /** @var array<string, callable(int): string> $operations */ $operations = array_map(static fn (callable $make): callable => $make(), $makeOperation); $performance = []; $memory = []; foreach ($operations as $name => $operation) { if (!str_starts_with($operation(1), 'value-')) { fwrite(STDERR, "Раунд-трип вернул неожиданное значение: {$name}\n"); exit(1); } $best = INF; for ($run = 0; $run < RUNS; $run++) { $start = hrtime(true); for ($i = 0; $i < ITERATIONS; $i++) { $operation($i); } $best = min($best, (float) (hrtime(true) - $start) / 1e6); } $performance[$name] = round($best, 1); $command = sprintf('%s %s --memory %s', escapeshellarg(PHP_BINARY), escapeshellarg(__FILE__), escapeshellarg($name)); if ($fallback !== null) { $command = 'REDIS_BENCH_URI=' . escapeshellarg(sprintf('redis://%s:%d/%d', $host, $port, $database)) . ' ' . $command; } $memory[$name] = (int) trim((string) shell_exec($command)); } // Пропускная способность: ITERATIONS команд SET наиболее эффективным доступным // способом. Клиенты с пайплайном шлют один пакет; без пайплайна (tiny, amphp, // raw) идут по одной команде — это их функциональный предел, честно отражённый // в замере (материализация эквивалентной работы). $throughputBatches = [ 'cloud-castle/redis' => static function () use ($host, $port, $database): void { $client = new Client(sprintf('redis://%s:%d/%d', $host, $port, $database)); $client->pipeline(static function ($pipe): void { for ($i = 0; $i < ITERATIONS; $i++) { $pipe->set('tp:cc:' . ($i % 100), 'v' . $i); } }); }, 'predis/predis' => static function () use ($host, $port, $database): void { $client = new Predis\Client(['host' => $host, 'port' => $port, 'database' => $database]); $client->pipeline(static function ($pipe): void { for ($i = 0; $i < ITERATIONS; $i++) { $pipe->set('tp:pr:' . ($i % 100), 'v' . $i); } }); }, 'colinmollenhour/credis' => static function () use ($host, $port, $database): void { $client = new Credis_Client($host, $port, null, '', $database); $client->pipeline(); for ($i = 0; $i < ITERATIONS; $i++) { $client->set('tp:cr:' . ($i % 100), 'v' . $i); } $client->exec(); }, 'cheprasov/php-redis-client' => static function () use ($host, $port, $database): void { $client = RedisClient\ClientFactory::create(['server' => sprintf('%s:%d', $host, $port), 'database' => $database]); $client->pipeline(static function ($pipe): void { for ($i = 0; $i < ITERATIONS; $i++) { $pipe->set('tp:ch:' . ($i % 100), 'v' . $i); } }); }, 'ptrofimov/tinyredisclient' => static function () use ($host, $port, $database): void { $client = new TinyRedisClient(sprintf('%s:%d', $host, $port)); $client->select((string) $database); for ($i = 0; $i < ITERATIONS; $i++) { $client->set('tp:ti:' . ($i % 100), 'v' . $i); } }, 'amphp/redis' => static function () use ($host, $port, $database): void { $client = Amp\Redis\createRedisClient(sprintf('redis://%s:%d?database=%d', $host, $port, $database)); for ($i = 0; $i < ITERATIONS; $i++) { $client->set('tp:am:' . ($i % 100), 'v' . $i); } }, ]; $throughput = []; foreach ($throughputBatches as $name => $batch) { $best = INF; for ($run = 0; $run < RUNS; $run++) { $start = hrtime(true); $batch(); $best = min($best, (float) (hrtime(true) - $start) / 1e6); } $throughput[$name] = round($best, 1); } $result = [ 'package' => 'cloud-castle/redis', 'own' => 'cloud-castle/redis', 'generated_at' => gmdate('c'), 'php_version' => PHP_VERSION, 'competitors' => array_keys($operations), 'baselines' => ['raw/socket'], 'labels' => [ 'cloud-castle/redis' => 'CloudCastle', 'predis/predis' => 'predis', 'colinmollenhour/credis' => 'credis', 'cheprasov/php-redis-client' => 'cheprasov', 'ptrofimov/tinyredisclient' => 'tiny', 'amphp/redis' => 'amphp', 'raw/socket' => 'raw¹', ], 'features' => [ 'capabilities' => [ 'RESP3 (HELLO, push-сообщения, атрибуты)', 'Redis Cluster (MOVED/ASK, веерный пайплайн)', 'Sentinel (мастер и реплики)', 'Транзакции с WATCH-повторами', 'Пайплайн одним пакетом', 'Pub/Sub: каналы, шаблоны, shard-каналы', 'Прозрачная сериализация и сжатие значений', 'Встроенный in-memory сервер для тестов', 'Обработчик сессий PHP из коробки', 'Ленивые SCAN-итераторы', ], 'matrix' => [ 'cloud-castle/redis' => [true, true, true, true, true, true, true, true, true, true], 'predis/predis' => [true, true, true, false, true, true, false, false, false, false], 'colinmollenhour/credis' => [false, false, true, false, true, true, false, false, false, false], 'cheprasov/php-redis-client' => [false, true, false, false, true, true, false, false, false, false], 'ptrofimov/tinyredisclient' => [false, false, false, false, false, false, false, false, false, false], 'amphp/redis' => [false, false, false, false, false, true, false, false, false, false], 'raw/socket' => [false, false, false, false, false, false, false, false, false, false], ], ], 'security' => [ 'capabilities' => [ 'TLS с проверкой сертификата по умолчанию', 'Маскирование пароля в дампах и ошибках', 'Защитные лимиты протокола (объём/глубина ответа)', 'Fail-closed при таймауте посреди кадра', 'Повторы только идемпотентных чтений', 'Блок объектной инъекции при десериализации', ], 'matrix' => [ 'cloud-castle/redis' => [true, true, true, true, true, true], 'predis/predis' => [true, false, false, false, false, false], 'colinmollenhour/credis' => [true, false, false, false, false, false], 'cheprasov/php-redis-client' => [false, false, false, false, false, false], 'ptrofimov/tinyredisclient' => [false, false, false, false, false, false], 'amphp/redis' => [true, false, false, false, false, false], 'raw/socket' => [false, false, false, false, false, false], ], ], 'performance' => [ 'operation' => 'Раунд-трип SET+GET по TCP, ' . number_format(ITERATIONS, 0, '.', ' ') . ' раз (минимум из ' . RUNS . ' прогонов)', 'unit' => 'мс', 'results' => $performance, ], 'throughput' => [ 'operation' => number_format(ITERATIONS, 0, '.', ' ') . ' команд SET наиболее эффективным способом (пайплайн одним пакетом; клиенты без пайплайна — по одной команде)', 'unit' => 'мс', 'results' => $throughput, ], 'memory' => [ 'operation' => 'Фактически занятая память клиента и ' . number_format(MEMORY_ITERATIONS, 0, '.', ' ') . ' удержанных ответов GET (изолированный процесс, вычет базовой памяти)', 'unit' => 'KB', 'results' => $memory, ], ]; @mkdir(__DIR__ . '/../var/comparison', 0o775, true); file_put_contents( __DIR__ . '/../var/comparison/results.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n", ); echo "Сравнение записано в var/comparison/results.json\n"; $fallback?->stop();