从 Clickhouse 到 Snowflake(一): 云原生

概述

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

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

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

  • 数仓场景,支持数据实时入仓、实时分析,支持结构化、半结构化数据的统一分析,利用 Clickhouse 构建简单的流式 ETL 处理框架,简化 ETL 流程。
  • 数据湖场景,支持对象存储 COS、MySQL、Elasticsearch 等多个数据源的查询,通过跟多个数据源做深度的整合,利高速的向量化执行引擎,提升多源数据查询的速度。
  • HTAP 场景,跟腾讯 CynosDB,TDSQL 无缝集成,实时的同步 OLTP 系统中的变更,基于列存和并行执行引擎,实现海量数据下的毫秒级分析;同时支持 MySQL 协议和语法,无缝兼容用户现有的工具,达到零迁移成本。

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

云原生 Clickhouse 架构设计

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

资料领取直通车:Golang云原生最新资料+视频学习路线icon-default.png?t=M85Bhttps://docs.qq.com/doc/DTllySENWZWljdWp4

Go语言学习地址:Golang DevOps项目实战icon-default.png?t=M85Bhttps://ke.qq.com/course/422970?flowToken=1043212

我们的整个架构设计如下图,具体包括三部分:

  • 集群管理层:分布式集群的大脑,它主要包含基于分布式一致性协议实现的元数据管理服务、多集群共享的分布式任务调度服务;
  • 计算层:用户通过创建的计算集群来实际使用分析服务,每个计算集群由多个节点组成,用户的查询任务在一个计算集群上的节点里完成,同一个用户的多个计算集群可共享集群管理层;
  • 存储层:基于共享存储实现,用户的所有数据都存放在共享存储内,可以被多个计算集群访问,同时它提供了廉价、按需、无限扩展的存储能力;

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

数据流

  • 用户直接连接 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 的速度,快速的减少底层小文件数量;

核心特性

易使用、易运维

过去在运维一个 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;

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

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

在引入 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 面临的很多问题也是由于多副本引起的。当前 Clickhouse 的多副本机制如下所示:

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

当前架构有以下缺陷:

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

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

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

方案概要:

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

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

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

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

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

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

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

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

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

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

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

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

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

秒级的弹性伸缩能力

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

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

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

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

持续兼容开源生态

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

所以我们在项目伊始就定下了一个原则----尽量少的侵入 Clickhouse,能够跟进开源社区 Clickhouse 进行持续升级。

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

模块结构如上图所示,在我们的架构中,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,云原生,Go语言,云原生架构)