引言

Vitess 是一个支持 PlanetScale 的开源数据库,其提供的 SQL 计算引擎最初是通过直接操作解析器生成的 SQL 抽象语法树(AST)来实现的 AST 评估器。在过去的一年中,我们逐步用一个虚拟机替换了原始 AST 评估器。尽管新虚拟机用原生 Go 编写,但其性能与 MySQL 的原生 C++ 评估代码非常接近。值得注意的是,这个新的虚拟机不仅速度更快,还比原始 Go 解释器更易于维护。让我们来回顾一下我们在实现这些惊人结果时所做的设计选择。


什么是 SQL 计算引擎?

Vitess 的设计目的是支持无限制的水平扩展。为实现这一目标,所有发送到 Vitess 集群的查询必须经过 **vtgate**。由于你可以部署任意数量的 vtgate 实例(它们本质上是无状态的),这允许你的集群容量线性扩展。

vtgate 的职责

每个 vtgate 的工作是整个分布式系统中最复杂的一部分:

  1. 它解析传入查询的 SQL;
  2. 创建一个分片感知的查询计划,这些计划会在集群的一个或多个分片中执行;
  3. 然后聚合查询结果,并将结果返回给用户。

Vitess 的实际表现(无论是在性能上还是在易用性上)如此出色的原因之一是,集群中的每个分片都由一个真正的 MySQL 实例支持。即使是更复杂的 SQL 查询,也可以分解为在底层 MySQL 数据库中执行的简单语句。因此,这些查询的结果始终与直接查询 MySQL 的预期结果匹配。

当查询变得复杂

然而,实际场景中的 SQL 查询可能非常复杂。我们需要支持几乎所有 MySQL 实例所支持的查询类型,并且需要跨多个 MySQL 实例进行评估。这意味着有时我们无法完全将评估工作交给 MySQL。
例如以下一个相对简单的查询:

SELECT inventory.item_id, SUM(inventory.count), AVG(inventory.price) AS avg_price
FROM inventory
WHERE inventory.state = 'available' AND inventory.warehouse IN ?
GROUP BY inventory.item_id
HAVING avg_price > 100;

假设这个查询在一个 Vitess 分片集群中执行,inventory 的数据可以存在于任意分片中。我们的查询规划器会准备一个查询所有分片的计划,将部分聚合(SUM 和 AVG)下推到 MySQL,随后在 vtgate 中本地执行聚合。而 WHERE 子句中的 statewarehouse 检查可以直接在每个分片的 MySQL 实例中执行。 但 HAVING 子句中的 avg_price > 100 仅适用于聚合结果,而该结果只在 Vitess 中可用,此时需要使用 Vitess 的 SQL 计算引擎。

Vitess 的计算引擎职责

Vitess 的计算引擎是一个解释器,它支持 MySQL 使用的 SQL 方言中的大部分标量表达式。这包括:

  • WHERE 子句
  • GROUP BY 子句中的条件

任何无法通过查询规划器下推到 MySQL 执行的 SQL 语句都会在本地通过 Go 引擎执行。


为什么执行速度至关重要?

SQL 是一种动态语言,充满各种微妙之处,而 MySQL 的 SQL 更是如此。为了完全匹配 MySQL 的行为,我们花费了大量时间处理 SQL 评估的各种边界情况。事实上,我们的测试套件和模糊测试工具非常全面,以至于我们经常发现 MySQL 原生评估引擎中的缺陷,并需要解决这些上游问题(例如[这个排序问题](链接)或[这个插入函数问题](链接))。
准确性固然重要,但在处理大多数查询时,这些表达式会针对每返回行至少执行一次甚至多次。为避免额外开销,评估必须尽可能快速。
Vitess 评估器的第一个版本是基于 AST 的解释器,它直接在我们的解析器生成的 SQL AST 上运行。这种设计直接简单,允许我们专注于准确性,但牺牲了性能。接下来让我们讨论这个解释器如何被一个更快且更易维护的虚拟机所取代。


解释器设计的演变

三种动态语言运行时的执行方式:

  1. AST 解释器:将语言语法解析为 AST,然后通过递归遍历 AST 节点进行评估。Vitess 的初版本正是基于 AST 的解释器。
  2. 字节码虚拟机(VM):先将 AST 编译为二进制字节码,再由虚拟机(模拟 CPU 的代码段)评估字节码。Vitess 近期发布的新版本使用的是这种方法。
  3. 即时编译器(JIT):将字节码直接编译为本机 CPU 指令,以便直接由 CPU 执行而无需虚拟机进行解释。

升级到字节码 VM 看起来是否合理?人们可能认为,SQL 表达式的动态类型非常复杂,并且 SQL 是一种非常高级的语言,操作原语很少,评估过程是线性的,没有循环且条件语句较少。因此,传统观点认为将 AST 评估转换为字节码 VM 或许意义不大。然而,实际表现显示这只是表面判断。


Vitess 全新虚拟机设计的实现

快速执行字节码:基于类型的优化

SQL 表达式的复杂性包括不同类型的动态操作,例如 a + 1 的实现方式取决于 a 是整数、浮点数还是字符串。通常需要运行时类型检查以决定如何执行操作。通过静态类型检查的设计,Vitess 能够在字节码生成阶段使用查询规划器的类型信息,对字节码指令进行类型优化,无需运行时重写。

基于函数指针的 VM 设计

虚拟机采用基于函数指针的设计,每条指令表示为回调函数。程序由这些函数指针组成,当执行字节码时直接调用这些函数。这种设计简化了虚拟机的实现,同时避免了复杂的分支跳转逻辑。同时,Go 的闭包支持允许直接捕获指令参数,使语言指令拥有强大的表达能力。


性能提升与比较

Vitess 的新 VM 不仅显著提升了 SQL 表达式的执行性能,还保持了易维护性。在基准测试中:

  1. 与原始 AST 解释器相比,新 VM 的 SQL 计算速度快了 **20 倍**;
  2. 在大多数场景下,新 VM 的性能已与 MySQL 的 C++ 原生评估引擎持平。

此外,新 VM 无需分配内存即可完成评估,这得益于静态类型检查和专用指令的设计。


总结

Vitess 新的虚拟机设计证明,Go 服务可以在保持轻量级的开发和部署优势的同时,克服其性能限制。通过引入基于函数指针的 VM 设计和静态类型检查,Vitess 的 SQL 计算引擎不仅性能惊人,还易于开发和维护,为分布式 SQL 解决方案树立了新的标杆。



提升 Go 中的解释器:追赶 C++ 的步伐插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://folen.top/2025/09/14/go-c/