Files
notifkit/profiling/docker-compose.yml
T
coo1estguy 880fe32b03 perf: rebuild profiling harness and reduce per-message pipeline cost
- Rebuild profiling suite with compose CLI, in-network k6 load generator, and production-shaped tiers
- Add fused pipeline mode for in-memory single-user stage hand-offs
- Route directly by contact ID and defer template rendering to delivery
- Add Redis connection sharing, auto-pipelining, stream read coalescing, and chunked provider throttling
- Switch hot message_logs and delivery_outbox inserts to unnest bulk SQL queries
- Drop redundant indexes on message_logs
- Optimize worker batching, flush concurrency, and dataloader windowing
2026-09-29 00:35:01 +05:30

199 lines
5.5 KiB
YAML

# Driven by src/runner.ts through the docker compose CLI; every ${VAR} below is
# set per tier by the runner. Nodes run the image the runner builds as
# notifkit-prof-node from profiling/Dockerfile.
name: notifkit-prof
x-node-env: &node-env
NODE_ENV: production
DATABASE_URL: postgres://notifkit:password@db:5432/notifkit
REDIS_URL: redis://redis:6379
ADMIN_API_KEY: perf-admin-key
HOST: 0.0.0.0
LOG_LEVEL: ${LOG_LEVEL:-info}
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-50}
DELIVERY_CONCURRENCY: ${DELIVERY_CONCURRENCY:-200}
DB_MAX_CONNECTIONS: ${DB_MAX_CONNECTIONS:-5}
PIPELINE_FUSED: ${PIPELINE_FUSED:-false}
RATE_LIMIT_PER_HOUR: ${RATE_LIMIT_PER_HOUR:-100}
# Far above any backlog a run builds: MAXLEN trims unread entries, and a
# trimmed notification is simply lost.
QUEUE_MAX_LEN: ${QUEUE_MAX_LEN:-5000000}
UNSUBSCRIBE_SECRET: profiling-unsubscribe-secret-0123456789
PUBLIC_URL: http://localhost:3000
PROVIDER_LATENCY_MS: ${PROVIDER_LATENCY_MS:-150-250}
PROVIDER_FAILURE_RATE: ${PROVIDER_FAILURE_RATE:-0}
# Worker services wait for the runner to open this gate, so ingest can be
# measured on its own and the backlog it builds drained as a second phase.
WORKER_GATE: "1"
# Set by --cpu-prof: V8 profiles land in results/cpuprof on shutdown.
NODE_OPTIONS: ${NODE_OPTIONS:-}
x-node: &node
image: notifkit-prof-node:latest
volumes:
- ./results/cpuprof:/prof
# Long enough for workers to finish in-flight messages and flush a profile.
stop_grace_period: 60s
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health >/dev/null || exit 1"]
interval: 2s
timeout: 2s
retries: 60
start_period: 5s
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: notifkit
POSTGRES_USER: notifkit
POSTGRES_PASSWORD: password
# Production settings scaled to the tier's memory, rather than the image's
# defaults (128MB shared_buffers, 1GB max_wal_size) that nobody deploys.
command:
- postgres
- -c
- shared_buffers=${PG_SHARED_BUFFERS:-256MB}
- -c
- effective_cache_size=${PG_EFFECTIVE_CACHE_SIZE:-768MB}
- -c
- maintenance_work_mem=${PG_MAINTENANCE_WORK_MEM:-64MB}
- -c
- work_mem=8MB
- -c
- max_connections=400
- -c
- wal_buffers=16MB
- -c
- min_wal_size=512MB
- -c
- max_wal_size=4GB
- -c
- checkpoint_completion_target=0.9
- -c
- random_page_cost=1.1
- -c
- effective_io_concurrency=200
- -c
- shared_preload_libraries=pg_stat_statements
- -c
- pg_stat_statements.track=all
- -c
- track_io_timing=on
shm_size: 512mb
ports:
- "35432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notifkit -d notifkit"]
interval: 1s
timeout: 2s
retries: 60
deploy:
resources:
limits:
cpus: "${DB_CPUS:-1.0}"
memory: "${DB_MEMORY:-1G}"
redis:
image: redis:7-alpine
# AOF with a once-a-second fsync is the durability production runs with;
# the streams are the only copy of a queued notification.
command:
[
"redis-server",
"--appendonly",
"yes",
"--appendfsync",
"everysec",
"--save",
"",
"--maxmemory-policy",
"noeviction",
]
ports:
- "36379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
timeout: 2s
retries: 60
deploy:
resources:
limits:
cpus: "${REDIS_CPUS:-1.0}"
memory: "${REDIS_MEMORY:-1G}"
# ── Vertical: one monolith running every service ──────────────────────────
server:
<<: *node
environment:
<<: *node-env
SERVICES: ${SERVER_SERVICES:-api,enricher,engine,delivery,scheduler,events}
ports:
- "35678:3000"
deploy:
resources:
limits:
cpus: "${SERVER_CPUS:-1.0}"
memory: "${SERVER_MEMORY:-1G}"
# ── Horizontal: API ingress nodes ─────────────────────────────────────────
api-1:
<<: *node
environment:
<<: *node-env
SERVICES: api
ports:
- "35678:3000"
deploy:
resources:
limits:
cpus: "${API_CPUS:-1.0}"
memory: "${API_MEMORY:-1G}"
api-2:
<<: *node
environment:
<<: *node-env
SERVICES: api
deploy:
resources:
limits:
cpus: "${API_CPUS:-1.0}"
memory: "${API_MEMORY:-1G}"
# ── Horizontal: identical pipeline workers, scaled by count ───────────────
worker-1: &worker
<<: *node
environment:
<<: *node-env
SERVICES: ${WORKER_SERVICES:-enricher,engine,delivery,scheduler,events}
deploy:
resources:
limits:
cpus: "${WORKER_CPUS:-1.0}"
memory: "${WORKER_MEMORY:-1G}"
worker-2: *worker
worker-3: *worker
worker-4: *worker
# ── Load generator, inside the compose network ────────────────────────────
loadgen:
image: grafana/k6:1.0.0
volumes:
- ./k6:/scripts:ro
- ./results/.k6:/out
command: ["run", "--quiet", "--no-usage-report", "/scripts/notify.js"]
deploy:
resources:
limits:
cpus: "${LOADGEN_CPUS:-4.0}"
memory: "2G"