PHPからGoへの大規模マイグレーションの話
こんにちは。Pairsのサーバーサイドエンジニアの鉄本です。
約1年半Pairsの開発に携わっており、修正に修正を重ねてきたため、仕様やデータベース構成にかなり詳しくなりました。
そんな中、Pairsの技術的負債の返済やインフラコストカットなどのために、PHPで書かれたアプリケーションをGoで書き直すことになりました。
今回は、このフルスクラッチに伴うデータマイグレーションの話をしたいと思います。
フルスクラッチに関しては、後日誰かが記事にしてくれるはずですのでお楽しみに。
使用した言語
PHP!
スピードと確実性を重視したため、流れに乗ってGopherになる夢はここで絶たれる…
と思いきや、とてもハイブリッドなプロジェクトとなりました。
実際には、以下のようなラインナップでした。
- PHP:マイグレーション本体
- Ruby:マイグレーション管理ツール
- Perl:設定ファイル生成ツール
- Python:DynamoDB (AWS周り) ツール
- Go:マイグレーション本体とアプリケーション本体のブリッヂ
ロジックやDB接続などの処理はPHPで実装し、Rubyから引数を渡して PHPを並列で呼び出す。
といった形でツール毎に最適な方法で準備していたら、いつのまにか多国籍軍になっていました。
マイグレーションの苦しみ
マイグレーションは、社内ではよく「私は、俺はやりたくない」と言われるような仕事でした。
そんな不人気な仕事をどう苦しみながら攻略したのか、いくつか紹介したいと思います。
Case1:突然のスキーマ変更
プロジェクトの中で三度の波がありました。
第一波:開発初期
作り始めてみて、「これ足りなくない?」というものが大量に発生する時期です。
このときに開発者の要望にホイホイ応えていると、マーケティング面での数字が出しにくい構成になってしまったり、スキーマを作成当初に意図していたことが消え去ってしまったり…といったことが起こりえます。
レポートラインを固めて、漏れなく、しかし冷静に対応できる環境を準備することが大切です。
ここでは口頭でのやり取りが本当に危ないです。
レポートラインは、Google SpreadSheetとSlackを連携させて整備しました。
シートに依頼内容が追加されたり、回答したときにはslackで通知が飛びます。
※社内のリマインドでは、「ひつじのモフボさん」をアイコンにしたbotが大活躍しています。
記録を残しつつも、スムーズにやり取りが進むのでオススメの方法です。
第二波:検証開始直後
検証チームが本格的にはいり、不具合が大量に報告されます。
その修正を進めるうちに、「このカラムが必要だ!!」というものがどんどん挙がってきます。
ここでは、いかに速やかに、既存の実装への影響を考慮して対応できるかが鍵になります。
スキーマ変更待ちで開発が滞るのは最悪のケースなので、自分がボトルネックになることの意識が非常に大切になります。
第三波:リリース直前
完成度を上げる段階で、致命的ではないがゆえに目をそむけていた「いらないカラム」や「命名が気になるカラム」がぽろぽろと出てきていました。
そして、追い打ちをかけるように、致命的なバグが見つかります。プロジェクトあるあるです。
これを機にと、今一度冷静になって、この際だから直したいものはと一気に直しました。
リリースしてから変更する場合の苦悩と天秤にかけたら、「対応すべき」という判断が瞬時にできました。
Case2:Dynamoの罠
eurekaはAWSフル活用で、Dynamoもよく登場します。
そこにあった罠についてお話しします。
マイグレーションは、データの正確さの次に移行にかかる時間も重視しています。
Pairsには、億単位のテーブルがあり、普通に移行すると1週間くらいかかります。
そこで、初めはEC2 c4.8xlarge(一番高いサーバです..)を何台も立ち上げて、大量に並列処理し、がっつりスループットを上げて移行する作戦をとりました。
第一の罠
初めは順調だったものの、どうも一定のデータ量を越えたあたりから、スループットのエラーが大量発生するように…
Slackへ流すようにしているので、大量の「スループットが上限に達している」との通知が…
※サーバー毎に番号を振り、エラーが発生したサーバーをアイコンでわかるようにしています。
明らかにスループットの上限超えてないのになんでだろう…としばらく踊らされました。
Dynamoの内部処理が原因だった
DynamoDBでは、スループットまたはデータ量が一定を超えると、自動的にパーティションが作成され、スループットも分散されます。
詳しくは公式に説明があります。
大体ですが、テーブルは10GB
, スループットはRead:600 - Write:1000
の組み合わせを超えるとパーティションが生成されてしまいます。
パーティション対策
では、パーティションが区切られてしまったらどうすればいいのでしょうか。
極論をいうと、スループットをさらにあげれば解決できます。
区切られたパーティションあたりに割り当てられるスループットが増えるので、お金で解決です。
※Dynamoはスループット課金です。
第二の罠
「そうか、スループットをさらに上げればいいのか!」で解決しないところがDynamoのまたかわいいところです。
1度区切られたパーティションは元に戻りません。
データ移行の時にだけ高いスループットが必要で、本番稼動ではそれほど必要としない場合、スループットを上げ続けなくてはならず、お金の垂れ流しです。
そこで、Goマイグレーションでは、「だらだら実行作戦」に移りました。
作戦名の通り、数日間かけてデータを移します。この件に関してはDynamoに完敗です。
安全側に倒したということで、お疲れ様でした。
スループットをあげる以外の方法はないのか
このままでは悔しいので、スキーマを見直す作戦に出ました。
先ほどパーティションが分割される話をしましたが、分割されても各パーティションに分散して処理が走れば効率よくスループットを使うことができます。
Dynamoはハッシュキーを指定してデータを処理しますが、そのハッシュキーの設定の仕方が鍵になってきます。
Pairs-goのスキーマは「日付」をハッシュキーにしていたので、一部のパーティションに処理が集中していました。
回避の方法は、公式にもあるように「2015–12–07.1」,「2015–12–07.123」といった乱数をうしろにつけてしまうことです。
もしくは、そのハッシュキーの構成をやめてしまうことです。
今回、「日付」はマーケティングデータの集計目的でSecondary-Indexに指定されており、他の方法での集計が可能だったので外すことにし、スループットの大量エラー発生を回避することができました。
余談になりますが、Dynamoデータのバックアップやリストアではdynamodumpを重宝しました。
導入後コマンド一つで実施できるのでオススメです。
こちらも一悶着ありましたが、また機会があれば書きたいと思います。
Case3: アプリケーション側との認識違い
一番危険で、浮き彫りになりにくい問題です。
例えば、質問への回答を保存するテーブルがあり、user_id, partner_idというカラムを持っているとします。
この場合、組み合わせはどちらになるでしょうか?(ここでは「カラムの命名が悪いよ!」という話は置きます)
- 質問者がuser_id / 回答者がpartner_id
- 回答者がuser_id / 質問者がuser_id
認識さえ合っていればどちらでも大丈夫なのですが、この認識がデータ移行側とアプリケーション側でずれていると、移行した質問の回答が本番で表示されない、他人の回答が自分の回答として表示されるという不具合が発生してしまいます。
認識ずれへの対策
防ぐ、そして検知することです。
まずは、共通認識が得られる場所を明確に。
スキーマ管理にはMySQLWorkBenchを使っており、テーブルスキーマをHTMLとして出力できる機能があります。
github.ioをつかって常に最新のスキーマを反映し、参照してもらうようにしました。
つぎに、移行済みのデータでテストをする。
個人情報はマスキングした状態で、移行済みデータを使った検証環境を準備し、稼働中の本番と検証環境に差異がないか、検証をしました。
時間制限のあるデータの検証はスケジューリングが難しかったですが、この検証をやらずしてリリースは不可能でした。
さいごに
長くなりましたが、データ移行のプロジェクトは約半年ほど続き、まだまだ語り尽くせないくらい様々なドラマがありました。
マイグレーションは技術的な面では大きな壁はなく、必要なのはこの3つだと思っています。
- 仕様への深い理解
- コミュニケーション(認識違い、抜け漏れをなくすため)
- データの整合性の担保
情報はサービスの要。それを扱う重大な仕事をできたことは貴重な経験でした。
ただし、MySQLでマイグレーションするならトリガ使えばよかったじゃないか、と強く反省しています。