done is better than perfect

自分が学んだことや、作成したプログラムの記事を書きます。すべての記載は他に定める場合を除き個人的なものです。

AvroのUnionに大量のtypeを入れないほうがいいという話

tl;dr;

(正直理解が怪しいところもあるので、詳しい方誰か教えて下さい。) Unionに大量の型を入れないほうが良い。どうしても入れたければ AVRO-2274 が入った Avro version 1.9以降を使いましょう。


自分用の備忘録なので細かい説明は省きます。

Apache Avro というシリアライゼーションフォーマットがあります。Apache Kafkaのメッセージのフォーマットだとかに使われることがあります。もともとはApache Hadoopに格納する際のデータフォーマットとして作られたっぽい?

Apache Avroには、いろいろな型があります。 int とか float みたいな primitive な型だったり、もう少し複雑な型もあります。

複雑な型の中に、 Union と呼ばれる型がある。これは C言語でいう union と似たようなもので、ある値に複数の型を定義できます。

avro.apache.org

Union型は、 nullable な値を示す際によく使われます。 例えば、 ["null", "string"] みたいな書き方をして、 null もしくは string が入った値であることを明示できます。

基本的には上記のような使われ方しかしないため、 union に入る型は多くて2つとかです。このようなときには大きな問題にはなりません。


Avroは、ユーザが型をrecord型で定義できます。このとき、ユーザが自前で定義した大量の型のどれか一つであることを示すためにunionを使った場合のことを考えます。

["myType1", "myType2", ... "myTypeN"]

なんでそんなことするの、ということは聞かないでください。

WriterでもReaderでも同じSchemaを使っていると想定した場合、上記のUnionが出てきたときに、Avro 1.8までは以下のようなresolutionをします。

for wbranch in writerunion:
    for rbranch in readerunion:
        if (wbranchとrbranchがマッチするか):
            resultを作る(branchを作る)
        memorize[(wbranch, rbranch)]  = branch

writerのunionの要素1つずつに対して、readerのunionの要素すべてをチェックし、マッチする要素を探し、結果を返す、ということをします。 更に、その結果をキャッシュ的にメモリに残します。

素数が多くなると、爆発的に計算量が多くなることはわかると思います。加えて、そもそもwriter unionの要素とreader unionの要素からbranchを作る処理が重いのですが、それにもまして 要素ごとの計算結果をキャッシュする(Weakなdictですらなく、普通のhashmapにもつ)ので、めっちゃメモリを使います。

なぜこんなことをするのかというと、Avroのspec的に、readerはwriterの書いた型と最初にマッチする型でresolutionすることが決まっているからです。

avro.apache.org

まあでも、見れば分かる通り、ReaderとWriterが同じunionのsubschemaを持っていることさえわかれば、そんなことしなくても直接対応する型でbranch作ればいいよね、っていうのが AVRO-2274 です。

実際、これでも同じ型かどうかのチェックは O2 かかるのですが、それ自体はそこまで遅くなく、キャッシュ量も大したことなくなります。よって、上記のように、大量にunionに型が入っているような場合は爆速になります。


このあたり読めばもう少し理解できるかな、と思うのですが、途中で挫折した。。 parsing tableとか懐かしすぎワロタ

avro.apache.org