ClickHouse Cloud Backup 带宽控制问题诊断以及原理分析
文章目录
- 背景介绍
- 发现问题
- 设置带宽,测试带宽控制精度
- 有效带宽为设置带宽的一半
- 原因探索
- 本地备份有同样的问题吗?
- 监控上的相关线索
- 文件被读取了两遍吗?
- Multipart场景下的读取过程
- 一个文件为什么会被重复seek两次?
- 找到问题的根本原因
- 能否关闭checksum
- 总结
背景介绍
出于数据冗余的需要,我们需要使用ClickHouse的CloudBackup功能,将ClickHouse的本地表数据备份到远程的、支持S3协议的云存储上,可以是比如S3或者GFS的共有云中的对象存储,也可以是本地搭建的兼容S3协议的私有对象存储比如MinIO。
当DevOps团队要求总的网络出口带宽必须控制在10Gbps(即大概1.25GB/s)以内的时候,意味着我们需要对ClickHouse CloudBackup的带宽控制进行详细的测试,甚至对其原理进行深入的理解。
看起来备份是一个非常简单的过程,但是,至少,在测试完成以后,我们需要能够准确地回答以下基本问题:
- ClickHouse的带宽限制是否能够做到多准确?比如,当磁盘读写速率和网络出口带宽都不是瓶颈,那么这个带宽控制和实际的运行带宽可以吻合得多好?
- ClickHouse的带宽限制是否支持在Server端进行控制?
- ClickHouse是一个分布式系统,Server端的带宽控制的粒度多大?可以控制一个Shard中多个Replica的总带宽,还是说,只能对单台Server进行控制?
- 如果ClickHouse的带宽限制支持在Server端进行控制,那么是否可以支持热生效?
- ClickHouse的带宽限制是否支持在Session端进行控制?显然,如果支持在Session端进行控制,那么意味着我们可以在不修改ClickHouse的Server端带宽限制的情况下,随时、方便地控制带宽
在后来我们测试的过程中,当我们发现ClickHouse的带宽控制既可以在Server端进行也可以在Session端进行时,我们又需要进一步回答更具体的问题:
- 如果是server端的带宽控制,那么这个值是在控制ClickHouse Server的总的出口带宽,还是只是为Session的带宽控制提供了默认值?
- 对于Session端的带宽控制,控制的是这个Session中所有的Backup Query能够使用的总带宽(因为我可以在一个session中异步发起多个并发的Backup/Restore Query),还是说,Session中的每一个Query都使用该值进行自己的Query的带宽控制,各个Query之间没有任何约束(当然,总的物理带宽会约束所有Query能够使用的带宽)?
我在我的很多文章中都介绍过,在排查问题或者调研一个项目以前,尽管技术细节不确定,但是排查问题或者调研的完成标准(Accept Criteria)其实是确定的,也就是说,在我们排查一个问题以前,我们已经知道最终必须要回答的问题。
发现问题
设置带宽,测试带宽控制精度
我们首先想看一下带宽控制是否有效。
我们在一台ClickHouse端,设置Server端的带宽控制为128MB/s,同时,我们触发一个backup query,用来将一张表的一个partition备份到远程的S3上。
<max_backup_bandwidth_for_server>134217728</max_backup_bandwidth_for_server>
然后,我们运行下面的命令,将该表的20240802
分区备份到GFS上:
rccp605-3.iad7.prod.conviva.com :) BACKUP TABLE default.app_events_local PARTITION '20240802' TO S3('https://storage.googleapis.com/clickhouse-backup-restore-benchmark/app_events_local/test_backup_158', 'GOOGDDRH7TTROHEVBA6RSUU7', 'soIRotkuSB1+T7yp405Xn019JK6eXRX8h6rLEByh') SETTINGS async = true
有效带宽为设置带宽的一半
我们后面在介绍源代码的时候会介绍,ClickHouse备份的时候,除了备份时候的元数据处理,在数据文件层面,就是将本地的列文件备份到远程。因此,假如带宽控制精准,并且我们已经知道了这个Partition的数据信息(文件在磁盘上的大小),那么这个备份所需要的时间其实是可以预计的:
比如,我们对一个磁盘压缩大小为44GB的Partition备份到GCS:
test-host.prod.com :) SELECT
partition,
count(1) AS num_of_parts,
sum(rows) AS num_of_rows,
toInt32((sum(bytes_on_disk) / 1024) / 1024) AS bytes_on_disk_mb,
toInt32((sum(data_compressed_bytes) / 1024) / 1024) AS data_compressed_bytes_mb,
toInt32((sum(data_uncompressed_bytes) / 1024) / 1024) AS data_uncompressed_bytes_mb
FROM system.parts
WHERE (table = 'app_events_local') AND (partition = '20240802')
GROUP BY partition
ORDER BY partition ASC
FORMAT Vertical
Query id: 300ece69-0d6a-4038-b886-f3e9d3c7bfea
Row 1:
──────
partition: 20240802
num_of_parts: 1
num_of_rows: 604283048
bytes_on_disk_mb: 43878
data_compressed_bytes_mb: 43854
data_uncompressed_bytes_mb: 544276
1 row in set. Elapsed: 0.008 sec.
可以看到,这个partition在磁盘上的数据量大概为44GB,如果备份带宽控制精准,那么备份需要用掉 43878MB / 128 = 5m40s
,即大概在6min中之内可以完成备份。
test-host.prod.com :) select * from system.backups format Vertical
SELECT *
FROM system.backups
FORMAT Vertical
Query id: 413d167c-31bd-4bb2-970f-57d10072a560
Row 1:
──────
id: 553ed3c0-b303-4ce4-93a7-aca217a737cd
name: S3('https://storage.googleapis.com/clickhouse-backup-restore-benchmark/app_events_local/test_backup_158', '******', '[HIDDEN]')
status: BACKUP_CREATED
error:
start_time: 2024-12-25 14:46:01
end_time: 2024-12-25 14:57:28
num_files: 292
total_size: 46009871762
num_entries: 267
uncompressed_size: 45998089621
compressed_size: 45998089621
files_read: 0
bytes_read: 0
整个备份从14:46:01开始,到14:57:28结束,用时大概11m27s,平均带宽为 43878MB / 687s = 63.9MB/s ≈ 64MB/s
这个实际有效带宽是我们设置的带宽的正好一半。这让我们非常疑惑,如果实际带宽与我们设置的带宽没有这么准确的减半关系,那么我们似乎有理由怀疑系统其他地方也许是瓶颈,比如,磁盘读写,网络出口带宽,远程的S3的写入速度约束等。但是,实际带宽刚好是我们设置带宽的一半,非常准确的一半。我们随后将max_backup_bandwidth_for_server
设置为64MB, 32MB等等,结果也一样,实际有效带宽刚好是我们设置的带宽的一半。
难道我们是哪个地方使用错了?再次在代码层面确认max_backup_bandwidth_for_server
的准确含义,没有问题啊,也没有针对S3的特殊解释
------------------------------------ ServerSettings.h --------------------------------
M(UInt64, max_backup_bandwidth_for_server, 0, "The maximum read speed in bytes per second for all backups on server. Zero means unlimited.", 0) \
原因探索
本地备份有同样的问题吗?
验证本地备份(即,将表的数据备份到本地磁盘)是否存在同样的问题,有利于我们缩小调查范围:
- 假如本地备份同样存在问题,那么我们似乎可以推断所有数据读取的带宽控制都有问题,甚至再次怀疑是我们对该参数的使用存在问题,因为,如此大范围的Bug似乎不太可能。
- 假如本地备份并不存在问题,那么似乎这的确更有可能是ClickHouse的Bug,并且,这个Bug位于云备份和本地备份不同的代码位置,而不在他们共同的代码位置。
我们使用如下命令将Partition 20240802
备份到本地其他磁盘:
rccp605-3.iad7.prod.conviva.com :) BACKUP TABLE default.app_events_local PARTITIONS '20240802' TO Disk('backups', '20240802-BACKUP-44') ASYNC
我们的测试结果如下:
rccp605-3.iad7.prod.conviva.com :) select * from system.backups format Vertical
SELECT *
FROM system.backups
FORMAT Vertical
Query id: c9fbbd62-5bee-4581-a89b-83176d51ea7c
Row 1:
──────
id: e21d8c35-b233-4265-9af7-ab2c8725caf8
name: Disk('backups', '20240802-BACKUP-44')
status: BACKUP_CREATED
error:
start_time: 2024-12-25 14:59:15
end_time: 2024-12-25 15:04:57
num_files: 292
total_size: 46009871762
num_entries: 267
uncompressed_size: 45998139227
compressed_size: 45998139227
files_read: 0
bytes_read: 0
整个本地备份从14:59:15
开始,到15:04:57
结束,用时大概5m42s
,平均带宽为 43878MB / 342s = 128.29 MB/s ≈ 128MB/s
,控制得非常精确。
我们后来在代码层面看到,无论是备份到S3还是备份到本地,使用的都是相同的Throttle实现(下文会详细介绍),所以,可以看到,这个Throttle控制类本身不存在问题。
监控上的相关线索
在发现问题以后,虽然能够复现问题,但是我们在日志层面无法找到任何线索。所以,看起来想要找到问题的根本原因,需要在关键位置添加日志,然后重新编译运行ClickHouse。但是在此之前,我们希望缩小调查范围,最直观的,这个流量减半,是发生在读取的过程中,还是发生在发送的过程中呢?
首先,我们想知道,在备份到S3的过程中,真实发送到网络上的数据量是多少?这涉及到这种猜想:有没有可能整个带宽控制是准确的,但是同一份数据往网络上发送了两次,所以,这样总体带宽(原始数据/备份时间)就看起来似乎减半了。
真实发送到网络的数据量,可以从ClickHouse暴露出来的metrics node_network_transmit_bytes_total
上看出来:
从这个Metrics上可以看到,在备份到S3的过程中,真实发送到网络上的数据量的确大致是磁盘上这个分区的文件数据量,大概为45GB。这意味着,同一份数据并没有往网络上发送两次,即,网络上的真实传输带宽(Network Transmit),就是我们设置的一半,即大约64MB/s。
而本地备份不需要网络传输,因此其带来的网络传输(Network Transmit)为0。
同时,我们从代码中看到,在往S3备份的过程中,写入到S3的Stream的字节数,都记录在Metrics WriteBufferFromS3Bytes中,因此我们立刻观察该Metrics:
可以看到,这个统计数据与上面提到的网络发送数据结果一致,在备份过程中写入的数据量数据只写了一次。
那么,在读取的时候的,备份到S3和备份到本地磁盘是什么样子的呢?按理说,二者都是读取同样的磁盘文件,因此读取层面应该没有任何差别。
我们查看代码,发现在读取的时候,暴露出来的LocalReadThrottlerBytes
反馈出了读取的总共数据量:
bool AsynchronousReadBufferFromFileDescriptor::nextImpl()
{
if (prefetch_future.valid())
{
/// Read request already in flight. Wait for its completion.
size_t size = 0;
size_t offset = 0;
{
Stopwatch watch;
CurrentMetrics::Increment metric_increment{CurrentMetrics::AsynchronousReadWait};
auto result = prefetch_future.get(); // 等待预取结束
ProfileEvents::increment(ProfileEvents::AsynchronousReadWaitMicroseconds, watch.elapsedMicroseconds());
size = result.size;
offset = result.offset;
}
prefetch_future = {};
file_offset_of_buffer_end += size;
size_t bytes_read = size - offset;
if (throttler)
{
throttler->add(bytes_read, ProfileEvents::LocalReadThrottlerBytes, ProfileEvents::LocalReadThrottlerSleepMicroseconds);
}
....
return false;
}
上面代码的基本逻辑是,通过预取的方式读取数据到缓存中,异步读取,阻塞等待读取结束。在读取结束以后,会将读取到的数据的字节数添加到统计变量LocalReadThrottlerBytes
中:
我们惊讶地看到:
- 备份到S3时的数据读取量是备份到本地的数据读取量的两倍
- 备份到本地的时候所读取的数据量的确等于这个partition在磁盘上的数据量,而备份到S3的时候的数据读取量是这个partition在磁盘上的数据量的两倍
- 如果按照监控显式的磁盘上的数据读取量除以时间,得到的数据读取速率刚好就是我们设置的备份带宽128MB/s。我们从上面监控可以看到,备份到S3和备份到本地,整个数据量随时间变化的斜率是一致的。
所以,我们知道了:原来,在备份到S3的时候,读取带宽控制有效且准确,完全做到了128MB/s
,但是,莫名其妙地,本地数据被读取了两次,造成最终的有效带宽减半,变成了64MB/s
。
到现在,我们排查问题的范围进一步缩小:不是数据被发送了两遍,而是数据被读取了两遍。
文件被读取了两遍吗?
我们的第一感觉是,是不是在备份到S3的时候,读取文件的第一遍总是失败,因此所有文件其实被读取了两遍,导致有效带宽减半?同时,Metrics指标LocalReadThrottlerBytes
记录的是读取文件的字节数,即使读取失败也不会回退,因此指标LocalReadThrottlerBytes
反馈了实际读取的数据量。
但是,我们在日志层面,没有发现读取异常发生。而且,即使发生异常,为什么会稳定发生?
因此,我们需要添加日志来验证文件实际被读取的次数。
我们打开了trace日志,以查看某一个文件的以下日志的打印次数:
void BackupReaderDefault::copyFileToDisk(const String & path_in_backup, size_t file_size, bool encrypted_in_backup,
DiskPtr destination_disk, const String & destination_path, WriteMode write_mode)
{
LOG_TRACE(log, "Copying file {} to disk {} through buffers", path_in_backup, destination_disk->getName());
auto read_buffer = readFile(path_in_backup);
std::unique_ptr<WriteBuffer> write_buffer;
auto buf_size = std::min(file_size, write_buffer_size);
if (encrypted_in_backup)
write_buffer = destination_disk->writeEncryptedFile(destination_path, buf_size, write_mode, write_settings);
else
write_buffer = destination_disk->writeFile(destination_path, buf_size, write_mode, write_settings);
copyData(*read_buffer, *write_buffer, file_size);
write_buffer->finalize();
}
这个日志是某一个线程开始将某个文件拷贝到远程S3时候打印的日志。在ClickHouse中,由于每一个列都有一个列文件,因此,当我们触发一个备份操作,会有一个线程池来并发处理所有需要备份的文件,进行并发备份。这个并发度可以通过metrics BackupsIOThreads
来观察到,如下所示。这里不再赘述:
我们以其中某一个列文件86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
为例,查看日志:
root@rccp605-3:/conviva/log/clickhouse-server# grep 86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin clickhouse-server.log.back|grep '2024.12.24 13'|grep 'Copying file'
2024.12.24 13:33:36.698382 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Trace> BackupWriterS3: Copying file store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin from disk disk1 through buffers
我们看到,这一行日志的确只打印了一次,说明,在调度层面,将某个文件拷贝到远程,的确只调度了一次。注意是调度了一次,在这一次的调度中,到底读取了几次呢?
Multipart场景下的读取过程
我们进一步查看往S3备份的过程,可以看到:
- 如果一个文件的大小大于
upload_settings.max_single_part_upload_size
,那么使用Multipart上传,即,通过Http Multipart的方式将文件上传给S3 - 如果一个文件的大小小于
upload_settings.max_single_part_upload_size
,那么不使用Multipart上传.
其中,upload_settings.max_single_part_upload_size
大小默认设置32MB:
M(UInt64, s3_max_single_part_upload_size, 32*1024*1024, "The maximum size of object to upload using singlepart upload to S3.", 0) \
// CopyDataToFileHelper::performCopy()
// 调用者是 void copyDataToS3File
void performCopy()
{
if (size <= upload_settings.max_single_part_upload_size) // 这个值是32MB
performSinglepartUpload(); // 小于32mb, 单part上传
else
performMultipartUpload(); // 搜索 void performMultipartUpload()
我们看了一下我们所选取的列文件,大小为91MB
,因此,其走的是Multipart上传:
root@rccp605-3:/conviva/log/clickhouse-server# ls -lh /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
-rw-r----- 2 clickhouse clickhouse 91M Dec 22 13:12 /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
我们进一步查看Multipart的上传过程:
void performMultipartUpload(size_t start_offset, size_t size)
{
calculatePartSize(size);
createMultipartUpload();
size_t position = start_offset;
size_t end_position = start_offset + size;
try
{
for (size_t part_number = 1; position < end_position; ++part_number)
{
size_t next_position = std::min(position + normal_part_size, end_position);
size_t part_size = next_position - position; /// `part_size` is either `normal_part_size` or smaller if it's the final part.
Stopwatch watch;
// UploadHelper::uploadPart,从position开始,上传part_size大小
uploadPart(part_number, position, part_size);
watch.stop();
ProfileEvents::increment(ProfileEvents::WriteBufferFromS3Bytes, part_size);
ProfileEvents::increment(ProfileEvents::WriteBufferFromS3Microseconds, watch.elapsedMicroseconds());
position = next_position;
}
}
....
}
可以看到,这个方法会计算出每一个S3 Part(注意这里的Part指的是S3 Http Part,不是ClickHouse中的Part)的大小,然后调用UploadHelper::uploadPart()
来上传这个part,其中,一个S3 Part的大小为16MB
。显然,在读取并上传一个part的时候,会通过文件的seek定位到指定的offset,然后从该offset读取指定大小的part。
我们从监控中可以看到S3备份过程中往S3上写入的总的Http Part的数量:
我们对这个数据进行了大致分析,可以看到,总的S3 Part数量为2.75K
,平均一个Part为16MB
,总的大小为2750 * 16 ≈ 44G
, 与实际数据量吻合。
那么,对于一个92MB
的文件,由于是Multipart Upload,每次读取一个16MB
的part的时候,这个16MB
的part是否只读取了一次呢?
在最底层,读取数据然后发送到远程,其实是为对应的S3 Http客户端提供对应的读缓存,S3 Client会将缓存中的数据发送过去:
// CopyDataToFileHelper::fillUploadPartRequest
std::unique_ptr<Aws::AmazonWebServiceRequest> fillUploadPartRequest(size_t part_number, size_t part_offset, size_t part_size) override
{
auto read_buffer = std::make_unique<LimitSeekableReadBuffer>(create_read_buffer(), part_offset, part_size);
/// Setup request.
auto request = std::make_unique<S3::UploadPartRequest>();
request->SetBucket(dest_bucket);
request->SetKey(dest_key);
request->SetPartNumber(static_cast<int>(part_number));
request->SetUploadId(multipart_upload_id);
request->SetContentLength(part_size);
request->SetBody(std::make_unique<StdStreamFromReadBuffer>(std::move(read_buffer), part_size));
/// If we don't do it, AWS SDK can mistakenly set it to application/xml, see https://github.com/aws/aws-sdk-cpp/issues/1840
request->SetContentType("binary/octet-stream");
return request;
}
从上面的代码可以看到,交付给S3 Client的缓存是create_read_buffer(),其中,create_read_buffer是一个传入进行的预定义的function。create_read_buffer()创建的缓存被进一步封装成一个LimitSeekableReadBuffer对象,从名字可以看到,这是一个可以seek到某个为止的缓存。这个理所当然,由于是Multipart Upload,因此需要一个part 一个 part地seek到指定为止,然后读取一部分数据,发送给S3。
那么,这时候的问题是,create_read_buffer()
的调用次数是否正确呢?
create_read_buffer
实际上是一个callback function,这个callback的定义如下。由于是在文件读取层面,因此这个Callback对于Backup to S3和Backup to Local是相同的:
void BackupWriterDefault::copyFileFromDisk(const String & path_in_backup, DiskPtr src_disk, const String & src_path,
bool copy_encrypted, UInt64 start_pos, UInt64 length)
{
// 在这个位置,备份到s3和备份到disk是一样的,为什么readFile中LocalReadThrottlerBytes会翻倍呢?
LOG_TRACE(log, "Copying file {} from disk {} through buffers", src_path, src_disk->getName());
auto create_read_buffer = [src_disk, src_path, copy_encrypted, settings = read_settings.adjustBufferSize(start_pos + length)]
{
if (copy_encrypted) {
return src_disk->readEncryptedFile(src_path, settings);
}
else
// 这里只是定义了一个callback
{
// std::unique_ptr<ReadBufferFromFileBase> DiskLocal::readFile
// 无论是备份到本地还是远程,这里都会调用
// 返回的是 AsynchronousReadBufferFromFileWithDescriptorsCache
return src_disk->readFile(src_path, settings);
}
};
// void BackupWriterS3::copyDataToFile 或 void BackupWriterDefault::copyDataToFile
copyDataToFile(path_in_backup, create_read_buffer, start_pos, length);
}
我们增加了日志,希望知道回调函数create_read_buffer()被调用了几次。这个日志增加在DiskLocal::readFile()中(我们之前不清楚上面代码的调用路径是会走src_disk->readEncryptedFile() 还是会走 src_disk->readFile()
,因为我们的磁盘起始是物理加密的,因此两个方法都加了日志,最后才确认实际上走的是src_disk->readFile()
路径):
root@rccp605-3:/conviva/log/clickhouse-server# grep 86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin clickhouse-server.log.back|grep '2024.12.24 13'|grep -v 'is invalid reading'|grep 'buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file'
2024.12.24 13:33:36.818498 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
2024.12.24 13:33:36.818562 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
2024.12.24 13:33:36.818589 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
2024.12.24 13:33:36.818626 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
2024.12.24 13:33:36.818652 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
2024.12.24 13:33:36.818698 [ 2394170 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> ReadBufferFromFileBase: mydebug Creating read buffer AsynchronousReadBufferFromFileWithDescriptorsCache from file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin with local throttle
这里的日志的含义是,CopyDataToFileHelper::fillUploadPartRequest()
方法在试图读取一个S3 Part的时候,都会调用一次回调函数create_read_buffer(),以将对应的part的数据load到缓存中,进而进一步被S3 Http 客户端发送到远程。
从上面的日志可以看到,create_read_buffer()
回调被调用了6
次,这完全符合预期:文件大小为92MB
,一个S3 Http Part大小为16M
,刚好需要发送6次Part,因此create_read_buffer()
被调用了6次。
所以,文件的Http Part的确是6个,在发送的时候为每一个part创建了一个AsynchronousReadBufferFromFileWithDescriptorsCache
读缓存,没有任何问题。
一个文件为什么会被重复seek两次?
那么问题来了,为什么LocalReadThrottlerBytes
指标会是文件大小的两倍呢?也就是说,尽管为每一个part创建了一个AsynchronousReadBufferFromFileWithDescriptorsCache
,但是,看起来,每一个AsynchronousReadBufferFromFileWithDescriptorsCache
被用来读取了两遍文件?
于是我企图看一下一个文件逐渐被seek的位置信息。正常情况下,如果一个文件被正常读取一次,那么seek的offset位置应该是逐渐递增,不会重叠:
root@rccp605-3:/conviva/log/clickhouse-server# grep 86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin clickhouse-server.log.back|grep '2024.12.24 13'|grep -v 'is invalid reading'|grep 'mydebug trying to reset to'
2024.12.24 13:33:36.818708 : mydebug trying to reset to offset 0 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:33:36.818709 : mydebug trying to reset to offset 16777216 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:33:36.818763 : mydebug trying to reset to offset 67108864 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:33:36.818785 : mydebug trying to reset to offset 33554432 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:33:36.818802 : mydebug trying to reset to offset 50331648 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:33:36.818853 : mydebug trying to reset to offset 83886080 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:13.788614 : mydebug trying to reset to offset 83886080 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:30.270481 : mydebug trying to reset to offset 0 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:30.448922 : mydebug trying to reset to offset 16777216 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:30.533604 : mydebug trying to reset to offset 67108864 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:30.615074 : mydebug trying to reset to offset 33554432 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
2024.12.24 13:34:30.648329 : mydebug trying to reset to offset 50331648 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin
看起来是两个东西在读这个文件,每一个offset的位置都被seek了两次。对于这个92MB
的文件,由于每个文件的大小是16MB
,因此被seek了6 * 2 = 12次,每一个顺序的seek位置如下:
- 0: 读取第1个part,
- 16777216(= 16MB): 读取第2个part,
- 67108864(= 64MB): 读取第4个part
- 33554432(= 32MB): 读取第3个part
- 50331648(= 48MB): 读取第5个part
- 83886080(= 80MB): 读取第6个part
找到问题的根本原因
由于依然不知道为什么被seek了两次,我们最后的希望,是通过堆栈找出调用者。于是,我们在seek的地方添加了如下hardcode代码,专门针对我们所观察的这个文件来打印堆栈:
------------------------- AsynchronousReadBufferFromFileDescriptor -------------------------
/// If 'offset' is small enough to stay in buffer after seek, then true seek in file does not happen.
off_t AsynchronousReadBufferFromFileDescriptor::seek(off_t offset, int whence)
{
LOG_INFO(&Poco::Logger::get("AsynchronousReadBufferFromFileDescriptor"),
"mydebug trying to reset to offset {} with whence {} for file {} ",
offset, whence, getFileName());
if (getFileName() == "/conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin") {
std::string stacktrace_str = boost::stacktrace::to_string(boost::stacktrace::stacktrace());
LOG_INFO(&Poco::Logger::get("AsynchronousReadBufferFromFileDescriptor"),
" Currently it is seek to offset {} with whence {} for file {}. stacktrace is {}",
offset, whence, getFileName(), stacktrace_str);
}
然后,针对offset(50331648
),我们看到了如下两个堆栈:
2024.12.24 13:33:37.601312 [ 2394205 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> AsynchronousReadBufferFromFileDescriptor: Currently it is seek to offset 50331648 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin. stacktrace is 0# DB::AsynchronousReadBufferFromFileDescriptor::seek(long, int) in /conviva/clickhouse-build/clickhouse
1# DB::LimitSeekableReadBuffer::nextImpl() in /conviva/clickhouse-build/clickhouse
2# DB::StdStreamBufFromReadBuffer::xsgetn(char*, long) in /conviva/clickhouse-build/clickhouse
3# std::__1::basic_istream<char, std::__1::char_traits<char>>::read(char*, long) in /conviva/clickhouse-build/clickhouse
4# Aws::Utils::Crypto::MD5OpenSSLImpl::Calculate(std::__1::basic_istream<char, std::__1::char_traits<char>>&) in /conviva/clickhouse-build/clickhouse
5# Aws::Utils::Crypto::MD5::Calculate(std::__1::basic_istream<char, std::__1::char_traits<char>>&) in /conviva/clickhouse-build/clickhouse
6# Aws::Utils::HashingUtils::CalculateMD5(std::__1::basic_iostream<char, std::__1::char_traits<char>>&) in /conviva/clickhouse-build/clickhouse
7# Aws::Client::AWSClient::AddChecksumToRequest(std::__1::shared_ptr<Aws::Http::HttpRequest> const&, Aws::AmazonWebServiceRequest const&) const in /conviva/clickhouse-build/clickhouse
8# Aws::Client::AWSClient::BuildHttpRequest(Aws::AmazonWebServiceRequest const&, std::__1::shared_ptr<Aws::Http::HttpRequest> const&) const in /conviva/clickhouse-build/clickhouse
9# DB::S3::Client::BuildHttpRequest(Aws::AmazonWebServiceRequest const&, std::__1::shared_ptr<Aws::Http::HttpRequest> const&) const in /conviva/clickhouse-build/clickhouse
10# Aws::Client::AWSClient::AttemptOneRequest(std::__1::shared_ptr<Aws::Http::HttpRequest> const&, Aws::AmazonWebServiceRequest const&, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
11# Aws::Client::AWSClient::AttemptExhaustively(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
12# Aws::Client::AWSXMLClient::MakeRequest(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
13# Aws::Client::AWSXMLClient::MakeRequest(Aws::AmazonWebServiceRequest const&, Aws::Endpoint::AWSEndpoint const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
14# Aws::S3::S3Client::UploadPart(Aws::S3::Model::UploadPartRequest const&) const in /conviva/clickhouse-build/clickhouse
15# DB::S3::Client::UploadPart(DB::S3::ExtendedRequest<Aws::S3::Model::UploadPartRequest> const&) const in /conviva/clickhouse-build/clickhouse
16# 0x0000000016253A39 in /conviva/clickhouse-build/clickhouse
17# 0x000000001625731F in /conviva/clickhouse-build/clickhouse
18# 0x0000000016257718 in /conviva/clickhouse-build/clickhouse
2024.12.24 13:34:30.919415 [ 2394205 ] {87f26b9c-ccd9-428d-b225-160716a48015} <Information> AsynchronousReadBufferFromFileDescriptor: Currently it is seek to offset 50331648 with whence 0 for file /conviva/data/nvme2n1/clickhouse/store/86c/86c91233-2b33-43e4-8a1e-10f0daa3801b/20240802_2_2_0/deviceHardwareType.bin. stacktrace is 0# DB::AsynchronousReadBufferFromFileDescriptor::seek(long, int) in /conviva/clickhouse-build/clickhouse
1# DB::LimitSeekableReadBuffer::nextImpl() in /conviva/clickhouse-build/clickhouse
2# DB::StdStreamBufFromReadBuffer::xsgetn(char*, long) in /conviva/clickhouse-build/clickhouse
3# std::__1::basic_istream<char, std::__1::char_traits<char>>::read(char*, long) in /conviva/clickhouse-build/clickhouse
4# Poco::StreamCopier::copyStream(std::__1::basic_istream<char, std::__1::char_traits<char>>&, std::__1::basic_ostream<char, std::__1::char_traits<char>>&, unsigned long) in /conviva/clickhouse-build/clickhouse
5# DB::S3::PocoHTTPClient::makeRequestInternal(Aws::Http::HttpRequest&, std::__1::shared_ptr<DB::S3::PocoHTTPResponse>&, Aws::Utils::RateLimits::RateLimiterInterface*, Aws::Utils::RateLimits::RateLimiterInterface*) const in /conviva/clickhouse-build/clickhouse
6# DB::S3::PocoHTTPClient::MakeRequest(std::__1::shared_ptr<Aws::Http::HttpRequest> const&, Aws::Utils::RateLimits::RateLimiterInterface*, Aws::Utils::RateLimits::RateLimiterInterface*) const in /conviva/clickhouse-build/clickhouse
7# Aws::Client::AWSClient::AttemptOneRequest(std::__1::shared_ptr<Aws::Http::HttpRequest> const&, Aws::AmazonWebServiceRequest const&, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
8# Aws::Client::AWSClient::AttemptExhaustively(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
9# Aws::Client::AWSXMLClient::MakeRequest(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
10# Aws::Client::AWSXMLClient::MakeRequest(Aws::AmazonWebServiceRequest const&, Aws::Endpoint::AWSEndpoint const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const in /conviva/clickhouse-build/clickhouse
11# Aws::S3::S3Client::UploadPart(Aws::S3::Model::UploadPartRequest const&) const in /conviva/clickhouse-build/clickhouse
12# DB::S3::Client::UploadPart(DB::S3::ExtendedRequest<Aws::S3::Model::UploadPartRequest> const&) const in /conviva/clickhouse-build/clickhouse
13# 0x0000000016253A39 in /conviva/clickhouse-build/clickhouse
14# 0x000000001625731F in /conviva/clickhouse-build/clickhouse
15# 0x0000000016257718 in /conviva/clickhouse-build/clickhouse
问题非常清楚了,第一个堆栈显式,在计算MD5 checksum的时候,调用了LimitSeekableReadBuffer.nextImpl()
定位到了具体的50331648
位置,然后,在正式读取文件的时候,又再次seek到了offset 50331648
的位置,因此,总共读取了两次。尽管在计算checksum的时候,没有将文件发送到远程的S3,但是,在通过LimitSeekableReadBuffer
-> AsynchronousReadBufferFromFileDescriptor
进行读取的时候,AsynchronousReadBufferFromFileDescriptor
其实并不清楚这是在进行checksum还是在进行实际的上传读取,因此这个读取全部进入到了metrics LocalReadThrottlerBytes
中。
能否关闭checksum
当我们发现checksum是由于S3客户端的checksum导致的时候,至少,整个ClickHouse的backup的带宽控制中的bug及其原理已经被我们找到,我们可以通过workaround的方式进行精准的带宽控制了:比如,我们限制带宽到128MB/s,我们就可以设置max_backup_bandwidth=256M/s
了。
但是,我们还是想探究一下背后优雅的解决方案。
- 如果Checksum可以关闭,那么我们至少在ClickHouse客户端可以选择关闭Checksum,这样,我们就可以对其进行精准带宽控制,而不用采取这种workaround的方式;但是,我想,大部分用户可能都不想这么去做。
- 如果Checksum不可以关闭,这个问题无法避免,至少,我们可以在带宽控制层面,自动帮助用户去调整背后实际的读取带宽,让最终的有效带宽为用户的设置值。比如,如果用户设置的
max_backup_bandwidth=128MB/s
,并且我们确定checksum无法关闭,那么我们可以讲背后的实际读取带调整为256MB/s
,从而让有效的网络带宽为128MB/s
aws这个模块是作为ClickHouse的submodule编译进来的,我们查看一下这个module的相关信息:
在.gitmodules
中,对应的aws-sdk的submodule定义如下:
[submodule "contrib/aws"]
path = contrib/aws
url = https://github.com/ClickHouse/aws-sdk-cpp
然后,我们进入到对应contrib/aws
路径查看其版本号:
root@rccd101-6c:/conviva/ClickHouse-Debug/contrib/aws# git branch
* (HEAD detached at ca02358dcc)
master
root@rccd101-6c:/conviva/ClickHouse-Debug/contrib/aws# cat VERSION
1.11.61
对照堆栈,我们找到了相应代码:
在ClickHouse准备上传一个Part的时候,会调用CopyDataToFileHelper::fillUploadPartRequest()
方法来构造一个Aws::S3:Model:UploadPartRequest
对象:
// CopyDataToFileHelper::fillUploadPartRequest
std::unique_ptr<Aws::AmazonWebServiceRequest> fillUploadPartRequest(size_t part_number, size_t part_offset, size_t part_size) override
{
auto read_buffer = std::make_unique<LimitSeekableReadBuffer>(create_read_buffer(), part_offset, part_size);
/// Setup request.
auto request = std::make_unique<S3::UploadPartRequest>();
request->SetBucket(dest_bucket);
request->SetKey(dest_key);
request->SetPartNumber(static_cast<int>(part_number));
request->SetUploadId(multipart_upload_id);
request->SetContentLength(part_size);
request->SetBody(std::make_unique<StdStreamFromReadBuffer>(std::move(read_buffer), part_size));
该对象的继承关系如下所示:
我们从代码里面看到,所有的S3的Http请求都会实现Aws::AmazonWebServiceRequest
, 其ShouldComputeContentMd5()
是False
,而 GetChecksumAlgorithmName()
方法返回的是空:
/**
* If this is set to true, content-md5 needs to be computed and set on the request
*/
inline virtual bool ShouldComputeContentMd5() const { return false; }
inline virtual Aws::String GetChecksumAlgorithmName() const { return {}; }
类Aws::S3:Model:UploadPartRequest
重写了Aws::AmazonWebServiceRequest
类的方法GetChecksumAlgorithmName()
方法:
Aws::String UploadPartRequest::GetChecksumAlgorithmName() const
{
// 在没有指定checksum algo 的情况下,默认使用md5
if (m_checksumAlgorithm == ChecksumAlgorithm::NOT_SET)
{
return "md5";
}
else
{
return ChecksumAlgorithmMapper::GetNameForChecksumAlgorithm(m_checksumAlgorithm);
}
}
从方法实现可以看到,如果当前使用的checksum是ChecksumAlgorithm::NOT_SET
,那么就使用默认的md5
作为checksum。 构造UploadPartRequest
的时候,默认的checksum是就是ChecksumAlgorithm::NOT_SET
:
UploadPartRequest::UploadPartRequest() :
m_bucketHasBeenSet(false),
m_contentLength(0),
m_contentLengthHasBeenSet(false),
m_contentMD5HasBeenSet(false),
m_checksumAlgorithm(ChecksumAlgorithm::NOT_SET),
调用者可以通过调用UploadPartRequest::SetChecksumAlgorithm()
方法来设置对应的Checksum算法,支持的Checksum算法放在enum Aws::S3::Model::ChecksumAlgorithm
中:
inline void SetChecksumAlgorithm(const ChecksumAlgorithm& value) { m_checksumAlgorithmHasBeenSet = true; m_checksumAlgorithm = value; }
enum class ChecksumAlgorithm
{
NOT_SET,
CRC32,
CRC32C,
SHA1,
SHA256
}
由于ClickHouse在调用CopyDataToFileHelper::fillUploadPartRequest
的时候没有显式通过方法设置checksum,因此,此时有效的 Checksum就是md5
:
// CopyDataToFileHelper::fillUploadPartRequest
std::unique_ptr<Aws::AmazonWebServiceRequest> fillUploadPartRequest(size_t part_number, size_t part_offset, size_t part_size) override
{
auto read_buffer = std::make_unique<LimitSeekableReadBuffer>(create_read_buffer(), part_offset, part_size);
/// Setup request.
auto request = std::make_unique<S3::UploadPartRequest>();
request->SetBucket(dest_bucket);
request->SetKey(dest_key);
request->SetPartNumber(static_cast<int>(part_number));
request->SetUploadId(multipart_upload_id);
request->SetContentLength(part_size);
request->SetBody(std::make_unique<StdStreamFromReadBuffer>(std::move(read_buffer), part_size));
/// If we don't do it, AWS SDK can mistakenly set it to application/xml, see https://github.com/aws/aws-sdk-cpp/issues/1840
request->SetContentType("binary/octet-stream");
return request;
}
我们从上面的堆栈可以看到,在上传一个S3 Part的时候会调用AWSClient::BuildHttpRequest()
, 在该方法中,会强制调用AWSClient::AddChecksumToRequest()
方法来尝试添加Checksum:
void AWSClient::BuildHttpRequest(const Aws::AmazonWebServiceRequest& request, const std::shared_ptr<HttpRequest>& httpRequest) const
{
....
AddChecksumToRequest(httpRequest, request);
....
}
我们看一下AWSClient::AddChecksumToRequest()
的具体过程:
AWSClient.cpp:
void AWSClient::AddChecksumToRequest(const std::shared_ptr<Aws::Http::HttpRequest>& httpRequest,
const Aws::AmazonWebServiceRequest& request) const
Aws::String checksumAlgorithmName = Aws::Utils::StringUtils::ToLower(request.GetChecksumAlgorithmName().c_str());
// Request checksums
if (!checksumAlgorithmName.empty())
{
// For non-streaming payload, the resolved checksum location is always header.
// For streaming payload, the resolved checksum location depends on whether it is an unsigned payload, we let AwsAuthSigner decide it.
if (checksumAlgorithmName == "crc32")
{
.....
}
......
else if (checksumAlgorithmName == "md5")
{
httpRequest->SetHeaderValue(Http::CONTENT_MD5_HEADER, HashingUtils::Base64Encode(HashingUtils::CalculateMD5(*(GetBodyStream(request)))));
}
else
{
AWS_LOGSTREAM_WARN(AWS_CLIENT_LOG_TAG, "Checksum algorithm: " << checksumAlgorithmName << "is not supported by SDK.");
}
}
可以看到,在checksum算法为md5的情况下,AWSClient::AddChecksumToRequest()
方法并没有调用ShouldComputeContentMd5()
来判断是否使用md5(UploadPartRequest
没有重写父类的AmazonWebServiceRequest::ShouldComputeContentMd5()
方法,因此UploadPartRequest::ShouldComputeContentMd5()
会返回False),而是调用UploadPartRequest::GetChecksumAlgorithmName()
获取对应的checksum名字,如果是md5,则会通过调用HashingUtils::CalculateMD5()
来计算md5 checksum。由于计算的过程中需要读取文件,对应的buffer stream就在参数request中,上文说过,这个buffer stream就是我们通过回调 create_read_buffer()
为每一个S3 Part创建的一个AsynchronousReadBufferFromFileWithDescriptorsCache
对象。
总结
本文详细讲述了我们在使用ClickHouse的过程中所经历的如下过程:
- 发现问题
- 确认为Bug
- 添加监控缩小调查范围,进行同类型其他使用场景的对比测试,缩小调查范围
- 添加日志,添加堆栈,最终找到问题根本原因
- 找到问题的解决方案
我想,这个从发现问题到最后解决问题的基本过程,是我们发现并解决一个复杂的分布式系统的各种问题的通用流程。我始终认为,一个分布式系统会有成百上千的各种bug,但是:
- 在我们具体的使用过程中,显著影响我们使用的Bug往往只有几个;
- 解决每一个Bug都需要专门的精力,尤其是对于ClickHouse,必须耐心构建整个CI/CD的流程,这样,当我们发现问题,就可以在源码层面随时增加日志,随时打印堆栈。有了源码级别的、随意可控的日志和堆栈,最终发现问题的原因只是时间问题。
- 监控很重要。在本文中,对某些指标的监控让我们迅速缩小了调查范围。但是其实,能够最大程度利用监控的前提是理解监控,理解监控的唯一方法,是读代码。。。
所以,重新回到我们在本文开头提到的这些问题,我们来回答这些问题:
- ClickHouse的带宽限制是否能够做到多准确?比如,当磁盘读写速率和网络出口带宽都不是瓶颈,那么这个带宽控制和实际的运行带宽可以吻合得多好?
- 从我们实际的测试结果来看,如果还没有触及到磁盘读写速率和网络出口带宽的上限,ClickHouse可以非常准确的对备份带宽进行控制。
- ClickHouse的带宽限制是否支持在Server端进行控制?
- ClickHouse Server端的带宽控制只是设置了客户端的默认带宽控制值,ClickHouse server无法限制全局上传带宽。
- ClickHouse是一个分布式系统,Server端的带宽控制的粒度多大?可以控制一个Shard中多个Replica的总带宽,还是说,只能对单台Server进行控制?
- ClickHouse无法控制多个Replica的总带宽,只能对单台Server的带宽设置默认值,各个Query可以自己设置带宽上限;
- 如果ClickHouse的带宽限制支持在Server端进行控制,那么是否可以支持热生效?
- ClickHouse的Server端的带宽控制不支持热生效。我们测试的结果是这样,同时,我们也在代码层印证了这个结果。在ClickHouse端,配置文件的reload是通过ConfigReloader来负责的,通过在构造ConfigReloader的时候传入一个Updater实现,ConfigReloader会以一个独立线程的方式反复调度Updater。在ClickHouse启动的主入口方法
int Server::main(const std::vector<std::string> & /*args*/)
中,构造了auto main_config_reloader = std::make_unique<ConfigReloader>
,这里的Updater中并不会动态reloadmax_backup_bandwidth_on_server
- ClickHouse的Server端的带宽控制不支持热生效。我们测试的结果是这样,同时,我们也在代码层印证了这个结果。在ClickHouse端,配置文件的reload是通过ConfigReloader来负责的,通过在构造ConfigReloader的时候传入一个Updater实现,ConfigReloader会以一个独立线程的方式反复调度Updater。在ClickHouse启动的主入口方法
- ClickHouse的带宽限制是否支持在Session端进行控制?显然,如果支持在Session端进行控制,那么意味着我们可以在不修改ClickHouse的Server端带宽限制的情况下,随时、方便地控制带宽
- 支持,我们在一个Session中,可以随时通过
SET max_backup_bandwidth
来调整带宽,设置完成以后,随后的Backup/Restore Query就会使用这个值来限制自己的带宽。
- 支持,我们在一个Session中,可以随时通过