1. 前言
Flume中spooldir类型的sources可以检测一个本地目录,并处理其中的文件。不过spooldir类型的sources有一个致命的问题:在读取文件发生异常时,比如:文件内容实际编码和flume启动时指定的配置文件中的编码设置不一致,就会报错,然后停止目录检测线程,但是已启动的flume agent进程并不会停止。如果发生了这种错误,我们通过jps命令来查看进程的状态,并发现不了这种错误,只能通过日志来查看。
这是非常难受的。我做的这个项目,需要使用Flume监视一个linux本地目录,有程序在不断的往这个目录中上传新的文件,Flume检测到新文件之后马上读取,如果我设置的输入编码(也就是监视目录中文件的编码)为UTF-8,但是别人往该目录中上传了一个GBK编码的文件,Flume就会报错,然后停止检测线程。
要解决这个问题,只能通过修改源码来实现了。
2. 开发工具
IDEA 2018.2.3
jdk 1.8.0_172 (子版本无所谓,应该没有影响)
maven 3.5.3 (中央仓库镜像使用的阿里云镜像)
apache-flume-1.8.0-src.tar.gz (Flume1.8源码,需要在Flume官网下载,注意下载的是源码)
3. 源码导入IDEA
将源码gz包用压缩软件解压,然后打开IDEA,菜单选择OPEN,然后打开源码即可,以下为截图:
打开源码之后,maven需要下载很多相关的依赖jar包,必要时需要手动点击已导入相关的依赖jar包,截图如下:
下载完之后,应该是有些jar包并没有下载完,这时需要我们手动去中央仓库:http://mvnrepository.com/ 下载没有导入的jar包。整个项目的pom.xml文件中有关非jar包依赖的错误可以不管。
4. flume-ng-core打包
flume-ng-core包下有自己的pom.xml文件,我们打包不用使用IDEA的maven插件打包,需要我们进入cmd命令窗口,使用mvn相关命令进行打包。
前提:Windows开发系统上安装并在环境变量中配置好了maven环境,cmd窗口中运行mvn -v命令能打印出maven的版本相关信息才行,截图如下:
打开资源管理器,找到flume-ng-core源码中pom.xml文件所在位置,比如我的位置为:D:\WorkSpace\apache-flume-1.8.0-src\flume-ng-core,截图如下:
一定要注意是flume-ng-core源码pom.xml文件所在的位置,不是整个flume1.8源码位置pom.xml文件位置。
在资源管理器地址栏输入:cmd,打开cmd窗口:
或者是直接打开cmd窗口,然后通过cd命令切换到flume-ng-core中pom.xml文件所在目录也行。
在cmd窗口运行命令:mvn clean package -DskipTests,清理并打包项目:
如果窗口卡住的话,多摁几次回车键即可。如果运行过程出错,重新运行命令即可。
打包成功之后,可以在flume-ng-core目录下找到targes目录,打开该目录,就有我们打包好的flume-ng-core-1.8.0.jar包:
将该jar包上传到linux服务器上替换掉原来的jar包(flume1.8/lib/目录下)即可。
5. 源码修改1,不推荐
这儿是源码修改部分,也是重中之重。
1. 网上找到的一般源码修改方法:
1. 源码修改:
org.apache.flume.source.SpoolDirectorySourceConfigurationConstants
将该类中88行代码中的 FAIL 改为 IGNORE 。
然后将flume-ng-core源码打包上传替换linux服务器中的jar包。具体打包方式查看 flume-ng-core打包 。
2. 测试:
1. 编写自定义拦截器插件:
主要代码:
package cn.com.bonc;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
public class TestIgnore implements Interceptor {
@Override
public void initialize() {
}
@Override
public Event intercept(Event event) {
Logger logger = LoggerFactory.getLogger(TestIgnore.class);
try {
String fileName = event.getHeaders().get("fileName");
String gbk = new String(event.getBody(), "GBK");
String utf8 = new String(event.getBody(), Charsets.UTF_8);
logger.info(fileName + "文件内容GBK解码后内容为:" + gbk);
logger.info(fileName + "文件内容UTF-8解码后内容为:" + utf8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return event;
}
@Override
public List<Event> intercept(List<Event> events) {
List<Event> results = new ArrayList<>();
Event event;
for (Event e : events) {
event = intercept(e);
if (event != null) {
results.add(event);
}
}
return results;
}
@Override
public void close() {
}
public static class Builder implements Interceptor.Builder {
@Override
public Interceptor build() {
return new TestIgnore();
}
@Override
public void configure(Context context) {
}
}
}
读取每个event body数据,然后将其按照UTF-8和GBK进行解码,然后以日志的形式输出。
将其打包上传到flume1.8/lib目录下。
2. 编写flume启动配置文件test.conf代码:
test.sources=r1
test.channels=c1
test.sinks=s1
test.sources.r1.type=spooldir
test.sources.r1.spoolDir=/home/data/flume/data
#设置监视目录文件编码为UTF-8
test.sources.r1.inputCharset=UTF-8
test.sources.r1.consumeOrder=random
test.sources.r1.recursiveDirectorySearch=true
test.sources.r1.deletePolicy=immediate
test.sources.r1.pollDelay=500
test.sources.r1.fileHeader=true
test.sources.r1.fileHeaderKey=fileAbsolutePath
test.sources.r1.deserializer.maxLineLength=10000
test.sources.r1.deserializer.outputCharset=UTF-8
#配置自定义拦截器
test.sources.r1.interceptors=i1
test.sources.r1.interceptors.i1.type=cn.com.bonc.TestIgnore$Builder
test.channels.c1.type=memory
test.sinks.s1.type=logger
test.sources.r1.channels=c1
test.sinks.s1.channel=c1
将其上传到linux服务器上,以便后续启动flume agent使用。
3. 启动Flume agent进行测试,准备工作:
gbk.txt文件,文件编码为GBK。
utf-8.txt文件,文件编码为UTF-8。
截图如下:
执行命令,启动Flume agent:/home/lib/flume1.8/bin/flume-ng agent -c ./ -f test.conf -n test | grep -v DEBUG
启动成功之后,往监视目录中上传上面准备好的两个文件,然后观察日志输出:
通过观察可以看出,虽然乱码文件没有导致线程的停止,继续往监视目录中上传新文件,文件依然可以被Flume处理。但是乱码文件被读取了,而且不管是用什么格式进行解码,输出之后都是乱码,这并不是我们想要的结果。
6. 源码修改2,强烈推荐!!!
注意:这是在源码基础上修改,上面的那个修改要还原!!!
主要涉及到两个类:
org.apache.flume.source.SpoolDirectorySource (这个类是spooldir类型source的主要类)
org.apache.flume.client.avro.ReliableSpoolingFileEventReader (这个类主要用来读取文件,将文件每行内容包装到event集合中,并对文件进行相关操作)
org.apache.flume.source.SpoolDirectorySource类,下面是我改动过的代码:
@Override
public void run() {
int backoffInterval = 250;
try {
while (!Thread.interrupted()) {
List<Event> events = reader.readEvents(batchSize);
if (events.isEmpty()) {
break;
}
sourceCounter.addToEventReceivedCount(events.size());
sourceCounter.incrementAppendBatchReceivedCount();
try {
//将读取到的events集合提交到channels中,如果提交成功,没有发生异常,则将reader读取文件状态设置为true,表示可以进行下次读取。
getChannelProcessor().processEventBatch(events);
reader.commit();
} catch (ChannelFullException ex) {
logger.warn("The channel is full, and cannot write data now. The " +
"source will try again after " + backoffInterval +
" milliseconds");
hitChannelFullException = true;
backoffInterval = waitAndGetNewBackoffInterval(backoffInterval);
continue;
} catch (ChannelException ex) {
logger.warn("The channel threw an exception, and cannot write data now. The " +
"source will try again after " + backoffInterval +
" milliseconds");
hitChannelException = true;
backoffInterval = waitAndGetNewBackoffInterval(backoffInterval);
continue;
}
backoffInterval = 250;
sourceCounter.addToEventAcceptedCount(events.size());
sourceCounter.incrementAppendBatchAcceptedCount();
}
} catch (Throwable t) {
logger.error("FATAL: " + SpoolDirectorySource.this.toString() + ": " +
"Uncaught exception in SpoolDirectorySource thread. " +
"Restart or reconfigure Flume to continue processing.", t);
//该属性用来标识是否有fatal异常,如果线程检测到该属性为ture,则会停止该线程,因此我们需要把这行代码注释掉,以防止发生异常时线程停止。
// hasFatalError = true;
//这个方法用来对异常进行类型判断,并且传播异常,为防止该异常造成线程停止,也把这行代码注释掉了。
// Throwables.propagate(t);
}
}
主要修改的就是run()方法的最后两行代码,直接注释掉了,相关解释可以通过代码注释查看。
org.apache.flume.client.avro.ReliableSpoolingFileEventReader类:
public List<Event> readEvents(int numEvents) {
/*
* 首先检查上次读取的events是否提交成功:
* 如果已经提交失败,则重置当前文件的处理状态,防止再次读取events。
* 如果提交成功,则查看当前文件指向是否有实例(也就是指向的文件是否存在,文件处理完会被删除或者被重命名),
* 如果没有实例,则将当前文件指向下个文件。
* 如果当前文件指向没有实例,则返回空events集合。
*/
if (!committed) {
if (!currentFile.isPresent()) {
throw new IllegalStateException("File should not roll when " +
"commit is outstanding.");
}
logger.info("Last read was never committed - resetting mark position.");
try {
currentFile.get().getDeserializer().reset();
} catch (IOException e) {
e.printStackTrace();
}
} else {
// Check if new files have arrived since last call
if (!currentFile.isPresent()) {
currentFile = getNextFile();
}
// Return empty list if no new files
if (!currentFile.isPresent()) {
return Collections.emptyList();
}
}
/*
* 从当前文件读取一定数量的events。
* 如果在读取文件时发生了异常,则打印异常信息,记录异常文件的绝对路径。
*/
List<Event> events = null;
try {
events = readDeserializerEvents(numEvents);
} catch (IOException e) {
logger.warn("读取文件时发生IO异常,请检查文件编码等是否和配置文件保持一致!详细错误信息如下:\t" +
"文件绝对路径:" + currentFile.get().getFile().getAbsolutePath() + ",错误信息:" + e);
}
/* It's possible that the last read took us just up to a file boundary.
* If so, try to roll to the next file, if there is one.
* Loop until events is not empty or there is no next file in case of 0 byte files */
/*
* 如果读取到的events集合为空,说明在读取当前文件时发生了异常,或者是该文件的内容全被读取完毕,然后调用retireCurrentFile()方法。
* retireCurrentFile()方法逻辑:
* 关闭当前文件,然后根据配置文件中的删除逻辑,对当前文件进行相关操作:重命名或者是直接删除。
* 上述处理逻辑会把当前文件删除!!!
* 获取下个文件:
* 如果没有下个文件(也就是新文件),则直接返回空events集合。
* 如果有下文件,则读取下个文件的内容,并封装到events集合中。
*/
//在此处需要判断一下该集合是否为null,解决空指针异常。至于为什么在生产环境下会发生空指针异常,目前尚不清楚。
while (events == null || events.isEmpty()) {
logger.info("Last read took us just up to a file boundary. " +
"Rolling to the next file, if there is one.");
try {
retireCurrentFile();
} catch (IOException e) {
e.printStackTrace();
}
currentFile = getNextFile();
if (!currentFile.isPresent()) {
return Collections.emptyList();
}
try {
events = readDeserializerEvents(numEvents);
} catch (IOException e) {
logger.warn("读取文件时发生IO异常,请检查文件编码等是否和配置文件保持一致!详细错误信息如下:\t" +
"文件绝对路径:" + currentFile.get().getFile().getAbsolutePath() + ",错误信息:" + e);
}
}
fillHeader(events);
committed = false;
lastFileRead = currentFile;
return events;
}
去掉该方法的异常抛出,将所有异常进行捕捉并处理,处理方式就是将当前读取文件的绝对路径易日志形式输出,以便后续在日志文件中查看监视目录中是哪个文件出错了,便于排错。相关解释可以通过代码注释查看。
将flume-ng-core源码打包上传替换Linux服务器原始jar包。具体打包方式查看 flume-ng-core打包 。
下面进行测试:
还是刚才的测试方式,下面是测试结果:
可以看到乱码文件并没有导致线程的终止,也没有被读取到Flume中,而且也被删除了(这跟我们配置的策略有关:test.sources.r1.deletePolicy=immediate),错误信息也被输出到了控制台,也有错误文件的绝对路径,这才是我们最想要的结果。
项目代码地址:https://gitee.com/wzq246810/apache-flume-1.8.0-src
项目根目录下有flume-ng-core-1.8.0.jar包,可以直接下载下来之后替换掉服务器上的jar包。
7. 其他
flume-1.9 已经修复了该问题,并且提供了再遇到非指定编码文件时处理的参数,用户可自行升级 flume 版本以解决该问题。