62.大数据之旅——电信日志项目05-电信日志数据处理

版权声明:版权归零零天所有,若有转载请注明出处。 https://blog.csdn.net/qq_39188039/article/details/86636247

一、zebra项目介绍与分析
在这里插入图片描述
如图所示,电信运营商的用户通过连接到互联网中的各种网络设备访问一个网站时,其访问信息会通过基站在网络中传递,一个基站负责收集某一片小区用户的上网数据,这些收集的数据都以日志信息进行存储。所有的用户上网行为都会有所记录。比如用户通过3G/4G下载某个app应用,或者登陆、使用某一个App软件,以及通过App发送的数据都会记录。
这样一来,我们就可以根据日志文件,来分析和统计数据,比如可以统计:
1.App下载热度排行
2.用户使用哪个App平均用时最长
3.用户使用哪个App耗费流量最多
…………
既可以根据数据,多维度来统计数据
这就是zebra项目的目的

二、日志数据
日志里的某一条数据(以下为一整行数据,以| 为分割符):

533||11|93287887015245963|6||||1|100.82.254.88|100.82.98.100|2152|2152|13849|147855076||||103|1409649427963|1409649428488|1|15|999||0|10.83.124.18||60914|0|137.175.9.211||80|734|329|4|2|0|0|0|0|221|29|0|0|20|221|12600|1260|1|0|1|3|6|200|221|221|255|559955.com|/tu/31322.JPG|559955.com|Mozilla/5.0 (Linux; Android 4.3; zh-cn; SAMSUNG-SM-G7108V_TD Release/02.15.2014 Browser/AppleWebKit537.36 Build/JSS15J) AppleWebkit/537.36 (KHTML, like Gecko) Version/1.5 Mobile Safari/537.36||http://www.701111.com/||0|0|0|0|||3|0|525|0|0|1:734/329

三、数据以 | 分割后,每个数据的含义(仅展示项目里用到的字段数据)
在这里插入图片描述
这是我们需要处理的每行数据中的字段,通过对这些数据的处理,我们可以得到不同小区的上网详情数据。具体来说,就是把一段时间内的同一个小区内访问同一网站、同一个ip的访问累计起来,就可以得到某小区内的某网站的访问详情。最后将数据写到关系型数据库里。
建表语句:
#创建数据总表

create table F_HTTP_APP_HOST(
reporttime datetime,
apptype int,
appsubtype int,
userip varchar(20),
userport int,
appserverip varchar(20),
appserverport int,
host varchar(255),
cellid varchar(20),
attempts bigint,
accepts bigint,
trafficul bigint,
trafficdl bigint,
retranul bigint,
retrandl bigint,
failcount bigint,
transdelay bigint
);
 

四、zebra项目整体架构
技术架构:
1.maven
利用Maven工程来管理项目。主要是利用Maven来管理jar包,以及利用maven生成avro的rpc方法及序列化对象
2.avro
利用avro实现对象的序列化以及节点间的rpc通信
3.zookeeper
利用zookeeper达到分布式环境的协调服务
工程架构:
视图:
在这里插入图片描述
1.zebra-contract 合同工程。此工程作用:
①管理整个项目的pom.xml文件
②定义全局变量参数
③定义avro相关的avsc文件及avdl文件
2.zebra-jobtracker 工程。此工程作用:
①定时扫描指定目录下是否有待处理文件
②根据用户定义的参数,对文件进行逻辑切块。
③根据文件切块数量生成对应的任务数量。(一个文件块相当于一个任务)
④将任务分发给一级引擎节点(TaskTracker )去处理。并通过zookeeper监控TaskTracker的状态来决定任务分配
3.zebra-engine1 工程。此工程作用:
①接收jobtracker发来的任务,根据任务进行对文件的处理
②将文件数据进行清洗和整理。(根据zebra要求的业务逻辑进行数据整理)
③将处理完的数据发给二级引擎,二级引擎做最后的合并
④通过zookeeper,注册自身节点信息状态,便于集群其他机器监控继而做相关的业务逻辑处理
4.zebra-engine2 工程。此工程作用:
①接收(一个或多个)一级引擎发来的数据。
②对数据进行归并处理
③将最后处理的数据结果落地(写文件或写数据库)

业务说明


数据以 | 分割后,每个数据的含义(仅展示项目里用到的字段数据)
在这里插入图片描述
业务字段处理逻辑

HttpAppHost hah=new HttpAppHost();
hah.setReportTime(reportTime);
//上网小区的id
hah.setCellid(data[16]);
//应用类
hah.setAppType(Integer.parseInt(data[22]));
//应用子类
hah.setAppSubtype(Integer.parseInt(data[23]));
//用户ip
hah.setUserIP(data[26]);
//用户port
hah.setUserPort(Integer.parseInt(data[28]));
//访问的服务ip
hah.setAppServerIP(data[30]);
//访问的服务port
hah.setAppServerPort(Integer.parseInt(data[32]));
//域名
hah.setHost(data[58]);
 
int appTypeCode=Integer.parseInt(data[18]);
String transStatus=data[54];
int appTypeCode=Integer.parseInt(data[18]);
String transStatus=data[54];
 
//业务逻辑处理
if(hah.getCellid()==null||hah.getCellid().equals("")){
hah.setCellid("000000000");
}
if(appTypeCode==103){
hah.setAttempts(1);
}
if(appTypeCode==103&&"10,11,12,13,14,15,32,33,34,35,36,37,38,48,49,50,51,52,53,54,55,199,200,201,202,203,204,205,206,302,304,306".contains(transStatus)){
hah.setAccepts(1);
}else{
hah.setAccepts(0);
}
if(appTypeCode == 103){
hah.setTrafficUL(Long.parseLong(data[33]));
}
if(appTypeCode == 103){
hah.setTrafficDL(Long.parseLong(data[34]));
}
if(appTypeCode == 103){
hah.setRetranUL(Long.parseLong(data[39]));
}
if(appTypeCode == 103){
hah.setRetranDL(Long.parseLong(data[40]));
}
if(appTypeCode==103){
hah.setTransDelay(Long.parseLong(data[20]) - Long.parseLong(data[19]));
}
CharSequence key=hah.getReportTime() + "|" + hah.getAppType() + "|" + hah.getAppSubtype() + "|" + hah.getUserIP() + "|" + hah.getUserPort() + "|" + hah.getAppServerIP() + "|" + hah.getAppServerPort() +"|" + hah.getHost() + "|" + hah.getCellid();
if(map.containsKey(key)){
HttpAppHost mapHah=map.get(key);
mapHah.setAccepts(mapHah.getAccepts()+hah.getAccepts());
mapHah.setAttempts(mapHah.getAttempts()+hah.getAttempts());
mapHah.setTrafficUL(mapHah.getTrafficUL()+hah.getTrafficUL());
mapHah.setTrafficDL(mapHah.getTrafficDL()+hah.getTrafficDL());
mapHah.setRetranUL(mapHah.getRetranUL()+hah.getRetranUL());
mapHah.setRetranDL(mapHah.getRetranDL()+hah.getRetranDL());
mapHah.setTransDelay(mapHah.getTransDelay()+hah.getTransDelay());
map.put(key, mapHah);
}else{
map.put(key,hah);
}

在这里插入图片描述
建表语句

create table F_HTTP_APP_HOST(
reporttime datetime,
apptype int,
appsubtype int,
userip varchar(20),
userport int,
appserverip varchar(20),
appserverport int,
host varchar(255),
cellid varchar(20),
attempts bigint,
accepts bigint,
trafficul bigint,
trafficdl bigint,
retranul bigint,
retrandl bigint,
failcount bigint,
transdelay bigint
);
 

后期可能会根据统计出来的数据,进行业务拆分。形成几个不同的维度进行查询:
1.应用欢迎度
2.各网站表现
3.小区Http上网能力
4.小区上网洗好
应用欢迎度表说明
在这里插入图片描述
建表语句:

create table D_H_HTTP_APPTYPE(
hourid datetime,
apptype int,
appsubtype int,
attempts bigint,
accepts bigint,
succratio bigint,
trafficul bigint,
trafficdl bigint,
totaltraffic bigint,
retranul bigint,
retrandl bigint,
retrantraffic bigint,
failcount bigint,
transdelay bigint
);

各网站的表现表
在这里插入图片描述
建表语句:

#创建各网站表现表
create table D_H_HTTP_HOST(
hourid datetime,
host varchar(255),
appserverip varchar(20),
attempts bigint,
accepts bigint,
succratio bigint,
trafficul bigint,
trafficdl bigint,
totaltraffic bigint,
retranul bigint,
retrandl bigint,
retrantraffic bigint,
failcount bigint,
transdelay bigint
);

小区HTTP上网能力表
在这里插入图片描述
#创建小区HTTP上网能力表

create table D_H_HTTP_CELLID(
hourid datetime,
cellid varchar(20),
attempts bigint,
accepts bigint,
succratio bigint,
trafficul bigint,
trafficdl bigint,
totaltraffic bigint,
retranul bigint,
retrandl bigint,
retrantraffic bigint,
failcount bigint,
transdelay bigint
);

小区上网喜好表
在这里插入图片描述

#创建小区上网喜好表
create table D_H_HTTP_CELLID_HOST(
hourid datetime,
cellid varchar(20),
host varchar(255),
attempts bigint,
accepts bigint,
succratio bigint,
trafficul bigint,
trafficdl bigint,
totaltraffic bigint,
retranul bigint,
retrandl bigint,
retrantraffic bigint,
failcount bigint,
transdelay bigint
);

HttpAppHost对象+字段计算规则


对象字段结构
HttpAppHost.avsc 代码:

扫描二维码关注公众号,回复: 5060864 查看本文章
{"namespace": "rpc.domain",
 "type": "record",
 "name": "HttpAppHost",
 "fields": [
     {"name": "reportTime", "type": ["string", "null"]},
     {"name": "cellid", "type": ["string", "null"]},
     {"name": "appType",  "type": ["int", "null"]},
     {"name": "appSubtype",  "type": ["int", "null"]},
     {"name": "userIP", "type": ["string", "null"]},
     {"name": "userPort",  "type": ["int", "null"]},
     {"name": "appServerIP", "type": ["string", "null"]},
     {"name": "appServerPort", "type": ["int", "null"]},
     {"name": "host", "type": ["string", "null"]},
     {"name": "attempts",  "type": ["int", "null"]},
     {"name": "accepts",  "type": ["int", "null"]},
     {"name": "trafficUL",  "type": ["long", "null"]},
     {"name": "trafficDL", "type": ["long", "null"]},
     {"name": "retranUL", "type": ["long", "null"]},
     {"name": "retranDL", "type": ["long", "null"]},
     {"name": "transDelay", "type": ["long", "null"]}
 ]
}

字段封装规则:

String reportTime=path.split("_")[1];
String[] data=line.split("\\|");
HttpAppHost hah=new HttpAppHost();
hah.setReportTime(reportTime);
hah.setCellid(data[16]);
hah.setAppType(Integer.parseInt(data[22]));
hah.setAppSubtype(Integer.parseInt(data[23]));
hah.setUserIP(data[26]);
hah.setUserPort(Integer.parseInt(data[28]));
hah.setAppServerIP(data[30]);
hah.setAppServerPort(Integer.parseInt(data[32]));
hah.setHost(data[58]);
 
int appTypeCode=Integer.parseInt(data[18]);
String transStatus=data[54];

字段计算规则:
1.cellid字段:如果没有小区编号(cellid),补齐为000000000(9个0)
示例:

if(hah.getCellid()==null||hah.getCellid().equals("")){
hah=hah.newBuilder(hah).setCellid("000000000").build();
}

2.attempts字段: if(app type code=103),attempts字段设置为1 注:app type code对应的下标元素18
示例:

if(oi.getAppTypeCode()==103){
hah.setAttempts(1);
}

3.accepts字段: if( App Type Code=103 & HTTP/WAP事物状态 in(10,11,12,13,14,15,32,33,34,35,36,37,38,48,49,50,
51,52,53,54,55,199,200,201,202,203,204,205,206,302,304,306)) , counter设置为1
注:TransStatus的下标元素54
示例:

if(oi.getAppTypeCode()==103&&"10,11,12,13,14,15,32,33,34,35,36,37,38,48,49,50,51,52,53,54,55,199,200,201,202,203,204,205,206,302,304,306".contains(""+oi.getTransStatus())){
hah.setAccepts(1);
}
else{
//防止出现null,导致报错
hah.setAccepts(0);
}

4.trafficUL字段: if( App Type Code=103 ),设置trafficUL流量。 注:trafficUL的下标33
示例:

if(oi.getAppTypeCode()==103){
hah.setTrafficUL(oi.getTrafficUL());
}

5.trafficDL字段: if( App Type Code=103 ),设置 trafficDL 流量。注:trafficDL的下标34
示例:

if(oi.getAppTypeCode() == 103){
hah = hah.newBuilder(hah).setTrafficDL(oi.getTrafficDL()).build();
}

6.retranDL字段: if( App Type Code=103 ) ,设置retranUL流量。注:retranDL的下标39
示例:

if(oi.getAppTypeCode()==103){
hah.setTrafficDL(oi.getTrafficDL());
}

7.retranUL字段: if( App Type Code=103 ),设置 retranDL 。注:retranUL的下标40
示例:

if(oi.getAppTypeCode() == 103){
hah.setRetranUL(oi.getRetranUL());
}

8.transDelay字段: if( App Type Code=103 ) ,transDelay=Procedure_End_time-Procedure_Start_time
注:startTime的下标是19,endTime的下标是20
示例:

if(oi.getAppTypeCode()==103){
hah.setTransDelay(oi.getProcdureEndTime() - oi.getProcdureStartTime());
}

如何判断是同一个用户:

hah.getReportTime() + "|" + hah.getAppType() + "|" + hah.getAppSubtype() + "|" + hah.getUserIP() + "|" + hah.getUserPort() + "|" + hah.getAppServerIP() + "|" + hah.getAppServerPort() +"|" + hah.getHost() + "|" + hah.getCellid(); 

共同组成一个唯一key值。key值相同则证明为同一个用户

累加代码:

if(map.containsKey(key)){
HttpAppHost mapHah=map.get(key);
mapHah.setAccepts(mapHah.getAccepts()+hah.getAccepts());
mapHah.setAttempts(mapHah.getAttempts()+hah.getAttempts());
mapHah.setTrafficUL(mapHah.getTrafficUL()+hah.getTrafficUL());
mapHah.setTrafficDL(mapHah.getTrafficDL()+hah.getTrafficDL());
mapHah.setRetranUL(mapHah.getRetranUL()+hah.getRetranUL());
mapHah.setRetranDL(mapHah.getRetranDL()+hah.getRetranDL());
mapHah.setTransDelay(mapHah.getTransDelay()+hah.getTransDelay());
}else{
map.put(key, hah);
}

如何处理分块时,start 和end不在行首或行尾的情况:
代码:

FileSplit split=OwnEnv.getSplitQueue().take();
 
//头部向前追朔
long headPosition = start;
if(start == 0){
 
}else{
while(true){
 
fc.position(headPosition);
ByteBuffer buf = ByteBuffer.allocate(1);
fc.read(buf);
if(new String(buf.array()).equals("\n")){
start = headPosition + 1;
break;
}else{
headPosition = headPosition - 1;
}
}
}
 
//尾部向前追谁
long endPosition = end;
if(end == file.length() - 1){
 
}else{
while(true){
fc.position(endPosition);
ByteBuffer buf = ByteBuffer.allocate(1);
fc.read(buf);
if(new String(buf.array()).equals("\n")){
end = endPosition;
break;
}else{
endPosition = endPosition - 1;
}
}
}

日志数据累加数据的po对象


OtherInfo:
public class OtherInfo{
 
private Integer appTypeCode = 0;
private Long procdureStartTime = 0L;
private Long procdureEndTime = 0L;
private Long trafficUL = 0L;
private Long trafficDL = 0L;
private Long retranUL = 0L;
private Long retranDL = 0L;
private Integer transStatus = -1;
private String interruptType = "fail";
 
public Long getTrafficUL() {
return trafficUL;
}
public void setTrafficUL(Long trafficUL) {
this.trafficUL = trafficUL;
}
public Long getTrafficDL() {
return trafficDL;
}
public void setTrafficDL(Long trafficDL) {
this.trafficDL = trafficDL;
}
public Long getRetranUL() {
return retranUL;
}
public void setRetranUL(Long retranUL) {
this.retranUL = retranUL;
}
public Long getRetranDL() {
return retranDL;
}
public void setRetranDL(Long retranDL) {
this.retranDL = retranDL;
}
public Integer getAppTypeCode() {
return appTypeCode;
}
public void setAppTypeCode(Integer appTypeCode) {
this.appTypeCode = appTypeCode;
}
public Long getProcdureStartTime() {
return procdureStartTime;
}
public void setProcdureStartTime(Long procdureStartTime) {
this.procdureStartTime = procdureStartTime;
}
public Long getProcdureEndTime() {
return procdureEndTime;
}
public void setProcdureEndTime(Long procdureEndTime) {
this.procdureEndTime = procdureEndTime;
}
public Integer getTransStatus() {
return transStatus;
}
public void setTransStatus(Integer transStatus) {
this.transStatus = transStatus;
}
public String getInterruptType() {
return interruptType;
}
public void setInterruptType(String interruptType) {
this.interruptType = interruptType;
}
}

jobtracker工程代码


在这里插入图片描述
Start代码:

public class Start {
 
public static void main(String[] args) {
ExecutorService ex=Executors.newCachedThreadPool();
ex.execute(new FileCollector());
ex.execute(new FileToBlock());
ex.execute(new RegistZKServer());
}
}

FileCollector代码:

public class FileCollector implements Runnable{
 
@Override
public void run() {
System.out.println("文件收集线程启动……");
try {
while(true){
//步骤1:先定位到存放日志文件的目录。然后遍历这个目录,得到待处理的文件
//思路:这个路径不应该写死,而应该写在配置文件里。配置文件及读取配置的类应该contract工程里
File dir=new File(GlobalEnv.getDir());
File[] files=dir.listFiles();
for(File file:files){
//步骤2:为了避免数据文件重复被处理,我们引入标识文件。标识文件没有任何内容,
//思路:先扫描是否有.ctr为后缀的标识文件,如果有,根据标识文件得到待处理文件。然后将待处理文件存放在阻塞队列里
//然后将标识文件删除即可。
if(file.getName().endsWith(".ctr")){
String csvFileName=file.getName().split(".ctr")[0]+".csv";
//步骤3:根据csvFileName得到具体文件,然后存在阻塞队列里,供后续处理
File csvFile=new File(dir,csvFileName);
GlobalEnv.getFileQueue().put(csvFile);
//步骤4:将当前标识文件删除
file.delete();
}
}
//步骤5:为了避免线程实时循环遍历文件,可以设定一个扫描周期.这个扫描周期应该在Global里来定义
Thread.sleep(GlobalEnv.getScanningInterval());
}
} catch (InterruptedException e) {
 
e.printStackTrace();
}
 
}
 
}

FileToBlock代码:

public class FileToBlock implements Runnable{
 
@Override
public void run() {
System.out.println("文件切块线程启动……");
try {
while(true){
//步骤6:从文件队列里取出文件。用take()取,如果没有文件可取,会产生阻塞
File file=GlobalEnv.getFileQueue().take();
System.out.println(file.getName());
//步骤7:开启切块,每块切多大?根据配置参数来决定。所以需要在env.properties来定义
//思路:先得到文件的总大小=》根据blocksize切块=》将每块的起始位置信息、处理的长度信息、以及文件名封装到某个对象里(FileSplit)
//=》然后将FileSplit存到队列里,供后续处理
//要求:FileSplit对象,要通过avro的rpc传递给一级引擎,所以这个FileSpilt我们需要定义对应的avsc类
//步骤8:在contract工程里,准备avro的FileSpilt对象。
long length=file.length();
 
long blocknum=length%GlobalEnv.getBlocksize()==0?length/GlobalEnv.getBlocksize():
length/GlobalEnv.getBlocksize()+1;
 
for(long i=1;i<=blocknum;i++){
 
//步骤9:对象封装
FileSplit fileSplit=new FileSplit();
fileSplit.setPath(GlobalEnv.getDir()+"\\"+file.getName());
fileSplit.setStart((i-1)*GlobalEnv.getBlocksize());
if(i<blocknum){
fileSplit.setLength(GlobalEnv.getBlocksize());
}else{
fileSplit.setLength(length-fileSplit.getStart());
}
System.out.println(fileSplit);
 
//步骤10:存入队列
GlobalEnv.getSplitQueue().put(fileSplit);
}
 
}
 
 
 
 
} catch (Exception e) {
e.printStackTrace();
}
 
 
}
 
}

RegistZKServer代码:

public class RegistZKServer implements Runnable{
private ZooKeeper zk=null;
 
@Override
public void run() {
try {
//步骤11:连接zk服务
zk=GlobalEnv.connectZKServer();
//步骤12:通过zookeeper来得到具体有几个一级节点
//(业务场景说明:一级引擎会先启动,启动之后会在engine1这个节点下注册临时节点。并注册自己的ip和rpc端口
//但本项目不考虑一级引擎数量动态增加或减少的情况,避免业务过于复杂。
List<String> childpaths=zk.getChildren(GlobalEnv.getEngine1path(), null);
for(String childpath:childpaths){
//步骤13:启动rpc线程负责与一级引擎通信,有几个一级引擎,就启动几个线程
new Thread(new RpcRunner(childpath, zk)).start();
 
}
} catch (Exception e) {
 
e.printStackTrace();
}
 
}
 
}
 RpcRunner代码:
public class RpcRunner implements Runnable{
private ZooKeeper zk;
private String childpath;
 
public RpcRunner(String childpath, ZooKeeper zk) {
this.zk=zk;
this.childpath=childpath;
}
 
@Override
public void run() {
try {
//步骤14.通过zk得知要连接的一级引擎的信息,比如ip,port
//一级引擎的注册信息的形式:DESKTOP-KQ9M3EK/192.168.234.1/9991
String data=new String(zk.getData(GlobalEnv.getEngine1path()+"/"+childpath,null,null));
String ip=data.split("/")[1];
int port=Integer.parseInt(data.split("/")[2]);
//步骤15:启动avro的rpc,根据ip和port连接一级引擎
NettyTransceiver nt=new NettyTransceiver(new InetSocketAddress(ip, port));
//步骤16:连接成功之后,从spilt队列里拿出filesplit对象,然后通过avro的rpc方法向一级引擎传filesplit
FileSplit filesplit=GlobalEnv.getSplitQueue().take();
//步骤17:准备rpc方法
RpcFileSplit proxy=SpecificRequestor.getClient(RpcFileSplit.class, nt);
//步骤18:通过rpc发送filespilt对象
proxy.sendFileSplit(filesplit);
//TODO 监听一级引擎的状态变化,如:从繁忙变空闲,就再从队列类拿出一个filesplit对象发给他,让他接着干活
 
 
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
 

engine_LV1代码


start代码:

public class Start {
 
public static void main(String[] args) {
System.out.println("一级引擎启动");
ExecutorService ex=Executors.newCachedThreadPool();
ex.execute(new RegistZKServerRunner());
ex.execute(new RpcServerRunner());
}
}
 

RegistZKServerRunner代码:

public class RegistZKServerRunner implements Runnable{
 
private ZooKeeper zk=null;
@Override
public void run() {
try {
 
//步骤1:连接zk服务器
zk=GlobalEnv.connectZKServer();
System.out.println("一起引擎注册zookeeper服务成功");
//步骤2:注册自己的节点信息 rpc通信ip,rpc访问端口,节点类型:临时顺序节点
String engineInfo=InetAddress.getLocalHost()+"/"+"9991";
zk.create(GlobalEnv.getEngine1path()+"/"+"node",engineInfo.getBytes(),Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
}catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 
 
}
 
}

RpcServerRunner代码:

public class RpcServerRunner implements Runnable{
 
@Override
public void run() {
 
//步骤3:启动rpc服务,接收jobtracker传来的filesplit
//步骤4:创建RpcFileSpilt接口的实现类,在实现类的实现方法里接收jobtracker传来的信息
Server server=new NettyServer(new SpecificResponder(RpcFileSplit.class,new RpcFileSplitImpl()),
new InetSocketAddress(9991));
System.out.println("一级引擎启动rpc服务端");
 
}
 
}

RpcFileSplitImpl代码:

public class RpcFileSplitImpl implements RpcFileSplit{
 
@Override
public Void sendFileSplit(FileSplit fileSplit) throws AvroRemoteException {
System.out.println("一级引擎收到:"+fileSplit);
return null;
}
 
}
 

AVRO扩展


一、为什么不使用ProtoBuffer
Avro(读音类似于[ævrə])是Hadoop的一个子项目,由Hadoop的创始人Doug Cutting(也是Lucene,Nutch等项目的创始人)牵头开发,当前最新版本1.3.3。Avro是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便地处理Avro数据。

 当前市场上有很多类似的序列化系统,如Google的Protocol Buffers, Facebook的Thrift。这些系统反响良好,完全可以满足普通应用的需求。针对重复开发的疑惑,Doug Cutting撰文解释道:Hadoop现存的RPC系统遇到一些问题,如性能瓶颈(当前采用IPC系统,它使用Java自带的DataOutputStream和DataInputStream);需要服务器端和客户端必须运行相同版本的Hadoop;只能使用Java开发等。但现存的这些序列化系统自身也有毛病,以Protocol Buffers为例,它需要用户先定义数据结构,然后根据这个数据结构生成代码,再组装数据。如果需要操作多个数据源的数据集,那么需要定义多套数据结构并重复执行多次上面的流程,这样就不能对任意数据集做统一处理。其次,对于Hadoop中Hive和Pig这样的脚本系统来说,使用代码生成是不合理的。并且Protocol Buffers在序列化时考虑到数据定义与数据可能不完全匹配,在数据中添加注解,这会让数据变得庞大并拖慢处理速度。其它序列化系统有如Protocol Buffers类似的问题。所以为了Hadoop的前途考虑,Doug Cutting主导开发一套全新的序列化系统,这就是Avro,于09年加入Hadoop项目族中。 

二、为什么使用AVRO
Avro依赖模式(Schema)来实现数据结构定义。可以把模式理解为Java的类,它定义每个实例的结构,可以包含哪些属性。可以根据类来产生任意多个实例对象。对实例序列化操作时必须需要知道它的基本结构,也就需要参考类的信息。这里,根据模式产生的Avro对象类似于类的实例对象。每次序列化/反序列化时都需要知道模式的具体结构。所以,在Avro可用的一些场景下,如文件存储或是网络通信,都需要模式与数据同时存在。Avro数据以模式来读和写(文件或是网络),并且写入的数据都不需要加入其它标识,这样序列化时速度快且结果内容少。由于程序可以直接根据模式来处理数据,所以Avro更适合于脚本语言的发挥。

 Avro的模式主要由JSON对象来表示,它可能会有一些特定的属性,用来描述某种类型(Type)的不同形式。Avro支持八种基本类型(Primitive Type)和六种混合类型(Complex Type)。基本类型可以由JSON字符串来表示。每种不同的混合类型有不同的属性(Attribute)来定义,有些属性是必须的,有些是可选的,如果需要的话,可以用JSON数组来存放多个JSON对象定义。在这几种Avro定义的类型的支持下,可以由用户来创造出丰富的数据结构来,支持用户纷繁复杂的数据。  

 Avro支持两种序列化编码方式:二进制编码和JSON编码。使用二进制编码会高效序列化,并且序列化后得到的结果会比较小;而JSON一般用于调试系统或是基于WEB的应用。对Avro数据序列化/反序列化时都需要对模式以深度优先(Depth-First),从左到右(Left-to-Right)的遍历顺序来执行。基本类型的序列化容易解决,混合类型的序列化会有很多不同规则。对于基本类型和混合类型的二进制编码在文档中规定,按照模式的解析顺序依次排列字节。对于JSON编码,联合类型(Union Type)就与其它混合类型表现不一致。 

 Avro为了便于MapReduce的处理定义了一种容器文件格式(Container File Format)。这样的文件中只能有一种模式,所有需要存入这个文件的对象都需要按照这种模式以二进制编码的形式写入。对象在文件中以块(Block)来组织,并且这些对象都是可以被压缩的。块和块之间会存在同步标记符(Synchronization Marker),以便MapReduce方便地切割文件用于处理。下图是根据文档描述画出的文件结构图: 

在这里插入图片描述
上图已经对各块做肢解操作,但还是有必要再详细说明下。一个存储文件由两部分组成:头信息(Header)和数据块(Data Block)。而头信息又由三部分构成:四个字节的前缀(类似于Magic Number),文件Meta-data信息和随机生成的16字节同步标记符。这里的Meta-data信息让人有些疑惑,它除了文件的模式外,还能包含什么。文档中指出当前Avro认定的就两个Meta-data:schema和codec。这里的codec表示对后面的文件数据块(File Data Block)采用何种压缩方式。Avro的实现都需要支持下面两种压缩方式:null(不压缩)和deflate(使用Deflate算法压缩数据块)。除了文档中认定的两种Meta-data,用户还可以自定义适用于自己的Meta-data。这里用long型来表示有多少个Meta-data数据对,也是让用户在实际应用中可以定义足够的Meta-data信息。对于每对Meta-data信息,都有一个string型的key(需要以“avro.”为前缀)和二进制编码后的value。对于文件中头信息之后的每个数据块,有这样的结构:一个long值记录当前块有多少个对象,一个long值用于记录当前块经过压缩后的字节数,真正的序列化对象和16字节长度的同步标记符。由于对象可以组织成不同的块,使用时就可以不经过反序列化而对某个数据块进行操作。还可以由数据块数,对象数和同步标记符来定位损坏的块以确保数据完整性。

 上面是将Avro对象序列化到文件的操作。与之相应的,Avro也被作为一种RPC框架来使用。客户端希望同服务器端交互时,就需要交换双方通信的协议,它类似于模式,需要双方来定义,在Avro中被称为消息(Message)。通信双方都必须保持这种协议,以便于解析从对方发送过来的数据,这也就是传说中的握手阶段。 

 消息从客户端发送到服务器端需要经过传输层(Transport Layer),它发送消息并接收服务器端的响应。到达传输层的数据就是二进制数据。通常以HTTP作为传输模型,数据以POST方式发送到对方去。在Avro中,它的消息被封装成为一组缓冲区(Buffer),类似于下图的模型: 

在这里插入图片描述
如上图,每个缓冲区以四个字节开头,中间是多个字节的缓冲数据,最后以一个空缓冲区结尾。这种机制的好处在于,发送端在发送数据时可以很方便地组装不同数据源的数据,接收方也可以将数据存入不同的存储区。还有,当往缓冲区中写数据时,大对象可以独占一个缓冲区,而不是与其它小对象混合存放,便于接收方方便地读取大对象。

 下面聊下Avro的其它方面信息。前文中引述Doug Cutting的话说,Protocol Buffer在传输数据时,往数据中加入注释(annotation),以应对数据结构与数据不匹配的问题。但直接导致数据量变大,解析困难等缺点。那Avro是如何应对模式与数据的不同呢?为了保证Avro的高效,假定模式至少大部分是匹配的,然后定义一些验证规则,如果在规则满足的前提下,做数据验证。如果模式不匹配就会报错。相同模式,交互数据时,如果数据中缺少某个域(field),用规范中的默认值设置;如果数据中多了些与模式不匹配的数据。则忽视这些值。 

 Avro列出的优点中还有一项是:可排序的。就是说,一种语言支持的Avro程序在序列化数据后,可由其它语言的Avro程序对未反序列化的数据排序。我不知道这种机制是在什么样的场景下使用,但看起来还是挺不错的。 

 当前关于Avro的资料挺少的,上面的文章也是我由官方文档和作者的文章来总结的。我相信其中肯定有很多错误,或许有些方面根本就理解错了。现在放出这篇总结,便于不断修订和补充,也是对这两天学习成果的分享,希望对想了解Avro的人有些许帮助,更希望大家指证我理解错误的地方,利于提高。 

上一篇 Log4j使用介绍

猜你喜欢

转载自blog.csdn.net/qq_39188039/article/details/86636247