关于项目中使用Structure Streaming的一点记录

写这篇日志主要是为了记录下在工作中的一点心得,防止以后碰到相同的问题的时候忘记。
      在今年的第2个季度时,搭建实时的数据仓库替换之前运行在阿里云上的流式统计程序成了我们第二个季度的主要任务。公司的流计算的  主要的任务分为日志清洗和清洗后的数据统计,在做这项任务的时候,在技术选型阶段,我们当时有Flink和Spark两个考虑。
其中Flink在处理乱序数据方面有先天的优势;而Spark在我们公司之前的项目中有使用(不会消耗新的集群资源),并且Spark的也有支持处理乱序数据的框架那就是Structure Streaming;Flink和Structure Streaming在处理乱序方面都是利用了WaterMark。
公司的数据流向为:niginx(记录各段埋点的日志数据)->flume(监控文件,抽取数据到kafka中)->Spark数据接入程序(进行日志的初步过滤,)->Spark(数据清洗)->Spark(数据统计)。
       其中我负责编写的任务有flume抽取数据到kafka(这部分比较简单只需要配置就完事了,其中使用了taildir的方式去监控多个文件,并且写入到kafka中),在写Kafka的时候,由于我们传入的key值为null所以写kafka topic的时候:kafka每隔topic.metadata.refresh.interval.ms(kafka的server.properties中配置)才会随机选择一个partition进行写入,在topic.metadata.refresh.interval.ms之内时将会往kafka中缓存的partition中写入。ps:当时我以为只是简单的随机写入。
在写数据接入程序的时候,当时面临一个非常严重的Json处理的问题,由于各端上报的数据全都是以Json为主,并且好多还是多层嵌套Json,当时我第一次使用GSON进行处理,然而在碰到嵌套Json的时候,却无法成功的转换为JSONObject而导致丢掉了一些正确的数据,后来成功的使用FastJson完美解决了这个问题。在此强推一波FastJson,这个由Ali开源的工具是真的很好用啊!
       在解决了第一个JSON处理的问题之后,后面又碰到了一个Spark Streaming输出日志的问题,由于我们需要进行错误日志的记录,当时我们使用的是Log4j进行日志输出,但是由于程序是分布式运行的所以导致在Driver端初始化了log4j的配置,而在excutor端没有被初始化,所以导致输出日志的时候没有输出到指定文件,后来采用在每个excutor端进行log4j初始化成功解决了这个问题。
     在写接入程序的时候我们还碰到了Base64解码的问题,由于我们的日志都是Base64编码的,而各个端由于在上报的时候由于隔断编解码的时候会出现附带多的字符,所以在解码时先对Base64字符串进行EUrlncode再UrlDecode后解决了这个问题,当然这只是基于Java的语言的问题,而再使用Python时候,是不会出现这样的问题的。
       在4月份的时候我开始了负责编写流式统计任务,由于在kafka多partition的时候无法保证数据消费的顺序性,而可能出现数据消费的乱序而导致数据的延迟到达,所以采用了Structure Streaming的方式来进行数据统计,而Structure Streaming使用基于WaterMark的方式解决这个问题,说起WaterMark时先谈流式计算的时候会出现以下这三个时间的概念:处理时间、事件时间、数据进行处理器的时间。而为了保证数据处理的正确性使数据达到真正的Exactly once的特性,我们使用了事件时间。说回WaterMark它就是一个时间点,在该时间点之前的数据表示已经被全部处理完,只有时间在它之后的才能被处理,而在它之前的将会被丢弃而不会处理。那它是怎样实现处理延迟到达的数据呢?它通过延迟生成水印的方式来实现处理延迟的数据,并且它也只能处理延迟一定时间的数据,而超过该允许延迟时间之外的数据也将不会被处理。比如我们允许延迟4分钟的数据被处理,则在刚开始的数据到达的时候,先根据数据的时间点减去4分钟,则为当前的WaterMark。比如数据时间为12:00,则当前的WaterMark为11:56,下一条数据时间为12:01,则WaterMark为11:57,下一条数据时间为12:02,则WaterMark为11:58,而WaterMark之前的数据将被丢弃,之前的数据才会被处理,所以通过这种方式我们很好的实现了延迟的数据处理。并且在水印时间大于窗口的EndTime的时候针对整个窗口的数据处理才会被执行,如果一致没有数据流入来使水印增加的时候那么整个流查询是不会进行下去的。
       其中Structure Streaming使用基于窗口的方式来进行数据处理,在该窗口内的数据将会被一起处理,由于Structure Streaming是基于DataSet的抽象数据结构在封装数据的,所以我们可以很好的使用SQL方式来编写程序,而窗口也只是一个封装好的Struct数据结构数据,其中包含了StartTime,EndTime两种时间,并且该窗口是前闭后开的,如果要获取窗口的时间则可以直接获取。
       另一个很重要的是Structure Streaming不能支持一个流的多次聚合函数的操作,比如一个DataSet我们不能多次进行分组聚合,这在Structure Streaming是被禁止的。同样如果想在对一个输入流进行多个不同的维度统计的话,我们需要开启多个流查询,则我们不能在一个维度统计WriteStream.start()后直接query.awaitetermination,而是要通过流查询管理器等待多个流查询结束的方式spark.streams.awaitAnyTermination()实现,这个请参考spark官网。
      在开发完程序之后,我们将任务部署到了Yarn上,由于Structure Streaming默认开启了checkpoint的机制,所以他会在任务执行的时候在hdfs中记录相关的程序执行记录。特别是在进行数据统计使用了聚合的时候,由于会将聚合的中间结果保存到hdfs中并且,默认一个partition为一个目录,这会造成大量的小文件增加了namenode的压力,可能会导致namenode经常死掉。所以解决方法有:
                 1)减小shuffle分区的数量(默认为200个):spark.sql.shuffle.partitions = 200
                 2)减小每个批次聚合的中间状态保存的最小批次数(默认为100个批次):spark.sql.streaming.minBatchesToRetain = 100

          在统计完侯,由于我们需要写入数据库中进行后面的进一步操作,但是Spark中没有默认的写数据库的函数,只是提供了一个接口ForeachWriter去实现这个接口实现自定义的写入操作,这个写入的时候是针对每个分区进行操作的,每一个分区会调用一次该ForeachWriter接口的方法,所以当时在配置数据库连接池的时候,由于没有理解这个导致当时数据库的连接经常超标,根据我们现在使用的数据库初始化连接数给10个最大20个现在是完全能够达到要求的。

          以上就是我在整个3月、4月的使用spark中遇到的问题,同时在消费kafka时候,消费者和分区的关系当时也碰到了问题,不过现在不打算在这讲清楚了,,当时碰到这些问题的时候踩了很多坑,最后在同事和万能百度上解决了,当前碰到的问题就只想到了这么多了(≧∇≦)ノ,希望这些能在以后帮到自己!准备睡觉了........

猜你喜欢

转载自blog.csdn.net/qq_32323239/article/details/90147365