从Clickhouse 到 Snowflake: 云原生

云原生Clickhouse优势概述

以Clickhouse为基础,借鉴Snowflake等系统的设计思路,打造一款高性能的云原生OLAP系统,为用户提供多场景下的一站式的数据分析平台。

  • 简单、易维护:集群管理、统一共享分布式调度服务
  • 高可用、可扩展:支持500万以上的Table
  • 低成本:存储成本至少降低了50%
  • 兼容开源,复用超高性能:兼容协议、语法、数据库存储格式

Clickhouse是一款性能十分强悍的OLAP引擎,凭借优秀的性能在用户行为分析、ABTest、在线报表等多个领域大放异彩,但是目前Clickhouse在易用性、稳定性、可维护性、功能特性等方面都还有较多不足,主要体现在以下几方面:

  • 易用性方面,需要用户感知本地表、分布式表、Zookeeper等概念,在建表、导入、查询等流程中都有涉及,后期Schema Change时也需要分别做处理,使用门槛较高;
  • 稳定性方面,重度依赖Zookeeper,Zookeeper已经成为明显的中心化瓶颈,尤其在多副本场景下单机的写入、Merge、DDL的执行都需要经过Zookeeper协调,一旦Zookeeper不稳定或者代码有Bug,整个分布式集群都会不稳定;
  • 可维护性方面,缺少节点加入、节点退出、副本均衡等必要的分布式管理功能,一旦集群数量变多,维护代价巨大;
  • 功能特性方面,没有真正的MPP查询层,在分布式Join、聚合等方面能力不足,语法上也不符合相关标准,有一定的学习成本;

Snowflake 是当下最火的云原生数仓,它SaaS化的设计理念极大的提升了数据开发者的使用体验,Clickhouse高性能的存储引擎和计算算子是一个非常优秀的底座,我们希望以Clickhouse为基础,借鉴Snowflake的设计思路,打造一款高性能的云原生OLAP系统,为用户提供多场景下的一站式的数据分析平台,包括:

  • HTAP 场景,与腾讯CDB、CynosDB等无缝集成,实时同步OLTP系统中的变更,基于列存和并行执行引擎,实现海量数据下的亚秒级分析;同时支持MySQL协议和语法,无缝兼容用户现有的工具,达到零迁移成本。
  • 数仓场景,支持用户数据实时入仓,为用户提供超高性能的在线分析服务;针对结构化、半结构化数据,实现统一的分析解决方案,利用Clickhouse丰富可扩展的算子,构建简单易用的流式处理框架简化ETL流程。
  • 数据湖场景,支持对象存储COS、CFS、Elasticsearch、MongoDB等多数据源的查询,通过跟多个数据源做深度的整合,利用高速的向量化执行引擎,提升多源数据的分析能力及性能。

从Clickhouse 到 Snowflake: 云原生_第1张图片

 

1、云原生Clickhouse架构设计

存算分离正在数据库领域掀起一场变革,不论OLTP还是OLAP系统都在拥抱存算分离。通过对Clickhouse的源码进行阅读分析,发现在Clickhouse中一旦实现了存算分离,那么稳定性、可维护性以及成本等方面的问题都会迎刃可解。Clickhouse虽然在分布式集群管理方面很弱,但是这也意味着在改造存算分离架构时的负担很少,成为了一个巨大的优势,所以我们把存算分离作为第一个目标,希望通过引入存算分离技术,并且围绕存算分离技术对分布式集群管理等各个方面进行改造,为用户提供一个更好的Clickhouse。

云原生Clickhouse的架构设计如下图,具体包括三层:

从Clickhouse 到 Snowflake: 云原生_第2张图片

(云原生Clickhouse架构设计图)

集群管理层:分布式集群的大脑,它主要包含基于分布式一致性协议实现的元数据管理服务、多集群共享的分布式任务调度服务;

计算层:用户通过创建的计算集群来实际使用分析服务,每个计算集群由多个节点组成,用户的查询任务在一个计算集群上的节点里完成,同一个用户的多个计算集群可共享集群管理层;

存储层:基于共享存储实现,用户的所有数据都存放在共享存储内,可以被多个计算集群访问,同时它提供了廉价、按需、无限扩展的存储能力;

数据流

  • 用户直接连接Clickhouse Node,而不是Master Node,所以可以通过现有的Clickhouse工具连接和使用;
  • 数据流在多个Clickhouse Node之间传输,不会经过Master Node,所以不会成为中心化瓶颈;

控制流

  • 控制流不再依赖Zookeeper来协调,而是通过一个全局共享的Master Node来实现;
  • 所有的分布式DDL命令都转发给Master Node,由Master Node协调DDL任务在各个节点上的执行,Master控制DDL的并发、失败处理;
  • Master Node存储了全局的、统一的Schema信息,为后续的MPP查询层打下了元数据的基础;
  • Master Node控制节点加入和退出等集群管理工作;
  • 由于Master服务是非常轻量的,所以可以在多个集群之间共享,能有效的降低部署Zookeeper带来的成本开销;

基于共享存储的存算分离机制

  • 强一致,数据存放到共享存储上,各个节点可以有一致的数据视图,任何一个节点写入数据,其他副本均立即可见;
  • 基于共享存储我们实现了Insert、Mutation、Alter等任务的冲突处理机制;
  • Merge/Mutation过程可以在任意一个副本上执行,多个节点可以Merge不同的Part,在高速导入的情况下,能加速Merge的速度,快速的减少底层小文件数量;

2、云原生Clickhouse核心特性

2.1 易使用,易运维

过去在运维一个Clickhouse集群时,很头疼的一点就是集群管理,例如,我们要向集群中增加一个节点,需要以下操作:

  • 启动新的节点;
  • 在新节点上创建本地表和分布式表,为了获取已创建的表信息,就需要到其他节点上查询,如果表比较多就需要脚本来配合,相信运维Clickhouse的同学都积累了不少脚本-;
  • 修改所有节点的配置文件Metrica.xml,把新节点信息写入,生效;

在新架构下,Master维护了全局统一的元数据信息,使得我们可以通过一条条简单的SQL命令来自动化集群管理, 例如下面这个命令就是向集群中增加一个副本节点:

ALTER CLUSTER cluster_name ADD BACKEND 'ip:port' TO SHARD  2;

这个命令首先会修改Master统一管理的元数据,然后新增的Clickhouse Node会从Master上同步相关元数据,更新本地配置,最终用户可以在Clickhouse上运行以下命令来获取集群的节点信息,同时当前Clickhouse的各种查询语句也可以继续使用集群的拓扑结构来构建复杂的查询:

SELECT * FROM SYSTEM.CLUSTERS;

此外,用户不需要再使用ReplicatedMergeTree引擎,不需要关注元数据在Zookeeper上的存储路径,只需要普通的MergeTree引擎即可,系统内部会自动为每个表分配唯一的数据存储路径,并且使互为副本的多个表共用存储空间。

CREATE TABLE t1 (
	partition_col_1 String,
	tc1 int,
	tc2 int)
ENGINE=MergeTree() 
PARTITION BY partition_col_1 
ORDER BY tc1;

2.2 统一共享的分布式任务调度服务

从Clickhouse 到 Snowflake: 云原生_第3张图片

(分布式DDL任务执行流程图)

在引入Master Node后,分布式DDL任务的执行流程如上图所示,下面以Create Table为例介绍一下具体的流程:

  • 用户连接任意一个Clickhouse Node发送Create Table on cluster 请求,该节点做初步的解析后,把这个命令以一个DDL Job的形式发送给Master;
  • Master Node做DDL Job的解析,把Job转化为需要发送给集群内所有Clickhouse Node的DDL Task,每一个Task都是建立Local Table;
  • Clickhouse Node执行完DDL Task之后给Master返回成功消息;
  • Master做元数据的持久化,并且给客户端Clickhouse Node1 返回成功消息;

基于Master的分布式架构还具备以下特点:

高可用:Master Node自身多副本,多副本之间通过一致性协议保证高可用。同时,我们考虑了Master Node和ClickHouse Node间的松耦合设计,即使Master Node全部故障,也不影响存量业务的普通读写操作,仅限制新DDL操作的执行。

并发控制:Master Node能够区分对不同Table的DDL请求,可以控制不同请求的并发级别,比如对于Alter是顺序执行,对于Mutation可以并发执行。

回滚机制:DDL Task不一定能够在所有的Clickhouse Node上全部成功,部分成功是常态;过去Clickhouse 在这种情况下会出现各个Clickhouse Node的状态不一致,我们引入了回滚机制,如果任意一个任务失败,整个Job就会失败,保持各个Clickhouse Node的状态一致.

垃圾清理机制:Clickhouse Node自身会定时的跟Master Node做状态同步,清理本地的垃圾Table或者数据目录。

从Clickhouse 到 Snowflake: 云原生_第4张图片

 

资料领取直通车:Golang云原生最新资料+视频学习路线

Go语言学习地址:Golang DevOps项目实战

2.3 基于存算分离架构的多副本

多副本技术是分布式系统底层存储的核心的机制,任何一个分布式系统都有大量的代码在处理多副本, Clickhouse 面临的很多问题也是由于多副本引起的。当前Clickhouse的多副本机制如下所示:

从Clickhouse 到 Snowflake: 云原生_第5张图片

(当前Clickhouse多副本机制)

当前架构有以下缺陷:

  • 依赖Zookeeper存储Log、Parts信息,在Local Table数量变多、数据量变多、导入频繁后会成为瓶颈,所以可以看到业界有很多改进都在设法降低Zookeeper中存储的元数据量,或者把不同的表放到不同的Zookeeper集群中;
  • 成本高,云上一般都是使用云磁盘,Clickhouse继续做副本结果导致磁盘存储成本高。比如云磁盘自身3副本,Clickhouse的ReplicationMergeTree 2副本,结果最后数据实际是6副本;此外云上很多客户都是中小客户,很多客户的Clickhouse集群的规模小于10,而Zookeeper本身就需要3个节点,带来了一些附加成本;

使用了共享存储之后,我们支持多读多写的模型,多副本管理的架构如下:

从Clickhouse 到 Snowflake: 云原生_第6张图片

 

方案概要:

  • 数据放到共享存储上,一个批量数据写入会产生一个Part;
  • 通过一个Commit Log记录Part的变更,比如Add Part, Remove Part等信息;
  • 所有的Clickhouse Replica读取Commit Log,根据Commit Log中记录的操作,更新本机内存中的Part列表信息,由于Part数据文件在共享存储上,所以不需要拉数据,直接加载就好。

为了能够让所有的副本都可以提供读写服务,基于Commit Log,我们增加了冲突处理机制,思路如下图所示:

从Clickhouse 到 Snowflake: 云原生_第7张图片

(冲突处理机制思路图)

  • 每个part 有一个唯一的UUID,不再使用part name做文件夹名字,而是使用uuid做文件夹名字,因为使用part name作为文件夹名字会带来名字冲突;
  • Commit Log提供写的冲突、回放机制,上层的Clickhouse进程写入Commit Log遇到冲突时,需要Replay CommitLog 然后重新提交Part信息;
  • Commit Log定期做Snapshot,一方面可以做Commit Log的回收,另外一方面也降低每次重启获取最新元数据的时间。

在存算分离架构下,成本相对于原来有大幅度降低:

  • 多个副本之间共享物理存储,相比过去2副本的情况下,存储成本至少降低一半;
  • 每个集群不需要单独部署Zookeeper,可以省去至少3个节点的资源成本;
  • 多读多写的模型,不存在只读备份节点的资源浪费,可提升资源利用率。

同时,多读多写的模型消除了传统主从副本在故障时的复杂切换逻辑,任意节点挂掉,其他副本都可以轻松接管读写,大幅提高系统可用性。

2.4 高可用无状态的数据服务层

Clickhouse社区目前有基于S3的多副本机制,也能够降低多副本带来的存储成本,但是这种方案目前有以下问题:

  • 共享存储需要依赖Zookeeper来协调,比如记录part的引用计数信息,Zookeeper记录的元数据更多了,每个Clickhouse Node与Zookeeper的交互更多了;
  • 在每个Clickhouse Node上仍需要记录一部分信息:
    • 在Metadata目录下仍保留存储表的元数据;
    • 每个数据文件会对应一个本地小文件,存储了数据文件到S3对象的映射关系;
  • 由于本地元数据的存在,所以Clickhouse Node本身不是无状态的,有以下问题:
    • 在节点宕机磁盘损坏时,这部分元数据就丢了,存储到S3上的数据在缺少元数据的情况下,也是不可用的;
    • 副本迁移的时候,虽然不需要拷贝数据了,但是元数据仍然需要同步,仍然使用了当前的多副本机制,在线上运维过程中发现这块代码的问题还是比较多的;

我们的改造思路是把所有的元数据都与本地存储剥离,使得基于Clickhouse的计算层彻底无状态:

  • Master Node保存了表的Schema元数据信息,每个Clickhouse Node本地的元数据只是一个缓存,一旦丢失,立即从Master同步;
  • 重新实现的基于共享存储的Part管理机制,将Part所有的信息都放到共享存储上,本地不再保存;

在存算分离模式下,多副本的目标已经从保证数据的可靠性转变为保证服务的可用性,通过把每个副本本地的状态消除,可以任意增加副本的数目,提升服务的可用性而不需要付出存储成本;另外还可以做到查询级别的调度,根据每个节点的健康状况和负载情况,把查询调度到合适的节点上来执行,而不需要复杂的多副本同步,服务整体的可用性大幅度提升。

2.5 秒级的弹性伸缩能力

在数据服务层完全无状态后,除了高可用之外,带来的一个巨大的优势是弹性,节点的加入和退出不需要复杂的数据同步机制,可以在秒级完成集群的伸缩:

  • 新节点加入时只需要从Master上获取表的Schema元数据,再从共享存储上获得Part元数据信息,就可以直接提供服务了,可以在秒级完成;
  • 旧节点删除,不需要等待数据迁移完成,可以直接下线,可以在秒级完成;

集群能够在秒级完成扩缩容有以下优势:

  • 低成本,分析型业务对系统资源的需求有明显的波峰、波谷的特点,例如,白天的资源消耗比晚上多,工作日比周末多,通过制定合适的集群伸缩计划可以显著降低对资源的需求;
  • 高性能,过去数据和节点是绑定的关系,一直遵循着计算跟着数据走的原则,而数据是在节点本地,所以计算是很难扩展的,当计算层无状态后,在一些场景下(比如大查询)可以直接扩充更多的计算节点,快速完成查询任务。

2.6 持续兼容开源生态

在改造Clickhouse的过程中,我们跟很多不同的团队做了一些交流,发现一个非常大的问题就是大家对Clickhouse做了大量的修改,然而由于各种原因这部分代码没能合并到社区,最终跟社区分叉了,所以出现了XXX版本Clickhouse;这一幕在过去10年的大数据历程中反复出现,我们都见过XXX公司内部版本的HDFS、HBase、Kafka等,而这些所谓的自研版本几乎都失败了。Clickhouse的功能迭代速度是很快的,比如最近社区推出的LLVM表达式优化、异步的Pipeline执行、zOrder等,都是非常重要的feature,对性能提升十分明显,一旦分叉这些功能就都用不上了,所以我们坚信兼容Clickhouse社区虽然会慢点,我们的设计会复杂点,但是会走的更远。

对于一个云服务而言,我们面对着各种各样的用户,很多用户的诉求是我们提供的Clickhouse能够跟随社区的版本升级。所以我们在项目伊始就定下了一个原则----尽量少的侵入Clickhouse,能够跟进开源社区Clickhouse进行持续升级。

从Clickhouse 到 Snowflake: 云原生_第8张图片

(模块结构图)

模块结构如上图所示,在我们的架构中,Clickhouse实际是一个单机的库,所以虽然我们实现了复杂的控制流和存算分离的功能,但是通过精巧的设计,基本上对Clickhouse没有侵入,改动了极少的代码,这使得后续的版本升级更加方便,能够随时合并Clickhouse社区的最新功能。

通过把Clickhouse进行云原生改造,与现状相比有以下优势:

• 简单、易维护:通过简单易用的集群管理、统一共享的分布式任务调度服务,可以大幅降低运维人员的运营压力,降低用户的使用门槛,运维同学可以一键完成集群伸缩、用户可以统一入口简单完成DDL操作等,整个系统更为简单易维护。

• 高可用、可扩展:整个系统的架构设计有充分的可用性考虑,各组件的容灾能力都颇具创新,后续也会继续增强;此外,消除了Zookeeper这个明显的中心化瓶颈后,系统支持的数据量可无限扩展,表数量仅受限于Master Node内存,在压力测试中,能够支持500万以上的Table,可覆盖绝大部分需求。

• 低成本:通过存算分离技术,存储成本至少降低了50%,消除了副本间的冗余写入开销,也去除了Zookeeper带来的附加成本;多读多写的模型保障了各个副本之间实时强一致,用户不需要在导入性能和一致性等级之间做权衡;资源调度方面,用户也可以根据集群的负载情况,秒级的弹性伸缩集群资源,可用于降低成本,也可用于提升一部分场景下的查询性能。

• 兼容开源,复用超高性能:持续完整的兼容性,对外的协议、语法、数据存储格式都完全兼容Clickhouse现有的版本,用户可以很方便的迁移,也可以充分享用ClickHouse的超高性能。

未来工作

  • MPP架构的查询引擎:这部分已经在紧张的设计开发中,我们以Clickhouse当前的向量化算子为基础,增加查询优化器、分布式join、聚合计算等功能,预期在今年晚些时候上线;除了支持Clickhouse之外,高性能的MPP执行框架还将支持对HDFS、对象存储、MySQL、Hive、Elasticsearch等多个数据源的查询,提升多源数据查询的速度。
  • 消除Shard,实现简单完全的分布式化:Shard是分布式领域的一个重要概念,主要是做数据在各个节点上的分布打散,然而这是分布式系统自身内部的逻辑,用户一般无需关注Shard,用户希望看到的是一个高度抽象、类单机的分布式系统。此外,当前数据副本和机器绑死的Shard方式,不利于充分发挥分布式系统在伸缩、故障恢复时的潜能,比如扩容时要求成倍进行、故障恢复时压力全部集中在少量节点上。所以我们也在努力改变这种使用方式方式,降低用户的使用负担或顾虑。

你可能感兴趣的:(Golang云原生,云原生,clickhouse,java,golang云原生,go语言)