一、离线报表晚一天,决策就晚一天
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 |
血的教训:
实时计算是数据的"高速公路",搭建好了,数据才能流得快。
个人观点,仅供参考