Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 68c8c86046 fix(cnpg): checkpoint tuning across all Postgres clusters
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Ten Postgres instances across seven CNPG clusters are ~48 GB/day of the
cluster's ~114 GB/day of client writes, and only ha-pg carried any
write-reduction tuning. The rest ran Postgres defaults, which favour throughput
over storage that wears out.

MEASURED, not assumed. ak-pg since 2026-05-17:

  checkpoints_timed   27,624      = 291/day, i.e. one every 5 min (the default)
  buffers_checkpoint  25,801,628  = ~206 GB flushed
  database size       1,046 MB

That is the whole database rewritten roughly twice a day. The pathology is
specific to idle databases: sessions, tokens and job state re-dirty the same few
pages constantly, and checkpoint_timeout=5min forces each of them to disk 288
times a day instead of letting them coalesce. At 15min they are written a third
as often.

max_wal_size is raised alongside it. Without that, checkpoints trigger on size
instead and the timeout change does nothing. It also bounds crash recovery to at
most 2GB of WAL replay.

Applied to ak-pg, pprl-pg, tr-pg, wp-pg, im-pg, gf-pg. ha-pg already had it.

synchronous_commit is deliberately NOT changed here. It is off for ha-pg because
a state recorder is telemetry; these clusters hold credentials, documents and job
state, where losing the last few hundred ms of commits on an unclean shutdown is
not an acceptable trade for write volume.

Separately, ha-pg shared_buffers 256MB -> 512MB. After 15h on Postgres its
buffers_backend was 629,704 of 939,025 total page writes -- 67% written by
backends rather than the checkpointer or bgwriter, which means the working set
does not fit in the buffer pool and pages are written out repeatedly instead of
coalescing. Memory limit raised 1Gi -> 1536Mi to keep the same headroom (measured
383Mi in use, of which 256MB was shared_buffers).

Verified before commit by rendering every cluster and asserting the tuning landed
under spec.postgresql.parameters with the barman plugin block untouched. That
check exists because the first attempt at this matched the wrong `parameters:`
key -- the barman plugin's, at 6-space indent under plugins: -- and corrupted
wp-pg's barmanObjectName. Reverted and redone with an indent-aware match.

Refs #434

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 06:55:42 +02:00
k.renner 0d65fb0982 Merge pull request 'fix: make small scratch emptyDirs RAM-backed instead of node-disk' (#530) from fix-emptydir-tmpfs into master
Reviewed-on: k.renner/homelab#530
2026-08-20 04:38:21 +00:00
k.renner ece3c93ad5 Merge pull request 'fix(trmnl): make the renderer's /tmp RAM-backed (34 GB/day of node writes)' (#529) from fix-trmnl-renderer-tmpfs into master
Reviewed-on: k.renner/homelab#529
2026-08-20 04:33:37 +00:00
claude-botandClaude Opus 5 b4f44b2002 fix(trmnl): make the renderer's /tmp RAM-backed (34 GB/day of node writes)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The renderer was the single largest writer in the cluster -- more than every
Ceph OSD and every database combined -- and none of it needed to touch disk.

Measured 2026-08-20 from the container's own cgroup io.stat:

  253:0 (node /dev/vda5)  249,092,697,088 bytes over 7.3 days  = ~34 GB/day
  252:48 (its own PVC)             340 MB total                = negligible

So the writes were not going to its volume at all; they were going to the
node's filesystem via a disk-backed emptyDir. `emptyDir: {}` without
`medium: Memory` is node storage, which is easy to read as "ephemeral, so
free".

The cause is cadence rather than volume. schedules.template.json renders on
"* * * * *", so Chrome is launched 1440 times a day and each launch creates,
fills and deletes a /tmp/puppeteer_dev_chrome_profile-* directory. At rest /tmp
holds under 8 MB -- it is pure churn for data that never outlives the render.

tmpfs pages count against the container's memory limit, so the 512Mi cap is
kept deliberately: measured usage is 129Mi against a 2Gi limit, leaving ample
headroom even if the cap were fully consumed.

Expected effect: ~34 GB/day off the OS disks, which measured 215 GB/day over a
4h window. This is a bigger cluster-wide reduction than the Home Assistant
recorder migration achieved, for a one-line change.

Refs #434

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 06:26:38 +02:00
8 changed files with 155 additions and 2 deletions
+21
View File
@@ -43,6 +43,27 @@ spec:
parameters:
shared_buffers: "256MB"
max_connections: "400"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
max_wal_size: "2GB"
wal_compression: "on"
# Async replication makes CNPG switchover slow because the standby has
# to chase missing WAL via barman-cloud and replay queued records
# before it can promote. With one synchronous standby, the standby is
+21
View File
@@ -29,6 +29,27 @@ spec:
parameters:
shared_buffers: "128MB"
max_connections: "50"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
max_wal_size: "2GB"
wal_compression: "on"
resources:
requests:
cpu: 100m
@@ -55,7 +55,21 @@ spec:
owner: homeassistant
postgresql:
parameters:
shared_buffers: "256MB"
# 512MB, not the 256MB this shipped with. Measured after 15h on Postgres:
# buffers_backend was 629,704 of 939,025 total page writes — 67% written
# by backend processes rather than the checkpointer or bgwriter. That
# ratio means the working set does not fit in shared_buffers, so backends
# evict dirty pages themselves and the same pages are written out
# repeatedly instead of coalescing in memory.
#
# The recorder touches a wide set of pages continuously (~30 state
# changes/sec across states, state_attributes and five indexes), which is
# exactly the workload that punishes a small buffer pool.
#
# Memory headroom checked before raising: the instance was using 383Mi of
# a 1Gi limit with 256MB of that being shared_buffers, so the limit goes
# to 1.5Gi to keep the same margin.
shared_buffers: "512MB"
max_connections: "50"
# The rest of this block is tuned to MINIMISE WRITE VOLUME, which is the
# entire reason for the migration. Defaults would give a correct database
@@ -87,4 +101,4 @@ spec:
cpu: 200m
memory: 512Mi
limits:
memory: 1Gi
memory: 1536Mi
+19
View File
@@ -43,6 +43,25 @@ spec:
shared_buffers: "512MB"
max_wal_size: "2GB"
wal_compression: "on"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
resources:
requests:
cpu: 500m
+21
View File
@@ -34,6 +34,27 @@ spec:
parameters:
shared_buffers: "256MB"
max_connections: "200"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
max_wal_size: "2GB"
wal_compression: "on"
# See ak-pg cluster.yaml for the rationale on synchronous replication.
synchronous:
method: any
+20
View File
@@ -33,6 +33,26 @@ spec:
shared_buffers: "128MB"
max_connections: "50"
wal_compression: "on"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
max_wal_size: "2GB"
resources:
requests:
cpu: 100m
+16
View File
@@ -362,6 +362,22 @@ spec:
name: trmnl-renderer-schedule
- name: tmp
emptyDir:
# RAM-backed, NOT disk. Without `medium: Memory` an emptyDir lands
# on the node's filesystem, and this one was the single largest
# writer in the cluster: 249 GB over 7.3 days = ~34 GB/day onto
# /dev/vda5, more than every Ceph OSD and every database combined
# (measured 2026-08-20 from the container's cgroup io.stat).
#
# The cause is cadence, not volume. schedules.template.json renders
# every 60 s, so Chrome is launched 1440 times a day and each launch
# creates, fills and deletes a /tmp/puppeteer_dev_chrome_profile-*
# directory. At rest /tmp holds under 8 MB; it is pure churn for
# data that never outlives the render.
#
# tmpfs pages count against the container's memory limit, which is
# why the cap stays at 512Mi: measured usage is 129Mi against a 2Gi
# limit, so even the full cap leaves ample headroom.
medium: Memory
sizeLimit: 512Mi
---
apiVersion: v1
+21
View File
@@ -56,6 +56,27 @@ spec:
parameters:
shared_buffers: "128MB"
max_connections: "50"
# Write-reduction tuning. These databases are largely idle, and the
# Postgres defaults favour throughput over storage that wears out.
#
# checkpoint_timeout is the lever. The 5min default pushes every dirty
# page to disk 288 times a day, so a small database whose same few pages
# are re-dirtied constantly (sessions, tokens, job state) gets rewritten
# over and over. Measured on ak-pg: 25.8M checkpoint buffers flushed
# (~206 GB) against a 1046 MB database -- the whole thing roughly twice a
# day. At 15min those pages coalesce and are written a third as often.
#
# max_wal_size must rise with it, or checkpoints trigger on size instead
# and the timeout change does nothing. It also bounds crash recovery to
# at most 2GB of WAL replay.
#
# NOT changed: synchronous_commit stays at the default `on`. It is off for
# ha-pg because a state recorder is telemetry; these hold credentials,
# documents and job state, where losing the last few hundred ms of commits
# on an unclean shutdown is not an acceptable trade.
checkpoint_timeout: "15min"
max_wal_size: "2GB"
wal_compression: "on"
resources:
requests:
cpu: 100m