☰
数据库加速芯片实战:从CPU卸载到FPGA的算子重构
2026/10/22 2:33:14 网站建设 项目流程

简介:一份聚焦异构计算框架下数据库加速专用芯片的技术解析文档,面向数据库工程师、芯片架构师及关注算力演进的技术人员,系统梳理从冯·诺依曼架构、摩尔定律失效到领域专用计算架构(DSA)的发展脉络,并对DPU、KPU等专用芯片在数据处理中的价值做了深入阐述。资源为单个PDF文件,压缩包约48.51MB,文件仅1个PDF文档,内容完整、图文并茂,适合直接阅读或作为内部技术分享素材。文档内容涉及算力系统从通用CPU向专用异构加速器演进的动因,数据库处理中的I/O瓶颈、内存计算、FPGA阵列与专用处理器芯片的对比分析,以及中科驭数等企业的实践方向,还引用了李国杰院士、ISCA等权威观点。该资源已有174人浏览学习,虽然热度不算高,但对理解智能时代算力基础设施变革极具参考价值。通过阅读,读者可理解通用处理器与专用加速器的职责分化,把握数据库性能瓶颈的根源及异构计算在云计算、边缘计算、AI场景下的落地路径,并获得关于算力系统演进与专用芯片机遇的体系化认知。

1. 数据库加不动了:为什么要把算子从通用CPU挪到专用芯片上

这几年做数据库加速的团队,很多都撞过同一堵墙:分析型查询跑不动,CPU打到 100%,但磁盘 IO 和内存带宽明明还有富余。查了很久才发现,问题不在数据读不出来,而在哈希连接、聚合、排序这些执行期算子把 CPU 流水线堵死了。异构计算框架下的数据库加速专用芯片,就是用来治这个病的——它把最贵的那几个算子从 CPU 卸载到 FPGA 或 ASIC 上,用定制的硬件流水线重写数据路径,CPU 只负责查询计划和串行逻辑。这个方向不是实验室玩具,适合任何被复杂查询性能卡住的分析型数据库、数据仓库或实时数仓场景。听起来很美,但落地时有一大堆坑,PCIe 带宽、设备内存容量、同步开销都会把收益偷偷吃掉,这篇文章就沿着一条可验证的路径把这个方向拆透。

2. 从通用 CPU 到异构加速:数据库算子卸载的硬件选型与系统代价

2.1 数据库负载里最耗 CPU 的三个位置:哈希连接、聚合、排序

先翻数据库内核的 profile,大部分分析型查询的执行时间都花在这三件事上。哈希连接(hash join)是最典型的:对右表建立哈希表,然后逐行拿左表的 key 去探测,这个过程内在逻辑就是随机访问内存。现代 CPU 虽然主频高,但一次 cache miss 就要付出几十甚至上百纳秒,哈希连接本质上是把 CPU 的流水线打断器按在地上摩擦。另一个大头是聚合运算,尤其是 GROUP BY 有大量维度的场景,维护哈希表、比较 key、内存读改写,每一步都在跟 cache 和分支预测作对。排序也好不到哪去,多路归并排序的外排阶段频繁回读写,CPU 在这里的收益很低。

既然瓶颈在算子,最直接的想法就是让硬件来重写这些算子的数据流。CPU 的核心优势是通用和乱序执行,但数据库算子是高度规律的批量数据流,每一步做什么都是确定的,这正好是专用硬件擅长的事情。GPU 看起来也能做,但实践中你会发现一个问题:数据库算子是强依赖链的,左右表要分多次搬进显存,中间结果集动不动几十 GB,显存放不下。GPU 适合计算密度极高的场景,而数据库加速更需要的是一块能接在 PCIe 上、以流式方式处理大批量元组数据的定制计算单元。FPGA 和 ASIC 在这个位置上有天然的结构优势。

2.2 FPGA 还是 ASIC:第一代加速器怎么选

这个问题几乎每个做数据库加速的团队都会吵一轮,而且往往要等到板卡画完才发现选错了。我的经验是:商用产品最终形态大概率是 ASIC,但第一版老老实实用 FPGA。

对比维度FPGAASIC
开发周期3–6 个月可出原型18 个月起步,流片失败成本极高
功耗单卡 15–40W同等性能可压到 10W 以内
灵活性可重编程,算子逻辑可迭代功能锁定,改一版要重新流片
量产成本单卡成本高,适合小批量量越大越便宜,前期 NRE 费用高
硬件调试可抓内部信号,可在线升级只能靠外部观察,排错难

第一张加速卡用 FPGA 有三个现实理由:一是哈希连接、聚合这类算子的数据格式和谓词类型还在调整中,FPGA 可以在不上电的情况下改逻辑;二是硬件驱动的软件栈没有成熟前,你需要在板卡还在开发时同步写主机侧的驱动和运行时,FPGA 的仿真和在线调试能力是唯一能拉通整个链路的工具;三是 ASIC 的 NRE 动辄上千万,项目没验证完收益之前没人敢签字。

什么时候确定要切 ASIC?三个条件同时满足:查询数据格式已经稳定一年以上,加速占整个查询链路的时间比例超过 40%,而且你有明确的批量出货量。否则 FPGA 的成本和功耗差距不足以抵消灵活性带来的风险。注意,FPGA 的灵活性不是无限的——时钟频率低、片上存储有限,很多在 CPU 上轻松的算子(比如复杂的表达式求值)放到 FPGA 上会非常痛苦,这一点后面避坑章节会重点展开。

2.3 异构系统的第一笔账:PCIe 带宽与数据搬运

不管选 FPGA 还是 ASIC,数据都要从主机内存穿过 PCIe 到达加速卡。这一步搬运成本是异构加速里最容易被忽略、也最致命的开销。一块普通的 PCIe Gen3 x16 加速卡,理论带宽 16GB/s,实际 DMA 传输只能跑到 10GB/s 左右,如果是小包传输,可能连 6GB/s 都到不了。一次查询要把左右表搬过去、结果搬回来,总搬运时间大约是数据量的 2 倍除以有效带宽。

动手做卸载决策前,我会先算一笔账,公式很简单:

T_accel = T_transfer + T_exec T_transfer = 2 * D / B_eff

其中 D 是参与运算的表和中间结果总大小,B_eff 是实测的有效 DMA 带宽。只有当 T_accel 明显小于 CPU 上的原始执行时间(我通常要求收益比大于 3 倍)才值得卸载。举个例子:一张 4GB 的事实表和 128MB 的维度表做哈希连接,CPU 实测 3.2 秒,卸载到加速卡设备端执行约 1.2 秒,搬运耗时约 0.85 秒,总账是 2.05 秒,收益 1.15 秒,值得做。但如果同一张表被过滤条件筛得只剩 64MB,搬运耗时只要 13 毫秒,CPU 跑也只要 80 毫秒,卸载只会越帮越忙。判断准则就一条:算子的计算时间和数据搬运时间的比值。计算密度越高、数据复用率越高,越适合卸载。

3. 把哈希连接算子卸载到 FPGA:一个能跑通的最小实现

3.1 哈希连接卸载的整体架构

哈希连接是所有数据库算子中最适合做硬件卸载的,因为它的访问模式可以被硬件流水线高度定制。主机侧跑查询计划,执行到哈希连接算子时,把左右表的数据流式搬进加速卡。加速卡内部一般分成四个单元:DMA 引擎负责从主机内存收数据,哈希构建核读右表建哈希表,哈希探测核拿左表的 key 去查表,最后是结果回写引擎把匹配结果 DMA 回主机。

这套架构的关键在于构建和探测两个阶段可以流水线重叠。右表数据还在 DMA 传输时,哈希构建核就可以边收边建;探测核只需要等构建核完成构建部分后再启动。设计时尽量让三个单元并行工作,否则加速器大部分时间都在空转。整个数据通路用 AXI Stream 接口串起来,每个单元只关心自己收到和送出的元组格式,单元之间用 FIFO 解耦。

数据格式要先定清楚。一个元组在设备端至少需要 key 和 payload 两个字段,key 是连接列,payload 是查询计划中后续需要的列投影。把整行都搬进设备是最浪费的做法,应该在上游先把投影做完,只送必要字段。常见做法是在数据库侧做一次投影裁剪,把每行宽度从 200 字节压到 24 字节以内再交给 DMA。

3.2 用 HLS 写哈希构建核

我用 Vitis HLS(Xilinx 的高层次综合工具)来写这个核,代码风格是 C/C++ 加 pragma。下面是一个经过裁剪的教学级实现,重点展示数据结构设计和流水线节奏,生产版本会在这个基础上加溢出处理和内存分层。

#include <ap_int.h> #include <hls_stream.h> #define NUM_BUCKET (1U << 20) // 桶数,取 2 的幂便于位运算取模 #define SLOT_DEPTH 8 // 每个桶的 slot 数,线性探测深度 typedef ap_uint<64> key_t; // 连接键类型,兼容 bigint typedef struct { key_t key; ap_uint<32> payload; // 携带的投影字段 } tuple_t; // 哈希构建核:从输入流读取元组,写入桶表 void hash_build(hls::stream<tuple_t>& in, hls::stream<uint32_t>& overflow) { #pragma HLS INTERFACE axis port=in #pragma HLS INTERFACE axis port=overflow #pragma HLS INTERFACE s_axilite port=return // 桶表放在板上 URAM/BRAM,这里用局部数组简化表示 key_t bucket_key[NUM_BUCKET * SLOT_DEPTH]; ap_uint<32> bucket_val[NUM_BUCKET * SLOT_DEPTH]; #pragma HLS RESOURCE variable=bucket_key core=RAM_S2P tuple_t t; uint32_t idx, base; key_t folded; for (bool cont = !in.empty(); cont; cont = !in.empty()) { in.read(t); // 64 位 key 折成 32 位再做桶索引,降低高半段全零时的分布不均 folded = t.key ^ (t.key >> 32); idx = (uint32_t)(folded & (NUM_BUCKET - 1)); base = idx * SLOT_DEPTH; // 线性探测:找到空槽则写入,否则计数溢出 for (int i = 0; i < SLOT_DEPTH; i++) { #pragma HLS UNROLL if (bucket_key[base + i] == 0) { bucket_key[base + i] = t.key; bucket_val[base + i] = t.payload; break; } if (i == SLOT_DEPTH - 1) { overflow.write(1); } } } }

这里有两个参数直接影响性能和容量。NUM_BUCKET 的经验值是右表行数的 1.2 到 1.5 倍,向上取 2 的幂,这样 (key & (NUM_BUCKET-1)) 就能代替取模运算,在硬件上少一个除法器。SLOT_DEPTH 设为 8 是权衡的结果:太浅冲突溢出率高,哈希表空间利用率低;太深 BRAM 占用直线上升,综合频率下降。8 在绝大多数数据分布下能把溢出率压在 2% 以下,我还是建议以实际数据集的 key 分布为准,不要无脑照抄。

折叠异或 t.key ^ (t.key >> 32) 是容易漏掉的细节。数据库主键往往高位没变化,比如自增 ID 的前 32 位几乎全是 0,如果直接用低 32 位做索引,大量数据挤在相邻桶里,冲突率会非常难看。先异或折叠再取模能让分布接近均匀。这个动作在 CPU 上要花好几个周期,在 FPGA 上只是一个组合逻辑 XOR,成本几乎为零。

3.3 主机侧驱动:用 ioctl 提交数据块并收结果

加速卡得有主机侧驱动来承接数据提交。第一版驱动可以做得粗,但接口设计要往生产方向靠。我先用 ioctl 串通整个链路,这样做的好处是内核态代码少,调试简单。下面这段是驱动中提交一批数据到加速器的核心路径。

#include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdint.h> #define ACCEL_JOIN_BATCH _IOW('A', 1, struct accel_join_cmd) struct accel_join_cmd { uint64_t src_left; // 左表源数据的 DMA 地址(物理地址) uint64_t src_right; // 右表源数据的 DMA 地址 uint64_t dst_out; // 结果回写的 DMA 地址 uint32_t left_rows; // 左表元组数 uint32_t right_rows; // 右表元组数 uint32_t batch_id; // 用于多查询并发区分任务 }; int accel_join_submit(int fd, struct accel_join_cmd *cmd) { if (ioctl(fd, ACCEL_JOIN_BATCH, cmd) != 0) { perror("ioctl accel join"); return -1; } // 设备完成一次批量任务后,把状态写入 BAR 空间的寄存器, // 驱动层用 poll 等待该寄存器置位,poll 超时时间按数据量估算 return poll_device_done(fd, cmd->batch_id, 3000); }

几个参数值得解释。left_rows 和 right_rows 是设备端判断批次结束的依据——DMA 引擎按固定包长收数,没有行数上限就无法确认一帧数据是否完整。batch_id 这个字段第一版经常被省略,等到两条查询并发提交时才发现设备端分不清结果属于谁,这个字段就是为了给驱动侧做多队列任务跟踪用的。

ioctl 的路径开销大约 2–5 微秒,短查询每秒提交几千次时会占掉相当比例,所以生产版驱动我一般会换 VFIO 加用户态轮询。把设备状态寄存器通过 mmap 映射进用户态,用忙等替代系统调用,单次提交开销能降到 500 纳秒以内。这属于第二阶段的优化,第一版不用碰。

4. 加速器之外还差一口气:查询计划改造与运行时适配

4.1 哪些算子值得卸载:先用三张表筛一轮,再动代码

哈希连接不是唯一可以卸载的算子,但也不是所有算子都值得卸载。我常用下面这张判定表做前期筛选,每行对应一组特征匹配:

算子计算密度访存特征硬件卸载性价比
哈希连接高内存随机访问密集极高,优先做
分组聚合(GROUP BY)高维护大哈希表,读改写多高,但要看 group key 基数
排序中多路归并,回写频繁中低,设备端存储消耗大
过滤 / 投影低纯顺序扫描低,CPU 向量化已经很优
多表嵌套循环连接极低每行触发子查询不建议,数据搬运无收益

判定思路是:先看算子的访存模式是随机还是顺序,再看单条元组在设备端被复用的次数。哈希连接在构建阶段右表每条元组只写一次,但探测阶段一张页大小的数据块会被哈希表反复查询,这个复用率天然适合硬件化。过滤和投影虽然简单,但它们的瓶颈在数据搬运,设备端跑得再快也只是把 CPU 的活换成 PCIe 的活,没有本质收益。

4.2 查询执行器怎么接加速设备:一个设备抽象层

硬件侧通了,主机侧的数据库执行器也要改造。我会在执行器内核里加一层设备抽象层(Device API),上层算子只依赖接口,不感知具体是 FPGA 还是 ASIC。接口设计越薄越好,太多功能会让驱动层变得不可维护。

class AccelDevice { public: // 申请 DMA 缓冲:要求页对齐,返回物理地址和内核地址的映射对 virtual void* alloc_dma(size_t bytes) = 0; // 异步提交一批哈希连接任务,调用后不阻塞 virtual int submit_join(const JoinJob& job) = 0; // 按 job_id 等待完成,timeout_ms 为超时保护 virtual int wait_join(uint32_t job_id, int timeout_ms) = 0; // 从结果缓冲区取回元组数统计 virtual void release(JoinJob& job) = 0; };

执行器侧调用这个抽象层时,我一般在执行计划里加一个判断节点:当哈希连接算子的输入表超过预设阈值、且连接键类型是设备支持的类型时,把该算子替换成 accel_join 调用,否则回退到 CPU 算子。阈值默认设 64MB,这个数字来自 PCIe 搬运开销和 CPU 执行时间曲线的交叉点。两边的 pipeline 都要保留,因为设备不可用时(比如任务队列塞满)必须有 fallback 路径,否则一条异常查询就能拖垮整个加速路径。

这里有个容易踩的坑:不要试图让加速器承担全部查询计划。SQL 解析、谓词下推、子计划优化这些工作在 CPU 上已经高度成熟,硬件化带来的收益非常有限。把加速器只当作一个高效的算子执行器,查询计划仍然由数据库内核主导,这样做改动最小、风险最可控。

4.3 零拷贝数据通道:页对齐、大页和 DMA 映射

数据库的缓冲池是用户态内存,直接把这个地址塞给驱动做 DMA 是行不通的,页对齐就是第一道坎。malloc 默认按 8 字节对齐分配,但 DMA 引擎要求至少 4KB 对齐才行。第二道坎是页大小,默认 4KB 页在搬运大批量数据时会产生大量缺页中断,TLB miss 也会拖慢轮询判断。

#include <stdlib.h> // 申请 2MB 对齐的 DMA 缓冲。4KB 对齐是下限,2MB 对齐对巨页更友好 void* dma_buf; size_t alloc_size = (size_t)4 * 1024 * 1024; // 4MB 缓冲 if (posix_memalign(&dma_buf, 2 * 1024 * 1024, alloc_size) != 0) { // 对齐失败,大概率是内存碎片化严重,改用驱动内部分配接口 dma_buf = driver_alloc_dma(alloc_size); } if (!dma_buf) { // 记录错误状态,业务侧回退到 CPU 算子 }

拿到页对齐的缓冲只是第一步,还要把虚拟地址翻译成物理地址写入设备描述符。这个翻译动作必须由内核驱动完成,用户态直接传地址会因为 IOMMU 的存在变得不可靠。拿到物理地址后,驱动再从缓冲区头尾剪掉未对齐的部分,因为在 2MB 对齐边界两侧的字节无法被 DMA 引擎安全访问。

巨页(HugePage)要不要用?我建议服从一个原则:只有单次 DMA 数据量超过 8MB 才值得。巨页能减少 TLB miss,但要提前在内核参数里预留,数据库进程没权限直接分配。如果项目里已经用了 DPDK 一类的用户态包处理库,它会自带巨页管理,直接复用它的页面池可以省不少事。

5. 数据库加速芯片落地的六个高频踩坑点

5.1 PCIe 还没喂饱:卸载小表连接反而更慢

现象:把一张 32MB 维表和 64MB 事实表的连接放到加速卡上,总耗时比 CPU 串行执行还慢了 40%。

原因:数据搬运时间约 20 毫秒,设备端执行约 8 毫秒,而 CPU 哈希连接实测只要 10 毫秒。搬运成了大头,收益被 PCIe 往返吃掉了。

解决:给卸载路径加容量阈值。我在执行器里设定的规则是:左右表数据量之和低于 64MB 直接走 CPU 算子,只有超过 64MB 才进入加速分支。阈值要根据实际板卡的 DMA 带宽重新标定,Gen4 x16 板卡的阈值可以下调到 32MB。

5.2 设备端哈希表空间不足:大表构建阶段溢出

现象:TPC-H 的 Q9 跑过一半,设备返回错误中断,查询回退到 CPU,结果 CPU 也撑不住,整个查询被拖死。

原因:加速卡板载内存总量 16GB,哈希构建核把右表投影后的所有中间字段都存进哈希表。GROUP BY 需要的辅助数据没做裁剪,一张 4 亿行的表灌进去直接撑爆。

解决:两个动作。一是投影裁剪,只保留连接键和后续计算需要的列,行宽从原来的 120 字节压到 24 字节;二是哈希表分块,把右表按哈希值分区切成多批,每批独立构建和探测,设备端内存按最大分区而非全表容量预留。我一般预留峰值空间的 3 倍以上才敢上生产。

5.3 回写方向带宽不够:结果集十几 GB 时堵车

现象:查询输出行数很大,join 结果超过源表大小,回写阶段设备内队列堆积,DMA 写方向排队到超时。

原因:很多 PCIe 加速卡的 DMA 读带宽高于写带宽,结果回写的带宽曲线不是线性的,小包回写时吞吐掉到读方向的 50% 以下。

解决:回写路径加双缓冲,一个缓冲区在 DMA 读回写时,另一个在设备内做格式转换,用流水线隐藏 PCIe 写方向延迟。测试选几个大结果集查询,把回写吞吐压到极限再验收。

5.4 同步等待吃掉收益:每次提交都等设备干完

现象:单算子卸载耗时从 20ms 减到 8ms,但端到端查询时间只降了 25%,远低于预期。

原因:执行器是同步调 wait_join,设备执行完一个批次后 PCIe 链路和设备空转,CPU 也在阻塞等待。短查询多、批次小的情况下,同步开销占比超过 30%。

解决:把提交和等待拆开,执行器一次提交多个分片任务到设备队列,最后统一等待。批处理深度调到 4 到 8,队列深度再往上走,任务的排队延迟会抵消空转收益,收益拐点需要用实验标定。

5.5 硬件黑匣子查不出错:错误结果只在压力下出现

现象:性能测试连跑 12 小时无恙,但 TPC-H 全部查询并发时偶尔出现 join 结果对不上,重启后消失,过几天重现。

原因:高负载加上温升导致设备内核时序不满足,极个别触发条件下出现亚稳态,错误结果通过校验时没被发现。

解决:三个手段。第一,设备内核时钟频率往下调一档,性能损失 5%,时序余量大幅提高;第二,在设备状态寄存器里增加一个错误计数器,主机侧定时读取,可以和运行时长关联,方便复现;第三,对结果流做分帧循环冗余校验,每 256 帧抽样校验一次,在性能和覆盖率之间取平衡。

5.6 驱动分支永远在执行 CPU 回退:加速没生效还看不出来

现象:上线后监控显示加速卡 DMA 流量几乎为零,但数据库的查询耗时确实降了。

原因:查询计划里过滤器和投影把表压到几十 MB,执行器阈值判断走了 CPU 分支;或者加速分支有 bug,异常回退逻辑把查询打到 CPU 路径,监控只看数据库整体耗时没拆分算子级数据。

解决:加算子级监控,把每条查询的“加速器实际提交批次”和“CPU 回退批次”以 Counter 形式暴露。上线前先跑一个标准查询集,对比两次执行计划,确认哈希连接算子确实被替换成设备算子。准备工作做不到位,后面排查会非常痛苦。

6. 验证一版加速方案值不值得投入:TPC-H、队列深度与功耗

6.1 用 TPC-H 的 Q1、Q9、Q18 三连验证硬件加速的收益

选 TP C-H 里的 Q1、Q9、Q18 是有讲究的。Q1 纯聚合,Q9 聚合加多表连接,Q18 大结果集哈希连接,分别覆盖了三种典型的算子压力,比只跑一个查询更有代表性。方法很简单:同一份 100GB 数据、同一条 SQL,先开加速通路跑,再关掉跑,各跑 3 轮取中位数。不要取平均值——硬件加速受 PCIe 链路抖动影响,方差很大,中位数更稳定。

列一张对比表记录收益,重点关注搬运与执行比:

查询编号纯 CPU 耗时加速后耗时收益比检查点
Q145s18s2.5x哈希构建核吞吐是否打满
Q978s31s2.5x多 join 流水线是否重叠
Q1892s40s2.3x回写带宽是否成为瓶颈

Q9 的收益比理论预期低一般不是设备问题,而是多连接场景下左右表的调度不够顺滑,PCIe 在搬运阶段闲置。调整思路是先加大批处理深度,再看板卡侧队列占用。

6.2 批量大小与队列深度的定参技巧

硬件加速的收益曲线通常不是线性的。我会固定数据量,把每次提交的批量从 1MB 依次调到 4MB、16MB、64MB,画出端到端耗时曲线。曲线拐点就是该板卡的甜点值,之后再加量,延迟反而因为排队升高。这个办法在换板卡、换驱动版本后都要重测,因为 DMA 描述符的构造和中断频率会变。

设备侧队列深度先给 4,配合异步提交。看板卡的热点是在搬运还是算,方法很土但有用:驱动统计每次提交后设备忙等待的周期数,若占比超过 60% 说明计算侧饱和,加大并行粒度;若设备空闲占比高,就是搬运侧问题,优先优化数据裁剪和 DMA 描述符。

做这类硬件项目,我的经验是不要迷信理论加速倍数。加速卡手册上标的数字是在理想数据分布下测出来的,真实查询的 key 分布、中间结果大小、并发度都会让收益缩水一半。每次调优都记录当时的队列深度、批量大小和设备频率,下次翻车时还能恢复参数。某次做模拟项目X时,我把哈希连接卸载阈值从 32MB 调到 8MB,性能直接倒退了 15%,折腾两天才发现是批次太小导致 DMA 开销吃满了链路。后来学乖了,所有硬件参数先跑全矩阵实验再定稿,没有数据支撑的改动一律不做。希望这些经验帮到你,让你在数据库加速芯片这条路上少交点学费。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询