Миграция баз данных в Kubernetes выглядит логичным шагом: хочется операторов, GitOps, автопочинку, единый способ доставки и управления. Для PostgreSQL один из популярных вариантов — CloudNativePG.
Но как только разговор доходит до продакшена, возникает очень приземлённый вопрос:
Сколько производительности я потеряю по сравнению с “голым” PostgreSQL на виртуалке?
В этой статье я воспроизвёл максимально честное сравнение:
native PostgreSQL на VM в Yandex Cloud;
CloudNativePG-кластер в Kubernetes в том же Yandex Cloud;
одинаковые конфиги PostgreSQL и схожие ресурсы;
тесты диска через fio и тесты PostgreSQL через pgbench.
Важное уточнение про стенд:
pgbench запускался с отдельной VM (то есть “локальная машина” клиента нагрузки — это тоже VM в YC).
И native PostgreSQL, и CloudNativePG — это удалённые PostgreSQL-серверы, до которых мы ходим по сети (TCP).
Сравнивается именно “сетевой клиент → VM с Postgres” против “сетевой клиент → Kubernetes-кластер с Postgres (CloudNativePG)”.
В итоге картинка получилась довольно типичной для продакшена: выигрыши в удобстве и управляемости Kubernetes есть, но платить за них приходится просадкой до ~40% по TPS.
Далее — подробности
Тесты производительности
Исходные данные
Облако: Yandex Cloud.
PostgreSQL: v14.
Конфиги PostgreSQL и ресурсы максимально выровнены между:
native Postgres VM;
CloudNativePG-кластером.
Хранилище:
native-vm— VM наyc-network-ssd, 533 GiB.cnpg-yc-network-ssd— CloudNativePG (PVC наyc-network-ssd), 533 GiB.cnpg-yc-network-ssd-io-m3— CloudNativePG (PVC наyc-network-ssd-io-m3), 558 GiB (кратность 93 GiB — ограничение Yandex Cloud).
Конфигурация PostgreSQL
Native PostgreSQL (VM)
Базовый конфиг - главное:
listen_addresses = '*'
port = 5432
max_connections = 5000
shared_buffers = 10GB
wal_level = logical
archive_mode = on
max_wal_senders = 20
logging_collector = on
ssl = on
shared_preload_libraries = 'pg_stat_statements'
Остальное — дефолты Debian/Ubuntu для PostgreSQL 14.
CloudNativePG (Pod внутри кластера)
custom.conf внутри Pod’а CloudNativePG:
cluster_name = 'db-stage-cnpg'
listen_addresses = '*'
port = '5432'
max_connections = '5000'
shared_buffers = '10GB'
wal_level = 'logical'
archive_mode = 'on'
archive_command = '/controller/manager wal-archive --log-destination /controller/log/postgres.json %p'
wal_keep_size = '512MB'
max_replication_slots = '32'
max_worker_processes = '32'
max_parallel_workers = '32'
ssl = 'on'
ssl_ca_file = '/controller/certificates/client-ca.crt'
ssl_cert_file = '/controller/certificates/server.crt'
ssl_key_file = '/controller/certificates/server.key'
ssl_min_protocol_version = 'TLSv1.3'
ssl_max_protocol_version = 'TLSv1.3'
logging_collector = 'on'
log_destination = 'csvlog'
log_directory = '/controller/log'
log_filename = 'postgres'
То есть:
По памяти и WAL (
shared_buffers,max_connections,wal_level,archive_mode) конфиги приведены к одному виду.CNPG добавляет:
жёсткий TLS 1.3;
собственный
archive_command;служебную обвязку оператора.
Методология тестирования
fio: как и зачем именно так
Цель fio — отделить “чистую” производительность диска от влияния PostgreSQL и Kubernetes.
Я тестировал три сценария:
Random 80% read / 20% write (
randrw_80_20_file)Блок
8k— соответствует странице PostgreSQL.80/20 по чтению/записи — типичный OLTP-паттерн.
direct=1— обходим page cache, тестируем именно диск/блоковое устройство.size=50G,runtime=300,numjobs=4,iodepth=32— даём диску выйти на плато по IOPS.
Последовательное чтение (
seq_read_file)bs=1M— имитация больших чтений (бэкапы, аналитику, последовательные сканы).runtime=300,iodepth=16— устойчивое измерение пропускной способности.
Последовательная запись (
seq_write_file)Тоже
bs=1M, сценарии bulk-загрузки/бэкапов/логирования.
Эти тесты запускались на том же томе, где лежат данные PostgreSQL:
на VM — это диск под
/var/lib/postgresql;в CNPG — это PVC, примонтированный в Pod.
Конфигурация fio в виде job-файла:
[randrw_80_20_file]
direct=1
bs=8k
size=50G
time_based=1
runtime=300
ioengine=libaio
iodepth=32
end_fsync=1
log_avg_msec=1000
directory=/data
rw=randrw
rwmixread=80
write_bw_log=randrw_80_20_file
write_lat_log=randrw_80_20_file
write_iops_log=randrw_80_20_file
[seq_read_file]
direct=1
bs=1M
size=50G
time_based=1
runtime=300
ioengine=libaio
iodepth=16
end_fsync=1
log_avg_msec=1000
directory=/data
rw=read
write_bw_log=seq_read_file
write_lat_log=seq_read_file
write_iops_log=seq_read_file
[seq_write_file]
direct=1
bs=1M
size=50G
time_based=1
runtime=300
ioengine=libaio
iodepth=16
end_fsync=1
log_avg_msec=1000
directory=/data
rw=write
write_bw_log=seq_write_file
write_lat_log=seq_write_file
write_iops_log=seq_write_file
Где /data — это директория на томе, который мы сравниваем (VM vs PVC).
pgbench: как и зачем именно так
Задача pgbench — померить итоговую производительность PostgreSQL, уже с учётом:
сетевой задержки,
оверхеда Kubernetes,
внутренних накладных расходов Postgres.
Я проверял 4 сценария:
In-memory | read-only
scale = 100— рабочий набор данных полностью помещается вshared_buffers = 10GB.Транзакции только на чтение (read-only), без модификаций.
Имитирует горячий кэш и активные сервисы читающего характера.
In-memory | read-write
Тот же
scale = 100.Стандартный сценарий
pgbenchсUPDATE/INSERT.Нагрузка, близкая к типичному OLTP с модификациями.
Disk-bound | read-only
scale = 300— часть данных уже “холодная”, без полного попадания в память.Тестируем вместе диск, планировщик, кеш-менеджер.
Disk-bound | read-write
Тот же
scale = 300, но с записью.
Для каждого сценария я прогонял pgbench с разным числом клиентов:
In-memory:
8,16,32,64,128соединений.Disk-bound:
8,16,32соединения.
Далее для каждой связки “сценарий + профиль окружения” я брал максимальное достигнутое значение TPS (по какому-то числу клиентов) и сравнивал их между собой.
Типовая команда выглядела примерно так:
# Подготовка:
pgbench -h <host> -p 5432 -U <user> -i -s 100 pgbench
# Пример прогона in-memory read-only:
pgbench -h <host> -p 5432 -U <user> \
-s 100 \
-c 32 \
-T 60 \
-M simple \
-S
И отдельно такие же прогоны для:
-s 300для disk-bound;без
-Sдля read-write сценариев.
Важно: одна и та же клиентская VM использовалась для всех тестов (native-vm и оба профиля CNPG), чтобы не подмешивать отличия сети/клиента.
Дисковая подсистема — результаты fio
Подробные результаты fio
Test | Metric | db-stage (VM Postgres) | cnpg-stage (yc-network-ssd, 533Gi) | cnpg-stage (yc-network-ssd-io-m3, 558Gi) |
|---|---|---|---|---|
randrw_80_20 (8K) | Read IOPS | ~5 900 | ~4 260 | ~10 200 |
randrw_80_20 (8K) | Write IOPS | ~1 500 | ~1 060 | ~2 540 |
randrw_80_20 (8K) | Read bandwidth | ~46.1 MiB/s (~48.3 MB/s) | ~33.3 MiB/s (~34.9 MB/s) | ~79.4 MiB/s (~83.3 MB/s) |
randrw_80_20 (8K) | Write bandwidth | ~11.6 MiB/s (~12.1 MB/s) | ~8.3 MiB/s (~8.7 MB/s) | ~19.9 MiB/s (~20.8 MB/s) |
randrw_80_20 (8K) | Average read latency | ~12.2 ms | ~5.2 ms | ~2.4 ms |
randrw_80_20 (8K) | Average write latency | ~37.6 ms | ~9.1 ms | ~3.1 ms |
randrw_80_20 (8K) | 95th percentile read latency | ~45 ms | ~19 ms | ~7.2 ms |
randrw_80_20 (8K) | 95th percentile write latency | ~102 ms | ~39 ms | ~9.4 ms |
randrw_80_20 (8K) | Disk utilization | ~99.96% ( | ~99.96% ( | n/a |
seq_read_file (1M) | Read IOPS | ~243 | ~248 | ~649 |
seq_read_file (1M) | Write IOPS | n/a | n/a | n/a |
seq_read_file (1M) | Read bandwidth | ~244 MiB/s (~256 MB/s) | ~248 MiB/s (~260 MB/s) | ~649 MiB/s (~681 MB/s) |
seq_read_file (1M) | Write bandwidth | n/a | n/a | n/a |
seq_read_file (1M) | Average read latency | ~131 ms | ~64.6 ms | ~24.6 ms |
seq_read_file (1M) | Average write latency | n/a | n/a | n/a |
seq_read_file (1M) | 95th percentile read latency | ~176 ms | ~108 ms | ~44 ms |
seq_read_file (1M) | 95th percentile write latency | n/a | n/a | n/a |
seq_read_file (1M) | Disk utilization | ~100% ( | ~99.92% ( | ~99.90% ( |
seq_write_file (1M) | Read IOPS | n/a | n/a | n/a |
seq_write_file (1M) | Write IOPS | ~100–102 | ~245 | ~403 |
seq_write_file (1M) | Read bandwidth | n/a | n/a | n/a |
seq_write_file (1M) | Write bandwidth | ~102 MiB/s (~106 MB/s) | ~246 MiB/s (~258 MB/s) | ~403 MiB/s (~423 MB/s) |
seq_write_file (1M) | Average read latency | n/a | n/a | n/a |
seq_write_file (1M) | Average write latency | ~315 ms | ~65.0 ms | ~39.7 ms |
seq_write_file (1M) | 95th percentile read latency | n/a | n/a | n/a |
seq_write_file (1M) | 95th percentile write latency | ~751 ms | ~118 ms | ~80 ms |
seq_write_file (1M) | Disk utilization | ~99.48% ( | ~99.71% ( | ~99.59% ( |
fio — итоги
Test | Metric | yc-ssd vs VM | io-m3 vs VM |
|---|---|---|---|
randrw_80_20 (8K) | Read IOPS | ~0.7× | ~1.7× |
randrw_80_20 (8K) | Write IOPS | ~0.7× | ~1.7× |
randrw_80_20 (8K) | Avg latency (R/W) | ~0.4× / 0.25× | ~0.2× / 0.1× |
seq read 1M | BW | ~1.0× | ~2.7× |
seq write 1M | BW | ~2.4× | ~4.0× |
Ключевое наблюдение:
cnpg-yc-network-ssdпо диску не хуже VM, а по латенсиям местами даже лучше.cnpg-yc-network-ssd-io-m3по диску заметно лучше, чем VM (до 1.7–4× по разным метрикам).
Если смотреть только на диск, Kubernetes/CloudNativePG ничего не ломают — наоборот, ssd-io даёт серьёзный запас по I/O.
PostgreSQL — результаты pgbench
Run 1 — native-vm
1. memory | read-only
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 323.8834 | 24.699 | 6.746 | 275.276 | 97084 |
16 | 632.9969 | 25.275 | 4.997 | 322.394 | 189713 |
32 | 1256.0993 | 25.474 | 5.974 | 430.211 | 376331 |
64 | 2528.0003 | 25.315 | 2.827 | 604.481 | 756920 |
128 | 4647.3853 | 27.541 | 15.614 | 1054.457 | 1389469 |
2. memory | read-write
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 85.1233 | 93.808 | 32.722 | 319.056 | 25537 |
16 | 168.0096 | 95.071 | 27.212 | 330.347 | 50395 |
32 | 315.0302 | 101.545 | 32.643 | 417.242 | 94683 |
64 | 279.0847 | 228.675 | 171.501 | 933.835 | 83981 |
3. disk-bound | read-only (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 266.0939 | 30.080 | 12.650 | 278.688 | 79763 |
16 | 489.3210 | 32.759 | 13.741 | 416.745 | 146913 |
32 | 1301.4582 | 24.627 | 7.390 | 401.414 | 391713 |
4. disk-bound | read-write (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 70.3715 | 113.720 | 40.130 | 297.878 | 21163 |
16 | 132.3504 | 121.090 | 45.843 | 291.197 | 39829 |
32 | 175.9314 | 182.255 | 72.873 | 524.280 | 52871 |
Run 2 — cnpg-yc-network-ssd
1. memory | read-only
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 271.4572 | 29.386 | 14.292 | 424.964 | 80861 |
16 | 505.4751 | 31.636 | 14.721 | 609.677 | 150955 |
32 | 860.2641 | 37.170 | 23.818 | 881.338 | 257659 |
64 | 1513.5510 | 42.216 | 20.370 | 1227.816 | 453903 |
128 | 2760.2675 | 46.347 | 17.858 | 2391.377 | 828080 |
2. memory | read-write
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 45.1320 | 205.699 | 52.248 | 686.389 | 13499 |
16 | 82.3552 | 193.479 | 46.000 | 553.582 | 24646 |
32 | 139.0492 | 229.477 | 58.532 | 796.448 | 41637 |
64 | 232.1353 | 273.886 | 65.644 | 967.231 | 69315 |
3. disk-bound | read-only (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 237.9814 | 33.386 | 16.957 | 380.222 | 70865 |
16 | 425.4590 | 37.219 | 23.289 | 675.216 | 126380 |
32 | 1034.7463 | 31.096 | 14.128 | 286.819 | 307070 |
4. disk-bound | read-write (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 58.8517 | 136.402 | 38.109 | 376.359 | 17523 |
16 | 104.3199 | 153.202 | 52.816 | 615.156 | 31024 |
32 | 134.1740 | 237.218 | 79.691 | 879.186 | 39887 |
Run 3 — cnpg-yc-network-ssd-io-m3
1. memory | read-only
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 286.9269 | 27.881 | 12.521 | 315.444 | 86000 |
16 | 536.8708 | 29.800 | 8.868 | 412.213 | 160855 |
32 | 922.2607 | 34.696 | 17.007 | 648.287 | 276113 |
64 | 1540.5269 | 41.540 | 18.707 | 936.857 | 460831 |
128 | 2463.9108 | 51.945 | 26.090 | 1514.833 | 735612 |
2. memory | read-write
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 37.8816 | 211.146 | 58.101 | 621.986 | 11349 |
16 | 69.3881 | 230.494 | 66.710 | 458.642 | 20800 |
32 | 115.6797 | 276.503 | 74.043 | 592.641 | 34659 |
64 | 191.5338 | 333.897 | 103.792 | 877.013 | 57368 |
3. disk-bound | read-only (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 256.4876 | 31.186 | 15.430 | 332.041 | 76878 |
16 | 460.9535 | 34.708 | 17.969 | 396.939 | 138120 |
32 | 800.6511 | 39.964 | 22.748 | 584.547 | 239760 |
4. disk-bound | read-write (scale=300)
number of clients | tps | latency average (ms) | latency stddev (ms) | initial connection time (ms) | number of transactions actually processed |
|---|---|---|---|---|---|
8 | 35.0369 | 228.285 | 65.286 | 406.871 | 10503 |
16 | 59.9819 | 266.669 | 63.726 | 546.668 | 17973 |
32 | 102.1597 | 313.090 | 75.048 | 654.571 | 30613 |
Сводная таблица по pgbench
1. Overview (максимальный TPS по сценарию)
run / profile | description / storage type | scale (memory) | scale (disk) | max tps memory read-only (clients) | max tps memory read-write (clients) | max tps disk read-only (clients) | max tps disk read-write (clients) |
|---|---|---|---|---|---|---|---|
Run 1 — | vm / local ssd | 100 | 300 | 4647.3853 (128) | 315.0302 (32) | 1301.4582 (32) | 175.9314 (32) |
Run 2 — | k8s / yc network-ssd | 100 | 300 | 2760.2675 (128) | 232.1353 (64) | 1034.7463 (32) | 134.1740 (32) |
Run 3 — | k8s / yc ssd-io (b3-m3) | 100 | 300 | 2463.9108 (128) | 191.5338 (64) | 800.6511 (32) | 102.1597 (32) |
2. Деградация vs native-vm (по максимальному TPS)
Падение производительности относительно Run 1 — native-vm, в процентах (чем больше число, тем хуже).
scenario | Run 2 — cnpg-yc-network-ssd | Run 3 — cnpg-yc-network-ssd-io-m3 |
|---|---|---|
memory read-only | 40.6% | 47.0% |
memory read-write | 26.3% | 39.2% |
disk-bound read-only (scale=300) | 20.5% | 38.5% |
disk-bound read-write (scale=300) | 23.7% | 41.9% |
3. Итоги по pgbench
Перенос в Kubernetes на
network-ssdдаёт ~20–40% деградации по TPS относительно native PostgreSQL на VM.Переход на
ssd-io(b3-m3) улучшает диски, но не улучшает TPS.Напротив, отставание от native-vm ещё усиливается: до ~38–47% ниже по пиковому TPS во всех сценариях.
Как ходит запрос: VM vs Kubernetes
Хочется наглядно показать, где возникает дополнительный оверхед.
1. Путь до native PostgreSQL на VM
pgbenchживёт на отдельной VM в Yandex Cloud.Подключается по TCP к VM с PostgreSQL.
На стороне БД:
нет kube-proxy, Service, Pod-сети;
только сеть YC + процесс
postgres.
[pgbench VM]
|
| TCP (иногда TLS, в зависимости от настроек)
v
+----------------------+
| VM с PostgreSQL |
| (native, systemd) |
+----------+-----------+
|
v
postgres процесс
2. Путь до PostgreSQL в CloudNativePG
Всё так же начинается с
pgbenchна отдельной VM.Подключение идёт к адресу Kubernetes Service (ClusterIP/LoadBalancer).
Далее:
kube-proxy / iptables;
выбор Pod’а;
Pod-сеть;
контейнер с PostgreSQL.
Плюс к этому:
TLS 1.3 по умолчанию (по конфигу CNPG);
дополнительные процессы/обвязка (WAL-архивер, контроллер CNPG и т.д.).
[pgbench VM]
|
| TCP (TLS)
v
+-------------------------+
| Yandex Cloud сеть |
+-----------+-------------+
|
v
+-------------------------+
| Node Kubernetes |
| (kubelet, kube-proxy) |
+-----------+-------------+
|
v
iptables / kube-proxy
|
v
ClusterIP Service
|
v
+-------------------------+
| Pod: PostgreSQL (CNPG) |
| контейнер postgres |
+-------------------------+
Почему такие просадки и что с этим делать?
По дискам CNPG не хуже VM, а на
ssd-ioдаже существенно лучше.Конфиги PostgreSQL по сути одинаковые:
те же
shared_buffers = 10GB,max_connections = 5000,wal_level = logical,включённый
archive_mode.
Тесты выполнялись с одной и той же клиентской VM, по сети, без Unix-socket.
Основные источники просадки:
1. Сетевой оверхед Kubernetes
Дополнительные hop’ы:
вход на Node;
kube-proxy/iptables;кластерная сеть;
Pod-сеть.
Каждый hop добавляет задержку и системные вызовы.
В in-memory тестах, где диск не является узким местом, именно эти накладные расходы становятся доминирующими.
2. TLS и его цена
В CNPG TLS 1.3 строгий и завязан на внутреннюю PKI оператора.
В native-vm SSL тоже включён, но сочетание протокола/шифров отличается.
При большом числе одновременных соединений шифрование становится заметной составляющей.
3. Контейнерное окружение
cgroups, лимиты и возможный CPU throttling;
шум от соседних Pod’ов на ноде;
дополнительные слои сетевой абстракции (veth, bridge/overlay).
4. Оверхед оператора и служебных компонентов
фоновые процессы CNPG;
WAL-архивер (дополнительная работа с WAL);
мониторинг, метрики, служебные запросы.
Все эти эффекты:
сложно вылечить простым тюнингом
postgresql.conf;плохо диагностируются простыми средствами (на графиках видно “есть просадка”, а где именно она рождается — уже отдельное исследование).
Выводы
1. Что говорят цифры
По данным fio:
CNPG на
yc-network-ssd≈ VM по диску;CNPG на
yc-network-ssd-io-m3существенно быстрее VM по I/O.
По данным pgbench:
Перенос PostgreSQL в Kubernetes (CloudNativePG +
network-ssd) даёт ~20–40% деградации по TPS.Переход на более быстрый диск (
ssd-io) не возвращает TPS, а отставание от native-vm даже растёт до ~38–47%.
2. Практическая интерпретация
Если для вашего проекта:
критичны TPS и latency,
а плюшки Kubernetes (оператор, GitOps, единый пайплайн для приложений и БД) не являются обязательными,
то:
Native PostgreSQL на VM в Yandex Cloud даёт более предсказуемую и высокую производительность.
CloudNativePG приносит:
удобный декларативный подход к управлению кластерами,
автоматизацию репликации и failover,
лучшее встраивание в Kubernetes-процессы.
Но за это приходится платить существенной ценой в производительности, особенно заметной при in-memory нагрузках и большом количестве запросов.
3. Решение по итогам тестов
В моём кейсе:
Конфиги PostgreSQL на native и CNPG сравнены и выровнены.
Storage “в лоб” либо сопоставим, либо лучше под CNPG.
Основной вклад в просадку TPS вносят:
сетевые hop’ы,
TLS,
контейнерное окружение и шум от соседей,
служебная обвязка оператора.
Полученная просадка до ~40% TPS для проекта критична.
Для данного проекта я отказался от использования CloudNativePG и оставил PostgreSQL на VM в Yandex Cloud, реализуя HA/репликацию/бэкапы классическими средствами.
UPD: ответы на частые вопросы из комментариев
Спасибо всем, кто не поленился разобрать методику и ткнуть в слабые места. Ниже — ответы на самые частые и справедливые вопросы, чтобы было понятнее, что именно я сравнивал и где границы применимости выводов.
UPD: ответы на частые вопросы из комментариев
Про TLS и «сравнение в молоко»
«Tls? Ну, камон, базу без TLS запускать в 2025…»
В тестах TLS был включён и на VM, и в cnpg:
на VM:
ssl = on, стандартные сертификаты Ubuntu;в cnpg:
ssl = 'on', ca/cert/key от оператора,ssl_min_protocol_version = 'TLSv1.3'.
Клиент (pgbench) — отдельная VM в том же облаке, в обоих вариантах ходит по TCP+TLS.
Никаких Unix-сокетов и «нечестного» локального доступа в случае native-VM не было.
Где крутился pgbench и какой сетевой путь
Сетевой путь во всех тестах был таким:
pgbenchна отдельной VM в YC;дальше — через сеть провайдера до:
VM с PostgreSQL (native), либо
ноды кластера YC Managed Kubernetes с cnpg (Service → kube-proxy/CNI → Pod).
Это был осознанный выбор: смоделировать один из типичных боевых кейсов, когда:
база живёт на отдельной VM или в k8s,
приложение/клиент — на другой VM/ноде, а не на том же хосте.
Сценарий «pgbench в k8s, в том же namespace/кластере, что и БД» — валидный и интересный, но в эту статью не попал.
Шумные соседи, taints/tolerations, QoS
«А ничего, что на ноде кластера куча сервисов?»
«numa? cpu pinning? guaranteed qos class?»
В рамках этих тестов я старался максимально «почистить» окружение:
для cnpg была выделенная нода в кластере YC Managed Kubernetes;
на ноде висели taints, а Pod cnpg имел соответствующие
tolerations;все служебные Pod’ы (логи, метрики и т.п.) были сжаты по
requests/limits, чтобы шум от них был минимален;Pod cnpg получил гарантированные реквесты CPU/RAM, эквивалентные ресурсам native VM.
Чего не было сделано:
NUMA pinning (привязка конкретных ядер),
CPU Manager / dedicated CPU pool,
ручной тюнинг планировщика Linux (sysctl,
sched_*, I/O scheduler и т.д.),игра с QoS Guaranteed с жёстким cpu-pinning.
И это, правда, важный момент:
я сравнивал не «идеально вытюнинганный k8s против идеально вытюнинганной VM», а два варианта «as is, но с минимальным здравым тюнингом». в рамках конкретного облачного провайдера - Yandex Cloud
Конфиги PostgreSQL: одинаковые или нет?
«Автор использует по-разному настроенные PostgreSQL»
Изначальный postgresql.conf на VM действительно был почти дефолтным, но боевой конфиг живёт в conf.d/custom.conf. Для честного сравнения я привёл его к тому, что генерирует cnpg:
На VM (conf.d/custom.conf):
listen_addresses = '*'
max_connections = 5000
shared_buffers = 10GB
max_wal_senders = 20
wal_level = logical
archive_mode = on
logging_collector = on
shared_preload_libraries = 'pg_stat_statements'
В cnpg (custom.conf от оператора):
listen_addresses = '*'
port = '5432'
max_connections = '5000'
shared_buffers = '10GB'
wal_level = 'logical'
archive_mode = 'on'
logging_collector = 'on'
ssl = 'on'
...
max_worker_processes = '32'
max_parallel_workers = '32'
max_replication_slots = '32'
wal_keep_size = '512MB'
wal_log_hints = 'on'
То есть:
ключевые вещи, влияющие на нагрузку
pgbench(соединения,shared_buffers,wal_level,archive_mode, TLS) — синхронизированы;различия остались в «операторной навеске» (
wal_keep_size,max_parallel_workers, логирование в csvlog и пр.).
Я осознанно не пытался «подбить» стендалон PostgreSQL под побитовый клон cnpg-конфига — задача статьи была показать, что получится, если:
«Мы берём наш боевой конфиг Postgres, поднимаем его на VM и поднимаем его же в cnpg в YC — что будет с TPS?»
Почему не сравнивал с Managed PostgreSQL в Yandex Cloud
«А чего тогда вообще с managed db не сравнить?»
Есть две причины:
Практическая. Я выбирал между:
self-managed VM+Postgres и
self-managed cnpg в k8s,
потому что так устроена моя инфраструктура и процессы.
Методологическая. У YC Managed PostgreSQL есть:
закрытые от пользователя оптимизации (ядро, диски, настройки),
и одновременно — жёсткие ограничения (например, лимиты на коннекты на ядро).
Это делает сравнение «VM vs cnpg vs MDB» гораздо менее прозрачным: мы уже сравниваем не только Postgres/среду, но и «сколько тюнинга за нас сделал провайдер».
В отдельной статье можно устроить именно «битву трёх миров» (VM, cnpg, MDB), но это будет уже другой эксперимент с другой постановкой задачи.
JIT, пулер соединений, тюнинг ОС
JIT. Я не выключал JIT явно ни там, ни там, оставил дефолты.
Для типичных
pgbench-запросов (простые короткие SQL) JIT обычно и так не включается из-за порога по стоимости.То есть влияние JIT на итоговые TPS в данном конкретном тесте — минимальное и одинаковое.
Пулеры (pgbouncer и пр.). Не использовались специально, чтобы:
не вносить дополнительный слой буферизации/лимитов,
мерить именно поведение «голого» PostgreSQL под нагрузкой.
Тюнинг ОС.
На VM — стандартный образ Ubuntu из YC, без глубокого ручного тюнинга
sysctl, I/O-планировщика и т.п.На нодах k8s — стандартные настройки Managed Kubernetes от Yandex Cloud (как есть).
Это осознанный компромисс: сравнение двух реалистичных, но не «вылизанных до упора» сетапов, максимально похожих на то, как это часто делается в жизни.
Что на самом деле утверждает эта статья
Я не утверждаю, что:
«PostgreSQL в Kubernetes всегда медленнее на 40%»;
«cnpg плохой/непригодный»;
«VM — единственно правильный путь».
Я утверждаю только следующее:
В моём кейсе (Yandex Cloud, PostgreSQL 14, одинаковый конфиг и ресурсы,
pgbenchс внешней VM, cnpg в YC Managed k8s)
я стабильно вижу 20–40% просадки TPS у cnpg относительно native-VM.fio показывает, что диски сами по себе не виноваты (особенно на
ssd-io),
а значит, существенная часть overhead’а лежит в:сетевом пути (Service/kube-proxy/CNI),
контейнерной обвязке (cgroups, планировщик, доп. контекст-свитчи),
операторной конфигурации.
Для м��их задач и моих SLO такой overhead — неприемлем, поэтому я отказался от миграции на cnpg именно в текущем виде.
И да — я полностью согласен с теми, кто пишет, что:
чтобы «докопаться до истины», нужно:
вынести Postgres в отдельный pod без оператора,
поиграть с
hostNetworkи CNI,включить CPU pinning / QoS guaranteed / NUMA-тюнинг,
прогнать дополнительные сценарии (в т.ч.
pgbenchвнутри кластера),
и тогда мы сможем точнее разделить:
«что даёт k8s/контейнеры»,
«что даёт оператор cnpg»,
«что даёт конкретный облачный провайдер».
