共计 6654 个字符,预计需要花费 17 分钟才能阅读完成。
本文整顿自 OpenMLDB PMC 陈迪豪在 2023 Qcon 寰球软件开发大会 AI 基础架构论坛上的发表的演讲实录。
心愿大家通过本文可能理解三个方面的内容:前沿的 AI 数据库架构设计、数据库内存优化思路和实现细节以及 OpenMLDB 内存优化在 AI 场景的实际。
本文目录:
- Al 数据库与内存性能优化
- OpenMLDB 与 Spark 内存计划
- OpenMLDB 对立编码优化实现
- 内存优化在 AI 场景的利用实际
AI 数据库与内存性能优化
什么是 AI 数据库
随着 AlphaGo 到 chatGPT 等越来越多的 AI 利用的落地,为了应答越来越多的 AI 的需要,AI 的基础设施我的项目也越来越多,涵盖硬件芯片设计、机器学习框架,以及针对 AI 工程化落地的数据库,其中包含 OpenMLDB 和一些向量数据库。
AI 数据库是针对机器学习和 AI 开发的数据库,已逐步成为 MLOps 的重要组件,反对包含离线和在线流程在内的 AI 利用的落地。
OpenMLDB 介绍
第四范式公司 AI 数据库的倒退:
- 2017 年开发以 FE 为外围的数据库服务
- 2020 年落地超过 100 个机器学习场景
- 2021 年开源 OpenMLDB 数据库我的项目
OpenMLDB 是专一于解决 AI 工程化落地的一种数据库,与传统数据库略有不同。它既不是像 Redis 或 MySQL 那样的在线数据库,也不是 OLAP 或 OLTP 数据库。相同,它是一种联合了离线和在线计算的数据库,能够满足机器学习工程化各种需要。OpenMLDB 的特点如下:
- 致力于解决 AI 工程化落地的数据治理难题
- 选用 SQL 和数据库开发体验升高开发门槛
- 人造保障线上线下计算一致性,实现毫秒级的计算提早
OpenMLDB 的应用
AI 数据库的应用流程与传统的数据库简直没有区别。首先,OpenMLDB 有 CLI,提供建库建表等接口,能够插入或加载数据,这些性能与常见在线数据库十分类似。
然而 OpenMLDB 有一个独特的离线特色计算性能,能够对海量的特色和原始数据进行离线计算,例如存储在 HDFS 或 Hive 数仓中的数百 TB 的数据,对这些数据能够提交分布式离线计算工作。
用于离线特色计算的的 SQL 计划能够间接上线,一旦 SQL 上线,它就成为了一个在线的 IPC 服务,能够让客户端调用该服务传递原始数据输出,并返回通过特色计算后的后果。当后果返回后,工作能够将特色集成到 TensorFlow、PyTorch 等模型推理服务中,从而实现一个端到端的机器学习的落地的利用。
OpenMLDB 架构设计
OpenMLDB 的架构设计包含离线特色计算局部和在线实时引擎,这两局部通过一个对立的一致性执行打算生成器实现一致性。
这个一致性执行打算生成器涵盖了一致性的引擎 ASTTree parser,该引擎解析 SQL 语法和词法,生成离线和在线申请打算,实现逻辑打算生成、逻辑打算优化、物理打算生成、物理打算优化等操作。
在这个对立执行引擎中,咱们应用了 OpenMLDB 提供的 SQL Parser 和 Validator 进行校验。同时,咱们还应用了 planner 来生成逻辑打算和物理打算,并对其进行优化。因为咱们应用的编程接口是 SQL,因而有很多优化空间,比方表达式下推、拼表、转重排等工作都能够在这个阶段实现。实现编程后,用户须要应用 Codegen,它能够为不同的硬件平台(例如 Mac、X86 的机器或 ARM 架构极其)生成不同的代码。最初,咱们应用一个执行器来治理行的编码器,对立 Schemas 治理、状态治理和迭代器等性能。
OpenMLDB 内存架构
- OpenMLDB 的数据是以行编码的 。传统的数据库像是 MySQL 应用的数据编码也是行编码。行编码的益处是同一行随机查问的时候会十分快,在一行内的列都是应用的间断内存。这个设计对 OpenMLDB 的在线查问性能十分重要。Spark 尽管也是离线计算,但 Spark 外部反对读取 Parquet,而 Parquet 属于列存储,Spark 读到 Parquet 后,它在外部也会转成一个行编码的格局,不便后续做数据的迭代和查问。
- OpenMLDB 离线和在线应用雷同的 Parser、Optimizer 和 Codegen。这是为了确保用户写的每一个表达式和生成打算都达到离线在线对立,从而生成一个 C 语言的函数代码,这个代码再依据不同的硬件平台编译成机器码。离线和在线对立应用同一套优化后的硬件码执行,这可从根本上保障它的特色一致性。
- OpenMLDB 用的技术为 LLVM JIT,对表达式生成平台相干的优化执行代码 。JIT 代表 Just In Time compiler,无需预编译,它是把表达式放到云端,应用 LLVM 后,间接在代码里对表达式做编码,而后生成跟平台无关的优化执行代码。
- 离线集成 Spark,基于 Java JNI 调用 C++ 代码接口 。因为离线的数据是海量的,要求大吞吐,必须反对分布式地执行。咱们的离线集成了 Spark 和 Flink 的批处理。此外,OpenMLDB 是基于 Java 的 JNI 去调 C++ 的代码接口。
总结一下,从性能角度思考,OpenMLDB 数据是以行编码的办法来存储的。为了保障离线在线的一致性,OpenMLDB 相当于用 C++ 写了一套对立的 SQL 编译器,再应用 LLVM 做代码生成。对于离线的集成,咱们集成了 Spark 和 Flink 的批处理,因为 Spark 是基于 JVM 的引擎,它只能通过 JNI 的办法调用 C++ 的接口。
思考一下
这个问题后续会进行解答。
OpenMLDB 与 Spark 内存计划
Spark 是大数据处理的事实标准,是所有大数据处理工具中不可或缺的一部分。作为一个分布式计算框架,从编程接口到计算性能方面,Spark 始终处于领先地位。然而,自 Spark 1.6 版本开始,其实现也遇到了计算瓶颈。这些瓶颈不仅来自硬件,也来自于代码逻辑自身。
为了解决这些瓶颈,Spark 引入了 Tungsten 内存优化计划,从最后应用简略的 Java 实现 row 格局,到实现分布式 RDD,再到最终应用 JVM 的 Unsafe 接口进行间断内存治理。这项优化计划还包含向底层指定级别的向量优化,从而进一步晋升了 Spark 的性能。
Spark Tungsten 内存优化
Tungsten 内存优化计划蕴含以下三点:
- 内存治理 。Tungsten 内存优化计划从新设计了 Spark 的 row 内存治理,采纳相似指针的计划来治理间断的内存,以优化列拜访和前面的 Codegen 计算。
- 内存优化 。官网博客中提到,Java 的字符串实现会导致内存节约。比方,一个四个字母的字符串 abcd,实践上只须要申请四个字节,但理论占用内存却可能达到 24 个字节。这是因为 Java 字符串实现中蕴含 12 个字节的 header,8 个字节的 hash 和 4 个字节的理论内容。Tungsten 的优化能够无效解决这个问题。
- Tungsten 的优化 。在优化前,Spark 的 row 实现是基于多个 column 对象的,每个 column 都是一个 Java 对象。这导致 JVM 治理的小对象特地多,GC 压力特地大。而 Tungsten 优化后,Spark 的 row 和 column 对象的生命周期其实是一样的,能够手动回收。这个信息过来是无奈通知 GC 进行优化的,只能将对象援用设为 0 期待 JVM 回收 GC 压力也比拟大。
Spark UnsafeRow 优化
Spark Tungsten 蕴含了 UnsafeRow 优化。
它基于 JVM 提供的一种 Unsafe 的 API。客户能够向 JVM 申请一段间断的内存,并自行治理该内存。然而,因为该内存不会主动开释,所以存在内存透露的危险。
Spark UnsafeRow 优化是将所有行转换为 UnsafeRow 对象。该行对象还蕴含内部的 schema 属性,还有一个指针,指向一个蕴含单行所有列的间断内存。Spark 通过指针和偏移来拜访用户须要的数据,例如读取的字节数、字节类型等。
此优化应用了行编码的 UnsafeRow,与 OpenMLDB 类似,它能够保障所需的数据在间断内存中,对于列的读性能很高。优化后,Spark Tungsten 能够缩小对小对象的治理和 GC 压力。例如,如果用户以前的一行有 100 列共 1 万行,它将具备 100 万个小对象,而当初不须要这么多小对象,内存对立由 Spark 来治理。
上图总结的是 Spark 的行格局,领有四列,每一列都是不同类型的数据,例如第一列是 int 类型,第二列是 string 类型,第三列是 double 类型,第四列也是 string 类型。在行的结尾,有一个 nullbitset,是一个 64 位的长整型,能够示意从 -(2 的 32 次方)到 +(2 的 32 次方)。
然而,在 int 或 long 中无奈示意 null。用户能够应用数字零示意有值,然而无奈应用 int 示意 null。因而,在许多架构设计中,包含 OpenMLDB 和 Flink 中的行都要反对 null 值。换句话说,无论用户存储的是什么,如果用户想示意 null,都必须应用一个独自的位来示意。零示意有值,而 1 示意 null,这就是 nullbitset。因而,个别须要应用多少位来示意 null 取决于行中有多少列。
有一个略微奇怪的中央是,行中的 int 在大多数操作系统实现中都是 32 位的,但在 Spark 中,它应用 64 位来示意。同样,因为字符串的长度可能是变长的,因而 Spark 中的字符串示意记录了大小和偏移量,用户能够在一般列类型的根底上,应用前面的变长区域来专门存储字符串内容。最初,用户能够依据偏移量和大小指针读取字符串内容。
然而,这里蕴含一些问题,例如,为什么 nullbitset 是 64 位?因为图表显示一共只有四列。实践上,四位就足够了。如果按最根本的单位,一个字节就能够了。然而,在 Spark 外部,为了读取访存不便,所有数据都依照 64 位来对齐。这意味着,无论是 int、double 还是 float,它们都应用 64 位,这会导致一些节约。另外,例如,UnsafeRow 没有版本信息,换句话说,如果内存构造发生变化,相应的代码也必须进行更改。还有一个问题,用户通过 row 指针无奈晓得行的大小是多少。用户只能像 Spark 一样,在内部有一个 Java 对象,专门保护这个 row 的长度。
总结如下:
在编写 Spark 代码时,通常应用其 DataFrame 对象进行操作,将其转换为 RDD 后,能够通过查问执行器对象(queryExecution)获取 RDD 的底层数据结构 internalRow,而其默认实现就是 UnsafeRow。通过将 internalRow 转换为 UnsafeRow 对象,能够不便地依照偏移量读取想要的值。这一点与咱们在 OpenMLDB 中进行的内存优化和内存对齐等操作密切相关。
然而,Spark UnsafeRow 也存在一些问题。首先,它的数据结构不够紧凑,尽管可能进步缓存性能,但也会造成一些内存节约。其次,UnsafeRow 没有版本信息,这可能会在代码降级后呈现兼容性问题。最初,查问执行器获取 RDD 列信息的过程会触发底层计算,这是一个已知的 bug,临时就不开展细说了。
Spark 比照 OpenMLDB Spark
Spark 是在版本 1.6 的时候就开始做的优化。2.0 的时候曾经十分稳固。下图是 OpenMLDB 和 Spark 3.0 的性能比照。纵坐标是运行工夫,OpenMLDB 在这种单窗口下计算性能能够晋升一倍,并且在多窗口下性能能够晋升五倍,它的运行工夫缩小到原来的 1/6。
此外,OpenMLDB 做了一些额定的歪斜优化,多线程当前,它的性能晋升可能更大。这个额定的歪斜优化也是 Spark 自身没有的局部。
能够看到,即便 Spark 做了这么多内存优化,缩小了 Java 的小对象,也通过了 UnsafeRow 的接口,然而它跟 OpenMLDB 纯 C 语言实现的代码在性能上还是有较大差别。
OpenMLDB 对立编码优化实现
本章节介绍 OpenMLDB 如何对接 Spark 性能优化。
OpenMLDB 行内存编码优化
- 和 Spark 一样,基于行存储,最大化在线行读取性能
- 相比于 Spark,基于 C++ 指针实现,没有 GC overhead
- 相比于 Spark,减少 Version header,反对多版本格局
- 相比于 Spark,Nullbitset 以 byte (8 bits) 为单位按需分配
- 相比于 Spark,不同类型按需分配空间,内存布局更加紧凑
这里展现 OpenMLDB 行的内存计划:
后面显示的是 6 个 byte 的 Header。其中因为它不会有超过 64 个版本,所以每个版本只须要一个 byte 来示意。这里用 32 位来示意一个 Size。BitMap set 是以 byte 为单位,最小是一个 byte。每个 field 外面的长短是能够变的。比方,一个 field 是 int,它只占 32 个 bit,Long 占 64 个 bit。前面同样也有个变长的一个存储字符串的区域。这个存储字符串的区域,OpenMLDB 也做了一个优化。它的每一个字符串,只有存下 offset 就能够了。所以它的 Size 其实是前面一个 offset 减去后面一个 offset,等于它理论的长度。而这就代表它把字符串的 Size 给优化掉了。此外,它不肯定须要用 32 位去示意 size 的长度。而是能够依据理论 row 的大小去算这个 offset 的值,不肯定是一个 32 位的 int。
Spark 与 OpenMLDB 行内存比照
下图是 OpenMLDB 与 Spark 的行内存的比照。几个特点包含:
- 内存更紧凑,反对多版本和蕴含整个行 size,所需存储的空间也更小。
- OpenMLDB Row,它跟 Spark 相比,多了一个 row size 和 header。然而它在内部不必独自存 size,而且 nullbitset 更小。所有的类型都是能够依据它理论的占用空间来示意。string offset 指针的地位用一个 byte 来示意,长度是 1。最终算进去的整个 row size 是 255 byte。这导致内存优化大幅度的缩小了靠近 45%。尤其是数据量越大,每一行所占的空间越少。这跟所列的类型无关。列数越多,能够节俭的内存空间也越多。
思考解答
回到后面的问题,计划是用 Spark 把数据读出来。Spark 的第一个 op 是从 Parquet 转成 UnsafeRow 的计算。它把 Spark 的数据转成一个 Spark CodeGen 代码反对的格局。但 C++ 代码怎么去读取转化后的格局呢?
答案是在离线引擎的架构下来反对 Spark 的数据格式。在离线引擎理论执行的时候去调用 Spark API,然而这外面的问题是两个零碎的内存格局自身并不兼容。而 OpenMLDB 提供了 C 的 API 是基于外部 row 的格局,Spark 提供的这个 RDD[Row] 和 RDD[internalRow],返回的都是 Scala 对象,基于 UnsafeRow 的格局。
OpenMLDB 离线引擎与 Spark 整合
兼容的计划有三个:
- GitHub 上代码外面默认的计划叫 encoder 和 decoder。
- 批改 Spark 的源码。然而这会导致前期不好保护,因为改变 Spark 底层的代码太多 。
- 批改 OpenMLDB 的代码,去兼容 Spark UnsafeRow 的格局。(采取计划)
OpenMLDB UnsafeRowOpt 优化
最初整顿反对性能优化的计划:
- 通过 execution engine,拿到 RDD[internalRow],转成 UnsafeRow。而后把这个 UnsafeRow 的指针传给 C 接口。如有须要,能够间接从 UnsafeRow 外面拿到列的值,把它转成 ByteArray 指针传递给 C 函数,就能够用 C 的办法去拜访。最初从测试后果来看性能晋升也是十分可观的。
- OpenMLDB 测试了十个场景,有些场景的列数特地的多,有些列数比拟少。在这能够看出加上 UnsafeRow 优化当前,这个运行的工夫从 500 多分钟降到 100 多分钟,大部分性能晋升都非常明显。
- 前面 OpenMLDB 也做了一些火焰图性能剖析。如果 OpenMLDB 不做优化,它的行计算占比是百分之三点多,占总额计算工夫是 3.4。加上性能优化后,行计算占比降到 1.43,整体工作的计算工夫缩小了。而且没有编码,开销也小很多。所以加上 UnsafeRow 优化当前,OpenMLDB 的整体性能会比 Spark 开源版本快很多。
内存优化在 AI 场景的利用实际
以举荐零碎为例,用户能够基于机器学习做建模,并且保障离线在线一致性。
写在最初
如果想进一步理解 OpenMLDB 或者参加社区技术交换,能够通过以下渠道取得相干信息和互动。
OpenMLDB 官网
https://openmldb.ai/
OpenMLDB GitHub 主页(更多动静请关注这里!)
https://github.com/4paradigm/OpenMLDB
OpenMLDB 微信交换群