Flume spooldir乱码文件导致监视线程停止解决方案

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 版本以解决该问题。

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

第一片心意

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值