Avroについて調べてみた

[Apache Avro](https://avro.apache.org/)について調べてみました。よくわからないところも多いので、指摘などがあればぜひコメントお願いします。

# 1. 概要
Avroは、スキーマ定義とバイナリでのエンコーディングを特徴とする、システム間でデータ交換を行うためのフォーマット。JSONでは型がなく文字列ベースで効率が悪くて困るような場合に使うもの。似たようなものとしては、[Protocol Buffers](https://developers.google.com/protocol-buffers/)や[Message Pack](https://msgpack.org/)などがある。

主に、ファイル形式、RPC用のプロトコル定義、Kafkaなどのメッセージ形式として使われる。以下では、RPCに関しては(よくわからないので)触れない。

日本語では、[Apache Avro vs Protocol Buffers](https://www.slideshare.net/seiyamizuno35/serialization-systems)の資料がAvroの概要とProtocol Buffersとの違いがまとまっていてわかりやすい。

C, C++, C#, Java, PHP, Python, Ruby については公式のライブラリが用意されているが、(特に新し目の機能は)言語によって対応度合いが若干違うように見える。Goは、[linkedin/goavro](https://github.com/linkedin/goavro)がよく使われていると思われる。以下の説明はJavaをベースにしている。


# 2. 主要な機能

## 2.1 スキーマ定義
スキーマはavscファイルで定義する。以下のデータ型が用意されていて、組み合わせてフィールドを定義する。若干古いが、Oracle NoSQL Databaseスタート・ガイドの[第7章 Avroスキーマ](https://docs.oracle.com/cd/E53557_01/html/GettingStartedGuide/avroschemas.html)に日本語のドキュメントがあるので参考になる。

|種類|データ型|
|---|---|
|Primitive Types|null, boolean, int, long, float, double, bytes, string|
|Complex Types|record, enum, array, maps, union, fixed|
|Logical Types|Decimal, Date, Time, Timestamp, Duration|

Logical Typeは、物理形式としてはPrimitive Typeを使ってシリアライズされる派生型。

スキーマは、JSON形式で記述する以外に、[IDL](https://avro.apache.org/docs/current/idl.html)から作成する方法もある。IDLを使うと、importなどの機能を使うことができる。

### 2.1.1 スキーマの例
(http://avro.apache.org/docs/current/spec.html より)

```
{"namespace": "example.avro",
 "type": "record",
 "name": "User",
 "fields": [
     {"name": "name", "type": "string"},
     {"name": "favorite_number",  "type": ["int", "null"]},
     {"name": "favorite_color", "type": ["string", "null"]}
 ]
```

### 2.1.2 IDLによるスキーマ定義の例
(https://mingqin.wordpress.com/2015/01/30/avro-idl-composites-avro-schemas-by-avrohubtool/ より)

```
@namespace("avro.examples.baseball")
protocol Baseball {
   import schema "name.avsc";
   import schema "coache.avsc";
   import schema "position.avsc";
   import schema "player.avsc";
   
   record Team {
       Player player;
       Coache  coache;
  }
}
```

### 2.1.3 Schema evolution
Avroでは、[互換性のルール](http://avro.apache.org/docs/current/spec.html#Schema+Resolution)があり、スキーマの変更時に互換性を維持できるようになっている。ただ、公式の仕様はわかりにくいので、[viniciusccarvalho/schema-evolution-samples](https://github.com/viniciusccarvalho/schema-evolution-samples
)の説明・例や、[Confluent Platformの各コンポーネントの特徴](https://qiita.com/kimutansk/items/cce50399499cefc22833) のほうがわかりやすい。


|互換性の種類|説明|
|---|---|---|
|前方互換|新しいスキーマで作られたデータを古いスキーマで読む|
|後方互換|古いスキーマで作成されたデータを新しいスキーマで読む|

前方互換性を維持するためには、スキーマへのカラム追加時にデフォルト値を指定する必要がある。


## 2.2 データ形式
Avroで用意されているデータ形式は3種類あり、用途に合わせて選択する。データの読み書きにはスキーマが必要になるため、ヘッダによるオーバーヘッドとスキーマの運用を考慮して決める必要がある。

|種類|内容|説明|スキーマの運用|
|:---|:---|---|---|
|Binary Encoding|バイナリのデータのみ、ヘッダなし|BinaryEncoderを使う。単純なエンコーディングとしてのみ使用する場合|ディレクトリやトピックとスキーマを規約で対応付けておく。スキーマ更新時は全体で同期をとって対応が必要|
|Object Container Files|ヘッダにスキーマ定義を含む|DataFileWriterを使う。ファイルでのデータ交換用|埋め込まれているスキーマをそのまま利用可能|
|Single-Object Encoding|ヘッダにスキーマのフィンガープリントを含む|BinaryMessageEncoderを使う。Kafkaなどのメッセージング用|フィンガープリントを使用してスキーマを解決する|

Object Container Files形式は、deflate/snappy圧縮も可能。また、BigQuery、RedShift、Sparkなどでサポートされている。[Amazon EMR の Avro フォーマットのデータを Amazon Redshift にロードする](https://dev.classmethod.jp/cloud/aws/amazon-redshift-emr-avro/)も参照。


メッセージ用途には、Object Container Files形式はオーバーヘッドが大きいので、軽量な形式としてSingle-Object Encodingが用意されているが、自前でスキーマのバージョンなどを埋め込んで解決してもよい。

動的にスキーマを解決する仕組みとしては、大掛かりになるが[Confluent Schema Registry](https://www.confluent.io/confluent-schema-registry/)もある。[Confluent Platformの各コンポーネントの特徴](https://qiita.com/kimutansk/items/cce50399499cefc22833)も参照。


## 2.3 シリアライズに使用される主要なクラス
基本的には、DatumWriterのいずれかのサブクラスと、EncoderもしくはFileWriterを使用してシリアライズする。シリアライズのサンプルは[Getting Started (Java)](https://avro.apache.org/docs/current/gettingstartedjava.html)を参照。デシリアライズでは、対応するReader/Decoderを使用する。

こちらも若干古いが、Oracle NoSQL Databaseスタート・ガイドの[第8章 Avroバインディング](https://docs.oracle.com/cd/E53557_01/html/GettingStartedGuide/avrobindings.html)に日本語のドキュメントがある。

|package|class/Interface|説明| 
|:--|:--|:--| 
|org.apache.avro.io|DatumWriter(IF)|スキーマに従ってインスタンスをEncoderに書き出す|
|org.apache.avro.io|SpecificDatumWriter|スキーマから自動生成されたクラスを使用してシリアライズ|
|org.apache.avro.io|GenericDatumWriter|クラスの自動生成を使わずにシリアライズする|
|org.apache.avro.io|ReflectDatumWriter|既存のクラスをシリアライズする|
|org.apache.avro.io|Encoder(IF)|フィールドの値をシリアライズする|
|org.apache.avro.io|BinaryEncoder|フィールドの値をバイナリエンコーディングする|
|org.apache.avro.file|DataFileWriter|DatumWriterをObject Container Files形式のファイルに出力する(encoder不要)|
|org.apache.avro.message|MessageEncoder(IF)|ByteBuffer, OutputStreamに出力する|
|org.apache.avro.message|BinaryMessageEncoder|Single Object EncodingでByteBuffer, OutputStreamに出力する|

PythonやRubyではクラス生成はサポートされていない(というか必要がない)ので、GenericDatumWriter相当のみとなる。[Getting Started (Python)](https://avro.apache.org/docs/current/gettingstartedpython.html)や、[Avro in Ruby](http://teamcoding.com/blog/2015/12/01/avro-in-ruby/)も参照。

Schema Evolutionを考えると、SpecificDatumWriterを使用する場合、自動生成されたクラスをどうバージョン管理するかが少し悩ましい気がする。


# Appendix
- [commercehub-oss/gradle-avro-plugin](https://github.com/commercehub-oss/gradle-avro-plugin) Gradleのplugin
- [[AVRO-1891]](https://issues.apache.org/jira/browse/AVRO-1891) JavaでLogical Typeを含むUnionができないというバグ。Nullを許可する場合にはunionを使うので、現状ではLogical TypeでNullを許可することができない。1.8.3で対応予定。
- [GADGT: The futue of gogen-avro](http://www.agardner.me/golang/avro/vm/gadgt/go/deserializer/2019/02/15/gdgt-future-of-gogen-avro.html)
- [How to encode/decode Kafka messages using Avro binary encoder? - Stack Overflow](https://stackoverflow.com/questions/8298308/how-to-encode-decode-kafka-messages-using-avro-binary-encoder)