Flume源码解析

202 阅读8分钟

 

source 到 channel 事务流程

image.png

 

1.      Source会采集一批数据,封装为event,缓存达到batch data的最大容量时(batch data的大小取决于配置参数batch size的值),Flume开启事务:

doPut():将这批event写入到临时缓冲区putList,putList是一个LinkedBlockingDeque,大小取决于配置Channel的参数transaction capacity的大小。

doCommit():检查channel内存队列是否足够合并,内存队列的大小由Channel的capacity参数控制, Channel的容量内存队列足够的时候,提交event成功。

doRollback(): channel内存队列空间不够时,回滚,这里会将整个putList中的数据都扔掉,然后给Source返回一个ChannelException异常,告诉Source数据没有采集上。Source会重新采集这批数据,然后开启新的事务。

 

2.      doTake():sink将数据剪切取到临时缓冲区takeList,takeList也是一个LinkedBlockingDeque,

大小取决于配置Channel的参数transaction capacity的大小,同时也拷贝一份放入写往HDFS的IO流中。

doCommit():如果event全部发送成功,就清除takeList。

doRollback():如果发送过程中出现异常,回滚,将takeList中的全部event归还给Channel。这个操作可能导致数据重复,如果已经写入一半的event到了HDFS,但是回滚时会向channel归还整个takeList中的event,后续再次开启事务向HDFS写入这批event时候,就出现了数据重复。

【1】LinkedBlockingDeque是一个基于链表实现的双向阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,可以看做无界队列,但也可以设置容量限制,作为有界队列。

 

  【2】相比于其他阻塞队列,LinkedBlockingDeque 多了 addFirst、addLast、peekFirst、peekLast 等方法。以first结尾的方法,表示插入、获取或移除双端队列的第一个元素。以 last 结尾的方法,表示插入、获取或移除双端队列的最后一个元素。但本质上并没有优化锁的竞争情况,因为不管是从队首还是队尾,都是在竞争同一把锁,只不过数据插入和获取的方式多了

 

1.      Application 入口

a)       校验启动的命令行参数,命令行参数作为main方法的arg传入

b)      判断有无  -no-reload-conf   参数。若有 ,则配置只加载一次,若没有,则默认30s加载一次。只要配置变更,便通知所有组件线程停掉,重新创建,开启。 现在要用热加载配置要配合zk。

c)      处理所有配置,创建对应组件实例   org.apache.flume.node.AbstractConfigurationProvider#getConfiguration

d)     停掉所有组件线程                  org.apache.flume.node.Application#stopAllComponents

e)     初始化所有组件             org.apache.flume.node.Application#initializeAllComponents

f)      启动所有组件线程,每个组件都由继承lifecycleAware,统一进行生命周期管理     org.apache.flume.node.Application#startAllComponents

i.          定时3秒后启动所有channel,调用具体实现类的start()方法,进行相应初始化(如队列,文件等)。然后将生命周期置为 start

        org.apache.flume.lifecycle.LifecycleSupervisor#supervise  -》 org.apache.flume.channel.file.FileChannel#start

 

ii.          睡眠等待确认channel是start状态后,启动所有sink

1.      sink分为单sink包装和多sink包装 

2.      pollingRunner包含sinkProcess包装类,sinkProcess包装类包含具体sink,先调用具体sink start方法再将具体sink置为启动,再启动pollingRunner线程,循环间隔调用具体sink 的 sinkProcess方法

 

iii.          启动所有source  (sourceRunner在创建时就分为两种类型,Event驱动和轮询)org.apache.flume.source.EventDrivenSourceRunner#start

1.       获取 sourceRunner拥有的source具体实现类,

2.       获取channel 处理器(封装了拦截器和渠道选择器,是channel和source的中间者),调用处理类(主要是拦截器)的初始化方法

3.       启动source,置为start。如果是pollable类型,是用一个PollingRunner线程间隔轮询调用具体的Source类的doProcess方法

 

         第一层是LifecycleAware 接口(生命周期的控制方法),被sinkRunner,sourceRunner抽象类实现,其包含具体soucre,sinkProcess实现类的属性。sinkRunner和sourceRunner又有各自的不同类型的实现类,正在的实现启动,停止时对source、sink、轮询线程的管理

        channel接口继承LifecycleAware。主要包含put,take方法。

        sinkProcess继承LifecycleAwar,主要是用代理模式,sinkRunner调sinkProcess,sinkProcess调具体sink

image.png

image.png

image.png

image.png

image.png

image.png

2.      source 拉取数据              

org.apache.flume.source.kafka.KafkaSource#doProcess

a)       while true 一直poll kafka数据直到 达到配置条件调用 processEventBatch 方法 ,该方法先调用拦截器处理这批event,再选择对应channel的缓冲队列进行存放                     org.apache.flume.channel.ChannelProcessor#processEventBatch

b)     遍历每个channel,获取其包含的事务对象,开启事务,拿到对应缓冲队列中的数据,遍历,调用公共抽象类的put方法,检查后再调用具体实现类的doPut方法

                   org.apache.flume.channel.file.FileChannel.FileBackedTransaction#doPut

c)      channel 具体实现类的doPut方法,例如FileChannel,先加读锁(读-读不互斥,读-写互斥,写-写互斥),再调用Log类的put方法写数据,再在putList(可能会有溢出报错的可能)里记录当前事件的文件id和位置offset构成的对象。 最后往事务写入队列里新增事务。inflightPuts.addEvent(transactionID, e.toLong());

d)     如果所有该批次事务处理成功,则调用FileChannel的commit方法把该事务持久化,然后删掉inflightPuts的对应事务。最后回到source,提交位移

e)     如果事务失败,putList清空,事务记为完成 inflightTakes.completeTransaction(transactionID); 。log将回滚操作持久化。回到kafkaSource捕获异常,计数器增加一次失败,然后回到PollingRunner线程,然后像之前一样间隔一下又去拉kafka。只是因为kafkaSource没提交位移,又重新拉。

f)      checkPoint 是 PutList 在某一稳定时刻的“快照”,而且每隔一段时间(checkpointInterval)File Channel会将内存队列持久化到磁盘文件,也就是我们配置的检查点目录下。为了保证内存队列“快照”的完整性,再将内存队列持久到磁盘文件时需要锁定内存队列,就是说此过程不Source不能写Channel,Sink也不能读Channel。你没猜错,上面的backupCheckpointDir就是检查点目录的备份目录,因为检查点文件是经常读写的,很容易在Flume Crash时导致文件损坏,所以如果要做到快速恢复,就可以给检查点配置一个复本。

 

3.      sink 拉取和写入数据

a)        SinkRunner线程会调用SinkProcess,sinkProcess会调用其拥有的sink的process方法

org.apache.flume.sink.hdfs.HDFSEventSink#process

b)     process方法先拿channel再拿事务,打开事务后先初始化一个BufferWriter类型的set集合,再调用channel的take方法

c)      take方法最终调用fileChannel的doTake方法,先加读锁,所以和doPut?不互斥(应该互斥)。然后queue.removeHead(transactionID)会将事务加入inflightTakes队列,之后takeList.offer将记录当前事件的文件id和位置offset构成的对象加入takeList,成功之后log将take操作持久化。

d)     若成功,每返回一个event给sink,sink拿到文件名,根据文件名拿对应BufferWriter和一个hdfswriter,没有则创建 ,最终就是调用outSteam(实际上就是hdfs客户端的FSOutputStream)写到流里,

每次写入都会判断是否达到滚动条件,然后进行滚动,刷数据,清理线程池。(如果配置了rollInval 间隔时间,开启的是定时线程,时间到了就会关闭,刷数据)

BucketWriter类用来滚动文件、处理文件格式以及数据的序列化等操作,其实就是负责数据 的写的

具体处理的写有以下几种

HDFSSequenceFile :configure(context)方法会首先获取写入格式writeFormat即参 数"hdfs.writeFormat",默认格式是二进制的 Writable(HDFSWritableSerializer.Builder.class),还有一个是 Text(HDFSTextSerializer.Builder.class),第三个是null;再获取是否使用HDFS本地文件系 统"hdfs.useRawLocalFileSystem",默认是flase不使用;然后获取writeFormat的所有配置信息 serializerContext;然后根据writeFormat和serializerContext构造 SequenceFileSerializer的对象serializer。在serializer中并无serializerContext配置的方 法,在1.3.0中此处的serializerContext没有任何作用,可能是为以后做的预留。

 

HDFSDataStream:configure(context)方法先获取serializerType类型,默认是 TEXT(BodyTextEventSerializer.Builder.class),此外还有 HEADER_AND_TEXT(HeaderAndBodyTextEventSerializer.Builder.class)、 OTHER(null)、AVRO_EVENT(FlumeEventAvroEventSerializer.Builder.class)共四种类 型;再获取是否使用HDFS本地文件系统"hdfs.useRawLocalFileSystem",默认是flase不使用;然后获取 serializer的所有配置信息serializerContext。serializer的实例化是在 HDFSDataStream.open(String filePath)方法中实现的。此处的serializerContext在BodyTextEventSerializer和 HeaderAndBodyTextEventSerializer均未用到,可能是做预留,但是 FlumeEventAvroEventSerializer在其Builder中用到了,并进行了配置。

 

HDFSCompressedDataStream:configure(context)方法和 HDFSDataStream.configure(context)是一样的,serializerType的类型是一样的;其他也是一样。 serializer的实例化是在HDFSCompressedDataStream.open(String filePath)方法中实现的,调用open(String filePath, CompressionCodec codec,CompressionType cType)来实例化。

 

writer为HDFSSequenceFile:append(event)方法,会先通过serializer.serialize(e)把event处理成一个Key和一个Value。

  (1)serializer为HDFSWritableSerializer时,则Key会是 event.getHeaders().get("timestamp"),如果没有"timestamp"的Headers则使用当前系统时间 System.currentTimeMillis(),然后将时间封装成LongWritable;Value是将event.getBody()封装 成BytesWritable,代码是bytesObject.set(e.getBody(), 0, e.getBody().length);

  (2)serializer为HDFSTextSerializer时,Key和上述HDFSWritableSerializer一样;Value 会将event.getBody()封装成Text,代码是textObject.set(e.getBody(), 0, e.getBody().length)。

  writer.append(event)中会将Key和Value,writer.append(record.getKey(), record.getValue())。

  writer为HDFSDataStream:append(event)方法直接调用serializer.write(e)。

  (1)serializer为BodyTextEventSerializer,则其write(e)方法会将e.getBody()写入输出流,并根据配置再写入一个"\n";

  (2)serializer为HeaderAndBodyTextEventSerializer,则其write(e)方法会将e.getHeaders() + " "(注意此空格)和e.getBody()写入输出流,并根据配置再写入一个"\n";

  (3)serializer为FlumeEventAvroEventSerializer,则其write(e)方法会将event整体写入dataFileWriter。

  writer为HDFSCompressedDataStream:append(event)方法会首先判断是否完成一个阶段的压缩 isFinished,如果是则更新压缩输出流的状态,并isFinished=false,否则剩下的执行和 HDFSDataStream.append(event)相同。

 

e)     sink继续调take,循环直到batchSize 停止。把outStream flush刷到hdfs。然后和source类似事务的处理。先是用hdfs提供的FileSystem打开一个

f)      如果报错的时候没有去调用rollback(自己实现的sink忘了),会导致文件id一直在takeList或者putList,后面就检查不出这个id是过期的,过期文件不会被删调。

g)     在调用write写的时候,是用线程的方式进行,就是并发的忘outStream里写,所以threadPoolSize就是用在这里。