1. 背景
在企业大数据处理系统中,Flink 和 ClickHouse 都是强大的数据处理工具,各自在数据流处理和实时分析方面发挥着重要作用。然而,在实际应用中,部分业务(尤其是需求多变的场景)更适合将更多的数据处理逻辑移至下游的 ClickHouse SQL 层,而不是在 Flink 中进行复杂的处理,具有显著的优势。
2. 现状
2.1 车联网会将不同主题的数据写入Kafka
比如驾驶信息主题(Kafka)、能耗信息主题(Kafka)、车辆项目流转主题(MYSQL)。
注:由于有个服务专门对这里的话题进行了基础的清洗,所以大数据部门这里直接将这三个话题作为DWD层
2.2 基于Flink消费和回写Kafka构建实时数仓
DWB层将两个驾驶信息主题和能耗信息主题双流Join,然后再和车辆项目流转主题进行异步维表查询关联。
DWS层基于DWB的宽表进行聚合,提取时间戳生成WaterMark,制定WaterMark策略,然后进行分组,开窗,聚合。
3. 为什么改造
Flink实时消费Kafka的主题,经过Flink编码实现了实时指标写入业务DB,这个过程发现几个问题:
- 业务DB所在的主从节点磁盘占用异常高。
- 业务需求经常变化需要重新大量编码和发版,不是很好维护。
- DWB制作好之后需要回写Kafka,增加了和Kafka的交互周期
- Flink最优秀的WaterMark处理乱序流在我们业务场景不能最佳实践,因为车端数据上报存在跨天的补发,如果做到严格不丢数据,最大乱序时间需要设置非常大,窗口关闭会很漫长,失去了实时的意义。
对于问题1,Flink实时写入业务DB不是标准的OLAP流程,会导致Binlog大量滚动,导致磁盘占用过多;
对于问题2,业务需求增加或者变化时我们需要想办法迅速满足业务需求;
对于问题3&4,如果将DWS层的聚合移到OLAP,那么DWB制作好后可以不回写Kafka,而是直接写入CK,在Kafka中减少了层数,而且CK天然擅长的就是多列的聚合。
3. 怎么改造
在调研了几款OLAP后,选择了表引擎丰富而且社区成熟性能优秀的Clickhouse,将采用标准的实时写入OLAP,接口调用OALP数据的方式进行改造。
改造主旨就是Flink轻逻辑,改造主旨就是Flink数据处理逻辑移至下游的CK层,既避免了业务DB磁盘问题,又避免了业务需求新增无法敏捷开发的问题。
3.1 第一期
- CK建表过程
-- 本地表
CREATE TABLE bigdata.fast_drive_base_local_v1 on cluster xxx_cluster --注意这里加入on cluster xxx_cluster才可以自动在每个节点建表
(
`create_time` DateTime,
`local_time` DateTime64(3, 'Asia/Shanghai'),
`productName` String,
`time` String,
`deviceName` String,
`mode` String,
`sequence` Int32,
`throttle` Float64,
`pilot` String,
`gnss` String,
`serverTime` String,
`steer` Float64,
`speed` Float64,
`_ver` UInt32 --在去重的基础上防止乱序
)
ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/fast_drive_base_local_v1', '{replica}',_ver)
PARTITION BY toYYYYMMDD(create_time)
PRIMARY KEY time
ORDER BY (time, deviceName) --根据车辆时间和车辆号去重
TTL create_time + toIntervalDay(7) --实时数据处理不需要保存太久的数据
-- 分布式表
CREATE TABLE bigdata.fast_drive_base_distributed on cluster xxx_cluster
(
`create_time` DateTime,
`local_time` DateTime64(3, 'Asia/Shanghai'),
`productName` String,
`time` String,
`deviceName` String,
`mode` String,
`sequence` Int32,
`throttle` Float64,
`pilot` String,
`gnss` String,
`serverTime` String,
`steer` Float64,
`speed` Float64
)
ENGINE = Distributed('xxx_cluster', 'bigdata', 'fast_drive_base_local_v1', cityHash64(deviceName)); --根据车辆编号hash分布
-- 这里注意关联本地表:fast_drive_base_local_v1
- 接口层调用CK的SQL示例/实时指标示例
-- 今日实时里程指标
select deviceName,
round(CAST(max(manual) as Float64) - CAST(min(manual) as Float64), 2) as manual,
round(CAST(max(autonomous) as Float64) - CAST(min(autonomous) as Float64), 2) as autonomous
from (
select deviceName,
autonomous,
manual
from default.fast_drive_base_distributed --累计里程明细表
where toDate(toUnixTimestamp(create_time)) = toDate(now())
and cast(manual as Float64) != 0
) l1
group by deviceName;
- 问题
总体来说第一期已经解决了老业务的大部分问题,但是业务繁忙时CK的CPU会打的有点高,虽然不影响业务我还是进行了第二期的优化。
3.2 第二期
- CK建表过程
-- 本地表
CREATE TABLE bigdata.fast_drive_base_local_v2 on cluster xxx_cluster
(
`create_time` DateTime,
`local_time` DateTime64(3, 'Asia/Shanghai'),
`productName` String,
`time` String,
`deviceName` String,
`mode` String,
`sequence` Int32,
`throttle` Float64,
`pilot` String,
`gnss` String,
`serverTime` String,
`steer` Float64,
`speed` Float64,
`_ver` UInt32
PROJECTION mqtt_drive (
SELECT *
ORDER BY
deviceName,
time
) -- 这里加入了CK的高阶功能:投影
)
ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/fast_drive_base_local_v2', '{replica}',_ver)
PARTITION BY toYYYYMMDD(create_time)
PRIMARY KEY time
ORDER BY (time, deviceName)
TTL create_time + toIntervalDay(7)
-- 分布式表
CREATE TABLE bigdata.fast_drive_base_distributed_v2 on cluster xxx_cluster
(
`create_time` DateTime,
`local_time` DateTime64(3, 'Asia/Shanghai'),
`productName` String,
`time` String,
`deviceName` String,
`mode` String,
`sequence` Int32,
`throttle` Float64,
`pilot` String,
`gnss` String,
`serverTime` String,
`steer` Float64,
`speed` Float64
)
ENGINE = Distributed('xxx_cluster', 'bigdata', 'fast_drive_base_local_v2', cityHash64(deviceName)); --根据车辆编号hash分布,注意这里对应的本地表
- 接口层柔性增加指标
select carNo,
latitude_nan_cnt, --纬度异常数量/经度同理
latitude_null_cnt --纬度为空数量/经度同理
from (
select carNo,
sum(latitude_nan) latitude_nan_cnt,
sum(latitude_null) latitude_null_cnt
from (
select carNo,
case when latitude = 'NaN' then 1 else 0 end latitude_nan,
case when latitude = '' then 1 else 0 end latitude_null
from bigdata.fast_drive_base_distributed_v2
where toDate(toUnixTimestamp(create_time)) = toDate(today())
) l1
group by carNo
) l2
where latitude_nan_cnt != 0
and latitude_null_cnt != 0
having latitude_nan_cnt > 600
or latitude_null_cnt > 600
order by carNo;
- 结论
有效解决了一期的问题:
由于该业务大量指标会按照车辆编号和时间查询/过滤,所有我优化了这种特定的查询,通过Projection预计算和组织数据,显著提高查询效率。