在使用flume收集数据时,有时候需要我们自定义source,而官方给的案例,有时也不能满足我们的需要,下面的案例是仿照源码的架构编写的。
下面的案例是:自定义source,用kafka代替channel,因为我们的目标就是,通过flume将数据采集到kafka,这样省去了从channel到sink的过程,提升了效率,而自定义source是为了防止重复传递数据。
在代码中我做了详细的解释:
package DataToKafka;
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用来保证不重复消费
* 原始日志文件
* 偏移量文件
* 编码格式
* 睡眠时间
*
* */
public class SourceMy extends AbstractSource implements EventDrivenSource, Configurable {
//定义成员属性
//定义原始文件路径
private String filepath;
//定义偏移量文件路径
private String offsetpath;
//定义编码集
private String charset;
//定义间隔时间
private Long interval;
/* //定义线程池
private ExecutorService excutor;*/
//定义FileRunner对象
private FileRunner fileRunner;
@Override//加载配置文件的,利用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(int nThread),括号中参数是定义线程池中的线程数
*可缓冲线程池
* Executors.newCachedThreadPool()
* 单线程的线程池
* Executors.newSingleThreadExecutor()
* */
//因为还要用线程去监控文件,所以要把executor定义在公共属性中
ExecutorService excutor = Executors.newSingleThreadExecutor();
//定义一个channel发送的对象
ChannelProcessor channelProcessor = getChannelProcessor();
//定义出一个线程
fileRunner = new FileRunner(filepath,offsetpath,charset,interval,channelProcessor);
//将线程交给线程池管理,线程执行命令
excutor.submit(fileRunner);
super.start();
}
//定义一个类实现Runnable接口
public class FileRunner implements Runnable {
//定义成员属性
private String filepath;
private String offsetpath;
private String charset;
private Long interval;
//定义channel发送类,作用就是将封装的event反送给channel
private ChannelProcessor channelProcessor;
//定义偏移量文件类
private File offsetFile;
//定义偏移量
private Long offset = 0L;
//定义原始文件的封装类
private RandomAccessFile accessFile;
//定义一个flag
private boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
//定义一个构造方法,参数需要上面的属性
//这个构造方法的作用是,读取偏移量文件内容,获取到偏移量,然后根据获取到的偏移量的值,将原始文件的位置搞到需要的偏移量的
//位置,相当于初始化,将一切准备开始读取原始文件之前的动作做完
public FileRunner(String filepath,String offsetpath,String charset,Long interval,ChannelProcessor channelProcessor) {
this.filepath = filepath;
this.charset = charset;
this.interval = interval;
this.offsetpath = offsetpath;
this.channelProcessor = channelProcessor;
//判断偏移量文件是否存在,若不存在,则创建一个偏移量文件,因为这个偏移量文件在run方法
//中也要用,所以定义到成员属性中
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方法中需要使用它去一行一行的读取原始文件,所以定义到成员属性中
accessFile = new RandomAccessFile(filepath,"r");//第一个参数是指定文件对象,第二个参数代表“只读”
//调用seek方法,传入刚才获取到的偏移量,代表着我们已经将文件的开始读取位置,搞到了偏移量的位置
accessFile.seek(offset);
} catch (IOException e) {
e.printStackTrace();
}
}
//run方法中,开始读取原始文件,封装event,发送给channel,并更新偏移量。如此一直循环下去
@Override
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文件夹中
我的原始的数据文件在:flume/data/gt.txt
我的存储偏移量的文件在:flume/data/offset.txt
编写的conf配置文件内容是:
a1.sources = r1
a1.channels = c1
a1.sources.r1.type = DataToKafka.SourceMy
a1.sources.r1.filepath = /opt/module/flume/data/gt.txt
a1.sources.r1.offsetpath = /opt/module/flume/data/offset.txt
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 = hadoop101:9092,hadoop102:9092,hadoop103:9092
a1.channels.c1.kafka.topic = gt
a1.channels.c1.kafka.consumer.group.id = gt-consumer
a1.sources.r1.channels = c1
将zookeeper启动,再将kafka启动
开启flume的监控命令
bin/flume-ng agent --conf conf/ --name a1 --conf-file job/flume-mysource-kafka.conf -Dflume.root.logger=INFO,console
flume-mysource-kafka.conf是我的conf配置文件的名称
可以在kafka中开启一个消费者,来消费指定的topic的内容,来检测是否成功将数据传到kafka中,也可以直接在kafka下的logs文件夹中找到指定的topic,进入topic文件夹中,查看以.log结尾的文件是否有数据