v2.8.1
NEW FEATURES
- add
clickhouse.drop_replica_if_exists(envCLICKHOUSE_DROP_REPLICA_IF_EXISTS, CLI--drop-replica-if-existsforrestore/restore_remote, also accepted as thedrop_replica_if_existsquery argument byPOST /backup/restoreandPOST /backup/restore_remote) — when the replica entry<replica_path>/replicas/<replica_name>of a restoredReplicated*MergeTreestill exists in ZooKeeper but no local table uses that path, executeSYSTEM DROP REPLICA '<replica_name>' FROM ZKPATH '<replica_path>'and restore the table with the ORIGINAL replication path from the backup, instead of the default behavior which silently rebinds the table toclickhouse.default_replica_path/clickhouse.default_replica_name; requires ClickHouse 20.4+, the restore fails when the drop is impossible (older server, or a local table still occupies the path), the statement is executed withoutON CLUSTERbecause the path and replica name are resolved with the macros of the current node; MUST stayfalseduring a concurrent multi-replica restore, fix #1162 - add
--streamingflag tocreate_remote,restore_remoteandwatch,--watch-streamingflag toserverand thestreamingquery argument toPOST /backup/create_remote,POST /backup/restore_remoteandPOST /backup/watch— process the backup table by table:create_remote --streamingruns freeze -> upload -> remove local copy per table,restore_remote --streamingruns download -> attach -> remove local copy per table, so only the tables in flight occupy local disk space and backups bigger than the free local space can be uploaded or restored; the local backup is not kept afterwards; interrupted runs are continued with--resumevia the dedicatedcreate_upload_streaming.state2/download_restore_streaming.state2state files (add these command names toapi.complete_resumable_after_restart_commandsto resume them after an API server restart); not available withuse_embedded_backup_restore: trueorremote_storage: custom, fix #780 - add
restore_cloudcommand andPOST /backup/restore_cloudREST API endpoint — restore a ClickHouse Cloud native S3 backup (created via external backup / Bring Your Own Backup) onto self-managed ClickHouse: reads the.backupmanifest directly from S3 (includingdata_file_name_generator: Checksumblob layout), rewritesENGINE = Shareddatabases toAtomicandShared*MergeTreetables to the matchingReplicated*MergeTree(adding--replicated-zk-path/--replicated-replicaarguments when Cloud DDL has none), applies the DDL and runsRESTORE TABLE ... FROM S3(...)/AzureBlobStorage(...)withallow_different_database_def=1, allow_different_table_def=1; source storage is S3-compatible (AWS, GCS overs3->endpoint: https://storage.googleapis.comwith HMAC keys, MinIO) or AzureBlobStorage (when--container/--azblob-restore-urlis passed orgeneral->remote_storage: azblob); supports--tablesfilter,--base-prefixfor incremental backups,--s3-restore-url/--azblob-restore-urloverrides,--skip-empty-tables,--continue-on-error,--drop,--paralleland--dry-run; note: backups containingPackedparts (data.packed) require ClickHouse 26.8+ on the target server, fix #1508 - add
--dry-runflag tocreate,create_remote,upload,download,restore,restore_remote,deletecommands and thedry-run(dry_run) query argument to the matching REST API endpoints — instead of performing the operation, report the number of tables, parts and the data size (split into local-disk and object-disk bytes, human readable in the log) which would be processed, honoring--tables,--partitions,--diff-from/--diff-from-remote,--schemaand the other filters;create --dry-runadditionally forecasts the disk space the backup hardlinks will occupy over time (bytes held in parts younger than 1/7/30 days approximate what background merges will rewrite within that horizon, up to the full local data size in the worst case) anddelete --dry-runreports the dependent incremental backups; API dry-run requests execute synchronously, are allowed even when another operation is in progress regardless ofapi.allow_parallel, and return the report as JSON in the response body; every dry-run also fills the newresultfield of its status row, so the report is visible viaGET /backup/status,GET /backup/actionsand thesystem.backup_actionstable (which gained aresult Stringcolumn, manually created tables need it added orinput_format_skip_unknown_fields=1); estimation limits:createdoesn't subtract--diff-from-remoteincrements,create_remote/uploadcan't know the compressed size before compression, parts of backups made by versions older than 2.7.0 have no per-part sizes and are counted inunknown_size_parts, fix #1012 - add
general.callback_url(envCALLBACK_URL) andgeneral.callback_timeout(envCALLBACK_TIMEOUT, default5s) — HTTP POST completion notification for API, one-shot CLI commands, and eachwatchiteration; API?callback=overrides the global URL when non-empty; the payload keeps the existingstatus/error/operation_idfields and addscommandandduration;statusis now alsocancelwhen the operation was killed via/backup/kill; callbacks are sent asynchronously, failures are logged and never change the backup result; one-shot CLI commands andwatchiterations are now registered in/backup/statuslike API operations, fix #1481 - add
general.status_history_size(envSTATUS_HISTORY_SIZE, default1000) — upper bound on how many finished operations are kept in the in-memory status list exposed by/backup/statusandsystem.backup_actions; needed becausewatchnow records one operation per iteration, so the history would otherwise grow for as long as the process lives; operations still running are never dropped, whatever their age delete local|remote <backup_name>andPOST /backup/delete/{where}/{name}now refuse to delete a backup which other backups require viarequired_backupand report the dependent backup names, instead of silently breaking the incremental backups chain (the breakage surfaced only later, when a descendant was downloaded or restored, and for object disks the descendantrequiredparts blobs were deleted together with the parent); pass--force(force=1for the API) to get the old behavior, or setgeneral.rebase_during_delete: true(envREBASE_DURING_DELETE, defaultfalse) to rebase every dependent increment first (same as therebasecommand) so the chain stays restorable — rebase copies the deleted backup parts into its dependents, so deletion time grows with the copied data size and a rebase failure aborts the delete.backups_to_keep_local/backups_to_keep_remoteretention is not affected, fix #1493restore --rbacnow converts RBAC objects betweenlocal_directoryandreplicateduser directories, when the target server has only one of the two shapes: a Keeper dump from the backup is written as<access_control_path>/<uuid>.sqlfiles when the target has noreplicateduser directory, and backup*.sqlfiles are written to Keeper (<zookeeper_path>/uuid/<uuid>plus the<zookeeper_path>/<type char>/<name>index znode) when the target has nolocal_directoryuser directory; previously the half which had no matching storage was silently lost while the restore still reported success; restoring a backup with RBAC objects onto a server which has neither user directory now fails explicitly, fix #881
IMPROVEMENTS
- support tables which declare their storage inline in the DDL via
SETTINGS disk = disk(...)instead of astorage_policy(ClickHouse 23.2+, tested from 24.8), including nestedcacheandencryptedwrappers over an object storage disk; such disks are registered by ClickHouse under a generated__tmp_internal_<hash>name (or undername = '...') and are absent frompreprocessed_configs/config.xml, socreate/upload/download/restoreused to fail with__tmp_internal_... is not present in object_disk.DisksCredentials; object storage credentials are now taken from the table DDL and the target disk is resolved again after the schema is restored, so the generated name may differ between the source and the target server. Note that the DDL stored in the backup metadata contains thesecret_access_keyin plain text, same as forS3/MySQLengine tables (#640), and thatclickhouse-backupneeds to run on the ClickHouse host (to read/var/lib/clickhouse/metadata/<db>/<table>.sql) or the server needsdisplay_secrets_in_show_and_select=1, becauseSHOW CREATE TABLEmasksdisk(...)arguments as'[HIDDEN]', fix #943 - fail fast instead of burning the whole
retries_on_failurexretries_durationbackoff budget per file when a remote object is permanently missing (S3NoSuchKey/404, GCS 404, AzureBlobNotFound, FTP/SFTP not-found) duringdownload,restore_remoteand other retried remote operations; addgeneral.allow_missing_files_on_download(envALLOW_MISSING_FILES_ON_DOWNLOAD, defaultfalse),--allow-missing-filesCLI flag fordownload/restore_remoteand theallow_missing_filesquery argument forPOST /backup/downloadandPOST /backup/restore_remote— salvage mode which skips data parts missing on remote storage with anerror-level log and a final summary, drops them from local table metadata and lets the intact tables/parts of a partially corrupted backup be restored, metadata files are never skipped, fix #1456 - add
s3.delete_batch_fallback_to_single(envS3_DELETE_BATCH_FALLBACK_TO_SINGLE, defaulttrue) ands3.delete_batch_min_size(envS3_DELETE_BATCH_MIN_SIZE, default0) — when a wholeDeleteObjectsbatch fails (some S3-compatible gateways such as DigitalOcean Spaces / Ceph RGW reset the response stream when the batch contains large objects, so retrying the same batch never succeeds and blocks retention and thewatchloop), the batch is split in halves down todelete_batch_min_sizeand finally its objects are deleted one by one withDeleteObject,s3.delete_concurrencyin parallel;general.delete_batch_sizeis now validated (1..1000 fors3), fix #1532 download --hardlink-exists-filesno longer does per-part filesystem and ClickHouse lookups, which dominated the download time on servers holding many local backups or many parts. Two changes: the shadow directories of the local backups are now indexed once perdownloadrun from their table metadata, so finding a hardlink candidate for a backup carrying legacy CRC64checksumscosts one map lookup plus onestatinstead of afilepath.Globover<disk>/backup/*/shadow/...(which ran twice per part, once for the free space check and once for the download itself); and thehash_of_all_fileslookup insystem.partsis now read once per table in chunks of 1000 hashes instead of oneSELECTper part. Both paths keep their previous results: an indexed candidate is still verified by the CRC64 of itschecksums.txtbefore being hardlinked, a local backup which can't be indexed (broken backup, unreadable metadata) marks the index incomplete and restores the old glob, and asystem.partscandidate which a merge removed after the snapshot was taken is detected before hardlinking and re-resolved by a single live query for that part, fix #1457
BUG FIXES
- FTP:
MkdirAllnow treats an already existing directory as success and remembers it in its directory cache, instead of loggingMkdirAll MakeDir(...) return error: 550 "...: File exists"and re-issuingMKDfor the same directories for every single uploaded object (hundreds of serialized round-trips percreate/uploadof an object disk backup); aMakeDirfailure for any other reason is no longer swallowed, it now fails the upload with the real error instead of lettingSTORfail later with a confusing550 ... No such file or directory. A failedSTORalso drops the cached directories of its path and is no longer classified as a permanently missing object, so it is retried (re-reading the source object) instead of aborting the whole backup on the first transient FTP error, fix theTestFTPCI failure of #1559 download/restore_remoteof an incremental backup no longer fail with<part> not found on <required_backup> and all required backups sequencewhen the local copy of an intermediate backup of the chain lists only a subset of its parts; the localbackup/<name>/metadata/<db>/<table>.jsonof a required backup could have been narrowed by an earlierdownload --partitions=..., byallow_missing_files_on_downloador by disk rebalancing, but it was trusted as the description of which backup of the chain physically holds a part, so the resolution stopped at that backup instead of recursing to its own required backup; the required backup table metadata is now always read unfiltered from remote storage and the local file is neither read nor overwritten, fix #1045downloadof an incremental backup no longer fails withfilepath.Walk: Link in walk: link ...: file existswhen a local backup already holds a copy of a required part from an earlier download: the required backup that copy was hardlinked from can be removed by the partial-required-backup cleanup and downloaded again later, which leaves the local copy pointing at the old inode; the freshly downloaded copy is now hardlinked over the leftover instead of aborting the download. A resumed download which already recorded that part indownload.state2still skips the re-download and keeps the leftover as it is, fix #1045restore --rbac/restore --rbac-onlyno longer fails withzk: node already existsorzk: version conflictwhen several replicas which share areplicateduser directory restore the same backup at the same time (typical whenrestore_remote --rbacis fired via the REST API on every node of a cluster) — the Keeper writer used a non-atomic get-then-create/set and treated a concurrent writer of a byte identical znode as an error, znodes are now written with an unconditional set-or-create anddropExistsRBACtolerates znodes already removed by another replica, so every replica exits successfully and the final RBAC state is the same everywhere;rbac_conflict_resolution: failstill fails on the second replica by design, and for replicated-only clusters setclickhouse: restart_command: "sql:SYSTEM RELOAD USERS"so the concurrent restores don't restart every replica at once, fix #1048createno longer leavesshadow/<uuid>orphans forever when the process is killed (OOM, SIGKILL, pod eviction) betweenFREEZEandUNFREEZE: everyFREEZE ... WITH NAME <uuid>is now recorded in<backup_name>/freezes.tmp(bbolt) until the table is unfrozen, andclean,delete local,clean_local_brokenand the local retention unfreeze the recorded shadow of backups which are not processed by a liveclickhouse-backupprocess (pid file orfreezes.tmplock) viaSYSTEM UNFREEZE WITH NAME(22.6+),ALTER TABLE ... UNFREEZE WITH NAME(21.4+) or direct removal;cleanis safe by default now and no longer wipes the wholeshadow/(which destroyed the data of a concurrentcreateand manualFREEZE),clean --older-than=24hadditionally removes unrecordedshadow/*directories not modified for that long (orphans of previous versions),clean --allkeeps the old wipe-everything behavior,clean --dry-runonly logs; the cleanup waits until theFREEZEqueries of the interrupted command leftsystem.processesand re-checks thatshadow/<uuid>stays removed, otherwise the server recreated it right after the removal;POST /backup/cleanacceptsolder_than,allanddry_runquery parameters andclean --older-than=24h --all --dry-runworks viaPOST /backup/actions, fix #1563serverno longer subscribesSIGINTto theSIGHUPreload channel, so Ctrl+C stops the server directly instead of sometimes reloading the config and canceling all commands twice before stoppingSIGINT/SIGTERMnow also stop a command waiting in the infinite ClickHouse reconnect loop (added for #857): the reconnect gives up as soon as the process is asked to terminate instead of retrying every 5 seconds forever, sokillandtimeoutno longer hang on an unreachable or incompatible server- one-shot CLI commands handle
SIGINT/SIGTERM: the first signal cancels the running command socreateunfreezes its shadow and unwinds, the second one exits immediately; previously onlyserverandwatchhandled signals and akill -TERM clickhouse-backup createleftshadow/<uuid>behind, fix #1563 restoreno longer hardlinksdefaultdisk parts into an object disk metadata tree (ATTACH PARTthen failed withcode: 27 ... Cannot parse input: expected '\n' before: 'checksums format version: 4') when clickhouse-server is started with a relative<path>./</path>(ClickHouse 25.x and older report relative paths insystem.disks.pathandsystem.tables.data_paths, 26.8 canonicalizes them). Such relative disk paths are now resolved againstclickhouse.disk_mapping["default"], which is the clickhouse-server working directory, andsystem.tables.data_pathsget exactly the same substitution, so a data path always matches its own disk; when the paths can't be resolved becausedisk_mappingis not set,create/upload/download/restorefail up-front with a message naming the relative disks instead of silently corrupting the restore. The same root now also resolves the relativesystem.databases.data_path/system.tables.metadata_path(previously a leading/was forced onto them, sopreprocessed_configs, the RBAC access directory andflags/force_drop_tablewere looked up at the filesystem root andcreatefailed withcan't parse config.xml from /preprocessed_configs/config.xml) and a relativeaccess_control_path. Additionally a table data path which matches no disk path at all no longer overwrites thedefaultdisk entry of a data path which really matched it, and a relativeclickhouse.disk_mappingvalue is now rejected at config validation instead of silently disabling the resolution, fix #1121restoreno longer merges data into a leftoverdetached/<part>directory: parts are now hardlinked into aclickhouse_backup_tmp_<part>directory and renamed into place, so ClickHouse never observes a half-built part, and a same-nameddetached/<part>left by a previous failed restore or a manualALTER TABLE ... DETACH PARTITIONis renamed todetached/ignored_<part>(logged atwarn) on every local disk of the destination table before the restore, becauseATTACH PARTtakes the first disk of the storage policy which has such a directory and a stale copy on another disk shadowed the freshly restored one (it failed withNo columns.txt in partor silently attached stale data); the renamed directory keeps a manually detached part visible insystem.detached_partsand re-attachable, whileATTACH PARTITIONskips it; parts onplain/plain_rewritableobject disks are restored on the bucket level and are not covered;downloadof an incremental backup no longer aborts withnon empty `files` can't find diskwhen the rebalanced disk carries onlyrequiredparts (they are uploaded with the base backup, so they legitimately have nofilesentry in the increment); the error raised when the destination table storage policy does not contain a disk the backup needs now names the table, the part and the disk and points atSELECT * FROM system.storage_policiesinstead of dumping a Go map, fix #1034upload/create_remoteno longer keep the relative paths of every file of a table in memory for the whole upload —splitFilesByNamewalked all parts of the table up front and the resulting list (which also retained the full.../shadow/<db>/<table>/<disk>/prefix of every path viastrings.TrimPrefix) lived until the last part was uploaded, so tables with 1000+ columns and thousands ofWideparts (millions of files) droveclickhouse-backup serverto 4 GB of live heap and 10 GB RSS; withupload_by_part: true(default) the files of a part are now listed inside its upload goroutine right before it is archived, so at mostupload_concurrencypart file lists are alive, and parts already recorded in the resumable state skip the directory walk;upload_by_part: falsestill lists all files (it has to split bymax_file_size) but detaches the relative path from the full path (~3x less memory per file), fix #1550- a zero-byte "folder placeholder" object whose key equals the remote
pathwith a trailing slash (created by cloud consoles "Create folder",aws s3api put-object --key <prefix>/and similar external tools, never byclickhouse-backupitself) was listed as a broken backup with an empty name, andbackups_to_keep_remoteretention (alsoclean_remote_broken) deleted that entry withpath.Join("", ...)collapsing to the remote path root, wiping every remote backup;list remotenow skips such nameless entries with a warning and remote deletion refuses an empty backup name, fix #1524 createon aplain_rewritabledisk no longer fails withNoSuchKeywhen a__meta/<token>/prefix.pathobject is deleted between the__meta/listing and its read (a background merge or a concurrentDROPremoves directories on the live disk); the vanished token is skipped the same way ClickHouse itself would not see the directory when building its path map at startup, any other read error still fails the backup- flags placed after positional arguments are no longer silently ignored —
download my_backup --tables db.huge_tabledownloaded the whole backup because urfave/cli v1 stops flag parsing at the first positional argument, and the same applied to every command flag after a backup name (upload x --delete-source,restore x --rm, ...); the CLI migrated from urfave/cli v1 to v3, which parses flags at any position, and global flags (--config/-c,--env) now also work both before and after the command name; v1 behavior is preserved where it matters: comma-containing values of--partitions,--schedule,--envare not split into multiple values (v3 default splitting is disabled), flag names, aliases, defaults andCLICKHOUSE_BACKUP_CONFIGstay unchanged, and each in-process API re-entry builds a fresh command tree so parallel API commands can't leak flag values into each other; visible differences:--helpoutput renders typed placeholders (--tables stringinstead of--tables value), moves global flags into a per-commandGLOBAL OPTIONS:section, and an explicit--rebind-replica-path-if-exists=falseno longer overridesrebind_replica_path_if_exists: truefrom the config file, fix #1459 create --schemaand--rbac-only/--configs-only/--named-collections-onlybackups no longer require remote storage when tables live on an object disk — the remote backup destination was created and connected even though no data is copied, so a local metadata-only backup failed withstorage type 'none' is not supportedwhenremote_storage: none, fix #1517GET /backup/killandkillinPOST /backup/actionsnow actually cancel commands which were started throughPOST /backup/actions— such commands re-enter the CLI app in process with--command-idpassed before the command name, so the value lands in the application flag set, but every command action read it withc.Int("command-id"), which in urfave/cli v1 only looks at the command's own flag set (each command re-declares the application flags viaFlags: append(cliapp.Flags, ...), so it saw the-1default). The command therefore ran with a fresh background context instead of the one owned by its status row and ignored cancellation; broken since 0a26ee69 (v2.1.0), which movedcommand-idfrom the commands to the application but kept thec.Intlookups- drop a stale
upload.state2whenupload --resumeruns for a backup which doesn't exist on remote storage anymore — the resumable state survives a successful upload and is removed only together with the local backup, socreate_remote --resume+delete remote+upload --resumeskipped every data file and uploaded a backup containingmetadata.jsononly, which looked valid inlist remote; an interrupted upload still leaves the backup folder on remote, so a real resume is not affected, fix #1492 keeper.Connect(used to back up and restoreuser_directories.replicatedRBAC objects andnamed_collections_storageof akeeper*type) failed with/zookeeper/node not exists in .../preprocessed_configs/config.xmlwhen the<zookeeper>section spells its endpoints with any element name other than exactlynode. ClickHouse itself accepts every child whose name starts withnode(<nodes>,<node1>, ...), and the ClickHouse Kubernetes operator renders itszookeeper: nodes: [...]YAML config as repeated<nodes>elements, so on such clusterscreatewith the defaultrbac_backup_always: true/named_collections_backup_always: truedied vialog.Fatalon every run andserver --watchwithwatch_is_main_process: truerestarted forever without producing a single backup; the parser now mirrors the server rule and the error messages name the element which was actually found. Keeper digest auth is now also taken from<zookeeper><identity>, the element ClickHouse itself reads, in addition to the<digest>element this tool looked for before, and the newclickhouse.keeper_identity(envCLICKHOUSE_KEEPER_IDENTITY) setting supplies it when the server config hides the value frompreprocessed_configs(hide_in_preprocessed,from_env, which the ClickHouse Kubernetes operator does) — without it the connection succeeded and every read failed withzk: not authenticated