为性能而设计 Neki

为性能而设计 Neki

💡 原文英文,约3400词,阅读约需13分钟。
📝

内容提要

Neki在PostgreSQL客户端与数据库之间引入路由层,核心设计是“尽量不做无用功”。它不预先解码所有结果,而是保留原始PostgreSQL协议字节,按需惰性处理:点查询直接透传字节;LIMIT仅需行边界;排序只解码排序键;投影通过复制列帧实现,无需解码;仅聚合等必须计算时才生成新值。这种分层可组合设计大幅减少解码与序列化开销。

🔎

延伸解读

惰性解码:按需处理而非全量解析

Neki 的核心优化是惰性解码:它不预先将所有结果解析为内部类型,而是保留原始 PostgreSQL 协议字节,仅在查询需要时解码特定部分。例如,点查询直接透传字节,LIMIT 只需行边界,排序只解码排序键,投影通过复制列帧实现。这种设计避免了不必要的解码和序列化开销,尤其适合路由层无需理解全部数据的场景。

与 Vitess 的对比:协议字节保留的优势

Vitess 在组件间使用 protobuf 传输结果,并在边界处将所有行转换为 [][]Value,即使路由器不查看任何值。Neki 则不同,其 ExecuteResponse 主要包含原始 PostgreSQL 响应字节,避免了为每行创建 protobuf 对象、长度切片和值对象。这种对比凸显了 Neki 在减少不必要工作上的设计选择,尤其适合需要高性能的路由场景。

分层可组合:灵活应对不同查询需求

Neki 的 DataRows 支持多种表示:原始字节切片、行切片或指针切片,仅在需要时创建下一层。行可以同时保留原始消息和缓存已解码的值,例如排序时缓存 created_at,而其他列保持未解码。这种分层设计允许针对查询的不同部分深入不同层次,而无需强制所有列都进行解码,从而在复杂查询中也能最小化工作量。

正确性与性能的平衡:保留原始格式

Neki 保留 Bind 参数的原始长度前缀表示,不仅为了性能,也为了正确性:解析和重新序列化可能改变字节(如浮点 NaN 载荷)。对于扩展类型,路由器可能无法求值,但可以传递协议字节。此外,当 client_encoding 变化时,文本参数需要重新编码。这些细节表明,在追求性能的同时,Neki 也注重保持数据的精确性和兼容性。

❓

Q&A

Neki 在 PostgreSQL 客户端和数据库之间加了路由层,为什么性能还能比传统方案好?

Neki 的核心设计是“尽量不做无用功”。它不预先解码所有结果,而是保留原始 PostgreSQL 协议字节,按需惰性处理:点查询直接透传字节;LIMIT 仅需行边界;排序只解码排序键;投影通过复制列帧实现,无需解码;仅聚合等必须计算时才生成新值。这种分层可组合设计大幅减少解码与序列化开销。

Neki 和 Vitess 在处理查询结果的方式上有什么主要区别?

Vitess 在 RPC 边界将 protobuf 结果全部转换为 [][]Value,每个非 NULL 列都会生成 Value,即使路由器从不查看这些值。Neki 则保留原始 PostgreSQL 协议字节,ExecuteResponse 主要是一个字节串,避免为每一行创建 protobuf 对象、长度切片和列值,只在查询需要时才进一步解码。

Neki 如何处理带 LIMIT 的查询?

对于带 LIMIT 的查询,Neki 需要知道已发送的行数,因此不能将响应视为不透明字节切片。Limit 调用 DataRows.Len(),首次调用时遍历原始响应并计数 DataRow 消息,以便一次性分配行切片;第二次遍历创建指向原始响应缓冲区的 DataRow 包装器。然后 Limit 构建指针切片来选取请求的行窗口,无需解码任何列值。

Neki 在排序查询中如何避免解码所有列?

排序时,Neki 需要合并各分片的结果以形成全局顺序,因此必须解码排序键列。它先像 LIMIT 一样获取行边界,然后通过读取列长度跳过不需要的列,定位到排序键列的字节并解码缓存。其他列(如 id、account_id)仅定位帧但不解码,排序合并后生成指向原始行的指针切片,不复制行数据。

Neki 如何在不解码的情况下移除查询结果中的多余列?

Neki 使用 SimpleProjection 将投影存储为相邻输入列的连续段(runs),每个段包含起始和结束列。执行时,ProjectInto 按段复制列帧的字节范围,并写入新的 DataRow 头(列数更新)。这样只需复制需要的列帧,无需解码和重新序列化值。例如,移除 created_at 列时,只复制 id 和 account_id 的帧。

Neki 在什么情况下必须解码并重新计算值?

当查询需要跨分片聚合时,例如 SELECT status, count(*) FROM orders GROUP BY status,Neki 必须处理分组和计数。它需要将分组键和部分计数作为值来合并,最终计数是新计算的值,没有原始 PostgreSQL 字节可复制。此时查询必须经过所有层:响应消息 -> 结果批次 -> 行 -> 列帧 -> 类型化值 -> 新计算值。

🏷️

标签

➡️

继续阅读