Миграция баз данных в 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»,
«что даёт конкретный облачный провайдер».
Комментарии (23)

outlingo
18.11.2025 12:51В публичных облаках, как правило, всякие PaaS/SaaS/KaaS работют в ВМ - то есть по сути, отличие только в том, что взяли обычную ВМ и накидали слоев кубера. Никто не пустит разных юзеров в один куб - поэтому ВМ, в ней куб, и поехали. То, что вам показывают "кластеры БД" а не ВМ-ки, не значит что ВМ нет, их просто не показывают. В противном случае слишком велики риски получить совершенно неожиданный неприятный "подарок" от одного юзера лругому
А вот почему кубовые сервисы оказались таким дном - интеренсый вопрос. В теории, сеть добавит 1-2мс в худшем случае, откуда берется остальное?

Sosnin
18.11.2025 12:51для полноты картины: хорошо бы увидеть
- сравнение СУБД в ВМ где диск физически прикручен с СУБД в k8s где диски тоже "поближе" к подам
- сравнение СУБД в ОС на голом железе с СУБД в k8s на голом железе
к сожалению нет возможности самому сделать стенд. Но может найдутся энтузиасты с возможностями? Пусть не подробный отчет, хотя бы цифры голые
Sleuthhound
18.11.2025 12:51Увы, но у Yandex.Cloud нет local-ssd на managed k8s, так что такой тест не получиться сделать. Если только разворачивать свой k8s на обычных VM с local-ssd, но это другая история и тут возникают вопросы - а правильно ли будет настроен кубер.

Sosnin
18.11.2025 12:51так я про ВМ во всяких х.Cloud и не говорил, там понятно, что слои физики и ВМ далеки друг от друга..
свой гипервизор - под ним ВМ-ки, к которым прикрутить физические SSD nvme
и на своем же гипервизоре ноды с физически прикрученными дисками

gecube
18.11.2025 12:51Я поржал. Сравнение в молоко. Tls ? Ну, камон, базу без тлс запускать в 2025. А еще, дорогой товарищ, ты поотключал какие либо сервисы на узле кластера, чтобы не было истории с шумными соседями ? Тюнинг планировщика линукс ? Нет ? А ничего, что даже настройки линукса на контейнерной машине и на стендэлоун виртуалке у облачных провайдеров отличаются? А чего тогда вообще с managed db не сравнить ? Ну, чтобы база в кубере натурально проиграла ? Потому что у облака явно в их managed db сервисах есть очень крутые оптимизации… не знаю, выводы так себе, прям…

gecube
18.11.2025 12:51Ну, и zolando писать… ну, полный стыд. Скриншоты для истории оставил. Вот и весь уровень статьи.

sysbes Автор
18.11.2025 12:51Спасибо! Рад, что смог вас порадовать. Я учту ваши комментарии в будущих тестах.

sysbes Автор
18.11.2025 12:51конструктивно:
mdb Postgres в YC имеет ключевое ограничение - 200 коннектов на ядро ( в вашей терминологии - оптимизацию)
"шумные" соседи - их "импакт" был в тестах исключен, но в реальном продуктовом кластере у вас не может быть ноды вообще с одним подом Postgres - нужны логи, метрики т.д. - всегда будет что-то служебное
Все тесты "синтестические" и каждый может сам на основе результатов этих тестов сделать свои собственные выводы и поделится ими с сообществом - почвой для которых и служит данный материал.
Спасибо за Ваш комментарий.

gecube
18.11.2025 12:51у вас не может быть ноды вообще с одним подом Postgres - нужны логи, метрики т.д. - всегда будет что-то служебное
валидно! Но тогда с тем же успехом можно констатировать, что это все на ноде с постгрес требует специфичной настройки, чтобы не ломать производительность pg. А самое главное - чтобы не заезжали другие, левые, пользовательские поды.

sysbes Автор
18.11.2025 12:51Безусловно. Для подов и ноды были сконфигурированы соответствующие Taints и Tolerations. В рамках теста была выделенная нода для cnpg. Все "служебные" поды имели ограничения по запросам и лимитам ресурсов - "шум" был сведен к возможному минимум. Так же были выделеные гаранитрованые реквесты для пода cnpg равные по ресурсам native vm c Postgres.

gecube
18.11.2025 12:51numa? cpu pinning? guaranteed qos class?

sysbes Автор
18.11.2025 12:51Согласен с вами. Если мы хотим разобраться в причинах просадки и попытаться нивелировать эффект, то нужно "копать" в данном направлении. В этом все и дело. В статье - мы говорим только о сравнении в "лоб" as is и синтетических тестах.

budnikovsergey
18.11.2025 12:51ну так-то на vm будут всё те же экспортеры и сборщики логов. Соседи отстреливаются через taints, если вдруг у нас такой highload. Так что единственная разница с VM это сетевой путь до пода, поскольку разницы между systemd unit и запущенными в контейнере сервисами нет никакой.
Ну а так, на мой взгляд, внутрикуберовые постгрессы решают задачу множественности выделенных кластеров постгресса для множества сред исполнения: тестовые стенды. Или обычные кластера обычных сервисов, из которых не делают ядро сервисов высокочастотного криптотрейдинга. Всё это даёт отличную плотность использования ресурсов для рядовой нагрузки. Наборы на VM существенно более расточительные в плане инфраструктуры.
gecube
18.11.2025 12:51Привет, Сергей!
В теории Вы абсолютно правы. Технически же появляется дьявол в деталях. Инстансы под кластера кубера и ВМ - разные (привет, таймвеб).
Конфигурация ОС под ВМ и под кубер разная (привет яндекс)
и прочая пачка влажных историй из эксплуатации.
Чтобы провести чистый эксперимент - надо брать условный VM в хетцнере с бубунтой, ставить туда постгрес как пакет операционной системы. Далее брать в том же хетцнере такую же VM (можно взять ТУ ЖЕ самую после полного формата диска), катить на нее воркера кубеадм кластера (возьмем еще одну виртуалку) и тогда уже и сравнивать. Не забыть еще про диски - что есть разница между локальным диском системы и ebs снаружи (в хетцнере не так, как в яндексе, так как производительность диска не меняется в зависимости от размера диска). Короче, говорить просто в вакууме, что кубер медленнее - это вроде бы с одной стороны результат, а с другой - ну, он бессмысленен.

budnikovsergey
18.11.2025 12:51По облачной части тест достаточно хорош: в обсуждаемом тесте постгресс тестируется на одинаковых compute одного провайдера. Также мы никак не лимитированы в настройках нод кубера в managed kubernetes от yandex cloud: daemonset + nsexec/nsenter, я таким успешно пользуюсь. Так что всем остальным в плане облака можно смело пренебречь в рамках обсуждаемого тестирования производительности postgresql в единых условиях. Однако автор использует по разному настроенные postgresql, в качестве аргумента используя "я настроил 10 одинаковых параметров", игнорируя остальную "навеску" от операторного решения. Я бы просто поднял под с голым постгрессом без всяких операторов с одинаковым конфигом на VM и в поде.

gecube
18.11.2025 12:51тогда получается, надо брать настройки от "кубернетесовского" cnpg постгреса и инжектировать в standalone, а дальше обсуждать влияние именно от k8s стека. Кстати, его можно легко нивелировать - через правильное использование csi (local path provisioner?), host network (чтобы не было задержки cni) etc.

andreynekrasov
18.11.2025 12:51Там cilium с включенным netkit и заменой kube-proxy нельзя поставить? Мне кажется не должно быть большой разницы на одном железе.
imho лучше сравнивать на своих серверах, а не виртуалках в облаке.

vasyakrg
18.11.2025 12:51Talos (cilium+linstor с дисками в одну реплику) с cnpg против патрони на etcd - и получаем одинаковый результат. Понятно что обвязка свое заберет, но блин не в -40% ))
Тюнинг конфига посгреса в кубе тут наименьшую результативность принесет, если ничего другого не делать ни с кубом, ни с cni\csi

npu1gh0st
18.11.2025 12:51Спасибо за статью, изложенная в ней методика тестирования имеет множество серых зон из-за которых выводы автора видятся преждевременными. Но отмечу, что сама тема статьи шикарна и актуальна.
Оставлю тут свои пожелания, для повторного тестирования:
клиента в виде pgbench некорректно запускать с отдельной vm - такого в реальной микросервисной среде не бывает, в обоих случаях нужно запускать pgbench в кубе, в одном случае в том же ns, что и бд.
Обязательно исключить троллинг на контейнерах Postgres, для этого выдать целочисленные и равные реквесты и лимиты на cpu, qos class guaranteed и получение ядер из dedicated пула с правильно настроенным cpu manager
Ну и с советами вышел я тоже согласен, в тестировании не хватает промежуточных вариантов, типо просто пода с голым postgres без оператора.
Для контекста, перешли из managed database postgres из yandex на postgres оператор zalando в кубах Cloud.ru и имеем схожую максимальную производительность в qps от той что была ранее (потеряли не более 5-7%) для всех сервисов для которых не критично ограничение в 1Гб/с на storage, к сожалению в cloud.ru нет более быстрых storage классов и для достижения сопоставимой производительности, нам пришлось шардировать кластера. Но у нас и не стояло задачи получить столько же, у сервисов после переезда не оказало влияния на созданные графики slo/sla конечных сервисов, что нас уже устроило.

Sleuthhound
18.11.2025 12:51>>нужно запускать pgbench в кубе, в одном случае в том же ns, что и бд.
Тут может быть куча разных кейсов. БД как правио живут в разных ns с приложениями, более того, БД могут жить в отдельном k8s чем приложение и это тоже валидный кейс.

Sleuthhound
18.11.2025 12:51Статья неплохая, но есть много вопросов
1) Почему 14-я версия пг? На дворе уже 18-я есть. Ну хотя бы на 17-й тестировали;
2) Не указано производился ли тюнинг ОС на VM: измененные параметры sysctl, параметры i/o планировщика и прочее
3) Не указана версия ubuntu на VM
4) Не понял из статьи был ли это Managet k8s от Yandex или или свой на VM, а так же не понял был ли это чистый кластер (без соседей) или там что-то уже работало.
5) Про jit в пг выше валидный вопрос.
6) Насколько я понял пулер соединений не использовался или использовался?
Все эти тонкости могут повлиять на итоговую производительность в тестах.
pg_expecto
А почему вы уверены, что полученный результат не выброс ?
Далее, какие выводы делаются на основании "latency stddev (ms) " ?
Разница между сценариями , весьма значительна.