第21章 实时ETL与流处理:Kafka基础、Spark Streaming入门、Flink基础概念、实时ETL架构设计
各位同学,欢迎来到实时ETL的世界。
说实话,我刚开始做数据工程师那会儿,实时处理还是个挺奢侈的事。那时候跑个离线任务,凌晨两点起来看调度结果都是家常便饭。后来业务部门跟我说:「能不能让数据延迟控制在分钟级?」我心想,这要求可真不低。
但今天,实时ETL已经成了数据架构的标配。你想想看,电商大促的实时大屏、风控系统的毫秒级拦截、推荐系统的实时特征更新……哪一样离得开流处理?
这一章,我就带你把这几个核心组件捋一遍。Kafka、Spark Streaming、Flink,还有实时ETL的架构设计,咱们一个一个来。
21.1 Kafka基础:实时数据管道的心脏
Kafka是什么?说白了,就是一个高吞吐、可持久化的消息队列系统。它最初由LinkedIn开发,后来捐给了Apache基金会。我个人的习惯是,把它看作「数据的高速公路」——所有实时数据都在这条路上跑,谁想用数据,谁就上路去取。
21.1.1 核心概念
- Producer(生产者):往Kafka里写数据的角色。比如你的业务日志、埋点数据,都由Producer发出去。
- Consumer(消费者):从Kafka里读数据的角色。Spark Streaming、Flink这些计算引擎,通常就是Consumer。
- Topic(主题):数据分类的标签。比如订单数据放一个Topic,用户行为放另一个Topic。
- Partition(分区):Topic的物理分片。分区越多,并行度越高,吞吐量越大。
- Broker(代理节点):Kafka集群中的一台服务器。多个Broker组成集群,保证高可用。
关键点:Kafka不处理数据,它只负责存和传。真正的计算逻辑,交给下游的流处理引擎。
21.1.2 我在项目中踩过的坑
我曾经遇到过一个场景:业务方要求数据延迟不超过5秒,但Kafka的消费端总是有积压。排查下来发现,是Topic的分区数设置得太少,导致Consumer的并行度不够。后来我把分区数从3调整到12,问题就解决了。
嗯,这里要注意:分区数不是越多越好。分区太多,Broker的元数据管理压力会变大,而且Consumer的Rebalance时间也会变长。一般建议分区数不超过Broker数量的10倍。
21.1.3 简单示例:生产与消费
# 启动Kafka(假设你已经安装了Kafka)
bin/kafka-server-start.sh config/server.properties
# 创建一个Topic,3个分区,1个副本
bin/kafka-topics.sh --create --topic order_events --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092
# 启动一个生产者,手动输入消息
bin/kafka-console-producer.sh --topic order_events --bootstrap-server localhost:9092
> order_id:1001, amount:299.00
> order_id:1002, amount:159.00
# 启动一个消费者,查看消息
bin/kafka-console-consumer.sh --topic order_events --from-beginning --bootstrap-server localhost:9092
21.2 Spark Streaming入门:微批处理的代表
Spark Streaming是Spark生态里的流处理组件。它的核心思想是「微批处理」——把源源不断的流数据,切成一个个小批次,然后用Spark引擎去处理每个批次。
我刚开始用Spark Streaming的时候,总觉得它不够「实时」。因为微批处理天然就有延迟,批次间隔最小也要几百毫秒。但后来我发现,对于大多数业务场景,秒级延迟完全够用了。而且Spark Streaming的生态好,能和Spark SQL、MLlib无缝集成,这一点很香。
21.2.1 DStream与结构化流
- DStream(离散化流):Spark Streaming的原始API。把流数据抽象成一系列RDD。操作方式和RDD类似,但要注意状态管理。
- Structured Streaming(结构化流):Spark 2.x之后推出的新API。基于DataFrame/Dataset,支持事件时间、水印、Exactly-Once语义。我个人更推荐用这个,写起来像批处理一样简单。
21.2.2 代码示例:从Kafka读取数据
from pyspark.sql import SparkSession
from pyspark.sql.functions import from_json, col
from pyspark.sql.types import StructType, StructField, StringType, DoubleType
# 创建SparkSession
spark = SparkSession.builder \
.appName("KafkaSparkStreaming") \
.getOrCreate()
# 定义数据Schema
schema = StructType([
StructField("order_id", StringType(), True),
StructField("amount", DoubleType(), True)
])
# 从Kafka读取流数据
df = spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "order_events") \
.load()
# 解析JSON数据
parsed_df = df.select(from_json(col("value").cast("string"), schema).alias("data")) \
.select("data.*")
# 实时计算总金额
result_df = parsed_df.groupBy().sum("amount")
# 输出到控制台
query = result_df.writeStream \
.outputMode("update") \
.format("console") \
.start()
query.awaitTermination()
小提示:Structured Streaming默认使用微批处理模式。如果你需要更低的延迟,可以开启连续处理模式(Continuous Processing),但要注意它目前只支持部分操作。
21.3 Flink基础概念:真正的流处理引擎
如果说Spark Streaming是「伪实时」,那Flink就是「真实时」。Flink的设计哲学是「事件驱动」——每来一条数据,就处理一条,不做批次切分。所以它的延迟可以做到毫秒级。
我记得第一次用Flink做实时风控,业务方要求延迟不超过100毫秒。Spark Streaming的微批处理模式,最小间隔也要500毫秒,根本满足不了。换成Flink之后,延迟直接降到了50毫秒以内。嗯,这就是Flink的硬实力。
21.3.1 Flink的核心概念
- DataStream:Flink中的流数据抽象。你可以对它做各种转换操作,比如map、filter、keyBy、window。
- Operator:算子,就是数据处理的基本单元。一个Flink作业由多个Operator组成。
- Keyed Stream:按某个字段分组的流。比如按用户ID分组,然后计算每个用户的实时行为。
- Window(窗口):把无限流切分成有限块。常见的有滚动窗口、滑动窗口、会话窗口。
- State(状态):Flink的杀手锏。它可以在算子内部维护状态,比如累加器、缓存、历史数据。状态可以持久化到后端存储,保证故障恢复。
21.3.2 代码示例:Flink消费Kafka
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.flink.api.common.serialization.SimpleStringSchema
import java.util.Properties
// 设置Kafka参数
val props = new Properties()
props.setProperty("bootstrap.servers", "localhost:9092")
props.setProperty("group.id", "flink-consumer-group")
// 创建执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 创建Kafka消费者
val consumer = new FlinkKafkaConsumer[String](
"order_events",
new SimpleStringSchema(),
props
)
// 读取数据流
val stream = env.addSource(consumer)
// 解析并计算
stream
.map(line => {
val fields = line.split(",")
(fields(0).split(":")(1).toInt, fields(1).split(":")(1).toDouble)
})
.keyBy(0) // 按订单ID分组(这里只是演示,实际不会这样分组)
.sum(1) // 累加金额
.print()
// 执行作业
env.execute("Flink Kafka Example")
注意:Flink的Checkpoint机制是保证Exactly-Once语义的关键。生产环境中一定要开启Checkpoint,并配置好状态后端(比如RocksDB)。我曾经见过有人没开Checkpoint,结果作业挂了之后数据全丢了……那叫一个惨。
21.4 实时ETL架构设计
好了,基础组件讲完了。现在咱们聊聊怎么把这些东西串起来,搭一个靠谱的实时ETL架构。
我个人的经验是,实时ETL架构可以分成三层:数据接入层、计算处理层、数据输出层。下面这张图,就是典型的实时ETL架构。
21.4.1 架构设计要点
- 数据接入层:所有数据先进入Kafka。这样做的好处是解耦——数据生产者和消费者互不影响。而且Kafka能缓冲流量峰值,防止下游被冲垮。
- 计算处理层:根据业务需求选择Spark Streaming或Flink。如果对延迟要求不高(秒级),且团队熟悉Spark生态,选Spark Streaming。如果要求毫秒级延迟,或者需要复杂的状态管理,选Flink。
- 数据输出层:处理完的数据,写到下游存储。常见的有:
- 实时数仓(ClickHouse、Doris)
- 搜索引擎(Elasticsearch)
- 缓存(Redis)
- 消息队列(Kafka)
21.4.2 避坑指南
我曾经在做一个实时大屏项目时,犯过一个低级错误:没有考虑数据倾斜。某个热门商品的订单量是其他商品的100倍,结果那个分区的处理速度跟不上,导致整个作业延迟飙升。
后来怎么解决的?我用了Flink的自定义分区器,把热点数据打散到多个子分区,再在算子内部做局部聚合。嗯,效果立竿见影。
总结一下:实时ETL不是简单的「Kafka + 计算引擎」拼凑。你要考虑数据一致性、容错性、延迟、吞吐量、运维成本……每一个点都可能成为瓶颈。
21.5 本章小结
这一章我们聊了Kafka、Spark Streaming、Flink,还有实时ETL的架构设计。说白了,实时ETL的核心就是「数据流进来,处理完流出去」。但怎么流得快、流得稳、流得准,才是真正的技术活。
我个人建议,初学者可以先从Kafka + Spark Streaming入手,因为Spark的生态更成熟,学习曲线更平缓。等你对流处理有了感觉,再深入Flink,体验一下真正的「事件驱动」。
好了,这一章就到这里。记住,纸上得来终觉浅,绝知此事要躬行。去搭个环境,跑几个Demo,比看十遍文章都管用。
公众号:蓝海资料掘金营,微信deep3321