本文字数:7772;估计阅读时间:20 分钟
作者:博睿数据 李骅宸(太道)& 小叮当
本文在公众号【ClickHouseInc】首发
本系列前两篇内容:
从ES到ClickHouse,Bonree ONE平台更轻更快!
100%降本增效!Bonree ONE平台通过ClickHouse实现了可观测信号数据的统一!
背景
Bonree ONE是博睿数据公司发布的一体化智能可观测平台。平台所有信号数据迁移到ClickHouse集群后,随着数据量的增长,对ZooKeeper的性能和稳定性要求也越来越高。ZooKeeper是一个开源的分布式协调组件,用于分布式系统之间的协调作用。但我们在使用中也遇到了一些痛点,如果不及时处理会影响到业务。最终我们选择用ClickHouse-Keeper替代ZooKeeper,解决写入性能和维护成本以及集群管理出现的问题。想做到丝滑切换,还需要考虑自动化升级、鉴权情况、数据迁移、数据验证等细节。
ZooKeeper vs ClickHouse-Keeper
ZooKeeper
在数据量和数据种类不断扩充的情况下,ClickHouse集群对于ZooKeeper的压力越来越大。例如在基于实时性要求高的告警数据入库查询时,数据查询的实时性要求秒级返回,ZooKeeper在我们实践中遇到过以下问题:
-
承载力有限。在我们自建云部署中,按Keeper 4C8G资源配置,单ZooKeeper集群(3个节点)的情况下,最多支持到5个shard(16C32G配置,双副本),每个shard的parts数量大概在2W左右,那5个shard总parts共计10W左右。在不断的数据量增加和表增加的情况下,集群规模会迅速膨胀。所以每扩容5个shard,就得再扩一套ZooKeeper集群,这也带来了极大的维护成本。
-
性能有瓶颈。初始ClickHouse存储1T数据,总共parts数量在1.2W以内时,数据可以在毫秒级插入成功,但是随着数据量不断增加,parts数量达到2W时,数据插入的响应时间会明显越来越慢,需要几十秒以上才可以插入成功。导致产品上会有查询不到数据,或者查询的数据不及时的现象,影响体验。
-
资源占用多。ZooKeeper占用内存和IO多,随着数据量的上涨,成本会越来越高。我们在同样的可观测性数据环境中对比发现,ZooKeeper内存消耗是ClickHouse-Keeper的4.5倍,IO占用是ClickHouse-Keeper的8倍。
-
稳定性不高。ZooKeeper使用Java语言开发,在内存设置不合理的情况下,会出现频繁Full GC从而导致服务中断,从而引起ClickHouse的性能抖动,有时还会出现zxid溢出的问题。另外,当ClickHouse后台执行副本间parts同步时,需要先在ZooKeeper上注册,这时如果ZooKeeper性能表现不佳,会引起后台线程剧增满载,从而导致副本间数据同步延迟。我们在自建云观察在使用ZooKeeper进行存储元数据时,副本间的parts数量延迟队列能达到1W多个,这将严重影响前台业务查询的数据的准确性。
ClickHouse-Keeper
基于以上痛点对比,加上写多读少的统计分析场景特点,我们选择了表现更佳的ClickHouse-Keeper作为Zookeeper的替代品,主要考虑以下几点:
判断兼容性。ClickHouse-Keeper兼容ZooKeeper客户端的协议,任何标准的ZooKeeper客户端都可以与ClickHouse-Keeper进行交互,支持使用ClickHouse的客户端命令。这样就对服务端来说,不改造就可以连接到ClickHouse-Keeper。
历史数据迁移。使用ClickHouse-keeper-converter工具对ZooKeeper历史数据进行转换,并将转换后的快照文件导入到ClickHouse-Keeper集群中。转换效率做到百万路径控制在五分钟以内,降低整体集群停止写入的时间,降低对线上产品的影响程度。
服务稳定。ClickHouse-Keeper使用C++语言编写,提升了整体服务的稳定性,从根本上解决了ZooKeeper的Full GC带来的服务中断,并可以通过调整服务端参数提升服务性能。
资源和性能。对元数据进行压缩存储降低服务的资源占用。同时在CPU和内存以及磁盘IO上使用效率更高。在更好资源情况下,ClickHouse-Keeper性能更佳。
方案演进
1.基于ZooKeeper的原有方案
在集群业务不断扩充的情况下,我们的业务也逐渐划分成重要业务和一般业务,重要业务的性能和查询需求需要优先保证,资源就得绝对隔离高保。我们知道在单个ClickHouse服务中,如果做大量的资源控制,对于大集群来说是一件灾难性的事件,因为我们每次都要重新划分新业务的比重同时调整资源比例。如果我们从物理上直接隔离不同业务的资源将是非常友好的一件事,这样对于运维只需要关心这个业务给多少资源支撑。早期我们因为业务多样复杂性和SLA的不同,通过Zookeeper实现了多个ClickHouse Cluster方案。但在ClickHouse不断扩容的情况下,整个ClickHouse对于ZooKeeper的压力也水涨船高,压力倍增。这时候使用一套ZooKeeper存储所有的元数据,对于ZooKeeper来说已经不能够支持ClickHouse提供正常的服务,插入延迟和任务堆积都将出现。为了解决这个困境,使用ClickHouse官方提供的多ZooKeeper方案。针对我们的业务数据特性和压力,每5个shard的ClickHouse存储parts存储到一个ZooKeeper集群(3个实例)。表的元信息存储到另一套单独的ZooKeeper集群中(3个实例)。
如上图举例,业务A和业务B需要做物理隔离,为A提供CK1集群(15个shard,双副本),为B提供CK2集群(5个shard,双副本),业务查询根据不同cluster名字映射到不同物理资源上的ClickHouse实例,从而做到资源隔离,业务不会影响。按5个shard为一组,ZK1、ZK2、ZK3三个集群平分CK1的15个shard的parts管理,ZK4管理CK2的5个shard的parts。CK1和CK2的所有表的元信息单独存储到公用ZK5集群中(出于稳定性要求考虑)。这样一共需要5套ZK集群(3x5 = 15个ZK实例),维护起来非常麻烦,并且存储时间久了性能和稳定性不太好。
2.基于ClickHouse-Keeper的新方案
测试中已验证使用ClickHouse-Keeper,提升了数据插入速度,提升了ClickHouse的稳定性,副本间同步文件的速度。因为ClickHouse-Keeper表现优异,我们完全不再需要用那么多套keeper管理,索性就维护一套ClickHouse-Keeper,完全可以支持到15个shard以上,相比原来方案精简了许多。
3.升级前的准备
我们知道ClickHouse会不断的更新在ZooKeeper上的数据,当我们停止入库后,其实还有一部分的后台任务在进行(例如后台merge任务),这些任务会改变在ZooKeeper上的数据内容,这样我们就没有办法判断升级之后和升级之前的内容是否是一致的,是否成功将ZooKeeper中的数据转换成功,同时我们又不能将ClickHouse停止,因为停止了ClickHouse的话,平台就无法使用。为此我们深入了解了ClickHouse在ZooKeeper上这些后台任务,并通过命令(例如:SYSTEM STOP MERGES)将这些任务停止掉。这样在升级前后ZooKeeper和ClickHouse-Keeper中的数据是完全一致的,也为我们后面进行数据对比验证提供了条件。因为全过程实现了自动化执行,保障了停止入库时间控制在30分钟内变更完成,不严重影响业务。
4.自动化升级(ZooKeeper -> ClickHouse-Keeper)
升级过程涉及跨机器执行多命令,以及对执行结果判断,大量的人为操作会导致出错概率成倍增加,并使升级时间大大增加。为了解决此问题,我们基于ansible开发自动化升级工具(ansible 是一款 IT 自动化工具,主要应用场景有配置系统、软件部署、持续发布及不停服平滑滚动更新的高级任务编排)。那么我们如何做的自动化升级呢?大致步骤如下:
(1)停止数据入库任务。
(2)停止ClickHouse的merge等后台任务。
(3)记录需要对比的指标数据。
(4)依次重启各个ZooKeeper集群获取最新产生的快照文件。
(5)按照ZooKeeper和ClickHouse-Keeper的对应规则进行快照文件Copy和转换成ClickHouse-Keeper的快照文件。
(6)将快照文件加载到ClickHouse-Keeper中,并进行抽样对比ZooKeeper和ClickHouse-Keeper节点内容。
(7)切换ClickHouse的元数据存储由ZooKeeper到ClickHouse-Keeper。
(8)查询对比指标,并与步骤(3)中指标对比。
(9)启动ClickHouse的merge等后台任务,启动数据入库。
在基于ansible做自动化升级之后,避免升级操作失误,真正做到了傻瓜式升级。同时提升了升级速度,由原先2-3小时的手动升级时间,缩短到现在的分钟级完成,降低了业务查询在ClickHouse的升级过程中影响范围。自动化核心过程:
//停止ClickHouse建库建表等操作
stopClickHouseManagers()
//停止ClickHouse 数据写入作
stopClickHouseConsumers()
//停止ClickHouse Merge等后台任务
ClickHouseStopMerges()
//获取Zookeeper集群最新的快照信息
getNewZookeeperSnap()
//转换Zookeeper快照生成ClickHouse keeper快照文件
createAndExecConvertShell(housekeeperClusterName)
//启动ClickHouse集群
startClickHouses()
//对比升级前后数据
checkClickHouseSelectData()
遇到的挑战
那么对于上面讲述的升级过程,就算完成了吗?其实那只是基础步骤。我们环境还更加复杂,除了历史部署了多cluster、多套ZK,还遇到了多套ZK转单套ClickHouse-Keeper(社区版只支持一转一)、ZK加密鉴权问题、数据验证效率等一些难题。
1.多套ZooKeeper转单套ClickHouse-Keeper
为什么会有这个需求呢?主要是因为我们在ZooKeeper和ClickHouse-Keeper性能测试中发现,ClickHouse-Keeper的性能在模拟ClickHouse 请求时性能远超ZooKeeper,对于已经是多套ZooKeeper存储parts的情况,为避免资源浪费,我们需要将集群进行缩减。在CK官方提供的工具中,只能进行一套ZooKeeper通过快照转换到一套ClickHouse-Keeper中。无法支持多套ZooKeeper 迁移到同一套ClickHouse-Keeper。如果不牵扯到服务缩减的话,还遇不到这个问题。那么我们是如何解决的呢,主要通过两方面:
-
在迁移之前我们会抽样一部分节点对这些节点信息进行保存,在迁移之后对比迁移之前抽样的节点信息是否一致,同时也会抽样表格数据进行查询,确保整个升级过程的前后,数据一致,以防异常情况带来的影响。
-
在原有的ClickHouse-keeper-converter源码中修改相关代码,支持多套ZooKeeper快照文件合并到同一套ClickHouse-Keeper。比如:
//循环反序列化所有的快照文件
for (const auto & item : existing_snapshots)
{
deserializeKeeperStorageFromSnapshot(storage, item.second, log);
}
//修改numChildren属性获取方式,不用自增id
storage.container.updateValue(parent_path, [path = itr.key] (KeeperStorage::Node & value) { value.addChild(getBaseName(path)); value.stat.numChildren = static_cast<int32_t>(value.getChildren().size());});
//创建转换快照文件时的文件输出流,为了后续diff工具快速读取用
int flags = (O_APPEND | O_CREAT | O_WRONLY);
std::unique_ptr<WriteBufferFromFile> out = std::make_unique<WriteBufferFromFile>("pathLog", DBMS_DEFAULT_BUFFER_SIZE, flags);
2.加密鉴权
我们有很多私有化TOB客户,历史上有些要求对ZK加密,有些不加密,情况不一,但都需要考虑能兼容各种,能丝滑升级。大家知道ZK可以进行ACL加密,对于已经加密的ZK集群转换如何处理,对于部分ZK集群加密转换如何处理,对于完全没有加密的ZK集群又如何处理,分不同策略:
-
全部加密,或全部不加密。全部加密并且加密信息一致的情况下,即每个ZooKeeper集群中的节点都是使用的相同的ACL策略,同时策略内容相同,并且ClickHouse-Keeper的ACL兼容ZK的ACL,则这种情况下我们是可以保留原有的加密信息,直接进行转换,同时ClickHouse中的配置文件也无需修改。如果全部没有加密,即ClickHouse在使用ZooKeeper的时候没有配置ACL信息,则这种情况也可以直接转换,同时ClickHouse中的配置文件也无需修改。
-
部分加密,或加密信息不一致。这种情况下就要将原有的加密信息去掉,同时修改ClickHouse 配置文件将加密方式修改为一致。去除ZooKeeper加密方式如下:
(1)添加超级管理员账户。在Zookeeper集群启动的时候在启动的JAVA命令中添加:
Dzookeeper.DigestAuthenticationProvider.superDigest=zookeeper:{XXXXXX}
(2)重启Zookeeper集群。
(3)使用Zookeeper客户端进入集群。
zkCli.sh -server ${zookeeper集群地址,格式为ip1:port,ip2:port}
(4)登录超级管理员账户。
addauth digest zookeeper:#{XXXXXX}
(5)执行取消节点的鉴权命令(目标路径下节点越多耗时越长)。
setAcl -R ${zookeeper的znode路径} world:anyone:cdrwa
(6)去除步骤(1)中的超级管理员账户,同时重启Zookeeper集群。
(7)待升级之后,看自己是否需要在ClickHouse-Keeper中再开启加密。
3.对比验证
我们知道在ZooKeeper中存储的路径是无法直接获取所有的路径,同时我们还遇到了多个ZooKeeper集群合并到同一个ClickHouse-Keeper集群中的情况。如果在自动化升级过程中不能快速准确的进行数据对比,升级时长会成倍增加,同时容易出现误判断,导致升级失败。对于如何快速获取路径和路径对比我们做了如下策略:
(1)我们在ZooKeeper快照转换成ClickHouse-Keeper快照的时候会将转换的所有的path都打印到一个pathlog目标文件中,在对比的时候对这个文件中的内容进行抽样,这样就将查询ZooKeeper的操作变成了读文件操作。原先读取900W个ZooKeeper路径大概需要9个小时,优化后只需要十几秒就可以完成(pathlog加载到内存中读取)。
(2)我们在获取到抽样的ClickHouse-Keeper之后,会根据转换关系,将znode路径和多个ZooKeeper集群的znode路径依次对比。同时在对比之前我们已经关闭了merge等任务,进一步保证临时目录不会出现,从而保证对比的有效性。分情况对比:
- 差异性路径。此类路径只存在于/clickhouse/tables的路径下,并且只会存在于唯一一套ZK集群中,则对比验证成功。反之,无论在多套ZK集群都不存在,或者存在于两套或两套以上,则对比验证失败。这样就保证了差异性路径数据的正确性。
- 公共路径。路径在所有ZK集群中的内容都是相同的,才算对比验证通过。这样就保证了公共路径数据的正确性。
4.ClickHouse-Keeper关键参数调优
-
max_requests_batch_size。这个表示发送到raft之前,请求攒批的最大size。集群越大,这个值适量调整大一些,利于攒批和性能提升,我们场景里把value设置为1W。
-
force_sync。表示请求是否同步写入日志,优化设置为false。
-
compress_logs。表示是否压缩日志,日志文件大小会影响启动速度和磁盘占用,优化设置为true。
-
compress_snapshots_with_zstd_format。表示是否压缩快照,日志文件大小会影响启动速度和磁盘占用,优化设置为true。
效果
我们将ZooKeeper替换为ClickHouse-Keeper之后,资源收益和写入耗时收益显著,解决了在ZooKeeper存储元数据的性能瓶颈问题,同时在易用性上降低了门槛。之前ZK的IO瓶颈会直接影响到整体ClickHouse存储的写入耗时。在我们自建云千亿数据实践验证中,从ZK已迁移到HouseKeeper后,CPU和内存占用节省了75%以上,故障率几乎没有。但更值得一提的是,即使再高配置的ClickHouse集群也不要忽视了IO的重要性。最终切换到ClickHouse-Keeper后,IO开销降低了8倍,同时性能也提高了近8倍,这个数据比较符合预期。原本没想到ClickHouse本身性能强悍,ClickHouse-Keeper的性能也毫不逊色!
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com
联系我们
手机号:13910395701
邮箱:Tracy.Wang@clickhouse.com
满足您所有的在线分析列式数据库管理需求