v2.8.2
NEW FEATURES
- add
clickhouse.skip_restore_non_sharded_replicated_tables(envCLICKHOUSE_SKIP_RESTORE_NON_SHARDED_REPLICATED_TABLES, defaultfalse),clickhouse.skip_check_non_sharded_replicated_tables_cluster(envCLICKHOUSE_SKIP_CHECK_NON_SHARDED_REPLICATED_TABLES_CLUSTER, default{cluster}) andclickhouse.always_sharded_macros(envCLICKHOUSE_ALWAYS_SHARDED_MACROS, default["shard"]), when truerestore,restore_remoteandrestore --dry-runrestore the data of non-sharded replicated tables (ZooKeeper path in the backup DDL resolves to the same value on every shard, macro values are read withcluster(..., system.macros),{database},{table},{uuid}are the same for the table everywhere) only on the first replica of the first shard of the cluster fromsystem.clusters, every other node restores only their schema and gets the data through replication, so the same config works on every node, a macro listed as sharded with the same value on all shards is reported with a warning, such tables are never rebound todefault_replica_path, not supported withuse_embedded_backup_restoreandrestore_cloud, fix #1181 use_embedded_backup_restore_clusternow orchestrates the whole cluster instead of only addingON CLUSTERto the BACKUP / RESTORE SQL:create,create_remote,restore,restore_remoteon the initiator node send the same command with the new--embedded-on-cluster-workerflag to every other node of the cluster through itssystem.backup_actionstable (clickhouse-backup serverwithapi.create_integration_tables: trueis required on every node) and wait for them, a worker skips the BACKUP / RESTORE SQL and only describes, uploads, downloads and fixes its ownshards/N/replicas/M/part of the shared destination, per tablemetadata/<db>/<table>.jsonmoved undershards/N/replicas/M/next to the.sqlof each node (such backups are taggedembedded,cluster=<name>, older backups without the tag keep the flat layout),.backup,metadata.json, rbac, configs and named collections stay on the initiator,delete localis propagated to the other nodes asdelete local --embedded-on-cluster-worker, sodeleteaccepts the flag too, a single node cluster behaves as before, fix #928- add
azblob.chunk_size(envAZBLOB_CHUNK_SIZE, default0), a fixed upload block size used whenceil(file size / chunk_size) <= max_parts_count, clamped to [2MiB, 4000MiB],0keeps the automaticfile size / max_parts_countclamped to [2MiB, 10MiB], brings back the explicit size removed together withAZBLOB_BUFFER_SIZEin v2.6.6, fix #1142 - back up and restore Kafka offsets of Kafka engine tables with
kafka_keeper_path/kafka_replica_name(StorageKafka2, ClickHouse 24.8+), previously only the schema was backed up and the restored table re-read from the consumer group offset (lost rows) or fromearliest(duplicates);createdetaches such tables before FREEZE so the offsets match the frozen data of the MV target (DETACH PERMANENTLYinENGINE=Replicateddatabases), attaches them back after FREEZE (also on error and cancel) and stores the Keeper subtree without ephemeral nodes inkafka_keeperfield ofmetadata/<db>/<table>.json;restorewrites the offsets to Keeper right beforeCREATE(after the--rmDROP), when the path already exists without active replicas the offsets are overwritten with a warning, when it has active replicas they are left untouched with a warning;--restore-database-mapping/--restore-table-mappingrewritekafka_keeper_path, a mapped restore with the samekafka_group_namenext to a live source on ClickHouse < 25.6 fails before any Keeper write, because consumer group rebalance would steal the partitions of the source; betweenrestore --schemaandrestore --data(Altinity ACM restore sequence) consumption is held by persistent partition lock nodes, which the data phase removes, add--skip-kafka-keeper-holdtorestore/restore_remote(RESTskip_kafka_keeper_hold) to disable it, embedded backups only log a warning, fix #1120 list remoteandrestore_remoteauto-detect native ClickHouse Cloud backups (BACKUP ... TO S3 / AzureBlobStoragewith<name>/.backupand withoutmetadata.json) unders3.path/azblob.path, such backups are listed withdata_format: cloudand size instead ofbroken (can't stat metadata.json),clean_remote_brokenandbackups_to_keep_remoteretention no longer delete them,restore_remote <name>(CLI andPOST /backup/restore_remote/{name}) switches torestore_cloudwith--tables,--partitions,--skip-empty-tables,--rm,--dry-runandrestore_schema_on_clustersupport, options whichrestore_cloudcan't apply anddownloadfail explicitly; the base backup chain of incremental cloud backups is resolved from<base_backup>of each.backup(verified bybase_backup_uuid) and passed toRESTOREwith the configured credentials instead of the ones embedded in the manifest, a base in the same bucket and path is shown as+<base>inlist remoteand protected by thedelete remoteguard, fix #1574- add
general.rbac_restore_always,general.config_restore_always,general.named_collections_restore_always(envRBAC_RESTORE_ALWAYS,CONFIG_RESTORE_ALWAYS,NAMED_COLLECTIONS_RESTORE_ALWAYS, defaultfalse), restore-side counterparts of*_backup_always, act as--rbac/--configs/--named-collectionsforrestoreandrestore_remote(including--streaming), disabled by default because these restores overwrite existing objects and runrestart_command - add
general.download_transfer_concurrency(envDOWNLOAD_TRANSFER_CONCURRENCY, default0=download_concurrency^2, negative keeps the legacy per-table limit), one transfer budget perdownloadshared by every part, archive and required part of all tables, so a single big table can use all the connections at the tail of a download, for FTP the budget is capped by2*ftp.concurrency; addgeneral.download_table_order(envDOWNLOAD_TABLE_ORDER,largest_first(default) ormetadata), only the download dispatch order changes, restore order is not affected, fix #1591 - add
general.file_transfer_concurrency(envFILE_TRANSFER_CONCURRENCY, default1), withcompression_format: nonefiles of one data part are transferred by several streams which only take slots of the shareddownload_transfer_concurrency(download) /upload_concurrency^2(upload) budget left idle by other parts, so the total number of transfers never grows above the budget, fix #1454 - add
gcs.ranged_download_concurrency(envGCS_RANGED_DOWNLOAD_CONCURRENCY, default0= disabled),gcs.ranged_download_min_size(default 64MiB),gcs.ranged_download_chunk_size(default 32MiB) andgcs.ranged_download_max_buffers(default64), archives larger thanranged_download_min_sizeare read by concurrent byte range requests and fed to decompression in order, without a temporary file, memory is capped process-wide byranged_download_max_buffers * ranged_download_chunk_size, works withdownload_max_bytes_per_second, thanks @minguyen9988, fix #1592
IMPROVEMENTS
- GCS: all pooled clients share one cached OAuth2 token, previously each client fetched its own token, so a cold
delete remotewithgcs.delete_concurrency: 50opened about 50 parallel TLS handshakes tooauth2.googleapis.comand could fail withauth: cannot fetch token: ... TLS handshake timeout, and every retry after a delete error repeated it downloadwithclickhouse.force_rebalancespreads parts round-robin across local JBOD disks whose free space differs by less than 1% of total space (typically several disks on the same filesystem), previouslyleast_usedpicked the disk with the largest free space from statvfs noise and all parts of a download could land on a single disk- Docker image: base image changed from
alpine:3.23toalpine:latestand pre-installed packages are upgraded withapk upgrade, thealpine:3.23base shippedlibcrypto3/libssl33.5.7-r0 with CVE-2026-63073 and CVE-2026-75803, thanks @dfdemar, fix #1588 - update go dependencies: aws-sdk-go-v2, azure-sdk-for-go, cloud.google.com/go/storage 1.69.0, clickhouse-go/v2 2.49.0, urfave/cli/v3 3.14.0, golang.org/x/crypto 0.58.0, klauspost/compress 1.20.3 and others
BUG FIXES
- the SQL query log no longer prints passwords embedded in URLs:
pass=/password=query string parameters anduser:password@are written as[MASKED], withapi.create_integration_tables: trueandapi.passwordset everyserverstart logged the DDL ofsystem.backup_actions,system.backup_listandsystem.backup_versionwith the API password in plain text inside theURL()engine argument, atinfolevel with the defaultclickhouse.log_sql_queries: true, the query sent to ClickHouse is unchanged, fix #1630 s3.chunk_sizeis now also used whenceil(file size / chunk_size)is exactlys3.max_parts_count, previously it fell back tofile size / max_parts_count, ReadMe.md now shows the real default5242880and the semantics ofs3.chunk_size, raise it to upload big files as fewer bigger parts instead ofmax_parts_count=1workaround, each multipart upload allocates about(s3.concurrency + 2) * chunk_sizebytes,s3.chunk_sizeandazblob.chunk_sizealso apply to uploads into object disks during restore, fix #1142watch --env=KEY=VALUEandserver --env=KEY=VALUEno longer lose the--envoverrides, config reload on everywatchiteration (also insideserver --watchandPOST /backup/watch) read only the config file and the real environment, so e.g.--env=REMOTE_STORAGE=ftpturned back into the config file value and backups went to the wrong remote storage, andservernever applied its--envto its own config at all (startup,POST /restart,SIGHUPand every API handler reload);--envis now applied to the process environment once at startup and kept for the whole process lifetime, so/backup/actionscommands and operations resumed after restart (api.complete_resumable_after_restart) inherit theserver --envtoo,--envpassed inside a/backup/actionscommand still wins and is restored after the command; a duplicated--envkey keeps the last value;servernow exits on an invalid config (including a bad--envvalue) instead of retrying every 5 seconds forever, only the ClickHouse connection is still awaited;general.disable_environment_override: truestill ignores--env--resume(anduse_resumable_state: true) now starts from scratch when the config which defines where the resumable state keys point to changed since the interrupted run:remote_storage, the active storage location (s3endpoint/bucket/path,gcsendpoint/bucket/path,azblobendpoint_suffix/account_name/container/path,cosurl/path,ftpaddress/path,sftpaddress/port/path;pathis not used bycreate),object_disk_path, the customer-provided encryption key (s3.sse_customer_key,gcs.encryption_key,azblob.sse_key, only its digest is stored),compression_formatforupload/create_remote, andrestore_database_mapping/restore_table_mappingforrestore/restore_remote --streaming; the state keys are relative to the storage path, soupload --resumeafter changings3.pathused to skip every file already uploaded to the old path; tuning concurrency, retries, credentials orlog_levelstill keeps the resume; the discarded state is logged as a warning with the changed parameters only;ResumeOperationsAfterRestartnow passes--restore-database-mapping/--restore-table-mappingback to the resumedrestore/restore_remote --streaming. Upgrade note: resumable states written by previous versions have no config fingerprint and are discarded on the first--resumeafter upgrading, so an interrupted operation restarts from scratch once, fix #1010watchno longer treats a failedbackups_to_keep_remote/backups_to_keep_localretention or--delete-sourceafter a completely uploaded backup as a failed upload: the new backup becomes the base of the next increment (previously the next increment was diffed from a stale base) and the failure is not counted towardstoo many errors ... abort watching, so a permanently failing post-upload step no longer stopswatch, it is reported only by the error log,/backup/status,system.backup_actionsand the callback, withapi.enable_metricsthefailed_create_remotescounter keeps growing andlast_create_remote_statusstays0while new backups are uploaded; all post-upload steps now run even when a previous one failed and their errors are reported together; with--schedulethefull_type: rebaseanddelete_previous_cyclesteps also proceed, a failed rebase is reported together with the post-upload error and the backup is not used as the base;use_embedded_backup_restore_clusterinitiator waits for the worker nodes before the backup is used as the base, a post-upload failure on a worker node is still counted as a failed upload;create_remote --streamingno longer reports such a complete backup as partial; one-shotcreate_remoteanduploadstill exit with a non-zero code, fix #1535restore --schema --rmwithoutrestore_schema_on_clusteron a replica ofENGINE=Replicateddatabase, when the database schema was already restored from another replica (ACM restore sequence andHow to back up / restore a sharded clusterin Examples.md), no longer fails withcode: 529 ... Cannot enqueue query on this replica, because it has replication lag,code: 36 ... It's not allowed to specify explicit zookeeper_path and replica_nameorTable ... already exists: localDROP DATABASEof a Replicated database is skipped (it removed only the current replica, which then recovered from the database DDL log concurrently with the following DROP/CREATE TABLE, tables are still dropped and their DROP TABLE is replicated),SYSTEM SYNC DATABASE REPLICAis executed before and after dropping tables (ClickHouse 22.5+), and{uuid}macro ofReplicated*MergeTreepath is no longer replaced with the backup table UUID and the replication path is no longer rebound todefault_replica_pathinside a Replicated database, fix #1611- backup and restore of parts on Azure object disks (
azure_blob_storage) now accept every disk configuration which clickhouse-server accepts, any Azure disk in the config which didn't match<storage_account_url>+<account_name>+<account_key>failed every backup with parts on any object disk: a disk without<account_key>(<use_workload_identity>true</use_workload_identity>or a managed identity) is accessed withDefaultAzureCredential, the same way asazblob.use_managed_identity: true,<connection_string>(account key, SAS,BlobEndpointorEndpointSuffix),<endpoint>with<endpoint_contains_account_name>and<endpoint_subpath>,<container>and<account_name>taken from the URL are supported, fix #1604 delete local|remoteandbackups_to_keep_remoteretention of backups created withuse_embedded_backup_restore: trueandembedded_backup_diskon an object disk (s3,azure_blob_storage) now delete the native BACKUP objects once both the local and the remote copy are gone:delete remotenever detected the backup as embedded (the cleanup ran overobject_disk_pathand removed nothing), files of the native backup named*.json(e.g.serialization.json) were skipped,tar/gzipdata archives,access,configsandnamed_collectionswere parsed as object pointers and failed the cleanup; notedelete remotenow removes the objects even when another host still keeps a downloaded local copy of the same backup, the same waydelete localalready did, contributed by @cryptoque, fix #1590listandtablestext output is now written to stdout as a single write, so a log line on stderr can no longer land in the middle of a backup row when both streams are merged (docker exec,2>&1), the same fixjson/yaml/csv/tsvformats got in v2.8.1restore/restore_remotewith--restore-database-mapping/--restore-table-mappingof tables on object disks (s3,azure_blob_storage, includingcacheandencryptedwrappers) no longer leaves the restored copy sharing object storage keys with the source table, soDROP TABLE|DATABASE ... SYNCof the copy no longer deletes the objects of the source (Code: 499 ... The specified key does not exist (S3_ERROR)on the source afterwards): the key rewrite was skipped when the disk endpoint key prefix contained the backup name (e.g. backupregressionon the diskhttps://s3/bucket/regression-test/live/), and the metadata files were linked intodetached/ the table directory before the keys were rewritten, so an interrupted restore left the copy pointing to the source keys; the rewritten key now also carries a hash of the destination table (<dir>_<backup_name>_<crc32(db.table)>/<object>), so several copies of one backup restored next to each other never share objects, the source key of the copy is always derived from the backup, so the same local backup can be restored several times, and metadata files are written via temp file + rename, so a repeated restore never modifies the parts of an already restored table through their shared hardlink, fix #1568upload --resumeno longer writes an incompletemanifest.bolt.gz: files of parts, archives and table metadata which the resumable state marks as already processed were skipped without being recorded in the manifest, so the manifest of a resumed upload listed only the files of the last attempt, overwrote the fuller one anddownload/restore_remotefell back to aWalk(ListObjectsV2) per skipped part. Skipped files are now recorded from the local backup without any extra remote listing; with--delete-source(create_remote) the local part files are already removed, so parts skipped on resume stay out of the manifest and still use theWalkfallback, fix #1569- SFTP
uploadof compressed archives (compression_formatother thannone) now keepssftp.concurrencywrites in flight per file, previously the archive stream from the compressor has no known size, so every 32KiB write waited for the previous one (about 8MB/s per file at 4ms RTT), fix #1607 - SFTP
downloadwithdownload_max_bytes_per_secondnow keeps concurrent reads per file, the rate limiter wrapper hidWriteToof the SFTP file, so every 32KiB read waited for the previous one and the download stayed at about 75% of the limit, fix #1607 - GCS ranged download (
gcs.ranged_download_concurrency) withdownload_max_bytes_per_secondnow reaches the limit instead of about 80% of it, the limit is applied to each range request on the wire instead of the in-order consumer, which waited idle until the first chunks arrived and never made up the lost time, fix #1607 - change default
gcs.ranged_download_min_sizefrom 256MiB to 64MiB, withgcs.ranged_download_concurrencyenabled archives of 64-256MiB, typical for big parts, were downloaded by a single stream and stayed the tail of the download, fix #1607 - GCS with
force_http: trueordisable_http2: trueno longer opens a new TCP connection for most requests, the HTTP/1.1 transport kept only 1 (64 withdisable_http2) idle connection, so a download with many concurrent transfers left closed connections in TIME_WAIT, exhausted the ephemeral port range and failed withconnect: cannot assign requested address, fix #1607 - security:
download/restore_remoteextraction of compressed archives rejects entries whose path escapes the target directory (../, tar-slip, CWE-22) and non-regular entries (symlinks, hardlinks), previously an archive planted in remote storage could write arbitrary files on the ClickHouse host, thanks @tpq2030 - S3
NoSuchUploadis no longer classified as a permanently missing object, so the upload retries with a new multipart upload instead of aborting,NoSuchKeyandallow_missing_files_on_downloadbehave as before, thanks @sanasol, fix #1579 create_remoteandupload --delete-sourceon CIFS/SMB mounts (Azure Files) no longer fail withdirectory not emptywhen local retention (backups_to_keep_local: -1) removes the just uploaded backup, the resumable<command>.state2file is now closed before retention, previously it was still open and CIFS deferred its deletion,GetLocalBackupsalso closes thebackupdirectory handle whenReaddirnamesfails, fix #1599- ClickHouse connection retries no longer leak connection pools, the pool whose
Pingfailed (wrong password, failed native handshake) is closed before the next retry or before returning the error, previously every retry left its drain goroutine and ticker running, thanks @assimelha - GCS with
gcs.encryption_keyfalls back to reading not encrypted objects again, GCS returns the reasonresourceNotEncryptedWithCustomerEncryptionKeyin lowerCamelCase and the check compared it case-sensitively with the UpperCamelCase spelling, so these requests failed clean --allkeepsshadow/increment.txt, previously it was removed with the rest ofshadow/and concurrent FREEZE queries which recreate it could leave it empty, after that every FREEZE failed withcode: 32, message: File /var/lib/clickhouse/shadow/increment.txt is empty- cleanup of shadow directories recorded in
freezes.tmpusesALTER TABLE ... UNFREEZE WITH NAMEwhen the table is known andSYSTEM UNFREEZE WITH NAMEonly for dropped tables, previouslySYSTEM UNFREEZEwas always used on 22.6+ and failed withcode: 344, message: Support for SYSTEM UNFREEZE query is disabledwhen theenable_system_unfreezeserver setting is not enabled createinterrupted by SIGINT / SIGTERM removes its partial local backup again, the cleanup used the already canceled context and failed withcontext canceled, leaving the backup directory behind