【架构实战】实时计算架构:Flink处理亿级数据流

一、离线报表晚一天,决策就晚一天

2020年,我们的数据报表是T+1:

  • 每天凌晨跑批
  • 早上8点出报表
  • 运营看昨天的数据

问题:

  • 618大促时,销量激增
  • 运营想看实时数据调整策略
  • 但只能看昨天的数据
  • 错过最佳调整时机

引入实时计算后:

  • 数据秒级更新
  • 实时大屏展示
  • 实时预警
  • 运营决策效率提升10倍

二、实时计算架构

2.1 Lambda架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Lambda架构                                    │
│                                                                  │
│    数据源 → Kafka ─┬→ Flink实时处理 → 实时结果                    │
│                   │                                              │
│                   └→ HDFS离线存储 → Spark离线处理 → 离线结果       │
│                                                                  │
│    实时结果 ─┬→ 合并 → 服务层                                     │
│    离线结果 ─┘                                                   │
│                                                                  │
│    优点:容错性好,离线数据修正实时数据                             │
│    缺点:维护两套系统,复杂度高                                    │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

2.2 Kappa架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Kappa架构                                     │
│                                                                  │
│    数据源 → Kafka → Flink实时处理 → 结果存储 → 服务层             │
│                                                                  │
│    特点:                                                        │
│    - 只维护一套实时处理系统                                        │
│    - Kafka作为唯一数据源                                          │
│    - 通过重放历史数据实现重新计算                                   │
│                                                                  │
│    优点:架构简单,维护成本低                                      │
│    缺点:历史数据重放成本高                                        │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

三、Flink核心应用

3.1 实时数据大屏

java 复制代码
/**
 * 实时订单统计
 */
public class OrderStatisticsJob {
    
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. Kafka数据源
        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "kafka:9092");
        props.setProperty("group.id", "order-statistics");
        
        FlinkKafkaConsumer<Order> source = new FlinkKafkaConsumer<>(
            "order-topic",
            new OrderDeserializer(),
            props
        );
        
        DataStream<Order> orders = env.addSource(source);
        
        // 2. 实时统计
        orders
            // 按时间窗口分组(每5秒一个窗口)
            .keyBy(Order::getProductId)
            .window(TumblingProcessingTimeWindows.of(
                Time.seconds(5)))
            // 聚合计算
            .aggregate(new OrderAggregateFunction())
            // 写入Redis
            .addSink(new RedisSink<>(...));
        
        env.execute("Order Statistics");
    }
}

/**
 * 订单聚合函数
 */
public class OrderAggregateFunction implements 
    AggregateFunction<Order, OrderAccumulator, OrderResult> {
    
    @Override
    public OrderAccumulator createAccumulator() {
        return new OrderAccumulator();
    }
    
    @Override
    public OrderAccumulator add(Order order, OrderAccumulator acc) {
        acc.addAmount(order.getAmount());
        acc.addCount(1);
        return acc;
    }
    
    @Override
    public OrderResult getResult(OrderAccumulator acc) {
        return new OrderResult(
            acc.getProductId(),
            acc.getCount(),
            acc.getAmount(),
            System.currentTimeMillis()
        );
    }
}

3.2 实时风控

java 复制代码
/**
 * 实时风控监测
 */
public class RiskControlJob {
    
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. 消费用户行为流
        DataStream<UserBehavior> behaviors = env
            .addSource(new KafkaSource<>("user-behavior"));
        
        // 2. 实时规则检测
        behaviors
            .keyBy(UserBehavior::getUserId)
            .process(new RiskDetectionProcessFunction())
            .addSink(new AlertSink());
        
        env.execute("Risk Control");
    }
}

/**
 * 风险检测函数
 */
public class RiskDetectionProcessFunction extends 
    KeyedProcessFunction<Long, UserBehavior, RiskAlert> {
    
    // 状态:用户最近行为列表
    private ListState<UserBehavior> behaviorState;
    
    @Override
    public void open(Configuration parameters) {
        ListStateDescriptor<UserBehavior> descriptor = 
            new ListStateDescriptor<>(
                "behaviors",
                UserBehavior.class);
        behaviorState = getRuntimeContext().getListState(descriptor);
    }
    
    @Override
    public void processElement(UserBehavior behavior, Context ctx, 
                               Collector<RiskAlert> out) throws Exception {
        // 1. 添加到状态
        behaviorState.add(behavior);
        
        // 2. 获取最近1分钟的行为
        List<UserBehavior> recentBehaviors = new ArrayList<>();
        for (UserBehavior b : behaviorState.get()) {
            if (System.currentTimeMillis() - b.getTimestamp() < 60000) {
                recentBehaviors.add(b);
            }
        }
        
        // 3. 规则检测
        // 规则1:1分钟内登录失败超过5次
        long loginFailCount = recentBehaviors.stream()
            .filter(b -> b.getType() == BehaviorType.LOGIN_FAIL)
            .count();
        if (loginFailCount > 5) {
            out.collect(new RiskAlert(
                behavior.getUserId(), 
                "LOGIN_FAIL_TOO_MANY",
                loginFailCount));
        }
        
        // 规则2:1分钟内下单超过10笔
        long orderCount = recentBehaviors.stream()
            .filter(b -> b.getType() == BehaviorType.ORDER)
            .count();
        if (orderCount > 10) {
            out.collect(new RiskAlert(
                behavior.getUserId(), 
                "ORDER_TOO_FREQUENT",
                orderCount));
        }
        
        // 4. 注册定时器清理过期状态
        ctx.timerService().registerProcessingTimeTimer(
            System.currentTimeMillis() + 60000);
    }
}

3.3 实时ETL

java 复制代码
/**
 * 实时数据同步
 */
public class RealtimeETLJob {
    
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = 
            StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. Canal数据变更流
        DataStream<CanalEntry> canalStream = env
            .addSource(new CanalSource("order-table"));
        
        // 2. 数据清洗转换
        DataStream<Order> orders = canalStream
            .filter(entry -> entry.getEventType() == INSERT || 
                           entry.getEventType() == UPDATE)
            .map(entry -> convertToOrder(entry));
        
        // 3. 写入多个目标
        // 3.1 写入ES(搜索)
        orders.addSink(new ElasticsearchSink<>(...));
        
        // 3.2 写入Redis(缓存)
        orders.addSink(new RedisSink<>(...));
        
        // 3.3 写入ClickHouse(分析)
        orders.addSink(new ClickHouseSink<>(...));
        
        env.execute("Realtime ETL");
    }
}

四、状态管理

4.1 状态后端配置

java 复制代码
// 配置RocksDB状态后端
StreamExecutionEnvironment env = 
    StreamExecutionEnvironment.getExecutionEnvironment();

// 1. 使用RocksDB(大状态)
env.setStateBackend(new EmbeddedRocksDBStateBackend());

// 2. 配置Checkpoint
env.enableCheckpointing(60000); // 每60秒checkpoint一次
env.getCheckpointConfig().setCheckpointingMode(
    CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
env.getCheckpointConfig().enableExternalizedCheckpoints(
    ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

// 3. 配置Savepoint路径
env.getCheckpointConfig().setCheckpointStorage(
    "hdfs:///flink/checkpoints");

4.2 状态恢复

java 复制代码
// 从Savepoint恢复
StreamExecutionEnvironment env = 
    StreamExecutionEnvironment.getExecutionEnvironment();

env.setStateBackend(new EmbeddedRocksDBStateBackend());

// 指定Savepoint路径
String savepointPath = "hdfs:///flink/savepoints/savepoint-123";
env.getCheckpointConfig().setCheckpointStorage(savepointPath);

// 从Savepoint恢复执行
env.execute("My Job");

五、踩坑实录

坑1:背压问题

问题:下游消费慢,导致上游数据积压。

解决方案

java 复制代码
// 1. 增加并行度
env.setParallelism(8);

// 2. 异步Sink
orders.addSink(new AsyncRedisSink<>());

// 3. 调整缓冲区大小
env.getConfig().setBufferTimeout(100);

坑2:数据倾斜

问题:某些key数据量大,导致热点。

解决方案

java 复制代码
// 1. 加盐打散
orders.keyBy(order -> order.getUserId() + "_" + 
    RandomUtils.nextInt(0, 10))
    .window(...)
    
// 2. 两阶段聚合
orders.keyBy(order -> order.getUserId() + "_" + salt)
    .window(...)
    .aggregate(...)  // 第一阶段:局部聚合
    .keyBy(...)
    .window(...)
    .aggregate(...); // 第二阶段:全局聚合

坑3:状态过大

问题:状态无限增长,内存溢出。

解决方案

java 复制代码
// 1. 使用TTL清理过期状态
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.hours(24))
    .setUpdateType(UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateVisibility.NeverReturnExpired)
    .build();

ValueStateDescriptor<String> descriptor = 
    new ValueStateDescriptor<>("state", String.class);
descriptor.enableTimeToLive(ttlConfig);

// 2. 使用RocksDB状态后端
env.setStateBackend(new EmbeddedRocksDBStateBackend());

坑4:Exactly-Once不生效

问题:重启后数据重复或丢失。

解决方案

java 复制代码
// 1. 确保Sink支持事务
FlinkKafkaProducer<String> sink = new FlinkKafkaProducer<>(
    "topic",
    new SimpleStringSchema(),
    props,
    FlinkKafkaProducer.Semantic.EXACTLY_ONCE  // 开启事务
);

// 2. 配置Checkpoint
env.enableCheckpointing(60000);
env.getCheckpointConfig().setCheckpointingMode(
    CheckpointingMode.EXACTLY_ONCE);

六、最佳实践

6.1 Flink调优清单

复制代码
Flink调优清单:

□ 并行度
  □ 与Kafka分区数匹配
  □ 根据数据量调整

□ 内存
  □ TaskManager内存充足
  □ Network缓冲区足够

□ 状态
  □ 大状态用RocksDB
  □ 配置状态TTL

□ Checkpoint
  □ 间隔合理(1-5分钟)
  □ 超时时间充足
  □ 保留Checkpoint

□ 背压
  □ 监控背压指标
  □ 及时扩容

七、总结

实时计算核心要点:

组件 作用 选型
消息队列 数据缓冲 Kafka
计算引擎 实时处理 Flink
状态存储 状态管理 RocksDB
结果存储 结果输出 Redis/ES

血的教训:

实时计算是数据的"高速公路",搭建好了,数据才能流得快。


个人观点,仅供参考