Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
2年前に削除したPHPクラスが、 ある日突然決済をエラーにした
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
ykagano
July 20, 2026
Technology
210
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
2年前に削除したPHPクラスが、 ある日突然決済をエラーにした
2026/7/20 PHPカンファレンス2026 発表資料
ykagano
July 20, 2026
More Decks by ykagano
See All by ykagano
決済システムの信頼性を支える技術と運用の実践
ykagano
0
1.2k
Copilot Agentを普段使いしてわかった、バックエンド開発で使えるTips
ykagano
1
2.3k
プレイングマネージャーになったときの話
ykagano
2
1.5k
WEBエンジニアが知っておきたい決済の仕組み
ykagano
2
3.9k
プロジェクトにおけるリーダブルコードの考え方
ykagano
2
2.2k
GWにスマートスピーカーアプリを作ってみた
ykagano
1
1.3k
開発効率を上げるSwaggerの話
ykagano
0
1.5k
Other Decks in Technology
See All in Technology
「守りたい体験」を渡すだけで E2E を生成させられるようになった話
hinac0
0
120
Genie Ontologyは銀の弾丸かを考える / Is Genie Ontology a Silver Bullet?
nttcom
0
370
ボーイスカウトルールでメモリやスキルを改善しよう
azukiazusa1
4
1.4k
kintone の AI コワーカーを、 Anthropic にエージェントを"ホストさせて"作った話 #devkinmeetup
sugimomoto
0
110
公式ドキュメントの歩き方etc
coco_se
1
120
SRE本の知られざる名シーン / The Hidden Gems of Google SRE Book
nari_ex
1
410
生成AI×AWS CDK×AWS FISで"振り返れる"ミニGameDayをつくろう
yoshimi0227
1
270
Empower GenAI with Agile - あなたのアジャイルが生成AIのバフになる仕組み
hageyahhoo
1
210
Gen3R: 3D Scene Generation Meets Feed-Forward Reconstruction
spatial_ai_network
0
130
Amplify Gen2でbackend.tsにCDKを定義する/しない事によるCDKの挙動の違いとユースケース
smt7174
1
330
型は壁、Rustでもバグを直すな、表現できなくせよ
nwiizo
14
2.1k
AI時代のYAGNI:「爆速で無駄になった機能」からの学び / 20260720 Naoki Takahashi
shift_evolve
PRO
2
230
Featured
See All Featured
SERP Conf. Vienna - Web Accessibility: Optimizing for Inclusivity and SEO
sarafernandez
2
1.5k
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
460
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
650
KATA
mclloyd
PRO
35
15k
More Than Pixels: Becoming A User Experience Designer
marktimemedia
3
460
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
970
The SEO Collaboration Effect
kristinabergwall1
1
500
Into the Great Unknown - MozCon
thekraken
41
2.6k
Google's AI Overviews - The New Search
badams
0
1.1k
Side Projects
sachag
455
43k
Designing Powerful Visuals for Engaging Learning
tmiket
1
450
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Transcript
© 2012-2026 BASE, Inc. 2026/7/20 PHPカンファレンス2026 2年前に削除したPHPクラスが、 ある日突然決済をエラーにした 1 @ykagano
2 © 2012-2026 BASE, Inc. 氏名:加賀野 祐(ykagano) 所属:BASE株式会社 BASE Department Product
Division 役割:シニアエンジニア(バックエンド) 趣味:お酒、旅行、キャンプ 2024年8月にBASE株式会社に入社 その前は以下の開発を経験してきました - 2009年〜:NET CASH - 2012年〜:WebMoney - 2017年〜:Yahoo!ウォレット、PayPay ykagano 自己紹介
3 © 2012-2026 BASE, Inc. アジェンダ • ある日突然、決済がエラーに • なぜ2年後に突然爆発したのか
• どう修正したのか
© 2012-2023 BASE, Inc. ある日突然、決済がエラーに 4
5 © 2012-2026 BASE, Inc. ある日、Sentryが鳴った 普段見ないエラーだった Sentry調査用のBotから AIに調査してもらった
6 © 2012-2026 BASE, Inc. 購入失敗は2件だけだった BASEはECなので、購入失敗は少ない件数でもサービスの根幹に関わる 絶対にケアが必要 → インシデント扱い
同じ原因で何度もエラーが起こるかもしれないので、Slackのインシデントチャンネルを立てて共有 何が起きていたのか エラーはどこで起きていたのか DBに保存された 詳細データ をPHPのクラスに復元する処理 クラスの存在チェックに失敗して LogicException → 決済フローが停止
7 © 2012-2026 BASE, Inc. MukashinoDetail 犯人は「2年前に削除したクラス」 2024 昔のコードの一掃 クラス削除
2年間の沈黙 エラーなし ・ 誰も気づかない ! 2026 突然の発火 決済エラー LogicException 削除したとき問題は何もなかった • grepで参照ゼロを確認 ✓ • テストはすべて通過 ✓ • コードレビューも通過 ✓ • リリース後もエラーなし ✓ それでも「時限装置」は仕掛けられていた
© 2012-2023 BASE, Inc. なぜ2年後に突然爆発したのか なぜ2年後に突然爆発したのか 8
9 © 2012-2026 BASE, Inc. 決済が失敗した pending と同じ keyである failed
の行には、クラス名がシリアライズされたまま残っていた DBには「遺物」が眠っていた key status detail 753190E46D820159 2023年・昔の決済 failed {"class": "MukashinoDetail"...} ← クラスは削除済み・復元できない 753190E46D820159 2026年・新規の決済 pending {"class": "ShinkinoDetail"...} 同じキーなのに並存できる (key は非UNIQUE) DBの中に眠る「遺物」 コードを消してもデータは残る 新規の key は作成時に以下の仕様で作られていた • 完了した決済なら key は重複しない • 失敗した決済は key の重複を許容する ← 今回はこのパターン
10 © 2012-2026 BASE, Inc. これまで新規の key 作成時に、削除済みのクラスと同じ key を引き当てることはなかった
今回が初めての事象 単独では誰も悪くない実装が時間差で噛み合って「時限装置」になった 発火の瞬間に対象のkeyの検索が「壊れた行」まで一緒にロードしてしまった 2年越しのキー衝突 1 決済処理 keyで検索 2 同一 key の 全行をロード 3 クラスに 復元を試みる 4 クラス存在 チェック → 失敗 5 決済エラー ⚠ 削除済みクラスを参照 する行も混入 購入失敗
11 © 2012-2026 BASE, Inc. key の生成には str_shuffle() を使用している エラーが発生する8日前のリリースで、key
生成より前に mt_rand() を消費するコードが購入経路に入り、 乱数消費位置の分布が過去と重なった(テープの同じ位置から key を引き始めた) これにより衝突確率が跳ね上がったと想定される(不具合ではなく、DBでの key の重複は仕様通り) キーの衝突がなぜ突然起きたか 過去のリクエスト 83 17 52 96 41 08 75 29 63 34 90 12 57 … リリース前 83 17 52 96 41 08 75 29 63 34 90 12 57 … リリース後(今回) 83 17 52 96 41 08 75 29 63 34 90 12 57 … 事前に消費 key生成に使用(str_shuffle) key生成に使用 → 開始位置がズレていて衝突しない 新コードが消費(mt_rand) key生成に使用 str_shuffle() と mt_rand() は同じ seed(初期値) なら毎回同じ乱数列(=同じテープ)を返す テープの同じ位置から同じ並びを引く → 同じ key が生成される(衝突) 同じ seed
12 © 2012-2026 BASE, Inc. grepが探せるのは「コードの参照」だけ テストが検証するのも「コードの動き」だけ DBに残るクラス名入りのデータは誰も見ていなかった さらに発火には低確率のキー衝突が必要 →
だから2年間、沈黙していた コードは消したが、データは消えていなかった
© 2012-2023 BASE, Inc. どう修正したのか 13
14 © 2012-2026 BASE, Inc. status でDBにフィルタをかけるようにした 壊れた行をそもそもロードしない Before DBから全行を取得
PHP側で除外 ⚠壊れた行もロードされて爆発 $details = $this->detailMapper->find(key: $key); $unfailedDetails = array_values(array_filter($details, function (Detail $detail) { return $detail->getStatus()->includedIn(Status::unfailed()); })); After DBクエリの段階で failed を除外 必要な行だけ取得 ✓壊れた行はそもそもロードしない $details = $this->detailMapper->find( key: $key, statuses: Status::unfailed(), );
15 © 2012-2026 BASE, Inc. 今回の事象は、2年前に削除したクラスが、DBにシリアライズされて残っており、 新規の決済が同じ key を引き当てたことで、過去データの復元に失敗し、決済がエラーになったものです 「もう使われていない」をgrepだけで判断していませんか
コードを消してもDBのデータは残る クラス名をシリアライズして保存する設計は時限装置になりうる 削除するときは「コードの参照」だけでなく「データの参照」も確認しましょう 皆さんの現場でも、同じ時限装置が眠っているかもしれません まとめ
16 © 2012-2026 BASE, Inc. 以上です ご清聴ありがとうございました!! BASE社の採用情報: https://binc.jp/jobs