記録帳

クラウド、データ分析、ウイスキーなど。

入門 Avro 第2版

オライリーっぽいタイトルつけてみました。こんにちは。
最近はダラダラと過ごしてしまっていますが、今回はApache Avroをちょっと触ってみたのでその記事を書こうと思います。

Avroとは?

効率的にデータが保存できる、バイナリのフォーマットです。
いわゆる行指向フォーマットというやつで、OLTP処理の場合に向いているフォーマットとのこと。
そのあたりの全体像はこのブログが詳しかったので、ぜひ。
カラムナフォーマットのきほん 〜データウェアハウスを支える技術〜 - Retty Tech Blog

また、こちらの本

の中には、

Avroにおいて鍵となる考え方は、ライターのスキーマとリーダーのスキーマは同一である必要がなく、互換性さえあればよいという考え方です。

とあります。
つまり、スキーマ定義(このデータは"Name”(String)、"Age"(int)という列を持っているぞ!のようなもの)がライターとリーダーで別々で持てるということです。
(互換性は持っておく必要あり)
別々で持てると、例えば「次のアプリからは今まであった"Age"ってカラムいらなくなったんだよなぁ~」というときは、リーダーのスキーマ定義だけ変更するとかの対応ができますよね。
スキーマの進化に対して、修正のハードルが低いんですよ、という内容が載っていました。

と、ここまで読んだら、実際にコード書いて確かめたくなりますよね。
というわけで、実際にpythonで書いてみました。

まずは公式チュートリアル

Apache Avro™ 1.11.0 Getting Started (Python)
とりあえずここに書いてある内容を読んで動かせば、avroファイルの書き込み、読み取りのイメージはつかめます。
ただ、

  • コードと説明が背景同じで書かれているのでどの部分がコードなのか分かりにくい
  • python3の実装と書いてあるのに、print文の書き方が2系(print("aaa")ではなく、print "aaa"になってる)

という罠があるので気を付けてください。

一応、ここからはチュートリアルの内容を見ていきます。
以下、チュートリアルのコードを私が見やすいように整形したものです。

pythonコード

import avro.schema
from avro.datafile import DataFileReader, DataFileWriter
from avro.io import DatumReader, DatumWriter

def avro_write(write_file, schema):
    writer = DataFileWriter(open(write_file, "wb"), DatumWriter(), schema)

    writer.append({"name": "Alyssa",
                   "favorite_number": 256})
    writer.append({"name": "Ben",
                   "favorite_number": 7,
                   "favorite_color": "red"})
    writer.close()

def avro_read(read_file):
    reader = DataFileReader(open(read_file, "rb"), DatumReader())
    for record in reader:
        print(record)
    reader.close()

write_file = "users.avro"
read_file = "users.avro"

schema = avro.schema.parse(open("user.avsc", "rb").read())

avro_write(write_file, schema)
avro_read(read_file)


スキーマ定義のjson(user.avsc)

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


単純に、書き込みと読み込みを行っています。
このpythonファイルを実行すると、実行結果はこの2行です。
{'name': 'Alyssa', 'favorite_number': 256, 'favorite_color': None}
{'name': 'Ben', 'favorite_number': 7, 'favorite_color': 'red'}

Alyssaの"favorite_color"は、python内の書き込みで指定していないため、結果出力ではNoneになっていますね。
これは、スキーマ定義で"favorite_color"のtypeに"null"が含まれているためです。
試しにスキーマ定義の"null"を削除してみると、エラーが返ってきました。

さて、基本的な読み込み書き込みはこれで私も理解したのですが、ここで1つ疑問が。

書籍内では、リーダーとライター別々のスキーマ持てるって聞いたけど、このチュートリアルだとライターしかスキーマ定義使ってなくね?

ということです。
確かに、読み込みの時はスキーマを使わなくても読み込めています。
どうやら、読み込みの時何も指定しなくても、ライターの定義を内部で保持していて、それを勝手に使ってくれるようです。
ただ、どうせなら別々のスキーマ定義使いたい!ということでその実装方法も調べてみました。

ライターとリーダーで別々のスキーマ定義を使ってみる

まずは、使うスキーマ定義を。
v1として前使っていたものと同じスキーマ、v2としてその中の"favorite_color"を落としたものを使います。
そして、ライターではv1、リーダーではv2を使ってみます。
つまり、ライターの時はfavorite_colorが定義されているからデータとしても書き込まれているけど、
リーダーの時はfavorite_colorを無視したい!というユースケースですね。

スキーマ定義_v1(user_v1.avsc)

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


スキーマ定義_v2(user_v2.avsc)

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


では、使うpythonコードです。
変更点は、リーダーにもスキーマを使うようにしたところです。
そのやり方がどこにも書いてなかったので結構てこずりましたが、頼るべきものは公式。
avro/io.py at release-1.7.7 · apache/avro · GitHub
上記の実際のavroのpython実装を見ると、DatumReader()のinitにこんなものが書いてあるじゃないですか。

f:id:supa25:20220306162824p:plain
実装

確かに、さっきまでの公式チュートリアルだとDatumReader() と引数なしで使っていた。
なので、リーダーのスキーマだけ指定するように

DatumReader(None, reader_schema)

という感じに使ったら、見事動きました。
以下、完成品です。

import avro.schema
from avro.datafile import DataFileReader, DataFileWriter
from avro.io import DatumReader, DatumWriter

def avro_write(write_file, schema):
    writer = DataFileWriter(open(write_file, "wb"), DatumWriter(), schema)

    writer.append({"name": "Alyssa",
                   "favorite_number": 256})
    writer.append({"name": "Ben",
                   "favorite_number": 7,
                   "favorite_color": "red"})
    writer.close()

def avro_read(read_file, schema):
    # ここでリーダーのスキーマ定義を使う
    reader = DataFileReader(open(read_file, "rb"), DatumReader(None,schema))
    for record in reader:
        print(record)
    reader.close()

write_file = "users.avro"
read_file = "users.avro"

schema_v1 = avro.schema.parse(open("user_v1.avsc", "rb").read())
schema_v2 = avro.schema.parse(open("user_v2.avsc", "rb").read())

avro_write(write_file, schema_v1)
avro_read(read_file, schema_v2)

実行結果はこちら。
{'name': 'Alyssa', 'favorite_number': 256}
{'name': 'Ben', 'favorite_number': 7}

見事、リーダーのスキーマ定義だけ落とした"favorite_color"が出力されていませんね。
これで、明日から急にAvroで別々のスキーマ定義したいといわれても対応できます。

まとめ

  • Avroは、ライターとリーダーで別々のスキーマ定義を使える。
  • 具体的には、読み込み、書き込みの時に使うメソッドにスキーマ定義を渡してやればいい。
    • ライター:DataFileWriter() の第3引数にスキーマを渡す。
    • リーダー:DatumReader() の第2引数にスキーマを渡す。

ちなみに、別々のスキーマ定義でも互換性がないと使えません。
その互換性については、公式のここに色々載っていたので、今度試してみたいですね。
Apache Avro™ 1.7.7 Specification

それでは!