大数据程序猿

都在说实时数据架构,你了解多少?

2020-04-28  本文已影响0人  Flink中文社区

作者:刘大龙@唯品会

随着互联网的发展进入下半场,数据的时效性对企业的精细化运营越来越重要, 商场如战场,在每天产生的海量数据中,如何能实时有效的挖掘出有价值的信息, 对企业的决策运营策略调整有很大帮助。此外,随着 5G 技术的成熟、广泛应用, 对于工业互联网、物联网等数据时效性要求非常高的行业,企业就更需要一套完整成熟的实时数据体系来提高自身的行业竞争力。

本文从上述现状及实时数据需求出发,结合工业界案例、笔者的实时数据开发经验, 梳理总结了实时数据体系建设的总体方案,本文主要分为三个部分:

第一部分主要介绍了当下在工业界比较火热的实时计算引擎 Flink 在实时数据体系建设过程中主要的应用场景及对应解决方案;
第二部分从实时数据体系架构、实时数据模型分层、实时数据体系建设方式、流批一体实时数据架构发展等四个方面思考了实时数据体系的建设方案;
第三部分则以一个具体案例介绍如何使用 Flink SQL 完成实时数据统计类需求。

一、Flink 实时应用场景

目前看来,Flink 在实时计算领域内的主要应用场景主要可分为四类场景, 分别是实时数据同步、流式 ETL、实时数据分析和复杂事件处理,具体的业务场景和对应的解决方案可详细研究下图, 文字层面不再详述。

640.png

二、实时数据体系架构

实时数据体系大致分为三类场景:流量类、业务类和特征类,这三种场景各有不同。

2.1 实时数据体系整体架构

640 2.jpg

整个实时数据体系架构分为五层,分别是接入层,存储层,计算层、平台层和应用层,上图只是整体架构的概要图,每一层具体要做的事情,接下来通过文字来详述。


渠道文章宣传内页.png

其中,平台层详细工作如下:

  1. 统一查询服务支持从底层明细数据到聚合层数据的查询,支持以SQL化方式查询Redis、Hbase等KV存储中的数据;
  2. 元数据及指标管理:主要对实时的Kafka表、Kudu表、Clickhouse表、Hive表等进行统一管理,以数仓模型中表的命名方式规范表的命名,明确每张表的字段含义、使用方,指标管理则是尽量通过指标管理系统将所有的实时指标统一管理起来,明确计算口径,提供给不同的业务方使用;
  3. 数据质量及血缘分析:数据质量分为平台监控和数据监控两个部分,血缘分析则主要是对实时数据依赖关系、实时任务的依赖关系进行分析。

平台监控部分一是对任务运行状态进行监控,对异常的任务进行报警并根据设定的参数对任务进行自动拉起与恢复,二是针对 Flink 任务要对 Kafka 消费处理延迟进行监控并实时报警。

数据据监控则分为两个部分,首先流式 ETL 是整个实时数据流转过程中重要的一环,ETL 的过程中会关联各种维表,实时关联时,定时对没有关联上的记录上报异常日志到监控平台,当数量达到一定阈值时触发报警, 其次,部分关键实时指标采用了 lambda 架构,因此需要对历史的实时指标与离线 hive 计算的数据定时做对比,提供实时数据的数据质量监控,对超过阈值的指标数据进行报警。

为了配合数据监控,需要做实时数据血缘,主要是梳理实时数据体系中数据依赖关系,以及实时任务的依赖关系,从底层ODS 到 DW 再到 DM,以及 DM 层被哪些模型用到, 将整个链条串联起来,这样做在数据/任务主动调整时可以通知关联的下游,指标异常时借助血缘定位问题,同时基于血缘关系的分析,我们也能评估数据的应用价值,核算数据的计算成本。

2.2 实时数据模型分层

640 3.png

离线数仓考虑到效率问题,一般会采取空间换时间的方式,层级划分会比较多;实时数仓考虑到实时性问题,分层则越少越好,另外也减少了中间流程出错的可能性,因此将其分为四层。

■ ODS 层:

■ DWD 层:

■ DIM 层:

■ DM 层:

(1)数据集市层

以数据域+业务域的理念建设公共汇总层,对于DM层比较复杂,需要综合考虑对于数据落地的要求以及具体的查询引擎来选择不同的存储方式,分为轻度汇总层和高度汇总层,同时产出,高度汇总层数据用于前端比较简单的KV查询, 提升查询性能,比如实时大屏,实时报表等,数据的时效性要求为秒级,轻度汇总层Kafka中宽表实时写入OLAP存储引擎,用于前端产品复杂的OLAP查询场景,满足自助分析和产出复杂报表的需求,对数据的时效性要求可容忍到分钟级;

(2)轻度汇总层
(3)高度汇总层

总体来说 DM 层对外提供三种时效性的数据:

首先是 Flink 等实时计算引擎预计算好的秒级实时指标,这种需求对数据的时效性要求非常高,用于实时大屏、计算维度不复杂的实时报表需求。

其次是 Spark SQL 预计算的延迟在分钟级的准实时指标, 该类指标满足一些比较复杂但对数据时效性要求不太高的数据分析场景,可能会涉及到多个事实表的join,如销售归因等需求。

最后一种则是不需要预计算,ad-hoc查询的复杂多维数据分析场景,此类需求比较个性化,灵活性比较高,如果 OLAP 计算引擎性能足够强大,也可完全满足秒级计算需求的场景; 对外提供的秒级实时数据和另外两种准实时数据的比例大致为 3:7,绝大多数的业务需求都优先考虑准实时计算或 ad-hoc 方式,可以降低资源使用、提升数据准确性,以更灵活的方式满足复杂的业务场景。

2.3 实时数据体系建设方式

整个实时数据体系分为两种建设方式,即实时和准实时(它们的实现方式分别是基于流计算引擎和 ETL、OLAP 引擎,数据时效性则分别是秒级和分钟级。

2.4 流批一体实时数据架构发展

从1990年 Inmon 提出数据仓库概念到今天,大数据架构经历了从最初的离线大数据架构、Lambda 架构、Kappa 架构以及 Flink 的火热带出的流批一体架构,数据架构技术不断演进,本质是在往流批一体的方向发展,让用户能以最自然、最小的成本完成实时计算。

为了应对业务方更复杂的多维实时数据分析需求,笔者目前在数据开发中引入 Kudu这个 OLAP 存储引擎,对订单等业务数据使用 Presto + Kudu 的计算方案也是在探索流批一体架构在实时数据分析领域的可行性。此外,目前比较热的数据湖技术,如 Delta lake、Hudi 等支持在 HDFS 上进行 upsert 更新,随着其流式写入、SQL 引擎支持的成熟,未来可以用一套存储引擎解决实时、离线数据需求,从而减少多引擎运维开发成本。

三、Flink SQL 实时计算 UV 指标

上一部分从宏观层面介绍了如何建设实时数据体系,非常不接地气,可能大家需要的只是一个具体的 case 来了解一下该怎么做,那么接下来用一个接地气的案例来介绍如何实时计算 UV 数据。大家都知道,在 ToC 的互联网公司,UV 是一个很重要的指标,对于老板、商务、运营的及时决策会产生很大的影响,笔者在电商公司,目前主要的工作就是计算 UV、销售等各类实时数据,体验就特别深刻, 因此就用一个简单demo 演示如何用 Flink SQL 消费 Kafka 中的 PV 数据,实时计算出 UV 指标后写入 Hbase。

3.1 Kafka 源数据解析

PV 数据来源于埋点数据经 FileBeat 上报清洗后,以 ProtoBuffer 格式写入下游 Kafka,消费时第一步要先反序列化 PB 格式的数据为 Flink 能识别的 Row 类型,因此也就需要自定义实现 DeserializationSchema 接口,具体如下代码, 这里只抽取计算用到的 PV 的 mid、事件时间 time_local,并从其解析得到 log_date 字段:

public class PageViewDeserializationSchema implements DeserializationSchema<Row> {

    public static final Logger LOG = LoggerFactory.getLogger(PageViewDeserializationSchema.class);
    protected SimpleDateFormat dayFormatter;

    private final RowTypeInfo rowTypeInfo;

    public PageViewDeserializationSchema(RowTypeInfo rowTypeInfo){
        dayFormatter = new SimpleDateFormat("yyyyMMdd", Locale.UK);
        this.rowTypeInfo = rowTypeInfo;
    }
    @Override
    public Row deserialize(byte[] message) throws IOException {
        Row row = new Row(rowTypeInfo.getArity());
        MobilePage mobilePage = null;
        try {
            mobilePage = MobilePage.parseFrom(message);
            String mid = mobilePage.getMid();
            row.setField(0, mid);
            Long timeLocal = mobilePage.getTimeLocal();
            String logDate = dayFormatter.format(timeLocal);
            row.setField(1, logDate);
            row.setField(2, timeLocal);
        }catch (Exception e){
            String mobilePageError = (mobilePage != null) ? mobilePage.toString() : "";
            LOG.error("error parse bytes payload is {}, pageview error is {}", message.toString(), mobilePageError, e);
        }
        return null;
    }

3.2 编写 Flink Job 主程序

将 PV 数据解析为 Flink 的 Row 类型后,接下来就很简单了,编写主函数,写 SQL 就能统计 UV 指标了,代码如下:

public class RealtimeUV {

    public static void main(String[] args) throws Exception {
        //step1 从properties配置文件中解析出需要的Kakfa、Hbase配置信息、checkpoint参数信息
        Map<String, String> config = PropertiesUtil.loadConfFromFile(args[0]);
        String topic = config.get("source.kafka.topic");
        String groupId = config.get("source.group.id");
        String sourceBootStrapServers = config.get("source.bootstrap.servers");
        String hbaseTable = config.get("hbase.table.name");
        String hbaseZkQuorum = config.get("hbase.zk.quorum");
        String hbaseZkParent = config.get("hbase.zk.parent");
        int checkPointPeriod = Integer.parseInt(config.get("checkpoint.period"));
        int checkPointTimeout = Integer.parseInt(config.get("checkpoint.timeout"));

        StreamExecutionEnvironment sEnv = StreamExecutionEnvironment.getExecutionEnvironment();
        //step2 设置Checkpoint相关参数,用于Failover容错
        sEnv.getConfig().registerTypeWithKryoSerializer(MobilePage.class,
                ProtobufSerializer.class);
        sEnv.getCheckpointConfig().setFailOnCheckpointingErrors(false);
        sEnv.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
        sEnv.enableCheckpointing(checkPointPeriod, CheckpointingMode.EXACTLY_ONCE);
        sEnv.getCheckpointConfig().setCheckpointTimeout(checkPointTimeout);
        sEnv.getCheckpointConfig().enableExternalizedCheckpoints(
                CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

        //step3 使用Blink planner、创建TableEnvironment,并且设置状态过期时间,避免Job OOM
        EnvironmentSettings environmentSettings = EnvironmentSettings.newInstance()
                .useBlinkPlanner()
                .inStreamingMode()
                .build();
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(sEnv, environmentSettings);
        tEnv.getConfig().setIdleStateRetentionTime(Time.days(1), Time.days(2));

        Properties sourceProperties = new Properties();
        sourceProperties.setProperty("bootstrap.servers", sourceBootStrapServers);
        sourceProperties.setProperty("auto.commit.interval.ms", "3000");
        sourceProperties.setProperty("group.id", groupId);

        //step4 初始化KafkaTableSource的Schema信息,笔者这里使用register TableSource的方式将源表注册到Flink中,而没有用register DataStream方式,也是因为想熟悉一下如何注册KafkaTableSource到Flink中
        TableSchema schema = TableSchemaUtil.getAppPageViewTableSchema();
        Optional<String> proctimeAttribute = Optional.empty();
        List<RowtimeAttributeDescriptor> rowtimeAttributeDescriptors = Collections.emptyList();
        Map<String, String> fieldMapping = new HashMap<>();
        List<String> columnNames = new ArrayList<>();
        RowTypeInfo rowTypeInfo = new RowTypeInfo(schema.getFieldTypes(), schema.getFieldNames());
        columnNames.addAll(Arrays.asList(schema.getFieldNames()));
        columnNames.forEach(name -> fieldMapping.put(name, name));
        PageViewDeserializationSchema deserializationSchema = new PageViewDeserializationSchema(
                rowTypeInfo);
        Map<KafkaTopicPartition, Long> specificOffsets = new HashMap<>();
        Kafka011TableSource kafkaTableSource = new Kafka011TableSource(
                schema,
                proctimeAttribute,
                rowtimeAttributeDescriptors,
                Optional.of(fieldMapping),
                topic,
                sourceProperties,
                deserializationSchema,
                StartupMode.EARLIEST,
                specificOffsets);
        tEnv.registerTableSource("pageview", kafkaTableSource);

        //step5 初始化Hbase TableSchema、写入参数,并将其注册到Flink中
        HBaseTableSchema hBaseTableSchema = new HBaseTableSchema();
        hBaseTableSchema.setRowKey("log_date", String.class);
        hBaseTableSchema.addColumn("f", "UV", Long.class);
        HBaseOptions hBaseOptions = HBaseOptions.builder()
                .setTableName(hbaseTable)
                .setZkQuorum(hbaseZkQuorum)
                .setZkNodeParent(hbaseZkParent)
                .build();
        HBaseWriteOptions hBaseWriteOptions = HBaseWriteOptions.builder()
                .setBufferFlushMaxRows(1000)
                .setBufferFlushIntervalMillis(1000)
                .build();
        HBaseUpsertTableSink hBaseSink = new HBaseUpsertTableSink(hBaseTableSchema, hBaseOptions, hBaseWriteOptions);
        tEnv.registerTableSink("uv_index", hBaseSink);

        //step6 实时计算当天UV指标sql, 这里使用最简单的group by agg,没有使用minibatch或窗口,在大数据量优化时最好使用后两种方式
        String uvQuery = "insert into uv_index "
                + "select log_date,\n"
                + "ROW(count(distinct mid) as UV)\n"
                + "from pageview\n"
                + "group by log_date";
        tEnv.sqlUpdate(uvQuery);
        //step7 执行Job
        sEnv.execute("UV Job");
    }
}

以上就是一个简单的使用 Flink SQL 统计 UV 的 case, 代码非常简单,只需要理清楚如何解析 Kafka 中数据,如何初始化 Table Schema,以及如何将表注册到 Flink中,即可使用 Flink SQL 完成各种复杂的实时数据统计类的业务需求,学习成本比API 的方式低很多。说明一下,笔者这个 demo 是基于目前业务场景而开发的,在生产环境中可以真实运行起来,可能不能拆箱即用,你需要结合自己的业务场景自定义相应的 kafka 数据解析类。

参考资料:

Flink Use Cases
基于Flink的严选实时数仓实践
如果你也想做实时数仓
菜鸟供应链实时数仓的架构演进及应用场景
美团点评基于 Flink 的实时数仓平台实践
知乎实时数仓架构实践及演进
OPPO数据中台之基石:基于Flink SQL构建实数据仓库
友信金服基于Flink构建实时用户画像系统的实践
实时数据中台,如何能做得更好?

上一篇下一篇

猜你喜欢

热点阅读