flume拦截器及问题解决

big data 专栏收录该内容
6 篇文章 0 订阅

概述

Flume 除了主要的三大组件 Source、Channel和 Sink,还有一些其他灵活的组件,如拦截器、SourceRunner运行器、Channel选择器和Sink处理器等。

组件框架图

今天主要来看看拦截器,先看下组件框架流程图,熟悉了大致框架流程学习起来必然会更加轻松: 

  1. 接收事件
  2. 根据配置选择对应的Source运行器(EventDrivenSourceRunner 和 PollableSourceRunner)
  3. 处理器处理事件(Load-Balancing Sink 和 Failover Sink 处理器)
  4. 将事件传递给拦截器链
  5. 将每个事件传递给Channel选择器
  6. 返回写入事件的Channel列表
  7. 将所有事件写入每个必需的Channel,只有一个事务被打开
  8. 可选Channel(配置可选Channel后不管其是否写入成功)

拦截器

拦截器(Interceptor)是简单插件式组件,设置在Source和Channel之间,Source接收到event在写入到对应的Channel之前,可以通过调用的拦截器转换或者删除过滤掉一部分event。通过拦截器后返回的event数不能大于原本的数量。在一个Flume 事件流程中,可以添加任意数量的拦截器转换或者删除从单个Source中来的事件,Source将同一个事务的所有事件event传递给Channel处理器,进而依次可以传递给多个拦截器,直至从最后一个拦截器中返回的最终事件event写入到对应的Channel中。 
flume-1.7版本支持的拦截器: 

编写自定义拦截器

自定义的拦截器编写,我们只需要实现一个Interceptor接口即可,该接口的定义如下:

 
  1. public interface Interceptor {
  2. /* 任何需要拦截器初始化或者启动的操作就可以定义在此,无则为空即可 */
  3. public void initialize();
  4. /* 每次只处理一个Event */
  5. public Event intercept(Event event);
  6. /* 量处理Event */
  7. public List<Event> intercept(List<Event> events);
  8. /*需要拦截器执行的任何closing/shutdown操作,一般为空 */
  9. public void close();
  10. /* 获取配置文件中的信息,必须要有一个无参的构造方法 */
  11. public interface Builder extends Configurable {
  12. public Interceptor build();
  13. }
  14. }

接口中的几个方法或者内部接口含义代码中已经标注,需要留意的地方就是考虑到多线程运行Source时,需要保证编写的代码是线程安全的。这里就不展示自定义拦截器代码了,仿照已有的拦截器,可以很容易的编写一个简单功能的自定义拦截器的。

实际使用及问题

问题:

目前环境中使用的都是tailSource、hdfsSink,在sink时根据时间对日志分割成不同的目录,但是实际过程中存在一些延迟,导致sink写入hdfs时的时间和日志文件中记录的时间存在一些差异;并且不能保留原有的日志文件名。

需求:

  1. 根据日志中记录的时间对文件进行分目录存储
  2. 将source端读取的日志名字符串添加至hdfsSink写入hdfs的文件名中(在hdfs文件中可以根据文件名区分日志)

日志格式如下:

 
  1. 2017/01/13 13:30:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  2. 2017/01/13 14:50:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  3. 2017/01/13 15:52:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  4. 2017/01/13 16:53:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  5. 2017/01/14 13:50:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  6. 2017/01/14 13:50:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  7. 2017/01/14 14:50:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":
  8. 2017/01/14 14:56:00 ip:123.178.46.252 message:[{"s":"bbceif1484117100097","u":"354910072847819","id":"2x1kfBk63z","e":

如何实现以上需求?

  1. 要了解TaildirSource如何读取日志文件,按行读取还是按数据量大小? 
    分析代码可知,无论单个事件操作还是批量操作均是按行读取
  2. hdfsSink如何对文件进行分目录? 
    若定义了hdfs.useLocalTimeStamp = true ,则是根据本地时间戳分目录,否则是从事件的header中获取时间戳。

明白了这两个问题,就可以继续往前走了。

实现需求1

Source端: 
经过调研查阅资料发现,有拦截器就可以直接实现该目标功能。使用RegexExtractorInterceptor正则抽取拦截器,匹配日志中的时间字符串,将其添加至Event的header中(header的key值为timestamp),写入header时序列化只能使用org.apache.flume.interceptor.RegexExtractorInterceptorMillisSerializer(该序列化器内部根据配置传入的pattern将时间转换为时间戳格式):

 
  1. agent1.sources.r1.interceptors = inter
  2. agent1.sources.r1.interceptors.inter.type = regex_extractor
  3. agent1.sources.r1.interceptors.inter.regex = ^(\\d\\d\\d\\d/\\d\\d/\\d\\d\\s\\d\\d:\\d\\d:\\d\\d).*
  4. agent1.sources.r1.interceptors.inter.serializers = s1
  5. #agent1.sources.r1.interceptors.inter.serializers.s1.type = org.apache.flume.interceptor.RegexExtractorInterceptorPassThroughSerializer //该序列化内部只是将传入的匹配项直接返回return
  6. agent1.sources.r1.interceptors.inter.serializers.s1.type = org.apache.flume.interceptor.RegexExtractorInterceptorMillisSerializer
  7. agent1.sources.r1.interceptors.inter.serializers.s1.name = timestamp
  8. agent1.sources.r1.interceptors.inter.serializers.s1.pattern = yyyy/MM/dd HH:mm:ss

Sink端: 
Sink端只需要注意不要设置hdfs.useLocalTimeStamp 为 true,也就是不使用本地时间,默认为false即可。

 
  1. agent1.sinks.k1.type = hdfs
  2. agent1.sinks.k1.channel = c2
  3. agent1.sinks.k1.hdfs.path = /user/portal/tmp/syx/test2/%Y%m%d/%Y%m%d%H
  4. agent1.sinks.k1.hdfs.filePrefix = events-%[localhost]-%{timestamp} //%[localhost] 获取主机名,%{timestamp} 获取事件header中key为timestamp的值value
  5. #agent1.sinks.k1.hdfs.useLocalTimeStamp = true //注意此处直接使用Event header中的timestamp,不适用本地时间戳
  6. agent1.sinks.k1.hdfs.callTimeout = 100000

实现需求2

tailDirSource端使用参数:

fileHeader false Whether to add a header storing the absolute path filename.
fileHeaderKey file Header key to use when appending absolute path filename to event header.

fileHeader 设置为 true ,可以将日志文件的绝对路径存储在事件的header中; 
fileHeaderKey 目前来说不需要设置,它指定了存储在header中路径的key 名(header中是以key-value对存储),默认为 file。如下:

 
  1. Event: { headers:{timestamp=1452581700000, file=/home/hadoop_portal/tiany/test.log} body: 32 30 31 36 2F 30 31 2F 31 32 20 31 34 3A 35 35 2016/01/12 14:55 }

因为hdfsSink可以直接从事件的header中读取字串作为hdfs文件名的一部分,可以通过将日志文件名添加至header中来实现。现在看起来实现上述需求就很简单了,只需要将绝对路径名修改为文件名就行了,这就可以修改tailDirSource中往 Event 中添加header时的代码了,如下:

 
  1. //ReliableTaildirEventReader.java中的readEvents方法
  2. Map<String, String> headers = currentFile.getHeaders();
  3. if (annotateFileName || (headers != null && !headers.isEmpty())) {
  4. for (Event event : events) {
  5. if (headers != null && !headers.isEmpty()) {
  6. event.getHeaders().putAll(headers);
  7. }
  8. if (annotateFileName) { //判断是否需要设置日志文件路径名至header中,为boolean类型
  9. int lastIndex = currentFile.getPath().lastIndexOf('/'); //获取绝对路径中最后一次出现'/'的索引,根据索引获取路径中的文件名字串即可
  10. event.getHeaders().put(fileNameHeader, currentFile.getPath().substring(lastIndex+1));
  11. }
  12. }
  13. }

按以上方法操作,两个需求可以算就是完成了,flume测试跑了一天,很符合需求,以为这样任务就完成了吗? NO,隔了一晚上悲催的事就发生了,flume狂报错,日志显示无法从事件header中获取到时间戳timestamp,很纳闷,不是明明就将timestamp写入到header中了吗? 
为了检测header中是否真的没有timestamp,将Sink修改为logger Sink(因为该方式可以将事件的header和body以日志形式打印出来,方便查看),修改之后测试跑了几个小时,接下来就是分析log,发现确实如报错,Source过来的日志有一些确实是没有时间字段的。 
这种问题该如何解决呢?其实也是很简单的,研究RegexExtractorInterceptor拦截器的源代码,发现其中只是对匹配到指定格式时做了相应的处理,但是对于未匹配到的日志行时不做任何处理,因而修改源代码,在未匹配到指定字串时,添加默认的时间戳即可,但是不能为空,因为hdfsSink分目录时必须要从事件header中获取到timestamp的,否则就会报错,修改后代码如下:

 
  1. public Event intercept(Event event) {
  2. Matcher matcher = regex.matcher(
  3. new String(event.getBody(), Charsets.UTF_8));
  4. Map<String, String> headers = event.getHeaders();
  5. if (matcher.find()) { //匹配到执行if语句中代码
  6. for (int group = 0, count = matcher.groupCount(); group < count; group++) {
  7. int groupIndex = group + 1;
  8. if (groupIndex > serializers.size()) {
  9. if (logger.isDebugEnabled()) {
  10. logger.debug("Skipping group {} to {} due to missing serializer",
  11. group, count);
  12. }
  13. break;
  14. }
  15. NameAndSerializer serializer = serializers.get(group);
  16. if (logger.isDebugEnabled()) {
  17. logger.debug("Serializing {} using {}", serializer.headerName,
  18. serializer.serializer);
  19. }
  20. headers.put(serializer.headerName,
  21. serializer.serializer.serialize(matcher.group(groupIndex)));
  22. }
  23. //日志中没匹配到指定时间格式,添加当前时间为时间戳
  24. } else {
  25. long now = System.currentTimeMillis();
  26. headers.put("timestamp", Long.toString(now));
  27. }
  28. return event;
  29. }

maven重新打包,替换掉原先的flume-ng-core.jar包即可,重新运行问题解决。

注意:若使用了KafkaChannel,parseAsFlumeEvent 应该使用默认值true,因为在Sink时需要读取Event中的header内容。

总结

flume的拦截器还是很有用的,可以在写入Channel之前先对日志做一次清洗,根据实际的需求编写自定义拦截器或者使用已有的拦截器,可以很方便的完成一些需求。对于这次的问题,虽然解决了,但是还是感觉很尴尬(日志提供方给出的日志格式每条日志都有时间字段,怪他们?no),其实主要还是由于自己没有考虑全面,只需要几行代码的事。因此在今后的学习工作生活中,无论干神马事,都得方方面面考虑,对于开发人员来说特别是故障处理、应急处理,很重要的。

  • 4
    点赞
  • 1
    评论
  • 7
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值