Миграция баз данных в 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/репликацию/бэкапы классическими средствами.

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


  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 на голом железе
    к сожалению нет возможности самому сделать стенд. Но может найдутся энтузиасты с возможностями? Пусть не подробный отчет, хотя бы цифры голые