背景
Kudu具有灵活的分区设计,允许通过哈希区和范围分区的组合在tablet之间分配行。该设计允许操作者控制数据局部性,以优化预期的工作量。
每个表都有一个分区模式,它由零个或多个哈希组件和一个范围组件组成。哈希组件具有一个或多个列。每列必须是主键列集的一部分,并且任何列不能同时出现在两个哈希组件中。范围组件可以具有零个或多个列,所有列都必须是主键的一部分。
使用分区键将Kudu表中的行映射到tablet。通过根据表的分区模式对行的列值进行编码来创建行的分区键。对于每个哈希桶组件,分区键包含组件中列值的哈希值。对于范围组件中的每一列,该键将包含该列的行编码值。每个tablet都有一个开始和结束分区键,它们覆盖了tablet的哈希桶和范围分配。要找到一行的tablet需要先找到该行的分区键落在那个tablet分区键的范围中。
Kudu在执行扫描时如果没有充分利用分区信息,会导致错过根据哈希桶和范围分区信息跳过“分区修剪”或者跳过无用tablet的机会。本设计文档的其余部分将详细介绍我们可以利用来修剪分区的具体机会,概述我们如何在客户端和服务器上实现此目标。
schema 例子
下面会列出2个table的schema案例
案例1
CREATE TABLE 'machine_metrics'
(STRING host, STRING metric, UNIXTIME_MICROS time, DOUBLE value)
PRIMARY KEY (host, metric, time)
DISTRIBUTE BY
HASH (host, metric) INTO 2 BUCKETS
RANGE (time) SPLIT ROWS [(1451606400000)];
tablet如下:
A: bucket(host, metric) = 0, range(time) = [(min), (1451606400000))
B: bucket(host, metric) = 0, range(time) = [(1451606400000), (max))
C: bucket(host, metric) = 1, range(time) = [(min), (1451606400000))
D: bucket(host, metric) = 1, range(time) = [(1451606400000), (max))
案例2
CREATE TABLE 'user_clicks'
(INT64 user_id, INT64 target_id, INT64 click_id)
PRIMARY KEY (user_id, target_id, click_id)
DISTRIBUTE BY RANGE (user_id, target_id) SPLIT ROWS [(1000, 1000)];
tablet如下:
A: range(user_id, target_id) = [(min, min), (1000, 1000))
B: range(user_id, target_id) = [(1000, 1000), (max, max))
扫描约束
Kudu有两种限制和过滤扫描结果的机制:column predicates和 primary key bounds。这些机制可用于表示扫描的不同约束,但它们可以表示的约束存在一些重叠。
在评估优化和修剪的机会时,应考Predicate 和primary key bound,但对于大多数修剪机会,更容易仅考虑其中一个。为了简化修剪逻辑并捕获最多的修剪机会,Kudu客户端在可能的情况下共享column predicates和primary key bounds之间的约束。这是通过将primary key bounds中的隐式predicates“提升”到predicates集中,并将predicates约束“推送”到primary key bounds来完成的。
例如,以下查询:
SELECT * FROM 'user_clicks'
WHERE primary_key >= (500, 500)
AND primary_key < (500, 750);
将primary key bounds中的隐式predicates“提升”到predicates集中
user_id = 500
target_id >= 500
target_id < 750
将predicates推入primary key bounds的例子
SELECT * FROM 'user_clicks'
WHERE user_id = 500
AND target_id < 700;
得到的primary key bounds如下:
primary_key >= (500, min)
primary_key < (500, 700)
修剪机会
范围修剪
如果表是使用拆分行进行范围分区,并且扫描用的predicates包含范围列的前缀,则扫描可能能够根据这些predicates修剪tablet。例如,查询:
SELECT * FROM 'machine_metrics'
WHERE time < 500;
这个查询可以把tablet B 和D给修剪掉。
主键修剪
当表在主键列的前缀上进行范围分区时(如同 user_clicks),可以使用特殊形式的Range Pruning优化。tablet的范围界限可以与扫描的上下主键界限进行比较,而不是基于扫描predicates进行修剪。由于当范围列是主键前缀时,上键和下键主要边界始终至少与predicates一样受约束,因此主键边界可以提供额外的修剪机会。例如:
SELECT * FROM 'user_clicks'
WHERE primary_key >= (500, 0)
AND primary_key < (1000, 500);
允许从主键范围中提取以下predicates:
user_id >= 500
user_id < 1001
扫描可以完全由tablet A满足,但提升的predicates无法修剪B。通过使用主键边界,tablet B可以被修剪。
哈希桶修剪
如果扫描在散列组件中的所有列上指定了等价的predicates,则扫描可以修剪不属于相应存储桶的所有tablet。例如:
-- Allows hash bucket pruning
SELECT * from 'machine_metrics'
WHERE host = "host001.example.com"
AND metric = "load-avg-1min";
-- Does not allow hash bucket pruning
SELECT * from 'machine_metrics'
WHERE host = "host001.example.com";
tablet的查找优化
为了让客户端扫描tablet,它必须从master服务器检索tablet位置。当一个tablet在扫描中被修剪时,这个tablet的位置将不再被需要,因此客户端可以通过不查找已修剪tablet的元数据来加速元数据操作。
实现
我们将在客户端和服务器上实现分区修剪,以便在所有情况下用最少量的工作以满足查询。在服务器上重复这样做并非必要的,但与访问磁盘相比,它是一种低开销操作,并且它使不做优化的客户端受益。
在客户端中,每次扫描一组分区键范围时,都会做一次扫描的约束评估,这些范围覆盖了扫描中未被修剪的tablet。使用这些分区键范围,只能从主站请求扫描所需的tablet元数据。
通过在扫描初始化期间将tablet的主键边界添加到扫描规范,服务器将更进一步,这可以在tablet的主键边界比扫描主键边界更受约束的情况下提供额外的修剪机会。如果可以修剪其分区,则服务器将立即返回空结果。
代替方案
我们可以让客户端添加tablet的上下主键范围,并重新运行扫描中每个tablet的范围界限和哈希桶分析。我建议我们不这样做,因为在实践中它需要在客户端上为每个平板电脑复制多个数据结构,并且预计不会经常产生更好的修剪机会。在服务器上,它是一个较轻的操作,因为原始的扫描谓词不需要复制(因为它可以在适当的位置进行操作),并且大多数优化步骤已经发生了。