由于工作需要,在数据收集上牵扯到多个维度的爬虫数据。之前的流程是:爬虫工程师通过文件方式保存爬取的数据,交付给我们做数据清洗处理,再导入到数据库。为了降低交互过程中的时间成本,提高效率,我们开始引入流处理的方式。
之前的模式:
使用流处理之后的模式:
通过kafka-python包的生产者写入数据
首先,需要对爬虫脚本进行改造。原先的写入文件代码部分可以不需要改动,只要对爬取到的数据增加写入kafka操作即可(kafka的相关配置可见上一篇文章)。
爬虫数据可分为两种,一种是定期的全量数据更新,另一种是增量数据。对于全量数据更新,我们可以在写入kafka中增加时间戳来区分版本。
通过kafka-python包,可以很方便的写入数据到kafka中:
from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers=['broker1:port', 'broker2:port', ...])
topic = "crawler"
# 写入kafka的数据需为二进制内容,所以需要进行转化。
# 对于object内容,可先通过json转化成JSON STRING,再用bytes转为二进制。
# spark有from_json函数可以转化JSON STRING
for i in range(100):
producer.send(topic, bytes('some_message_bytes' + str(i), encoding="utf-8"))
参考资料:
Structured Streaming消费Kafka数据
Spark提供了很好的批流统一API,而最近刚推出的delta也是如此。这样,流处理也能受益于针对Dataframe的优化。
为了使用kafka数据源,需要加载相应的jar包,所以在启动pyspark或者是通过spark-submit提交时,需要加入相关依赖:
$ pyspark --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.4.3
$ spark-submit --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.4.3
使用maven做项目管理时,可以把spark-sql-kafka加入到依赖中
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-sql-kafka-0-10 -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql-kafka-0-10_2.12</artifactId>
<version>2.4.3</version>
<scope>provided</scope>
</dependency>
使用readStream来读入流,指定format为kafka,kafka的broker配置以及绑定的主题(可以绑定多个主题)。还可以指定offset的位置(有latest, earliest以及具体对每一个topic的每一个分区进行指定)。
df = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "host1:port1,host2:port2") \
.option("subscribe", "topic1,topic2") \
.option("startingOffsets", """{"topic1":{"0":23,"1":-2},"topic2":{"0":-2}}""") \
.load()
这样子,获得到的dataframe就可以使用DataFrame相关的操作。得到清洗后的数据就可以写入到sink。spark常用的文件格式为parquet,有关parquet的介绍可以参考后面的链接资料。
# 写入到parquet文件
df.writeStream.format("parquet"). \
option("path", "save_path"). \
option("checkpointLocation", "save_path/checkpoints"). \
start()
大家可能会困惑Structured Streaming是怎么对kafka的offset进行管理。我们看到读入流的时候要设置offset,那么如果程序中断之后再重启会是怎样呢?
这里,我们注意到流写入到sink的时候,必须要设置一个checkpointLocaion,Structured Streaming就是在这个目录下来管理offset。如果程序中断之后再重启,虽然在读入流的时候设置的是某一个offset,但是在写入流的时候,如果已经存在了checkpointLocation,那么流会从之前中断的地方继续处理,即读入流对offset的设置只是针对checkpointLocation第一次初始化的时候有效。
在实际使用Structured Streaming的时候,我们也遇到了一些问题:
-
对于长期运行的Structured Streaming程序,如何做到动态使用资源
首先先评估是否有必要使用长期运行的streaming程序,如果对数据实时性要求没那么高,可以考虑做定期的流任务。如果需要长期运行,可以考虑spark的动态分配资源选项(听闻bug比较多):
--conf spark.dynamicAllocation.enabled=true \ --conf spark.dynamicAllocation.initialExecutors=2 \ --conf spark.dynamicAllocation.minExecutors=2 \ --conf spark.dynamicAllocation.maxExecutors=5
-
使用Structured Streaming写入parquet文件时,会导致产生很多小的parquet文件,这样子对HDFS的namenode压力比较大
可以参考这篇文章, 主要的两个解决方案是:
- 在Structured Streaming中通过coalesce来减少分区,从而减少写入的parquet文件数量
- 通过运行一个批处理程序来读入多个小parquet文件,通过repartition为指定数量后再写入parquet文件
参考资料: