$30 off During Our Annual Pro Sale. View Details »

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. 365 日 24 時間稼働必須サービスの
    完全無停止 DB 移行
    〜 MongoDB to Amazon Aurora 〜

    View Slide

  2. 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)

    View Slide

  3. どんなサービス ?

    View Slide

  4. collection の規模感
    RubyKaigi 2017

    View Slide

  5. Ruby biz Grand prix 2017

    View Slide

  6. Our Team

    View Slide

  7. はじまりはじまり

    View Slide

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

    View Slide

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

    View Slide

  10. このトークで主に話すこと
    ● 具体的なデータ移行方法
    ● 移行のために作ったツールの設計 / 内部実装

    View Slide

  11. 移行対象のコレクション

    node_values
    ● 翻訳データが格納されたコレクション
    ● 約 12 億ドキュメント

    page_node_values
    ● どのページにどの翻訳データがあるかが格納されたコレク
    ション
    ● 大まかに言うと page と node_values のジャンクションテー
    ブル ( ジャンクションコレクション )
    ● 約 26 億ドキュメント

    View Slide

  12. 制約
    ● そもそもダウンタイムゼロである必要はあったのか
    ● 仮にダウンタイムがあっても翻訳データはキャッシュされ
    ているので 10000+ の Web サイト / サービスは翻訳可能
    ● しかし、ダウンタイムがあるとその間は翻訳の作成 / 更新 /
    削除は不可能
    ● ユーザは日本だけでなく世界中に存在
    ● たとえば、 EC サイトなどは頻繁に新しいページが公開され
    るが、その間新しい翻訳がなされないと元言語以外を使用
    するユーザからの売上は確実に減少する
    ● ビジネスサイドと話し合いをした結果、数分であればダウ
    ンタイムの許可は取れそう
    ● しかし、ダウンタイムゼロにこしたことはないし、エンジ
    ニアとしはチャレンジングなのでやりたかった

    View Slide

  13. なぜ MongoDB から移行するのか
    ● そもそもスキーマレスである必要がなかった
    ● 厳密な整合性求められるケースが増えてきた

    Mongos 突然の死 ( 不安定 )
    ● クエリが激烈に重くなり調べてみるとある Mongo サーバー
    だけインデックスがはられていない

    Mongoid の機能不足
    ● 小さなチームにはメンテナンスコストが高すぎた

    Etc
    ● ちゃんと話そうとすると時間足りないので省略。別の機会
    にでも。なぜ Aurora なのかも同じく省略

    View Slide

  14. 移行手順

    View Slide

  15. Step0: アプリケーションコードの修正
    両方の DB を使えるようアプリケーションコードを修正する
    ● すべての DB アクセスを Abstracter クラス経由に書き換える
    ● ユーザごとにどちらの DB を使用するかのフラグを持たせる

    `use_mongo?` はフラグを参照している
    ● フラグは MongoDB にある users collection の field

    View Slide

  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

    View Slide

  17. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  18. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  19. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  20. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  21. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  22. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  23. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  24. 移行ステップと対応するフラグ名一覧
    aurora_write aurora_read aurora
    Step 1 Step 2 Step 3 Step 4
    nil

    View Slide

  25. Step1: Abstractor を経由していることを保証する
    Write
    Read
    ● フラグは nil
    ● これまで通り R/W が MongoDB に対して行わ
    れている状態
    ● これまでと違うのは Abstractor を経由して
    R/W が行われていること

    Moped::Query#initialize にモンキーパッチをあ
    てクエリを組み立てる前に Abstractor を経由
    して呼びだされていなければ警告を出すよ
    うにした

    Step1 をデプロイし 2,3 日様子をみて上記の
    警告が出ていなければ全ての DB へのアクセ
    スが Abstractor 経由で行われていることをお
    およそ保証できる
    nil
    Aurora

    View Slide

  26. 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

    View Slide

  27. 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: タスク実行終了

    View Slide

  28. 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

    View Slide

  29. 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: タスク実行終了

    View Slide

  30. 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

    View Slide

  31. Step 4: Aurora のみを使う
    ● フラグは aurora

    Aurora のみを使う
    ● すべてのユーザに対して aurora フラグがつ
    いたらデータ移行完了
    ● その後、 Abstractor を削除してフラグを参照
    せず常に Aurora を使うようリファクタした
    後、全ユーザーの Aurora フラグを削除する
    aurora
    Read
    Write

    View Slide

  32. 移行ステップと対応するフラグ名一覧
    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

    View Slide

  33. フラグ実装のポイント
    ● フラグを切り替えた瞬間に現在処理中のリクエストが
    MongoDB から Aurora に切り替わるとタイミングによってはエ
    ラー発生 or 不整合データができる
    ● リクエストごとに利用するフラグは同じものを利用する必
    要がある

    View Slide

  34. フラグ実装のポイント

    Controller のインスタンス変数として保持する方法



    ● フラグは様々な箇所で参照されるため、この設計の場合
    Controller から current_flag を引数で引き回すことになりいま
    いち

    View Slide

  35. フラグ実装のポイント
    ● クラス変数にフラグを保持してグローバルで参照



    ● こんなことはやってはいけない
    ● スレッドセーフではない

    View Slide

  36. フラグ実装のポイント

    `Thread.local` に flag を保持しグローバルに参照 ver1







    ● スレッドセーフだが同じスレッドが別のリクエストを処理
    した場合、前回のリクエストでセットした値が格納された
    ままなので、これもよくない

    View Slide

  37. フラグ実装のポイント

    `Thread.local` に flag を保持しグローバルに参照 ver2







    ● リクエストごとに必ず値をクリアすれば OK
    ● もしくはフラグセット時に `||=` せず毎回上書きしてもよい
    ● もしくは steveklabnik/request_store gem 使うのがよい

    View Slide

  38. フラグ実装のポイント

    Rails5.2 にマージされた ActiveSupport::CurrentAttributes もやりたいこ
    とは同じでリクエストごとにグローバルな値を保持できる

    View Slide

  39. フラグベースのデータ移行のデメリット
    ● すべてのデータを一回で移行し新しいコードベースで動かす方
    法と比べると、アプリケーションコードの修正コストがかかる
    ● ユーザごとのデータ移行なので一回で移行するより時間がかか


    Step2,3 は両方の DB に書き込むので、増えた分多少レスポンスタ
    イムが増えサーバー負荷があがる

    View Slide

  40. フラグベースのデータ移行のメリット
    ● ダウンタイムがない
    ● すべてのデータを一回で移行し移行先の DB を利用した新しいコ
    ードベースで動かす方法と比べると、ユーザごとにデータ移行
    と新しいコードベースを徐々に使うことで、バグの影響範囲を
    小さくしかつバグ早期発見が可能
    ● なにか問題が発生したとき
    ● 小さいスコープで ( ユーザごとに )
    ● できるだけはやく ( コード修正 && デプロイなしでフラグを
    更新するだけで )
    ● 正常に動く状態に戻すことができる ( 元の DB を利用しサー
    ビスを提供し続けることができる )

    View Slide

  41. 移行ツールの内部実装

    View Slide

  42. 移行ツールの内部実装

    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 にも保持 )

    View Slide

  43. 移行ツールの内部実装

    Aurora Inserter タスクのパフォーマンス

    1500 レコード ( ドキュメント ) / 秒

    (text 型のデータもあるのでレコード単位での計測結果は
    正確とは言えないが、移行するレコード数からおおよそ
    の実行時間を見積もることができる位の精度 )

    7 億レコード保持するユーザも存在し、このユーザだけで
    約 5.4 日かかる計算 ( そんなに待ちたくない )

    Aurora Inserter タスク以外にも Aurora upserter や consistency
    checker タスクのことも考えるとさらに時間がかかる

    I / O の割合が多いんだから thread 使って書きなおすことに
    した

    View Slide

  44. 移行ツールの内部実装

    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 が上限まできたら書き込みを待ち、空なら取り出すの
    をまたなければいけない

    View Slide

  45. 移行ツールの内部実装

    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

    View Slide

  46. View Slide

  47. 作戦

    View Slide

  48. マルチスレッド

    View Slide

  49. 移行ツールの内部実装

    Aurora Inserter タスク ver マルチスレッドのパフォーマンス
    ● スレッド数 (producer 2, consumer 6)
    ● コア数 4

    2300 レコード ( ドキュメント ) / 秒 1.5x faster

    7 億レコード保持するユーザが約 3.5 日かかる計算 ( そんな
    に待ちたくない )
    ● コア数とスレッド数を上げればパフォーマンスは上がる
    が、 I / O 以外の処理も並列にできればさらなるパフォー
    マンスの向上が期待できた
    ● そこで

    View Slide

  50. View Slide

  51. マルチサーバー

    View Slide

  52. JRuby 採用理由

    Real threading でマルチコアを活用したい

    jruby-9.1.8.0

    View Slide

  53. JRuby 採用理由

    Real threading でマルチコアを活用したい

    jruby-9.1.8.0
    ● 余談 : ProducerConsumer パターンを実装する際、ひそかに JRuby
    でも動かせるよう書いていた

    View Slide

  54. ディレクトリ構成

    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
    に依存したコードを削除
    ● 細かいところはまだあるが、
    おおよそこれで動く

    View Slide

  55. 移行ツールの内部実装

    Aurora Inserter タスク (JRuby マルチスレッドのパフォーマンス )
    ● スレッド数 (producer 2, consumer 6)
    ● コア数 4

    4300 レコード ( ドキュメント ) / 秒 2.8x faster

    7 億レコード保持するユーザが約 1.9 日かかる計算 ( そんなに
    待ちたくない )

    10x 以上はやくしたい
    ● そこで

    View Slide

  56. マルチサーバー X マルチスレッド
    ● コア数 8 のサーバーを 20 台用意
    ● 移行対象のドキュメントの主キーを 20 分割し、各サーバにファ
    イルとして配布
    ● 事前に DB 負荷状況や同時接続数の上限などを確認
    ● 実装の都合上、 Aurora Inserter の処理 (step1~3) の step3 は全サーバ
    ーが step1,step2 を終わってから実行する必要があった。ようは
    マルチサーバーとはいえ同期的に各 Step を行う必要があった

    View Slide

  57. View Slide

  58. capistrano/sshkit gem

    Capistrano gem の dependency に指定されている gem で
    Capistrano の ssh 関連のコードはこの sshkit gem のラッパー

    ssh 経由でパラレルに実行する部分と同期的に実行する部分
    を DSL で簡単に指定できる

    sshkit は JRuby で CI が回っていなかったため Rails アプリ側の
    gemfile に追加し Rails の rake タスクから jruby のコードを実行

    View Slide

  59. 所管

    View Slide

  60. 結果

    View Slide

  61. 2.4 時間 54x faster

    View Slide

  62. View Slide

  63. FIN

    View Slide