Rails Developers Meetup 2018: Day 1 発表資料 https://techplay.jp/event/639872
365 日 24 時間稼働必須サービスの完全無停止 DB 移行〜 MongoDB to Amazon Aurora 〜
View Slide
Hi! I’m kyuden●Github: kyuden●Twitter: @kyuden_●Sorcery gem commiter●https://github.com/Sorcery/sorcery●Banken gem creator●https://github.com/kyuden/banken●WEB+DB Press Ruby 連載 (vol96~101)
どんなサービス ?
collection の規模感RubyKaigi 2017
Ruby biz Grand prix 2017
Our Team
はじまりはじまり
昨年 7 月に38 億レコード ( ドキュメント ) を不整合データなしダウンタイムゼロでMongoDB から Amazon Aurora にデータ移行した
このトークで主に話すこと● 具体的なデータ移行方法● 移行のために作ったツールの設計 / 内部実装
移行対象のコレクション●node_values● 翻訳データが格納されたコレクション● 約 12 億ドキュメント●page_node_values● どのページにどの翻訳データがあるかが格納されたコレクション● 大まかに言うと page と node_values のジャンクションテーブル ( ジャンクションコレクション )● 約 26 億ドキュメント
制約● そもそもダウンタイムゼロである必要はあったのか● 仮にダウンタイムがあっても翻訳データはキャッシュされているので 10000+ の Web サイト / サービスは翻訳可能● しかし、ダウンタイムがあるとその間は翻訳の作成 / 更新 /削除は不可能● ユーザは日本だけでなく世界中に存在● たとえば、 EC サイトなどは頻繁に新しいページが公開されるが、その間新しい翻訳がなされないと元言語以外を使用するユーザからの売上は確実に減少する● ビジネスサイドと話し合いをした結果、数分であればダウンタイムの許可は取れそう● しかし、ダウンタイムゼロにこしたことはないし、エンジニアとしはチャレンジングなのでやりたかった
なぜ MongoDB から移行するのか● そもそもスキーマレスである必要がなかった● 厳密な整合性求められるケースが増えてきた●Mongos 突然の死 ( 不安定 )● クエリが激烈に重くなり調べてみるとある Mongo サーバーだけインデックスがはられていない●Mongoid の機能不足● 小さなチームにはメンテナンスコストが高すぎた●Etc● ちゃんと話そうとすると時間足りないので省略。別の機会にでも。なぜ Aurora なのかも同じく省略
移行手順
Step0: アプリケーションコードの修正両方の DB を使えるようアプリケーションコードを修正する● すべての DB アクセスを Abstracter クラス経由に書き換える● ユーザごとにどちらの DB を使用するかのフラグを持たせる●`use_mongo?` はフラグを参照している● フラグは MongoDB にある users collection の field
移行ステップと対応するフラグ名一覧WriteReadReadWrite 2ReadWrite 1ReadWriteWrite 1Write 2aurora_write aurora_read auroraStep 1 Step 2 Step 3 Step 4nilAurora Aurora Aurora Aurora
移行ステップと対応するフラグ名一覧aurora_write aurora_read auroraStep 1 Step 2 Step 3 Step 4nil
Step1: Abstractor を経由していることを保証するWriteRead● フラグは nil● これまで通り R/W が MongoDB に対して行われている状態● これまでと違うのは Abstractor を経由してR/W が行われていること●Moped::Query#initialize にモンキーパッチをあてクエリを組み立てる前に Abstractor を経由して呼びだされていなければ警告を出すようにした●Step1 をデプロイし 2,3 日様子をみて上記の警告が出ていなければ全ての DB へのアクセスが Abstractor 経由で行われていることをおおよそ保証できるnilAurora
Step 2: MongoDB に Write した後 Aurora にも Write する● フラグは aurora_write●Step1 と同じく R/W は MongoDB に対して行う●Step1 と違うのは Mongo への Write に成功した後 Aurora にも Write すること●aurora_write フラグをつけるのはある時点のMongoDB のデータを Aurora に移行する Rakeタスク Aurora inserter を実行した後●aurora_write フラグがついたら Aurora inserterタスク実行中に変更されたデータを auroraに移行する aurora_upserter タスクを実行するReadWrite 2Write 1aurora_writeAurora
Aurora inserter Rake タスクとは●Aurora Inserter とは実行開始時点 (A) 以前に更新された MongoDB のデータを Aurora のスキーマに合わせて Insert する Rake タスク● ユーザごとに実行しエラーなしで Insert が完了した後、そのユーザに対してaurora_write フラグを付与する● 地点 A での MongoDB のデータを Aurora に移行できる● タスク実行中の地点 A~B に変更のあったデータも関しては Aurora にはない状態 ( ユーザごとなので A~B の期間は短い )● 地点 B 以降は aurora_write フラグによりMongoDB と同じのデータが Aurora にも書き込まれるA BA: タスク実行開始B: タスク実行終了
Step 2: MongoDB に Write した後 Aurora にも Write する● フラグは aurora_write●Step1 と同じく R/W は MongoDB に対して行う●Step1 と違うのは Mongo への Write に成功した後 Aurora にも Write すること●aurora_write フラグをつけるのはある時点のMongoDB のデータを Aurora に移行する Rakeタスク Aurora inserter を実行した後●aurora_write フラグがついたら aurora inserterタスク実行中に変更されたデータを auroraに移行する aurora_upserter タスクを実行するReadWrite 2Write 1aurora_writeAurora
Aurora upserter Rake タスク●Aurora upserter タスクとは指定された期間に変更のあった MongoDB のデータを Aurora のスキーマに合わせて Insert or Update or Deleteする Rake タスク●Aurora upserter タスクの期間として AuroraInserter タスク実行中の地点 AB を指定することでユーザごとにすべてのデータをAurora に移行できる●Aurora upserter タスク実行後、ユーザごとにすべてのデータが両方の DB に存在するかをチェックする Rake タスク consistency checkerを実行する●Aurora checker タスクに失敗する場合はAbstractor や Aurora へのデータコンバートなどにバグがある可能性があるので修正して、再度 Aurora upserter タスクをかけるA BA: タスク実行開始B: タスク実行終了
Step 3: Aurora から Read する● フラグは aurora_read●R/W を Aurora に対して行う●Aurora への Write に成功した後 MongoDB にもWrite する (step2 とは逆 )●aurora_read フラグをつけるのは consistencychecker タスクを実行し両方の DB に同じデータがあることを確認した後●MongoDB にも Write しているので、 Aurora への R/W でなにか問題が発生した場合、デプロイなしですぐに aurora_write フラグに戻しMongoDB の R/W に戻すことができるaurora_readReadWrite 1Write 2Aurora
Step 4: Aurora のみを使う● フラグは aurora●Aurora のみを使う● すべてのユーザに対して aurora フラグがついたらデータ移行完了● その後、 Abstractor を削除してフラグを参照せず常に Aurora を使うようリファクタした後、全ユーザーの Aurora フラグを削除するauroraReadWrite
フラグ実装のポイント● フラグを切り替えた瞬間に現在処理中のリクエストがMongoDB から Aurora に切り替わるとタイミングによってはエラー発生 or 不整合データができる● リクエストごとに利用するフラグは同じものを利用する必要がある
フラグ実装のポイント●Controller のインスタンス変数として保持する方法●●●● フラグは様々な箇所で参照されるため、この設計の場合Controller から current_flag を引数で引き回すことになりいまいち
フラグ実装のポイント● クラス変数にフラグを保持してグローバルで参照●●●● こんなことはやってはいけない● スレッドセーフではない
フラグ実装のポイント●`Thread.local` に flag を保持しグローバルに参照 ver1●●●●●●●● スレッドセーフだが同じスレッドが別のリクエストを処理した場合、前回のリクエストでセットした値が格納されたままなので、これもよくない
フラグ実装のポイント●`Thread.local` に flag を保持しグローバルに参照 ver2●●●●●●●● リクエストごとに必ず値をクリアすれば OK● もしくはフラグセット時に `||=` せず毎回上書きしてもよい● もしくは steveklabnik/request_store gem 使うのがよい
フラグ実装のポイント●Rails5.2 にマージされた ActiveSupport::CurrentAttributes もやりたいことは同じでリクエストごとにグローバルな値を保持できる
フラグベースのデータ移行のデメリット● すべてのデータを一回で移行し新しいコードベースで動かす方法と比べると、アプリケーションコードの修正コストがかかる● ユーザごとのデータ移行なので一回で移行するより時間がかかる●Step2,3 は両方の DB に書き込むので、増えた分多少レスポンスタイムが増えサーバー負荷があがる
フラグベースのデータ移行のメリット● ダウンタイムがない● すべてのデータを一回で移行し移行先の DB を利用した新しいコードベースで動かす方法と比べると、ユーザごとにデータ移行と新しいコードベースを徐々に使うことで、バグの影響範囲を小さくしかつバグ早期発見が可能● なにか問題が発生したとき● 小さいスコープで ( ユーザごとに )● できるだけはやく ( コード修正 && デプロイなしでフラグを更新するだけで )● 正常に動く状態に戻すことができる ( 元の DB を利用しサービスを提供し続けることができる )
移行ツールの内部実装
移行ツールの内部実装●Aurora Inserter タスクとは● 実行開始時点以前に更新された MongoDB のデータを Auroraのスキーマに合わせて Insert する Rake タスク●Aurora Inserter タスクが内部でやっていること1. node_values のデータ移行2. page_node_values のデータ移行3. page_node_values にある node_values の外部キーを更新●MongoDB では主キーに BSON::ObjectId を使用していたが、Aurora 移行に伴い auto increment される id を使用したかったため。 ( なお MongoDB の主キーは mongo プリフィックスをつけて mongo_id として varchar で Aurora にも保持 )
移行ツールの内部実装●Aurora Inserter タスクのパフォーマンス●1500 レコード ( ドキュメント ) / 秒●(text 型のデータもあるのでレコード単位での計測結果は正確とは言えないが、移行するレコード数からおおよその実行時間を見積もることができる位の精度 )●7 億レコード保持するユーザも存在し、このユーザだけで約 5.4 日かかる計算 ( そんなに待ちたくない )●Aurora Inserter タスク以外にも Aurora upserter や consistencychecker タスクのことも考えるとさらに時間がかかる●I / O の割合が多いんだから thread 使って書きなおすことにした
移行ツールの内部実装●Producer Consumer パターンProducer 1Producer 3Producer XConsumer 1Consumer 2Consumer XQueue●Producer は仕事に必要なデータを生産して Queue に詰める●Consumer は Queue からデータを取り出して仕事を消費する● ある Producer が Queue を参照し書き込み終わるまで、他のProducer に割り込まれてはいけない● ある Consumer が Queue を参照し取り出すまで、他の Consumerに割り込まれてはいけない●Queue が上限まできたら書き込みを待ち、空なら取り出すのをまたなければいけない
移行ツールの内部実装●Producer Consumer パターンProducer 2 Consumer 2QueueProducer 1Producer 3Consumer 1Consumer X●Producer は MongoDB から移行データを取得して Queue につめる●Consumer は移行データを Queue から取りだし Aurora 用に加工しAurora につめる●Thread 数の調整や Queue の中継によって Producer と Consumer 間の処理スピードの差異を吸収しパフォーマンスの向上が期待できる● 簡略化したサンプルコードは次のスライドに掲載Aurora
作戦
マルチスレッド
移行ツールの内部実装●Aurora Inserter タスク ver マルチスレッドのパフォーマンス● スレッド数 (producer 2, consumer 6)● コア数 4●2300 レコード ( ドキュメント ) / 秒 1.5x faster●7 億レコード保持するユーザが約 3.5 日かかる計算 ( そんなに待ちたくない )● コア数とスレッド数を上げればパフォーマンスは上がるが、 I / O 以外の処理も並列にできればさらなるパフォーマンスの向上が期待できた● そこで
マルチサーバー
JRuby 採用理由●Real threading でマルチコアを活用したい●jruby-9.1.8.0
JRuby 採用理由●Real threading でマルチコアを活用したい●jruby-9.1.8.0● 余談 : ProducerConsumer パターンを実装する際、ひそかに JRubyでも動かせるよう書いていた
ディレクトリ構成●Rails アプリのルートディレクトリで `mkdir juby` して `rbenvlocal jruby-9.1.8.0`●Gemfile には `activerecord-jdbcmysql-adapter` と `mongoid` を指定。 DB の接続情報をjruby/config 配下に記載●jruby/models 配下に Rails のapp/models 配下のモデルファイルなど今回使用するファイルをコピーする●Rails アプリで使用していた Gemに依存したコードを削除● 細かいところはまだあるが、おおよそこれで動く
移行ツールの内部実装●Aurora Inserter タスク (JRuby マルチスレッドのパフォーマンス )● スレッド数 (producer 2, consumer 6)● コア数 4●4300 レコード ( ドキュメント ) / 秒 2.8x faster●7 億レコード保持するユーザが約 1.9 日かかる計算 ( そんなに待ちたくない )●10x 以上はやくしたい● そこで
マルチサーバー X マルチスレッド● コア数 8 のサーバーを 20 台用意● 移行対象のドキュメントの主キーを 20 分割し、各サーバにファイルとして配布● 事前に DB 負荷状況や同時接続数の上限などを確認● 実装の都合上、 Aurora Inserter の処理 (step1~3) の step3 は全サーバーが step1,step2 を終わってから実行する必要があった。ようはマルチサーバーとはいえ同期的に各 Step を行う必要があった
capistrano/sshkit gem●Capistrano gem の dependency に指定されている gem でCapistrano の ssh 関連のコードはこの sshkit gem のラッパー●ssh 経由でパラレルに実行する部分と同期的に実行する部分を DSL で簡単に指定できる●sshkit は JRuby で CI が回っていなかったため Rails アプリ側のgemfile に追加し Rails の rake タスクから jruby のコードを実行
所管
結果
2.4 時間 54x faster
FIN