MySQL
Elasticsearch
replication
リアルタイムインデクシング

MySQL(Replication Protocol)とElasticsearchのほぼリアルタイム連携の実現(リアルタイム・インデクシング)

内容

・リアルタイム検索、リアルタイム集計ができるようになる
・MySQLとElasticsearchをリアルタイムに近い時間で同期する方法
・実際のコードはとても複雑なので考え方をダラダラと説明する
・ここでの説明が100%正しいとは思わないで

全体の流れ

flow.PNG

変更の検知

 MySqlが変更したことを検知するために「Replication Protocol」を使って疑似的なslave db(以下「dummy slave」とする)を構築する。
 dummy slaveは以下の二つに分かれる
・masterからBinLogを受け取って検知した情報を保存する。(1プロセス)→Replication Protocol受側
・情報からドキュメントを作成しElasticsearchにPUTする(複数プロセス)→Elasticsearch PUT

MySQL設定

・専用スレーブを用意する。データを限界のスピードでElasticsearchに叩き込むのでサービスに影響がでないようにするため。
・専用スレーブのbinlog_format = 'ROW';にする。STATEMENTやMIXEDでは変更を検知できてもSQLを解析する必要があるし、現実的ではない。

Replication Protocol受側で行う仕事

・Replication Protocolを受けるプログラムを書くのはとても大変なので「Replication Protocol PHP」「Replication Protocol C」などで検索してライブラリを見つける。
・binlogが流れてくる量はとても多いので、db,table,eventを絞ること。
・イベントをファイルやメッセージqueなどに出力する。
・自動で再接続する必要がある。binlogの接続はsocketを開きっぱなしでデータが流れ込む。このため接続がよく切れてしまう。タイミングとしてはDB負荷が高い時に発生する。
・自動再接続の時に取得済みポジションより前のポジションで接続し直すこと
・・MySQLが落ちた場合はその時発生したポジションがなくなってしまう。(クラッシュリカバリが働くのか?)
・この処理のプロセス・スレッドでElasticsearchにPUTしないこと。binlogの処理が追い付かなくなってリアルタイム性がなくなる。

リソースを絞り込む

「Replication Protocol受側で行う仕事」、「ElasticsearchへPUT」のどちらで行ってもよい
ドキュメントを構成するテーブルが更新されたら、その根元の主キーを特定、取得する。
以下の概念データモデル画像「table_F」が更新された場合その根元(強実体)「table_A」の主キーを取得する。
更新テーブルだけを取得してElasticsearchのドキュメントを更新する場合、更新コストがとても高く複雑になるのであきらめる。

teble.PNG
(概念データモデルの表記はIPA)

マスタ tableのようなデータを更新した場合は多くのデータが更新されてしまうので、根元のid数が多くなってしまう。
これはしょうがないのであきらめる。システムにもよるが更新頻度が少ないのでそんなに気ならない。
もしくは、マスタ tableの先の値を保管しないようにして、DBアクセスを利用できないか検討する。

ElasticsearchへPUT

・上記で「根元の主キー」が特定されているので、その根元に関係するドキュメントを構築して、Elasticsearchの「_id」を生成する。「_id」は確実に重複しない&そのidは何度でも生成可能であること(冪等性を使ってドキュメントの更新をするため)。例えば、会員番号01234がありこの番号は「会員」ドキュメントでは重複されないことが保証されている場合、idは「member_01234」とする。こうすることで01234に関係するテーブルが更新されても同じIDで叩き込めるため更新コストが低くなる。Elasticsearchのauto idは使えない。(もしauto idを使った場合は検索→id特定→更新になるのでコストが高い)
・「根元の主キー」は、まとめて受け取ること(たとえば1秒間に発生したイベント)
・受け取ったIDをまとめてSELECTすること
・まとめて処理受け取らないと処理が追い付かなくなってリアルタイム性がなくなる。
・同じイベントの固まりで「根元の主キー」が同じものは一つにする
・取得できたらElasticsearchへPUT
・取得できなかったらElasticsearchへDELETE

 とにかく冪等性!

平行性の問題

ElasticsearchへPUT/DELETEするとき、速度の問題から平行で処理を行うので順番が狂ってしまう。
たとえば、「member_01234」がほとんど同じ時間に「作成→更新→削除→作成」が行われたときにイベントの種類を気にしてElasticsearchにDeleteを投ると最終的には「member_01234」が存在するはずなのにElasticsearchから消えてしまう。
解決方法として、
・イベントの種類は問わないで根元のIDを取得することに注目する
・取得できたらPUTをElasticsearchに投げる
・取得できなかったらDELETEをElasticsearchに投げる
これだけで入れ違いによるデータの不整合発生確率が低くなる

平行性のそもそも・・・

「削除→作成」の処理がトランザクション中にまとまっていることがほとんどで、その場合は「MySQL上”作成”の状態でdummy slaveへイベントが発生する」ので問題が無い(binlogの仕組み上そうなる)。別トランザクションで削除、作成、更新が行われる場合は問題なので「なんだか変なシステム」である可能性がある。CURDでの確認ができてないかもね。

DBの設計が幼いと・・

 強実体、弱実体で関係する項目の持ち方を間違えるとリソースを取得するSQLが重くなってしまう。1:Nなら自然にそうなるが、1:1の場合は意識して設計しないと後で大変になる。

最初の取り込みは?

 根元のIDをまとめて取り出して、上記の処理に食わせてあげればいい。食ってる最中も変更がどんどん流れてくるので、冪等性でちゃんと最新になる。

ALTER TABLEは?

 属性追加やインデックスの変更程度のALTER TABLEが流れても特に問題なかった。削除はドキュメントの生成にかかわらなければ問題ないはず。

はまったポイント

・協調動作(マルチプロセス、マルチスレッド)の方式はよく検討した方がいい。
・MySQL、Elasticsearchが異常な状態によくなるので、自動で復旧するようにすること
・Elasticsearchにどのくらい負荷がかかっているか正しく検知して、PUTのタイミングを調整すること
・24時間365日動作するのでメモリーリークに気を付けること
・MySQLが落ちた時に認識しているポジションが無くなること。
・メモリを大量に消費するので、根本ID数を適当に絞るようにしないと落ちる