有了前两篇博文的基础,相信大家对Flume Agent的内部结构已经有了个初步的了解,现在我们来详细介绍最常用的文件通道——File Channel,本篇博客主要介绍Eevnt是如何完成写到File Channel这一操作的。
上一篇: http://manzhizhen.iteye.com/blog/2298159
Channel是联系Source和Sink的桥梁,内存Memory Channel性能虽高,但对于日志数据处理这块,实时并不是第一重要的,几乎所有以日志作为数据源的数据分析都只能说是近乎实时的。对于大多数数据分析来说,日志丢失是不可忍受的,所以,现在线上使用最多的Channel,就是File Channel。在大多数系统的设计中,为了保证高吞吐量,都会允许一小部分数据损失(比如每隔几秒再进行刷盘操作,将缓冲区的数据写如磁盘文件),但File Channel没有这样设计,它通过在一次事务中提交多个Event来提高吞吐量,做到了只要事务被提交,那么数据就不会有丢失。但需要注意,Flume Channel是没有数据副本的,意味着如果磁盘损坏,那么数据就无法恢复了。
我们先来看看File Channel有哪些属性可以配置:
我们在来看看,在Flume Agent的配置文件中,File Channel是怎么配置的,见下图:
上图是官网中的截图,可以看到,图中设置了两个目录,第一个是检查点的目录(checkpointDir),第二个是数据的目录(dataDirs)。。那么,检查点是做什么用的?我们知道,在File Channel中,有一个内存队列来保存已被Source写入但还未被Sink消费的Event数据的指针,Event指针指向的就是Event在数据目录下数据文件中存放位置,所以,你很自然的就能想到检查点指的就是内存队列在某一稳定时刻的“快照”,而且每隔一段时间(checkpointInterval)File Channel会将内存队列持久化到磁盘文件,也就是我们配置的检查点目录下。为了保证内存队列“快照”的完整性,再将内存队列持久到磁盘文件时需要锁定内存队列,就是说此过程不Source不能写Channel,Sink也不能读Channel。你没猜错,上面的backupCheckpointDir就是检查点目录的备份目录,因为检查点文件是经常读写的,很容易在Flume Crash时导致文件损坏,所以如果要做到快速恢复,就可以给检查点配置一个复本。
从GitHub上下载Flume的源码,直接定位到flume-ng-channels模块,在该模块中,你肯定直接找到的是FileChannel.java类,在FileChannel中,我们看到了如下常见属性(省略了部分其他属性):
private Integer capacity = 0; private int keepAlive; protected Integer transactionCapacity = 0; private Long checkpointInterval = 0L; private long maxFileSize; private long minimumRequiredSpace; private File checkpointDir; private File backupCheckpointDir; private File[] dataDirs; private Log log; private final ThreadLocal<FileBackedTransaction> transactions = new ThreadLocal<FileBackedTransaction>(); private boolean fsyncPerTransaction; private int fsyncInterval;
我接着看了下FileChannel的方法,发现大部分操作的实现是在上面的log属性中,于是,我点开了Log.java。用Eclipse的Ctrl+O的快捷键,我们直接预览Log类中的内部方法和属性,发现我们想要的都在里面,将Source将Event放入Channel肯定调用的是Log#put(long transactionID, Event event)方法,Sink从Channel取Event肯定调用的是Log#get(FlumeEventPointer pointer)方法等。如果直接看Log的这些方法,是不容易看明白它是怎么被调用,是被谁调用这些问题的,于是我们先直接从Source的角度来看Channel的Event写入过程。
每个Source都需要设置一个通道处理器(ChannelProcessor),写入Channel不是由Source来完成的,通道处理器用于暴露服务来将Event写入Channel,它是单线程的(Executors.newSingleThreadExecutor)。通道处理器写入Event的put操作有两种:processEvent和processEventBatch,分别用来写入单个Event和一次性批量写入多个Event,两个方法的内部处理过程都差不多。通道处理器将一个Event写入到Channel的整个过程包含5部分:1.将Event进行拦截器链进行过滤;2.通过通道选择器来选择该Event需要写入的哪些Channel;3.从Channel中获取一个事务;4.调用Channel的put方法将Event写入Channel;5.提交事务或回滚。其中如果某(批次)Event需要写入多个Channel,则步骤3-5是在for循环中执行的,可见,如果要将Event写入n个通道,则整个过程将产生n次事务操作。所以,纵观Source、Channel和Sink的实现,Channel的实现无疑是最重量级的。
咱们先看第1步,拦截器构造工厂(InterceptorBuilderFactory)将用户配置的拦截器组装成拦截器链,通道处理器将需要写入Channel的Event先放入拦截器链进行过滤,如果最后返回的Event不为空,说明没被过滤掉,拦截器不一定只是为了过滤Event,它还可以给Event的头部添加一些必要信息,比如数据的日志文件来源等。
第2步是确定该(批次)Event需要写入哪些Channel,这些Channel包括要求(required)的和可选(optional)的,要求的Channel是需要保证写入成功的(如果失败则会重试),可选的Channel只会尝试写入一次,不管失败与否。每个通道处理器实例都配置有一个通道选择器(ChannelSelector),通道处理器的构造器的入参就是通道选择器,Channel的选择就是由通道选择器来做的。通道选择器会对Event的头部信息来进行筛选,决定该Event需要写入到哪些Channel,具体配置可以参考官方文档,该部分不看代码也能明白其实现,所以通道选择器不做具体介绍了。
知道要写入哪些Channel后,通道抽利器将for循环遍历Channel列表将该(批次)Event依次放入Channel中,将该(批次)Event写入该Channel前,会先通过改Channel创建事务(步骤3),全部写入成功(步骤4)后将提交事务(步骤5,commit),否则会回滚(步骤5,rollback)并抛出异常。这将会有个潜在的问题,如果一个Event需要写入多个Channel,当写入其中某个Channel导致事务回滚时,由于在他之前的Channel已经写入成功了,所以当该(批次)Event再次被交给通道处理器时,上次那些已经成功写入Event的Channel将会被重复写入,这似乎是Flume设计的一个缺陷,所以只能下游的系统自己来处理重复消息了。写完要求(required)的Channel,将会继续将Event写入可选(optional)的Channel(如果配置了的话),写入可选的Event也是会开启事务的,但如果出错只会回滚但不会对外抛出异常。
咱们接着看第3步