Slide 1

Slide 1 text

Update Billion Records Kaigi on Rails 2023

Slide 2

Slide 2 text

Kaigi on Rails 2023 開催 🎉

Slide 3

Slide 3 text

• ⾃⼰紹介 • 課題と前提 • レコード更新チャレンジ • 学び 写真・画像 Agenda

Slide 4

Slide 4 text

写真・画像 Hiroki Tokutomi 株式会社TimeTree • CTO室所属、Backendチーム/SREチーム https://twitter.com/talkto_me https://github.com/ta1kt0me ⾃⼰紹介

Slide 5

Slide 5 text

写真・画像 • 簡単に予定を共有できる • カレンダーの中で気軽に相談できる スマホの中で⾒れる壁掛けカレンダー カレンダーシェアアプリ

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

begin

Slide 8

Slide 8 text

写真・画像 課題と前提

Slide 9

Slide 9 text

きっかけ IUUQTUJNFUSFFBQQDPNJOUMKBOFXTSPPNTIBSFEFWFOUDMPTF

Slide 10

Slide 10 text

👦 < ちょっとモデルの関連キー⾒直したいんだよね!

Slide 11

Slide 11 text

👦 < 新キーのカラムは追加してるよ!数⼗億件あるけど! 👦 < ちょっとモデルの関連キー⾒直したいんだよね!

Slide 12

Slide 12 text

👦 < 新キーのカラムは追加してるよ!数⼗億件あるけど! 👦 < ちょっとモデルの関連キー⾒直したいんだよね! 👦 < 少し前にも全件更新したし、いけるいける!

Slide 13

Slide 13 text

👦 < 新キーのカラムは追加してるよ!数⼗億件あるけど! 👦 < ちょっとモデルの関連キー⾒直したいんだよね! 👦 < 少し前にも全件更新したし、いけるいける!

Slide 14

Slide 14 text

整理してみる • 解消する価値のある技術的負債 • 対象は1テーブル、レコード数は50~60億 • 変更内容はデータを埋めるだけ • 作業の1年前に⼤体数⼗⽇かけて同じテーブルの全データ更新を実施 • 更新作業に時間がかかることはチームで認識している • 更新期間も安定してサービスを提供したい 要求

Slide 15

Slide 15 text

• Monolith Ruby on Rails • Sidekiq • AWS • CloudFront • S3 • ECS • Aurora MySQL • DynamoDB • Elasticache for Redis • etc … Backend構成要素

Slide 16

Slide 16 text

以前はどうやってたの? 複数プロセス起動して並列に更新する • rails runner • grosser/parallel • zdennis/activerecord-import ⼤量データを⼀括で更新するバッチ処理でよく⾒かけるパターン 巨⼤テーブルでなければこのアプローチを使うことが多い • 更新対象の from/to • parallelで起動するプロセスの並列数

Slide 17

Slide 17 text

写真・画像 レコード 更新チャレンジ

Slide 18

Slide 18 text

Take 1. 前例踏襲

Slide 19

Slide 19 text

過去のアプローチに従ってみる • 複数プロセス更新での実⾏時間を計測したい • アプローチの課題を理解したい

Slide 20

Slide 20 text

結果 • 実⾏時間は約70分/1,000万件 • サービスへの影響はなかった • この時点で⼤きめのパフォーマンスチューニングはしていない 対象は⼤体500~600倍、実⾏し続ければ1ヶ⽉弱…?

Slide 21

Slide 21 text

⾒えてきた課題 更新対象の指定で処理時間のブレが⼤きい サービス成⻑に起因(後述) 更新処理をコントロールしづらい 実⾏状況を外部からモニタリングしづらい ⻑時間実⾏し続けることが難しい、けど⻑時間実⾏し続けたい • 環境の都合上リリースの度に中断 • スパイクが予測される状況で中断

Slide 22

Slide 22 text

課題を深掘り 更新対象の指定 • 処理対象のfrom/to にテーブルのidカラム(PK)の値を指定 • Idに Snow fl ake ID を利⽤ • 64ビットの整数値、timestampに基づいて⽣成される時系列ソート済みのID • Twitter の TweetのIDで利⽤されていた • TimeTreeの場合、timestampにcreated_atを使っている • 1億件分更新したい場合、⼤体1億件分の期間の from, to をこのフォーマットに変換して指定 + ————— —————— ——— ——— — —— — —— — —— ——— — —— ——— — —— — —+ | timestamp (41ビット) | ゾーンID (10ビット) | シーケンス番号 (13ビット) | + ————— —————— ——— ——— — —— — —— — —— ——— — —— ——— — —— — —+ 例)timestampに “2020-01-01 00:00:00.000 UTC”、ゾーンIDに”10”、シーケンス番号に”1”を指定 10110111101011110011001101110100000000000 0000001010 0000000000001 => 13235854403174481921

Slide 23

Slide 23 text

更新期間の指定のムラ

Slide 24

Slide 24 text

改善アイデア 更新対象の指定で処理時間のブレが⼤きい from/toを⼀定の期間で分割、分割期間ごとに並列で処理する 更新処理をコントロールしづらい 更新処理をSidekiq Workerで処理する⾮同期ジョブにする • rails runner の実⾏環境の制限に依存しない • 実⾏状況のモニタリングも容易

Slide 25

Slide 25 text

期間の分割⾒直し Ұఆͷظؒ5ͰJEΛ෼ׂ ىಈ#BUDIͷύϥϝʔλͱͯ͠ࢦఆՄೳ ظؒ5ΛฒྻͰॲཧ Take 1 改善案 ฒྻ਺ͰJEͷൣғΛ෼ׂ

Slide 26

Slide 26 text

Take 2.

Slide 27

Slide 27 text

Take 2 アイデア • 更新対象の from/to • 分割期間

Slide 28

Slide 28 text

Take 2 アイデアちょっと待って • 更新対象の from/to • 分割期間

Slide 29

Slide 29 text

⾮同期で更新 前提 • 通常のワークロードより優先度低 • データベースの負荷を抑えたい • 全件更新に時間がかかることは問題ない 制御したいこと • workerあたりの実⾏時間 • Sidekiqクラスター全体での更新Job数 • 更新処理を全て停⽌するトリガー

Slide 30

Slide 30 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ②from/toの分割リストをRedisにpush ④更新対象のfrom/toを pop、なければ終了 ⑤更新 ③⾮同期Jobを登録 ⑥⾮同期Jobを登録

Slide 31

Slide 31 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数

Slide 32

Slide 32 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ②from/toの分割リストをRedisにpush

Slide 33

Slide 33 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ②from/toの分割リストをRedisにpush ③⾮同期Jobを登録

Slide 34

Slide 34 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ④更新対象のfrom/toをpop(   ) ③⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 35

Slide 35 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ④更新対象のfrom/toをpop ⑤更新 ③⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 36

Slide 36 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑤更新 ⑥⾮同期Jobを登録 ③⾮同期Jobを登録 ④更新対象のfrom/toをpop ②from/toの分割リストをRedisにpush

Slide 37

Slide 37 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑤更新 ⑥⾮同期Jobを登録 ③⾮同期Jobを登録 ④更新対象のfrom/toをpop ②from/toの分割リストをRedisにpush

Slide 38

Slide 38 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑦更新対象のfrom/toをpop(  ) ⑤更新 ③⾮同期Jobを登録 ⑥⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 39

Slide 39 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑦更新対象のfrom/toをpopできない ⑤更新 ③⾮同期Jobを登録 ⑥⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 40

Slide 40 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑧分割リストのデータを削除 ⑤更新 ③⾮同期Jobを登録 ⑥⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 41

Slide 41 text

全体像 ①rails runnerで起動処理実⾏ • 更新対象の from/to • 分割期間 • Jobの同時起動数 ⑧分割リストのデータを削除 ⑤更新 ③⾮同期Jobを登録 ⑥⾮同期Jobを登録 ②from/toの分割リストをRedisにpush

Slide 42

Slide 42 text

コンセプト: 起動処理

Slide 43

Slide 43 text

コンセプト: 更新ジョブ

Slide 44

Slide 44 text

結果 実⾏時間 実⾏時間は約40分/1,000万件(変更前の175%⾼速化) 実⾏し続ければ約2~3週間強 Take.1 の課題 実⾏中もリリース可能、問題があれば全処理停⽌可能 カスタマイズせず、Sidekiqの管理画⾯やAPMのメトリクスで実⾏状況を確認できる Workerごとの実⾏時間もパラメータによりある程度調整可能

Slide 45

Slide 45 text

早くなった、楽できた、はっぴー🎃

Slide 46

Slide 46 text

やりきった 🎉

Slide 47

Slide 47 text

写真・画像 学び

Slide 48

Slide 48 text

学び ⼤量データの変更には時間がかかる • ⻑期戦になるので関係者の理解が必要 • ⼩さなデータで簡単なことでも、後に回せば回すほどもっと⾟くなる ⼩さく試して進める • PoCのフィードバックサイクルを素早く回す • 可能であれば⼩さいデータセットから始め、少しずつ⼤きくして問題を捉えていく 選択肢を広げるためのinputの⼤切さ • 改善のヒントは様々な場所に潜んでいるので広く、深く探す • 前例や背景、変更対象や関連のドメインの把握、制限やトレードオフの理解、少数の異常データの存在

Slide 49

Slide 49 text

他の全件更新に適⽤できるか? 🙅 or 🙆 • 🙆 適⽤できたケースもある • 😰 変更対象が今回と同じでもさらにデータが増え続けたら… • 🙅 READよりもWRITE(更新)が多いケースはLockが多発して使えなかった • 🧐 変更内容や対象の特性次第で前例に捉われずに考え続ける

Slide 50

Slide 50 text

end

Slide 51

Slide 51 text

⼀緒に最⾼のカレンダーサービス作りませんか?