从云托管到云原生背后的核心技术

大家好,今日小科来聊聊一篇关于从云托管到云原生背后的核心技术解析的文章,现在让我们往下看看吧!
阿里云ClickHouse数据库从上线到现在,已经走过了两年多的时间。在此期间,我们在云上积累了大量的客户案例,也在长期的客服支持中对ClickHouse数据库生态有了深刻的理解。在充分了解现有产品的生态和客户的实践痛点后,我们对开源的ClickHouse数据库进行了全新的架构升级,接下来我们将为用户带来全新的云原生版ClickHouse数据库。
云本地ClickHouse架构
虽然“云原生数据库”这个词耳熟能详,但是对于数据库产品是否“云原生”,很多用户还没有建立一个明确的衡量标准。过去,用户对云托管数据库的感觉是,数据库实例从本地物理机迁移到云上的ECS,本地磁盘成为可靠的云盘存储。同时,云数据库产品将提供稳定易用的运维监控手段。
云托管模式将用户从运维的人工部署时代解放出来。云原生数据库给用户的核心感受是提供更好的资源灵活性和数据共享能力。从数据库引擎内核来看,云原生数据库需要更好地拥抱和利用云平台的基础设施能力。一些传统的数据库架构模型是基于本地不可靠的存储设备和有限的计算资源设计的,在云平台的前提下将不再是最佳选择。我们即将推出的云原生ClickHouse数据库,对于用户来说主要有以下三个形态上的变化(也是云原生ClickHouse数据库核心能力不断进化的方向):
首先是存储资源和计算资源的解绑。存储资源可以按使用量收费,不再需要因为磁盘存储空间不足而被迫整体扩容。
其次,是计算资源的极致弹性,可以分分钟拉起、摧毁、扩充、收缩计算资源的容量,用户可以在预测计算组负载时分时调整规格,最大限度减少闲置资源;
最后,它是单个数据库实例下的多计算组资源模型。计算组之间的CPU/内存资源相互隔离,但共享整个集群的存储数据。用户可以在此基础上做不同类型的业务负载隔离和读写负载隔离。
业界也有很多用户形态相同的产品案例,说明大家对云原生的认知是一致的,都在向极致灵活、计算服务、存储数量的方向演进。只是计算资源弹性能力的差异,使得不同的产品在用户使用时会有不同的感受。对于大多数用户来说,分钟级的计算资源需要提前拉起,资源是常驻的。如果这个计算资源被‘拉起’的速度足够快,那么提前拉起的动作就可以省略。对于用户来说,计算资源是完全面向服务的,随时可以使用并在使用后释放。
云原生ClickHouse数据库架构图如下:
每个计算组向用户公开一个独立的链接字符串,用户可以通过不同的链接字符串登录相应的计算组。从用户的角度来看,计算组的概念相当于原始云托管模式的一个“集群”。计算组之间的机器资源是完全隔离的,计算组收到的查询请求只会在自己组内的节点上执行。持久存储层与计算组资源分离,它不附属于单个计算组,多个计算组需要共享一个持久存储层。云原生ClickHouse使用分布式对象存储作为持久存储层,分布式KV系统为集群的所有元数据提供存储服务,实现计算组之间数据的互见互通。
单个计算组由多个节点组成,每个节点都会拉起clickhouse-server作为主进程提供查询服务。同时,每个节点会挂载ESSD云盘作为本地缓冲盘。这个缓冲盘目前有两个功能:读取热数据的缓存和写入实时数据的临时存储(实时写入不会写入持久存储)。所以这里的缓冲盘相当于有状态,需要在节点迁移销毁的过程中进行处理。最后如图所示,我们对数据做了分片处理,但是节点和分片的数量并不完全相等,只能保证分片能够均匀分布到节点上。下一章,我们将更详细的分析解读云原生ClickHouse的架构选择。
关键选择
关于碎片
在传统的基于Share Nothing架构的MPP数据库中,数据一般按照单个主键列进行Harding(分析场景一般使用hash分区),不仅数据被Harding,计算节点也被绑定到相应的数据分片上,单个计算节点只负责读写某个数据分片,目前开源ClickHouse的分布式部署模式就是这种情况。计算节点和数据分片之间这种一对一的映射强绑定关系,给计算节点的横向扩展和收缩能力带来了很大的限制。一旦用户的ClickHouse集群需要增加和减少节点,它就需要重新分片所有数据。重新分片是一个完全符合逻辑的数据重新定位过程,相当于从旧集群中选择数据,然后将其插回到新集群中。clickhouse-copier是社区提供的数据逻辑重定位工具。纵观所有分布式数据库产品,数据的分片或多或少会限制集群的扩展性和灵活性。数据分级对分布式数据库产品如此重要的原因是,如果没有数据分级,
基于主键的记录写入变更就缺少了最重要的并行能力。
目前只有在完全放弃主键语义的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。
元数据中心化
开源版本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表引擎的跨计算组只读能力构建,下一节中将详细展开。
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,给予宝贵的反馈意见。
这篇好文章是转载于:知行礼动
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 知行礼动
- 本文地址: /news/detail/tanhccfejc