Goがブラックフライデーを救った方法

先ほど、負荷が増加するにつれて、本番環境の重要なサービスのバックエンドでのPythonの使用を徐々に放棄し、Goに置き換える方法について説明しました。 そして今日、私、Madmin開発チームのチームリーダーであるDenis Girkoは、詳細を共有したいと考えています。これは、当社のビジネスにとって最も重要なサービスの1つの例で発生した理由と理由です。クーポンの割引を考慮して価格を計算します。







クーポンを扱う仕組みは、オンラインストアで少なくとも一度は購入したことがある人なら誰でも代表するでしょう。 特別ページまたはバスケットに直接、クーポン番号を入力すると、約束された割引に従って価格が再計算されます。 計算は、クーポンが提供する割引の種類に応じて異なります-パーセントで、固定金額の形式で、または他の数学を使用します(たとえば、ロイヤルティプログラム、店舗のプロモーション、商品の種類などを追加で考慮します)。 当然、注文はすでに新しい価格で発行されています。



ビジネスは、価格を操作するこれらすべてのメカニズムに満足していますが、少し異なる視点からサービスについてお話ししたいと思います。



仕組み



バックエンドのこれらすべての困難を考慮した価格設定のために、個別のサービスが用意されました。 しかし、彼は常に独立していませんでした。 このサービスは、オンラインストアの開始から1〜2年後に登場し、2016年までに、マーケティング活動用のさまざまなコンポーネント(Madmin)を含む大規模なPythonモノリスの一部になりました。 彼は後にマイクロサービスアーキテクチャに移行したため、独立した「ブロック」として際立っていました。



通常、モノリスの場合と同様に、Madminは修正され、多数の開発者と部分的に対応しました。 サードパーティのライブラリがそこに統合されたため、開発が簡素化されましたが、多くの場合、パフォーマンスに最高の影響はありませんでした。 しかし、その時点では、サービスはタスクの素晴らしい仕事をしたので、販売中の重負荷への抵抗についてはあまり気にしませんでした。 しかし、2016年はすべてを変えました。







アメリカでは、ブラックフライデーは前世紀の60年代から知られています。 ロシアでは、アクションはゼロから作成する必要がありましたが、2010年代に開始されました-市場はその準備が整っていませんでした。 しかし、主催者の努力は無駄ではありませんでしたが、毎年、販売日中にサイトへのユーザートラフィックが増加しました。 そのため、価格計算サービスのそのバージョンでは過剰な負荷との衝突は時間の問題でした。



ブラックフライデー2016。そして私たちは彼女を寝坊しました



セールのアイデアは最大限に活用されたため、「ブラックフライデー」は、深夜までにほぼ毎週ウェブサイトのオーディエンスが来店するという点で、他の日と異なります。 これは、すべてのサービスにとって困難な時期です。 一年を通してスムーズに動作するそれらのそれらでさえ、問題は時々出ます。



現在、予想される負荷をまねて、新しい「ブラックフライデー」の準備を進めていますが、2016年はまだ異なった行動をとっていました。 重要な日より前にMadminをテストする場合、通常の日にユーザーの行動シナリオを使用して負荷耐性をテストしました。 結局のところ、このテストは実際の状況をまったく反映していません。「ブラックフライデー」には同じクーポンを持っている人がたくさんいるからです。 その結果、この割引を考慮に入れた価格計算サービスは、3倍(通常の日と比較して)の負荷に対処できず、販売の最も暑いピーク時に2時間顧客にサービスを提供する機能をブロックしました。



サービスは真夜中の1時間前に「行きました」。 すべてはデータベース(その時点ではMySQL)からの切断から始まり、その後、価格計算サービスのすべての実行中のコピーが接続し直すことができませんでした。 そして、まだ接続しているものは負荷に耐えられず、ベースロックに引っかかって応答を停止しました。



偶然にも、後輩は当直のままであり、サービスの崩壊の時点で彼はオフィスの家から帰る途中でした。 彼は、現場に到着し、「重砲」、つまり緊急任務官を呼び出したときにのみ問題につながることができました。 しかし、彼らは一緒になって、2時間後にしか状況を正常化しませんでした。



手続きが始まると、サービスがどれほど最適ではないかに関する詳細が明らかになり始めました。 たとえば、1つのクーポンを計算するために、データベースに対して28のクエリが実行されたことが判明しました(すべてが100%のCPU使用率で機能したことは驚くことではありません)。 同じブラックフライデークーポンを使用した上記のユーザーは状況を単純化しませんでした。さらに、すべてのクーポンにアプリケーションカウンターがありました。そのため、このカウンターを参照するたびに負荷が増加しました。



2016年は、私たちに多くの思考の糧を与えました-主にこの状況が二度と起こらないようにクーポンとテストで仕事を調整する方法について。 そして、金曜日はこの写真でここで最もよく説明されている数字で:





2016年ブラックフライデーの結果



ブラックフライデー2017。私たちは真剣に準備していましたが...



良いレッスンを受けたので、次の「ブラックフライデー」に向けて事前に準備し、サービスを大幅に再構築して最適化しました。 たとえば、最終的に2種類のクーポンを作成しました:制限と無制限-データベースへのアクセスを同時にブロックしないように、人気のあるクーポンを適用するためのスクリプトからデータベースへのエントリを削除しました。 同時に、ブラックフライデーの1〜2か月前に、サービスでMySQLからPostgreSQLに切り替え、コードの最適化により、データベースコールの数を28から4〜5に減らしました。これらの改善により、テストサービスをSLA要件に拡張できました-回答3秒で、600 RPSで95パーセンタイル。



私たちの改良が本番環境での古いバージョンのサービスの作業をどれだけ加速したかはわかりませんが、その時点で2つのバージョンのPythonコードがブラックフライデーに一度に準備されていました-高度に最適化された既存のバージョンとゼロから作成された完全に新しいコードです。 本番環境では、2つ目がロールアウトされ、この昼と夜の前にテストされました。 しかし、すでに「戦闘中」であることが判明したため、彼らはまだ十分にテストされていませんでした。



顧客のメインフローが到着した「緊急」の日に、サービスの負荷は指数関数的に増加し始めました。 一部の要求は最大2分間処理されました。 一部のリクエストの処理が長いため、他のワーカーの負荷が増大しました。



私たちの主な目標は、このような貴重なトラフィックをビジネスに提供することでした。 しかし、「鉄で鋳造する」ことは問題を解決せず、すぐに忙しい労働者の数が100%に達することが明らかになりました。 正確に何に直面しているかわからないので、uWSGIでharakiriをアクティブにし、通常のリクエスト用にリソースを解放するために長いリクエスト(6秒以上処理される)を単純に絞り込むことにしました。 そして、それは本当に抵抗するのに役立ちました-労働者は完全に使い果たされるほんの数分前に解放され始めました。



少し後に、状況を把握しました...これらは、非常に大きなバスケット(40から100の製品)と、範囲に制限のある特定のクーポンのリクエストであることがわかりました。 この状況は、新しいコードではうまくいきませんでした。 配列の不正な動作が示され、無限再帰に変わりました。 その後、大きなバスケットのケースをテストしましたが、トリッキーなクーポンとの組み合わせではテストしなかったのは興味深いです。 解決策として、単に異なるバージョンのコードに切り替えました。 確かに、これはブラックフライデーの終了の3時間前に発生しました。 この瞬間から、すべてのバスケットが正しく処理され始めました。 そして、その時点で販売計画を完了しましたが、奇跡によって通常の5倍の負荷のために世界的な問題を回避しました。



ブラックフライデー2018



2018年までに、サイトにサービスを提供する負荷の高いサービスのために、Goを徐々に実装し始めました。 過去のブラックフライデーの歴史を考慮すると、割引計算サービスは処理の最初の候補の1つでした。







もちろん、すでに「戦闘テスト済み」バージョンのPythonを保存することもできます。新しい「ブラックフライデー」の前に、重いライブラリをオフにして、最適でないコードを捨てることができました。 ただし、Golangはすでにその時点で定着しており、より有望に見えました。



今年の夏に新しいサービスに切り替えたため、次の販売の前に、負荷プロファイルの増加を含め、それをうまくテストすることができました。



テスト中に、高負荷の観点から見た弱点が引き続きベースであることが判明しました。 トランザクションが長すぎるため、接続のプール全体を選択し、要求がキューに入れられたという事実に至りました。 そのため、アプリケーションのロジックを少しやり直し、データベースの使用を最小限に抑え(それ以外に何もすることがない場合にのみ参照する)、ブラックフライデーで人気のあるクーポンのデータベースとデータからディレクトリをキャッシュする必要がありました。



確かに、今年は負荷予測を上向きにミスしました:ピークの6-8倍の成長に備えて、そのような大量のリクエストに対してサービスの良い仕事を達成しました(キャッシュの追加、実験機能の事前無効化、いくつかの事柄の単純化、追加のKubernetesノードの展開レプリカ用のデータベースサーバーでさえ、最終的には不要でした)。 実際、ユーザーの関心の高まりはそれほど大きくなかったため、すべてが通常どおりに行われました。 サービスの応答時間は、95パーセンタイルで50ミリ秒を超えませんでした。



私たちにとって最も重要な特徴の1つは、1つのコピーに十分なリソースがない場合にアプリケーションがどのようにスケーリングするかです。 Goはハードウェアリソースをより効率的に使用するため、同じ負荷でより少ないコピーを実行する必要があります(同じハードウェアリソースでより多くのリクエストを処理します)。 今年、販売のピーク時には、アプリケーションの16個のインスタンスが動作しており、1秒間に平均300リクエストを処理し、1秒間に最大400リクエストのピークを処理しました。これは通常の負荷の約2倍です。 昨年、Pythonサービスには102個のインスタンスが必要でした。



Goの最初のアプローチからのサービスはすべてのニーズを閉じたように思えます。 しかし、Golangは「すべての問題に対するワンストップソリューション」ではありません。 いくつかの機能なしではできませんでした。 たとえば、サービスがKubernetesマルチプロセッサノードで開始できるスレッドの数を制限する必要がありました。これにより、スケーリング時に、実稼働環境の「隣接」アプリケーションと干渉しません(デフォルトでは、Goが使用するプロセッサの数に制限はありません)。 これを行うには、GoのすべてのアプリケーションでGOMAXPROCSを設定します。 これがどれほど有用であったかを喜んでコメントします-私たちのチームでは、これは「隣人」の劣化に対処する方法に関する仮説の1つにすぎませんでした。



もう1つの「セットアップ」は、キープアライブとして保持される接続の数です。 Goの通常のhttpおよびDBクライアントはデフォルトで2つの接続のみを保持するため、競合する要求が多くあり、TCP接続セットアップのトラフィックを節約する必要がある場合、それぞれMaxIdleConnsPerHostおよびSetMaxIdleConnsを設定してこの値を増やすことは理にかなっています。



ただし、これらの手動の「ねじれ」を使用しても、Golangは将来の販売のために大きなパフォーマンスマージンを提供してくれました。



All Articles