Slide 1

Slide 1 text

STORES 株式会社 長年運用されているサービスの 主要データ移行をサービス停止せず 安全にリリースしました 2024年 12月

Slide 2

Slide 2 text

目次 2 自己紹介 今日のプログラム 移行プロジェクトでやりたいこと 移行のやり方 移行リリースをしてみて まとめ 01 02 03 04 05 06

Slide 3

Slide 3 text

自己紹介 STORES 株式会社 テクノロジー部門 リテール GTM B グループ エンジニアリングマネジャー 山下 隼人 (Hayato Yamashita) @phayacell 3

Slide 4

Slide 4 text

今日のプログラム

Slide 5

Slide 5 text

最初に質問です 50,000,000 件 5 +α

Slide 6

Slide 6 text

答えは? STORES ネットショップ 注文データの件数 6

Slide 7

Slide 7 text

今日のプログラムは この注文データのデータ構造を移行する (書き込み先を切り替える) プロジェクトのお話です 7

Slide 8

Slide 8 text

今日のプログラムで持ち帰れること(結論) 1. アプリケーションでやるカナリアリリース 2. 新旧データ構造の差分チェックの有用性 3. テスト不足でも安全にリリースする方法 8

Slide 9

Slide 9 text

前提:STORES ネットショップは? 12年 サービス運用しています データはたくさんあります 主要となる注文データの件数は先述 9

Slide 10

Slide 10 text

STORES の説明をさせてください B2C 事業者の顧客データを中心とした フロントオフィス プラットフォーム 提供しているサービスには、以下のものがあります 10

Slide 11

Slide 11 text

STORES ネットショップの略歴(レジを含む) 2012年8月 STORES ネットショップ、サービスローンチ 2020年5月 注文データの件数が 10,000,000 を超える 2021年6月 STORES レジ、サービスローンチ 2024年5月 注文データの件数が 50,000,000 を超える 11

Slide 12

Slide 12 text

STORES は長年運用しているサービスです 長年運用しているサービスのデータには、いくつかの特性があります ● とにかくデータ量が増えていくもの ● 書き込み回数の多いもの ● 読み込み回数の多いもの ● マスタデータとしてほとんど変化しないもの などなど…… 12

Slide 13

Slide 13 text

STORES の注文データは 注文データには、以下の特性があります ● とにかくデータ量が増えていくもの ● 書き込み回数の多いもの ● 読み込み回数の多いもの ● マスタデータとしてほとんど変化しないもの 13

Slide 14

Slide 14 text

長年運用しているサービスのつらみ 当時の最善を尽くしたデータ構造設計 改修に次ぐ改修が重ねられ だんだんと扱いづらく…… まるで九龍城砦のよう 14

Slide 15

Slide 15 text

そこで、我々はこうしました 15

Slide 16

Slide 16 text

そうだ、新しいデータ構造に移行しよう

Slide 17

Slide 17 text

移行プロジェクトでやりたいこと

Slide 18

Slide 18 text

新しいデータ構造の要求 データ構造の最適化 これまでの積み重ねられた改修を踏まえたい STORES レジにも耐えられるデータ構造へ レジもネットショップと同じ DB / テーブルで注文データを扱いたい ネットショップからはじまった注文データの構造では、 より複雑な店頭販売の注文データの構造に耐えられない…… 18

Slide 19

Slide 19 text

だから、データ構造を新しく作ろう Not schema migrate Generate new TABLE 19

Slide 20

Slide 20 text

考えられる選択肢 1. 古いデータを新しいデータ構造に移行する 2. 古いデータは移行せず、放置する 3. 古いデータは移行せず、全部削除する 20

Slide 21

Slide 21 text

考えられる選択肢から、どうやって選ぼう? 改修頻度は高い? → YES 書き込み頻度は高い? → YES 読み込み頻度は高い? → YES 過去データも参照され続ける? → YES 21

Slide 22

Slide 22 text

なので、我々はこうしました 22

Slide 23

Slide 23 text

古いデータを新しいデータ構造に移行する

Slide 24

Slide 24 text

移行のやり方

Slide 25

Slide 25 text

リリースの流れをまとめました 基本方針:ご安全に 動き続けているサービスを止めたくない できるなら、サービス停止もしたくない 大量のデータを一気に移行することはせず、 少しずつ切り替えていく 25

Slide 26

Slide 26 text

安全なデータ移行の手順の、全体像 1. 新しいデータ構造を作る 2. 新しいデータ構造にも書き込む(二重書き込み) 3. 新しいデータ構造から書き込む(書き込み先の変更) 4. 新しいデータ構造を参照する(読み込み先の変更) 5. 古いデータ構造をやめる 26

Slide 27

Slide 27 text

安全なデータ移行の手順の、ここまではできている(前提) 1. 新しいデータ構造を作る 2. 新しいデータ構造にも書き込む(二重書き込み) 27 注文データ (古い) 注文データ (新しい) 同期

Slide 28

Slide 28 text

安全なデータ移行の手順の、ここをやる! 1. 新しいデータ構造を作る 2. 新しいデータ構造にも書き込む(二重書き込み) 3. 新しいデータ構造から書き込む(書き込み先の変更) 4. 新しいデータ構造を参照する(読み込み先の変更) 5. 古いデータ構造をやめる 28

Slide 29

Slide 29 text

二重書き込みのイメージ 29 注文リクエスト 注文データ作成 注文データ (古い) 注文データ変換 古い→新しい 注文データ (新しい) 注文完了 作 成 変換

Slide 30

Slide 30 text

二重書き込みのイメージを、こうしたい!(書き込み先の変更) 30 注文リクエスト 注文データ作成 注文データ (古い) 注文データ変換 新しい→古い 注文データ (新しい) 注文完了 作 成 変換

Slide 31

Slide 31 text

二重書き込みのイメージを、最終的にはこうしたい!(古いデータ構造をやめる) 31 注文リクエスト 注文データ作成 注文データ (古い) 注文データ変換 新しい→古い 注文データ (新しい) 注文完了 作 成 変換

Slide 32

Slide 32 text

リリース手法は そして、少しずつ切り替えるため カナリアリリースをしよう 32

Slide 33

Slide 33 text

カナリアリリースとは A canary release (or canary launch or canary deployment) allows developers to have features incrementally tested by a small set of developers. Feature flags like an alternate way to do canary launches and allow targeting by geographic locations or even user attributes. If a feature's performance is not satisfactory, then it can be rolled back without any adverse effects. It is named after the use of canaries to warn miners of toxic gases (Miner's canary). ref. Feature toggle#Canary_release - Wikipedia 33

Slide 34

Slide 34 text

カナリアリリースの、メリットは? 少しずつ新しい作成処理(カナリア) を動かすことができる もしバグが見つかれば、 被害を最小限にするためリリースを中断 これまで動いていた元の処理(ベースライン) に戻すことができる 34

Slide 35

Slide 35 text

カナリアリリースを、アプリケーションで? ロードバランサーでやることが多い カナリアリリース 今回の移行プロジェクトでは、 これをアプリケーションでやります (厳密には段階的リリースかも) 35

Slide 36

Slide 36 text

カナリアリリースをアプリケーションでする理由 ひとつのデータに対するリクエストが 複数のリクエストに分かれても 一貫してカナリア・ベースラインの どちらで処理するか、振り分けができる 36

Slide 37

Slide 37 text

複数のリクエストに分かれる? 注文データ作成は、 ひとつのリクエストで完結しない ユーザからの「注文する」ボタン押下後、実は redirect & callback が 発生する場合がある(〇〇ポイントが貯まる決済手段とか) ひとつの注文データの処理が完了するまでは、そのデータに対する処理を ベースライン or カナリアを統一させたい(じゃないと調査が大変) AWS CodeDeploy や LB を使ったカナリアリリースでは、これが難しい 37

Slide 38

Slide 38 text

なので、アプリケーションでカナリアリリースを管理する カナリアで作成したデータに 「カナリアで処理された」とわかる情報を残す(フラグで良い) redirect & callback でリクエストが複数に分かれても、 上記を参照してベースライン or カナリアのロジックを判別できる さらに、バグ発見時の調査も容易になった(実際に判断しやすかった) 38

Slide 39

Slide 39 text

注文データ作成の流れ ① 39 注文リクエスト 新しいロジック 外部サービスへ redirect 古いロジック No カナリア? Yes

Slide 40

Slide 40 text

注文データ作成の流れ ② 40 新しいロジック 注文完了 古いロジック False True カナリア フラグ 外部サービスから callback

Slide 41

Slide 41 text

よし、だいたいできた? 書き込み先の変更 アプリケーションによるカナリアリリース イメージできたし、だいたいできたよね? 41

Slide 42

Slide 42 text

よし、だいたいできた? 書き込み先の変更 アプリケーションによるカナリアリリース イメージできたし、だいたいできたよね? いいえ 🙅 42

Slide 43

Slide 43 text

データ移行に立ちはだかる壁 増改築されてきた ロジック・カラム カバレッジ不足の テストコード 43

Slide 44

Slide 44 text

データ移行に立ちはだかる壁、積み重なった(積み重なっていない)歴史 増改築されてきたロジック・カラム 膨大なパターンになるロジック 増えてきたカラムの数も膨大に…… カバレッジ不足のテストコード テストカバレッジが少ない RSpec files 動いているコードが正義の状態…… 44

Slide 45

Slide 45 text

データ移行に立ちはだかる壁、積み重なった(積み重なっていない)歴史 増改築されてきたロジック・カラム 膨大なパターンになるロジック 増えてきたカラムの数も膨大に…… カバレッジ不足のテストコード テストカバレッジが少ない RSpec files 動いているコードが正義の状態…… 45 😇

Slide 46

Slide 46 text

動いているコードが正義なら 動いているコードを品質の担保にしよう 1. 移行前のコード(ベースライン)で作成した注文データ 2. 移行後のコード(カナリア)で作成した注文データ 理屈上、同じ注文データができるはず これらを比較すれば良い! 46

Slide 47

Slide 47 text

新 旧 これらを比較すれば良いのイメージ 47 注文リクエスト 注文データ作成 注文データ (古い) 作 成 注文リクエスト 注文データ作成 作 成 注文データ (古い) 注文データ (新しい) 変換 これらを比較すれば良い 変換 注文データ (新しい)

Slide 48

Slide 48 text

つまり、こういうこと(実際よりかんたんにしています) # ベースラインの注文データ作成 post '/orders', params: old_order = Order.find(response.parsed_body['id']) # カナリアの注文データ作成 create_feature_flag post '/orders', params: new_order = Order.find(response.parsed_body['id']) expect(old_order).to eq new_order 48

Slide 49

Slide 49 text

カナリアを鳴かせる用意もする リリース後のバグ検知も重要 カナリアで新しいデータ構造の注文データを作成した後、 古いデータ構造の注文データに変換する(二重書き込み) それを新しいデータ構造の注文データに再変換 再変換結果で差分が出てきたら、バグの可能性あり! エラートラッキングツールで通知をする(Sentry を使用) 49

Slide 50

Slide 50 text

つまり、こういうこと(実際よりかんたんにしています) # カナリアで注文データ作成 => 古いデータ構造に変換 old_structure_order = Converter.inverse(new_structure_order) # 差分検証 diff = Comparator.call( old_structure_order, new_structure_order, ) Sentry.capture_message(diff:) if diff.present? 50

Slide 51

Slide 51 text

これで安全にリリースできる 51

Slide 52

Slide 52 text

これで安全にリリースできる、はず? 🤔 52

Slide 53

Slide 53 text

長くなったので、ここまでのまとめ ● 新しいデータ構造を作って、二重書き込み ● 書き込み先の変更は、以下で品質を担保 ○ アプリケーションのカナリアリリース ○ 新旧データ構造の差分検証をするテスト ● リリース後のバグ検知も忘れない 53

Slide 54

Slide 54 text

移行リリースしてみて

Slide 55

Slide 55 text

移行リリースの方針:分割する単位(段階的リリース) リリースの割合を刻むのは、 適用する利用事業者の数 最初は数事業者から入れていき、 段階的に割合を増やして、全体適用を目指す 55

Slide 56

Slide 56 text

移行リリースの方針:初期→経過観察 一通りの処理(決済完了から返金まで)が カナリアで動いたことを確認 一通りが動いたかどうかについては、 新旧ロジックのどちらかを使ったのかを記録済み その注文データのフラグを見れば、 カナリア・ベースラインのどちらかがわかる 56

Slide 57

Slide 57 text

移行リリースの方針:リリース完了まで 次はランダム選定で数%まで割合を増やし、 一定期間の様子を見る あとは全事業者に適用するまで広げていく―― 57

Slide 58

Slide 58 text

失敗は? あった 🙈 カナリアに切り替えた途端、 一定時間後に動く非同期処理のパフォーマンスが悪化したり 想定していなかったエッジケースで、 カナリアで作った注文データに差分が見つかったり 58

Slide 59

Slide 59 text

失敗:でも、サービス停止せず feature flags を DB で管理していたので、 切り戻すのはフラグを折るだけですぐに終わる フラグを折ると、以降の注文は 信頼と実績のベースラインで動き、すぐに止血完了 差分が出てしまったデータはあるべき状態になるように修正 新旧の変換・逆変換ができるので、正しい側に合わせる 59

Slide 60

Slide 60 text

その後の対策 パフォーマンスの悪化 原因箇所を特定 それをチューニングして検証・再リリース 想定していなかったエッジケース エッジケースを考慮したテストを追加 周辺コードを読み直して精査・再リリース 60

Slide 61

Slide 61 text

その後 これらを繰り返して、 全部の利用事業者で、 新ロジック(カナリア)に切り替えた 🎉 61

Slide 62

Slide 62 text

まとめ

Slide 63

Slide 63 text

今日のプログラムで持ち帰れること(結論・再掲) 1. アプリケーションでやるカナリアリリース 2. 新旧データ構造の差分チェックの有用性 3. テスト不足でも安全にリリースする方法 63

Slide 64

Slide 64 text

今日のプログラムで持ち帰れること(結論・再掲) 1. アプリケーションでやるカナリアリリース ロードバランサーでは得られないメリット ● リクエスト単位とは異なる別の単位で、 意図的にリリース後の挙動に切り替えられる ● 同じコード上に feature flags で分岐しているので、 新旧ロジックを使ったテストで活用できる 64

Slide 65

Slide 65 text

今日のプログラムで持ち帰れること(結論・再掲) 2. 新旧データ構造の差分チェックの有用性 新旧データ構造を相互変換できることで得られたメリット ● 多くのカラムがあるテーブルでも、 同じオブジェクトに対する比較でまとめて検証できる ● データ構造の変換・再変換で、 おかしなデータになっていないかを差分で検証できる 65

Slide 66

Slide 66 text

今日のプログラムで持ち帰れること(結論・再掲) 3. テスト不足でも安全にリリースする方法 先述のふたつを組み合わせることで得られたメリット ● アプリケーションでやるカナリアリリースだから、 新旧ロジックの両方を同じテストで動かせる ● 新旧データ構造の相互変換ができるから、 同じオブジェクトを比較して全カラムの検証ができる 66

Slide 67

Slide 67 text

おわりに 長年運用しているサービスで扱う主要データは いつか古くなって運用に耐えづらくなり、 新しいデータ構造に置き換えたくなることがあるでしょう 今後も長くサービスを運用していくため、 いまの実態に合わせたデータ構造へ 安全に切り替えてみるのはどうでしょうか? 67