打造一款强大成熟的数据库有多难


作者: 康凯森

日期: 2022-11-17

分类: OLAP


从 2015 年开始,我在美团先后维护和研发过 Apache HBase, Apache Kylin, Apache Druid 和 Apache Doris, 对大数据系统和 OLAP 数据库有了深入理解,2020 年开始,我加入了 StarRocks,我们先后打造了业界领先的向量化执行器,CBO 优化器,Pipeline 并行引擎,支持高效 Update 的存储引擎和极速的数据湖分析,支持存储分离的 Cloud Native 版本也即将面世,一路走来,随着我们攻克一个又一个技术难题,解决一个又一个用户需求和难题,服务着越来越多的客户,我对如何打造一个强大且成熟的数据库有了更深的理解,在前一篇文章 如何快速打造一个高性能数据库原型 中,我们看到利用开源系统打造一个数据库原型是如此的简单,在这篇文章中,我会和大家介绍打造一个强大且成熟的数据库都有哪些难点,之后会再用几篇文章介绍我们如何解决这些难点。

本质矛盾

作为一家创业公司或者一家大公司的数据库部门,打造一款强大且成熟的数据库都会有下面两个本质矛盾:

1 有限人力和无限工作之间的矛盾

各种各样的用户需求,无止境的性能优化,永远也修不完的 Bug,一个又一个 Killer Feature 的打造,时刻不停的代码重构,增加不完的测试 Case,不间断的技术调研和系统设计,这些工作都需要大量人力,但无论哪家公司,人力总是不足的,优秀的数据库人才更是稀缺。

2 飞速增长与成熟稳定的矛盾

如果数据库的代码不大变,面对的应用场景不大变,我们让一个数据库变稳定会容易很多,但是如果一个数据库的代码在飞速变化和增长,应对的应用场景不断丰富和变化,让一个数据库稳定下来就会很难。

1 定位和独特性之难

一款数据库想要在产品和商业上取得成功,想在很多竞品中脱颖而出,就必须有清晰的定位和自己的独特性,一款数据库的独特性就是自己的卖点,比如目前 StarRocks 的卖点就是极速统一。 一旦定位和独特点定义清楚,也便意味着全公司产研资源的重心和方向的倾斜。 而一款数据库想要让自己的独特性成为 Killer Feature,就必须超越所有竞品,这就意味着,这家数据库公司必须有足够强大的创新能力,足够强大的工程能力,才能做到其他数据库公司一定时间内无法做到的。

2 架构之难

一个数据库的架构决定了一个数据库未来的上限,决定了一个数据库未来支持更多用户需求的难易程度,如果架构设计有误,后面的很多功能和优化成本就会很高,甚至无法实现。不过在当下,云时代数据库的架构逐渐趋同,想要有足够的独特性和明显的优势还是比较困难。

3 功能之难

Killer Feature 的创新性和独特性

一般数据库在产品功能上肯定会推出几个 Killer Feature, 作为自己产品的卖点,但是想做作为 Killer Feature,就必须比竞品好很多,这时候就必然需要 Killer Feature 拥有一定的创新性和独特性。

持续不断的用户需求

用户越来越多,用户的需求自然就会越多,有些用户的需求很小众和奇葩,但有时候为了拿下一个用户,我们却不得不做,而且有时候做了还会给系统带来额外的复杂性。

杂七杂八的认证

比如 CMMI 认证,通信认证,信创认证,国产服务器认证等等,这些认证里面的一些功能其实并不是你的数据库产品所必须的,做了不会增加什么产品竞争力,但是我们为了获取客户,我们还是必须投入大量的人力去通过这些认证。

用户迁移时的功能对齐

当一个新系统越来越好,要扩大市场规模,必然要去替换用户已有的旧数据库,但是替换过程中经常会遇到语法,函数,功能不对齐的场景,这时候,如果这个用户很重要,你就不得不去做一些和传统数据库功能对齐的事情。

系统越强大会越复杂

  • 越强大的优化可能跨模块越多:比如 StarRocks 的低基数优化和导入,查询,Delete,Update,Compaction,Schema Change, MVCC 等很多模块都有关系,所以这类功能的测试本身就会很复杂
  • 一个新的功能或者优化可能会打破旧的功能或者优化的假设:比如 Tablet 并行可能和 Local exchange 有冲突,比如 Query Cache 可能和 Shared Scan 有冲突

系统越成熟,新的功能和优化要求越高,大规模使用周期越长

  • 新系统的第一个版本往往会给用户建立一个 Baseline,之后的版本做的功能和优化就必须考虑所有用户的场景,在自己的用户场景下没有或者很少有 Bad Case
  • 用户使用一个系统稳定后,升级的动力会越来越弱,所以越靠后的版本得到用户大规模验证的周期也会更长

4 性能之难

CBO 优化器 Plan 的稳定性和正确性

  • 统计信息的变化会导致查询 Plan 发生剧变
  • 某些优化就是会导致部分查询变快, 部分查询变慢
  • 选择度估计和基数估计一般都假设了数据的均匀分布,但这和实际情况并不相符
  • 统计信息收集如何做到相对准确又不耗费大量的系统资源

单核性能优化

单核性能优化是一个没有止境的过程,我在 StarRocks 技术内幕:向量化编程精髓 一文中已经解释了单核优化的关键点和方法论,所有算子和函数的深度优化是需要数十人年的事情

多核扩展性优化

单核性能走到世界第一后,我们投入了大量精力在优化多核性能,多核扩展性的瓶颈定位相比单核会更困难些,我们已经做了大量的优化,但还远远不够。不同类型的查询在高并发下会遇到不同的瓶颈:Lock,线程池,RPC,调度问题,NUMA,CPU Cache,内存管理,IO 异步等。

多机扩展性优化

多机的扩展性的常见瓶颈点主要包括:

  • RPC 的扩展性
  • 元数据的扩展性
  • 如何解决数据倾斜
  • 如何通过调度策略解决热点,充分调用整个集群的算力
  • 优化器要确保每个算子,函数都可以生成分布式的执行计划,不会有单点执行的瓶颈

存储引擎的优化

存储引擎需要在导入性能和查询性能之间进行权衡,一般情况下,导入时候做的事情越多,查询时候的代价就更低些:

  • 更新能力的持续优化
  • 导入能力的持续优化
  • 压缩和编码
  • Compaction 策略的持续优化
  • 事务能力的优化
  • 内存使用上的优化
  • 各个级别 Cache 能力的优化

针对特定场景的性能优化一般会增加系统的复杂性

我们数据库一般都是面向通用场景开发,但是在特定场景下,我们可以获取更多信息和上下文,这时候,我们就可以进行更多的针对性优化,但这样带来的问题就是,系统的代码逻辑更加复杂,测试和维护的难度更高。

如何保证性能不退化

当系统复杂之后,多人协作经常会出现的问题是,一个人之前精心写的一段对性能影响很大的代码,被后人不小心改掉了;或者是优化 A 优化了某些场景的性能,但是却导致之前优化 B 的优化失效了。

如何能 Cover 不同的硬件环境

数据库这个复杂的软件是构建在硬件之上的,硬件是决定性能的基础,但是 CPU 有不同的型号,网络带宽有高有低,磁盘的吞吐和 IOPS 也有很大的差别,有可能一些软件层面的优化只对某些硬件环境生效。

5 稳定性之难

一款数据库的成熟,主要体现在稳定性上,而打造一个稳定的数据库有哪些难点呢:

SQL 是声明式的

  • 机器可以生成成千上万行的 SQL,SQL 各种算子和函数的组合是无法穷尽的,要保证任何一条 SQL 没有 bug,是极其困难的
  • SQL 里面的 Null 和 Nullable 是比较恶心的,不仅对性能有很大影响,对正确性也有比较大的影响

成百上千的用户场景

每个用户的硬件配置,环境信息,应用特点不一样,都可能引发不同的问题。

各种各样的集群规模

很多时候,一些稳定性,扩展性问题只有当集群规模达到一定程度,当数据量大到一定程度,当并发高到一定程度,才会暴露出来,而如何在产品发布之前在有限的资源下,通过测试暴露这些问题,也是一个难点。

功能的组合

很多时候,一个功能单独 Work 没有问题,但是多个功能相互影响时,一些 Bug 才会暴露出来,比如节点下线,触发数据的均衡和复制,又会触发数据版本的问题,进而触发查询的问题;比如导入,查询,Compaction,系统任务对 IO 资源的共同影响,这些复杂功能组合时的测试难度会更大。

函数的预期行为没有标准

比如 Date 类型和数字的比较,字符串和数字的比较,应该是直接报错,还是隐式转换,隐式转换的公共类型转成啥。 这些其实都没有统一的标准,每家数据库都有自己的实现。关键问题是即使我们改成最合理的表现,一些用户可能会因为习惯自己熟悉的数据库的表现,觉得最合理的表现是不合理的。

还有聚合函数溢出后行为,Decimal 运算精度的确定,函数一些异常行为是抛异常还是转 Null, 我们不仅要兼容用户期望的行为和正确性,还要兼顾正确性和性能。

多版本维护

由于快速迭代和发展,我们必然要维护很多版本,这给稳定性提出了更大的挑战:

  1. 用户升级到最新版本会越来越慢
  2. 定位,复现问题成本会更高,比如研发得用指定版本的代码部署环境,复现问题
  3. 解决问题后,也必须给所有版本 Cherry-pick,这时候很容易遗漏某个版本
  4. 要确定某个 Bug 到底在哪些版本修复了,成本也比较高
  5. 一个某个版本发生大的改动或者重构,之后的 Bug Fix PR Cherry-pick 就无法自动化,人工操作的成本会很高

兼容性问题

当我们维护很多个版本后,有时候不得不做一些不兼容的改动:

  1. 之前的行为本身是错误的或者不合理的
  2. 想完全删除某些旧代码

查询层是无状态的,所以不兼容的改动一般可以绕过或者改 SQL 解决,但是存储层和元数据层因为是有状态的,一旦有不兼容的问题会很麻烦。

不同语言的行为不一致

对于类似 StarRocks 这种多进程,多语言的数据库,在稳定性问题上还有一个额外的挑战是:

  • 不同语言的一些标准库行为会不一样,这会导致在不同模块计算相同函数的结果不一致
  • 之前遇到不同语言 Java 和 C++ 取余的结果不一致

这几年来,我遇到过挺多这样的 Case。

编译器的 Bug

一般编译器被各种项目大量使用,我们开发者遇到 Bug 的机会比较少,不过在开发 StarRocks 向量化的过程中,我就遇到了一个编译器的 Bug: 编译器进行向量化优化后,把 Boolean 值转成了 255,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97255。 还有最近我们同学遇到的一个 C++ 正则标准库的 Bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86164#c8

分布式相关的问题

现在的数据库几乎都是分布式数据库,所以分布式系统遇到的问题,一般数据库都会遇到,比如分布式系统的常见挑战:单点故障,部分失败,不可靠的网络,不可靠的时钟。

硬件故障

数据库的存储引擎必须保证数据能被正确的存储和读取,但是我们在实际环境中不可避免的会遇到各种硬件故障:磁盘坏掉,磁盘数据错误,服务器宕机,机房断电,网络异常等。

6 生态之难

数据导入

  • 数据格式的多样性:CSV,JSON,Parquet,ORC, ARROW 等
  • 数据源的多样性:Local File, Kafka,Hive,数据湖,传统数据库等
  • 导入时支持 Transform (表达式计算)

数据导出

  • 导出格式的多样性
  • 导出位置的多样性:客户端,本地,分布式存储等
  • 全量导出和部分导出

BI 工具:数据可视化

市面上流行的 BI 工具多达几十种,每种 BI 工具深入使用后都或多或少会有些兼容性问题:Session 变量的兼容性;数据类型的兼容性;函数的兼容性等,要都完美兼容,必然有大量的人力投入。

数据迁移

要支持用户从传统数据库和各种竞品数据库顺滑迁移,我们就必须和各种数据库进行对接,这里面有大量琐碎的工作。

比如我们想将 Presto 的用户迁移到 StarRocks 上,我们就需要在 SQL 语法层进行兼容。

比如单机数据库上的一些很容易实现的功能,在分布式数据库上就会相对比较困难,这时候如果为了功能对齐,就需要不少的工作。

7 安全之难

安全包括:认证,鉴权,审计,加密,脱敏等,这里面每一项都有着大量的工作,对于金融,政府客户,安全体系要求很高;在公有云上,安全要求会更高。

8 易用性之难

在未来,毋容置疑,易用性会越来越重要,将会是一款数据库的核心竞争力,数据库分析师需要知道的数据库知识会越来越少,需要进行的操作会越来越少。 易用性主要体现在 3 个层次:

  1. 架构层面的易用性:比如 自适应执行,Automatic Clustering, Automatic Index, Automatic Scale 等。
  2. 产品层面的易用性:比如接口定义和功能上是不是足够简洁清晰,一个命令可以导入各种数据源,各种数据格式的文件,一个命令可以查询可以数据源,文件等。
  3. 细节层面的易用性:文档是否完善,报错信息是否清晰易懂,可观测性是否足够好。

难点如何解决

当你将这些点一个一个深入思考下去,你肯定会理解打造一个强大且成熟的数据库是多么困难,那么这些难题如何解呢?我之后会用几篇文章讲讲我的一些思考,欢迎关注我的微信公众号,第一时间获取文章推送。


欢迎来知识星球和我交流