Hadoop 之管理 Avro
----------------------------------------------------------------------------------------------------------------------------------------------
Apache Avro 是一个中立语言的(language-neutral) 数据序列化系统。该项目由 Doug Cutting (the creator of Hadoop) 创建,旨在解决 Hadoop Writables 的
主要不足:缺乏语言可移植性。拥有一个可以被多种语言处理的数据格式比只绑定到单一语言上的数据格式更易于与更广泛的应用共享数据集。 Avro 目前可以被
C, C++, C#, Java, JavaScript, Perl, PHP, Python, and Ruby 语言处理。
但为什么要创建一个新的数据序列化系统? Avro 具有一系列特性,它们一起形成了与其他系统的不同,例如,Apache Thrift or Google’s Protocol Buffers .
与这些系统及其他系统类似, Avro 数据描述使用了一种语言独立的架构(a language-independent schema)。但与其他系统不同的是,在 Avro 中代码生成是可选的,
这意味着可以对遵循一个指定模式(schema)的数据进行读写操作,即使在此之前代码从没看到过这个特定的数据模式。为此, Avro 假定数据模式总是存在的(schema
is always present) ———— 在读取数据和写入数据时都存在(at both read and write time) ———— 这样可以形成一个非常紧凑的编码,因为编码的值不需要与一个
字段标识符绑定。
Avro schemas 通常写成 JSON 格式,数据通常用一个二进制格式编码,但也有其他选项。有一个称为 Avro IDL 的高级别的语言(a higher-level language called
Avro IDL) 用于程序员们使用更熟悉的 C-like 类的语言来写 Avro schemas 。还有一个基于 JSON 的数据编码器(a JSON-based data encoder), 对于人类是可读的,
对构造原型和调试 Avro 数据很有用。
Avro 规范(Avro specification)精确定义了所有实现必须支持的二进制数据格式,也定义了一些 Avro 实现应该支持的一些其他特性。但规范中没有定义 API 规范:
实现对 API 拥有全方位的控制能力,可以根据需要操作 Avro 数据并给出相应的 API, 因为每个 API 都是语言相关的。事实上,只要有一种二进制格式就足够了。
Avro 具有丰富的模式解析(schema resolution) 能力。在精心定义的约束条件下,用于读取数据的模式不需要与用于写入数据的模式相同。这就是 Avro 支持 模式
演变的机制(This is the mechanism by which Avro supports schema evolution)。例如,通过在用于读取老数据的模式中声明,一个新的,可选的的字段可以加入
到一个记录中(record)。新客户端和旧的客户端一样能够读取老的数据,而新客户端可以使用新字段写入新的数据。相反,如果旧的客户端看到新编码的数据,它会
优雅地忽略新字段并继续处理就像它对旧的数据格式所做的一样。
Avro 为顺序的对象规范了一个对象容器格式(object container format), 类似于 Hadoop 的顺序文件。一个 Avro datafile 有一个元数据节(metadata section)
模式存储在其中,使文件成为自描述的(self-describing)。 Avro datafiles 支持压缩和可切分,这是 MapReduce 数据输入格式至关重要的特性。事实上,Avro
datafile 的支持远远超出 MapReduce:本书中所有的数据处理框架(Pig, Hive, Crunch, Spark) 都可以读写 Avro datafiles 。
Avro 也可用于 RPC
*
*
*
1. Avro 数据类型与模式 (Avro Data Types and Schemas)
--------------------------------------------------------------------------------------------------------------------------------------------------
Avro 定义了少量的基本数据类型,可以通过模式(by writing schemas)的方式用于建立应用程序特定的(application-specific)数据结构。为了互操作性,实现必须
支持所有的 Avro 类型。
Avro 基本数据类型列于下表。每种基本类型可以通过使用 type 属性使用更冗长的形式,例如:
{ "type": "null" }
Avro primitive types
+===========+===========================================================+===========+
| 类型 | 描述 | Schema |
+-----------+-----------------------------------------------------------+-----------+
| null | The absence of a value | "null" |
+-----------+-----------------------------------------------------------+-----------+
| boolean | A binary value | "boolean" |
+-----------+-----------------------------------------------------------+-----------+
| int | 32-bit signed integer | "int" |
+-----------+-----------------------------------------------------------+-----------+
| long | 64-bit signed integer | "long" |
+-----------+-----------------------------------------------------------+-----------+
| float | Single-precision (32-bit) IEEE 754 floating-point number | "float" |
+-----------+-----------------------------------------------------------+-----------+
| double | Double-precision (64-bit) IEEE 754 floating-point number | "double" |
+-----------+-----------------------------------------------------------+-----------+
| bytes | Sequence of 8-bit unsigned bytes | "bytes" |
+-----------+-----------------------------------------------------------+-----------+
| string | Sequence of Unicode characters | "string" |
+-----------+-----------------------------------------------------------+-----------+
Avro 也定义了复杂类型,如下表:
Avro complex types
+===========+=======================================================================================+===========================================+
| 类型 | 描述 | Schema example |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| array | An ordered collection of objects. All objects in a particular array must have the | { |
| | same schema. | "type": "array", |
| | | "items": "long" |
| | | } |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| map | An unordered collection of key-value pairs. Keys must be strings and values may be | { |
| | any type, although within a particular map, all values must have the same schema. | "type": "map", |
| | | "values":"string" |
| | | } |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| record | A collection of named fields of any type. | { |
| | | "type": "record", |
| | | "name": "WeatherRecord", |
| | | "doc": "A weather reading.", |
| | | "fields": [ |
| | | {"name":"year", "type":"int"}, |
| | | {"name":"temperature","type": "int"}, |
| | | {"name":"stationId","type": "string"}] |
| | | } |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| enum | A set of named values. | { |
| | | "type": "enum", |
| | | "name":"Cutlery", |
| | | "doc": "An eating utensil.", |
| | | "symbols": |
| | | ["KNIFE", "FORK","SPOON"] |
| | | } |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| fixed | A fixed number of 8-bit unsigned bytes. | { |
| | | "type": "fixed", |
| | | "name":"Md5Hash", |
| | | "size": 16 |
| | | } |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
| union | A union of schemas. A union is represented by a JSON array, where each element in the | [ |
| | array is a schema. Data represented by a union must match one of the schemas in the | "null", |
| | union. | "string", |
| | | {"type": "map","values": "string"} |
| | | ] |
+-----------+---------------------------------------------------------------------------------------+-------------------------------------------+
每种 Avro 语言 API 都有对应于每一个 Avro 类型语言特定的表现形式。例如, Avro 的 double 类型,在 C, C++ 以及 Java 中由 double 表示,在 Python 中由一个
float 表示,而在 Ruby 中由 Float 表示。
对于一种语言,可能有不止一种的表现形式,或者映射。所有语言都支持一个动态映射,即便运行前不知道该模式也可以使用。 Java 称之为通用映射(Generic mapping)
另外, Java 和 C++ 实现能为一个 Avro schema 生成代码来表示数据。代码生成(Code generation),在 Java 中称为特殊映射(Specific mapping) ,如果在读取或写入
数据之前有一个 schema 的拷贝能优化数据处理。生成的类也为用户代码提供了一个比通用映射更加面向问题域的API(a more domain-oriented API)。
Java 还有第三种映射,即反射映射(the Reflect mapping),使用反射机制将 Avro 类型映射为预先存在的 Java 类型。这种映射比通用和特殊映射慢,但可能是定义类型
的一个便利方法,因为 Avro 能自动推断一个 schema .
下表列举了 Java 的类型映射。如表所示,特殊映射与通用映射相同,除非特别标注(并且反射映射与特殊映射相同,除非特别说明)。特殊映射与通用映射只在 record,
enum, 以及 fixed 类型上不同,它们都有生成的类(类名由 name 和可选的 namespace 属性控制)。
Avro Java type mappings
+===========+===============================================+===========================================+===========================================+
| Avro 类型 | Generic Java mapping | Specific Java mapping | Reflect Java mapping |
+-----------+-----------------------------------------------+-------------------------------------------+-------------------------------------------+
| null | null type |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| boolean | boolean |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| int | int | byte, short, int |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| long | long |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| float | float |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| double | double |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| bytes | java.nio.ByteBuffer | Array of bytes |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| string | org.apache.avro.util.Utf8 or java.lang.String | java.lang.String |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| array | org.apache.avro.generic.GenericArray | Array or java.util.Collection |
+-----------+-------------------------------------------------------------------------------------------+-------------------------------------------+
| map | java.util.Map | |
+-----------+-----------------------------------------------+-------------------------------------------+-------------------------------------------+
| record | org.apache.avro.generic.GenericRecord | Generated class implementing | Arbitrary user class with constructor; |
| | | org.apache.avro.specific.SpecificRecord | all inherited instance fields are used |
+-----------+-----------------------------------------------+-------------------------------------------+-------------------------------------------+
| enum | java.lang.String | Generated Java enum | Arbitrary Java enum |
+-----------+-----------------------------------------------+-------------------------------------------+-------------------------------------------+
| fixed | org.apache.avro.generic.GenericFixed | Generated class implementing | org.apache.avro.generic.GenericFixed |
| | | org.apache.avro.specific.SpecificFixed | |
+-----------+-----------------------------------------------+-------------------------------------------+-------------------------------------------+
| union | java.lang.Object |
+-----------+---------------------------------------------------------------------------------------------------------------------------------------+
NOTE:
------------------------------------------------------------------------------------------------------------------------------------------------
Avro string 能被表示为 Java String 或者 the Avro Utf8 Java 类型。使用 Utf8 是因为效率:因为它是可变的,一个 Utf8 实例可以重用读取或写入一系列值。
同时,Java String 在对象创建时解码 UTF-8,而 Avro Utf8 是延迟解码的,在有些情况下能提升性能。
Utf8 实现了 Java’s java.lang.CharSequence 接口,提供一些与 Java 类库的互操作性。其他情况,可能有必要把 Utf8 实例转换为 String 对象,通过调用它的
toString() method .
Utf8 是通用映射和特殊映射的默认类型,但对于一个特定的映射使用 String 类型也是可以的。有很多方法可以做到这一点,第一种方法是在 schema 中使用
String 设置 avro.java.string 属性:
{ "type": "string", "avro.java.string": "String" }
或者,对于特殊映射,可以生成具有基于 String 的 getter 和 setter 的类。如果使用 Avro Maven 插件,这可以通过设置配置属性 stringType 为 String 。
最后,注意 Java 反射映射总是使用 String 对象,因其设计用于 Java 兼容性,而不是考虑性能。
*
*
*
2. 内存中的序列化和反序列化 (In-Memory Serialization and Deserialization)
--------------------------------------------------------------------------------------------------------------------------------------------------
Avro 为序列化和反序列化提供了 API, 这对集成 Avro 到现有系统中很有用,例如一个已定义好了分帧格式的消息系统。其他情况,可以考虑使用 Avro datafile 格式。
写一个 Java 程序用来从流中读取和写出到流。以一个简单的 Avro schema 开始,表示一对字符串作为一条记录:
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings.",
"fields": [
{"name": "left", "type": "string"},
{"name": "right", "type": "string"}
]
}
如果这个 schema 保存到一个类路径上文件(a file on the classpath), 名为 StringPair.avsc(.avsc is the conventional extension for an Avro schema)。可以
使用下面两行代码载入:
Schema.Parser parser = new Schema.Parser();
Schema schema = parser.parse(getClass().getResourceAsStream("StringPair.avsc"));
可以使用 Generic API 创建一个 Avro record 实例:
GenericRecord datum = new GenericData.Record(schema);
datum.put("left", "L");
datum.put("right", "R");
下一步,序列化记录到一个 output stream:
ByteArrayOutputStream out = new ByteArrayOutputStream();
DatumWriter<GenericRecord> writer = new GenericDatumWriter<GenericRecord>(schema);
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
writer.write(datum, encoder);
encoder.flush();
out.close();
这里有两个重要的对象:DatumWriter 和 Encoder 。一个 DatumWriter 将数据对象转换成一个 Encoder 可以理解的类型,Encoder 之后写出到 output stream. 这里
使用了一个 GenericDatumWriter, 传递 GenericRecord 的字段给 Encoder .
GenericDatumWriter 需要把 schema 传递过来,因为它依据这个 schema 来确定数据对象里的值来写出到数据流。在调用 writer 的 write() method 之后,刷新encoder,
然后关闭输出流。
可以反转这个过程,从字节缓存中读取对象:
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(schema);
Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null);
GenericRecord result = reader.read(null, decoder);
assertThat(result.get("left").toString(), is("L"));
assertThat(result.get("right").toString(), is("R"));
从 result.get("left") and result.get("right") 返回的对象是 Utf8 类型, 因此这里通过调用 toString() 方法转换为 Java String 对象。
The Specific API
------------------------------------------------------------------------------------------------------------------------------------------------
看下等价的使用 Specific API 的代码。可以通过使用 Avro Maven 插件编译从 schema 文件生成 StringPair class 。相关 POM :
<project>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<version>${avro.version}</version>
<executions>
<execution>
<id>schemas</id>
<phase>generate-sources</phase>
<goals>
<goal>schema</goal>
</goals>
<configuration>
<includes>
<include>StringPair.avsc</include>
</includes>
<stringType>String</stringType>
<sourceDirectory>src/main/resources</sourceDirectory>
<outputDirectory>${project.build.directory}/generated-sources/java</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
</project>
作为 Maven 的备选方案,可以使用 Avro’s Ant task
org.apache.avro.specific.SchemaTask ,或 Avro command-line tools 用于为 schema 生成 Java 代码。
在序列化和反序列化代码中,作为替代 GenericRecord 构造 StringPair 实例,使用一个 SpecificDatumWriter 写出到数据流,并
使用一个 SpecificDatumReader 读回对象:
StringPair datum = new StringPair();
datum.setLeft("L");
datum.setRight("R");
ByteArrayOutputStream out = new ByteArrayOutputStream();
DatumWriter<StringPair> writer = new SpecificDatumWriter<StringPair>(StringPair.class);
Encoder encoder = EncoderFactory.get().binaryEncoder(out, null);
writer.write(datum, encoder);
encoder.flush();
out.close();
DatumReader<StringPair> reader =
new SpecificDatumReader<StringPair>(StringPair.class);
Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null);
StringPair result = reader.read(null, decoder);
assertThat(result.getLeft(), is("L"));
assertThat(result.getRight(), is("R"));
*
*
*
3. Avro 数据文件 (Avro Datafiles)
--------------------------------------------------------------------------------------------------------------------------------------------------
Avro 的对象容器文件格式用于存储 Avro 对象序列(sequences of Avro objects). 它非常类似于 Hadoop 的 sequence 文件格式设计。它们主要的区别在于 Avro
Datafiles 为跨语言可移植设计的,因此,例如可以用 Python 写入一个文件而使用 C 语言中读出。
一个 datafile 有一个文件头包含元数据(metadata), 其中含有 Avro schema 和一个同步标记(sync marker), 后跟一系列的(可选压缩的)数据块,数据块中包含已序列
化的 Avro 对象。数据块由文件中唯一的一个同步标记分隔,在定位到文件中任意的一个点之后,可以快速地与一个数据块边界重新同步,例如一个 HDFS 数据块边界。
因此,Avro Datafiles 是可切分的,这使它对高效的 MapReduce 处理易于控制。
把 Avro 对象写入一个数据文件与写入到一个流中类似。使用一个 DatumWriter, 但不使用 Encoder, 使用 DatumWriter 创建一个 DataFileWriter 实例。然后创建一个
新的数据文件(which, by convention, has a .avro extension) 并把对象追加到文件:
File file = new File("data.avro");
DatumWriter<GenericRecord> writer =
new GenericDatumWriter<GenericRecord>(schema);
DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<GenericRecord>(writer);
dataFileWriter.create(schema, file);
dataFileWriter.append(datum);
dataFileWriter.close();
写入数据文件的对象必须遵循文件的 schema, 否则,调用 append() method 时会抛出异常。
这个例子演示了写入到一个本地文件(java.io.File in the previous snippet), 但实际上通过调用 DataFileWriter 的重载 create() method,可以写入到任何
java.io.OutputStream . 例如写到 HDFS 上的一个文件,通过调用 FileSystem 的 Create() 方法得到一个 OutputStream 。
从一个数据文件中读回对象与之前从一个 in-memory 流中读取对象类似,一个重要的区别是:不需要指定一个 schema, 因为它可以从文件的元数据中读取。实际
上,可以从 DataFileReader 实例上得到 schema, 使用 getSchema() 方法,并且可以验证它是写入原始对象使用的 schema :
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>();
DataFileReader<GenericRecord> dataFileReader = new DataFileReader<GenericRecord>(file, reader);
assertThat("Schema is the same", schema, is(dataFileReader.getSchema()));
DataFileReader 是一个常规的 Java 迭代器,因此可以通过调用它的 hasNext() and next() methods 从头至尾迭代它的数据对象:
assertThat(dataFileReader.hasNext(), is(true));
GenericRecord result = dataFileReader.next();
assertThat(result.get("left").toString(), is("L"));
assertThat(result.get("right").toString(), is("R"));
assertThat(dataFileReader.hasNext(), is(false));
然而,不是使用通常的 next() 方法,而应优先使用它带有一个对象返回实例的重载形式(in this case, GenericRecord),因为它会重用这个对象,为含有很多对象的文件
节约了对象分配和垃圾回收的开销。下面是习惯用法:
GenericRecord record = null;
while (dataFileReader.hasNext()) {
record = dataFileReader.next(record);
// process record
}
如果对象重用不重要,可以使用如下比较短的形式:
for (GenericRecord record : dataFileReader) {
// process record
}
对于通常情况是,读取一个在 Hadoop 文件系统上的文件,使用一个 Hadoop Path 对象的 Avro 的 FsInput 来指定输入文件。 DataFileReader 实际上提供了随机访问
Avro 数据文件的能力(via its seek() and sync() methods),然而,大多数情况下,顺序的流访问就足够了,使用 DataFileStream . DataFileStream 可以从任何的
Java InputStream 中读取数据。
*
*
*
4. 互操作性 (Interoperability)
--------------------------------------------------------------------------------------------------------------------------------------------------
为了演示 Avro 的语言互操作性,使用一种语言写入一个数据文件(Python),然后使用另一种语言读取(Java)
1. Python API
----------------------------------------------------------------------------------------------------------------------------------------------
下面的程序从标准输入读取逗号分隔的字符串,把它们作为 StringPair records 写入到一个 Avro datafile. 创建了一个 DatumWriter 和一个 DataFileWriter
对象。注意,代码中嵌入了 Avro schema , 尽管可以从一个文件中读取获得相同的效果。
Python 把 Avro records 表示为 dictionaries; 从标准输入读取的每一行被转化成一个 dict 对象,然后追加给 DataFileWriter.
//A Python program for writing Avro record pairs to a datafile
import os
import string
import sys
from avro import schema
from avro import io
from avro import datafile
if __name__ == '__main__':
if len(sys.argv) != 2:
sys.exit('Usage: %s <data_file>' % sys.argv[0])
avro_file = sys.argv[1]
writer = open(avro_file, 'wb')
datum_writer = io.DatumWriter()
schema_object = schema.parse("\
{ "type": "record",
"name": "StringPair",
"doc": "A pair of strings.",
"fields": [
{"name": "left", "type": "string"},
{"name": "right", "type": "string"}
]
}")
dfw = datafile.DataFileWriter(writer, datum_writer, schema_object)
for line in sys.stdin.readlines():
(left, right) = string.split(line.strip(), ',')
dfw.append({'left':left, 'right':right});
dfw.close()
运行这个程序之前,需要为 Python 安装 Avro:
% easy_install avro
运行程序,指定要写入的文件名(pairs.avro),然后通过标准输入发送输入对,使用 Ctrl-D 标记文件结束:
% python ch12-avro/src/main/py/write_pairs.py pairs.avro
a,1
c,2
b,3
b,2
^D
2. Avro 工具集 (Avro Tools)
----------------------------------------------------------------------------------------------------------------------------------------------
下一步,使用 Avro 工具(written in Java) 来显示 pairs.avro 文件的内容。其 tojson command 将一个 Avro datafile 转换为 JSON 并打印到控制台:
% java -jar $AVRO_HOME/avro-tools-*.jar tojson pairs.avro
{"left":"a","right":"1"}
{"left":"c","right":"2"}
{"left":"b","right":"3"}
{"left":"b","right":"2"}
结果成功地在两个 Avro 实现之间(Python and Java)交换了复杂的数据。
*
*
*
5. Schema 解析 (Schema Resolution)
--------------------------------------------------------------------------------------------------------------------------------------------------
可以选择使用一种与写入数据时使用的 schema (the writer’s schema) 不同的 schema (the reader’s schema) 来将数据读回。这是一个强大的工具,因为它能够使
schema 演化(schema evolution). 为了演示,考虑为字符串对使用一个新的 schema, 添加一个 description 字段:
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings with an added field.",
"fields": [
{"name": "left", "type": "string"},
{"name": "right", "type": "string"},
{"name": "description", "type": "string", "default": ""}
]
}
可以使用这个 schema 读取之前我们序列化的数据,因为,至关重要地,给 description 字段一个默认值(the empty string), Avro 读取的记录中如果没有这样的字段
定义,会使用这个默认值。如果忽略这个 default 属性,当尝试读取旧格式的数据时会得到一个错误。
NOTE
---------------------------------------------------------------------------------------------------------------------
如果使用 null 作为默认值而不是 empty 的字符串,定义 description 字段应该使用一个带有 null Avro 类型的 union:
{"name": "description", "type": ["null", "string"], "default": null}
当 reader 的 schema 不同于 writer 的 schema 时,使用 GenericDatumReader 有两个 schema 对象的构造器,reader 的和 writer 的,
按以下顺序:
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(schema, newSchema);
Decoder decoder = DecoderFactory.get().binaryDecoder(out.toByteArray(), null);
GenericRecord result = reader.read(null, decoder);
assertThat(result.get("left").toString(), is("L"));
assertThat(result.get("right").toString(), is("R"));
assertThat(result.get("description").toString(), is(""));
对于 datafiles ,有 writer 的 schema 存储在元数据中,我们只需要明确指定 reader 的 schema, 把 null 传递给 writer 的 schema:
DatumReader<GenericRecord> reader = new GenericDatumReader<GenericRecord>(null, newSchema);
另一种使用不同 reader schema 的常用场景是从 record 中移除字段(drop fields in a record),这种操作称为 projection. 在持有的记录中包含有当量的字段并且
只想读取其中很少的一部分时这种方法很有用。例如,下面的 schema 可以用于只读取一个 StringPair 的 right 字段:
{
"type": "record",
"name": "StringPair",
"doc": "The right field of a pair of strings.",
"fields": [
{"name": "right", "type": "string"}
]
}
Schema 解析的规则直接影响 Schema 如何从一个版本演变到下一版本,并在 Avro 规范中对所有的 Avro 类型阐明。下表从 reader 和 writer (or servers and clients)
角度展示一个 record 演化规则的概要:
Schema resolution of records
+===========+===========+===========+=======================================================================================================+
| 新 schema | Writer | Reader | 操作 |
+-----------+-----------+-----------+-------------------------------------------------------------------------------------------------------+
| 添加 | Old | New | The reader uses the default value of the new field, since it is not written by the writer. |
| +-----------+-----------+-------------------------------------------------------------------------------------------------------+
| 字段 | New | Old | The reader does not know about the new field written by the writer, so it is ignored (projection) |
+-----------+-----------+-----------+-------------------------------------------------------------------------------------------------------+
| 移除 | Old | New | The reader ignores the removed field (projection). |
| +-----------+-----------+-------------------------------------------------------------------------------------------------------+
| 字段 | New | Old | The removed field is not written by the writer. If the old schema had a default defined for |
| | | | the field, the reader uses this; otherwise, it gets an error. In this case, it is best to update the |
| | | | reader’s schema, either at the same time as or before the writer’s. |
+-----------+-----------+-----------+-------------------------------------------------------------------------------------------------------+
另一个演化 Avro schema 有用的技术是使用名称别名(name aliases). 别名允许在 schema 中使用与原始写入数据时使用的 schema 中不同的名称来读取 Avro 数据。
例如,下面的 reader schema 用于新的字段名称 first 和 second 替代 left 和 right 读取 StringPair:
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings with aliased field names.",
"fields": [
{"name": "first", "type": "string", "aliases": ["left"]},
{"name": "second", "type": "string", "aliases": ["right"]}
]
}
注意,别名用于转换(at read time) writer schema 到 reader schema, 但被别名的名称对 reader 是不可用的,在这个例子中, reader 不能使用字段名 left 和
right, 因为它们已经被转换为 first 和 second
*
*
*
6. 排序次序 (Sort Order)
--------------------------------------------------------------------------------------------------------------------------------------------------
Avro 为对象定义了排序次序。对于大多数 Avro 类型,正如我们所期望的那样,排序次序是它的自然顺序,例如,数值类型是按它们的数值升序排序。其类型有一点
巧妙,例如, 枚举类型按它们定义的符号值次序进行比较,而不是按符号字符串的值次序比较。
除了 record 类型外,所有均按照 Avro 规范中预先定义的规则进行排序,用户不能改写。对于 record 类型,可以通过为字段指定 order 属性(order attribute) 来
控制排序次序。可以取三个值其中之一:ascending (the default), descending (to reverse the order), 或者 ignore (so the field is skipped for comparison
purposes)。
举个例子,下面的 schema (SortedStringPair.avsc) 定义了一个有序的 StringPair record, 按 right 字段降序排序。 left 字段在排序时被忽略:
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings, sorted by right field descending.",
"fields": [
{"name": "left", "type": "string", "order": "ignore"},
{"name": "right", "type": "string", "order": "descending"}
]
}
record 的字段按 reader schema 在文档中的次序两两比较,因此,指定一个合适的 reader schema, 可以对数据记录使用任意的次序。下面的 schema (
SwitchedStringPair.avsc)定义了一个先按 right 字段,然后按 left 字段排序的排序次序:
{
"type": "record",
"name": "StringPair",
"doc": "A pair of strings, sorted by right then left.",
"fields": [
{"name": "right", "type": "string"},
{"name": "left", "type": "string"}
]
}
Avro 实现高效的二进制比较操作。也就是说, Avro 不必将二进制数据反序列化成对象来执行比较,因为它能直接在字节流上执行比较操作。例如,在原始的 StringPair
schema(with no order attributes), Avro 实现二进制比较如下:
第一个字段(The first field), left, 是一个 UTF-8 编码的字符串, Avro 可以按词汇字节进行比较。如果不同,次序就确定了, Avro 可以就此结束比较过程。否则,
如果这两个字节序列相同,就比较两个第二个字段(right), 因为这个字段是另一个 UTF-8 字符串,因此再一次进行字节级别上的词汇比较。
注意,在此描述的比较功能,与为 Writable 类型在 Implementing a RawComparator for speed 写的二进制 comparator 具有完全相同的逻辑。最重要的是 Avro 为
我们提供了comparator, 因此我们不需要写以及维护这些代码。也可以通过改变 reader 的 schema, 很容易地改变排序次序。对于 SortedStringPair.avsc 和
SwitchedStringPair.avsc schema, Avro 使用的比较函数与刚刚描述的内容基本上是相同的。不同点是要考虑使用哪些字段,考虑字段使用的排序次序,以及使用升序
还是降序。
*
*
*
7. Avro MapReduce
--------------------------------------------------------------------------------------------------------------------------------------------------
Avro 提供了很多类用于简化对 Avro 数据运行 MapReduce 程序,它们在 org.apache.avro.mapreduce 包中。
再次工作在气象数据集,查出每年最高气温的 MapReduce 程序,这次使用 Avro MapReduce API . 使用下面的 schema 表示气温记录:
{
"type": "record",
"name": "WeatherRecord",
"doc": "A weather reading.",
"fields": [
{"name": "year", "type": "int"},
{"name": "temperature", "type": "int"},
{"name": "stationId", "type": "string"}
]
}
下面的程序读取文本输入,并写出到包含气温记录的 Avro 数据文件中作为输出。
// MapReduce program to find the maximum temperature, creating Avro output
public class AvroGenericMaxTemperature extends Configured implements Tool {
private static final Schema SCHEMA = new Schema.Parser().parse(
"{" +
" \"type\": \"record\"," +
" \"name\": \"WeatherRecord\"," +
" \"doc\": \"A weather reading.\"," +
" \"fields\": [" +
" {\"name\": \"year\", \"type\": \"int\"}," +
" {\"name\": \"temperature\", \"type\": \"int\"}," +
" {\"name\": \"stationId\", \"type\": \"string\"}" +
" ]" +
"}"
);
public static class MaxTemperatureMapper extends Mapper<LongWritable, Text, AvroKey<Integer>, AvroValue<GenericRecord>> {
private NcdcRecordParser parser = new NcdcRecordParser();
private GenericRecord record = new GenericData.Record(SCHEMA);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
parser.parse(value.toString());
if (parser.isValidTemperature()) {
record.put("year", parser.getYearInt());
record.put("temperature", parser.getAirTemperature());
record.put("stationId", parser.getStationId());
context.write(new AvroKey<Integer>(parser.getYearInt()),
new AvroValue<GenericRecord>(record));
}
}
}
public static class MaxTemperatureReducer extends Reducer<AvroKey<Integer>, AvroValue<GenericRecord>, AvroKey<GenericRecord>, NullWritable> {
@Override
protected void reduce(AvroKey<Integer> key, Iterable<AvroValue<GenericRecord>> values, Context context)
throws IOException, InterruptedException {
GenericRecord max = null;
for (AvroValue<GenericRecord> value : values) {
GenericRecord record = value.datum();
if (max == null ||
(Integer) record.get("temperature") > (Integer) max.get("temperature")) {
max = newWeatherRecord(record);
}
}
context.write(new AvroKey(max), NullWritable.get());
}
private GenericRecord newWeatherRecord(GenericRecord value) {
GenericRecord record = new GenericData.Record(SCHEMA);
record.put("year", value.get("year"));
record.put("temperature", value.get("temperature"));
record.put("stationId", value.get("stationId"));
return record;
}
}
@Override
public int run(String[] args) throws Exception {
if (args.length != 2) {
System.err.printf("Usage: %s [generic options] <input> <output>\n",
getClass().getSimpleName());
ToolRunner.printGenericCommandUsage(System.err);
return -1;
}
Job job = new Job(getConf(), "Max temperature");
job.setJarByClass(getClass());
job.getConfiguration().setBoolean(
Job.MAPREDUCE_JOB_USER_CLASSPATH_FIRST, true);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
AvroJob.setMapOutputKeySchema(job, Schema.create(Schema.Type.INT));
AvroJob.setMapOutputValueSchema(job, SCHEMA);
AvroJob.setOutputKeySchema(job, SCHEMA);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(AvroKeyOutputFormat.class);
job.setMapperClass(MaxTemperatureMapper.class);
job.setReducerClass(MaxTemperatureReducer.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new AvroGenericMaxTemperature(), args);
System.exit(exitCode);
}
}
运行:
------------------------------------------------------------------------------------------------------
% export HADOOP_CLASSPATH=avro-examples.jar
% export HADOOP_USER_CLASSPATH_FIRST=true # override version of Avro in Hadoop
% hadoop jar avro-examples.jar AvroGenericMaxTemperature \
input/ncdc/sample.txt output
使用 Avro tools JAR 可以查看输出结果,工具将 Avro 数据文件内容以 JSON 格式输出:
% java -jar $AVRO_HOME/avro-tools-*.jar tojson output/part-r-00000.avro
{"year":1949,"temperature":111,"stationId":"012650-99999"}
{"year":1950,"temperature":22,"stationId":"011990-99999"}
*
*
*
8. 使用 Avro MapReduce 排序 (Sorting Using Avro MapReduce)
--------------------------------------------------------------------------------------------------------------------------------------------------
使用 Avro 的排序能力并联合 MapReduce 写一个对 Avro 数据文件排序的程序。
// A MapReduce program to sort an Avro datafile
public class AvroSort extends Configured implements Tool {
static class SortMapper<K> extends Mapper<AvroKey<K>, NullWritable, AvroKey<K>, AvroValue<K>> {
@Override
protected void map(AvroKey<K> key, NullWritable value, Context context) throws IOException, InterruptedException {
context.write(key, new AvroValue<K>(key.datum()));
}
}
static class SortReducer<K> extends Reducer<AvroKey<K>, AvroValue<K>, AvroKey<K>, NullWritable> {
@Override
protected void reduce(AvroKey<K> key, Iterable<AvroValue<K>> values, Context context) throws IOException, InterruptedException {
for (AvroValue<K> value : values) {
context.write(new AvroKey(value.datum()), NullWritable.get());
}
}
}
@Override
public int run(String[] args) throws Exception {
if (args.length != 3) {
System.err.printf(
"Usage: %s [generic options] <input> <output> <schema-file>\n",
getClass().getSimpleName());
ToolRunner.printGenericCommandUsage(System.err);
return -1;
}
String input = args[0];
String output = args[1];
String schemaFile = args[2];
Job job = new Job(getConf(), "Avro sort");
job.setJarByClass(getClass());
job.getConfiguration().setBoolean(
Job.MAPREDUCE_JOB_USER_CLASSPATH_FIRST, true);
FileInputFormat.addInputPath(job, new Path(input));
FileOutputFormat.setOutputPath(job, new Path(output));
AvroJob.setDataModelClass(job, GenericData.class);
Schema schema = new Schema.Parser().parse(new File(schemaFile));
AvroJob.setInputKeySchema(job, schema);
AvroJob.setMapOutputKeySchema(job, schema);
AvroJob.setMapOutputValueSchema(job, schema);
AvroJob.setOutputKeySchema(job, schema);
job.setInputFormatClass(AvroKeyInputFormat.class);
job.setOutputFormatClass(AvroKeyOutputFormat.class);
job.setOutputKeyClass(AvroKey.class);
job.setOutputValueClass(NullWritable.class);
job.setMapperClass(SortMapper.class);
job.setReducerClass(SortReducer.class);
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new AvroSort(), args);
System.exit(exitCode);
}
}
程序使用 Java 中表示的通用类型参数 K 能够排序任何类型的 Avro records, 程序使用了 Generic Avro 映射,因而不需要任何代码生成。
排序操作发生在 MapReduce 的混洗阶段(shuffle),排序函数由传递给程序的 Avro schema 确定。
让我们使用这个程序来为之前创建的 pairs.avro 文件排序,使用 SortedStringPair.avsc schema 以 right 字段降序排序。
首先,使用 Avro tools JAR 观察输入文件的内容:
% java -jar $AVRO_HOME/avro-tools-*.jar tojson input/avro/pairs.avro
{"left":"a","right":"1"}
{"left":"c","right":"2"}
{"left":"b","right":"3"}
{"left":"b","right":"2"}
然后运行排序:
% hadoop jar avro-examples.jar AvroSort input/avro/pairs.avro output ch12-avro/src/main/resources/SortedStringPair.avsc
最后再检查输出结果:
% java -jar $AVRO_HOME/avro-tools-*.jar tojson output/part-r-00000.avro
{"left":"b","right":"3"}
{"left":"b","right":"2"}
{"left":"c","right":"2"}
{"left":"a","right":"1"}
*
*
*
9. 其他语言的 Avro (Avro in Other Languages)
--------------------------------------------------------------------------------------------------------------------------------------------------
对于 Java 语言之外的语言和框架,有多种选择可以与 Avro 数据一起工作。
AvroAsTextInputFormat 设计用来允许 Hadoop 流式程序(Hadoop Streaming programs)读取 Avro 数据文件。文件中的每个数据都转换为一个字符串,数据的 JSON
格式表示,或者如果类型为 Avro bytes 仅仅表示为原始字节(raw bytes)。另一方面,可以指定 AvroTextOutputFormat 作为一个流式作业的输出格式来创建一个使用
bytes schema 的 Avro 数据文件,每个数据是由 tab 分隔的 key-value 对,写到流式输出(written from the Streaming output)。
对 Avro 进行处理,考虑使用其他框架也是有价值的,如 Pig, Hive, Crunch, and Spark, 因为通过指定恰当的存储格式,它们都能够读写 Avro datafiles 。