Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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)

Slide 3

Slide 3 text

どんなサービス ?

Slide 4

Slide 4 text

collection の規模感 RubyKaigi 2017

Slide 5

Slide 5 text

Ruby biz Grand prix 2017

Slide 6

Slide 6 text

Our Team

Slide 7

Slide 7 text

はじまりはじまり

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

なぜ MongoDB から移行するのか ● そもそもスキーマレスである必要がなかった ● 厳密な整合性求められるケースが増えてきた ● Mongos 突然の死 ( 不安定 ) ● クエリが激烈に重くなり調べてみるとある Mongo サーバー だけインデックスがはられていない ● Mongoid の機能不足 ● 小さなチームにはメンテナンスコストが高すぎた ● Etc ● ちゃんと話そうとすると時間足りないので省略。別の機会 にでも。なぜ Aurora なのかも同じく省略

Slide 14

Slide 14 text

移行手順

Slide 15

Slide 15 text

Step0: アプリケーションコードの修正 両方の DB を使えるようアプリケーションコードを修正する ● すべての DB アクセスを Abstracter クラス経由に書き換える ● ユーザごとにどちらの DB を使用するかのフラグを持たせる ● `use_mongo?` はフラグを参照している ● フラグは MongoDB にある users collection の field

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Step1: Abstractor を経由していることを保証する Write Read ● フラグは nil ● これまで通り R/W が MongoDB に対して行わ れている状態 ● これまでと違うのは Abstractor を経由して R/W が行われていること ● Moped::Query#initialize にモンキーパッチをあ てクエリを組み立てる前に Abstractor を経由 して呼びだされていなければ警告を出すよ うにした ● Step1 をデプロイし 2,3 日様子をみて上記の 警告が出ていなければ全ての DB へのアクセ スが Abstractor 経由で行われていることをお およそ保証できる nil Aurora

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

フラグ実装のポイント ● Controller のインスタンス変数として保持する方法 ● ● ● ● フラグは様々な箇所で参照されるため、この設計の場合 Controller から current_flag を引数で引き回すことになりいま いち

Slide 35

Slide 35 text

フラグ実装のポイント ● クラス変数にフラグを保持してグローバルで参照 ● ● ● ● こんなことはやってはいけない ● スレッドセーフではない

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

フラグ実装のポイント ● `Thread.local` に flag を保持しグローバルに参照 ver2 ● ● ● ● ● ● ● ● リクエストごとに必ず値をクリアすれば OK ● もしくはフラグセット時に `||=` せず毎回上書きしてもよい ● もしくは steveklabnik/request_store gem 使うのがよい

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

フラグベースのデータ移行のデメリット ● すべてのデータを一回で移行し新しいコードベースで動かす方 法と比べると、アプリケーションコードの修正コストがかかる ● ユーザごとのデータ移行なので一回で移行するより時間がかか る ● Step2,3 は両方の DB に書き込むので、増えた分多少レスポンスタ イムが増えサーバー負荷があがる

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

移行ツールの内部実装

Slide 42

Slide 42 text

移行ツールの内部実装 ● 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 にも保持 )

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

移行ツールの内部実装 ● 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 が上限まできたら書き込みを待ち、空なら取り出すの をまたなければいけない

Slide 45

Slide 45 text

移行ツールの内部実装 ● 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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

作戦

Slide 48

Slide 48 text

マルチスレッド

Slide 49

Slide 49 text

移行ツールの内部実装 ● Aurora Inserter タスク ver マルチスレッドのパフォーマンス ● スレッド数 (producer 2, consumer 6) ● コア数 4 ● 2300 レコード ( ドキュメント ) / 秒 1.5x faster ● 7 億レコード保持するユーザが約 3.5 日かかる計算 ( そんな に待ちたくない ) ● コア数とスレッド数を上げればパフォーマンスは上がる が、 I / O 以外の処理も並列にできればさらなるパフォー マンスの向上が期待できた ● そこで

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

マルチサーバー

Slide 52

Slide 52 text

JRuby 採用理由 ● Real threading でマルチコアを活用したい ● jruby-9.1.8.0

Slide 53

Slide 53 text

JRuby 採用理由 ● Real threading でマルチコアを活用したい ● jruby-9.1.8.0 ● 余談 : ProducerConsumer パターンを実装する際、ひそかに JRuby でも動かせるよう書いていた

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

移行ツールの内部実装 ● Aurora Inserter タスク (JRuby マルチスレッドのパフォーマンス ) ● スレッド数 (producer 2, consumer 6) ● コア数 4 ● 4300 レコード ( ドキュメント ) / 秒 2.8x faster ● 7 億レコード保持するユーザが約 1.9 日かかる計算 ( そんなに 待ちたくない ) ● 10x 以上はやくしたい ● そこで

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

所管

Slide 60

Slide 60 text

結果

Slide 61

Slide 61 text

2.4 時間 54x faster

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

FIN