1724576284
2024-08-23 10:36:00
これまでに Web サービスや Web アプリを構築したことがあれば、手順はご存知でしょう。データベースを選択し、Web サービス フレームワークを選択します (そして、今日ではフロントエンド フレームワークを選択しますが、それについてはここでは触れません)。
これは数十年前から続いてきたことであり、これが今でも Web アプリを構築する最良の方法であるかどうか疑問に思う人はいません。過去 10 年間で多くのことが変化しました。
- ディスクははるかに高速です(NVMe)
- ディスクははるかに堅牢です (EBS/EFS など)
- RAMは非常に安価で、ほとんどのスタートアップではすべてのデータをRAMに収めることができるでしょう。
- ご希望に応じて、数百のコアを備えたマシンをレンタルすることもできます。
2010 年に私が初めて Rails のスタートアップで働いたときはそうではありませんでした。しかし最も重要なのは、過去 10 年間に起こった非常に重要な新しい変化が 1 つあることです。
このブログ記事では、ウェブ開発のための新しいアーキテクチャを解説します。私たちはこれを次のような用途でうまく活用しています。 スクリーンショットボットぜひご利用いただければ幸いです。
このブログ記事は、探索、展開、抽出の3つの部分に分けられます。 ケント・ベックは 3X です。スタートアップの各段階でニーズは異なります。ここでは、3 つのフェーズすべてでアーキテクチャをどのように使用するかを説明します。
探検する
あなたは新しいスタートアップです。製品を繰り返し改良していますが、人々がそれをどのように使うのか、あるいは もし 彼らはそれを使うつもりです。
今日のほとんどのスタートアップにとって、これは、MySQL、PostgreSQL、MongoDB などを基盤として、Rails、Django、Node などを選択することを意味します。
「シンプルに」とあなたは言うでしょうが、これは十分簡単なことのように思えます。
しかし、これは可能な限り単純なのでしょうか?もっと単純なものにできるでしょうか?Webサービスとデータベースインスタンスがまったく同じものだったらどうなるでしょうか?SQLiteのようにデータがシリアル化されているものを使うことを言っているのではなく、RAMのメモリがすべて は データベース。
データを SQL クエリにシリアル化する必要がないとしたら、どんなにすばらしいものを構築できるか想像してみてください。まず、単一の DB と通信する複数のフロントエンド サーバーは必要ありません。必要な場合は、より多くの RAM と CPU を備えたより大きなサーバーを用意するだけです。インデックスはどうでしょうか。メモリ内インデックスを使用できます。これは、実質的にはオブジェクトを検索するためのハッシュ テーブルです。ディスクの待ち時間に合わせて最適化された B ツリーなどの巧妙なインデックスは必要ありません。(実際、従来のデータベースではおそらく不可能だったインデックスも使用できます。 そのような指標の一つ 機能コレクションの使用は、Screenshotbot のスケーラビリティにとって非常に重要でした。
また、データベースへのラウンドトリップを減らすために特別なアーキテクチャも必要ありません。特に、スレッドが IO に縛られなくなるため、非同期 IO の処理は一切必要ありません。データの取得は RAM の読み取りだけの問題です。突然、コードのデバッグも非常に簡単になりました。
バックグラウンド ジョブは、この大規模なプロセスで実行されるスレッドにすぎないため、バックグラウンド ジョブを実行するためにサービスは必要ありません。
並行性要件のほとんどは単純なメモリ内ミューテックスと条件変数で満たすことができるため、複雑な並行性プロトコルは必要ありません。
しかし、ここで重要な部分があります。プロセスがクラッシュしたときにどうやって回復するかということです。答えは簡単で、RAM 内のすべてのスナップショットを定期的に撮るだけです。
ちょっと待ってください。最後のスナップショットから変更を加えた場合はどうなるでしょうか。ここが賢いところです。RAMの一部を変更するたびに、トランザクションをディスクに書き込むようにします。次のような行があるとします。 foo.setBar(2)、これはまず、変更したことを示すトランザクションを書き込みます bar の分野 foo 2に設定し、実際にフィールドを2に設定します。 new Foo() Foo オブジェクトが作成されたことを示すトランザクションをディスクに書き込み、新しいオブジェクトを返します。
そのため、プロセスがクラッシュして再起動すると、まずスナップショットが再ロードされ、トランザクションログが再生されて状態が完全に回復されます。(インデックスの変更はトランザクションログの一部である必要はありません。たとえば、フィールドにインデックスがある場合、 bar から Foo、 それから setBar インデックスを更新するだけで、スナップショットから読み取られたか、トランザクションから読み取られたかに関係なく更新されます。
最後に、このアーキテクチャにより、これまでは書けなかった新しい種類のコードが可能になります。すべてのリクエストが同じプロセスによって処理されるため、 いつもの 終了されないということは、ページを提供するために使用できるクロージャをメモリに保存できることを意味します。たとえば、Screenshotbotで https://screenshotbot.io/n/nnnnnnn URLは実際にはサーバー上のクロージャであり、 んんんんん 内部クロージャにマップされます。しかし驚くべきことに、この単純な変更により、ページ遷移にわたってオブジェクトをシリアル化する必要がなくなります。クロージャにはオブジェクトへの参照があるため、すべてのリクエストにわたってオブジェクト ID を渡す必要はありません。JavaScript では、これは仮に次のようになります。
function renderMyObject(obj) {
return ...
obj.delete()) >Delete
...
}
つまり、すばやく反復できるということです。デバッグする必要がある場合、デバッグする必要があるサービスは 1 つだけです。コードをプロファイルする必要がある場合、プロファイルする必要があるサービスは 1 つだけです (MySQL のスロー クエリ ログは不要)。監視するサービスは 1 つだけです。その 1 つのサービスがダウンすると、サイトは確実にダウンしますが、サービスとサーバーは 1 つしかないため、障害の可能性も大幅に低くなります。サーバーがダウンすると、AWS は数分以内に新しいサーバーを自動的に起動して置き換えます。
また、データベースをモック化する必要がなくなったため、テスト コードの記述もはるかに簡単になりました。
拡大する
つまり、迅速に行動し、反復し、アイデアを構築し、その過程でゆっくりと顧客を獲得するのです。
そしてある日、あなたは著名な顧客を獲得しました。ビンゴ、あなたは今 拡大する スタートアップのフェーズ。
しかし、問題があります。この著名な顧客は 99.999% の可用性を要求しているのです。
確かに、先ほど説明したアーキテクチャではこれを処理できません。サーバーがダウンした場合、AWS がサーバーを復旧するまで数分間待つ必要があります。復旧後も、プロセスがディスクからスナップショットを復元するまでに数分間待つ必要がある場合があります。再デプロイも難しい作業です。サービスを再起動すると、サーバーが数分間ダウンする可能性があります。
ここで Raft コンセンサス プロトコルが登場します。
Raftは素晴らしいアルゴリズムとプロトコルです。 有限状態機械 (Web サーバー/データベース) をターゲットとし、基本的にトランザクション ログを複製します。これで、非常にシンプルなアーキテクチャを採用し、3 台のマシンに複製できるようになりました。リーダーがダウンした場合、数秒以内に新しいリーダーが選出され、リクエストの処理を継続します。
私たちは、開発者がコードを書く方法を根本的に変えることなく、シンプルで小さなサービスを本格的な高可用性データベースに変えました。
このメカニズムを使用すると、サーバーを停止することなくローリング デプロイを実行することもできます (ただし、サーバー プロセスを再起動することはほとんどありませんが、これについては後で詳しく説明します)。サービスが 1 つしかないため、可用性の保証を計算することも簡単です。
抽出する
あなたのスタートアップは順調に業績を伸ばしており、何千もの大口顧客を抱えています。
正直に言うと、Screenshotbot はまだこの段階ではありません。現在の状況については後ほど説明します。ただし、予測されるボトルネックを監視するなど、この可能性に備えています。
ここでの解決策は、大企業がデータベースで既に行っているシャーディングです。Web サービスをシャードに分割し、各シャードを独自のクラスターにすることができます。特に、Screenshotbot では、既にこれを実行しています。各エンタープライズ カスタマーには専用のクラスターが与えられます。(おもしろい話: Meta ラフトに切り替え 各 MySQL クラスターのレプリケーションを処理するため、基本的には同じことを行いますが、別のデータベースは使用しません。
私は今日の問題を解決するタイプの人間なので、他に何を期待すればよいかわかりません。私が予想する主なボトルネックは、コミット スレッドのスケーリングです。読み取りスレッドは見事に並列化されています。各トランザクションを 1 つずつ適用するコミット スレッドが 1 つあります。Raft アルゴリズムは複数のトランザクションをまとめてディスクにコミットするため、ディスク レイテンシはこれに関係ないことがわかりました。私の主な懸念は、トランザクションを適用するための CPU コストがシングル コアのパフォーマンスを超えることです。このような状況が発生するとは到底考えられませんが、可能性はあります。この時点で、コミットのコストをプロファイルして改善するか (たとえば、一部の作業をトランザクション スレッドから移動する)、シャーディングを検討することができます。それが実現したら、別のブログ記事を書くことになるでしょう。
私たちのスタック
アイデアについて説明したので、次は私たちのスタックについて、そしてそれがなぜこのアーキテクチャに適しているのかについて説明します。
私たちはCommon Lispを使用しています。スクリーンショットボットの最初の実装ではMySQLを使用していましたが、すぐに bknr.データストア MySQLでの同時実行の処理は難しく、Screenshotbotは高度な同時実行アプリケーションであるため、BKNR Datastoreは、 探検する セクションと同じですが、Common Lisp 用に構築されています。(他の言語にも同様のライブラリがありますが、それほど多くはありません。)
Common Lisp はマルチスレッド化も高度に行われており、Web リクエストが単一プロセス内のスレッドによって処理されるため、このアーキテクチャではこれが重要になります。Ruby や Python はこの要件を満たしません。
先ほど述べたクロージャの考え方も使用します。ただし、これはサーバーを頻繁に再起動できないことを意味します (サーバーを再起動すると、クロージャが失われます)。したがって、コードの再ロードは、実行中のプロセスでコードをホット リロードするだけです。Common Lisp はこの点で優れていることがわかり、標準の大部分はコードの再ロードの処理に関するものになっています (たとえば、クラス定義が変更された場合、そのクラスのオブジェクトをどのように更新しますか? そこには基準がある。
時々、サーバーを再起動します。現在、サーバーを再起動するのは 1 か月か 2 か月に 1 回程度です。再起動する必要がある場合は、Raft クラスターでローリング再起動を実行します。インストールごとに 3 台のサーバーのクラスターを使用しており、1 台のサーバーがダウンしても問題ありません。Kubernetes は使用していません。必要ありません (少なくとも今のところは)。
Raft実装では、bknr.datastoreをベースに独自のカスタムライブラリを作成しました。 bknr.クラスター、ボンネットの下に素晴らしい ブラフト Baidu のライブラリです。Braft は非常に堅牢なので、強くお勧めします。Braft はバックグラウンド スナップショットも処理するため、スナップショットを作成している間も、サーバーは引き続きリクエストに応答できます。
画像ファイルや、データストアの一部ではない BLOB を保存するには、3 つのサーバー間で共有される EFS (高可用性 NFS) を使用します。EFS はエラー状態を処理する必要がないため、S3 よりも簡単に操作できます。また、EFS では外部サーバーとやり取りせず、ディスクに書き込むだけなので、コードのテストが容易になります。
これはどの程度スケールしますか? これは、私たちの目的に十分対応できるほどの拡張性があります。私たちには大企業の顧客が数社ありますが、特に有名な顧客が 1 社あります。Screenshotbot は彼らの CI 上で実行されるため、コミットやプル リクエストごとに API リクエストが何百回も届きます。それにもかかわらず、彼らのリクエストに対応するために必要なのは、4 コア 16GB のマシンだけです (レプリカ用にも同様のマシンがあり、ほとんどがアイドル状態で稼働しています)。それでも CPU 使用率は最大 20% になりますが、それでもそのほとんどは画像処理によるものなので、コア数を増やす必要が生じる前に拡張する余地は十分にあります。必要のないスケールに合わせて設計しないことが重要です。
まとめ
このアーキテクチャは新しいスタートアップにとって素晴らしいと思いますし、もっと多くの企業がこれを採用してくれることを願っています。もちろん、選択した言語用に私たちが構築したツールのいくつかを構築する必要があります。(ただし、Common Lisp を使用する場合は、すべてここで使用できます。 すべてオープンソース。
私たちは、bknr.datastore、Braft、Raft の背後にいる人々に非常に感謝しています。彼らの努力がなければ、私たちはこれらを何も実現できなかったでしょう。
もしこれが役に立ったり、興味深いと思ったら、ソーシャルメディアでシェアしていただけると嬉しいです。 [email protected] ご質問がございましたら。
#データベースなしで高可用性 #Web #サービスを構築する #Screenshotbot #ブログ