1 ポイント 投稿者 GN⁺ 2024-06-10 | 2件のコメント | WhatsAppで共有
  • 収益化を始めたばかりのスタートアップがサブスクリプション決済の障害に見舞われたが、社内では再現できず、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件のコメント

 
znjadong 2024-06-11

えっ、AIによる自動生成コードは必ずレビューすべきでしょう。それをなぜそのまま使うんですか。

 
GN⁺ 2024-06-10
Hacker News の意見
  • モニタリングの不在が1万ドルを吹き飛ばしたということ。アプリがデータベース例外を継続的に、大量に出していたのに、誰にも通知が届いていなかった
    そうした通知があれば、5日間の調査ではなく5分間の調査で終わっていたはず。通知体制を直していないなら、実際には何も直していない

    • その通り。ログメッセージにはIDが一意ではないと出ていたはずで、そうすればデバッグ時間はずっと短くなっていたはず
      すべてがうまく動いているときのプログラミングは簡単で、問題を処理する部分が難しい
    • こういうことはますます一般的になっている。企業や創業者は、選んだクラウドプロバイダーが魔法のように何でもやってくれると信じて、インフラのことを考えない
      有料顧客が入ってくる瞬間から、ロギング、モニタリング、通知、セキュリティなどを扱う知識と経験のある人が必要。DevOpsを素人のように扱ってはいけない
    • テストがないのも悪いし、AIが作ったものを三重に確認せずに使うのも危険
      だが、データベースにエラーロギングと通知がないというのが本当に狂っている部分。これは20年もののレガシーコードではなく新製品で、DBエラーをデータ検証のように使っていた時代のコードでもない
    • デプロイしてすぐ寝たことが、ここでの危険信号に見える。午前9時ごろにデプロイして、勤務時間中に問題をモニタリングすべきだった
    • ChatGPTはモニタリングが必要だとは教えてくれなかったようだ
  • ブログ記事が404になっているので、Web Archiveのリンクを残しておく
    https://web.archive.org/web/20240610032818/https://asim.bear...
    著者が重要な追記をしていた。ここでの慣行は非常に悪く、恥ずかしいレベルであり、その後、堅牢な単体/統合テストと通知/ロギングを追加したとのこと。結局は人間のミスで、振り返れば明らかに避けられた出来事だったという内容
    また、会社の創業初期の数週間、大きな時間的プレッシャーの中で起きたことで、本番環境でのバグ再現性が特異だった面白い話として見てほしい、とも付け加えていた

  • エラーはすぐに見えていた。チームへの敬意はあるが、これはChatGPTとはあまり関係がなく、チームが十分に慣れていないプログラミングモデルを使ったことに大きく関係している
    たとえコードレビューを通っていたとしても、5分で設定できる監視ツールがあるだけで検出できた可能性が高い

    • 公平に言えば、このバグを探そうとして見ていなければ気づけなかったと思う。それでも、監視やごく基本的な手動テストさえあれば即座に見つかったはず、というのは正しい
    • チームはデータベースログやアプリケーションログで基本的なトラブルシューティングもできなかったように見える。これは単純なエラーだったし、テーブルの暗黙的ロックのような一時的なエラーが起きたらどうするのか心配になる
    • 無邪気なミスではなく、タイトルは意図的にクリックベイトかつ検索語最適化用になっている。ChatGPTがミスしたかのように示唆して、不安をあおるクリックを誘っている
      「LLMの使用中にプログラミングエラーを起こし、品質保証をしなかったため1万ドルかかった」というタイトルでは、経営陣の「ChatGPTがやらかしたら、うちのエクスポージャーはいくらになる?」といった反応は引き出せない。LinkedInにこの記事を投稿する中間管理職や上級管理職は非常に多いだろう
      LLMは「ミス」をすることはできない。決定論的ではなく、推論したり考えたり論理を実行したりすることもできない。統計的確率を使う、非常に派手な単語サラダ生成器であり、生成物が正しい、または正確である保証はないので、定義上「ミス」という表現も適切ではない
      追記: 記事は明らかな理由で大きくダウンボートされた後、順位が突然跳ね上がっており、これはモデレーターがブーストしたことを意味しているように見える: https://hnrankings.info/40627558/
      ルール上タイトルを変えるべきレベルのクリックベイト記事がモデレーターにブーストされたのは笑える。しかも投稿者がY Combinator企業の所属らしいのも、完全な偶然なのだろう: https://news.ycombinator.com/item?id=40629998
    • 興味深いことに、このエラーを見つけたもう一つの存在もChatGPT-4oだった。画像付きのチャットは共有できないが、間違ったコードの画像を貼り付けて「コードの何が問題か」と尋ねたところ、次のように教えてくれた
      主キーの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: を追加したことが問題を直した
    • Pythonの経験がほとんどなければ、uuid.uuid4() をPrismaのようなところのスキーマ定義のように考えたのだと思う。なので、このバグ自体は驚きではなく、自分も同じミスをしていたかもしれない
      それでも kubectl logs を一度実行すればすぐに見つけられたはずだ。しかもNext.jsとPrismaからPythonに行くって? なぜ?
  • ミス自体は理解できる。ChatGPTなしでコードを書いても、比較的簡単に見落としそうに見える
    しかし、最初の失敗の後になぜ見つからなかったのかは理解できない。この会社にはロギングがなかったのか? バックエンドがUUIDを再利用しようとしているという事実は、エラーを見ればすぐに分かったはずだ

    • 顧客が抗議するまで、エラーが起きていることすら知らなかった。顧客より先にどんなエラーが起きたのかを把握しているべきで、ロギング、アラート、何らかの形の監視があれば役に立ったはずだ
    • これが本当の問題だ。速く動くプロジェクトなら、いずれまたこうした本番バグが起きる可能性は高い。次回は特定に5日もかからないことを願うだけだ
    • このバグを把握するのに5日かかったのは、かなり正気ではない
    • 複数のStripeサブスクリプションのような特殊ケースが、一般的な単体テストから漏れるのは十分あり得る。記事にあるように受け入れテストで再現するのも簡単ではなかっただろうし、ChatGPTに焦点を当てるのは投稿者も他の人たちもやや行き過ぎている
      String の代わりに呼び出し可能オブジェクトを渡すべき関数に、誤って文字列を渡してしまうことはよくある。ORMを使っていなければこの特定の問題は防げたと思うが、ORMに対する個人的な偏見かもしれない。データベース以外の文脈でも似たようなバグは十分起こり得る
      このバグを確実に捕まえられたはずだと大言壮語している人たちは、私よりはるかに優れたエンジニアなのか、より可能性が高いのは、自分の能力について少し勘違いしているのだと思う
      ただし、ログがない、またはログを見ていないことは本当に理解しがたい。ECSならDuplicate Key例外は追加設定なしでCloudWatchに伝播していたように思うが、そうならなかったのか、なっていたのに夜通しどんな例外が起きたのか誰も確認しなかったのかが気になる
    • この種のプロセス失敗があるため、Amazonにはエラー修正、つまり事後分析プロセスがある: https://aws.amazon.com/blogs/mt/why-you-should-develop-a-cor...
      このような状況では、なぜ検知が遅れたのか、なぜ診断に時間がかかったのかを問うことが有用だ
  • 人間が書いたコードでも同じミスを何度も見たことがある。特に React / TypeScript / JavaScript では、誰かがラムダを書き忘れることがよくある
    ブログ記事は問題の根本原因をきちんと説明せず、すぐに ChatGPT のせいにしているように感じる。急いで作業し、大きな変更や同僚レビューのないコミットを main に入れると、こういうことが起きる
    本当の問題は、急ぎ、近道を選び、十分なテストと同僚によるコードレビューをしなければエラーが起きるということだ。複数の登録オプションを試すテストがあるだけでも、すぐに見つけられたと思う

    • ChatGPT に対する自分のメンタルモデルは、永遠に昇進できないジュニアエンジニアだ。いずれ解雇される人材だが、その代わり無限に速くタイピングできるので、かなり慎重に使えば役に立つこともある
      こういう人を財務的に重要なコードの近くに置けば似た問題が起きるだろうし、テストもほとんどなしにそのコードをデプロイすると決めた人の判断を疑ってしまう
    • こういう問題はあちこちでよくある。たとえば Vue のコンポーネント属性にはデフォルト値を持たせられるが、オブジェクトや配列を返す関数の代わりにリテラルオブジェクトや配列をデフォルト値にすると大変なことになる
      このケースに対する lint ルールがなかったというのは驚きだ
    • ChatGPT は注意をそらす要素にすぎない。コードを何が生成したかではなく、そのコードで何をするかが重要だ
    • コードは ChatGPT が書き、push し、その後のレビューも ChatGPT がする構造だったということだ。/s
      そうでなかったことを願う
  • 「元のプロジェクトはフルスタックの NextJS だったが、まずすべてを Python/FastAPI に移行したかった」という部分には目が覚めた
    顧客もいないスタートアップがどうやって書き直しを正当化するのか分からない

    • 同じことを考えていたので、自分がおかしいのかと思った。こんな初期に書き直すのはあり得ないと思っていたのに、コメントで誰も指摘していないのかと思った
      顧客の有無に関係なく、こんな早い段階で、実質的に Node から Python への横移動をなぜするのか分からない。顧客が数百人いて Go などに変えようとしている状況ならまだ理解できるかもしれないが、それでもやはり疑問だ
    • しかも、明らかに見えるバグにも気づけないほど経験の浅い言語で書き直しをしていた
    • 自分の場合、いま作業中の実プロジェクトなら、C# は良い言語でランタイムや Web フレームワークも悪くないが、開発速度を落とし、痛点が積み上がり続けるから、ということはあり得る
      たとえば DTO オブジェクトを大量に作らなければならないのに、AutoMapper は自分が使っているバージョンの組み合わせとプロジェクト設定では動かず、Entity Framework と JSON のシリアライズ/デシリアライズも、得られるものより苦痛のほうが大きい
      もちろん段階的に解決することはできる。ドキュメントを深く掘り、ハックを混ぜ、パッケージをアップグレードし、設定を書き直す、といった形で可能だ。だが人間なら、比喩的なガソリン缶を持って全部燃やし、2つ目のシステムをもっと良くしたくなる。もちろん実際に良くなるのではなく、別の痛点が生まれるだけで、最初のシステムがしていたことを全部できなかったり、まともにできなかったりすることもある
      職場でもレガシーや面倒なシステムを見るたびに同じ衝動に駆られる。書き直せと叫ぶ脳に勝つには、積極的で継続的な努力が必要だ。たまに書き直しやコンテナ導入のようなアーキテクチャ変更がうまくいくこともあるが、たいていは火の中に飛び込むか、終わりのない作業につながる
      システム運用や他の開発者の開発者体験を改善できるという高い確信がある場合を除けば、その衝動に屈しないほうが幸いだ
    • この件での ChatGPT の唯一の失敗は、その能力のせいで、リリース前の書き直しがランウェイを使ううえで合理的で簡単なことだと錯覚させたことだ
  • ChatGPT はむしろアプリが稼いだお金を生み出した側だ。ChatGPT なしでは実装する能力がなかったからだ
    コーディング、デバッグ、ロギング、モニタリングができなかった能力不足が1万ドルを吹き飛ばしたのであり、この話における ChatGPT の純効果はプラスだ

    • github.com/reworkd にあるこのチームのプロジェクトを見ると、プロダクトとチームの成熟度がすぐ分かる。絵文字駆動開発
      すべてのコミットメッセージに絵文字が入っている。サル、バナナ、ロケット、花火など、ないものはない
    • 特に月20ドルのコストで考えると、なおさらそうだ
    • 1万ドルははした金だ。Elon は今日出た xAI のミスで5,000億ドルを失うかもしれない
      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 で書き直しているからだろう。

    • シード資金として50万ドルがあり、その上にさらに120万ドルがあり、燃やしてしまうための無料 AWS クレジットもあるからだ。
  • 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() がオブジェクト初期化後に変わる内部状態に依存していたらどうなるのか分からない。