Braze が Ruby を大規模に活用する方法
公開: 2022-08-18あなたがハッカー ニュース、開発者の Twitter、またはその他の同様の情報源を読んでいるエンジニアであれば、「Rust vs C の速度」、「Node.js を作るもの」などのタイトルの記事をほぼ確実に 1,000 も目にしたことがあるでしょう。 js は Java よりも高速ですか?」、または「Golang を使用する理由と開始方法」。 これらの記事では、一般的に、スケーラビリティーや速度の点で明らかに選択できる特定の言語が 1 つあり、それを採用するしかないことを主張しています。
私が大学にいて、エンジニアとしての最初の 1 年か 2 年の間に、私はこれらの記事を読み、すぐに新しい言語やフレームワークを定期的に学習するためのお気に入りのプロジェクトを立ち上げました。 結局のところ、それは「地球規模で」「これまでに見たことのない速度で」動作することが保証されていました。 最終的に、ほとんどのプロジェクトでは、これらの非常に特殊なものはどちらも実際には必要ないことがわかりました。 キャリアが進むにつれ、どの言語やフレームワークを選択しても、実際にこれらのことが無料で得られるわけではないことに気付きました。
代わりに、言語やフレームワークではなく、システムのスケーリングを検討しているときに実際に最大の手段となるのはアーキテクチャであることを発見しました。
Braze では、巨大な世界規模で事業を展開しています。 はい、Ruby と Rails を 2 つの主要なツールとして使用しています。 ただし、すべてを可能にする "global_scale = true" 構成値はありません。これは、アプリケーションの奥深くから展開トポロジに至るまで、よく考え抜かれたアーキテクチャの結果です。 Braze のエンジニアは、スケーリングのボトルネックを常に調査し、システムを高速化する方法を考え出しています。その答えは通常、「Ruby から離れること」ではありません。アーキテクチャの変更になることはほぼ確実です。
それでは、Braze が思慮深いアーキテクチャを活用して実際に速度と大規模なグローバル スケールを解決する方法と、Ruby と Rails がどこに適合するか (および適合しないか) を見てみましょう。
クラス最高のアーキテクチャの力
シンプルな Web リクエスト
当社の事業規模から、お客様のユーザー ベースに関連付けられたデバイスが毎日何十億もの Web リクエストを作成し、Braze Web サーバーで処理する必要があることがわかっています。 また、最も単純な Web サイトでも、クライアントからサーバーへのリクエストとその逆のリクエストに関連する比較的複雑なフローが発生します。
クライアントの DNS リゾルバー (通常は ISP) が、Web サイトの URL のドメインに基づいて、どの IP アドレスに移動するかを判断することから始まります。
クライアントが IP アドレスを取得すると、リクエストがゲートウェイ ルーターに送信され、ゲートウェイ ルーターはそのリクエストを「ネクスト ホップ」ルーターに送信します (これは数回発生する可能性があります)。これは、リクエストが宛先 IP アドレスに到達するまで続きます。
そこから、リクエストを受信したサーバーのオペレーティング システムがネットワークの詳細を処理し、Web サーバーの待機中のプロセスに、リッスンしていたソケット/ポートで着信リクエストを受信したことを通知します。
Web サーバーは、応答 (要求されたリソース、おそらく index.html) をそのソケットに書き込みます。これは、ルーターを経由してクライアントに戻ります。
シンプルなウェブサイトにはかなり複雑なものですよね? 幸いなことに、これらの多くは私たちに代わって処理されます (これについては後で詳しく説明します)。 しかし、私たちのシステムには、データ ストア、バックグラウンド ジョブ、同時実行性の問題など、対処しなければならないことがまだあります。 それがどのように見えるかを見てみましょう。
スケールをサポートする最初のシステム
DNS とネーム サーバーは通常、ほとんどの場合、それほど注意を払う必要はありません。 トップレベル ドメインのネーム サーバーには、おそらく "yourwebsite.com" をドメインのネーム サーバーにマップするためのエントリがいくつかあります。Amazon Route 53 や Azure DNS などのサービスを使用している場合は、それらが名前を処理します。ドメインのサーバー (たとえば、A、CNAME、またはその他の種類のレコードの管理)。 通常、この部分のスケーリングについて考える必要はありません。これは、使用しているシステムによって自動的に処理されるためです。
ただし、フローのルーティング部分は興味深いものになる可能性があります。 Open Shortest Path First や Routing Information Protocol など、いくつかの異なるルーティング アルゴリズムがあり、それらはすべて、クライアントからサーバーへの最速/最短ルートを見つけるように設計されています。 インターネットは事実上巨大な接続されたグラフ (またはフロー ネットワーク) であるため、活用できる複数のパスが存在する可能性があり、それぞれに対応する高コストまたは低コストがあります。 絶対的に最速のルートを見つける作業を行うのは非常に困難であるため、ほとんどのアルゴリズムは適切なヒューリスティックを使用して許容可能なルートを取得します。 コンピューターとネットワークは常に信頼できるとは限らないため、Fastly を使用してクライアントのサーバーへのルーティング機能を強化しています。
Fastly は、世界中のポイント オブ プレゼンス (POP) に非常に高速で信頼性の高い接続を提供することで機能します。 それらをインターネットの州間幹線道路と考えてください。 ドメインの A および CNAME レコードは Fastly を指しているため、クライアントのリクエストは高速道路に直接送られます。 そこから、Fastly はそれらを適切な場所にルーティングできます。
ろう付けの正面玄関
クライアントのリクエストは Fastly の高速道路を通り、Braze プラットフォームの正面玄関に到着しました。次に何が起こるのでしょうか?
単純なケースでは、フロント ドアはリクエストを受け入れる単一のサーバーになります。 ご想像のとおり、これではうまくスケーリングできないため、実際には Fastly を一連のロード バランサーにポイントします。 ロード バランサーが使用できるさまざまな戦略がありますが、このシナリオでは、Fastly がロード バランサーのプールへのリクエストを均等にラウンド ロビンすることを想像してみてください。 これらのロード バランサーはリクエストをキューに入れ、それらのリクエストを Web サーバーに分散します。これは、クライアント リクエストがラウンドロビン方式で処理されていることも想像できます。 (実際には、特定の種類のアフィニティには利点があるかもしれませんが、それは別の機会に取り上げます。)
これにより、取得するリクエストのスループットと処理できるリクエストのスループットに応じて、ロード バランサーの数と Web サーバーの数をスケールアップできます。 これまでのところ、汗をかくことなく巨大なリクエストの猛攻撃を処理できるアーキテクチャを構築しました! ロード バランサーのリクエスト キューの弾力性を介してバースト トラフィック パターンを処理することもできます。これは素晴らしいことです。
ウェブサーバー
最後に、エキサイティングな (Ruby) 部分である Web サーバーに到達します。 Ruby on Rails を使用していますが、これは単なる Web フレームワークであり、実際の Web サーバーは Unicorn です。 Unicorn は、マシン上で多数のワーカー プロセスを開始することによって機能します。各ワーカー プロセスは、OS ソケットで作業をリッスンします。 プロセス管理を処理し、リクエストの負荷分散を OS 自体に任せます。 要求をできるだけ速く処理するための Ruby コードが必要なだけです。 他のすべては、Ruby の外部で効果的に最適化されています。
お客様のアプリケーション内の SDK によって、または REST API を介して行われるリクエストの大部分は非同期であるため (つまり、クライアントに特定の応答を返すために操作が完了するのを待つ必要はありません)、 API サーバーは非常にシンプルです。リクエストの構造、API キーの制約を検証し、Redis キューにリクエストを投げて、すべてがチェックアウトされたらクライアントに 200 レスポンスを返します。
この要求/応答サイクルは、Ruby コードの処理に約 10 ミリ秒かかり、その一部は Memcached と Redis での待機に費やされます。 これをすべて別の言語で書き直したとしても、これ以上のパフォーマンスを引き出すことは実際には不可能です。 そして最終的には、これまで読んできたすべてのアーキテクチャーにより、このデータ取り込みプロセスを拡張して、増え続けるお客様のニーズを満たすことができます。
ジョブ キュー
これは過去に検討したトピックなので、ここでは深く掘り下げません。ジョブ キューイング システムの詳細については、キューによる回復力の達成に関する私の投稿をご覧ください。 大まかに言うと、ジョブ キューとして機能する多数の Redis インスタンスを活用し、実行する必要がある作業をさらにバッファリングします。 当社の Web サーバーと同様に、これらのインスタンスは、特定のアベイラビリティ ゾーンで問題が発生した場合により高い可用性を提供するために、アベイラビリティ ゾーン全体に分割され、冗長性のために Redis Sentinel を使用してプライマリ/セカンダリ ペアで提供されます。 これらを水平方向と垂直方向の両方にスケーリングして、容量とスループットの両方を最適化することもできます。
労働者
これは確かに最も興味深い部分です。どのようにしてワーカーをスケールさせるのでしょうか?
何よりもまず、ワーカーとキューは、顧客、作業の種類、必要なデータ ストアなど、いくつかのディメンションによってセグメント化されています。これにより、高可用性を実現できます。 たとえば、特定のデータ ストアに問題がある場合でも、他の機能は問題なく動作し続けます。 また、これらのディメンションに応じて、ワーカー タイプを個別に自動スケーリングすることもできます。 つまり、特定の種類の作業が増えれば、より多くのワーカーをスケールアップできます。
ここから、言語またはフレームワークの選択が問題になる可能性があります。 最終的に、より効率的なワーカーは、より多くの作業をより迅速に行うことができます。 C や Rust などのコンパイル済み言語は、Ruby などのインタープリター型言語よりも計算タスクではるかに高速になる傾向があり、一部のワークロードではより効率的なワーカーにつながる可能性があります。 しかし、私はトレースを調べるのに多くの時間を費やしており、Braze の全体像から見ると、生の CPU 処理は驚くほど少ない量です。 処理時間のほとんどは、データ ストアや外部リクエストからの応答を待つことに費やされており、数値を処理することはありません。 そのために高度に最適化された C コードは必要ありません。
データストア
これまでに説明したことはすべて、かなりスケーラブルです。 それでは、少し時間を取って、従業員が最も多くの時間を費やす場所、つまりデータ ストアについて話しましょう。
SQL データベースを使用する Web サーバーまたは非同期ワーカーをスケールアップしたことがある人なら誰でも、特定のスケールの問題、つまりトランザクションに遭遇したことがあるでしょう。 2 つの FulfillmentRequest と PaymentReceipt を作成する Order の完了を処理するエンドポイントがある場合があります。 トランザクションでこれがすべて行われないと、データの一貫性が失われる可能性があります。 1 つのデータベースで多数のトランザクションを同時に実行すると、ロックやデッドロックに多くの時間が費やされる可能性があります。 Braze では、オブジェクトの独立性と最終的な整合性を通じて、データ モデル自体でそのスケーリングの問題に正面から取り組んでいます。 これらの原則により、データ ストアから多くのパフォーマンスを引き出すことができます。

独立したデータ オブジェクト
非常に正当な理由により、Braze では MongoDB を多用しています。つまり、MongoDB シャードを実質的に水平方向にスケーリングし、ストレージとパフォーマンスをほぼ直線的に増加させることができるからです。 ユーザー プロファイルは相互に独立しているため、これは非常にうまく機能します。ユーザー プロファイル間で維持する JOIN ステートメントや制約関係はありません。 各顧客が成長したり、新しい顧客を追加したり (またはその両方) した場合、新しいデータベースと新しいシャードを既存のデータベースに追加するだけで、容量を増やすことができます。 このレベルのスケーラビリティを維持するために、マルチドキュメント トランザクションのような機能は明示的に避けています。
MongoDB とは別に、分析情報のバッファリングなどの一時的なデータ ストアとして Redis をよく利用します。 これらの分析の多くの信頼できる情報源は、MongoDB に独立したドキュメントとして一定期間存在するため、バッファとして機能する Redis インスタンスの水平方向にスケーラブルなプールを維持します。 このアプローチでは、ハッシュされたドキュメント ID がキーベースのシャーディング スキームで使用され、独立性によって負荷が均等に分散されます。 定期的なジョブは、水平方向にスケーリングされたデータ ストアから別の水平方向にスケーリングされたデータ ストアにこれらのバッファーをフラッシュします。 スケール達成!
さらに、上記のジョブ キューと同様に、これらのインスタンスにも Redis Sentinel を使用します。 また、さまざまな目的のためにこれらの Redis クラスターの多数の「タイプ」をデプロイし、制御された障害フローを提供します (つまり、特定のタイプの Redis クラスターに問題がある場合、無関係な機能が同時に失敗し始めることはありません)。
結果整合性
Braze は、ほとんどの読み取り操作の原則として結果整合性も活用します。 これにより、ほとんどの場合、MongoDB レプリカ セットのプライマリ メンバーとセカンダリ メンバーの両方からの読み取りを活用できるようになり、アーキテクチャがより効率的になります。 このデータ モデルの原則により、スタック全体でキャッシュを多用することができます。
Memcached を使用したマルチレイヤー アプローチを使用します。基本的に、データベースからドキュメントをリクエストするときは、最初にマシンローカルの Memcached プロセスを非常に短い存続時間 (TTL) でチェックし、次にリモートの Memcached インスタンスをチェックします (データベースに直接問い合わせる前に。 これにより、顧客設定やキャンペーンの詳細などの一般的なドキュメントのデータベース読み取りを大幅に削減できます。 「最終的」というと恐ろしく聞こえるかもしれませんが、実際にはほんの数秒であり、このアプローチを採用すると、真実のソースからの膨大な量のトラフィックが削減されます。 コンピューター アーキテクチャのクラスを受講したことがある場合は、このアプローチが CPU の L1、L2、および L3 キャッシュ システムのしくみといかに似ているかを認識しているかもしれません。
これらのトリックを使用すると、おそらくアーキテクチャの最も遅い部分から多くのパフォーマンスを絞り出し、スループットまたは容量のニーズが増加したときに、必要に応じて水平方向にスケーリングできます。
Ruby と Rails が適合する場所
ここに問題があります。各レイヤーが水平方向に適切にスケーリングする全体的なアーキテクチャを構築するために多大な労力を費やすと、言語またはランタイムの速度は、思っているよりもはるかに重要ではないことがわかります。 つまり、言語、フレームワーク、およびランタイムの選択は、まったく異なる要件と制約のセットで行われます。
Ruby と Rails は、Braze が 2011 年に開始されたとき、チームの迅速なイテレーションを支援してきた実績があり、それを可能にし続けているため、GitHub、Shopify、およびその他の主要なブランドで今でも使用されています。 これらは引き続き Ruby および Rails コミュニティによってそれぞれ積極的に開発されており、どちらもさまざまなニーズに対応できる優れたオープンソース ライブラリ セットを今でも持っています。 このペアは、非常に高い柔軟性があり、一般的なユースケースに対してかなりの単純さを維持しているため、高速反復に最適です。 私たちはそれを毎日使っていると圧倒的に真実であることがわかります.
これは、Ruby on Rails が誰にとってもうまく機能する完璧なソリューションであると言っているわけではありません。 しかし、Braze では、データ取り込みパイプライン、メッセージ送信パイプライン、および顧客向けダッシュボードの大部分を強化するのに非常にうまく機能することを発見しました。これらはすべて迅速な反復を必要とし、Braze の成功の中心です。プラットフォーム全体。
Rubyを使わないとき
ちょっと待って! Braze で行うすべての作業が Ruby で行われるわけではありません。 ここ数年、さまざまな理由から、他の言語やテクノロジに向けて方向転換するよう呼びかけた場所がいくつかあります。 そのうちの 3 つを見てみましょう。Ruby に頼る場合と頼らない場合についての追加の洞察を提供するためです。
1. 送信者サービス
結局のところ、Ruby は非常に高度な同時ネットワーク リクエストを 1 つのプロセスで処理するのが得意ではありません。 これは問題です。なぜなら、Braze が顧客に代わってメッセージを送信している場合、最終サービス プロバイダーによっては、ユーザーごとに 1 つの要求が必要になる場合があるからです。 100 通のメッセージを送信する準備ができたら、次のメッセージに進む前に、それぞれのメッセージが終了するのを待ちたくありません。 そのすべての作業を並行して行うことをお勧めします。
「Sender Services」、つまり Golang で記述されたステートレス マイクロサービスに入ります。 上記の例の Ruby コードは、これらのサービスの 1 つに 100 個のメッセージすべてを送信できます。サービスはすべてのリクエストを並行して実行し、それらが完了するのを待ってから、Ruby に一括応答を返します。 これらのサービスは、並行ネットワークに関して言えば、Ruby で実現できるものよりも大幅に効率的です。
2.電流コネクタ
Braze Currents の大容量データ エクスポート機能により、Braze のお客様は、多数のデータ パートナーの 1 つまたは複数にデータを継続的にストリーミングできます。 プラットフォームは Apache Kafka を利用しており、ストリーミングは Kafka コネクタを介して行われます。 これらは技術的には Ruby で記述できますが、正式にサポートされている方法は Java を使用することです。 また、Java が高度にサポートされているため、これらのコネクタの作成は、Ruby よりも Java の方がはるかに簡単です。
3.機械学習
機械学習の仕事をしたことがある人なら、選択する言語が Python であることをご存知でしょう。 Python の機械学習ワークロード用の多数のパッケージとツールは、同等の Ruby サポートを覆い隠しています。TensorFlow や Jupyter ノートブックなどは私たちのチームにとって重要ですが、これらのタイプのツールは Ruby の世界には存在しないか、十分に確立されていません。 したがって、機械学習を活用する製品の要素を構築する際には、Python に傾倒しています。
言語が重要な場合
明らかに、Ruby が理想的な選択ではなかった上記のいくつかの素晴らしい例があります。 別の言語を選択する理由はたくさんあります。ここでは、考慮すべき特に役立つと思われるいくつかの理由を紹介します。
コストを切り替えずに新しいものを構築する
新しいドメイン モデルを使用し、既存の機能との密結合の統合を行わない、まったく新しいシステムを構築する場合は、必要に応じて別の言語を使用する機会があるかもしれません。 特に、組織がさまざまな機会を評価している場合、小規模で孤立したグリーンフィールド プロジェクトは、新しい言語やフレームワークを試すための優れた現実世界の実験になる可能性があります。
タスク固有の言語エコシステムと人間工学
一部のタスクは、特定の言語またはフレームワークを使用するとはるかに簡単になります。ダッシュボード機能の開発には Rails と Grape が特に気に入っていますが、オープンソース ツールが存在しないため、機械学習コードを Ruby で作成するのは絶対に悪夢です。 ある種の機能や統合を実装するために特定のフレームワークやライブラリを使用したい場合があり、言語の選択はそれによって影響を受けることがあります。これにより、ほぼ確実に開発エクスペリエンスがより簡単または高速になるためです。
実行速度
場合によっては、生の実行速度を最適化する必要があり、使用する言語がそれに大きく影響します。 多くの高頻度取引プラットフォームと自動運転システムが C++ で記述されているのには十分な理由があります。 ネイティブにコンパイルされたコードは非常に高速です。 私たちの Sender Services は、まさにその理由で Ruby では利用できない Golang の並列処理/同時実行プリミティブを利用します。
開発者の習熟度
一方、孤立したものを構築している、または使用したいライブラリを念頭に置いている可能性がありますが、言語の選択はチームの他のメンバーにはまったくなじみがありません。 関数型プログラミングに大きく傾いた Scala で新しいプロジェクトを導入すると、チームの他の開発者に親しみやすさの障壁が生じる可能性があり、最終的には知識の孤立やネット速度の低下につながります。 Braze ではこれが特に重要であると考えています。高速なイテレーションに重点を置いているため、組織ですでに広く使用されているツール、ライブラリ、フレームワーク、および言語の使用を奨励する傾向があります。
最終的な考え
過去にさかのぼって、巨大なシステムでのソフトウェア エンジニアリングについて 1 つのことを自分に言い聞かせるとしたら、次のようになります。ほとんどのワークロードでは、全体的なアーキテクチャの選択によってスケーリングの限界が決まり、言語の選択よりも速くなります。 その洞察は、ここブレイズで毎日証明されています。
Ruby と Rails は、適切に設計されたシステムの一部である場合、信じられないほどうまくスケーリングできる素晴らしいツールです。 Rails は非常に成熟したフレームワークでもあり、真の顧客価値を迅速に反復して生み出すという Braze の文化を支えています。 これらのことから、Ruby と Rails は私たちにとって理想的なツールであり、今後何年も使い続ける予定のツールです。
Brazeで働くことに興味がありますか? エンジニアリング、製品管理、ユーザー エクスペリエンスの各チームで、さまざまな職種を募集しています。 採用情報ページをチェックして、私たちの募集中の役割と私たちの文化について学んでください。
