11.1 MiniBatch 聚合
针对无界聚合算子,说简单点就是把一组输入的数据放到缓存里,减少吞吐的开销 默认情况下,对于无界聚合算子来说,mini-batch 优化是被禁用的。开启这项优化,需要设置选项
TableConfig configuration = tEnv.getConfig();
configuration.set("table.exec.mini-batch.enabled", "true"); //开启小批量优化
configuration.set("table.exec.mini-batch.allow-latency", "5 s"); //缓存5秒的输入记录
configuration.set("table.exec.mini-batch.size", "5000"); // 每个聚合运算符任务可以缓冲的最大记录数
11.2 Local-Global 聚合
Local-Global 聚合是为解决数据倾斜问题提出的,通过将一组聚合分为两个阶段,首先在上游进行本地聚合,然后在下游进行全局聚合,类似于 MapReduce 中的 Combine + Reduce 模式。简单来说就是map端聚合之后reduce处理map端聚合的数据。
Configuration configuration = tEnv.getConfig().getConfiguration();
configuration.setString("table.exec.mini-batch.enabled", "true"); //本地-全局聚合取决于是否启用了mini-batch
configuration.setString("table.exec.mini-batch.allow-latency", "5 s");
configuration.setString("table.exec.mini-batch.size", "5000");
configuration.setString("table.optimizer.agg-phase-strategy", "TWO_PHASE"); //启用两阶段聚合,即local-global聚合
11.3 拆分 distinct 聚合
**使用场景:**Local-Global 优化可有效消除常规聚合的数据倾斜,例如 SUM、COUNT、MAX、MIN、AVG。但是在处理 distinct 聚合时,其性能并不令人满意。
如果 distinct key (即 user_id)的值分布稀疏,则 COUNT DISTINCT 不适合减少数据。即使启用了 local-global 优化也没有太大帮助。因为累加器仍然包含几乎所有原始记录,并且全局聚合将成为瓶颈(大多数繁重的累加器由一个任务处理,即同一天)。
这个优化的想法是将不同的聚合(例如 COUNT(DISTINCT col)
)分为两个级别。第一次聚合由 group key 和额外的 bucket key 进行 shuffle。bucket key 是使用 HASH_CODE(distinct_key) % BUCKET_NUM
计算的。BUCKET_NUM
默认为1024,可以通过 table.optimizer.distinct-agg.split.bucket-num
选项进行配置。第二次聚合是由原始 group key 进行 shuffle,并使用 SUM
聚合来自不同 buckets 的 COUNT DISTINCT 值。由于相同的 distinct key 将仅在同一 bucket 中计算,因此转换是等效的。bucket key 充当附加 group key 的角色,以分担 group key 中热点的负担。bucket key 使 job 具有可伸缩性来解决不同聚合中的数据倾斜/热点。
类比离线中处理数据倾斜时。将key打散成很多份之后再聚合。
如何开启:
tEnv.getConfig() .set("table.optimizer.distinct-agg.split.enabled", "true"); // enable distinct agg split
11.4 在 distinct 聚合上使用 FILTER 修饰符
在某些情况下,用户可能需要从不同维度计算 UV(独立访客)的数量,例如来自 Android 的 UV、iPhone 的 UV、Web 的 UV 和总 UV。很多人会选择 CASE WHEN
,例如:
SELECT day,
COUNT(DISTINCT user_id) AS total_uv,
COUNT(DISTINCT CASE WHEN flag IN ('android', 'iphone') THEN user_id ELSE NULL END) AS app_uv,
COUNT(DISTINCT CASE WHEN flag IN ('wap', 'other') THEN user_id ELSE NULL END) AS web_uv
FROM T GROUP BY day
但是,在这种情况下,建议使用 FILTER
语法而不是 CASE WHEN。因为 FILTER
更符合 SQL 标准,并且能获得更多的性能提升。FILTER
是用于聚合函数的修饰符,用于限制聚合中使用的值。将上面的示例替换为 FILTER
修饰符,如下所示:
SELECT day,
COUNT(DISTINCT user_id) AS total_uv,
COUNT(DISTINCT user_id) FILTER (WHERE flag IN ('android', 'iphone')) AS app_uv,
COUNT(DISTINCT user_id) FILTER (WHERE flag IN ('wap', 'other')) AS web_uv
FROM T GROUP BY day
Flink SQL 优化器可以识别相同的 distinct key 上的不同过滤器参数。例如,在上面的示例中,三个 COUNT DISTINCT 都在 user_id
一列上。Flink 可以只使用一个共享状态实例,而不是三个状态实例,以减少状态访问和状态大小。在某些工作负载下,可以获得显著的性能提升。