- 収益化を始めたばかりのスタートアップがサブスクリプション決済の障害に見舞われたが、社内では再現できず、5日間にわたり原因特定が遅れた
- 障害は、ChatGPTが作成したPrisma/TypeScript→Python/SQLAlchemyの変換形式をコピーした際、UUID生成関数の代わりにハードコードされたID文字列がデフォルト値のように入っていたことから始まった
- AWS ECSタスク8個と各5インスタンスという構成のため、ユーザーは最大40個のユニークIDプールのいずれかに当たり、日中は頻繁なデプロイによって問題が隠れていた
- 夜間にデプロイが止まると各サーバーの単一IDが使い切られ、その後の新規サブスクリプション試行はunique IDの衝突で失敗した
- 毎日50件の苦情、5日間、月額40ドルのサブスクリプション料金を基準に、損失は月間10,000ドルと推定され、テスト・ロギング・アラートの不在とコードコピーが障害対応を大きくした
収益化直後に露呈したサブスクリプション障害
- スタートアップは5月に初めて収益化を有効にし、リリースから1時間以内に最初の顧客を獲得した
- 翌朝、Gmailには40件を超えるユーザーからの苦情がたまっていた
- ユーザーはサブスクリプションを完了できなかった
- サブスクリプションボタンを押すと無限に回るローディングスピナーが表示されると知らせてきた
- 新しいアカウントを作って自分たちで確認したが、社内ではサブスクリプションが正常に動作し、原因を再現できなかった
- 業務時間中は苦情がほとんどなく、障害は主に夜の間に蓄積した
時間的プレッシャーの中での収益化実装
- 5月はYC S23バッチが始まった時期で、チームはリリース後にどの方向が最適なのか確信を持てずにいた
- YCのグループパートナーであるDaltonは、有料サブスクライバーを方向性を決める指標にし、想定していた月額価格を2倍に上げるよう助言した
- 最終価格は月額40ドルに決まった
- プロジェクトはもともとフルスタックのNextJSだったが、収益化作業の前後でPython/FastAPIへの移行を進めた
- 移行過程でChatGPTを活用した
- Stripe連携も完了した
- その後5日間、睡眠時間は大きく減り、毎日30〜50件の苦情メールに対応しなければならなかった
ChatGPTが作ったモデル変換形式
- バックエンドのマイグレーション過程で、データベースモデルをPrisma/TypeScriptからPython/SQLAlchemyへ移した
- モデル変換は退屈で、ChatGPTがうまくこなすと判断し、ほぼ全体のマイグレーションに使用した
- 生成されたコードはコピー&ペーストした後に動作確認を行い、本番環境でも正常に見えたため、そのまま進めた
- 当時、データベースへの挿入は依然としてNext APIが担当しており、Pythonバックエンドはデータベースを読み取るだけだった
- サブスクリプション機能を実装する中で、初めてPythonからDBレコードを挿入し始めた
- 新しいSQLAlchemyモデルは自分で作ったが、既存モデルでChatGPTが作った形式をそのままコピーした
- すべてのモデルのID生成方式に同じ問題が入り込んだ
実際の原因と、なぜ日中は見えなかったのか
- 核心的なエラーは、UUIDを生成する関数やラムダを渡さず、単一のハードコードID文字列を渡していた点だった
- 特定のバックエンドインスタンスであるユーザーがそのIDでサブスクリプションを完了すると、同じインスタンスでの以降のサブスクリプション試行はユニークIDの衝突を起こした
- バックエンド構成が、この問題をさらに長く隠した
- AWSでECSタスクを8個運用していた
- 各タスクはバックエンドインスタンスを5個実行していた
- ユーザーは潜在的に40個の異なるIDのいずれかに到達し得た
- 日中はmainブランチに1日10〜20回直接コミットし、そのたびに新しいバックエンドのデプロイが発生した
- デプロイが起きるたびに、顧客が使える新しいIDが40個生まれた
- 夜間はコミットとデプロイが止まり、各サーバーの単一IDが急速に使い切られた
- 最初はサブスクリプション可能なサーバーが40個近くあったが、時間が経つにつれてほぼ0個に近づいた
損失規模とその後の対応
- 損失は
50 emails/day x 5 days x $40/month と計算し、月間10,000ドルの売上損失と推定した
- この計算は苦情を送ったユーザーのみを基準にしている
- 原因を見つけるまでに5日、多数のメール、数百件のSentryログ、Stripeエンジニアとの長いDiscordでの会話、5つの重要ファイルのレビューが必要だった
- 原因を発見した後、Adamがすぐに修正を上げた
- その後、強力なユニット/統合テスト、アラート、ロギングを追加した
- この出来事は、人為的ミス、不十分なテスト、コードコピー、mainへの直接プッシュが重なると、たった1行でも大きな売上損失につながり得ることを示している
2件のコメント
えっ、AIによる自動生成コードは必ずレビューすべきでしょう。それをなぜそのまま使うんですか。
Hacker News の意見
モニタリングの不在が1万ドルを吹き飛ばしたということ。アプリがデータベース例外を継続的に、大量に出していたのに、誰にも通知が届いていなかった
そうした通知があれば、5日間の調査ではなく5分間の調査で終わっていたはず。通知体制を直していないなら、実際には何も直していない
すべてがうまく動いているときのプログラミングは簡単で、問題を処理する部分が難しい
有料顧客が入ってくる瞬間から、ロギング、モニタリング、通知、セキュリティなどを扱う知識と経験のある人が必要。DevOpsを素人のように扱ってはいけない
だが、データベースにエラーロギングと通知がないというのが本当に狂っている部分。これは20年もののレガシーコードではなく新製品で、DBエラーをデータ検証のように使っていた時代のコードでもない
ブログ記事が404になっているので、Web Archiveのリンクを残しておく
https://web.archive.org/web/20240610032818/https://asim.bear...
著者が重要な追記をしていた。ここでの慣行は非常に悪く、恥ずかしいレベルであり、その後、堅牢な単体/統合テストと通知/ロギングを追加したとのこと。結局は人間のミスで、振り返れば明らかに避けられた出来事だったという内容
また、会社の創業初期の数週間、大きな時間的プレッシャーの中で起きたことで、本番環境でのバグ再現性が特異だった面白い話として見てほしい、とも付け加えていた
愚かなミスではあるが、人間は個人であれ集団であれ愚かなミスをするものだ
https://0912i390129ionkjan.bearblog.dev/how-a-single-chatgpt...
https://webcache.googleusercontent.com/search?q=cache%3Ahttp...
エラーはすぐに見えていた。チームへの敬意はあるが、これはChatGPTとはあまり関係がなく、チームが十分に慣れていないプログラミングモデルを使ったことに大きく関係している
たとえコードレビューを通っていたとしても、5分で設定できる監視ツールがあるだけで検出できた可能性が高い
「LLMの使用中にプログラミングエラーを起こし、品質保証をしなかったため1万ドルかかった」というタイトルでは、経営陣の「ChatGPTがやらかしたら、うちのエクスポージャーはいくらになる?」といった反応は引き出せない。LinkedInにこの記事を投稿する中間管理職や上級管理職は非常に多いだろう
LLMは「ミス」をすることはできない。決定論的ではなく、推論したり考えたり論理を実行したりすることもできない。統計的確率を使う、非常に派手な単語サラダ生成器であり、生成物が正しい、または正確である保証はないので、定義上「ミス」という表現も適切ではない
追記: 記事は明らかな理由で大きくダウンボートされた後、順位が突然跳ね上がっており、これはモデレーターがブーストしたことを意味しているように見える: https://hnrankings.info/40627558/
ルール上タイトルを変えるべきレベルのクリックベイト記事がモデレーターにブーストされたのは笑える。しかも投稿者がY Combinator企業の所属らしいのも、完全な偶然なのだろう: https://news.ycombinator.com/item?id=40629998
主キーのUUID生成は
str(uuid.uuid4())ではなく、呼び出し可能なuuid.uuid4を直接使うべきで、SQLAlchemyが値を作るときに関数を呼び出すとのことだった。日付のデフォルト値はserver_default=text("(now())")が期待どおりに動作しない可能性があるのでfunc.now()を使うように言い、uuidとSQLAlchemyのtextのインポートも確認するように述べ、タイムゾーン処理のためにDateTime(timezone=True)も検討するように言っていたその後、修正コードとして
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), unique=True, nullable=False)を提示し、ここでlambda:を追加したことが問題を直したuuid.uuid4()をPrismaのようなところのスキーマ定義のように考えたのだと思う。なので、このバグ自体は驚きではなく、自分も同じミスをしていたかもしれないそれでも
kubectl logsを一度実行すればすぐに見つけられたはずだ。しかもNext.jsとPrismaからPythonに行くって? なぜ?ミス自体は理解できる。ChatGPTなしでコードを書いても、比較的簡単に見落としそうに見える
しかし、最初の失敗の後になぜ見つからなかったのかは理解できない。この会社にはロギングがなかったのか? バックエンドがUUIDを再利用しようとしているという事実は、エラーを見ればすぐに分かったはずだ
Stringの代わりに呼び出し可能オブジェクトを渡すべき関数に、誤って文字列を渡してしまうことはよくある。ORMを使っていなければこの特定の問題は防げたと思うが、ORMに対する個人的な偏見かもしれない。データベース以外の文脈でも似たようなバグは十分起こり得るこのバグを確実に捕まえられたはずだと大言壮語している人たちは、私よりはるかに優れたエンジニアなのか、より可能性が高いのは、自分の能力について少し勘違いしているのだと思う
ただし、ログがない、またはログを見ていないことは本当に理解しがたい。ECSならDuplicate Key例外は追加設定なしでCloudWatchに伝播していたように思うが、そうならなかったのか、なっていたのに夜通しどんな例外が起きたのか誰も確認しなかったのかが気になる
このような状況では、なぜ検知が遅れたのか、なぜ診断に時間がかかったのかを問うことが有用だ
人間が書いたコードでも同じミスを何度も見たことがある。特に React / TypeScript / JavaScript では、誰かがラムダを書き忘れることがよくある
ブログ記事は問題の根本原因をきちんと説明せず、すぐに ChatGPT のせいにしているように感じる。急いで作業し、大きな変更や同僚レビューのないコミットを main に入れると、こういうことが起きる
本当の問題は、急ぎ、近道を選び、十分なテストと同僚によるコードレビューをしなければエラーが起きるということだ。複数の登録オプションを試すテストがあるだけでも、すぐに見つけられたと思う
こういう人を財務的に重要なコードの近くに置けば似た問題が起きるだろうし、テストもほとんどなしにそのコードをデプロイすると決めた人の判断を疑ってしまう
このケースに対する lint ルールがなかったというのは驚きだ
そうでなかったことを願う
「元のプロジェクトはフルスタックの NextJS だったが、まずすべてを Python/FastAPI に移行したかった」という部分には目が覚めた
顧客もいないスタートアップがどうやって書き直しを正当化するのか分からない
顧客の有無に関係なく、こんな早い段階で、実質的に Node から Python への横移動をなぜするのか分からない。顧客が数百人いて Go などに変えようとしている状況ならまだ理解できるかもしれないが、それでもやはり疑問だ
たとえば DTO オブジェクトを大量に作らなければならないのに、AutoMapper は自分が使っているバージョンの組み合わせとプロジェクト設定では動かず、Entity Framework と JSON のシリアライズ/デシリアライズも、得られるものより苦痛のほうが大きい
もちろん段階的に解決することはできる。ドキュメントを深く掘り、ハックを混ぜ、パッケージをアップグレードし、設定を書き直す、といった形で可能だ。だが人間なら、比喩的なガソリン缶を持って全部燃やし、2つ目のシステムをもっと良くしたくなる。もちろん実際に良くなるのではなく、別の痛点が生まれるだけで、最初のシステムがしていたことを全部できなかったり、まともにできなかったりすることもある
職場でもレガシーや面倒なシステムを見るたびに同じ衝動に駆られる。書き直せと叫ぶ脳に勝つには、積極的で継続的な努力が必要だ。たまに書き直しやコンテナ導入のようなアーキテクチャ変更がうまくいくこともあるが、たいていは火の中に飛び込むか、終わりのない作業につながる
システム運用や他の開発者の開発者体験を改善できるという高い確信がある場合を除けば、その衝動に屈しないほうが幸いだ
ChatGPT はむしろアプリが稼いだお金を生み出した側だ。ChatGPT なしでは実装する能力がなかったからだ
コーディング、デバッグ、ロギング、モニタリングができなかった能力不足が1万ドルを吹き飛ばしたのであり、この話における ChatGPT の純効果はプラスだ
すべてのコミットメッセージに絵文字が入っている。サル、バナナ、ロケット、花火など、ないものはない
https://grook.ai/share?id=e269e88a7b1a71eff4f176c864b30161&x...
元はフルスタックの NextJS で、バックエンドを Python/FastAPI に移行する中で、Prisma/Typescript のデータベースモデルを Python/SQLAlchemy に翻訳していたという。この作業が退屈で、ChatGPT がかなりうまくやると見て、ほぼ全体の移行に使ったという内容だ
そもそも ChatGPT がなければこの事前移行を試みなかったはずなので、純効果がプラスだとは見なしにくい。既存スタックにはより良いエラーロギングがあったかもしれないし、なかったかもしれない。また、自分で書いたコードなので構造をよりよく把握していて、その必要性が低かったかもしれない
収益化を有効にする前に「すべてのコードをもう一度書き直す」という判断自体も興味深くはある
「ここのプラクティスが悪く、避けられたことだと最初に言っておきたい。大きな時間的プレッシャーがあった別の時期のことだ。この点を踏まえて読んでほしい」という文言がある
こういう制約条件のせいで、ソフトウェアのサブスクリプションは怖い
競合状態のせいでユーザーに二重請求したこともある。だからお金に関係するタイムアウトやエラーを見ると、とりあえず決済は通ったと仮定して、あとで再確認するくらい偏執的になった
TypeScript と Python のコード、Next.js のようなフレームワーク、AWS タスク 8個でそれぞれインスタンスを5個動かしていながら、売上は40ドル、開発期間は数週間だけだったって?
いったい何が起きているのかと思う。時間の制約でコードがひどかったと直したと言いながら、実際には言語をまたいだリファクタリングと、何の理由もない分散システム構築に時間を使っていた点がさらに悪い。
機能とばかげた技術的複雑さを同時にジャグリングした、自傷的な複雑さだ。何を考えていたのか分からない。
追記: YC 2023年夏の会社なのに、2024年夏になってもプロダクトはまだウェイトリストの後ろにあるようだ。たぶん Rust で書き直しているからだろう。
Python 全体で1000行も書いたことがなさそうなのに、問題をきちんと見つけている。
Python には、Common Lisp の評価戦略を正しく真似できていない欠陥がある。省略可能な関数引数のデフォルト値式に
foo=obj.whatever()のような引数があると、obj.whatever()は関数呼び出し時ではなく、関数定義が処理される時点で評価される。効率のために意図的にそうしたのではないかと思う。Python にはもう一つ欠陥があり、リストのような一般的なオブジェクトに対する本物のリテラル構文がない。
[1, 2, 3]はリテラルではなくコンストラクタに近く、評価されるたびに新しいリストを作って値を詰める必要がある。設計者は、
list=[]のようなパラメータが引数を省略されるたびに新しい空リストを作るようにはしたくなかったのだろう。Lisp では'(1 2 3)と'()は本物のリテラルで、参照されるたびに同じオブジェクトを指す。プログラマはデフォルト値式として(list 1 2 3)を使うか'(1 2 3)を使うかを選べる。前者は
[1, 2, 3]のように毎回変更可能な新しいオブジェクトを作り、後者はほぼ確実に同じオブジェクトを返し、信頼性があり移植可能な形では変更できない。現代の人気言語は Lisp の機能の大半を備えているのだから、失うものはないという冗談のように見える。foo=obj.whatever()でobj.whatever()が関数呼び出し時ではなく関数定義の処理時に評価される、という話は正しいはずがないように見える。.whatever()がオブジェクト初期化後に変わる内部状態に依存していたらどうなるのか分からない。