(一)数据序列化的意义
当我们需要将采集的日志存入文件或者是传输到下一个系统时,需要将数据对象转化为字节流格式,也就是数据的序列化过程。通常情况下,搭建一套数据仓库系统,会经历如下四个阶段:
1.没有序列化方案:该方案将数据定义为字符串格式,并以文本的形式保存,如果存在多条数据,则采用分隔符来分隔,例如“{|[”等。当增加字段时,通过在文本的最后增加新行的方式来进行,修改及解析比较繁琐,修改需要改动的依赖多。
2.采用编程语言内置的序列化方案,例如Java的Serialization等,这种方式会将日志系统与开发语言绑定起来。
3.格式化数据表达方式,如XML、JSON等,该方式能够约束每个字段的类型,为后续的优化和共享带来了便利。
4.采用序列化框架,例如Thrift、Avro、Protobuf等,通过引入schema的概念,使得序列化和维护变得非常高效,并且有了代码的特征。序列化框架的优点有:提供IDL用以描述数据的schema信息;支持跨语言的读写;数据的编码存储能够节约存储空间;支持schema的演化,也就是可以根据一定的规则修改schema。
目前比较常用的开源组建包括Facebook的Thrift、Google的Protobuf、Apache的Avro等,这些方案大同小异,并不存在绝对的优点,需要根据其特点灵活使用。
(二)Thrift方案
Thrift是Facebook开源的RPC框架,具备了序列化和RPC两个功能,几乎支持所有的编程语言,并提供了一套完整的IDL语言用来定义Schema信息,在实际应用中非常广泛。
以下是一个简单的Thrift例子:
// 命名空间定义
include "shared.thrift"
namespace cpp tutorial
// 定义别名
typedef i32 MyInteger
const i32 INT32CONSTANT = 9853
// 定义枚举类型
enum Operation {
ADD = 1,
SUBTRACT = 2,
MULTIPLY = 3,
DIVIDE = 4
}
// 定义结构体
struct Work {
1: i32 num1 = 0,
2: i32 num2,
3: Operation op,
4: optional string comment,
5: required int id
}
Thrift通过struct关键字来描述对象,由四种属性构成:
1.域编号:每个域必须是唯一的整数,可以不连续,通过该编号Thrift能够实现向前及向后的兼容性;
2.域修饰:包括required和optional两个关键字,用来对域的值进行限制;
3.域类型:包括int、long等基本类型,也支持set、list、map等复杂容器;
4.域名称:同一个struct下的域名必须唯一。
一旦给出了数据的Thrift IDL定以后,可以通过Thrift提供的编译器直接生成目标语言代码,例如Java:
thrift --gen java tutorial.thrift
下面是Java使用例子:
public class HelloServerDemo {
public static final int SERVER_PORT = 8090;
public void startServer() {
try {
System.out.println("HelloWorld TSimpleServer start ....");
//在这里调用了 HelloWorldImpl 规定了接受的方法和返回的参数
TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>( new HelloWorldImpl());
TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
TServer.Args tArgs = new TServer.Args(serverTransport);
tArgs.processor(tprocessor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer server = new TSimpleServer(tArgs);
server.serve();
} catch (Exception e) {
System.out.println("Server start error!!!");
e.printStackTrace();
}
}
public static void main(String[] args) {
HelloServerDemo server = new HelloServerDemo();
server.startServer();
}
}
public class HelloClientDemo {
public static final String SERVER_IP = "localhost";
public static final int SERVER_PORT = 8090;
public static final int TIMEOUT = 30000;
public void startClient(String userName) {
TTransport transport = null;
try {
transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
// 协议要和服务端一致
TProtocol protocol = new TBinaryProtocol(transport);
HelloWorldService.Client client = new HelloWorldService.Client(protocol);
transport.open();
String result = client.sayHello(userName);
System.out.println("Thrify client result =: " + result);
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
} finally {
if (null != transport) {
transport.close();
}
}
}
public static void main(String[] args) {
HelloClientDemo client = new HelloClientDemo();
client.startClient("china");
}
}
(三)Protobuf方案
Protobuf全名为Protocol Buffers,是Google开源的序列化框架,主要支持Java、C++及Python三种语言,语法和使用方式与Thrift非常类似,但没有实现RPC功能。由于采用了更加紧凑的数据编码方式,大部分情况下,Protobuf比Thrift占用的存储空间更小,且解析速度更快。如果不需要支持其他的语言,且准求性能,可以使用Protobuf来替代。
以下是一个简单的Protobuf例子:
syntax = "proto3";
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
message AddressBook {
repeated Person people = 1;
int32 id = 2;
}
Protobuf采用message关键字来统一描述对象,经过Protobuf编译后,会生成对应语言的类结构,同时,message也由四个属性构成:
1.域编号:每个域必须是唯一的整数,但可以不连续,该编号是前后兼容性的保障;
2.域修饰:包括required、optional和repeated三个关键字,repeated表示该域的值可以有多个;
3.域类型:支持常见的数据类型,包括bool、int32、float、double和string等,从Protobuf3开始,增加了对map的支持;
4.域名称:同一个message下每个域名必须唯一。
一旦给出了对应数据的IDL定以后,同样可以使用Protobuf提供的编译器生成目标语言代码,例如Java:
protoc --java_out src/ tutorial.proto
生成后的java代码被放在了当前目录src下。Java代码例子如下:
public class GenerareClass {
public static void main(String[] args) throws IOException {
String protoPath = System.getProperty("user.dir") + "\\src\\main\\resources\\proto";
List<String> protoFileList = new ArrayList<String>();
File f = new File(protoPath);
File fa[] = f.listFiles();
for (int i = 0; i < fa.length; i++) {
File fs = fa[i];
if (fs.isFile()) {
protoFileList.add(fs.getName());
}
}
for (String protoFile : protoFileList) {
System.out.println(protoFile);
String strCmd = "D:/file/blockChain/protobuf-java-3.5.1/protobuf-3.5.1/src/protoc.exe --java_out=../../java " + protoFile;
Runtime.getRuntime().exec(strCmd, null, new File(protoPath));
}
}
}
(四)Avro方案
Avro是Hadoop生态中支持的RPC框架,是为了给Hadoop提供一个高效灵活且易于演化的基础库,具有如下几个特点:
1.动态类型:Avro不需要生成代码,它将数据与schema存放在一起,这样数据处理过程中不需要生成代码;
2.未标记数据:读取Avro数据时schema是已知的,因此类型信息减少,这使得序列化后的数据量变小;
3.不需要显示指定域编号:处理数据时新旧schema都是已知的,因此通过字段名称既可以解决兼容性问题。
以下是一个简单的Avro例子:
{"namespace": "tutorial.avro",
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": ["int", "null"]},
{"name": "favorite_color", "type": ["string", "null"]}
]
}
Avro的语法与前两种比较类似,只不过无需显示指定编号。Avro最初只支持JSON格式,后来增加了IDL的支持。Java语言的操作命令如下:
java -jar avro-tools.jar idl tutorial.avdl tutorial.avpr
java -jar avro-tools.jar compile protocol tutorial.avpr src/
Java例子如下:
User.Builder builder = User.newBuilder();
builder.setName("张三");
builder.setAge(30);
builder.setEmail("zhangsan@*.com");
User user = builder.build();
//序列化
File diskFile = new File("/data/users.avro");
DatumWriter<User> userDatumWriter = new SpecificDatumWriter<User>(User.class);
DataFileWriter<User> dataFileWriter = new DataFileWriter<User>(userDatumWriter);
//指定schema
dataFileWriter.create(User.getClassSchema(), diskFile);
dataFileWriter.append(user);
dataFileWriter.fSync();//多次写入之后,可以调用fsync将数据同步写入磁盘(IO)通道
user.setName("李四");
user.setEmail("lisi@*.com");
dataFileWriter.append(user);
dataFileWriter.close();
//反序列化
DatumReader<User> userDatumReader = new SpecificDatumReader<User>(User.class);
// 也可以使用DataFileStream
// DataFileStream<User> dataFileStream = new DataFileStream<User>(new FileInputStream(diskFile),userDatumReader);
DataFileReader<User> dataFileReader = new DataFileReader<User>(diskFile, userDatumReader);
User _current = null;
while (dataFileReader.hasNext()) {
//注意:avro为了提升性能,_current对象只会被创建一次,且每次遍历都会重用此对象
//next方法只是给_current对象的各个属性赋值,而不是重新new。
_current = dataFileReader.next(_current);
//toString方法被重写,将获得JSON格式
System.out.println(_current);
}
dataFileReader.close();
(五)序列化框架的对比