云数据库ClickHouse:从云托管到云原生背后的核心技术解析

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 阿里云数据库ClickHouse自从上线以来,已经走过了两年多的时间,期间我们积淀了大量的云上客户案例,在长期的客户服务支持中也对ClickHouse数据库生态有了非常深刻的认知。在充分了解现有产品的生态和客户实践使用痛点后, 我们对开源ClickHouse数据库做了全新的架构升级,接下来将为用户带来全新的云原生版本ClickHouse数据库。

作者:阿里云数据库OLAP产品部 仁劼


前言


随着云服务平台的IaaS能力不断发展升级,用户数据规模的不断膨胀,主流的数据库产品都在往云化、服务化发展。云原生这个词已经和数据库紧密结合在了一起,云服务厂商们都在致力于打造自己的云原生数据库产品矩阵。


阿里云数据库ClickHouse自从上线以来,已经走过了两年多的时间,期间我们积淀了大量的云上客户案例,在长期的客户服务支持中也对ClickHouse数据库生态有了非常深刻的认知。在充分了解现有产品的生态和客户实践使用痛点后, 我们对开源ClickHouse数据库做了全新的架构升级,接下来将为用户带来全新的云原生版本ClickHouse数据库。

云原生ClickHouse架构


虽然云原生数据库这个词已经耳熟能详,但是很多用户对数据库产品是否"云原生"还没有构建起清晰的衡量标准。以往的云托管模式数据库,对用户的体感就是:数据库实例从本地的物理机器搬到了云上的ECS,本地磁盘变成了可靠的云盘存储,云数据库产品同时会提供稳定易用的运维、监控手段。


云托管模式把用户从手动部署运维的时代中解放了出来。而云原生数据库面向用户最核心的一个体感就是要提供更好的资源弹性能力、数据共享能力。从数据库引擎内核层面来看,云原生数据库需要更好地去拥抱利用云平台上的基础设施能力,一些传统的数据库架构模式是基于本地不可靠存储设备、有限计算资源来设计的,在云平台这个大前提下将不再是最优选择。我们将推出的云原生ClickHouse数据库对用户来说主要有以下三方面的形态变化(它们也是云原生ClickHouse数据库内核能力持续演进的方向):


  • 首先是存储资源和计算资源的解绑,存储资源可以做到按使用量来计费,不再需要因为磁盘存储空间不足而被迫去进行整体扩容;
  • 其次是计算资源的极致弹性,可以做到分钟级的计算资源拉起销毁以及扩缩容,用户在预判计算组负载的情况下可以进行分时调整规格,做到最小资源闲置;
  • 最后是单个数据库实例下的多计算组资源模式,计算组之间CPU/Memory资源相互隔离但是共享整个集群的存储数据,用户可基于此做不同类型的业务负载隔离以及读写负载隔离;


相同用户形态的产品在业界也有很多case,说明大家对云原生的认知是相同的,都在往极致弹性、计算服务化、存储按量的方向演进。只是计算资源弹性能力的差异,让不同产品在用户使用上会有不同感受,分钟级的计算资源拉起对绝大部分用户来说都需要进行预先拉起并且资源常驻,这个计算资源"拉起"如果足够快,就可以省去预先拉起的动作,对用户而言计算资源就完全是服务化了,随用随有,用完释放。


云原生ClickHouse数据库的架构图如下:

1.jpg


其中每个计算组对用户暴露的都是一个独立的链接串,用户通过不同的链接串就可以登录对应的计算组。计算组这个概念从用户视角就相当于原来云托管模式的一个"集群",计算组之间的机器资源完全隔离,计算组收到的查询请求只会在自己组内的节点上执行。持久化存储层从计算组资源中剥离开,它不附属于某个单一的计算组,多计算组需要共用一套持久化存储层。云原生ClickHouse以分布式对象存储作为持久化存储层,加上分布式KV系统提供集群所有元数据的存储服务,来达到计算组间数据相互可见互通。


单个计算组由多个节点组成,每个节点都会拉起clickhouse-server作为主进程来提供查询服务。同时每个节点都会挂载ESSD云盘作为本地缓存盘,这个缓存盘当前有两个作用:热数据的读缓存、实时数据的写入暂存(实时写入并不会写穿到持久化存储)。所以这里的缓存盘相当于是有状态的,需要在节点迁移销毁的过程中进行处理。最后如图所见,我们对数据做了shard处理,但是节点的数量和shard并不是完全相等只是保证了shard能够均匀分配到节点。下一章将对云原生ClickHouse的架构选择做更细致的分析解读。

Key Choice

关于shard

传统的Share Nothing架构MPP数据库中,数据一般都是根据个别主键列进行sharding(分析类场景一般使用hash分区),不光对数据进行了sharding,也把计算节点绑定到了对应的数据shard上,单个计算节点只会负责某个数据shard的读写处理,当前开源ClickHouse的分布式部署模式就是如此。这种计算节点和数据shard的一一映射强绑定关系,给计算节点的水平扩缩容带来了极大的限制,用户的ClickHouse集群一旦需要加减节点,就需要对全量数据进行re-sharding。Re-sharding是一个完全逻辑数据搬迁的过程,相当于把老集群的数据select出来,再insert回新集群中,clickhouse-copier就是社区为此提供的一个数据逻辑搬迁工具。纵观所有的分布式数据库产品,数据的sharding或多或少都会对集群的扩展能力、弹性能力产生一定制约。数据sharding之所以对分布式数据库产品如此重要,是因为如果没有数据sharding,基于主键的记录写入变更就缺少了最重要的并行能力。


目前只有在完全放弃主键语义的Hive大数据生态或者一些云数仓产品中,数据才可以不用进行强制sharding。ClickHouse生态当前虽然没有实时主键能力,但是作者认为云原生版ClickHouse仍然不能抛弃掉数据本身的sharding,因为ClickHouse的Merge存储还是存在部分主键语义的(其实就是它的Order By Key),主要表现在ReplacingMergeTree/AggregatingMergeTree等变种引擎中。ClickHouse提供了主键最终一致性(optimize partition)的能力,也有主键读时一致性(final scan)的能力。


如果数据本身没有进行sharding,那这两个能力的性能或者代价将出现数量级倒退,打破一些场景下的用户使用模式。另外一个重要原因是ClickHouse当前还不具备一个非常完备的MPP计算引擎,它强依赖于数据sharding来达到分布式并行的能力。而在计算引擎层面,我们希望云原生版ClickHouse是能够不断跟随社区的,始终保持和开源版本的SQL能力一致,提供和开源ClickHouse一样的使用方法。


最终我们在云原生ClickHouse中保留了数据本身的sharding设计,但改造了ClickHouse的单机引擎,使得单个节点能够负责任意多个数据shard。有了这个能力,数据的shard数量就和集群的节点数解绑开了。设定一个较大的初始数据shard数量,可以保障节点数量在一定范围内自由变换。当计算节点数变化时只要数据shard还是能够被均匀分配,就不需要进行非常重的全量数据re-sharding操作,只需按照shard进行本地状态数据的物理搬迁(这里的本地状态数据主要指实时写入的本地暂存数据),然后计算节点重新分配挂载shard就可以了。


关于多计算组:一写多读 vs 多写多读


在云原生ClickHouse的架构设计中,多个计算组对集群的数据是相互可见并共享的,对一个单表的具体某个数据分片来说,多计算组是一写多读还是多写多读就决定了多计算组之间数据共享的方式。ClickHouse核心的MergeTree引擎是完全可以做到多写多读的,它最初的设计就是多写无冲突的模式。但是一些其他非主流的表引擎则不一定能做到。另一个重要的点是MergeTree在数据写入初期会进行多次数据文件合并,这种短时间内反复写入删除的操作对持久化层的对象存储是非常不友好的,而且当前云原生ClickHouse底层使用的对象存储写入带宽也非常宝贵。所以云原生ClickHouse当前选择了一写多读的模式,对具体某个单表的实时数据写入只有一个主计算组会接受请求然后持久化到本地的ESSD缓存盘,等待实时写入数据在后台充分合并后再往对象存储上搬迁。


其他计算组收到这个表的写入请求时,最多只能是进行请求转发。当前云原生ClickHouse的设定下,用户创建表时请求的计算组就作为该表的写计算组,用户可以通过此方式来指定规划不同表对应到不同的写计算组。大家应该注意到了云原生ClickHouse是表级的一写多读模式,它可以把不同表的写入负载映射到不同计算组上,而不是简单的单个计算组负责所有的写。这种一写多读模式的好处就是把对象存储的写入压力最小化了,但引入了额外的实时数据状态,导致在跨计算组读、计算组节点销毁迁移的过程中都需要进行额外的处理。


在代码架构设计或者用户使用体验上,无疑是多写多读(实时数据写穿到共享存储)模式更优,而且多写多读对于MergeTree存储来说也完全不困难。一写多读模式是我们基于当前产品化能力的一个综合考量设计,后续持久化对象存储层的写入带宽有进一步的提升,ESSD云盘演进出类似共享挂载等能力后,云原生ClickHouse可以进一步升级演进成多写多读模式。

核心技术解析


单节点多shard


熟悉ClickHouse的用户应该知道,ClickHouse的存储能力都是以库引擎、表引擎的方式暴露出来的。这种插件式的存储模块设计,使得ClickHouse中提供的表引擎十分多样化。针对单个MergeTree表引擎去实现数据分片逻辑是无法保证实例级别的数据sharding的。另一个问题是ClickHouse中关于数据sharding的信息也并不是在原有的存储表引擎中定义的,而是在单独的Distributed表引擎(Proxy表)中,也就是正常的存储表引擎中完全没有sharding信息的,只有用户通过Proxy表访问时才会附加sharding规则进行读写路由。


以上的问题导致在单机ClickHouse原有的存储设计上叠加shard概念非常困难同时也会打破原有的使用方式 ,最后我们通过在Catalog层面进行了改造,让单个节点看起来真的变成了"多个节点",这才做到了和ClickHouse的MPP计算引擎完全兼容。


如下图所示,云原生版ClickHouse在整个实例状态层面插入了Catalog Context。Catalog Context会统一管理暴露给用户的库表元数据信息,Catalog Context之间的库表元数据管理完全独立。相当于把原来单个节点的部分元数据移到了Catalog Context内部。Catalog Context内当前管理的对象主要就是存储层的库和表对象,不同Catalog Context之间的库表对象相互不影响。我们通过把多个数据shard绑定到节点唯一的Catalog Context上实现了单节点服务多个数据shard的能力。节点之间的链接请求通过设定catalog参数,就可以访问指定的数据shard。

2.jpg

元数据中心化

开源版本ClickHouse集群中的所有数据都是本地化无中心的一个状态,这里并不是说ClickHouse已经具备了元数据去中心化的能力,而是元数据还没有进入到分布式一致性的阶段。这就导致了用户在自己运维部署的时候经常出现一些诡异的问题:本地配置文件不一致、节点上的库表结构未同步等等。云原生ClickHouse引入了分布式事务KV系统构建整个集群的元数据底座,以此将所有的集群元数据进行了中心化管理。云原生ClickHouse中的元数据主要包括以下几类:


  • 参数配置文件,config.xml users.xml等;
  • Access Control信息,覆盖集群的账户、角色、权限、Quota、Profile等数据;
  • 分布式DDL任务队列;
  • Shard粒度的库表元数据信息(DDL);
  • MergeTree表引擎特有的元数据,Data Part信息,异步Mutation任务信息;


像参数配置、账户权限数据、分布式DDL任务等,都是可以由单个节点向Controller节点发起数据变更请求,Controller节点再操作底层KV系统进行持久化。所有其它节点都会通过和Controller节点的心跳来感知元数据的版本变化,不断对齐内存中的元数据状态。这里可能存在一个最基本的多节点写写冲突问题,对不同的元数据类型系统有不同的解决方式。在底层分布式事务KV系统中用户的读写都是可串行化的隔离级别,而Controller节点层面对不同类型的元数据修改可能采取的策略包括:覆盖写、拒绝掉版本落后的修改提交(节点提交的修改版本落后于KV系统里的版本)、自动Sequential写(DDL任务队列)。


库和表的DDL信息是云原生ClickHouse中最核心的元数据信息,这部分元数据在多节点间无法对齐对用户造成的困扰也最大。ClickHouse的去中心化,单层架构模式,使得它没有传统分布式数据库中的"Master"节点,用户看到的直接就是底下的"Worker"节点,而每个"Worker"节点必然会有自己本地的一个元数据版本。用户在进行Alter Table的时候会基于当前节点的元数据生成发布对应的分布式DDL任务,然后等待所有节点异步完成该任务。这就导致ClickHouse在执行分布式Alter Table的时候多个节点会有短暂不一致的现象。


为此ClickHouse社区版本已经在做对应的分布式DDL任务并行处理优化,通过DDL任务的并行处理提升效率,可以大大减少单个任务引起的全队列阻塞,避免节点间元数据长时间的不一致。云原生版ClickHouse经过单节点多shard能力改造后,库表元数据的管理粒度已经从节点级别变成了shard级别。实例所有数据shard中的库表元数据都会保存到中心化的分布式KV系统中,不同数据shard间的元数据相互独立,使数据shard可以动态挂载到任意节点中。节点挂载某个数据shard后,在Catalog Context中会有异步的DDLWorker线程监控执行对应计算组中发布的DDL任务,进而执行shard级别的元数据变更。


在单计算组多节点场景下,有可能存在多节点的Alter Table冲突。ClickHouse对这类写写冲突的处理方法非常简单:所有分布式DDL任务都在同一个队列里面,并且一定会被可串行化执行,这样一来和前序任务有冲突的任务一定都会失败,相当于延判冲突到最后才把冲突错误返回。而在多计算组场景下,集群进一步引入了读写同步问题:某个计算组修改表的元数据后其他计算组如何感知这个元数据变化。


上一章中介绍了云原生ClickHouse一写多读的架构设计,在库表的元数据管理上系统也是按照一写多读模式来工作,每个用户表都只会有一个Owner的计算组,用户的Alter Table操作都只能路由到对应的Owner计算组节点中。Owner计算组通过分布式DDL任务队列来完成所有shard分片的元数据变更。当这部分元数据持久化到分布式KV系统后,非Owner计算组就可以通过和Controller节点的心跳来感知表的元数据版本变化。


非Owner计算组在对待只读表的时候,会根据他们的引擎类型进行转换来提供查询服务,例如Mysql外表引擎、Distributed表引擎原本就没有本地状态,可以用ReadOnly SnapShot的方式进行构建管理,另一些数据链路类的Kafka表引擎等则不需要跨计算组可见,其中最复杂还是MergeTree表引擎的跨计算组只读能力构建,下一节中将详细展开。

3.jpg

MergeTree数据共享

ClickHouse的MergeTree存储引擎本身就具备异地更新,无内存状态的特性,非常适合做基于共享存储的一写多读。熟悉ClickHouse的同学应该清楚MergeTree表引擎的数据状态可以由DataPart集合来表示,DataPart都是Immutable的对象,对应到存储上就是一个独立的文件夹(提交后不会再修改),而MergeTree的异步变更状态则由Mutation集合来表示,DataPart和Mutation的名字里都引用了commit id,可以表示出它们commit的"时间"范围,以及彼此的可见性。


为了对MergeTree做一写多读改造,我们首先把DataPart元数据和Mutation记录这两部分信息放到了分布式KV存储中,让多计算组都可见。而DataPart的变更和Mutation的执行只会由具有写权限的单个节点来执行。只读节点有了DataPart的元数据集合,就可以轻易地从共享存储中构造出当前MergeTree表引擎的只读对象。这里主要存在以下三个核心难点:


首先是只读计算组如何实时获取维护DataPart的集合,第一种方式是在定时从Controller进行全量同步,第二种则是全量同步+后台增量同步,系统会根据只读表使用的频率来择优选择同步的方式。为了达到增量同步的目的,我们在分布式KV中不光记录了DataPart的瞬时集合,也记录了DataPart的变更日志,只读节点通过不断同步变更日志就可以完成增量同步。


在现有的一写多读模式下,部分DataPart的存储数据可能还处在Owner计算组节点的本地缓存盘中,对于这部分数据只读节点无法通过共享存储访问,转而只能通过内部查询的方式进行数据读取,这里我们对原有的MergeTree引擎读链路也进行了改造。


原有的MergeTree表引擎是启动时进行索引数据强加载的模式,在动态多计算组模式下这种强加载行为在很多场景下并不成立,用户会在特定的计算组中倾向使用特定的业务表,所以我们把DataPart的加载模式改造成了Lazy Load的模式,大大减少了计算组之间的非必要影响。

对象存储加速

在使用对象存储的数据类型产品中,一定会有两类加速优化伴随:第一是对象存储的元数据读写操作加速;第二是热点数据的本地磁盘缓存加速。ClickHouse的MergeTree存储引擎非常依赖底层文件系统接口的操作性能,rename、hardlink、listDir、rmDir等都是MergeTree存储引擎中的常用操作,而对象存储本身并不提供类fs的API,直接用对象存储来模拟以上的fs操作在性能上也不可接受。开源ClickHouse中S3对象存储功能使用了本地磁盘文件作为S3 Object的引用句柄,对这个本地"句柄文件"的读写删除都会进一步操作S3 Object。在云原生ClickHouse中,我们把这部分对象存储的引用句柄也保存到了分布式KV系统中。


我们在Controller节点中借用了POSIX文件系统的抽象,来组织管理对象存储文件的引用信息,具体的引用信息都被保存到inode中,通过这种方式云原生ClickHouse的分布式共享存储具备很高的目录操作性能,同时还兼容hardlink等高级使用方法。操作常规的目录接口时,ClickHouse内核会请求Controller节点来完成对应操作,而在性能关键的读写链路上,ClickHouse内核则只会和Controller交互必要的引用信息,然后旁路掉Controller节点直接读写实际的对象存储文件。


ClickHouse在读取对象存储时,可以使用本地磁盘构建数据缓存,提升热点数据的扫描吞吐。云原生ClickHouse在这里并没有选择使用类似alluxio的分布式共享文件缓存,主要的原因是ClickHouse属于long running的MPP架构范畴节点和数据之间有明确的归属绑定,我们致力于把单机维度的缓存能力做到极致,在ClickHouse内核中构建这个数据缓存能力是最高效的,同时也最能理解系统负载。

结语


云原生ClickHouse目前正在全网公测阶段,当前只开放单计算组形态,多计算组形态仍正在内部测试优化阶段,后面会进一步放开。单计算组产品形态下最大的使用优化点是计算资源的平滑变配能力,水平扩容模式对比开源集群有数量级的效率提升。整体扩容时间和集群总存储量解耦,只和本地缓存盘中的实时数据量有关。欢迎大家来体验使用云原生ClickHouse,给予宝贵的反馈意见。

目录
相关文章
|
4天前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术深度解析:从基础到应用的全面介绍
人工智能(AI)技术的迅猛发展,正在深刻改变着我们的生活和工作方式。从自然语言处理(NLP)到机器学习,从神经网络到大型语言模型(LLM),AI技术的每一次进步都带来了前所未有的机遇和挑战。本文将从背景、历史、业务场景、Python代码示例、流程图以及如何上手等多个方面,对AI技术中的关键组件进行深度解析,为读者呈现一个全面而深入的AI技术世界。
50 10
|
5天前
|
存储 关系型数据库 MySQL
double ,FLOAT还是double(m,n)--深入解析MySQL数据库中双精度浮点数的使用
本文探讨了在MySQL中使用`float`和`double`时指定精度和刻度的影响。对于`float`,指定精度会影响存储大小:0-23位使用4字节单精度存储,24-53位使用8字节双精度存储。而对于`double`,指定精度和刻度对存储空间没有影响,但可以限制数值的输入范围,提高数据的规范性和业务意义。从性能角度看,`float`和`double`的区别不大,但在存储空间和数据输入方面,指定精度和刻度有助于优化和约束。
|
11天前
|
机器学习/深度学习 人工智能 自然语言处理
秒级响应 + 99.9%准确率:法律行业文本比对技术解析
本工具基于先进AI技术,采用自然语言处理和语义匹配算法,支持PDF、Word等格式,实现法律文本的智能化比对。具备高精度语义匹配、多格式兼容、高性能架构及智能化标注与可视化等特点,有效解决文本复杂性和法规更新难题,提升法律行业工作效率。
|
8天前
|
数据采集 存储 JavaScript
网页爬虫技术全解析:从基础到实战
在信息爆炸的时代,网页爬虫作为数据采集的重要工具,已成为数据科学家、研究人员和开发者不可或缺的技术。本文全面解析网页爬虫的基础概念、工作原理、技术栈与工具,以及实战案例,探讨其合法性与道德问题,分享爬虫设计与实现的详细步骤,介绍优化与维护的方法,应对反爬虫机制、动态内容加载等挑战,旨在帮助读者深入理解并合理运用网页爬虫技术。
|
14天前
|
机器学习/深度学习 自然语言处理 监控
智能客服系统集成技术解析和价值点梳理
在 2024 年的智能客服系统领域,合力亿捷等服务商凭借其卓越的技术实力引领潮流,它们均积极应用最新的大模型技术,推动智能客服的进步。
49 7
|
19天前
|
负载均衡 网络协议 算法
Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式
本文探讨了Docker容器环境中服务发现与负载均衡的技术与方法,涵盖环境变量、DNS、集中式服务发现系统等方式,以及软件负载均衡器、云服务负载均衡、容器编排工具等实现手段,强调两者结合的重要性及面临挑战的应对措施。
47 3
|
23天前
|
网络协议 网络性能优化 数据处理
深入解析:TCP与UDP的核心技术差异
在网络通信的世界里,TCP(传输控制协议)和UDP(用户数据报协议)是两种核心的传输层协议,它们在确保数据传输的可靠性、效率和实时性方面扮演着不同的角色。本文将深入探讨这两种协议的技术差异,并探讨它们在不同应用场景下的适用性。
61 4
|
22天前
|
安全 持续交付 Docker
深入理解并实践容器化技术——Docker 深度解析
深入理解并实践容器化技术——Docker 深度解析
43 2
|
22天前
|
供应链 算法 安全
深度解析区块链技术的分布式共识机制
深度解析区块链技术的分布式共识机制
44 0
|
22天前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
45 0
下一篇
DataWorks