Upgrade to Pro — share decks privately, control downloads, hide ads and more …

365日24時間稼働必須サービスの 完全無停止DB移行

365日24時間稼働必須サービスの 完全無停止DB移行

Rails Developers Meetup 2018: Day 1 発表資料
https://techplay.jp/event/639872

Kyuden Masahiro

March 24, 2018
Tweet

More Decks by Kyuden Masahiro

Other Decks in Technology

Transcript

  1. 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)
  2. 昨年 7 月に 38 億レコード ( ドキュメント ) を 不整合データなし

    ダウンタイムゼロで MongoDB から Amazon Aurora に データ移行した
  3. 昨年 7 月に 38 億レコード ( ドキュメント ) を 不整合データなし

    ダウンタイムゼロで MongoDB から Amazon Aurora に データ移行した
  4. 移行対象のコレクション • node_values • 翻訳データが格納されたコレクション • 約 12 億ドキュメント •

    page_node_values • どのページにどの翻訳データがあるかが格納されたコレク ション • 大まかに言うと page と node_values のジャンクションテー ブル ( ジャンクションコレクション ) • 約 26 億ドキュメント
  5. 制約 • そもそもダウンタイムゼロである必要はあったのか • 仮にダウンタイムがあっても翻訳データはキャッシュされ ているので 10000+ の Web サイト

    / サービスは翻訳可能 • しかし、ダウンタイムがあるとその間は翻訳の作成 / 更新 / 削除は不可能 • ユーザは日本だけでなく世界中に存在 • たとえば、 EC サイトなどは頻繁に新しいページが公開され るが、その間新しい翻訳がなされないと元言語以外を使用 するユーザからの売上は確実に減少する • ビジネスサイドと話し合いをした結果、数分であればダウ ンタイムの許可は取れそう • しかし、ダウンタイムゼロにこしたことはないし、エンジ ニアとしはチャレンジングなのでやりたかった
  6. なぜ MongoDB から移行するのか • そもそもスキーマレスである必要がなかった • 厳密な整合性求められるケースが増えてきた • Mongos 突然の死

    ( 不安定 ) • クエリが激烈に重くなり調べてみるとある Mongo サーバー だけインデックスがはられていない • Mongoid の機能不足 • 小さなチームにはメンテナンスコストが高すぎた • Etc • ちゃんと話そうとすると時間足りないので省略。別の機会 にでも。なぜ Aurora なのかも同じく省略
  7. Step0: アプリケーションコードの修正 両方の DB を使えるようアプリケーションコードを修正する • すべての DB アクセスを Abstracter

    クラス経由に書き換える • ユーザごとにどちらの DB を使用するかのフラグを持たせる • `use_mongo?` はフラグを参照している • フラグは MongoDB にある users collection の field
  8. 移行ステップと対応するフラグ名一覧 Write Read Read Write 2 Read Write 1 Read

    Write Write 1 Write 2 aurora_write aurora_read aurora Step 1 Step 2 Step 3 Step 4 nil Aurora Aurora Aurora Aurora
  9. Step1: Abstractor を経由していることを保証する Write Read • フラグは nil • これまで通り

    R/W が MongoDB に対して行わ れている状態 • これまでと違うのは Abstractor を経由して R/W が行われていること • Moped::Query#initialize にモンキーパッチをあ てクエリを組み立てる前に Abstractor を経由 して呼びだされていなければ警告を出すよ うにした • Step1 をデプロイし 2,3 日様子をみて上記の 警告が出ていなければ全ての DB へのアクセ スが Abstractor 経由で行われていることをお およそ保証できる nil Aurora
  10. 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 タスクを実行する Read Write 2 Write 1 aurora_write Aurora
  11. 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 B A: タスク実行開始 B: タスク実行終了
  12. 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 タスクを実行する Read Write 2 Write 1 aurora_write Aurora
  13. Aurora upserter Rake タスク • Aurora upserter タスクとは指定された期間に 変更のあった MongoDB

    のデータを Aurora の スキーマに合わせて Insert or Update or Delete する Rake タスク • Aurora upserter タスクの期間として Aurora Inserter タスク実行中の地点 AB を指定する ことでユーザごとにすべてのデータを Aurora に移行できる • Aurora upserter タスク実行後、ユーザごとに すべてのデータが両方の DB に存在するかを チェックする Rake タスク consistency checker を実行する • Aurora checker タスクに失敗する場合は Abstractor や Aurora へのデータコンバートな どにバグがある可能性があるので修正し て、再度 Aurora upserter タスクをかける A B A: タスク実行開始 B: タスク実行終了
  14. Step 3: Aurora から Read する • フラグは aurora_read •

    R/W を Aurora に対して行う • Aurora への Write に成功した後 MongoDB にも Write する (step2 とは逆 ) • aurora_read フラグをつけるのは consistency checker タスクを実行し両方の DB に同じデ ータがあることを確認した後 • MongoDB にも Write しているので、 Aurora へ の R/W でなにか問題が発生した場合、デプ ロイなしですぐに aurora_write フラグに戻し MongoDB の R/W に戻すことができる aurora_read Read Write 1 Write 2 Aurora
  15. Step 4: Aurora のみを使う • フラグは aurora • Aurora のみを使う

    • すべてのユーザに対して aurora フラグがつ いたらデータ移行完了 • その後、 Abstractor を削除してフラグを参照 せず常に Aurora を使うようリファクタした 後、全ユーザーの Aurora フラグを削除する aurora Read Write
  16. 移行ステップと対応するフラグ名一覧 Write Read Read Write 2 Read Write 1 Read

    Write Write 1 Write 2 aurora_write aurora_read aurora Step 1 Step 2 Step 3 Step 4 nil Aurora Aurora Aurora Aurora
  17. フラグ実装のポイント • `Thread.local` に flag を保持しグローバルに参照 ver1 • • •

    • • • • • スレッドセーフだが同じスレッドが別のリクエストを処理 した場合、前回のリクエストでセットした値が格納された ままなので、これもよくない
  18. フラグ実装のポイント • `Thread.local` に flag を保持しグローバルに参照 ver2 • • •

    • • • • • リクエストごとに必ず値をクリアすれば OK • もしくはフラグセット時に `||=` せず毎回上書きしてもよい • もしくは steveklabnik/request_store gem 使うのがよい
  19. フラグベースのデータ移行のメリット • ダウンタイムがない • すべてのデータを一回で移行し移行先の DB を利用した新しいコ ードベースで動かす方法と比べると、ユーザごとにデータ移行 と新しいコードベースを徐々に使うことで、バグの影響範囲を 小さくしかつバグ早期発見が可能

    • なにか問題が発生したとき • 小さいスコープで ( ユーザごとに ) • できるだけはやく ( コード修正 && デプロイなしでフラグを 更新するだけで ) • 正常に動く状態に戻すことができる ( 元の DB を利用しサー ビスを提供し続けることができる )
  20. 移行ツールの内部実装 • 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 にも保持 )
  21. 移行ツールの内部実装 • Aurora Inserter タスクのパフォーマンス • 1500 レコード ( ドキュメント

    ) / 秒 • (text 型のデータもあるのでレコード単位での計測結果は 正確とは言えないが、移行するレコード数からおおよそ の実行時間を見積もることができる位の精度 ) • 7 億レコード保持するユーザも存在し、このユーザだけで 約 5.4 日かかる計算 ( そんなに待ちたくない ) • Aurora Inserter タスク以外にも Aurora upserter や consistency checker タスクのことも考えるとさらに時間がかかる • I / O の割合が多いんだから thread 使って書きなおすことに した
  22. 移行ツールの内部実装 • Producer Consumer パターン Producer 1 Producer 3 Producer

    X Consumer 1 Consumer 2 Consumer X Queue • Producer は仕事に必要なデータを生産して Queue に詰める • Consumer は Queue からデータを取り出して仕事を消費する • ある Producer が Queue を参照し書き込み終わるまで、他の Producer に割り込まれてはいけない • ある Consumer が Queue を参照し取り出すまで、他の Consumer に割り込まれてはいけない • Queue が上限まできたら書き込みを待ち、空なら取り出すの をまたなければいけない
  23. 移行ツールの内部実装 • Producer Consumer パターン Producer 2 Consumer 2 Queue

    Producer 1 Producer 3 Consumer 1 Consumer X • Producer は MongoDB から移行データを取得して Queue につめる • Consumer は移行データを Queue から取りだし Aurora 用に加工し Aurora につめる • Thread 数の調整や Queue の中継によって Producer と Consumer 間 の処理スピードの差異を吸収しパフォーマンスの向上が期待 できる • 簡略化したサンプルコードは次のスライドに掲載 Aurora
  24. 移行ツールの内部実装 • Aurora Inserter タスク ver マルチスレッドのパフォーマンス • スレッド数 (producer

    2, consumer 6) • コア数 4 • 2300 レコード ( ドキュメント ) / 秒 1.5x faster • 7 億レコード保持するユーザが約 3.5 日かかる計算 ( そんな に待ちたくない ) • コア数とスレッド数を上げればパフォーマンスは上がる が、 I / O 以外の処理も並列にできればさらなるパフォー マンスの向上が期待できた • そこで
  25. JRuby 採用理由 • Real threading でマルチコアを活用したい • jruby-9.1.8.0 • 余談

    : ProducerConsumer パターンを実装する際、ひそかに JRuby でも動かせるよう書いていた
  26. ディレクトリ構成 • Rails アプリのルートディレク トリで `mkdir juby` して `rbenv local

    jruby-9.1.8.0` • Gemfile には `activerecord- jdbcmysql-adapter` と `mongoid` を 指定。 DB の接続情報を jruby/config 配下に記載 • jruby/models 配下に Rails の app/models 配下のモデルファイ ルなど今回使用するファイル をコピーする • Rails アプリで使用していた Gem に依存したコードを削除 • 細かいところはまだあるが、 おおよそこれで動く
  27. 移行ツールの内部実装 • Aurora Inserter タスク (JRuby マルチスレッドのパフォーマンス ) • スレッド数

    (producer 2, consumer 6) • コア数 4 • 4300 レコード ( ドキュメント ) / 秒 2.8x faster • 7 億レコード保持するユーザが約 1.9 日かかる計算 ( そんなに 待ちたくない ) • 10x 以上はやくしたい • そこで
  28. マルチサーバー X マルチスレッド • コア数 8 のサーバーを 20 台用意 •

    移行対象のドキュメントの主キーを 20 分割し、各サーバにファ イルとして配布 • 事前に DB 負荷状況や同時接続数の上限などを確認 • 実装の都合上、 Aurora Inserter の処理 (step1~3) の step3 は全サーバ ーが step1,step2 を終わってから実行する必要があった。ようは マルチサーバーとはいえ同期的に各 Step を行う必要があった
  29. capistrano/sshkit gem • Capistrano gem の dependency に指定されている gem で

    Capistrano の ssh 関連のコードはこの sshkit gem のラッパー • ssh 経由でパラレルに実行する部分と同期的に実行する部分 を DSL で簡単に指定できる • sshkit は JRuby で CI が回っていなかったため Rails アプリ側の gemfile に追加し Rails の rake タスクから jruby のコードを実行
  30. FIN