New to Kestra?
Use blueprints to kickstart your first workflows.
Recover a ClickHouse replica stuck read-only after losing Keeper metadata. Gated SYSTEM RESTORE REPLICA, then a re-read of system.replicas.
A ReplicatedMergeTree table keeps its replication metadata in ClickHouse Keeper. Lose that metadata, through a wiped Keeper volume, a Keeper cluster rebuilt from scratch or a deleted znode, then the table flips to is_readonly = 1 and refuses every write while the data sits untouched on local disk. The usual reflex makes it worse: SYSTEM RESTART REPLICA only reinitialises the session against Keeper, so it restarts straight back into read-only. The statement that recovers the table is SYSTEM RESTORE REPLICA, which re-creates the Keeper metadata from the parts already on disk. This guard tells the two causes apart, restores only the ones that need it, then proves the table takes writes again.
This blueprint was created by zkasuran.
Measured on ClickHouse 24.8 with a separate ClickHouse Keeper node while building this.
system.replicas alone. A lost Keeper session reads is_readonly = 1 with is_session_expired = 1 and an EMPTY zookeeper_exception. Lost metadata reads is_readonly = 1 with is_session_expired = 0 and a KEEPER_EXCEPTION naming the missing path. A flow that treats every read-only replica the same reaches for the wrong statement half the time.is_session_expired = 1, Keeper was started again, both returned to is_readonly = 0 with no intervention. That is why those rows are reported here and never touched.is_readonly on its own. Writes start failing immediately with a Keeper No node error while is_readonly still reads 0. The flag is set when the replica next reinitialises, so the detector reads the znode path through system.zookeeper rather than trusting the flag alone.SYSTEM RESTART REPLICA was run twice against the table with its metadata deleted. is_readonly stayed 1 both times. SYSTEM RESTORE REPLICA on the same table returned is_readonly to 0 with total_replicas and active_replicas back at 1. The refused INSERT was accepted on the next attempt with the original 1000 rows intact.system.zookeeper returns zero rows for a path that does not exist on 24.8 instead of raising. It also accepts a subquery in its path IN (...) predicate, so the znode probe is one join rather than a task per table.system.zookeeper BLOCKS while Keeper is unreachable, it does not raise. A literal path read sat there until it was cancelled. Two things keep that out of the flow. The probe's path set is scoped to replicas that are read-only with a live session, so during a full Keeper outage the set is empty and ClickHouse never contacts Keeper at all, which was measured returning 0 immediately with Keeper stopped. On top of that both reads carry SETTINGS max_execution_time, measured raising TIMEOUT_EXCEEDED on a probe that was already hanging.dry_run is true and auto_remediate is false, so a plain run reports the state without touching anything.auto_remediate is true and dry_run is false. It then waits for a human to resume the Pause with approved set true. An unanswered gate reads as not approved.SYSTEM RESTORE REPLICA, because the right answer for that one is to fix Keeper and let the session come back.SYSTEM RESTORE REPLICA rebuilds the metadata from local parts, so a part that lived only on another replica and never replicated here is not recovered by it. An operator approving the gate is agreeing that this host holds the data worth keeping.system.replicas asks only whether the table is still read-only. It drops the znode and session predicates the fix moves, so a partial restore cannot read clean.max_execution_time ceiling from query_timeout_seconds, so a Keeper outage turns into a failed task with a Slack alert rather than a run that hangs holding a worker.survey_readonly (io.kestra.plugin.jdbc.clickhouse.Query) reads every replica with is_readonly = 1 in the database and labels each one RESTART, RESTORE or REVIEW from is_session_expired plus a system.zookeeper read of the table's own zookeeper_path. That probe only asks about replicas that are read-only with a live Keeper session, so a Keeper outage leaves it with nothing to look up.detect keeps the RESTORE cases only, the replicas that are read-only with a live session and no znode. It also reads the local active part count and row count behind each one from system.parts.summarize (io.kestra.plugin.core.output.OutputValues) records the targeted names, the per target evidence and the full survey once, so the log, the alert and the failure message all read the same value.log_detect (io.kestra.plugin.core.log.Log) prints both lists, the evidence per target plus the reason SYSTEM RESTART REPLICA will not help here.guard (io.kestra.plugin.core.flow.If) branches on the RESTORE count. With none found, clean logs the survey and the run ends, which is also the path a purely session-expired replica takes.findings_alert (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the targets with their evidence on every detection, in dry run and in remediate mode.mode (io.kestra.plugin.core.flow.If) enters the remediation branch only when auto_remediate is true and dry_run is false. Otherwise report_only lists what would be restored and stops.approval_gate (io.kestra.plugin.core.flow.Pause) holds the run until a human resumes it with approved. apply restores only when approved is true.remediation_restore_loop (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs SYSTEM RESTORE REPLICA <database>.<table> once per target, with allowFailure so a refused restore does not stop the pass.reverify re-reads system.replicas for exactly those tables. verify_result fails the run through io.kestra.plugin.core.execution.Fail if any of them is still read-only.SYSTEM RESTORE REPLICA only where that statement is the fix.SYSTEM RESTART REPLICA, which is the step that recovers a table whose Keeper metadata is genuinely gone.system.replicas shows every targeted table out of read-only.ClickHouse reports the read-only flag in system.replicas then leaves the decision to a human. It has no scheduler for the check, no way to tell a session blip from metadata loss without someone reading several columns at once, no approval step in front of SYSTEM RESTORE REPLICA and no proof afterwards that the table came back. Kestra adds the missing pieces. It sweeps on a schedule, classifies each read-only replica, shows a human what the rebuild will be based on before anything runs, gates the restore behind dry-run plus approval, re-reads system.replicas to turn a hopeful restore into a verified one, applies the same connection settings to every task then posts a Slack alert the moment a step fails.
Run on Kestra 2.0.5 against ClickHouse 24.8 with ClickHouse Keeper in its own container, 15 executions covering every path. Two ReplicatedMergeTree tables were created, w35_events with 1000 rows and w35_healthy with 50, both confirmed writable with is_readonly = 0. The Keeper znode for w35_events was then deleted with clickhouse-keeper-client rmr and the replica reinitialised, which left it is_readonly = 1 with is_session_expired = 0 and a KEEPER_EXCEPTION. w35_healthy was left untouched throughout. Every number below was confirmed with clickhouse-client against system.replicas, not from the Kestra log.
INSERT returned Code: 242 TABLE_IS_READ_ONLY, Table is in readonly mode since table metadata was not found in zookeeper. After the restore the same INSERT was accepted and the table went from 1000 rows to 1001, so the original rows survived the rebuild.SYSTEM RESTART REPLICA is the wrong tool and was measured as such. Run twice against the table with its metadata deleted, is_readonly stayed 1 both times. SYSTEM RESTORE REPLICA on the same table took it to is_readonly = 0 with total_replicas and active_replicas back at 1.is_readonly = 1 with is_session_expired = 1, the detector returned 0 restore targets while the survey reported 2 RESTART rows, Keeper was started again and both tables returned to is_readonly = 0 on their own.is_readonly read 1 before and after.SYSTEM RESTORE REPLICA, the re-read reported 0 still read-only. clickhouse-client confirmed is_readonly 1 to 0 with the row count intact.SELECT and INSERT was refused with ACCESS_DENIED. The re-read found the table still read-only so the run FAILED rather than reporting success.approved false restored nothing and the table stayed read-only.A ClickHouse server reachable over the JDBC HTTP interface, at least one ReplicatedMergeTree table plus a user that can read system.replicas, system.zookeeper and system.parts and run SYSTEM RESTORE REPLICA on the target tables.
CLICKHOUSE_USERNAME: ClickHouse user, read when use_secret_auth is true.CLICKHOUSE_PASSWORD: password for that user.SLACK_WEBHOOK_URL: used by findings_alert and the errors block.SECRET_.| Name | Type | Default | Description |
|---|---|---|---|
clickhouse_url |
STRING | jdbc:clickhouse://localhost:8123/default |
ClickHouse JDBC URL. |
use_secret_auth |
BOOL | true |
Read credentials from secrets. Set false only for inline test credentials. |
database |
STRING | default |
Database whose replicated tables are checked. |
table |
STRING | "" |
Optional single table filter. Empty checks every replicated table. |
query_timeout_seconds |
INT | 30 |
Ceiling on the two reads that probe Keeper, so an unreachable Keeper fails the task instead of hanging. |
dry_run |
BOOL | true |
Report without restoring. |
auto_remediate |
BOOL | false |
Restore behind the approval gate when dry_run is false. |
CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD and SLACK_WEBHOOK_URL as namespace secrets.database pointed at a database holding ReplicatedMergeTree tables. The default dry run reports every read-only replica, which statement each one needs, plus the local parts behind any restore target.dry_run false and auto_remediate true. Resume the paused execution with approved true.local_active_parts or local_rows so a target whose local disk holds almost nothing is reported rather than restored, since a rebuild from local parts would lose the rest.SYSTEM RESTORE REPLICA runs per replica.system.zookeeper_connection or the Keeper four letter commands, so a run during a partial Keeper outage reports and stops before it classifies anything.SYSTEM SYNC REPLICA on the other replicas, so the rebuilt metadata is picked up everywhere before writes resume.