Kudu 与 Impala 到底是什么:从写入一行到执行一条 SQL

Kudu Impala 数据仓库 查询引擎 实时分析

第一次接触 Kudu 和 Impala,最容易产生两个误解:把 Kudu 当成“另一个 HDFS”,或者把 Impala 当成“装在 Kudu 上面的数据库”。两种说法都没有抓住边界。

Kudu 是分布式存储引擎。 它负责把表切成 Tablet,保存每一行、处理主键、更新、删除、快照和多副本。

Impala 是分布式 SQL 查询引擎。 它负责理解 SQL、选择执行计划、安排并行任务、扫描数据、执行 Join、聚合和排序,再把结果返回客户端。

两者组合后,Impala 可以查询和修改 Kudu 表,但数据最终仍由 Kudu 保存;Kudu 不负责解析 GROUP BY 或规划多表 Join,Impala 也不自己实现 Kudu 的副本和落盘格式。

下文不会把组件名逐个罗列,而是沿一条数据链展开:先确定谁负责什么,再追踪一行数据如何进入 Kudu、如何通过 WAL 和 Raft 变得可靠、如何从 Base 与 Delta 中读出;随后让 Impala 接管这批数据,走完扫描、Join 和聚合。理解主链后,再讨论缓存为何只能加速其中一段,以及这套架构与 Doris 的取舍。

先用一张图划清职责

flowchart LR
    APP[业务写入 / CDC] --> KC[Kudu Client]
    SQL[SQL 客户端 / BI] --> FE[Impala Frontend]
    FE --> PL[Planner 与 Scheduler]
    PL --> EX[Impala Executors]
    EX -->|KuduScanNode / Kudu Client| TS[Kudu Tablet Servers]
    KC --> TS
    KM[Kudu Masters] -->|表与 Tablet 元数据| KC
    KM -->|副本位置| EX
    TS --> WAL[WAL 与 Raft 副本]
    TS --> STORE[MemRowSet / DiskRowSet / Delta]
    EX -->|Exchange| EX
    EX --> RESULT[Join / Aggregate / Sort / 结果]

可以把它们类比为仓库和分析团队:Kudu 决定货物怎样编号、放在哪个仓位、如何盘点和复制;Impala 读取仓位信息,安排多个工人并行取货,再完成统计和汇总。

这个类比有一个边界:Kudu 不只是被动文件系统。它知道表结构、主键和快照,也能在 Tablet Server 内执行列裁剪与谓词过滤。

Kudu 是什么

Kudu 是面向结构化表的分布式存储系统。建表时必须定义主键,并通过 Hash、Range 或两者组合把主键空间切成 Tablet。

它提供的核心能力包括:

  • 主键唯一性;
  • 低延迟 INSERT、UPSERT、UPDATE 和 DELETE;
  • 按主键或范围读取;
  • 列式扫描与编码压缩;
  • MVCC 快照读取;
  • Tablet 级 Raft 多副本;
  • 后台 Flush、Compaction 与历史版本回收。

Kudu 不负责:

  • 解析任意 SQL;
  • 决定多表 Join 顺序;
  • 执行分布式 GROUP BY;
  • 像传统关系数据库那样提供完整外键、触发器和复杂跨行事务语义。

应用可以直接使用 Kudu Client 读写,也可以通过 Impala SQL 操作 Kudu 表。

Kudu 怎样保存一张表

一张表先分区成多个 Tablet,每个 Tablet 覆盖一段分区键范围。Tablet 的多个副本分布在不同 Tablet Server 上,其中一个是 Raft Leader。

Table
├── Tablet A:hash bucket 0,时间范围 9 月
│   ├── Leader @ TS1
│   ├── Follower @ TS2
│   └── Follower @ TS3
├── Tablet B:hash bucket 1,时间范围 9 月
└── Tablet C:hash bucket 0,时间范围 10 月

Master 管理表结构、分区和 Tablet 副本位置,但数据读写通常由客户端直接访问 Tablet Server。客户端先从 Master 获取位置并缓存,之后不必让每次请求都经过 Master。

数据到底存在哪里

答案很明确:实际表数据存储在 Kudu Tablet Server 的数据目录里。 Impala Catalog 保存的是“这是一张 Kudu 表、有哪些列、底层表 ID 和 Master 地址是什么”等元数据,不保存表中的每一行。

Tablet Server 磁盘上的内容可以分成三组:

flowchart TB
    T[一个 Tablet 副本]
    T --> M[Tablet Metadata]
    T --> D[Data Blocks]
    T --> W[WAL Segments]
    D --> B[Base Data CFiles]
    D --> I[主键索引与 Bloom Filter]
    D --> R[Redo DeltaFiles]
    D --> U[Undo DeltaFiles]
    M --> MAP[记录 RowSet 与 Block ID 的对应关系]
    W --> LOG[尚需恢复或复制的行操作日志]

Base Data

DiskRowSet 的稳定基线按列保存。每列对应一个或多个由 Block Manager 管理的数据块,内部通常使用 CFile 格式编码、压缩并建立位置索引。查询只需要少数列时,Tablet Server 可以不读取其他列。

索引

编码主键索引用于按键和范围定位,Bloom Filter 用来快速证明某个 DiskRowSet 不包含目标主键。索引也是 Kudu 点查和更新定位没有退化成全表扫描的重要原因。

Delta

已经落盘的行发生 UPDATE 或 DELETE 时,Kudu 不立刻重写整套 Base Data,而是记录稀疏 Delta。内存中是 DeltaMemStore,Flush 后成为 Redo DeltaFile;Major Delta Compaction 把新值吸收到 Base 后,还会保留必要的 Undo Delta,让旧快照能够还原历史值。

Tablet Metadata

元数据记录 Tablet 当前有哪些 RowSet、各 RowSet 引用了哪些数据块、Schema 版本和分区信息。数据块不是靠文件名猜归属,而是通过持久化元数据关联。

WAL

WAL 是按顺序追加的操作日志,保存在独立日志段中。它不是最终查询格式:查询不会为了读当前表而从头扫描 WAL。WAL 的主要职责是崩溃恢复和 Raft 复制期间的操作保留。

WAL 到底怎样工作

WAL 的全称是 Write-Ahead Log,核心约束是:一次变更在被认为可靠之前,必须先拥有可恢复的日志记录。

在 Kudu 中,WAL 还与 Raft 操作序列结合。日志项带有 Term、Index、HybridTime 和具体行操作。简化后可以分成 Replicate 与 Commit 两类状态:

REPLICATE:这项操作已经进入 Raft 日志,拥有确定顺序
COMMIT:这项操作已经满足提交条件并完成状态机提交

一次 Leader 写入的关键顺序是:

sequenceDiagram
    participant Q as 写入队列
    participant C as Raft Consensus
    participant LW as Leader WAL
    participant FW as Follower WAL
    participant A as Tablet Apply
    participant M as MVCC
    Q->>C: 提交行操作
    C->>LW: 追加 REPLICATE
    C->>FW: UpdateConsensus RPC
    FW->>FW: 追加副本日志
    FW-->>C: 持久化确认
    C->>C: 多数派已复制,推进 Commit Index
    C->>A: 按日志顺序 Apply
    A->>M: 提交可见状态
    C->>LW: 记录/推进 COMMIT 状态

图中省略了并发流水和实现细节,但有三点不能丢:

  1. 操作先获得 Raft 顺序,所有副本按同一顺序处理;
  2. 多数副本持久化后才能推进提交;
  3. Tablet 状态机 Apply 后,写入才成为正常读取可见的已提交状态。

Group Commit

WAL 不会机械地为每一行单独做一次磁盘同步。多个并发请求可以组成 Log Entry Batch,共享一次底层同步成本。这就是 Group Commit。

请求 A ─┐
请求 B ─┼→ Log Entry Batch → append → fsync → 分别唤醒 A/B/C
请求 C ─┘

批次太小,固定同步成本无法摊薄;为了等更大的批次停留太久,又会增加单次写入延迟。实际效果取决于并发度、设备 fsync 尾延迟和日志队列调度。

WAL Segment

日志连续增长时会滚动成多个 Segment。旧 Segment 不能仅因为“时间久”就删除,因为落后副本可能还要读取其中的 Replicate,崩溃恢复也可能需要尚未进入持久化 RowSet 的操作。

Kudu 使用日志锚点和最小保留 Index 约束回收。只有当对应变更已经被安全 Flush/Compaction 到数据块,并且复制与恢复不再依赖该段时,旧 WAL 才能被垃圾回收。

崩溃恢复

Tablet Server 重启时,不是只重放 WAL,也不是只读取 DiskRowSet:

读取 Tablet Metadata
  → 打开已持久化 DiskRowSet 与 DeltaFile
  → 确定持久化边界
  → 扫描仍需恢复的 WAL Segment
  → 按顺序重放尚未落盘的已提交操作
  → 重建 MemRowSet / DeltaMemStore
  → 恢复 Tablet 对外服务

这也是 WAL 和 Flush 必须通过持久化边界协调的原因。先删除日志、后发现 RowSet 元数据没有原子安装,会造成数据丢失;一直不释放日志,又会让日志盘无限增长。

Kudu 自己是怎样读取数据的

无论请求来自直接 Kudu Client 还是 Impala KuduScanNode,最终都由目标 Tablet Server 执行本地扫描。

一次快照读取需要同时面对“新行还在内存、旧行已经列式落盘、更新又在 Delta 中”这三种状态:

flowchart LR
    R[Scan Request] --> S[确定 MVCC Snapshot]
    S --> M[扫描 MemRowSet]
    S --> RT[RowSetTree 选择候选 DiskRowSet]
    RT --> IDX[主键索引 / Bloom / 范围裁剪]
    IDX --> BASE[读取投影列 Base Data]
    BASE --> DELTA[合并可见 Redo / Undo Delta]
    M --> MERGE[按主键归并]
    DELTA --> MERGE
    MERGE --> P[执行可下推谓词]
    P --> B[返回 Scan Batch]

快照时间决定某条 INSERT、UPDATE 或 DELETE 是否可见。同一 DiskRowSet 在两个不同快照下可能返回不同值。扫描器把 Base 和 Delta 合成逻辑行后,上层看不到这些物理层次,只看到符合快照的一批记录。

点查会更强调主键索引与 Bloom Filter;范围扫描则需要打开多个相交 RowSet Iterator 并有序归并。RowSet 重叠越多、Delta 越多,读取时要合并的结构也越多,所以后台 Compaction 直接影响长期读延迟。

一行写入 Kudu 时发生了什么

假设写入一条设备状态:

UPSERT INTO device_state
VALUES ('device-17', NOW(), 36.8, '正常');

如果通过 Impala 执行,Impala 负责解析 SQL 并用 KuduTableSink 建立 Kudu Session;真正的写入一致性仍由 Kudu 完成。

sequenceDiagram
    participant C as 客户端或 Impala
    participant M as Kudu Master
    participant L as Tablet Leader
    participant F as Raft Followers
    participant W as WAL
    participant T as Tablet Storage
    C->>M: 查询目标 Tablet 与 Leader
    M-->>C: 返回副本位置
    C->>L: 批量行操作
    L->>L: 校验主键、行锁、分配时间戳
    L->>W: Group Commit
    L->>F: Raft Replicate
    F-->>L: 多数副本确认
    L->>T: Apply 到 MemRowSet 或 DeltaMemStore
    L-->>C: 返回结果

新行通常进入当前 MemRowSet;如果更新的是已经落盘的行,变更进入对应 DiskRowSet 的 DeltaMemStore。WAL 保证节点崩溃后可重放,Raft 保证多数副本对操作顺序达成一致,MVCC 时间戳决定并发扫描能否看到该变更。

后台再把 MemRowSet Flush 成列式 DiskRowSet,把 DeltaMemStore Flush 成 DeltaFile,并通过 Compaction 偿还不断累积的读取成本。

因此,“实时写入”不是数据直接跳过日志写进内存,而是关键路径只完成 WAL、复制和内存 Apply,把列式重写和整理留给后台。

Impala 是什么

Impala 是 MPP(大规模并行处理)SQL 引擎。每个 impalad 都能接收查询;接收本次 SQL 的节点成为 Coordinator,其他节点按计划执行 Fragment Instance。

Impala 提供的核心能力包括:

  • SQL 语法与语义分析;
  • 表、列、分区和统计信息解析;
  • Join 顺序与分布方式选择;
  • 分布式 Fragment 规划;
  • HDFS、Parquet、Kudu、Iceberg 等数据源扫描;
  • Hash Join、聚合、排序、窗口函数;
  • Runtime Filter、LLVM Codegen;
  • 内存跟踪、Spill 和 Admission Control;
  • Runtime Profile 与查询状态管理。

Impala 不负责:

  • 替 Kudu 保存 Tablet 数据;
  • 替 Kudu 做 Raft 复制和崩溃恢复;
  • 把所有数据复制进自己的专用格式;
  • 保证错误 SQL 在没有统计信息时仍选出理想计划。

Impala 是计算层。Daemon 会缓存元数据并常驻执行环境,但查询的数据依然来自底层存储。

Impala 怎样认识一张 Kudu 表

Impala Catalog 中保存表的 SQL 元数据,包括列定义、Kudu Master 地址、底层 Kudu 表标识和主键信息。Kudu 的 HMS 集成可以让表结构变化同步到 Hive Metastore 与 Impala Catalog。

规划 Kudu 查询时,Frontend 创建 planner.KuduScanNode。它使用 Kudu Client 获取表和 Tablet 信息,将 SQL 中能够下推的条件转换成 Kudu Predicate,再构建 Kudu Scan Token。

一个 Scan Token 可以理解为可序列化的扫描任务:

扫描哪张表
+ 读取哪些列
+ 主键上下界
+ 谓词
+ 快照与读取模式
+ 目标 Tablet 和副本位置

Scheduler 把 Token 尽量分配给接近 Tablet 副本的 Executor。后端反序列化 Token,创建 Kudu Scanner,直接连接 Tablet Server。

一条 Kudu 查询是怎样执行的

使用下面的查询贯穿整条路径:

SELECT region, AVG(temperature) AS avg_temp
FROM device_metrics
WHERE event_time >= '2026-09-01 00:00:00'
  AND status = '正常'
GROUP BY region
ORDER BY avg_temp DESC;

1. SQL 解析与分析

Impala Frontend 将 SQL 解析为语法树,绑定表和列,检查类型与权限。此时它知道 region、temperature、event_time 和 status 来自 Kudu 表。

2. 谓词分类

并非所有条件都要把行取回 Impala 后再判断。可以转换为 Kudu Predicate 的条件会绑定到 KuduScanNode,例如时间范围、等值和部分比较条件。

不能安全下推的表达式仍由 Impala 执行。原则不是“函数看起来简单就下推”,而是底层必须能保持相同语义,包括 NULL、类型转换和边界行为。

3. 列裁剪

这条查询只需要四列。Scan Token 的 Projection 不会请求表中其他列。Kudu 的列式存储因此可以跳过无关 CFile,减少 IO、解码和网络传输。

4. Tablet 与 Scan Token 裁剪

如果 event_time 参与 Range 分区,时间条件可以直接排除不相交的 Tablet。如果主键前缀和 Hash 分区条件足够完整,还可以进一步缩小 Bucket。

剩余 Tablet 被切成 Scan Token,形成天然并行度。

5. Executor 打开 Kudu Scanner

Impala 后端的 KuduScanNode 或 KuduScanNodeMt 取得 Token,KuduScanner 反序列化后连接 Tablet Server。源码入口位于:

  • fe/.../planner/KuduScanNode.java
  • be/src/exec/kudu/kudu-scan-node-base.cc
  • be/src/exec/kudu/kudu-scan-node.cc
  • be/src/exec/kudu/kudu-scanner.cc

每个 Scanner 请求投影列、谓词和快照。Tablet Server 在本地完成 RowSet 选择、列读取与 Delta 合并,再按批返回结果。

6. Kudu Tablet Server 生成快照结果

Tablet 读取当前 MemRowSet 和相交 DiskRowSet。DiskRowSet 的基线列数据还要与当前快照可见的 Redo/Undo Delta 合并。

RowSetTree 找候选 RowSet
  → 主键范围与谓词裁剪
  → 读取 Projection 指定的列
  → 合并 Snapshot 可见 Delta
  → 过滤行
  → Kudu Scan Batch

Kudu 返回的是符合底层谓词的批次,不是最终 GROUP BY 结果。

7. Impala 执行聚合和排序

各 Executor 收到扫描批次后,先做局部聚合,计算每个 Region 的 sum(temperature) 与 count。局部结果经 Exchange 按 Region 重新分区,上层 Fragment 合并聚合状态并计算平均值,最后排序。

flowchart LR
    SQL[SQL] --> FE[Frontend 分析]
    FE --> KN[KuduScanNode]
    KN --> TOK[按 Tablet 生成 Scan Tokens]
    TOK --> E1[Executor 1]
    TOK --> E2[Executor 2]
    TOK --> E3[Executor 3]
    E1 -->|Kudu Scanner| T1[Tablet Server]
    E2 -->|Kudu Scanner| T2[Tablet Server]
    E3 -->|Kudu Scanner| T3[Tablet Server]
    T1 --> A1[局部聚合]
    T2 --> A2[局部聚合]
    T3 --> A3[局部聚合]
    A1 --> X[Exchange: 按 region 分区]
    A2 --> X
    A3 --> X
    X --> F[最终聚合与排序]
    F --> R[返回客户端]

这条路径说明了职责边界:Kudu 在 Tablet 内完成存储级扫描,Impala 在集群内完成关系运算。

有 Join 时谁做什么

假设设备明细在 Kudu,地区维表在 Parquet:

SELECT d.province, COUNT(*)
FROM kudu_device_event e
JOIN dim_device d ON e.device_id = d.device_id
WHERE e.event_time >= NOW() - INTERVAL 1 HOUR
GROUP BY d.province;

Kudu 不知道 Parquet 维表,也不会执行这个 Join。Impala Planner 根据统计决定广播维表还是让两侧按 device_id 重分区。

如果维表较小,Impala 可以构建 Runtime Filter,把维表中出现的 Device ID 范围或集合信息传给 Kudu Scan。KuduScanNode 支持接收适用的 Min/Max 等过滤条件,以减少扫描返回的数据。

flowchart TB
    DIM[Parquet 维表 Scan] --> BUILD[Hash Join Build]
    BUILD --> RF[Runtime Filter]
    RF -. 下推 .-> KS[KuduScanNode]
    KTS[Kudu Tablet Scan] --> KS
    KS --> PROBE[Hash Join Probe]
    BUILD --> PROBE
    PROBE --> AGG[按省份聚合]

Runtime Filter 是运行时优化,不改变职责:Impala 生成过滤器,Kudu Scanner 接收能够表达的过滤条件并减少数据。

Impala 写 Kudu 又是怎样工作的

执行 INSERT、UPSERT、UPDATE 或 DELETE 时,Impala 负责:

  1. 解析 SQL;
  2. 计算输入表达式;
  3. 扫描上游数据;
  4. 将行路由到 KuduTableSink;
  5. 收集和报告 Kudu 写入错误。

KuduTableSink 使用 Kudu Client Session 批量提交。Kudu 负责主键检查、Tablet 路由、WAL、Raft、MVCC 和最终存储。

因此通过 Impala 写 Kudu,并不意味着写入先落到 Impala。Impala 只是 SQL 到 Kudu 行操作的执行桥梁。

为什么这套组合适合实时分析

前面已经分别走完了 Kudu 的写入路径和 Impala 的查询路径,现在可以回答两者组合的核心问题:为什么新数据能够较快进入分析,而不必等待一轮离线整理。

这里的“实时”应理解为:写入 Kudu 的数据不必先跑一轮小时级 ETL 转换为 Parquet,提交后即可被后续 Impala 查询扫描。

它来自几个环节的组合:

  • Kudu 用 MemRowSet 和 Delta 接住实时变更;
  • WAL Group Commit 与 Raft 保持持久性和副本一致性;
  • Flush、Compaction 在后台完成列式整理;
  • Impala Daemon 与元数据常驻,减少查询启动成本;
  • Tablet/Token 提供扫描并行度;
  • 列裁剪和谓词下推减少数据;
  • Exchange、Join 和局部聚合完成分布式分析。

它不保证每条查询都“秒回”。扫描范围过大、分区错误、热点 Tablet、Delta 堆积、统计信息过期或 Join 数据倾斜,都可能把延迟拉高。

为什么要把存储与查询引擎分开

实时性解释的是一条数据链为什么短,职责分离解释的是系统为什么要切成两个产品。

Kudu 与 Impala 的“存算分离”更准确地说是职责分离,不是“计算节点完全无状态、所有数据都放对象存储”。Kudu Tablet Server 既持久化数据,也执行主键查找、谓词过滤、MVCC 快照以及 Base/Delta 合并;Impala 则组织跨 Tablet、跨表的 SQL 计算。

这种边界带来四个实际价值:

  • Kudu 可以围绕写入、复制、快照和存储布局演进,Impala 可以围绕优化器、Join、聚合和并行执行演进;
  • 同一个 Impala 查询可以把 Kudu 的热数据与 Parquet、Iceberg 等历史数据连接起来,不必把所有数据塞进一种格式;
  • 应用可以用 Kudu Client 做低延迟主键读写,同时让分析人员继续使用 SQL;
  • 查询计算和数据副本可以分别扩容,但并非完全互不影响:扫描仍会消耗 Tablet Server 的 CPU、内存和磁盘带宽。

代价也很明确:需要运维两套服务、两套指标与版本兼容关系;跨越计算和存储边界会产生 RPC;性能问题也必须沿 Impala Profile、Kudu Scan RPC 和 Tablet 存储状态逐层定位。

Kudu + Impala 与 Doris 怎么选

明确这条边界的收益和成本后,才能与 Doris 做公平比较。

不能简单地把两者概括成“分离”和“一体化”。Doris 既有 FE + BE、本地盘保存数据的存算一体形态,也提供共享存储与计算组解耦的存算分离形态。更有意义的是比较工作负载和系统边界。

需求 更值得优先评估的方案 原因
已有 Hadoop 数据湖,热表在 Kudu、历史表在 Parquet/Iceberg Kudu + Impala 一条 SQL 可以组合多种存储,Kudu 专注可变热数据
应用既要 Kudu API 主键读写,又要分布式 SQL 分析 Kudu + Impala 行服务与分析共享 Kudu 的一致快照,不必反复导出
新建实时 OLAP,期望导入、存储、索引、物化视图和 SQL 在一个系统内完成 Doris 存算一体 系统边界更少,通常更容易建设和运维
高并发看板、明细查询、聚合加速,希望充分利用本地数据与索引 Doris 存算一体 BE 同时管理本地数据和执行,链路短,优化手段集中
对象存储为底座,需要弹性计算、多计算组和资源隔离 Doris 存算分离 计算组可共享数据,并利用本地缓存降低远端读取成本

Doris 的 Unique Key Merge-on-Write 也能处理更新,因此“有更新就必须选 Kudu”并不成立。反过来,Kudu + Impala 的价值也不只是更新能力,而是 Kudu API、Hadoop 生态、多存储联合查询以及清晰的存储/SQL 边界。最终应使用真实表结构、更新比例、查询并发和 SLA 做压测,而不是只比较单条 SQL。

缓存机制:第二次查询为什么可能更快

架构选择决定了缓存分布在哪里。Kudu 与 Impala 没有一块包办所有事情的“总缓存”;“缓存”在两套系统里指向不同层次。先看完整路径:

flowchart LR
    SQL[SQL 请求] --> CC[Impala Catalog Cache<br/>表结构、分区、统计信息]
    CC --> PLAN[规划与执行]
    PLAN -->|Kudu 表| KC[Kudu Client 元数据缓存<br/>Tablet 与 Leader 位置]
    KC --> TS[Kudu Tablet Server]
    TS --> BC[Kudu Block Cache<br/>CFile 数据块与索引块]
    PLAN -->|文件表| DC[Impala Data Cache<br/>远端文件数据]
    PLAN --> BP[Buffer Pool<br/>查询工作内存与 Spill]
    PLAN --> CG[Codegen Cache<br/>编译后的执行代码]

    classDef meta fill:#dbeafe,stroke:#2563eb,color:#172554
    classDef storage fill:#dcfce7,stroke:#16a34a,color:#052e16
    classDef query fill:#fef3c7,stroke:#d97706,color:#451a03
    class CC,KC meta
    class TS,BC,DC storage
    class PLAN,BP,CG query

Kudu 的缓存

Block Cache 位于 Tablet Server,缓存已经持久化的 CFile 数据块和索引块,键可理解为文件标识加块偏移。热点列、主键索引或 Bloom Filter 再次被读取时,可以避免重复访问磁盘。容量受限时按缓存策略淘汰;由于已落盘的 CFile 基本不可变,行更新通常写入新的 Delta,而不是原地修改旧块,因此不需要像可变页缓存那样逐行失效旧块。

Kudu Client 元数据缓存保存 Tablet 的分区范围、副本和 Leader 位置。Impala 复用 Kudu Client 后,不必为每次扫描都询问 Master。遇到 Leader 变更、Tablet 拆分或位置过期时,客户端会重新定位,而不是永久相信旧条目。

Raft Log Cache在共识层短期保留最近的 Replicate 消息,Follower 落后时可以直接补发;缓存没有所需位置时再从 WAL 读取。它优化副本追赶,不是 SQL 数据缓存。

这里必须区分两个经常被误称为缓存的结构:

  • MemRowSet 保存尚未 Flush 的新行;
  • DeltaMemStore 保存尚未 Flush 的更新和删除。

它们是 Tablet 当前可见数据的一部分,并由 WAL、Raft 和 MVCC 保证恢复与一致性。机器重启后不能简单丢弃,所以它们是内存中的持久状态,不是“丢了也没关系”的缓存。

Impala 的缓存

Catalog Cache保存在 Impala Daemon 中,包括表结构、分区、文件、列统计等规划信息。Catalog Service 通过 Statestore 分发变更;外部系统绕过 Impala 修改文件或元数据时,才需要根据场景使用 REFRESH 或 INVALIDATE METADATA。它缓存的是元数据,不是表中的行。

Data Cache主要服务 HDFS、对象存储等文件扫描,在本地介质保存远端文件的数据范围。它对 Kudu Scan 通常不起作用,因为 Kudu 数据不是由 Impala 当作普通文件读取,而是通过 Scanner RPC 从 Tablet Server 返回。

Codegen Cache复用已经编译的表达式和执行代码,减少相似查询的 LLVM 编译成本。Buffer Pool则管理当前查询的 Join、聚合、排序和扫描缓冲区,并在内存不足时 Spill;它是执行期资源管理,不是跨查询长期保存业务结果的缓存。

Impala 查询 Kudu 时到底命中哪一层

第一次查询后,后续查询可能因为 Impala 已有 Catalog 信息、Kudu Client 已知道 Tablet 位置、Kudu Block Cache 和操作系统页缓存已经变热、Codegen 已复用而更快。但默认情况下,Impala 不会把某条 Kudu 查询返回的全部行当成通用结果缓存长期保存。

因此,缓存命中也不能掩盖扫描设计问题。全表扫描即使命中内存,仍要在 Kudu 端做 MVCC 合并和过滤、经网络传输结果,再在 Impala 端聚合或 Join。真正稳定的低延迟首先来自分区裁剪、主键范围、谓词下推和列裁剪,缓存只是缩短剩余路径。

Kudu + Impala 不是什么

理解职责、数据路径和缓存以后,还需要用四个反例收紧适用边界。

不是把 OLTP 和 OLAP 完全合成一个系统

Kudu 支持行级变更,但并不等于完整 OLTP 数据库。复杂事务、外键、高并发短事务和二级索引需求仍要单独评估。

不是比 Parquet 永远更快

不可变历史数据用 Parquet 通常压缩更好、存储成本更低。Kudu 为更新能力、主键索引和在线服务付出额外资源。

不是不用设计分区

Hash Bucket 和 Range Partition 决定 Tablet 数量与热点。查询能否裁剪 Tablet,也直接取决于条件是否覆盖分区键。

不是 Impala 自动替你维护 Kudu

Kudu 的 Flush、Compaction、副本和磁盘健康仍需单独监控。Impala Profile 能看到 Kudu Scan 耗时,却不会替 Kudu 修复后台维护积压。

怎样判断问题出在哪一层

查询慢时先看边界,不要同时调两个系统的全部参数。

Kudu 层

  • 命中了多少 Tablet;
  • 是否存在热点或远程副本访问;
  • Kudu Scan RPC 往返次数;
  • RowSet 与 Delta 数量;
  • Tablet Server 的 CPU、磁盘和 Compaction;
  • 谓词与列裁剪是否真正下推。

Impala 层

  • Planner 估算基数与实际行数;
  • KuduScanNode 的 RowsRead、RowsReturned;
  • Scan Token 是否分布均匀;
  • Runtime Filter 到达时间和过滤效果;
  • Exchange 数据量;
  • Join、Aggregate、Sort 是否 Spill;
  • Fragment Instance 是否倾斜。

如果 Kudu 已经只返回少量行,而查询仍慢,应继续查 Impala 的 Join、Exchange 或聚合;如果 KuduScanNode 读取远多于最终结果,应先查分区、下推和 Tablet 存储状态。

最后把整条链串起来

  1. Kudu 保存结构化数据,并负责主键、更新、快照和副本。
  2. Impala 理解 SQL,并负责计划、并行扫描、Join、聚合和结果。
  3. Impala 通过 Kudu Client 和 Scan Token 直接访问 Tablet Server,不经过一轮文件导出。
  4. 查询快不快,既取决于 Kudu 少读了多少数据,也取决于 Impala 怎样移动和计算这些数据。

把这四句话连起来,完整路径就是:写入先在 Kudu 中经过 WAL、Raft 和内存 Apply,随后被 Flush 为 Base/Delta;查询由 Impala 生成 Scan Token,Kudu 按快照合并并过滤行,Impala 再完成跨节点 Join、聚合和排序。元数据缓存、Block Cache 与 Codegen Cache 可以缩短其中若干环节,却不会改变这条主链。

因此,Kudu 与 Impala 的组合不是两个产品名的简单叠加:Kudu 让变化中的数据保持可靠、可更新且可按快照读取,Impala 把这些数据组织成分布式 SQL 计算。是否选择它,应回到数据是否持续变化、是否需要多存储联合查询、团队能否承担两套系统,以及真实负载下的延迟和吞吐。