在使用flume收集数据时,有时候需要我们自定义source,而官方给的案例,有时也不能满足我们的需要,下面的案例是仿照源码的架构编写的。
下面的案例是:自定义source,用kafka代替channel,因为我们的目标就是,通过flume将数据采集到kafka,这样省去了从channel到sink的过程,提升了效率,而自定义source是为了防止重复传递数据。
代码如下,在代码中有备注解释:
package DataCollect;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.EventDrivenSource;
import org.apache.flume.channel.ChannelProcessor;
import org.apache.flume.conf.Configurable;
import org.apache.flume.event.EventBuilder;
import org.apache.flume.source.AbstractSource;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
*自定义source,用来保证不重复消费
* 原始日志文件
* 偏移量文件
* 编码格式
* 睡眠时间
* */
/*
* flume1.6及以前版本
* 如果想监控目录中的多个文件
* 文件的目录
* Filelisten类
* 拿出改名字的文件名
* 变化的文件
* 删除的文件
* 新增的文件
* */
//要继承抽象的AbstractSource类,然后要实现EventDrivenSource、Configurable接口
public class MySource extends AbstractSource implements EventDrivenSource, Configurable {
//定义成员属性
//定义原始文件路径
private String filepath;
//定义偏移量文件路径
private String offsetpath;
//定义编码集
private String charset;
//定义间隔时间
private Long interval;
//定义线程池
private ExecutorService executor;
//filerunner对象
private fileRunner fileRunner;
//加载采集方案(配置文件)
//利用context取出配置文件中的各种定义信息
public void configure(Context context) {
//加载原始日志文件
filepath = context.getString("filepath");
//加载偏移量文件
offsetpath = context.getString("offsetpath");
//加载编码格式
charset = context.getString("charset","UTF-8");
//加载睡眠时间
interval = context.getLong("interval",5000L);
}
//执行操作的
@Override
public synchronized void start() {
//创建线程
//固定线程的线程池
// Executors.newFixedThreadPool()
//可缓冲线程池
// Executors.newCachedThreadPool()
//单线程的线程池
//因为还要用线程去监控文件,所以要把executor定义在公共属性中
executor = Executors.newSingleThreadExecutor();
//创建一个channel发送的类的对象
ChannelProcessor channelProcessor = getChannelProcessor();
//创建对象
fileRunner = new fileRunner(filepath,offsetpath,charset,interval,channelProcessor);
//线程要去执行命令
executor.submit(fileRunner);
super.start();
}
//定义一个类实现Runnable接口
public class fileRunner implements Runnable{
//定义成员属性
private String filepath;
private String offsetpath;
private String charset;
private Long interval;
//定义原始日志文件的封装类
private RandomAccessFile accessFile;
//定义channel发送类,作用就是将封装的event反送给channel
private ChannelProcessor channelProcessor;
//定义偏移量
private Long offset=0L;
//定义偏移量文件类
private File offsetFile;
//定义一个标志
private boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
//定义一个构造方法,参数需要上面的属性
//这个构造方法的作用是,读取偏移量文件内容,获取到偏移量,然后根据获取到的偏移量的值,
//将原始文件的位置搞到需要的偏移量的位置,相当于初始化,将一切准备开始读取原始文件之前的动作做完
public fileRunner(String filepath, String offsetpath, String charset, Long interval, ChannelProcessor channelProcessor) {
this.filepath = filepath;
this.offsetpath = offsetpath;
this.charset = charset;
this.interval = interval;
this.channelProcessor=channelProcessor;
//判断偏移量文件是否存在,若不存在,则创建一个偏移量文件,因为这个偏移量文件在run方法中也要用,所以定义到成员属性中
//封装成File类
offsetFile = new File(this.offsetpath);
//判断,如果不存在,则创建
if(!offsetFile.exists()){
try {
offsetFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
//读取偏移量。这里要读取文件中的内容了,但是以往都是使用IO流进行读取文件内容,这里并没有使用IO流
//我们可以使用文件的工具类,FileUtils,这个工具类中封装了对文件的各种操作
try {
//读取出偏移量
String offsetStr = FileUtils.readFileToString(offsetFile);//这个方法的参数需要传入一个文件对象
//判断,如果读取出的偏移量不是null或者“”
if(null != offsetStr && !"".equals(offsetStr)){
//因为在run方法中会使用到该偏移量,所以定义到成员属性中
offset = Long.parseLong(offsetStr);//将字符串转成long型
}
//将原始文件搞到我们需要开始读取的位置(根据偏移量)
//这里使用了RandomAccessFile类,这个类中有个seek方法,可以直接定义到我们需要的文件位置,类似于C++中的指针的含义
//还有个readLine的方法,可以读取一行文件内容
//在run方法中需要使用它去一行一行的读取原始文件,所以定义到成员属性中
//将文件封装成RandomAccessFile
accessFile = new RandomAccessFile(filepath, "r");//第一个参数是指定文件对象,第二个参数代表“只读”
//调用seek方法,传入刚才获取到的偏移量,代表着我们已经将文件的开始读取位置,搞到了偏移量的位置
accessFile.seek(offset);
} catch (IOException e) {
e.printStackTrace();
}
}
//run方法中,开始读取原始文件,封装event,发送给channel,并更新偏移量。如此一直循环下去
public void run() {
while (flag){
//得读取数据
try {
//读取原始文件数据
String line = accessFile.readLine();
//判断读取到的一行内容是否为空,若为空代表暂时读取了文件所有的内容了,休眠一会
if(StringUtils.isNotEmpty(line)){
//将line封装成event
//方法的第二个参数需要的是Charset类的对象,我们定义的charset是字符串,所以要经过下面所写那样,转换成对象
Event event = EventBuilder.withBody(line, Charset.forName(charset));
//发送给channel
channelProcessor.processEvent(event);
//获取读完数据的原始日志的偏移量
offset = accessFile.getFilePointer();
//将偏移量更新到偏移量文件
//第一个参数需要一个文件对象,第二个参数是要写到文件中的数据(String型)
FileUtils.writeStringToFile(offsetFile,offset+"");
}else{
//没有读取到原始文件内容,休眠一会,休眠时间是一个间隔时间
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//关闭资源的
@Override
public synchronized void stop() {
if(!Thread.currentThread().isInterrupted()){
try {
Thread.currentThread().wait(5000);
//关闭上面的循环,使用FileRunner类的对象调用set方法,将flag的值改为false
fileRunner.setFlag(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
super.stop();
}
}
先把代码打成jar包,然后上传到flume下的lib文件夹中
我的原始的数据文件在:/opt/module/flume/data/gaotie.txt
我的存储偏移量的文件在:/opt/module/flume/data/postfile
我的conf配置文件名为:flume-file-kafka2.conf
编写的conf配置文件内容是:
a1.sources=r1
a1.channels=c1
a1.sources.r1.type=DataCollect.MySource
a1.sources.r1.filepath=/opt/module/flume/data/gaotie.txt
a1.sources.r1.offsetpath=/opt/module/flume/data/postfile
a1.sources.r1.charset=utf-8
a1.sources.r1.interval=5000
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = myhadoop101:9092,myhadoop102:9092,myhadoop103:9092
a1.channels.c1.kafka.topic = gaotie
a1.channels.c1.kafka.consumer.group.id = gaotie-consumer
a1.sources.r1.channels = c1
然后先启动zookeeper
bin/zkServer.sh start
再启动kafka
bin/kafka-server-start.sh config/server.properties &
最后开启flume的监控命令
bin/flume-ng agent --conf conf/ --name a1 --conf-file job/flume-file-kafka2.conf -Dflume.root.logger=INFO,console
可以开启kafka的消费者命令
bin/kafka-console-consumer.sh --zookeeper myhadoop101:2181 --topic gaotie --from-beginning
来消费指定的topic的内容,来检测是否成功将数据传到kafka中,也可以直接在kafka下的logs文件夹中找到指定的topic,进入topic文件夹中,查看以.log结尾的文件是否有数据