2 ポイント 投稿者 GN⁺ 2023-08-12 | 1件のコメント | WhatsAppで共有
  • 単一のSaaSアプリケーションで Postgresのボトルネック が深刻化し、CPU使用率が60〜80%に張り付き、一時は100%まで上昇して短時間の障害につながった
  • より大きなDBインスタンスへ切り替える 垂直スケーリング はすでに限界に達しており、同じやり方では負荷増加をこれ以上吸収しにくかった
  • 書き込みシャーディングとマイクロサービスは容量と運用の柔軟性を高められるが、バックアップ・モニタリング・マイグレーション・ORM・ネットワークトポロジーに至るまで 複雑性のコスト を抱え続ける
  • 実際には3か月かけて重いクエリの削除、Railsコードの最適化、Postgresのチューニングを行い、一部の高コストな読み取り専用クエリを レプリカDB に切り分けた
  • その結果、週間ピーク時のDB CPU使用率は90%から30%に下がり、次世代アーキテクチャへ移行する前に現行システムをより長く活用できる余地が生まれた

Postgresのボトルネックと垂直スケーリングの限界

  • 単一のSaaSアプリケーションの負荷が高まるにつれ、Postgresの性能 が中核的なボトルネックになった
    • CPU使用率は60〜80%の範囲に張り付いていた
    • 少なくとも一度は100%まで急上昇し、短時間の障害が発生した
  • 以前はDBが逼迫するたびに、より大きなインスタンスへ置き換えて時間を稼いでいた
    • このやり方のおかげで、機能開発のような他の作業に集中できていた
    • 当時はすでに最大のインスタンスを使っており、これ以上の 垂直スケーリング は不可能だった

魅力的だが高コストな次世代アーキテクチャ

  • 議論された主な選択肢は 書き込みシャーディングマイクロサービス だった
  • 書き込みシャーディングは独立したDBクラスターを用意し、パーティショニング戦略に従ってデータを特定のDBへ書き込む方式である
    • 潜在的には容量を2〜3桁規模まで増やせる
  • マイクロサービスはモノリスを複数のサービスに分割し、各サービスが独自のデータストアを持つようにする
    • サービスごとのワークロードに合わせてデータストアを選べる
  • どちらの方式も耐障害性と運用回復力の面で選択肢を広げるが、当面の目標は DB性能を制御可能な状態 に戻すことだった

複雑性は一度導入すると継続的にコストを生む

  • 複雑性の増加は、新しい構造を実装するコストを超えて、その後の 注意力コスト へとつながる
  • DBシャーディングを選ぶと、今後の技術的判断のたびに新たな複雑性も一緒に扱わなければならない
    • バックアップ
    • モニタリング
    • マイグレーション
    • ORM
    • ネットワークトポロジー
  • マイクロサービスも同様の負担を生み、追加アーキテクチャの維持によって機能提供が遅れたり、断念されたりする可能性がある

まずは現行システムの中で余力を見つける

  • 大きなアーキテクチャ転換が必要に見えるときでも、既存システムには 追加の余力 が残っていることが多い
  • ワークロードの調整、性能チューニング、補助システムの追加だけで、数か月から数年の時間を稼げることがある
  • こうした選択肢が実行可能なら、次世代システムを新たに構築する前にまず試してみる価値がある

実際に行った最適化

  • 最初の作業では、2人のエンジニアが約 3か月 にわたり主にDB性能問題に取り組んだ
    • 単一の特効薬はなかった
    • テレメトリで重いクエリを見つけた
    • Railsコードベースでクエリ発生箇所を特定した
    • クエリを最適化または削除した
    • 複数のPostgres設定をチューニングした
  • 2つ目の作業では、別の2人のエンジニアがコードベースを修正し、一部の高コストな 読み取り専用クエリ をレプリカDBで実行するようにした
    • Webクライアントのポーリングが引き起こす、最も頻繁な SELECT クエリをメインDBから切り離した

結果と運用原則

  • 2つの作業を合わせて、DBの週間ピークCPU使用率は 90%から30% に低下した
    • 週間ピークCPU使用率: {l:90,30}
  • CPUの余力が大きく生まれ、メインDBから負荷を逃がせる能力も高まった
  • コードベースの複数箇所に手を入れ、複数の開発者が協業することで、既存システムに関する分散知識も蓄積された
  • 複雑性が常に悪いわけではなく、いずれはDBアーキテクチャの根本的な限界に達する前に、より複雑な構造へ移行する必要がある
  • それまでは、まず現行システムを徹底的に活用すれば、できるだけ 退屈でシンプルなシステム のままでより長く運用でき、コスト面でも実用面でも有利である

1件のコメント

 
GN⁺ 2023-08-12
Hacker News のコメント
  • データベースの性能問題を見ていて、新規プロジェクトに関する自分のいちばん強い考えは、アプリケーションのホットパスで JOIN をまったく使わないように設計しよう、というものです
    ストレージは安いので、すべて非正規化してトランザクション内で全部更新すればよい。JOIN をなくすとどれほど速くなるか、本当に驚きます。アドホックな分析クエリは、分析用の別データベースにレプリケーションすればよい
    Amazon の DynamoDB には複雑な感情がありますが、正しく使うにはまず利用パターンを計画し、スキーマは後から決めるべきだという点は、リレーショナルデータベースにも取り入れる価値があります。最近では、分析目的でないなら JOIN は不要とまで考えています。主要なデータベースには ACID 特性があり、ストレージはばかげているほど安いので、単に非正規化すればよい
    ホットパーティションを防ぐには、整数ではなく UUID に近いものを使うほうがよい。万能ではなく欠点もありますが、いつか破綻する整数の優れた性能より、水平方向にスケールできる「常にそこそこ」の性能に慣れることができます
    さらに過激な考えとしては、すべてのカラムにインデックスを張ろうというものもありますが、それはまた別の日に話す話題です

    • 正直、まったく同意できません。スタートアップを作っていた 1〜5 年目は性能をほとんど気にせず、6 年目に特定のテーブルの性能問題が発生し、エンジニア数か月分の作業で最適化しました
      技術的負債の観点では、最初からすべてを高速に作ろうとしていたら、はるかに高くついていたはずです。開発速度が大きく落ち、いくつかの危機で失敗していた可能性が高い
      その代わり、実際に必要な額より月に数千ドル多くマシン費用を払い、エンジニアを十分に採用できず機能開発の機会費用が大きかった時期に、数か月分の人手を節約できました。どこがボトルネックになるか事前には分からなかったため、すべてを最初から高速にしようとすると 10〜20 倍は多くの作業が必要だったはずです。一部のボトルネックは意外なものでした
      JOIN は大規模では有害になり得ますが、ほとんどのスタートアップは少なくとも初期にはスケールの問題を抱えていません。非正規化は良い最適化になり得ますが、変更のたびにすべてのコピーを同期するという速度面のコストを払います。誰かが非正規化された非正本フィールドを更新しないバグを作り、ユーザーに古いデータを見せることになります。たいていは JOIN を使い、後で read-aside キャッシュなどで最適化するほうが、スキーマをねじ曲げるより総コストの面で安いです
    • 適切にインデックスが張られていて、同じサーバー内で行われる JOIN なら、性能に大きな影響を与えるべきではありません。「JOIN をなくすとはるかに速くなる」という話は、JOIN を正しく使うなら事実ではありません
      逆に、1 つのデータを 1 か所ではなく 20 か所に書くのは性能上はるかに遅く、クエリも非常に複雑でバグが出やすくなります。18 か所は更新したが 2 か所を忘れる、ということが起きます
      非正規化の利点として安いストレージが挙げられますが、ここでストレージは最も小さな問題です。はるかに大きくなったバグの表面積と悪い書き込み性能こそが本当の問題であり、その書き込み性能は読み取り性能まで簡単に食いつぶし得ます
    • アプリケーションには「高い(tall)」アプリケーションと「広い(wide)」アプリケーションがあります。データベース設計と最適化に関する助言のほとんどは、高いアプリケーション向けです
      高いアプリケーションは基本的に 1 つのことだけを行い、残りはそれを補助します。思い浮かぶビッグテックのほとんどがこれに当てはまります。データモデルの中で本当に重要な駆動概念は数個しかありません
      Facebook は事実上、人、投稿、広告だけです。Netflix はアカウントと番組程度です。Amazon のプロダクトは販売者、購入者、商品が中核で、物流用のいくつかがその背後にさらにあるかもしれません
      高いアプリケーションが多い理由は簡単だからです。よく「エンタープライズ」と呼ばれる広いアプリケーションより、はるかに簡単です。エンタープライズソフトウェアがひどい理由は難しいからで、ここには最も探索されていない領域と莫大な機会があります。Oracle のような既存プレイヤーはここで惨憺たる出来であり、高いアプリケーション式の思考で入っていけば、あなたもそうなるでしょう
      「JOIN を絶対に使うな」「単一テーブル中心で設計せよ」といった助言は高いアプリケーションには筋が通りますが、広いアプリケーションにはひどい助言です。非常に高いアプリケーション企業が中核能力の外のことを試みて惨憺たる失敗をするのをよく見ますが、それはこの助言を神聖視する人たちで満たされているからです
      この助言は、すでに成功していて、簡単なことをしており、低いところに実った果実を取り尽くした会社のためのものです。まだ成功の犠牲者になっていない高いアプリケーションでさえ、性能のためにデータモデルを切り刻む必要はありません。すでに巨大な成功を収め、最後の一滴の性能を絞り出す会社だけがそうした悩みを抱えるのであって、そういう会社こそ助言を最も必要としていません。「FAANG がやっているからあなたもやるべき」「ユーザーが 10 億人いたらどうする?」といった高いアプリケーション中心の助言は、数十億人に広告を見せることより面白いことをしようとする人たちの頭を汚染します
    • ここ数年、大きなテーブルの JOIN 性能問題より、開発者が非正規化に頼ったことで生じた苦痛のほうを多く見てきました
      大きな JOIN は後でマテリアライズドビューに押し込んだり、カラムストアへ ETL したりするなど、性能を直す方法があります。しかし誰かが subtotal_cents カラムを Order、Invoice、Payment、NotificationEmail、UserProfileRecentOrders モデルにコピーしておき、296 か所で参照または更新するようにしてしまうと、正常な状態に戻る道のりは長くなります
    • JOIN 自体が性能のボトルネックであるケースは、ほとんど見たことがありません。観察した単一の原則があるとすれば「テーブルを大きくしすぎるな」です
      たいてい問題は、履歴レコード的な性格のテーブルで生じます。日常運用に必要なデータは実際のテーブル内のごく一部なのに、どんなインデックスがあっても巨大なテーブルでの操作は遅くなるものです。インデックスをさらに追加する行為自体も問題になります
      少なくとも従来型のリレーショナルデータベースでは、すべてのカラムにインデックスを張るだけでは不十分で、使われ得る適切な複合カラムインデックスを張る必要があります。DynamoDB は異なるかもしれません
  • 手持ちのものを最大限に絞り出し、しばらく絞り出した後で問題の見方を変えて、ここ、ここ、そしてここまで絞れると、突然まだ多くが残っていることに気づく
    巨大なモノリスを2カ月ほど最適化し、PMとチームがもう絞れるところはないと見ていた2,000 RPS未満の状態から、ハードウェア交換で3,200 RPS未満まで上げ、数日手を入れて4,000 RPS、もう少し努力して1万 RPS、1週間ほど後には4万 RPSまで上げた
    「このくらいで十分だ、これ以上進める必要はない」という声も出たが、かなり多くのものを変えると、単一マシンで200万 RPS以上に跳ね上がり、1カ月後には低レイテンシで4,000万 RPS以上を安定して処理するようになった。まだもう少し押し込む余地はある
    今は引き出せる容量の5%も使っていない。単に問題の考え方を変えただけで、それほどの変化があった。古いサーバーから新しいサーバーへ移しただけでは、1,800 RPSから3,000 RPSを少し超える程度に上がったのがすべてだった。ハードウェアの追加は根本問題を直さず、複雑さの追加は問題を先送りしただけだった。問題の考え方を変えると、問題そのものと答えが変わった

    • モノリスがいったい何をそんなにひどくやっていたから、そこまで性能を絞り出せたのか本当に気になる。クエリがめちゃくちゃだったのか、クエリが多すぎたのか、キャッシュがまったくなかったのか、並行して処理できることを同期的にやっていたのか気になる
    • これらのリクエストの内容やプロトコルが何なのか、リクエストとレスポンスそれぞれの平均ペイロードサイズがどのくらいなのか共有してもらえるのか気になる
  • 「モノリスを相互接続された複数のサービスに分割し、各サービスが自分の条件に合わせてスケールできる独自のデータストアを持つようにする」という話に付け加えると、この時点で可能なすべてのマイクロサービスを分離する必要はない。「どの分離が最も大きな影響をもたらすか?」と問えばよい
    私の場合は、Mongoから一部の時系列データをCassandraへ分離した。Cassandraのテーブル構造のほうがはるかによく合っていた。そのデータセットには明確に定義されたスキーマがあり、Cassandraはデータをはるかに効率よくパックできた。その部分ではJSONドキュメントの柔軟性は必要なかった
    そのデータが全体データの大半だったため、その後Mongoはかなり満足できる状態になった。必要な分離はたった一つだった。技術的には前後ともモノリスで、同じサービスが2つのデータベースに書き込むようになっただけだった
    皮肉なことに、後になって机上の空論型のアーキテクトが全データをJSONドキュメントストアに統合したがり、「私たちはすでにその道を通っていて、行き着く先を知っている」というような議論が何度も続いた

    • マイクロサービスというアイデアが、あまりに当然の「解決策」のように投げ込まれるのは興味深い。そうではない
      垂直スケーリングこそ当然の最初の解法であるべきだ。多くの人が見落とす点で、記事でも一部触れているが、垂直スケーリングはデータベースの一貫性を壊さない水平スケーリングに近い
      絞り出せる余地は多く、性能問題が起きたからといって、ジョインやデータ検証を無視するようなアンチパターンを安易に持ち出す必要があるケースはまれだ
    • 記事でも、ある意味では分離をしていた。特定の重いSELECTクエリをレプリカに回したからだ
    • 面白いことに、私は逆の問題をよく経験する。相対的に寿命が短く頻繁に更新されるデータにCassandraがなぜ向かないのかを、何度も説明しなければならない。tombstoneのせいだ
    • 1台のサーバーが2つのデータストアから読めるという点も重要だ。兄弟コメントで言われている読み取りレプリカのように、同じプロセス内でPostgresとRedisを一緒に読めないようにするものはない
  • 開発者がEXPLAIN/ANALYZEの結果を読んで適切なインデックス設計とクエリ最適化をできるようになれば、多くの過剰設計の判断を避けられる
    クエリを記録し、非常に頻繁に実行されるものや実行時間が長いものを絞り込み、頻繁に実行されるものはキャッシュし、重いものは最適化すればよい。これを体系的に行えば、システムはより健全になる
    私の経験上、大きく役立つものはAPM、スロークエリログ、データベースの読み取り/書き込みレプリカ、パーティショニングとシャーディングだ

    • explainの出力を読むこと自体も、かなり大きな課題になり得る。データベースは完全に別世界で、直接SQLをほとんど書かないなら特にそうだ
      https://explainmysql.com のようなツールは、実際に何を最適化すべきかをより明確に示してくれるので、データベースを設定できる程度の知識はあるが、内部での使われ方を理解するほどではない開発者にとって、より使いやすい枠組みになる
      誰かがすでに、スキーマとログを入れると魔法のように改善用のSQLを返してくれるAIシステムを作っていそうだ。信じるかどうかは分からないが、多くの会社は専任DBAを雇うより、そういうものを使いたがるだろう
    • SQL実行計画を理解するためのよい資料があるのか気になる。現在のプロジェクトでMS SQL Serverのクエリ性能問題にかなり悩まされている
      クエリごとに常にインデックスヒントを指定すべきなのか分からない。インデックスが存在するのに、クエリがそのインデックスを使っていないように見えることがある。SQL実行計画を使えば、この問題をよりよく理解できそうだ
    • 「次世代アーキテクチャ」セッションを何度見たか数え切れないが、その大半は現在の実装に対するデューデリジェンスで置き換えられた
      悪いコードを新しいアーキテクチャで直すことはできない。問題をしばらく先送りするだけだ
  • 好きな言葉の一つを思い出した。「望む軍隊や、いずれ持ちたい軍隊ではなく、いま持っている軍隊で戦争に出る」
    この言葉が Donald Rumsfeld から出たという事実は無視したくなるかもしれない。それでも「unknown unknowns」のような優れた言葉もある
    チームで働くとき、この言葉をよく思い出す。全員が完全に同意していたり、同じ理解や共通目標を持っていたりするわけではない。非効率だったり、自分の好みではないやり方で働く人もいるかもしれない。それでもチームがないよりはあるほうがよいので、いまいるチームで目標を達成する最善の方法を見つける必要がある
    システムにもよく当てはまる

    • Rumsfeld にはかなり優れた引用句が多い。その多くはイラク戦争がなぜあれほどひどいことになったのかを説明する文脈で出てきたもので、その状況こそ、彼の引用句だけを見れば彼が提供できたはずに思えるリーダーシップを切実に必要としていた
    • Rumsfeld という名前を聞くたびに、彼が10分以上、自分はトカゲではないと否定することを拒み続けた場面を思い出す
      https://www.youtube.com/watch?v=XH_34tqxAjA
    • 「どんな戦闘計画も、敵と接触した後には生き残れない」
      https://www.google.com/search?q=no+battle+plan+survives
    • Mattis の「敵にも一票がある」も、現実を思い出させる良い言葉だ。人々はひどく腹を立てるが、セキュリティ、プライバシー、DRM のような文脈では有用だ
    • Steven Pressfield の似た一文も好きだ。「アスリートは、痛みなしに目覚める日など決して来ないことを知っている。彼は負傷したまま試合に出なければならない」
      これはシステムよりも、むしろ私たち自身によく当てはまる
  • 私の経験では、ORM の上に作られた Web アプリでは、データベース負荷が問題になったときにクエリ最適化で取れる低いところにぶら下がった果実が驚くほど多い
    「N+1 問題があるか」といった基本を超えて、ORM はときどき最適なクエリを作れない。複雑な本番 Web アプリを ORM なしで作りたいとは思わないが、ときには ORM から抜け出せる必要がある
    本番で実際に実行され、最も多くのリソースを使っているクエリをプロファイルしなければならない。そのクエリを見て、対象テーブルの形を把握する必要がある。ORM が join を使っているが実際にはサブクエリが必要な場合もあり、その逆もある。結果の一部を事前に集計したり、複雑な join で WHERE 条件を調整したりする必要があることもある
    そこそこ頻繁に実行される ORM 生成クエリがデータベースを殺していて、実行に20秒以上かかっていたものが、いくつかの小さな修正だけで1秒未満になった例も見た

    • いま Python ORM の SQLAlchemy で作業しているが、INSERT で RETURNING を使わせるのは些細なことではない。直感的でない expire_on_commit=False オプションを設定する必要がある
      このオプションも RETURNING の使用を保証するわけではなく、データベースドライバとデータベースが対応し、ORM がそのドライバ/データベースの組み合わせで対応していれば使う、というものだ。生成された SQL はログには出るが実際に検査する API がないため、自前でログを捕捉して拾ってこない限り、テストスイートで RETURNING の使用を強制する方法がない。幸い Pytest フレームワーク内では非常に簡単ではある
      ORM は好きだが、こういうものは複数の層でうんざりするほど複雑だ。SQLAlchemy が巨大なライブラリで、すべてが簡単にはなり得ないことも理解している。それでもこの例は、ORM 利用のトレードオフをよく示している
      Core で insert() 自体を使えば望みどおりになることは分かっている。ここで言っているのは、ORM オブジェクトを AsyncSession.add() する場合の話だ
    • ORM の大きな問題は、オブジェクト指向がほとんどのドメインでソフトウェアやデータを整理する良い方法ではないところにある
      ほとんどのビジネスロジックは、OOP よりも関係代数の言語といくつかの拡張で表現するほうがよい
  • 「複雑性増加の本当のコスト、そしてしばしばはるかに大きいコストは注意力である」という言葉は、つまり認知負荷でもある
    いまだにダウンタイムがあるのに、全体がどう動いているのか誰も分からないマイクロサービスシステムで働くのに疲れた。ほとんどは実際には分散モノリスで、変更は複数サービスにまたがり、順番にデプロイしなければならない。データは複製しなければならず、作業は同期しなければならず、状態は共有しなければならない
    https://www.youtube.com/watch?v=y8OnoxKotPQ

    • 「全体がどう動いているのか誰も分からない」マイクロサービスは、非常によくあるアーキテクチャ上の悪臭だ。本来の要点は、誰も全体を知ることはできず、知るべきでもないというところにある
      ところが、システムを直したり修正したりするために誰かが全体を知る必要があるなら、それは単一責任や API を通じた正しい抽象化といったルールを破っている強い兆候だ。しかし私の経験では非常によくある。N 個のマイクロサービスからなるパイプラインをデバッグするには、しばしば N 個のサービスをすべてローカルで実行し、ビルドしなければならない
      厳密に言えば、これはモノリスにネットワークパーティションと無限のビルド/デプロイの変動性を加えたものだ。結局、どんな人間のプログラマーの能力も超える、極めて難しい作業環境だと思う
  • この記事は良い。ここ数カ月、マネージャーに同じメッセージを伝えようとしていたが、あまり成功しなかった
    過負荷の Redshift クラスターが何度も崩壊し、すでに RA3 ノードで上限まで引き上げているため、いま「次世代インフラ」への大規模マイグレーションを始めようとしているところだ。ここで言う次世代インフラとは、CDK で管理される3つの Redshift クラスターのことだ
    新しいインフラは現在の構成よりはるかに複雑で、皆が期待している銀の弾丸になるのかは確信が持てない

  • 退屈な解決策だ。生命や身体に直結する超重要システムを作っているのでない限り、これが基本の答えであるべきだ。総所有コストは確実にはるかに低くなる
    大きな冗長システムを運用するリソースがないなら、その冗長システムが追加した複雑性そのものが問題になるケースをあまりにも頻繁に見てきた。単純さに集中するほうがよい
    複雑性を支えるために人員を大きく追加しなければならないが、費用とリスク評価のうえでその必要がないなら、より単純なほうがはるかによい。結局、前に進むには巨大なプロジェクトが必要になったケースを見なかったわけではないが、ときにはそれでさえ、その時点まで複雑性を抱え続けてきた総量よりは小さいと思う。何を作っているのかに大きく左右される

  • システムの中から性能最適化の余地を見つけて、残っている性能を絞り出す解決策は本当に楽しい
    Richard L. Sites の本 Understanding_Software_Dynamics を思い出す。この本はレイテンシの問題を測定して修正する方法、そして大規模環境でレイテンシ削減がどれほど大きな節約につながるかを教えてくれる
    こうした問題を測定して推論するのは難しいが、解決策はしばしば単純だ。たとえば9ページで「[a] simple change paid for 10 years of my salary.」と言っている
    いつかそのくらい影響力のある最適化をしてみたい

    • Google でそういうことを少なくとも一度はやったことがある。Google は膨大な量のマシンリソースを使っており、優れた性能ツールも備えているので、肥沃な環境だ
      https://research.google/pubs/pub36575/
      ただし周囲には賢い人も多いので、大きな機会を見つけたなら、他の誰も飛びついていない理由がたいていある。技術的な理由かもしれないし、組織的な理由かもしれない
      後者の例として、Google は通常、リソース面の圧力がある時でなければ、この種の仕事に大きな報酬を与えない。ある最適化で同僚ボーナスをおよそ100ドルもらったことはあったかもしれないが、10%のコミッションや昇進、10年間出社せずに給料を受け取る権利のようなものはまったくなかった。一般に会社は、エンジニアがコスト削減よりも売上成長に取り組むことを好む。この方針が正しいかどうかは、私の給与等級を超えた話だ
    • 最終的な解決策で気になる点は、可能な最大構成でインスタンスを立ち上げた後になって、ようやくクエリを最適化したことだ
      今では管理コンソールで数回クリックして設定をさらに上げることができなくなり、そのため容量問題を解決するために実際に頭を使わざるを得なかったのだと思う。コードの特定部分をもっと早く最適化していれば、そもそもそれほど大きなインスタンス構成は必要なかったかもしれない