Бенчмарки кэша PostgreSQL: 3.1.0

В этих измерениях сравниваются pg_local_cache RESP MGET, Valkey и подготовленный SQL на двух VM в частной сети. Все результаты получены на финальной сборке 3.1.0 c431bcc. Для матриц чтения и тестов записи приведены медианы повторных запусков; оба закреплённых однопарных случая Valkey io-threads=1 имеют по два повтора, остальные закреплённые случаи — по пять. Смешанные тесты, 120-секундные проверки устаревания и часовой soak — одиночные запуски. Эти прогоны не обещают производительность production.

Результаты финальной сборки

Чтение на закреплённую пару vCPU сервера

Клиент Go/pgx использовал 256 клиентов и один ключ на MGET. Каждый случай выполнялся 20 секунд. Два однопарных случая Valkey io-threads=1 повторялись дважды; остальные закреплённые случаи — пять раз. PostgreSQL и Valkey закреплялись за N парами vCPU (2N vCPU). Valkey проверялся с io-threads=2N и io-threads=1; подготовленный SQL использовал те же ключи.

Пары vCPU Ключи pg_local_cache запросов/с (p99 мс, vCPU) Valkey io-threads=2N (запросов/с, p99 мс, vCPU) Valkey io-threads=1 (запросов/с, p99 мс, vCPU) Подготовленный SQL (запросов/с, p99 мс, vCPU)
1 Горячие 209,696 (2.46, 1.99) 91,478 (3.24, 2.00) 180,141 (2.59, 0.85) 23,200 (24.90, 2.00)
1 100k случайных 195,950 (2.65, 2.00) 88,672 (3.38, 2.00) 175,853 (2.65, 0.92) 22,433 (25.95, 2.00)
2 Горячие 336,691 (2.02, 3.78) 266,340 (1.69, 4.00) 162,230 (2.85, 0.66) 46,386 (16.38, 4.00)
2 100k случайных 326,125 (1.98, 3.83) 267,826 (1.72, 4.00) 160,220 (2.92, 0.69) 45,211 (17.56, 4.00)
4 Горячие 360,968 (2.33, 5.24) 357,161 (2.20, 7.95) 160,633 (2.85, 0.66) 88,604 (6.75, 8.00)
4 100k случайных 353,941 (2.33, 5.27) 351,344 (2.20, 7.95) 159,414 (2.92, 0.68) 86,790 (6.88, 8.00)

На одном ядре лучшей конфигурацией Valkey был один поток. На двух ядрах лучшим был io-threads=4; p99 Valkey оказался ниже при меньшей пропускной способности. На четырёх ядрах pg_local_cache и io-threads=8 показали почти одинаковую клиент-ограниченную пропускную способность; серверный CPU pg_local_cache был ниже.

Raw runs: Valkey io-threads=2N, Valkey io-threads=1.

Чтение при доступных 16 vCPU

Эти прогоны не закрепляли CPU и повторялись три раза. В сценариях с ограничением клиента узел нагрузки использовал около 7–13 ядер CPU. При Valkey io-threads=8 наблюдаемые различия медианной пропускной способности MGET 16/64 составили примерно от −2,15 % до +1,26 %; диапазоны не пересекались ни в одном из четырёх сравнений на 64 ключах. Эти выборки не доказывают статистическую эквивалентность. При io-threads=1 случайный MGET 16 на 256 клиентах показал 116 130 против 97 489 запросов/с (на 19,1 % больше у pg_local_cache). Широкие MGET ограничивались клиентом и сетью; для io-threads=1 нет результата на 64 ключах.

Ключи Ключей MGET Клиенты pg_local_cache (запросов/с, p99 мс, vCPU) Valkey io-threads=8 (запросов/с, p99 мс, vCPU) Valkey io-threads=1 (запросов/с, p99 мс, vCPU) Подготовленный SQL (запросов/с, p99 мс, vCPU)
Горячие 1 64 217,724 (0.70, 2.89) 206,634 (0.70, 7.85) 167,929 (0.71, 0.66) 132,433 (0.96, 11.32)
Горячие 1 256 372,683 (2.39, 4.21) 365,947 (2.33, 7.92) 161,455 (2.92, 0.68) 173,571 (4.26, 13.14)
Горячие 16 64 82,858 (3.38, 3.13) 83,098 (3.38, 6.41) 79,179 (3.38, 0.69) 73,512 (2.05, 8.78)
Горячие 16 256 122,496 (9.83, 4.07) 122,817 (9.83, 7.42) 111,790 (8.06, 0.83) 100,966 (6.36, 11.49)
Горячие 64 64 30,241 (8.78, 3.40) 30,845 (9.04, 3.57) — 27,790 (6.88, 9.05)
Горячие 64 256 39,531 (43.52, 4.26) 39,995 (47.71, 5.66) — 34,519 (30.67, 12.23)
100k случайных 1 64 215,310 (0.70, 2.89) 205,081 (0.70, 7.85) 162,778 (0.73, 0.67) 131,563 (0.97, 11.34)
100k случайных 1 256 366,860 (2.39, 4.31) 360,894 (2.33, 7.92) 158,444 (2.98, 0.68) 169,884 (4.26, 13.19)
100k случайных 16 64 79,505 (3.51, 3.45) 79,556 (3.44, 6.68) 74,270 (3.38, 0.79) 69,262 (2.13, 9.12)
100k случайных 16 256 116,130 (10.35, 4.42) 114,683 (9.83, 7.53) 97,489 (7.67, 0.95) 91,572 (7.01, 11.96)
100k случайных 64 64 29,000 (8.78, 3.81) 29,464 (9.04, 4.61) — 26,169 (7.14, 9.39)
100k случайных 64 256 37,356 (43.52, 4.93) 38,175 (40.37, 7.58) — 32,756 (31.20, 13.03)

Для случайных ключей, MGET 64 и 256 клиентов p99 pg_local_cache составил 43,52 мс против 40,37 мс у Valkey. Подготовленный SQL имел значительно меньшую пропускную способность в тестах одного ключа; результаты на пакетах зависят от размера и метрики задержки.

Raw runs: Valkey io-threads=8, Valkey io-threads=1.

Накладные расходы записи

Три 15-секундных повтора сравнивают транзакции обычной таблицы, транзакции через pg_local_cache и обычные транзакции с последующим Valkey DEL. Проценты рассчитаны относительно соответствующего случая обычной таблицы.

Операция synchronous_commit Клиенты Обычная таблица tx/s (DB µs/tx) pg_local_cache tx/s (DB µs/tx, изменение) Обычная таблица + Valkey DEL tx/s (DB µs/tx, изменение)
Update on 32 46,747 (109) 45,065 (119, −3.6%) 33,234 (204, −28.9%)
Update on 64 63,479 (108) 61,027 (121, −3.9%) 37,659 (238, −40.7%)
Update off 32 119,156 (75) 110,987 (87, −6.9%) 53,374 (158, −55.2%)
Update off 64 148,358 (73) 136,194 (83, −8.2%) 53,853 (189, −63.7%)
Insert on 32 48,831 (97) 47,020 (105, −3.7%) 34,863 (184, −28.6%)
Insert on 64 65,688 (97) 63,011 (108, −4.1%) 39,642 (218, −39.7%)
Insert off 32 123,679 (63) 117,868 (72, −4.7%) 58,168 (135, −53.0%)
Insert off 64 138,454 (68) 130,805 (77, −5.5%) 58,361 (165, −57.8%)

Для UPDATE pg_local_cache уступал обычной записи на 3,6–8,2%; для INSERT накладные расходы составили 3,7–5,5%. Обычный UPDATE плюс Valkey DEL был ниже по пропускной способности на 28,9–63,7%; для INSERT разница составила 28,6–57,8%.

Raw runs: write overhead.

Чтение во время записи

Каждый случай выполнялся один раз в течение 30 секунд: 64 читателя запрашивали по одному ключу из 60k строк; writers использовали synchronous_commit=off. Valkey работал в режиме cache-aside (запись SQL, затем DEL) с io-threads=8. Устаревшие записи — результат финальной проверки sentinel; n/a означает, что кэшируемых записей для проверки нет.

Стек Распределение ключей Writer Чтений/с p99 чтения, мс Доля попаданий Записей/с Устаревшие записи
pg_local_cache Uniform Нет 214,508 0.71 1.000 0 0
pg_local_cache Uniform Update 10,000/s 195,384 0.86 0.948 10,000 0
pg_local_cache Uniform Update 30,000/s 161,919 1.04 0.837 29,999 0
pg_local_cache Uniform Update unlimited 82,640 1.69 0.432 111,013 0
pg_local_cache Zipf Нет 216,297 0.70 1.000 0 0
pg_local_cache Zipf Update 10,000/s 198,454 0.86 0.964 10,000 0
pg_local_cache Zipf Update 30,000/s 168,875 1.01 0.923 30,000 0
pg_local_cache Zipf Update unlimited 96,297 1.46 0.834 113,505 0
Valkey cache-aside Uniform Нет 223,537 0.61 1.000 0 0
Valkey cache-aside Uniform Update 10,000/s 156,983 1.92 0.936 10,000 0
Valkey cache-aside Uniform Update 30,000/s 42,294 7.14 0.577 29,998 0
Valkey cache-aside Uniform Update unlimited 29,692 8.78 0.444 38,481 1
Valkey cache-aside Zipf Нет 222,575 0.63 0.999 0 0
Valkey cache-aside Zipf Update 10,000/s 168,911 1.65 0.960 10,000 0
Valkey cache-aside Zipf Update 30,000/s 75,642 4.78 0.887 29,999 0
Valkey cache-aside Zipf Update unlimited 48,993 6.88 0.852 40,735 0
Prepared SQL Uniform Нет 129,714 0.97 — 0 n/a
Prepared SQL Uniform Update 10,000/s 121,567 1.13 — 10,000 n/a
Prepared SQL Uniform Update 30,000/s 106,949 1.33 — 30,000 n/a
Prepared SQL Uniform Update unlimited 77,574 2.02 — 93,955 n/a
Prepared SQL Zipf Нет 129,937 0.97 — 0 n/a
Prepared SQL Zipf Update 10,000/s 122,182 1.13 — 10,000 n/a
Prepared SQL Zipf Update 30,000/s 107,852 1.33 — 29,999 n/a
Prepared SQL Zipf Update unlimited 78,601 2.08 — 95,199 n/a
pg_local_cache Uniform Insert unlimited 117,531 1.20 1.000 114,066 0
Valkey cache-aside Uniform Insert unlimited 70,302 4.92 1.000 46,622 0
Prepared SQL Uniform Insert unlimited 84,294 1.98 — 95,094 n/a

Во всех перечисленных смешанных тестах pg_local_cache завершил работу без устаревших записей. Только в одном случае UPDATE у Valkey осталась одна устаревшая sentinel-запись: Uniform-ключи при неограниченной скорости обновления. При 30k UPDATE/s и Uniform чтение составило 161 919/s у pg_local_cache, 42 294/s у Valkey cache-aside и 106 949/s у подготовленного SQL.

Raw runs: mixed reads and writes.

Проверки устаревших записей за 120 секунд

Каждая проверка запускалась один раз: 64 читателя и 64 неограниченных UPDATE writer, затем полная проверка устаревших записей. Valkey использовал io-threads=8.

Стек Распределение ключей Чтений/с p99 чтения, мс Записей/с Устаревшие записи Примечание
pg_local_cache Uniform 82,565 1.687552 108,580 0 —
pg_local_cache Zipf 0 n/a 101,797 0 См. примечание ниже.
Valkey cache-aside Uniform 30,210 8.781824 36,508 2 —
Valkey cache-aside Zipf 28,568 8.781824 35,789 1 —

Читатель pg_local_cache Zipf не прошёл стартовую проверку эквивалентности при параллельной записи (проблема harness), поэтому показал ноль чтений. Проверка устаревших записей всё равно выполнилась и нашла ноль.

Raw runs: 120-second stale probes.

Часовой soak-тест

Один запуск: MGET 16 по 100k случайных ключей, 64 клиента и 20 000 UPDATE/s. RssAnon RESP worker измерялся раз в минуту.

Raw runs: one-hour soak.

Стенд и методика

По данным lscpu, каждая VM имела 16 vCPU: 2 сокета × 4 ядра на сокет × 2 потока на ядро (топология VM), 62 ГБ RAM и Debian 13 с ядром 6.12.111+deb13-amd64. На VM базы работали PostgreSQL 16.15 (max_connections=1000, shared_buffers=1048576) и pg_local_cache 3.1.0 c431bcc (включён, memory_budget_mb=1024, workers=8). Таблица содержала 100 000 JSON-строк в среднем по 166 байт. Valkey 8.1.1 использовал io-threads=8 в тестах записи, смешанных тестах и проверках устаревания. От VM нагрузки к VM базы iperf3 показал 10,4 Gbit/s, ping — RTT min/avg/max 0,090/0,143/0,871 мс. Клиент Go/pgx работал на второй VM. Чтение запускалось на 20 секунд; запись — 15 секунд, смешанные тесты — 30 секунд, проверки устаревания — 120 секунд, soak — 3 600 секунд.

Закреплённая матрица использовала 256 клиентов; каждый случай повторялся пять раз, кроме двух однопарных случаев Valkey io-threads=1 — у них было по два повтора. Полная матрица и тесты записи имели по три повтора; смешанные тесты, проверки устаревания и soak запускались по одному разу. IRQ сетевой карты не закреплялись. Все таблицы относятся к финальной сборке c431bcc. Пропускная способность указана в запросах/с, если не указаны транзакции/с; задержка — p99, CPU — серверные vCPU.

Окружение и исходные JSONL: окружение, данные стенда, закреплённое чтение, закреплённое чтение Valkey io-threads=1, чтение на всех ядрах, все ядра Valkey io-threads=1, накладные расходы записи, чтение во время записи, проверки 120 секунд и часовой soak.

Повторный запуск через bench/

Нужны Python 3, SSH-алиасы для VM базы и нагрузки. На VM базы нужны PostgreSQL 16, Valkey, mpstat, sar, ip и systemd; на VM нагрузки — скомпилированный клиент Go в /root/bench-client. Подробности: bench/README.md и клиент Go/pgx.

Стандартная матрица чтения: 20 секунд, три повтора, 1/16/64 ключа, 16/64/256 клиентов, горячий набор и 100k ключей:

python3 bench/run_bench.py reads.jsonl

Пример закрепления одной пары vCPU; замените 0-1 на CPU ID логических потоков этой пары vCPU:

PGLC_SERVER_CPUS=0-1 REPEATS=5 BATCHES=1 CLIENTS=256 KEY_SPACES=0,100000 python3 bench/run_bench.py pinned.jsonl

Скрипт закрепляет PostgreSQL и Valkey через AllowedCPUs и снимает закрепление после запуска. Он записывает серверный CPU, задержку, сетевой RX/TX и CPU клиента. Перед запуском настройте io-threads Valkey согласно нужной строке.

Запуск теста записи и 30-секундной смешанной нагрузки:

REPEATS=3 WRITE_SECONDS=15 WRITE_CLIENTS=32,64 python3 bench/write_bench.py writes.jsonl
REPEATS=1 MIXED_SECONDS=30 MIXED_RATES=none,10000,30000,unlimited MIXED_KEY_DISTS=uniform,zipf python3 bench/write_bench.py mixed.jsonl

Скрипты дописывают JSONL и сохраняют завершённые прогоны при ошибке. Тест записи измеряет CPU базы на транзакцию; смешанный тест также записывает p99, попадания/промахи, скорость записи и финальную проверку контрольных записей.

Исторические результаты 2.x на MacBook

Старые JSON — исторические измерения на Apple M3 Max с PostgreSQL 16; стенд и схема клиента отличаются от двух VM выше. Они включают SQL mget API из 2.x, удалённый в 3.0.0. Описание старого API: документация 2.0.4. В актуальной версии чтение из кэша выполняется через RESP MGET.

Обновлено .