ハイパー成長に対処するためのバックエンドインフラ 2019年半ばには、CeleryとRabbitMQの2つの技術が、注文のチェックアウトやダッシャーの割り当てなど、当社のプラットフォームの重要な機能を可能にする非同期作業を処理するシステムに電力を供給する重要なスケーリングの課題と頻繁な停止に直面しました。
私たちは、堅牢なソリューションを反復し続けながら停止を停止した単純なApache Kafkaベースの非同期タスク処理システムでこの問題を迅速に解決しました。 私たちの最初のバージョンは、既存のCeleryタスクの大部分に対応するために必要な最小限の機能セットを実装しました。 本番環境では、Kafkaを使用したときに発生した新しい問題に対処しながら、Celery機能のサポートをさらに追加し続けました。
CeleryとRabbitMQを使用して直面した問題
RabbitMQとCeleryは、注文チェックアウト、商人の注文送信、Dasherの位置処理など、DoorDashで900以上の異なる非同期タスクを実行する DoorDashが直面した問題は、rabbitmqが過度の負荷のために頻繁にダウンしていたことでした。 タスク処理がダウンした場合、DoorDashは効果的にダウンし、注文を完了することができませんでした,私たちの商人やダッシュのための収益損失の結果,そし 私たちは、次の面で問題に直面しました。
- 可用性:需要による停止可用性の低下。
- スケーラビリティ:RabbitMQは、当社のビジネスの成長に合わせて拡張することができませんでした。
- 可観測性:RabbitMQは限られたメトリックを提供し、セロリの労働者は不透明でした。
- 運用効率:これらのコンポーネントを再起動するには、時間がかかり、手動のプロセスでした。
非同期タスク処理システムが高可用性ではなかった理由
私たちが直面した最大の問題は停止であり、需要がピークに達したときに RabbitMQは、負荷、過度の接続解約、およびその他の理由によりダウンします。 注文は停止され、停止から回復するためには、システムを再起動するか、まったく新しいブローカーを起動して手動でフェールオーバーする必要があります。
可用性の問題をより深く掘り下げてみると、次のサブ問題が見つかりました。
- Celeryは、ユーザーがカウントダウンまたはETAで将来のタスクをスケジュー これらのカウントダウンの私達の重い使用は仲介商の顕著な負荷増加で起因した。 私たちの停止のいくつかは、カウントダウンを伴うタスクの増加に直接関連していました。 私たちは最終的に、将来の作業のスケジューリングのために別のシステムを支持して、カウントダウンの使用を制限することにしました。
- 突然のトラフィックのバーストにより、rabbitmqはタスクの消費量が予想よりも大幅に低下した状態になります。 私たちの経験では、これはRabbitMQバウンスでのみ解決できました。 RabbitMQにはフロー制御の概念があり、キューが追いつくことができるように、あまりにも速く公開されている接続の速度を低下させます。 フロー制御は、多くの場合、常にではないが、これらの可用性の低下に関与していました。 フロー制御が開始されると、パブリッシャーは効果的にネットワーク遅延と認識します。 トラフィックのピーク時にレイテンシが増加すると、要求が上流に積み重なるにつれて大幅な速度低下が起こる可能性があります。
- 私たちのpython uWSGI webワーカーには、タイムアウトを超えたプロセスを強制終了できるharakiriという機能がありました。 停止や減速の間、harakiriはプロセスが繰り返し殺され、再起動されたとして、RabbitMQブローカーへの接続解約をもたらしました。 何千人ものwebワーカーがいつでも実行されているため、harakiriを引き起こした遅さは、RabbitMQに余分な負荷を追加することによって、遅さにさらに貢献します。
- 生産では、重大な負荷がない場合でも、セロリ消費者のタスク処理が停止したいくつかのケースを経験しました。 私たちの調査の努力は、処理を停止したであろうリソースの制約の証拠をもたらさず、労働者は、彼らがバウンスされた後に処理を再開しました。 この問題は根本的な原因ではありませんでしたが、RabbitmqではなくCelery worker自体に問題があると思われます。
全体として、これらの可用性の問題はすべて、高い信頼性が最優先事項の一つであるため、私たちにとって受け入れられませんでした。 これらの停止は、逃した注文と信頼性の面で私たちに多くの費用がかかっていたので、私たちはできるだけ早くこれらの問題に対処する解決策を必
レガシーソリューションがスケールしなかった理由
次の最大の問題はスケールでした。 DoorDashは急速に成長しており、我々はすぐに私たちの既存のソリューションの限界に達していました。 従来のソリューションには次の問題があったため、継続的な成長に追いつくものを見つける必要がありました。
垂直スケーリングの限界に達する
利用可能な最大のシングルノードRabbitMQソリューションを使用していました。 これ以上垂直方向にスケールするパスはなく、すでにそのノードを限界までプッシュし始めていました。
高可用性モードでは容量が制限されていました
レプリケーションのため、プライマリ-セカンダリ高可用性(HA)モードでは、シングルノードオプションに比べてスループットが低下し、シングルノードソリューションよりもヘッドルームがさらに少なくなりました。 私たちは、可用性のためにスループットを交換する余裕がありませんでした。第二に、プライマリ-セカンダリHAモードは、実際には、私たちの停止の重大度を軽減しませんでした。
フェールオーバーは完了するまでに20分以上かかり、しばしば手動の介入を必要とする立ち往生します。 メッセージは、多くの場合、同様にプロセスで失われました。
DoorDashが成長し続け、タスク処理を限界まで押し進めているため、私たちはすぐにヘッドルームを使い果たしていました。 私たちは、処理ニーズの増加に応じて水平に拡張できるソリューションが必要でした。
CeleryとRabbitMQがどのように限られた可観測性を提供したか
どのようなシステムで何が起こっているのかを知ることは、可用性、スケーラビリティ、
上記で概説した問題をナビゲートしたとき、
- 私たちは利用可能なRabbitMQメトリックの小さなセットに制限されていました。
- 私たちは
- 私たちはセロリの労働者自身への可視性が限られていました。
私たちは、システムのあらゆる側面のリアルタイムのメトリックを見ることができる必要があり、観測可能性の制限にも対処する必要があ
運用効率の課題
RabbitMQの運用に関するいくつかの問題にも直面しました。
- 観察した永続的な劣化を解決するために、RabbitMQノードを新しいノードにフェール この操作は、関係するエンジニアのための手動と時間がかかり、多くの場合、ピーク時の外に、夜遅くに行われなければならなかった。
- DoorDashには、この技術のスケーリング戦略を策定するために傾くことができる社内のCeleryやRabbitMQの専門家はいませんでした。
Rabbitmqの運用と維持に費やされたエンジニアリング時間は持続可能ではありませんでした。 私たちは、より良い私たちの現在および将来のニーズを満たした何かを必要としました。
CeleryとRabbitMQの問題に対する潜在的な解決策
上記の問題で、次の解決策を検討しました。
- CeleryブローカをRabbitmqからRedisまたはKafkaに変更します。 これにより、Celeryを引き続き使用することができ、異なる潜在的により信頼性の高いバッキングデータストアが使用できます。
- Djangoアプリにマルチブローカーのサポートを追加して、消費者が望むロジックに基づいてN個の異なるブローカーに公開できるようにしました。 タスク処理は複数のブローカー間で分割されるため、各ブローカーは初期負荷のほんの一部を経験します。
- 新しいバージョンのCeleryとRabbitMQにアップグレードします。 CeleryとRabbitMQの新しいバージョンでは、信頼性の問題を修正することが期待されていましたが、Django monolithからコンポーネントを並行して抽出していたため、時間がかかりました。
- Kafkaがサポートするカスタムソリューションに移行します。 このソリューションは、私たちがリストした他のオプションよりも多くの労力を要しますが、従来のソリューションで抱えていたすべての問題を解
各オプションには長所と短所があります:
オプション 長所 短所 redis as broker - ElasticCacheとマルチAZサポートによる可用性の改善
- ElasticCacheによるブローカの可観測性の改善
- ElasticCacheによるブローカの可観測性の改善
- ブローカーとして
- 改善された運用効率
- redisとの社内運用経験と専門知識
- ブローカースワップは、セロリでサポートされているオプションとして直 Redisクラスター化モードでは
- 単一ノードRedisは水平にスケールしません
- セロリの可観測性の改善はありません
- このソリューションは、セロリのワーカーがタスクの処理を停止した観測された問題に対処しません
kafkaをブローカとして - Kafkaは高可用性にすることができます
- Kafkaは水平にスケーラブルです
- 観測性を向上させましたブローカーとしてカフカ
- 改善された運用効率
- doordashは、社内カフカの専門知識を持っていました
- ブローカースワップは、セロリでサポートされているオプ
- Harakiri connection churnはKafkaのパフォーマンスを大幅に低下させません
- KafkaはまだCeleryでサポートされていません
- Celeryワーカーがタスクの処理を停止する観測された問題に対処していません
- celeryの可観測性の改善はありません
- 社内の経験にもかかわらず、DoorDashでkafkaを大規模に運用していませんでした。
複数のブローカー - 可用性の改善
- 水平スケーラビリティ
- 観測性の改善なし
- 運用効率の改善なし
- セロリのワーカーがタスクの処理を停止する観測された問題に対処していない
- ハラキリによる接続チャーンの問題に対処していない
- li>
アップグレードバージョン - rabbitmqが劣化した状態で立ち往生する問題を改善する可能性があります
- セロリのワーカーが動けなくなる問題を改善する可能性があります
- 長期的な戦略を実装するためにヘッドルームを購入する可能性があります
- 観測されたバグを修正することは保証されていません
- 可用性、スケーラビリティ、可観測性、運用効率に関する問題をすぐに修正することはありません
- RabbitMQとCeleryの新しいバージョンは、Pythonの新しいバージョンを必要としました。
- ハラキリによる接続チャーンの問題に対処していません
カスタムKafkaソリューション - Kafkaは高可用性が可能です
- Kafkaは水平にスケーラブルです
- kakfaをブローカーとした観測性が向上しました
- 運用効率が向上しました
- 社内Kafkaの専門知識
- ブローカーの変更はまっすぐです-foward
- ハラキリ接続解約はkafkaのパフォーマンスを大幅に低下させません
- セロリの労働者がタスクの処理を停止する観測された問題に対処します
- 他のすべてのオプションよりも実装するための作業
- 社内の経験にもかかわらず、DoorDashでkafkaを大規模に運用していませんでした
kafkaをオンボーディングするための戦略
必要なシステムの稼働時間を考慮して、以下に基づいてオンボーディング戦略を考案しました。最短時間で信頼性の利点を最大化するために、以下の原則。 この戦略には、次の3つのステップが含まれていました。
- 地面にぶつかると実行されます: 私たちは、構築していたソリューションの基本を、他の部分で反復しているときに活用したいと考えていました。 私たちは、新しい燃料ポンプで交換しながら、レースカーを運転するには、この戦略を例える。
- 開発者によるシームレスな採用のための設計の選択肢:私たちは、異なるインターフェイスを定義することに起因する可能性のあるすべての開発者
- ダウンタイムゼロの増分ロールアウト: 大きな派手なリリースが初めて野生でテストされ、失敗の可能性が高くなるのではなく、より長い期間にわたって野生で個別にテストできる小さな独立した機能を出荷することに焦点を当てました。
地面にぶつかって走った
Kafkaへの切り替えは、私たちのスタックの大きな技術的な変更を表しましたが、痛んで必要なものでした。 従来のRabbitMQソリューションの不安定さのために毎週ビジネスを失っていたので、私たちは無駄にする時間がありませんでした。 私たちの第一の優先事項は、暫定的な安定性をもたらし、より広範な採用でより包括的なソリューションを反復して準備するために必要なヘッドルームを提供するために、最小実行可能な製品(MVP)を作成することでした。
私たちのMVPは、コンシューマーがそれらのメッセージを読み取り、fqnからタスクをインポートし、指定された引数と同期して実行する間に、タスクの完全修飾名(Fqn)とkafkaへのpickle化された引数を公開するプロデューサーで構成されていました。
図1: 私たちが構築することにしたMinimal Viable Product(MVP)アーキテクチャには、rabbitmqへのタスクの公開を停止する最終状態の前に、レガシー(赤の破線)と新しいシステム(緑の実線)の両方に相互に排他的なタスクを公開する中間状態が含まれていました。
開発者によるシームレスな採用のための設計の選択肢
開発者の採用は、開発よりも大きな課題です。 動的に設定可能な機能フラグに基づいて、どちらかのシステムにタスクの送信を動的にルーティングするCeleryの@task注釈のラッパーを実装することで、こ これで、同じインターフェイスを使用して両方のシステムのタスクを記述できます。 これらの決定が行われたため、エンジニアリングチームは新しいシステムと統合するための追加作業を行う必要がなくなり、単一の機能フラグを実
MVPの準備ができたらすぐにシステムを展開したかったのですが、Celeryと同じ機能はまだすべてサポートされていませんでした。 Celeryでは、ユーザーはタスク注釈のパラメータを使用してタスクを設定したり、タスクを送信したりすることができます。 より迅速に起動できるようにするために、互換性のあるパラメータのホワイトリストを作成し、大部分のタスクをサポートするために必要な機能の最小数をサポートすることを選択しました。
図2: 私たちは、まず低リスクと低優先度のタスクから始めて、KafkaベースのMVPにタスクボリュームを迅速にランプアップしました。 これらのうちのいくつかは、オフピーク時間に実行されたタスクであり、これは上記のメトリックのスパイクを説明しています。
図2に示すように、上記の2つの決定により、2週間の開発の後にMVPを起動し、起動後1週間後にRabbitMQタスク負荷を80%削減しました。 私たちはすぐに停止の私たちの主な問題に対処し、プロジェクトの過程で、残りのタスクの実行を可能にするために、より多くの難解な機能をサポー
増分ロールアウト、ダウンタイムゼロ
kafkaクラスターを切り替え、ビジネスに影響を与えることなくRabbitMQとKafkaを動的に切り替える機能は、私たちにとっ この機能は、クラスターのメンテナンス、負荷の削除、段階的な移行など、さまざまな操作にも役立ちました。 このロールアウトを実装するために、メッセージ送信レベルとメッセージ消費側の両方で動的機能フラグを利用しました。 ここで完全に動的であることのコストは、二重容量で実行されている私たちの労働者の艦隊を維持することでした。 この艦隊の半分はRabbitMQに、残りはカフカに捧げられました。 二重容量で労働者の艦隊を動かすことは確実に私達の下部組織に課税していた。 ある時点で、私たちはすべてのワーカーを収容するために完全に新しいKubernetesクラスターをスピンアップしました。
開発の初期段階では、この柔軟性がうまく機能しました。 新しいシステムに自信が持てたら、ワーカーマシンごとに複数の消費プロセスを実行するなど、インフラストラクチャの負荷を軽減する方法を検討し さまざまなトピックを移行するにつれて、小さな予備容量を維持しながら、RabbitMQの作業者数の削減を開始することができました。
解決策は完璧ではなく、必要に応じて反復します
生産中のMVPでは、製品を反復して研磨するために必要なヘッドルームがありました。 不足しているすべてのCelery機能を、最初に実装するタスクを決定するために使用したタスクの数でランク付けしました。 いくつかのタスクで使用される機能は、カスタムソリューションに実装されていませんでした。 代わりに、その特定の機能を使用しないようにこれらのタスクを書き直しました。 この戦略で、私たちは最終的にすべてのタスクをセロリから移動しました。
Kafkaを使用すると、注意が必要な新しい問題が発生しました。
- タスク処理の遅延が発生する行頭ブロック
- 展開がパーティションリバランスをトリガーし、遅延が発生しました
Kafkaの行頭ブロック問題
Kafkaトピックは、単一のコンシューマ(コンシューマグループごと)が割り当てられたパーティションのメッセージを到着した順序で読み取るようにパーティション分割されます。 単一のパーティション内のメッセージの処理に時間がかかりすぎると、下の図3に示すように、そのパーティション内の背後にあるすべてのメッセージの この問題は、優先度の高いトピックの場合には特に悲惨なことがあります。 私たちは、遅延が発生した場合にパーティション内のメッセージを処理し続けることができるようにしたいです。
図3: Kafkaの行頭ブロックの問題では、パーティション内の遅いメッセージ(赤)は、その背後にあるすべてのメッセージが処理されないようにブロックします。 他のパーティションは、期待どおりに処理を続行します。並列処理は基本的にPythonの問題ですが、この解決策の概念は他の言語にも適用できます。 下の図4に示す私たちの解決策は、ワーカーごとに1つのKafka-consumerプロセスと複数のタスク実行プロセスを収容することでした。 Kafka-consumerプロセスは、Kafkaからメッセージをフェッチし、タスク実行プロセスによって読み取られるローカルキューに配置します。 ローカルキューがユーザー定義のしきい値に達するまで消費を継続します。 このソリューションでは、パーティション内のメッセージをフローさせることができ、遅いメッセージによって一つのタスク実行プロセ このしきい値は、ローカルキュー内の転送中のメッセージの数も制限します(システムクラッシュの場合に失われる可能性があります)。
図4:非ブロッキングKafkaワーカーは、ローカルメッセージキューと二つのタイプのプロセスで構成されています: kafka-consumerプロセスと複数のtask-executorプロセス。 Kafka-consumerは複数のパーティションから読み取ることができますが、簡単にするために1つだけを説明します。 この図は、処理が遅いメッセージ(赤)は、完了するまで単一のタスク実行者のみをブロックし、パーティション内のその背後にある他のメッセージは、他のタスクエグゼキュータによって処理され続けることを示しています。
デプロイの混乱
私たちはDjangoアプリを1日に複数回デプロイします。 私たちが気づいた解決策の一つの欠点は、展開がkafkaでパーティション割り当てのリバランスをトリガーすることです。 トピックごとに異なるコンシューマグループを使用して再バランスの範囲を制限していましたが、再バランス中にタスクの消費を停止する必要があ ほとんどの場合、計画されたリリースを実行するときに速度低下が許容される場合がありますが、たとえば、バグを修正するために緊急リリースを実行しているときには致命的なことがあります。 その結果、カスケード処理の減速が導入されます。
新しいバージョンのKafkaとクライアントは、増分協調リバランスをサポートしており、リバランスの運用上の影響を大幅に削減します。 このタイプのリバランスを支えるために私達の顧客を改善することは選択の私達の解決今後行くことです。 残念ながら、増分協調リバランスは、私たちの選択したKafkaクライアントではまだサポートされていません。
主要な勝利
このプロジェクトの終了により、稼働時間、スケーラビリティ、可観測性、分散化の面で大幅な改善が実現されました。 これらの勝利は、当社のビジネスの継続的な成長を確実にするために不可欠でした。
これ以上の繰り返しの停止はありません
このカスタムKafkaアプローチの展開を開始するとすぐに、繰り返しの停止を停止しました。 停止は非常に悪いユーザーの経験に起因していた。MVPで最も使用されているセロリ機能の小さなサブセットのみを実装することで、2週間で作業コードを本番環境に出荷することができました。
- MVPを導入することで、ソリューションを強化して新しい機能を実装し続けるにつれて、RabbitMQとCeleryの負荷を大幅に削減することができました。
タスク処理はもはや成長の制限要因ではありませんでした
kafkaをアーキテクチャの中心に据え、DoorDashとその顧客が成長を続けることを可能にする、高可用性と水平方向の拡張性を備えたタスク処理システムを構築しました。
大規模に拡張された可観測性
これはカスタムソリューションだったので、ほぼすべてのレベルでより多くのメトリックをベイクすること 各キュー、ワーカー、およびタスクは、運用環境および開発環境では非常に細かいレベルで完全に監視できました。 この観測性の向上は、生産的な意味だけでなく、開発者の生産性の面でも大きな勝利でした。
Operational decentralization
可観測性の向上により、アラートをTerraformモジュールとしてテンプレート化し、すべての単一のトピックに明示的に所有者を割り当て、暗黙的に900以上の
タスク処理システムの詳細な操作ガイドにより、すべてのエンジニアがトピックやワーカーで運用上の問題をデバッグしたり、必要に応じてkafkaクラ 日々の業務はセルフサービスであり、インフラチームからのサポートはめったに必要ありません。要約すると、私たちはrabbitmqをスケールする能力の上限に達し、代替案を探す必要がありました。 私たちが行った代替案は、カスタムKafkaベースのソリューションでした。 Kafkaを使用することにはいくつかの欠点がありますが、上記で説明したいくつかの回避策が見つかりました。
重要なワークフローが非同期タスク処理に大きく依存する場合、スケーラビリティを確保することが最も重要です。 同様の問題が発生した場合は、私たちの戦略からインスピレーションを取ること自由に感じ、それは私たちに努力の80%で結果の20%を与えました。 この戦略は、一般的なケースでは、迅速に信頼性の問題を軽減し、より堅牢で戦略的なソリューションのために痛んで必要な時間を購入するための戦術的著者は、このプロジェクトに貢献してくれたClement Fang、Corry Haines、Danial Asif、Jay Weinstein、Luigi Tagliamonte、Matthew Anger、Shaohua Zhou、およびYun-Yu Chenに感謝したいと思います。
Unsplashのtian kuanによる写真