[AI 协作笔记] gRPC 传输优化:基于 Flattening 与 Bitset 的高效方案
Source: Dev.to
背景与技术挑战
在设计数据库中间件的 API 时,我们通常需要返回多行的查询结果。若采用传统的 gRPC 定义方式,会面临以下性能与实现上的问题。
1.1 Payload 冗余问题 (Key Repetition)
最直观的 Protobuf 定义通常是将每一行数据定义为一个 Map 或 Object:
message Row {
map data = 1;
}
message Response {
repeated Row rows = 1;
}
问题分析
这种结构会导致严重的 Payload 膨胀。假设查询结果有 10,000 条数据,且包含字段 customer_id, created_at, status。在传输过程中,这些字段名称 (Key) 会被重复传输 10,000 次,占用了大量的带宽资源。
1.2 Protobuf 对 NULL 值的限制
Protobuf (proto3) 的设计哲学将标量类型 (Scalar Types) 视为不可为 null。
string字段若为NULL,传输时会被序列化为空字符串""。- 客户端无法区分这是真正的“空值”还是“原始数据库中的 NULL”。
虽然可以使用 google.protobuf.StringValue 等 Wrapper 类型解决,但会增加额外的 Message 嵌套层级与处理开销。
解决方案:类数据库底层架构
针对上述挑战,AI 建议跳脱传统的 API 对象思维,参考 数据库底层 (Columnar Storage) 或 ODBC/JDBC 驱动 的实现方式。核心优化策略包含以下两个部分:
A. 数组扁平化 (Flattening)
- Header (Metadata):单独传输一次字段定义 (
columns)。 - Body (Values):将所有数据值摊平成一个巨大的“一维数组” (
values)。
此设计完全移除每行数据中的 Key 传输,显著降低 Payload 大小。
B. Bitset (Bitmap) 机制
- 使用 Bitset 来标记每一个值是否为 NULL。
- 1 个 bit 对应 1 个值:Bit =
1表示该值为 NULL,Bit =0表示该值有效。
空间效率
每 8 个字段值仅需消耗 1 byte 的额外空间。对于 1,000 行 × 8 列的数据,仅需约 1 KB 的 Overhead 即可精确记录所有 NULL 状态。
实作细节 (Implementation Essentials)
3.1 Protobuf 定义 (proto/query.proto)
message QueryResponse {
repeated string values = 3; // 扁平化数值
repeated Column columns = 4; // 字段定义
bytes null_bitmap = 7; // NULL 标记位元流
int32 row_count = 6;
}
3.2 Server 端:编码与压缩 (Encoding)
Server 端的任务是一次性遍历数据库结果,同时完成“数值扁平化”与“Bitmap 生成”。
// $result['rows'] 是数据库返回的二维数组
$values = [];
$packedBytes = "";
$currentByte = 0;
$bitIndex = 0;
foreach ($result['rows'] as $row) {
foreach ($row as $value) {
if ($value === null) {
$values[] = ""; // 值放空字符串占位
$currentByte |= (1 << $bitIndex); // 设置对应位元为 1
} else {
$values[] = (string)$value;
// 位元保持为 0(默认)
}
$bitIndex++;
if ($bitIndex === 8) {
$packedBytes .= chr($currentByte);
$currentByte = 0;
$bitIndex = 0;
}
}
}
// 处理最后不足 8 位的情况
if ($bitIndex > 0) {
$packedBytes .= chr($currentByte);
}
3.3 Client 端:解码与还原 (Decoding)
Client 端收到数据后,需要根据 columns 数量切割,并参考 null_bitmap 将 NULL 还原回来。
$fetchedRows = [];
$columns = $response->getColumns();
$colCount = count($columns);
$values = $response->getValues(); // 取得扁平化数组
$bitmap = $response->getNullBitmap(); // 取得 Bitmap string
$rowCount = $response->getRowCount();
$bytePos = 0;
$bitPos = 0;
for ($r = 0; $r < $rowCount; $r++) {
$row = [];
for ($c = 0; $c < $colCount; $c++) {
$flatIndex = $r * $colCount + $c;
// 取得对应位元
$byte = ord($bitmap[$bytePos]);
$isNull = ($byte >> $bitPos) & 1;
$row[] = $isNull ? null : $values[$flatIndex];
// 移动位元指针
$bitPos++;
if ($bitPos === 8) {
$bitPos = 0;
$bytePos++;
}
}
$fetchedRows[] = $row;
}
通过上述对称的逻辑,我们即可以极低的运算成本完成数据的压缩与还原。
优化效益分析
采用此架构后,我们获得了以下具体效益:
-
极致的传输效率
扁平化设计使 Payload 大小与数据量呈线性增长,不受字段名称长度影响,对大数据量查询的带宽节省效果显著。 -
精确的类型还原
Client 端可通过null_bitmap精确还原数据库的 NULL 状态,解决了 gRPC 默认类型的限制。 -
解析性能提升
对于 PHP 与其他语言而言,处理一维数组通常比处理大量嵌套对象拥有更好的 CPU Cache 命中率与更低的内存碎片。
总结
这个优化案例展示了在现代分布式系统中,适度引入 底层系统设计思维 的重要性。通过与 AI 的协作,我们跳脱了单纯的 API 设计框架,利用 位元运算 与 数据结构优化,以极低的成本解决了 gRPC/Protobuf 在数据库应用场景下的先天限制。