近年来,随着大数据分析技术的普及和物联网产业的兴起,越来越多的企业开始重视海量数据的收集和分析处理活动,希望从庞大的数据资料中挖掘出高价值的信息和洞见。而在数据规模快速膨胀的同时,企业对数据处理平台的软硬件基础设施也提出了更高的要求,并在这一领域催生了很多高水平的前沿技术变革。
在这样的趋势下,由俄罗斯 Yandex 开发的一款名为 Clickhouse 的数据库产品就在众多竞争者中脱颖而出,凭借十亿乃至百亿行的数据规模下依旧具备秒级返回能力的卓越性能,赢得了全球众多大厂的青睐。Clickhouse 究竟有怎样的独门绝技,如何做到如此强悍的性能表现,实践中又是如何在主流云平台上部署和优化的?9 月 22 日,来自京东智联云云产品研发部的架构师王向飞老师做客 InfoQ,介绍了 Clickhouse 数据库在京东智联云的落地应用与优化改进经历,为想要深入了解 Clickhouse 的小伙伴们送上了一堂干货满满的技术分享课程。
本文总结自王向飞老师的在线公开课:《Clickhouse 在京东智联云的大规模应用和架构改良》。
根据数据库处理的业务数据量,以及处理数据的不同方式,人们把数据库分为 OLTP 和 OLAP 两大类型。由俄罗斯 Yandex 搜索引擎公司开发并开源的 Clickhouse 数据库,其初始定位就是 Yandex 内部的分析型数据库,符合 OLAP 类型数据库的实现特点。Clickhouse 的性能超过了很多流行的商业数据库,已经得到了包括 CloudFlare、Spotify、阿里云、腾讯云、京东智联云、今日头条、携程等诸多头部大厂的采用。
OLTP vs OLAP
常见的 Oracle、MySQL 等数据库都属于 OLTP 类型,也就是 On-Line Transaction Processing,联机事务处理。OLTP 数据库处理请求和数据时对延迟要求很高,并且要保证数据的完整性和一致性。此类数据库是面向最终客户的,需要具备 7x24 不间断服务能力。
OLAP 的含义是联机分析数据处理,这种数据库需要存储海量、但很少更新修改的数据,主要用于多维度历史数据分析统计目的。出于这种需求,OLAP 数据库需要确保足够高的查询效率,至少 90% 的请求要在很短的时间内返回。
另一方面,OLAP 数据库并不是直接面对最终客户,而是更关注数据吞吐,要求海量数据尽快持久化,为业务决策、战略定位和分析、个性化推荐等任务提供分析统计能力,所以这类数据库中的查询一般都是较低频的。
Clickhouse 的关键特性:列式存储
基于 OLAP 数据库的特点,Clickhouse 采用了基于列的数据存储引擎。传统的行式数据库在存储信息时,是在数据库中按顺序逐个记录的。以用户注册信息为例,行式数据库会将每个用户的姓名、职业、年龄等数据依次记录下来。当业务需要查找注册用户的职业或年龄分布时,数据库需要打开所有存储用户注册信息的文件,遍历全部数据行,依次挑出所有职业和年龄信息进行汇总。使用这种方式,查询遍历的数据往往远大于所需的数据大小,对 IO 能力会造成严重浪费。
相比之下,列式存储会将数据分为多个属性列,例如用户注册信息分为职业、年龄等多个属性,并按这些属性列分为多个文件分别存储。这样当查询需要获取其中某些属性的数据时,只需查找对应文件即可完成,大大节约了 IO 需求。
以一个包含 1 亿条数据的测试表为例,一个简单的 count 查询在 MySQL 上需要两分多钟,而在列式存储的 Clickhouse 数据库上仅用不足 1 秒就返回了结果。那么,Clickhouse 具体都使用了哪些技术来实现如此高的效率提升呢?
B+Tree vs MergeTree
在深入研究 Clickhouse 之前,我们首先以传统的 MySQL InnoDB 的存储格式来做对比。
从 InnoDB 的逻辑结构图可以看到,在 InnoDB 中所有数据会被放在表空间内。表空间可以看作是 InnoDB 的逻辑最高层,由多个段组成,段又分为数据段、索引段。数据生成时按顺序写入数据段,随着数据记录的增多,InnoDB 会将一些主键值放到索引段内以实现快速定位。
随着数据量不断增多,数据库形成了名为 B+Tree 的树状结构。这个树有层级结构,会横向生长,其查询的复杂度取决于树的高度。B+Tree 的数据节点一般存储主键值,根据主键查找时可以通过叶子节点大概定位数据页,之后直接读取数据页即可。
而 Clickhouse 的数据架构类似关系型数据库,其中包括了解析器,主要负责将 SQL 语句通过词法分析、语法分析等转换成计算机可读的抽象语法树。另外还有优化器,逻辑优化负责优化抽象语法树的逻辑,比如简化一些长难运算表达式,做一些语义优化等。物理优化则负责生成可以直接执行的物理执行计划,指导数据库管理系统如何获取数据表、如何进行数据 join、排序等等。
Clickhouse 的物理执行计划可认为是一个数据流图,也就是数据的有向无环图。在这个图里,数据从一个管道传到另一个管道,也就是从一个操作符传到另一个操作符。查询执行器是用来执行计划的引擎,它会从存储引擎中取出数据,并返回给客户端。
如上图,Clickhouse 在启动时加载配置信息,然后根据不同的解析协议监听不同的服务端口。客户端发送来 SQL 请求后,首先它会对 SQL 进行语法解析,然后生成抽象语法树,并进行一系列的逻辑优化、物理优化,生成执行计划。接下来由不同的执行器根据 SQL 请求来将执行计划分发到本地或远端的存储引擎,从存储引擎中取出数据。数据经过一系列的计算加工后返回给客户端,客户端就可以输出缓冲区读取查询结果。
MergeTree 存储过程
相比 InnoDB 使用的 B+Tree,Clickhouse 使用的是 MergeTree 存储引擎来存储数据。这里以一个 Clickhouse 表为例:
本例中,我们根据出生日期做一个数据分区,主键选用用户的名字,并设置 SETTINGS index_granularity=3。表建成后插入 10 条记录,分为 2001 年 3 月和 2001 年 2 月两个数据区间。表建完、数据写完以后,Clickhouse 默认会在数据文件存放路径下建一个相应的表名:
这里可以看到,10 条数据分了两个文件夹来存储。文件夹命名时,其第一部分是分区键,也就是出生日期;1_1(2_2)代表每个数据分区内数据块最小块和最大块的编号。最后的数字 0 代表合并层级。
上图是 MergeTree 中对 Data part 进行元数据管理的结构体。其中,partition id 代表数据所处的分区 id;min block、max block 代表数据写入的版本信息——用户每次批量写的数据都会生成一个 Data part,同一批写入的数据会被标记为唯一的 block number。MergeTree 存储引擎后台会定期通过异步任务合并数据,且只会合并位于同一个数据分区内的数据,还要求 min block 和 max block 数据区间必须是连续非重合的。
第四个 level 字段默认新插入的数据都是 0,之后会随着合并次数的增加在原来的基础上依次增大。下面的 mutation 字段在数据更正时使用。如果要进行数据的更正操作,Clickhouse 会默认给 mutation 字段进行标记和更新。
虽然测试数据只有一张表 10 条数据,但它会在磁盘目录上生成大量文件。具体来说,Clickhouse 默认每一个列生成一个文件,默认数据文件放在 bin 文件里。每一个数据分析目录下生成一个 count 文件,记录分区里有多少行数据。
本例中,建表时设置的 SETTINGS index_granularity 设为 3。插完数据以后观察主键索引,可以发现它会把主键以每 3 条记录为一个区间,将主键信息存储在 primary.idx 里。
结合前文例子来看数据全景。假设下面绿颜色的就是要写入的一批数据,存放用户的名字;假设每个名字占用 4 个字节,可以看到绿颜色上边有一个 granule,写的是 8192。指定 granule 是 8192 之后,数据在写入时会放到一个具有缓冲区的 OutPort 流中,按照一个 granule 一个 granule 来写;写完第一个 granule,当发现这个缓冲区内数据大小超过 64KB,这时就会把数据进行压缩落盘,放在下边的粉红色文件块里。落数据块时会先写一个文件头,文件头由三部分组成,如上图所示。
第二段 8192 的数据,压缩完之后数据块可能比较长一些;可以发现,数据每次写入就会产生两个文件。一个是 bin 文件,也就是压缩后的数据文件。另一个文件就是主键 index 文件。但这样以来,在数据查询时不知道数据究竟在数据文件里的哪一块,不知道该怎么拆分 bin 文件,如果把整个 bin 文件都加载内存以后扫描,效率是会很差的。为了解决这个尴尬的问题 Clickhouse 引入了 mrk 文件。写数据文件的时候会把 bin 文件头信息写到 mrk 文件里。比如说第一块数据写完之后,会把起始位置、解压缩后的位置、解压缩前的位置放在 mrk 这个文件块里, 作为一行记录。查询时直接根据主键 index 记录的偏移量找到对应的 mrk 记录的某段数据的起始位置,之后读取数据即可。
MergeTree 也有一些异步任务处理,主要有三部分:首先是定期把一些有问题的、提交失败的、或写失败的数据文件清理掉;然后是定期把一些比较琐碎的插入语句生成的小文件块合并为大的文件块;还有偶尔有一些更正,例如数据的更新删除生成的临时文件,MergeTree 也会把对应的数据文件汇总成一个比较大的 Part。
智联云选择 K8s 部署的原因
京东智联云基于现有的云平台部署 Clickhouse 时,是基于 K8s 团队提供的强大运维调度平台来实现的。之所以选择基于 K8s 来部署有几方面原因:
首先,K8s 可以屏蔽底层的环境差异,使用户无需再具体关注主机网络、存储、API 接口等变化,只需将精力集中到数据库管理开发任务上。
其次,构建这个 JCHDB 平台并不只是为京东智联云内部使用。这个平台构建完成后,不仅可以在公有云上给客户提供服务,并且在私有化部署或者跨云部署时,都可以完全不用对架构做任何修改直接部署。
智联云部署 Clickhouse 的流程
数据库系统是一个比较复杂的有状态业务系统,分片跟副本之间有状态关系,这时候就需要维护 Pod 与 Pod 之间的关系,K8s 为此提供了 operator 功能。京东智联云二次开发了开源的 Clickhouse operator,丰富了 operator 的 API 功能,并且为安全生产的需要给它打上一些额外的标签,控制 Pod 调度的亲和性,防止主从副本落到同一个物理机上。
这个 operator 开发部署完毕,安装到 K8s 以后,K8s 就有了管理调度 Clickhouse 状态的能力。在它的外部会借助 helm 系统,将提前定义好的一些表单发送给 K8s,由 K8s 来根据表单里定义的这些参数来创建需要的实例。
除了创建最基础的 Clickhouse,如果需要有复制关系引擎,它也会一同创建 Zookeeper;同时为了丰富监控能力,便于 DBA 进行服务器的运维监控,它还会创建一个 Promethus,还有可视化的 Grafana。这样它就可以直接在 VPC 里通过 Grafana 来监控数据库的运维状态。
这个服务还会创建一个绑定 headless service ip 地址的域名,用户可以通过这个数据库域名直接连到这一套 Clickhouse 系统上。由上图右可见,这个 Pod 的底层存储使用了京东的云硬盘,它会在建 Pod 时申请一个 PVC 控制器,PVC 控制器会绑定京东云硬盘。这样就形成了计算与存储分离的架构,可以进一步提升计算能力。
京东智联云目前在高性能、高可用和可扩展层面上都有自己的特色:
高性能方面,智联云采用最新一代的云主机,CPU 最大可以支持 64 核心,单个 Pod 最大可以扩展到 512G 内存。
高可用方面,智联云借助 K8s 调度管理平台,发现有 Pod 不可用时 K8s 会自动将这个 Pod 剔除。同时平台会新建一个 Pod,基于 StatefulSet 机制将被删除的 Pod 所绑定的云盘挂到新 Pod 上。这样如果有实例由于某些硬件原因出现问题,可以在分或秒级完成实例替换。
可扩展方面,智联云基于云主机、云硬盘的一些基础组件,提供了一些灵活的扩展接口,可以直接在原地扩容。智联云支持热扩容,可以在不影响用户使用的前提下,在分钟级甚至秒级就可以完成 CPU 数量、内存容量或磁盘空间的扩容。
京东智联云还提供了完善的监控体系,可以帮助 DBA 更好地观察的数据库的运行状况。平台不但提供了 service 级别的数据库监控,同时还能把所有 Pod 所使用的磁盘空间、CPU、内存都展示给用户。有了这些信息,用户可以更加直观地观察到每一个 Pod 的压力分布情况,进而方便灵活地调整数据压力,避免某个 Pod 出现数据瓶颈。
根据这些监控的目标,用户可以灵活地定义告警信息。智联云支持多维度数据告警,可以通过邮件、短信、微信等形式告警。
Clickhouse 自身具备强大的数据处理能力,还能很好地兼容 SQL 语句。但在实际设计和使用过程中,不能仅仅把它当成一个传统关系型数据库的增强替代品,这样可能会限制 Clickhouse 的潜力发挥。企业需要对传统的数据仓库、设计理念,以及数据上下游的流转方式做出改进,发挥想象力和创造力,更好地利用 Clickhouse 的列式存储、并行计算等数据能力,为数据业务创造更大价值。
京东智联云的 Clickhouse 目前正在产品公测,欢迎大家使用。
点击【阅读原文】获取课程视频