http://www.aboutyun.com/forum.php?mod=viewthread&tid=20701
背景
01
02
03
04
05
06
07
08
09
10
|
#数据目标路径,最终取到的数据在HDFS上的位置
etl.destination.path=
/user/username/topics
#执行信息存放路径,最重要的是上次读取的kafka的offset
etl.execution.base.path=
/user/username/exec
#消息解码类,camus读到的数据是byte[]格式的,可以在自定义类进行反序列化
camus.message.decoder.class=com.linkedin.camus.etl.kafka.coders.SimpleMessageDecoder
#要读取的topic
kafka.whitelist.topics=
#时区,这个很重要,因为数据存放是按日期的
etl.default.timezone=Asia
/Chongqing
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
date_string=$(
date
'+%Y/%m/%d/%H'
) partion=$(
date
'+%Y-%m-%d_%H'
)
table_name= service_log_table
filePath=
"/user/username/topics/hourly/"
$date_string
"/"
hive< create table
if
not exists $table_name (
mapJson STRING,
desc STRING,
str1 STRING,
str2 STRING,
str3 STRING)
PARTITIONED BY (dt STRING)
STORED AS TEXTFILE;
load data inpath
'$filePath'
overwrite into table $table_name partition (dt=
'$partion'
);
EOF
|
1
2
3
4
|
public interface InputFormat
InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
RecordReader
split
,JobConf job,Reporter reporter) throws IOException;
}
|
1
2
3
4
5
6
7
8
|
public abstract class OutputFormat
//
创建一个写入器
public abstract RecordWriter
//
检查结果输出的存储空间是否有效
public abstract void checkOutputSpecs(JobContext context) throws IOException, InterruptedException;
//
创建一个任务提交器
public abstract OutputCommitter getOutputCommitter(TaskAttemptContext context) throws IOException, InterruptedException;
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
//Job
开始被执行之前,框架会调用setupJob()为Job创建一个输出路径
void setupJob(JobContext jobContext);
//
如果Job成功完成,框架会调用commitJob()提交Job的输出
void commitJob(JobContext jobContext);
//
如果Job失败,框架会调用OutputCommitter.abortJob()撤销Job的输出
void abortJob(JobContext jobContext, JobStatus.State state);
//Task
执行前的准备工作,类似setupJob
void setupTask(TaskAttemptContext taskContext);
//Task
可能没有输出,也就不需要提交,通过needsTaskCommit()来判断
boolean needsTaskCommit(TaskAttemptContext taskContext);
//Task
成功执行,提交输出
void commitTask(TaskAttemptContext taskContext);
//Task
执行失败,清理
void abortTask(TaskAttemptContext taskContext);
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
//
获取kafka集群TopicMetadata信息,这些信息包括各个partition的leaderId,replicas以及Isr,存在于每一个broker上,所以即便brokerList不完整或者有broker挂了也不会影响。
List
//
对于每一个topic下的每一个partition,执行操作
for
(TopicMetadata topicMetadata : topicMetadataList) {
for
(PartitionMetadata partitionMetadata : topicMetadata.partitionsMetadata()) {
//
为每个partition的leader创建LeaderInfo对象
LeaderInfo leader =new LeaderInfo(new URI(
"tcp://"
+ partitionMetadata.leader().getConnectionString()),partitionMetadata.leader().
id
());
//
下面这一段,就是将leader相同的partition放在一起
//
最终得到Map
if
(offsetRequestInfo.containsKey(leader)) {
ArrayList
topicAndPartitions.add(new TopicAndPartition(topicMetadata.topic(),partitionMetadata.partitionId()));
offsetRequestInfo.put(leader, topicAndPartitions);
}
else
{
//
和上面一样,只是在offsetRequestInfo中没有leader时有个新建List的操作
.....
}
}
}
|
01
02
03
04
05
06
07
08
09
10
11
12
|
//
获取当前请求的topic
public abstract String getTopic();
//
获取当前请求的URI
public abstract URI getURI();
//
获取当前请求的partition
public abstract int getPartition();
//
获取请求的起始offset
public abstract long getOffset();
public abstract long getEarliestOffset();
//
获取partition的最大偏移量
public abstract long getLastOffset();
public abstract long estimateDataSize();
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
@Override
public
List
throws
IOException {
//获取配置项中mapred.map.tasks的map数目,一个mapper可以执行多个partition的抽数任务
int
numTasks = context.getConfiguration().getInt(
"mapred.map.tasks"
,
30
);
//按数据量由大到小给每个partition的request排序
reverseSortRequests(requests);
//初始化splists,这时候每个splits里面的request数目为0
List
new
ArrayList
for
(
int
i =
0
; i < numTasks; i++) {
if
(requests.size() >
0
) {
kafkaETLSplits.add(
new
EtlSplit());
}
}
//分配request到各个splits,算法很简单,当前哪个splits的数据量最少,就给那个splits添加request,因为是按由大到小排序的,所以任务比较均匀
for
(CamusRequest r : requests) {
getSmallestMultiSplit(kafkaETLSplits).addRequest(r);
}
return
kafkaETLSplits;
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
while
(
true
) {
//reader为KafkaReader类型,该类专门负责逐条读取kafka消息
if
(reader ==
null
|| !reader.hasNext()) {
//从InputSplit中弹出request
EtlRequest request = (EtlRequest) split.popRequest();
//传给mapper的key,这里是类成员变量,后面直接由getCurrentKey()返回
key.set(request.getTopic(), request.getLeaderId(), request.getPartition(), request.getOffset(),request.getOffset(),
0
);
//在reader为null,或者一个partition中的数据已经读完的情况下,新建reader,reader负责从kafka中读取数据
reader =
new
KafkaReader(inputFormat, context, request, CamusJob.getKafkaTimeoutValue(mapperContext),CamusJob.getKafkaBufferSize(mapperContext));
//创建消息decoder,由配置camus.message.decoder.class指定,从kafka中读取出的为byte[]类型,decoder类负责将字节转换为最终在hdfs上存放的格式
decoder = createDecoder(request.getTopic());
}
while
((message = reader.getNext(key)) !=
null
) {
//用decoder解码消息,封装在CamusWrapper中
CamusWrapper wrapper = getWrappedRecord(message);
value = wrapper;
return
true
;
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
//key中包含的是关于kafka的信息,如当前消息的offset,topic,parition等
//val中是具体的message,也就是前面从kafka 消息的bytes[]中获取的
public
void
write(EtlKey key, Object val)
throws
IOException, InterruptedException {
//当前message的key是否和前面message的一致,因为一个split可能包含不同paritition的数据,也就是一个mapper就可能处理不同partition
if
(!key.getTopic().equals(currentTopic)) {
//如果topic不一致,则需要清除writer,重新创建新的topic的,之所有有很多writer,是因为topic下会有多个partition,不同partition写入到不同的文件中
for
(RecordWriter
writer.close(context);
}
dataWriters.clear();
//更新当前topic
currentTopic = key.getTopic();
}
//将当前的key的offset存入到 (topic+partition)-> offset 的map中,因为同一partition消息是顺序的,所以最后存入的肯定是offset最大的消息,committer会将这个map写入到执行记录目录中,下一次运行时以这个offset为起点
committer.addCounts(key);
CamusWrapper value = (CamusWrapper) val;
//获取写入文件名
String workingFileName =EtlMultiOutputFormat.getWorkingFileName(context, key);
//如果现有的writer中不包含对应文件的,则新建一个
if
(!dataWriters.containsKey(workingFileName)) {
dataWriters.put(workingFileName, getDataRecordWriter(context,workingFileName, value));
log.info(
"Writing to data file: "
+ workingFileName);
}
//将key-value写入到Taks的输出目录下临时文件中
dataWriters.get(workingFileName).write(key, value);
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//获取配置etl.destination.path对应的目录,作为最终输出目录
Path baseOutDir = EtlMultiOutputFormat.getDestinationPath(context);
//这里fs是前面write函数写入的目录,也就是Task的output路径
//列举其中的文件,数据文件的文件名都是以data开头的
for
(FileStatus f : fs.listStatus(workPath)) {
String file = f.getPath().getName();
if
(file.startsWith(
"data"
)) {
//数据文件名中包含topic,partition,offset,time等信息
//根据文件名,消息计数等信息获得最终的输出路径和输出文件名
//输出路径是以小时分割的,basePath/year/month/day/hour/filename
String partitionedFile =
getPartitionedPath(context, file, count.getEventCount(), count.getLastKey().getOffset());
//如果路径不存在,创建路径
Path dest =
new
Path(baseOutDir, partitionedFile);
if
(!fs.exists(dest.getParent())) {
mkdirs(fs, dest.getParent());
}
//拷贝文件,从Task输出目录下拷贝到目标文件
commitFile(context, f.getPath(), dest);
}
}
//后面还有一段写入offset的,只有写入数据成功后才会写入offset
|