Тонкая настройка autovacuum в PostgreSQL 16
14 мар 2025 · 7 мин чтения
Autovacuum в PostgreSQL — это фоновый процесс, который следит за таблицами и вызывает VACUUM и ANALYZE автоматически. По умолчанию его параметры подходят для небольших баз, но в продакшн-системах с высокой нагрузкой на запись они часто приводят к table bloat и деградации производительности.
Ключевые параметры для агрессивной таблицы транзакций:
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 2,
autovacuum_vacuum_insert_scale_factor = 0.005
);
Параметр vacuum_scale_factor = 0.01 означает: запустить VACUUM, как только 1% строк в таблице помечен как мёртвый. Для таблицы на 10 млн строк это 100 000 — гораздо меньше дефолтных 20%.
Для связки с приложением на Rust используем deadpool-postgres, который позволяет держать пул из 32 соединений без блокировок:
// Оптимизированный пул соединений к PostgreSQL через tokio
use tokio_postgres::{Client, Config, NoTls};
use deadpool_postgres::{Manager, Pool, PoolConfig};
pub async fn build_pool(dsn: &str) -> Result<Pool, Box<dyn std::error::Error>> {
let config = dsn.parse::<Config>()?;
let manager = Manager::new(config, NoTls);
let pool_cfg = PoolConfig {
max_size: 32,
..Default::default()
};
Ok(Pool::builder(manager)
.config(pool_cfg)
.build()?)
}
Мониторинг: запрос pg_stat_user_tables по полям n_dead_tup, last_autovacuum и autovacuum_count позволяет понять, справляется ли autovacuum с нагрузкой в реальном времени.
Tokio каналы: когда mpsc, а когда broadcast
28 фев 2025 · 5 мин чтения
В Tokio есть четыре типа каналов: mpsc, broadcast, oneshot и watch. Каждый решает свою задачу, и неправильный выбор ведёт к потере сообщений или deadlock.
mpsc (multi-producer, single-consumer) — стандартный выбор для очередей задач. Поддерживает backpressure через bounded-вариант: если буфер полон, отправители ждут. Идеален для fan-in паттерна.
broadcast использует ring-buffer фиксированного размера. Все подписчики получают копию каждого сообщения. Если подписчик отстаёт и буфер переполняется, он получит RecvError::Lagged. Подходит для событий, которые нужны всем — например, сигналы shutdown.
use tokio::sync::{mpsc, broadcast};
// Задачи в очередь — mpsc
let (tx, mut rx) = mpsc::channel::<Task>(256);
// Сигнал остановки всем воркерам — broadcast
let (shutdown_tx, _) = broadcast::channel::<()>(1);
let mut shutdown_rx = shutdown_tx.subscribe();
tokio::select! {
task = rx.recv() => { /* обработка задачи */ }
_ = shutdown_rx.recv() => { break; }
}
oneshot — для одного ответа на запрос (request-response). watch — когда важно только последнее значение (конфиги, текущее состояние сервиса).
HPA vs VPA в Kubernetes: практический разбор
10 фев 2025 · 6 мин чтения
HPA (HorizontalPodAutoscaler) масштабирует количество реплик на основе метрик — CPU, памяти или custom-метрик через Metrics API. Работает в реальном времени: при превышении порога контроллер добавляет поды, при снижении — убирает.
VPA (VerticalPodAutoscaler) анализирует исторические данные потребления и корректирует requests/limits контейнеров. Главная проблема: в режиме Auto VPA пересоздаёт поды для применения новых значений, что вызывает кратковременный downtime.
# HPA по CPU — классика
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Использовать HPA и VPA одновременно на CPU/memory нельзя — они будут конкурировать. Рабочая связка: VPA в режиме Off (только рекомендации) + HPA по CPU. VPA помогает правильно выставить requests, HPA — отвечает за горизонтальное масштабирование.
Трассировка TCP-соединений через eBPF без перезапуска сервиса
22 янв 2025 · 5 мин чтения
Классический подход к трассировке — добавить логирование в код и перезапустить сервис. С eBPF это не нужно: можно повесить kprobe прямо на ядерную функцию tcp_connect и получать события в реальном времени без изменения приложения.
Данные передаются через ring buffer (рекомендуется вместо устаревшего perf_event_array): меньше копирований, нет потерь при пике событий, читается одним системным вызовом.
# bpftrace: отслеживаем все новые TCP-соединения
bpftrace -e '
kprobe:tcp_connect {
$sk = (struct sock *)arg0;
printf("%-6d %-16s → %s:%d\n",
pid, comm,
ntop(AF_INET, $sk->__sk_common.skc_daddr),
$sk->__sk_common.skc_dport >> 8
);
}'
Для production используем libbpf + CO-RE (Compile Once – Run Everywhere): программа компилируется один раз и работает на любом ядре ≥ 5.8 без пересборки. Skeleton-файл, генерируемый bpftool gen skeleton, содержит весь необходимый loader-код на C.
WAL-G + S3: непрерывные бэкапы PostgreSQL в продакшне
5 янв 2025 · 6 мин чтения
WAL-G — инструмент для непрерывного архивирования WAL-сегментов и базовых бэкапов PostgreSQL напрямую в объектное хранилище. В отличие от pg_basebackup, поддерживает инкрементальные бэкапы, delta-копии и параллельную загрузку.
Ключевое преимущество — PITR (Point-In-Time Recovery): восстановление базы на любой момент времени в пределах хранимых WAL-сегментов. Для бизнеса это означает RPO в секунды при цене хранения S3.
# postgresql.conf — включаем архивирование
archive_mode = on
archive_command = 'wal-g wal-push %p'
archive_timeout = 60
# Базовый бэкап (запускать по cron)
WALG_S3_PREFIX=s3://backups/pg \
AWS_REGION=eu-central-1 \
wal-g backup-push /var/lib/postgresql/16/main
# Восстановление на момент времени
wal-g backup-fetch /var/lib/postgresql/16/main LATEST
# recovery.conf: recovery_target_time = '2025-01-04 23:00:00'
Retention: wal-g delete retain FULL 7 оставляет 7 последних полных бэкапов с их WAL-цепочками. Мониторим через pg_stat_archiver — поле failed_count должно быть равно нулю.
io_uring изнутри: как устроен новый async I/O в Linux
18 дек 2024 · 7 мин чтения
io_uring (добавлен в Linux 5.1) — принципиально другой подход к асинхронному I/O. Вместо syscall на каждую операцию приложение и ядро общаются через два кольцевых буфера в shared memory: SQ (submission queue) для запросов и CQ (completion queue) для результатов.
В режиме SQPOLL выделенный ядерный поток следит за SQ, и приложению вообще не нужно делать syscalls для отправки операций — только читать CQ. При интенсивном I/O это даёт измеримое снижение latency.
use io_uring::{IoUring, opcode, types};
let mut ring = IoUring::new(128)?;
let fd = types::Fd(file.as_raw_fd());
let read_e = opcode::Read::new(fd, buf.as_mut_ptr(), buf.len() as u32)
.build()
.user_data(0x42);
unsafe { ring.submission().push(&read_e)?; }
ring.submit_and_wait(1)?;
let cqe = ring.completion().next().unwrap();
println!("Read {} bytes", cqe.result());
Fixed buffers (IORING_REGISTER_BUFFERS) убирают накладные расходы на маппинг страниц при каждой операции. На синтетической нагрузке io_uring в SQPOLL-режиме показывает на 15–20% меньше CPU usage по сравнению с epoll.