Schedule icon
Query icon
If icon
Switch icon
Log icon
SlackIncomingWebhook icon

ClickHouse Replica Lag and Readonly Health Gate

Classify ClickHouse replica health from system.replicas, alert on lag or readonly state, and optionally restart the replica.

Categories
DataInfrastructure

A ClickHouse replica that falls behind on its replication queue degrades reads silently - queries still return results, just increasingly stale ones. A replica that loses its ClickHouse Keeper session degrades harder: it flips to readonly and stops accepting writes outright, often without an obvious client-side error until something tries to insert into it. This blueprint reads ClickHouse's own system.replicas table to tell these two failure modes apart, classifies the result into a three-state risk level, alerts on anything but a clean bill of health, and can resync a lagging or readonly replica with ClickHouse's own SYSTEM RESTART REPLICA statement, behind two independent gates.

How it works

  1. periodic_replica_check (io.kestra.plugin.core.trigger.Schedule) runs every 15 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. check_replica_status (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) reads system.replicas filtered to target_table, populating row.is_readonly, row.is_session_expired, row.absolute_delay, row.queue_size, row.active_replicas, row.total_replicas, row.zookeeper_exception - or no row at all.
  3. evaluate_replica_presence (io.kestra.plugin.core.flow.If) branches on whether a row was returned.
    • else (UNREACHABLE): alert_table_not_replicated reports that target_table has no row in system.replicas at all.
    • then (row found): nested evaluate_replica_health (io.kestra.plugin.core.flow.If) branches on is_readonly == 0.
      • else (UNREACHABLE): alert_replica_readonly reports the readonly/lost-session state, with zookeeper_exception if ClickHouse recorded one.
      • then (writable): nested evaluate_replica_lag (io.kestra.plugin.core.flow.If) branches on absolute_delay > max_absolute_delay_seconds or active_replicas < total_replicas.
        • else (HEALTHY): log_replica_healthy records a clean check.
        • then (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, restart_replica runs SYSTEM RESTART REPLICA {{ target_table }}. alert_replica_at_risk always fires to Slack regardless of which remediation path ran.
  4. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  5. The errors block alerts Slack separately if the flow itself fails outright - an unreachable cluster or bad credentials must not read as "no risk" or "healthy".

What you get

  • A three-state answer (HEALTHY / AT_RISK / UNREACHABLE) sourced directly from ClickHouse's own replication state, not inferred from query latency or an external agent.
  • Lag and lost-session treated as two distinct severities, so a replica that is merely behind is never confused with one that has stopped accepting writes.
  • A separate UNREACHABLE path for a table that is not even a Replicated table, so a configuration mistake is never reported as a healthy replica.
  • Remediation scoped to exactly SYSTEM RESTART REPLICA on the one table you named - it cannot touch any other table or any row of data.
  • Two independent gates (auto_remediate, dry_run) between "risk detected" and "replica actually restarted".

Who it's for

  • Platform and database reliability teams running ClickHouse with ReplicatedMergeTree-family tables who need an early signal when a replica falls behind or loses its Keeper session.
  • On-call engineers who want one Slack alert with the exact lag and session numbers instead of running SELECT * FROM system.replicas by hand during an incident.
  • Anyone evaluating the ClickHouse plugin who wants a worked example of Query driving both system-table introspection (system.replicas) and a SYSTEM administrative statement (RESTART REPLICA), not just table reads and inserts.

Why orchestrate this with Kestra

system.replicas run by hand answers one moment in time; it does not decide a cadence, classify the result into a three-state risk level, gate a restart behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any replica is restarted, and an execution history that shows exactly when a replica degraded and whether anything was done about it.

Prerequisites

  • A ClickHouse cluster with at least one table using a ReplicatedMergeTree-family engine and a working ClickHouse Keeper (or ZooKeeper) ensemble - system.replicas has no row at all for a plain MergeTree table.
  • A database user with SELECT on system.replicas and, if auto_remediate will ever be "true", permission to run SYSTEM RESTART REPLICA.
  • A Slack incoming webhook for alerts.

Local testing: docker run -d --name clickhouse-replica-gate -p 8124:8123 -p 9001:9000 clickhouse/clickhouse-server:latest docker network connect YOUR_KESTRA_NETWORK clickhouse-replica-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set clickhouse_url to jdbc:clickhouse://clickhouse-replica-gate:8123/default from inside that network, or jdbc:clickhouse://localhost:8124/default from the host). Host port 8124 is used here specifically so it never collides with Kestra's own UI on 8080. A single-node container has no real Keeper ensemble to replicate against, so a genuinely meaningful ReplicatedMergeTree test needs either ClickHouse Keeper configured on the same node or a second container - a true replica-lag scenario cannot be fully exercised on one bare container.

Secrets

  • CLICKHOUSE_USERNAME / CLICKHOUSE_PASSWORD: credentials used by every io.kestra.plugin.jdbc.clickhouse.Query task in this flow.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_replica_at_risk, alert_replica_readonly, alert_table_not_replicated, and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_CLICKHOUSE_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • clickhouse_url (STRING, default jdbc:clickhouse://localhost:8124/default): JDBC URL.
  • target_table (STRING, default default.events): fully qualified db.table audited and, if remediated, restarted.
  • max_absolute_delay_seconds (INT, default 300): lag ceiling before a writable replica counts as AT_RISK.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for SYSTEM RESTART REPLICA to actually run.

Outputs

  • outputs.check_replica_status.row.*: the full system.replicas row for target_table every run (is_readonly, is_session_expired, absolute_delay, queue_size, active_replicas, total_replicas, zookeeper_exception), or no row at all if the table is not Replicated.
  • outputs.restart_replica: present only when remediation actually ran; SYSTEM RESTART REPLICA returns no result set, so this output carries no row data of its own, only its existence matters.

Quick start

  1. Start ClickHouse locally with the command above and create a ReplicatedMergeTree table (requires Keeper/ZooKeeper configured - see Prerequisites) under target_table.
  2. Add the CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD, and SLACK_WEBHOOK_URL secrets.
  3. Run the flow manually with the defaults and confirm log_replica_healthy fires on a freshly created, idle replica.
  4. Pause replication or disconnect Keeper briefly to push absolute_delay past max_absolute_delay_seconds, then re-run and confirm alert_replica_at_risk fires.
  5. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the restart without running it.
  6. Run once with auto_remediate: "true" and dry_run: "false" and confirm restart_replica runs, then verify with SELECT * FROM system.replicas that absolute_delay has recovered.
  7. Enable periodic_replica_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Loop over several target_table values with io.kestra.plugin.core.flow.Loop to run the same gate across every Replicated table in a cluster in one flow.
  • Add a check on queue_size or future_parts alongside absolute_delay to catch a replica that is building a backlog before its lag clock actually starts moving.
  • On the UNREACHABLE (readonly) branch, use SYSTEM RESTORE REPLICA instead of SYSTEM RESTART REPLICA when local data exists but Keeper metadata was lost - ClickHouse's own documentation describes RESTORE REPLICA as the correct recovery for that specific case, which this flow does not attempt to distinguish from a routine session expiry.
  • Route the UNREACHABLE (readonly) branch to a different Slack channel than AT_RISK lag, since a lost Keeper session is a harder failure than lag alone.

Pitfalls

  • system.replicas has no row at all for a non-Replicated table. A plain MergeTree table pointed at by target_table makes evaluate_replica_presence report UNREACHABLE on every run, which is correct behavior, not a bug - this gate only has something to audit for ReplicatedMergeTree-family engines.
  • is_readonly and lag are different failure modes, not degrees of the same one. A replica can have absolute_delay = 0 and still be is_readonly = 1 immediately after losing its Keeper session, before any lag has had time to accumulate - that is why evaluate_replica_health is checked before evaluate_replica_lag, not folded into the same condition.
  • There is no structured "replica status" or "restart replica" task in this plugin. Both system.replicas introspection and the SYSTEM RESTART REPLICA statement go through the same generic io.kestra.plugin.jdbc.clickhouse.Query task used elsewhere in this repo's own ClickHouse flows (system.mutations, system.parts).
  • SYSTEM RESTART REPLICA makes the table temporarily unavailable. Per ClickHouse's own documentation, this reinitializes the Keeper session and the table cannot serve queries normally while that happens - it is a real, if usually brief, availability cost, which is exactly why it sits behind both auto_remediate and dry_run.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for SYSTEM RESTART REPLICA to run; setting only one leaves the other still blocking.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.
  • This blueprint is UNTESTED against a live ClickHouse cluster. The system.replicas column names are taken verbatim from ClickHouse's own system-tables documentation, and SYSTEM RESTART REPLICA syntax from ClickHouse's own SYSTEM statements documentation - but no Docker container or Kestra engine was run to execute this flow end to end, and a true multi-replica lag scenario was not exercised at all.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.