Миграция баз данных в 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.

Я тестировал три сценария:

  1. 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.

  2. Последовательное чтение (seq_read_file)

    • bs=1M — имитация больших чтений (бэкапы, аналитику, последовательные сканы).

    • runtime=300, iodepth=16 — устойчивое измерение пропускной способности.

  3. Последовательная запись (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 сценария:

  1. In-memory | read-only

    • scale = 100 — рабочий набор данных полностью помещается в shared_buffers = 10GB.

    • Транзакции только на чтение (read-only), без модификаций.

    • Имитирует горячий кэш и активные сервисы читающего характера.

  2. In-memory | read-write

    • Тот же scale = 100.

    • Стандартный сценарий pgbench с UPDATE/INSERT.

    • Нагрузка, близкая к типичному OLTP с модификациями.

  3. Disk-bound | read-only

    • scale = 300 — часть данных уже “холодная”, без полного попадания в память.

    • Тестируем вместе диск, планировщик, кеш-менеджер.

  4. 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% (vdb)

~99.96% (vdd)

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% (vdb)

~99.92% (vdd)

~99.90% (vdd)

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% (vdb)

~99.71% (vdd)

~99.59% (vdd)

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 — native-vm

vm / local ssd

100

300

4647.3853 (128)

315.0302 (32)

1301.4582 (32)

175.9314 (32)

Run 2 — cnpg-yc-network-ssd

k8s / yc network-ssd

100

300

2760.2675 (128)

232.1353 (64)

1034.7463 (32)

134.1740 (32)

Run 3 — cnpg-yc-network-ssd-io-m3

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     |
+-------------------------+

Почему такие просадки и что с этим делать?

  1. По дискам CNPG не хуже VM, а на ssd-io даже существенно лучше.

  2. Конфиги PostgreSQL по сути одинаковые:

    • те же shared_buffers = 10GB,

    • max_connections = 5000,

    • wal_level = logical,

    • включённый archive_mode.

  3. Тесты выполнялись с одной и той же клиентской 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 не сравнить?»

Есть две причины:

  1. Практическая. Я выбирал между:

    • self-managed VM+Postgres и

    • self-managed cnpg в k8s,

    потому что так устроена моя инфраструктура и процессы.

  2. Методологическая. У 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 — единственно правильный путь».

Я утверждаю только следующее:

  1. В моём кейсе (Yandex Cloud, PostgreSQL 14, одинаковый конфиг и ресурсы, pgbench с внешней VM, cnpg в YC Managed k8s)
    я стабильно вижу 20–40% просадки TPS у cnpg относительно native-VM.

  2. fio показывает, что диски сами по себе не виноваты (особенно на ssd-io),
    а значит, существенная часть overhead’а лежит в:

    • сетевом пути (Service/kube-proxy/CNI),

    • контейнерной обвязке (cgroups, планировщик, доп. контекст-свитчи),

    • операторной конфигурации.

  3. Для моих задач и моих SLO такой overhead — неприемлем, поэтому я отказался от миграции на cnpg именно в текущем виде.

И да — я полностью согласен с теми, кто пишет, что:

  • чтобы «докопаться до истины», нужно:

    • вынести Postgres в отдельный pod без оператора,

    • поиграть с hostNetwork и CNI,

    • включить CPU pinning / QoS guaranteed / NUMA-тюнинг,

    • прогнать дополнительные сценарии (в т.ч. pgbench внутри кластера),

  • и тогда мы сможем точнее разделить:

    • «что даёт k8s/контейнеры»,

    • «что даёт оператор cnpg»,

    • «что даёт конкретный облачный провайдер».

Комментарии (23)


  1. pg_expecto
    18.11.2025 12:51

    Далее для каждой связки “сценарий + профиль окружения” я брал максимальное достигнутое значение TPS (по какому-то числу клиентов) и сравнивал их между собой.

    А почему вы уверены, что полученный результат не выброс ?

    Далее, какие выводы делаются на основании "latency stddev (ms) " ?

    Разница между сценариями , весьма значительна.


  1. outlingo
    18.11.2025 12:51

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

    А вот почему кубовые сервисы оказались таким дном - интеренсый вопрос. В теории, сеть добавит 1-2мс в худшем случае, откуда берется остальное?


  1. Sosnin
    18.11.2025 12:51

    для полноты картины: хорошо бы увидеть
    - сравнение СУБД в ВМ где диск физически прикручен с СУБД в k8s где диски тоже "поближе" к подам
    - сравнение СУБД в ОС на голом железе с СУБД в k8s на голом железе
    к сожалению нет возможности самому сделать стенд. Но может найдутся энтузиасты с возможностями? Пусть не подробный отчет, хотя бы цифры голые


    1. Sleuthhound
      18.11.2025 12:51

      Увы, но у Yandex.Cloud нет local-ssd на managed k8s, так что такой тест не получиться сделать. Если только разворачивать свой k8s на обычных VM с local-ssd, но это другая история и тут возникают вопросы - а правильно ли будет настроен кубер.


      1. Sosnin
        18.11.2025 12:51

        так я про ВМ во всяких х.Cloud и не говорил, там понятно, что слои физики и ВМ далеки друг от друга..

        свой гипервизор - под ним ВМ-ки, к которым прикрутить физические SSD nvme
        и на своем же гипервизоре ноды с физически прикрученными дисками


  1. gecube
    18.11.2025 12:51

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


    1. gecube
      18.11.2025 12:51

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


    1. sysbes Автор
      18.11.2025 12:51

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


    1. sysbes Автор
      18.11.2025 12:51

      конструктивно:

      • mdb Postgres в YC имеет ключевое ограничение - 200 коннектов на ядро ( в вашей терминологии - оптимизацию)

      • "шумные" соседи - их "импакт" был в тестах исключен, но в реальном продуктовом кластере у вас не может быть ноды вообще с одним подом Postgres - нужны логи, метрики т.д. - всегда будет что-то служебное

      Все тесты "синтестические" и каждый может сам на основе результатов этих тестов сделать свои собственные выводы и поделится ими с сообществом - почвой для которых и служит данный материал.

      Спасибо за Ваш комментарий.


      1. gecube
        18.11.2025 12:51

        у вас не может быть ноды вообще с одним подом Postgres - нужны логи, метрики т.д. - всегда будет что-то служебное

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


        1. sysbes Автор
          18.11.2025 12:51

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


          1. gecube
            18.11.2025 12:51

            numa? cpu pinning? guaranteed qos class?


            1. sysbes Автор
              18.11.2025 12:51

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


        1. budnikovsergey
          18.11.2025 12:51

          ну так-то на vm будут всё те же экспортеры и сборщики логов. Соседи отстреливаются через taints, если вдруг у нас такой highload. Так что единственная разница с VM это сетевой путь до пода, поскольку разницы между systemd unit и запущенными в контейнере сервисами нет никакой.
          Ну а так, на мой взгляд, внутрикуберовые постгрессы решают задачу множественности выделенных кластеров постгресса для множества сред исполнения: тестовые стенды. Или обычные кластера обычных сервисов, из которых не делают ядро сервисов высокочастотного криптотрейдинга. Всё это даёт отличную плотность использования ресурсов для рядовой нагрузки. Наборы на VM существенно более расточительные в плане инфраструктуры.


          1. gecube
            18.11.2025 12:51

            Привет, Сергей!

            В теории Вы абсолютно правы. Технически же появляется дьявол в деталях. Инстансы под кластера кубера и ВМ - разные (привет, таймвеб).

            Конфигурация ОС под ВМ и под кубер разная (привет яндекс)

            и прочая пачка влажных историй из эксплуатации.

            Чтобы провести чистый эксперимент - надо брать условный VM в хетцнере с бубунтой, ставить туда постгрес как пакет операционной системы. Далее брать в том же хетцнере такую же VM (можно взять ТУ ЖЕ самую после полного формата диска), катить на нее воркера кубеадм кластера (возьмем еще одну виртуалку) и тогда уже и сравнивать. Не забыть еще про диски - что есть разница между локальным диском системы и ebs снаружи (в хетцнере не так, как в яндексе, так как производительность диска не меняется в зависимости от размера диска). Короче, говорить просто в вакууме, что кубер медленнее - это вроде бы с одной стороны результат, а с другой - ну, он бессмысленен.


            1. budnikovsergey
              18.11.2025 12:51

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


              1. gecube
                18.11.2025 12:51

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


  1. andreynekrasov
    18.11.2025 12:51

    Там cilium с включенным netkit и заменой kube-proxy нельзя поставить? Мне кажется не должно быть большой разницы на одном железе.

    imho лучше сравнивать на своих серверах, а не виртуалках в облаке.


  1. vasyakrg
    18.11.2025 12:51

    Talos (cilium+linstor с дисками в одну реплику) с cnpg против патрони на etcd - и получаем одинаковый результат. Понятно что обвязка свое заберет, но блин не в -40% ))

    Тюнинг конфига посгреса в кубе тут наименьшую результативность принесет, если ничего другого не делать ни с кубом, ни с cni\csi


  1. 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 конечных сервисов, что нас уже устроило.


    1. Sleuthhound
      18.11.2025 12:51

      >>нужно запускать pgbench в кубе, в одном случае в том же ns, что и бд.

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


  1. voidstrx
    18.11.2025 12:51

    А jit везде был отключен?


  1. 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) Насколько я понял пулер соединений не использовался или использовался?

    Все эти тонкости могут повлиять на итоговую производительность в тестах.