基于 Flink 的实时滤波系统建设实践

363 阅读4分钟

实时平台初期架构

在数据系统未建设初期,由于对实时数据的需求较少,并且功能比较单一,形成不了完整的数据体系。我们采用的是“一路到底”的开发模式即,数据入库后通过脚本程序获取数据库的数据进行简单过滤并分库分表存储。

图1 初期实时数据架构

但是,随着产品和业务人员对数据需求的不断增多,新的挑战也随之发生。

  1. 数据越来越多,“随心所欲”的开发导致数据随处拷贝,数据量剧增。
  2. 需求越来越多,有的需要阈值过滤,有的需要算法辅助分析过滤,单一的开发模式导致后续代码难以维护。
  3. 缺少实时性,无法在对业务产生影响之前发现并修复问题。## 实时数据仓库的构建

实时数据过滤的构建

为解决以上问题,我们根据生产离线数据的经验,选择使用分层设计方案来建设过滤系统,其分层架构如下图所示:

架构图 (2).png 该方案由以下四层构成:

  1. ODS 层:主要由业务实时数据队列组成。
  2. 数据层:业务领域整合配置数据对明细数据进行补充,构建实时数据。
  3. 数据过滤层:使用滤波算法对数据进行过滤
  4. App 层:为了具体需求而构建的应用层

通过多层设计我们可以将处理数据的流程沉淀在各层完成。比如在数据过滤层统一完成数据的过滤、清洗、规范、脱敏流程;在数据层加工数据。提高了数据的复用率。

我们对主要的实时计算引擎进行了技术调研。总结了各类引擎特性如下表所示:

实时计算方案列表如下:

项目/引擎StormFlinkspark-treaming
API灵活的底层 API 和具有事务保证的 Trident API流 API 和更加适合数据开发的 Table API 和 Flink SQL 支持流 API 和 Structured-Streaming API 同时也可以使用更适合数据开发的 Spark SQL
容错机制ACK 机制State 分布式快照保存点RDD 保存点
状态管理Trident State状态管理Key State 和 Operator State两种 State 可以使用,支持多种持久化方案有 UpdateStateByKey 等 API 进行带状态的变更,支持多种持久化方案
处理模式单条流式处理单条流式处理Mic batch处理
延迟毫秒级毫秒级秒级
语义保障At Least Once,Exactly OnceExactly Once,At Least OnceAt Least Once

从调研结果来看, Flink 在数据延迟上和 Storm 更接近,对现有应用影响最小。而且在公司内部的测试中 Flink 的吞吐性能对比 Storm 有十倍左右提升。综合考量我们选定 Flink 引擎作为实时数据滤波引擎。

下图展示一个完整的使用 Flink 引擎生产一张实时数据的过程:

实时数据处理.png

实时计算方案时序图如下:

sequence时序图.png

使用文档

  1. 开发人员通过物连网的品牌型号管理界面设置过滤配置,目前主要由(过滤字段、采样数,滤波算法,阈值)组成
  2. 如配置界面没有所需要的滤波算法,需在代码里嵌入继承AbstractConvertFilter类并加入@Component、@Filter 并实现如doFilter接口:
package com.aero.flink.processors.converter.filter;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ReflectUtil;
import com.aero.flink.processors.converter.filter.annotation.Filter;
import com.aero.service.data.po.base.ConvertBase;
import com.aero.service.device.entiy.FilterRule;
import com.aero.service.device.enums.FilterAlgorithmEnum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Component;

/**
 * @author ksssss
 * @date 2022/8/25 下午12:02
 */
@Component
@Filter
public class SimpleConvertFilter extends AbstractConvertFilter {

    public SimpleConvertFilter() {
        // 所使用的算法枚举
        super(FilterAlgorithmEnum.CLIPPING_FILTER);
    }


    /**
     * @param filterRule   代表过滤规则,目前主要由(过滤字段、采样数,滤波算法,阈值)组成
     * @param convertBases 代表实时数据
     * @return 返回过滤了的数据
     */
    @Override
    protected List<? extends ConvertBase> doFilter(FilterRule filterRule, List<? extends ConvertBase> convertBases) {
        List<ConvertBase> filterBases = new ArrayList<>();
        for (ConvertBase convertBase : convertBases) {
            Double threshold = filterRule.getThreshold();
            String convertField = filterRule.getFieldName();
            Object valueObj = ReflectUtil.getFieldValue(convertBase, convertField);
            List<Double> values = new ArrayList<>();
            if (valueObj instanceof List) {
                values = (List<Double>) valueObj;
            } else if (valueObj instanceof Double) {
                values = Collections.singletonList((Double) valueObj);
            }
            if (CollectionUtil.isEmpty(values)) {
                continue;
            }
            if (filterRule.isLessOrEqual()) {
                boolean match = values.stream().anyMatch(val -> val <= threshold);
                if (match){
                    filterBases.add(convertBase);
                }
            }
            if (filterRule.isGreaterOrEqual()) {
                boolean match = values.stream().anyMatch(val -> val >= threshold);
                if (match){
                    filterBases.add(convertBase);
                }
            }
        }
        return filterBases;
    }
}

并添加属于自己的算法枚举如:

package com.aero.service.device.enums;

import com.fasterxml.jackson.annotation.JsonFormat;

/**
 * @author ksssss
 * @date 2022/8/31 下午5:05
 */
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum FilterAlgorithmEnum {
    CLIPPING_FILTER("限幅滤波", 0),
    ;
    public final String name;
    public final int sign;

    FilterAlgorithmEnum(String name, int sign) {
        this.name = name;
        this.sign = sign;
    }

    public static FilterAlgorithmEnum getFilterAlgorithmBySign(int sign){
        for (FilterAlgorithmEnum value : values()) {
            if (value.sign == sign) {
                return value;
            }
        }
        return null;
    }
}

过滤类的类图如下图所示:

未命名文件 (2).png