データのIOが糞過ぎるが、誰も助けてくれない

だーれも助けてくれないので、自力でピンチを脱出するしかない。ひとまず、自分用のメモを作る。自分用のメモなので、他人が理解できなくても気にしない。実験的に、いろいろ考えたことをだらだらと書いていく事にする。まとめたりしない。いや、あとで別エントリでまとめるかもしれないけど、ひとまずここはダラっと行く。

問題:データのIOが糞過ぎて話にならない状態。はやくなんとかしないと!

データオブジェクトをバイナリフォーマッタを使ってSerialize/Deserializeするだけの、簡単なお仕事です。本当に簡単ならピンチじゃないんだけど、私がバカでスキル不足なのでピンチになっている。困るなあもう。

簡単なんだけど、問題は通信帯域。今まで100MbpsのLAN環境で動かしていたものをNTTの光とかADSLを経由して広域でつなぐ。それでも動くようにしないといかん。

ひとまず、ネットワーク越しのIO処理は基本非同期でやって、必要な時は同期するIOクラスをなんとかして作る。とにかく同期の必要がある!という日までは勝手に非同期処理する。

Serializeは細切れで順次来るのを、順次やっていけばいい。ただし、「今、この瞬間、ここまで書いとけ!」と同期したい時がやってくるので、データの書き込み履歴を、直近の同期以降分をもっていて、「よし、あとこんだけ書き込み同期したら終わる!」みたいな同期関数を作る。あれだ、データの一連番号もってて、大小比較で判断するのがいいな。書き込み未完了分をQで管理か。

データの構造は後でDeserializerが細切れで読み取れるように双方向リスト的な感じで。ケツからも読めるし、要素1個分が壊れていてもほかの要素に影響がないようにする。

Header(Serialized data length) Serialized data Trailer(Serialized data length)

こんなので要素1個分。これがどんどん順番に並んでいく感じ。データ長をヘッダとトレーラに持てば、ヘッダ・トレーラ長が固定という前提で、次の要素の場所を計算できるので、まあ、これでいいということにする。

Deserializerは、すでにSerializeされたデータをいきなりケツを読んだ後に先頭から順次読み出し。ある部分からデータの書き換えが発生するので、それに対応できる必要あり。ケツを読みだすのはまず真っ先にやっておいて、その後はとにかくできるだけ早く全部を読み切る。ここがネットワーク越しになって一番のボトルネック。読み出せた分から順次ヘッダとトレーラの整合性を確認しつつデータをDeserialize。Deserializeされたデータオブジェクトはリストにするのがいい。ということは最後の要素が最初に読み込まれるから、それ以降は最後のデータの前にInsertみたいな感じになるか。うーん。最後の要素は先頭からの相対位置が変わっちゃうので、「最後のヤツをくれ」的に抽象的にしておかないといかん。
あれだなー、物理的に回線越しにデータを読み出すのと、解析と切り出しをやるのは非同期にしないと。もちろん、API叩いた方とも非同期でないといかん。
Deserializeされた(あるいはされる予定の)データオブジェクトListへのアクセサメソッドで既にデータの読み出しとDeserializeが終わっているかどうかをチェックして、もしDeserializeが終わっていなかったら、そこで同期。終わっていればラッキーだし、終わっていなければアンラッキー。
ん、まてまて。データオブジェクトがListになっていて、ひとまず読み終わった分のデータしか入っていなくて、全体として読み出していないデータがどれぐらいあるのかわからないから、同期のしようがないじゃないか。バカだ。
なので、全体としてデータがどれぐらいあるのかあらかじめわかっておく必要がある。あれだ、ヘッダ・トレーラを中身のデータ長だけじゃなくて、何番目のデータか一連番号をもっておけばいいな。そうすればケツを読み取った時点でList、というかArrayだな。Arrayの要素数が判明する。ケツを読み終わった時点でサイズだけぼーんと拡張しておけば、そのArrayの要素をIsNothigで調べれば読み出しが終わっているのかどうか調べられる。ふむ。
ヘッダ・トレーラの大きさが問題だなあ。データ長はuint16では心もとない。となるとuint32を使うしかない。それと、一連番号は・・・ んー、そっちもuint16ではちょっと心もとない。uint32を使うしかない。ということは1個分のデータ要素にuint32がヘッダ・トレーラ合わせて4個分=16オクテット必要ということに。うーん、まあ、堅牢性と応答性の為の必要経費としてはまあ、しょうがないか。
次のデータの読み取りオフセットの計算方法は+=header.SerializedDataLength + 16で終わりか。うん、たぶんOK。
んー、でもまてよ。Serializeの途中でIOが止まっちゃったら、ケツからの読み出しが意味のある値を返さない。そうなったら時間はかかるけど先頭から全部読みなおし? マジで? 発生確率は低いと思うけど支払うコストが大きすぎないか? うーん、となるとall bit onか市松模様みたいな同期パターンを書いておいて、それを調べながら読むとか? うーん、同期を毎回書くべきか、それもと10件に1件ぐらいそっと挿入しておくぐらいがいいか。まあ、IOが中途半端になるリスクとの取引だったら、100件に1件ぐらいでもいいか。あああああ、だめだだめだ。諸般の事情でそれはだめだったww 同期パターンは毎回必要! えーと、長さは、うーん、32bitにしとくか。ということは

Header(Serialized data length,Serial number) Serialized data Trailer(Serialized data length,Serial number) Sync Pattern

こんなか。でもってデータ1件あたりのコスト増は20オクテット。1万件だと20万オクテット=200KB? 通信帯域がもし5MBit/secだったとしたら600KB/s程度なんで、全部合わせても0.3秒ぐらいで終わっちゃう話? よしそれなら全然OK。
ということは、Deserializerの最初の仕事はケツから一定の量、2σぐらいの確率で同期パターンが入っているであろう量をまず一発読み込んで、それでだめならもう一回がんばる。それがだめなら… を5回がんばるか。それでだめなら先頭からやりなおしだ。σがいるから、データ長は実際にしらべないといかんな。でもって、リトライアウトして、これはデータがハードクラッシュしてますねという時はエンドユーザに「ちょっと待っててね!」を出さないといかん。データのDeserializerにそんなUI持たせるの? うーん、無理だなw 最初にDeserializerにデータがハードクラッシュしているかどうかを問い合わせる関数がいる。でもって、それを調べた人がエンドユーザ向けに状況を説明。うん。
ああ、そうだ、先頭から読んだとしてもデータがクラッシュしている場合はある。その時はゴメンナサイをしないといけない。もういっそケツを調べて同期パターン見つけられないなら、そこであきらめてもいいかもしれないな。

うん、よし。大体の方向性は見えた。あー、でも、他のデータとの一貫性が、どのデータまで保たれていたのか調べたいところではある。堅くするならそれがいる。そしてこれは堅くないと困るんだ。うーん、またデータのIOが増えるよ! Serializeは問題にならないけど、Deserializeの時間がまた増えることになる。うーん、うーん。

まあいい、どうせコストは支払わないといけないんだ。あきらめ。

いや、違うな。他のデータとの一貫性チェックは少なくともDeserializerの仕事ではない。それはまた別なところで実装すべき機能だ。ひとまず、おいておく。

よし、ひとまず、こんなもんだろ。これでテストPを書いてみるか。