得物上一代日志平台的存储主要依赖于 ES。随着公司业务的高速发展,日志场景逐步产生了一些新需求,主要表现在:应用数量逐步增多,研发需要打印更多的日志定位业务问题,安全合规需要保留更长时间的日志。随着 Clickhouse 的应用广泛,我们了解到行业部分知名公司已经将日志平台逐步由 ES 迁移至Clickhouse,以此来获取更好的写入性能与高压缩比。因此我们与日志平台研发团队开始进行日志平台新存储的选型评估,本文会介绍我们如何通过 Clickhouse 的冷热分离存储替代 ES 的实施方案。
行式
列式
<path>/data1/ClickHouse/data/</path> --为了便于查找,我们建议在默认的存储路径下方添加存储策略
<storage_configuration>
<disks>
<hot>
<path>/data1/ClickHouse/hot/</path> --这里注意,使用存储策略后,建议任何数据路径之间不要有子集关系
</hot>
<cold>
<path>/data2/ClickHouse/cold/</path>
</cold>
</disks>
<policies>
<ttl>
<volumes>
<hot>
<disk>hot</disk>
</hot>
<cold>
<disk>cold</disk>
</cold>
</volumes>
</ttl>
</policies>
</storage_configuration>
<path>为ClickHouse默认的存储路径,找到该标签后在下方添加存储策略标签<storage_configuration>。
<storage_configuration>:固定标签,定义存储策略。
<disks>:固定标签,下面会定义磁盘名称,以及磁盘绝对路径。
<hot>、<cold>:自定义标签,用来标记该路径,可按照此名称定义便于区分。
<policies>:固定标签,定义具体存储策略名称。
<ttl>:自定义标签,定义具体存储策略的名称,用于表级TTL,可按照此名称定义便于区分。
<volumes>:固定标签,定义卷组。
<hot>、<cold>:卷组名称,每个卷组下可以包括一个或多个disk标签,disk标签取值为<disks>标签下定义的磁盘名称。
CREATE TABLE db_rdsauditlog_local ON CLUSTER auditlog
(
`check_rows` Int64,
`client_ip` String,
`db` String,
`fail` Int64,
`instance_id` String,
`latency` Int64,
`origin_time` DateTime('Asia/Shanghai'),
`return_rows` Int64,
`sql` String,
`thread_id` Int64,
`update_rows` Int64,
`user` String,
`tables` Array(String),
`sqlhash` String,
`sqlfingerprint` String,
`sqltype` String,
INDEX instance_sqltype (instance_id, sqltype) TYPE set(100) GRANULARITY 5,
INDEX origintime_sqlhash (instance_id,sqlhash) TYPE set(100) GRANULARITY 5,
INDEX origintime_instance_clientip(instance_id, client_ip) TYPE set(100) GRANULARITY 5
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(origin_time)
ORDER BY origin_time
TTL origin_time + INTERVAL 6 hour TO DISK 'cold'
SETTINGS storage_policy = 'ttl',index_granularity = 8192;
Create table db_rdsauditlog on cluster auditlog as db_rdsauditlog_local ENGINE=Distributed(auditlog, default, db_rdsauditlog_local, rand());
创建表级 TTL ,要想使用分级冷热存储,必须要指定存储策略,才能使用到 Disk ,为什么要指定存储策略,我的理解是TTL可以指定到 Disk 和 Volumn 级,但是 Volumn 级别只能定义在存储策略里。
PARTITION BY--
分区键,表级可选的参数。在大多数情况下,其实不需要分区键,同时即便使用了分区键,不太建议使用比月更细粒度的分区键,分区不会加速查询(与 ORDER BY 表达式相反)。你永远不应该使用太细化的分区。不要按客户端标识符或名称对数据进行分区,而是将客户端标识符或名称作为 ORDER BY 表达式中的第一列(官方文档这句话还是要好好牢记,一般我们只建议时间字段做分区)。以上是官方文档的原话,但是实际中我们需要根据具体情况是可以创建甚至到小时级别粒度的分区,还是那句话,看需求,用不好会有副作用,后面会提到。
业务背景中除了提到需要很高的写入能力,很高的压缩比,实际需求调研中,基础架构同学还提出各个业务域都有自己的日志时间查询范围需求(ES作为存储时,研发甚至需要能够提供天级的日志保留时间),比如7天,30天,90天,甚至更长等不同范围,因为冷数据查询频次较低,可以使用更低廉的存储介质,另外有一些数据保留一段时间之后不会被查但需要满足合规要求,再长的日志就可以删除。基础架构希望DBA能够帮忙在ClickHouse的存储上对数据保留时间提供一些建议,尽最大可能降低存储成本。
总结一下就是如下需求:
如何能尽可能满足各个业务域的天级保留策略?
如何将数据能够根据日期存放在不同的存储介质中?
多级存储策略选用什么样的存储介质能够尽最大可能降低存储成本?
天级保留策略,我们使用了表分区策略,并规避了一些坑。
多级存储主要使用了三级存储策略,即Hot+Cold+Arch(Archive后续均已 Arch 代替)。
Hot盘选用ESSD PL1盘挂载宿主机,磁盘名称为hot;Cold选用ESSD PL0盘挂载宿主机,磁盘名称为cold;Arch我们最终选择直接挂载OSS文件系统(因为中间我们还调研过JuiceFS,但最终还是放弃了),磁盘名称为arch。
max_partitions_per_insert_block,默认值是100。
max_parts_in_total,默认值10w。
测试环境中由于有一些应用数据较少,导致攒批数据比较难,加上若代码在批次写入数据的地方处理不好,很容易出现频繁写入较少数据行的part,加上应用数,环境较多,导致写入的数据较碎,一个表内active的part数据非常容易超过10w(可以查看system.parts表中状态为active的数据个数),多次报错too many parts in total的错误。这里可以适当提高background_pool_size的值提升合并速度,但是对于大量较碎的part也是杯水车薪,不能解决根本问题。最终我们放弃方案1,选择方案2。
如何解决?经过我们讨论,我们想到一个方法,表中添加两个字段log_save_time,oss_save_time,这两个字段是int类型,将分区字段调整为PARTITION BY (toDate(log_time), log_save_time, oss_save_time)。log_save_time为每条日志在hot盘中保留的时间,超出会被任务移动到cold盘;oss_save_time为每条日志在cold盘中保留的时间,超出会被移动到arch盘(这里的移动任务会在下面介绍到)。任务每天会查询system.parts表,查看分区字段中三个字段对比,即toDate(log_time)和当前日期比较,差值大于分区中log_save_time的值,则移动该分区到cold盘,差值大于oss_save_time则移动到arch盘。这样如果应用日志保留策略的元数据信息修改,新产生的日志数据这两个字段也会写入新的保留策略值,新数据会落到不同的分区中。那么如何实现?进入问题2。
PARTITION BY (toDate(log_time), log_save_time, oss_save_time)
ORDER BY (application, environment, log_time, ip, file_offset)
TTL origin_time + INTERVAL 24 hour TO DISK 'cold' --类似这样,
SETTINGS allow_nullable_key = 1, storage_policy = 'ttl', index_granularity = 8192
另外还有一个坑就是,表级TTL一旦要修改TTL的保留时间,ClickHouse会reload表的所有part目录,导致IO Util打满,集群无法响应,这个很坑,目前还没有好的办法解决,所以放弃该方案。
CREATE TABLE dw_log.tb_logs_local
(
`application` String,
`environment` String,
`ip` String,
`filename` String,
`keys` Array(Nullable(String)),
`values_string` Array(Nullable(String)),
`values_number` Array(Nullable(Float64)),
`file_offset` Nullable(UInt32),
`message` String CODEC(ZSTD(1)),
`log_type` String,
`log_time` DateTime64(3),
`log_level` String,
`trace_id` String,
`pid` Int64,
`endpoint` String,
`log_save_time` Int32,
`oss_save_time` Int32,
`meta_size` Int64,
INDEX meta_size meta_size TYPE SET(100) GRANULARITY 2,
INDEX application application TYPE SET(100) GRANULARITY 2,
INDEX environment environment TYPE SET(100) GRANULARITY 2,
INDEX ip ip TYPE SET(100) GRANULARITY 2,
INDEX idx_message message TYPE tokenbf_v1(512, 2, 0) GRANULARITY 2,
INDEX trace_id trace_id TYPE SET(100) GRANULARITY 2,
INDEX log_level log_level TYPE SET(10) GRANULARITY 2,
INDEX pid pid TYPE SET(100) GRANULARITY 2,
INDEX idx_endpoint endpoint TYPE tokenbf_v1(512, 2, 0) GRANULARITY 2,
INDEX logtime log_time TYPE minmax GRANULARITY 2
)
ENGINE = MergeTree
PARTITION BY (toDate(log_time), log_save_time, oss_save_time)
ORDER BY (application, environment, log_time, ip, file_offset)
SETTINGS allow_nullable_key = 1, storage_policy = 'ttl', index_granularity = 8192
alter table dw_log.tb_logs_local on cluster default MOVE PARTITION XXX to disk 'cold'
解决了过期策略,表结构的设计后,前面提到的arch磁盘来存储基本不查的数据,使用低存储成本介质来降低成本,我们首先想到的就是能不能使用OSS?答案是可以,同时我们查看过费用,同等容量的 OSS 成本仅是 ESSD PL0 的三分之一,这无疑可以大幅降低存储费用。但怎么用最优,需要调研+测试。
可见 JuiceFS 支持将对象存储挂载到 ECS 上,通过文件系统做本地盘访问,同时支持读缓存来加速文件读取,我们也做了测试,确实读写性能还不错。
QPS(ESSD PL0) | QPS(JuiceFS) | |
Q1.1 | 30.919 | 28.642 |
Q1.2 | 210.061 | 191.958 |
Q1.3 | 395.616 | 350.915 |
Q2.1 | 5.344 | 5.053 |
Q2.2 | 5.786 | 6.029 |
Q2.3 | 6.173 | 6.446 |
Q3.1 | 3.297 | 2.618 |
Q3.2 | 4.484 | 4.657 |
Q3.3 | 5.427 | 5.748 |
Q3.4 | 243.571 | 216.825 |
Q4.1 | 2.978 | 3.011 |
Q4.2 | 9.057 | 8.845 |
Q4.3 | 14.022 | 14.155 |
基于以上问题,我们为了避免引入其他技术栈带来额外的运维成本,当然这里并不是说 JuiceFS 有什么弊端,而是在我们这个场景下需要的是尽可能少引入其他组件。恰好我们有现成的对象存储服务。于是继续测试了原生挂载 OSS 的方案(也就是下面要讲到的方案)。这次调研也让我们深入了解了JuiceFS的架构和优势,为后续在其他场景的应用奠定了基础。
ClickHouse 原生的 MergeTree 本身就支持直接挂载 S3 作为数据盘使用,我们基于这个特性做了测试,结果符合预期。虽然官方的 demo 是 S3,因为 OSS 也是支持 S3 协议,所以也是同样可以使用的。需要注意的是 endpoint 这里必须 http 开头,否则无法正常挂载。
<storage_configuration>
<disks>
<hot>
<path>/data1/ClickHouse/hot/data/</path>
<move_factor>0.1</move_factor>
</hot>
<cold>
<path>/data2/ClickHouse/cold/data/</path>
</cold>
<arch>
<type>s3</type>
<endpoint>http://log-sh.oss-cn-xx-internal.xxx.com/xxxxxxxxxx/</endpoint>
<access_key_id>xxxxxxxx</access_key_id>
<secret_access_key>xxxxxx</secret_access_key>
<metadata_path>/data1/ClickHouse/disks/s3/</metadata_path>
<cache_enabled>true</cache_enabled>
<data_cache_enabled>true</data_cache_enabled>
<cache_path>/data1/ClickHouse/disks/s3/cache/</cache_path>
</arch>
</disks>
<policies>
<ttl>
<volumes>
<hot>
<disk>hot</disk>
</hot>
<cold>
<disk>cold</disk>
</cold>
<arch>
<disk>arch</disk>
</arch>
</volumes>
</ttl>
</policies>
</storage_configuration>
测试情况:
a. 写测试,move数据从hot盘到OSS。
https://ClickHouse.com/docs/en/engines/table-engines/mergetree-family/custom-partitioning-key/
https://www.juicefs.com/docs/zh/community/cache_management
https://www.juicefs.com/docs/zh/community/architecture
*文/Chen
要是觉得文章对你有帮助的话,欢迎评论转发点赞~