Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
updated_at に依存したら ⼤変なことになった megane42 / Hikaru Kazama @ giftee 2019/11/28 gotanda.rb
Slide 2
Slide 2 text
免責 このスライドに載せたコードや スキーマは⼀部簡略化しています このしくじりによるトラブルは、今はすべて解消しています
Slide 3
Slide 3 text
第 I 部 : 背景
Slide 4
Slide 4 text
弊社 giftee デジタルギフトを作って売っています
Slide 5
Slide 5 text
プロダクト giftee campaign platform フォロー / RT すると抽選でギフトをプレゼントします
Slide 6
Slide 6 text
機能紹介 実績集計機能 管理画⾯から ⽇ごとの抽選者数 がわかる
Slide 7
Slide 7 text
create_table "entries" do |t| # ... t.bigint "campaign_id", null: false t.string "lottery_status", default: "fresh" t.datetime "created_at", null: false t.datetime "updated_at", null: false # ... end
Slide 8
Slide 8 text
class Entry # ... enum lottery_status: [ :fresh, :winner, :loser ] def draw! 抽選処理 ? winner! : loser! end def self.daily_drawers group("DATE(updated_at)").count end # ... end
Slide 9
Slide 9 text
当時の実装 抽選を回すと entry の updated_at が更新される 逆に、それ以外に更新される機会はない じゃあ updated_at を使って ⽇ごとの抽選者数 を集計しよう 後述しますが、 これ⾃体はしくじりじゃない と思ってます
Slide 10
Slide 10 text
第 II 部 : いよいよしくじります
Slide 11
Slide 11 text
悲劇はこの⽇起きた 2018-09-26 とある⼤型メンテの⽇ entries テーブルにカラムを追加 entries テーブルのレコード全体にデータ遡及が必要
Slide 12
Slide 12 text
Entry.winners.each do |e| e.update(new_column: "foo") end
Slide 13
Slide 13 text
+---------+----------------+---------------------+---------------------+ | id | lottery_status | created_at | updated_at | +---------+----------------+---------------------+---------------------+ | 5 | winner | 2017-08-04 02:37:51 | 2018-09-26 01:53:54 | | 20 | winner | 2017-08-04 12:55:04 | 2018-09-26 01:53:54 | | 42 | winner | 2017-08-04 13:57:21 | 2018-09-26 01:53:54 | | 50 | winner | 2017-08-04 14:07:08 | 2018-09-26 01:53:54 | | 93 | winner | 2017-08-04 14:55:32 | 2018-09-26 01:53:54 | | 109 | winner | 2017-08-04 15:08:01 | 2018-09-26 01:53:54 | | 121 | winner | 2017-08-04 15:18:09 | 2018-09-26 01:53:54 | | 137 | winner | 2017-08-04 15:28:05 | 2018-09-26 01:53:54 | | 147 | winner | 2017-08-04 15:36:19 | 2018-09-26 01:53:54 | | 177 | winner | 2017-08-04 15:56:18 | 2018-09-26 01:53:54 |
Slide 14
Slide 14 text
しくじり データ遡及⽤スクリプトで updated_at を考慮していなかった その結果、ほとんどのレコードの updated_at がメンテ時刻に更新 された 実績が壊れた 2018-09-26 の当選者が⼤量に発⽣
Slide 15
Slide 15 text
第 III 部 : 対応
Slide 16
Slide 16 text
⼀次対応 created_at で代⽤ ほとんどの場合 created_at と updated_at は数秒の差しかない entry レコード作成直後に抽選を実⾏しているから
Slide 17
Slide 17 text
恒久対応 drawed_at カラムを新設 抽選実⾏時に時刻を埋める 既存のレコードに関しては created_at の値をコピー
Slide 18
Slide 18 text
class Entry # ... def draw! + transaction do 抽選処理 ? winner! : loser! + update!(drawed_at: Time.zone.now) + end end # ... end
Slide 19
Slide 19 text
と簡単に⾔うけれど entries テーブルにカラムを増やすのはかなり⼤変 レコード数が多い n000 万のオーダー 常にレコードが増え続けている マイグレーション中にロックがかかると困る ⼆次被害を避けるために、念⼊りなリハーサルを実施
Slide 20
Slide 20 text
第 IV 部 : 教訓
Slide 21
Slide 21 text
何がしくじりだったのか? はじめから drawed_at のような専⽤カラムを作らなかったこと? メンテ時に updated_at を考慮し忘れたこと?
Slide 22
Slide 22 text
トレードオフ もし drawed_at を⽤意するなら: 抽選処理の実装時に、 drawed_at を埋める処理を忘れずに書く必 要がある データ遡及メンテのときは、何も考えなくてよい else ( updated_at に依存するなら): 抽選処理の実装時には、何も考えなくてよい データ遡及メンテのときに、 updated_at を更新してしまわない か気にする必要がある
Slide 23
Slide 23 text
トレードオフ(抽象化) もし専⽤カラムを作るなら: 定常的な開発時にひと⼿間かかる 突発的なメンテ時に何も考えなくてよい else ( updated_at に依存するなら): 定常的な開発時に何も考えなくてよい 突発的なメンテ時にひと⼿間かかる
Slide 24
Slide 24 text
個⼈的な意⾒ 突発的なメンテ時の⽅が、慌てていることが多い 突発的なメンテ時に何も考えなくてよい⽅がうれしい 基本的には専⽤カラムを⽤意した⽅がよさそう
Slide 25
Slide 25 text
別の観点からの教訓 知識に経験が伴うと⼈は強くなる created_at updated_at とは別に専⽤カラムを⽤意する流派があ ること⾃体は知っていたが、「なぜそうするのか」まではわかっ ていなかった 今は、 ⾔葉ではなく⼼で理解できた わけもわからず従っているベストプラクティスがまだまだある きっとそれらにも理由がある
Slide 26
Slide 26 text
まとめ updated_at に依存したコードを書いている⼈は、突発メンテ時に ⼗分気をつけましょう 不安な場合は専⽤カラムを作りましょう たくさん経験を積んで or 共有しあって強くなっていきましょう
Slide 27
Slide 27 text
updated_at に依存したら ⼤変なことになった megane42 / Hikaru Kazama @ giftee 2019/11/28 gotanda.rb
Slide 28
Slide 28 text
おまけ : DB メンテのリハーサル中に得た知⾒
Slide 29
Slide 29 text
nullable なカラムを追加している最中でも INSERT できる (Aurora (MySQL)) MySQL にはオンライン DDL という機能があり、ALTER TABLE 中 に更新系のクエリが実⾏できる https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl- operations.html MySQL 互換をうたっている Aurora は、その辺も互換性あるの? やってみたら追加できた !!! 必ずご⾃⾝の環境でも確認してください !!!
Slide 30
Slide 30 text
テーブル全体を UPDATE したら徐々にロック された (Aurora (MySQL)) UPDATE entries SET drawed_at = created_at WHERE entries.drawed_at IS NULL 上記の SQL を実⾏すると、 entries テーブルが ID : 1 から徐々にロ ックされていった 更新ができないだけでレコード新規追加はできる 対象レコードを「ID 1 から 100 万まで」のように絞りながら⼩刻み に実⾏していくことで、影響を最⼩化できる !!! 必ずご⾃⾝の環境でも確認してください !!!