【アーキテクチャ】バックエンドサービスアーキテクチャの高性能設計手法

「N は高く、N は可能」、高パフォーマンス、高同時実行性、高可用性、高信頼性、スケーラビリティ、保守性、可用性などは、バックグラウンド開発ではおなじみの言葉であり、ほとんどの場合、同様の意味を表すものもあります。この一連の記事は、バックグラウンド アーキテクチャ設計で一般的に使用されるテクノロジと手法を議論して要約し、一連の方法論にまとめることを目的としています。

序文

この記事では、主にサービス アーキテクチャ設計における高パフォーマンスのテクノロジと手法について説明し、まとめています。以下のマインド マップに示すように、左側の部分は主にプログラミング アプリケーションに偏り、右側の部分はコンポーネント アプリケーションに偏っています。記事は展開されます。図の内容によると。

ここに画像の説明を挿入

1.ロックフリー

ほとんどの場合、マルチスレッドによって同時実行パフォーマンスが向上しますが、共有リソースが適切に処理されないと、激しいロック競合が発生してパフォーマンスが低下する可能性もあります。この状況に直面して、一部のシナリオでは、特に基礎となるフレームワークでロックフリー設計を採用しています。ロックフリーの主な実装には、シリアル ロックフリーとデータ構造ロックフリーの 2 つがあります。

1.1、シリアルロックフリー

ロックフリー シリアルの最も単純な実装は、redis/Nginx がこの方法を採用しているなど、シングルスレッド モデルです。ネットワーク プログラミング モデルでは、従来の方法では、メイン スレッドが I/O イベントの処理を担当し、読み取ったデータをキューにプッシュし、ワーカー スレッドがキューからデータを取り出して処理します。 /semi-asynchronous モデルは、次の図に示すように、キューがロックされている必要があります。

ここに画像の説明を挿入

上図のモードは、ロックフリーのシリアル形式に変更することができ、MainReactorが新規接続を受け付けた際に、多数のSubReactorから1つを選択して登録し、I/OスレッドとバインドするChannelを作成します。接続は遅延なく読み書きでき、同期せずに同じスレッド上で実行できます。

ここに画像の説明を挿入

1.2. ロックフリー構造

ハードウェアによってサポートされるアトミック操作を使用して、ロックフリーのデータ構造を実装できます。多くの言語では、ロックの実装に使用できる CAS アトミック操作 (go のアトミック パッケージや C++11 のアトミック ライブラリなど) が提供されています-空きキュー。ロックフリー プログラミングと、単純なスレッド セーフな単一リンク リスト挿入操作を使用した通常のロックの違いを見てみましょう。

template<typename T>
struct Node
{
    
    
    Node(const T &value) : data(value) {
    
     }
    T data;
    Node *next = nullptr;
};

ロックされたリスト WithLockList があります。

template<typename T>
class WithLockList
{
    
    
    mutex mtx;
    Node<T> *head;
public:
    void pushFront(const T &value)
    {
    
    
        auto *node = new Node<T>(value);
        lock_guard<mutex> lock(mtx); //①
        node->next = head;
        head = node;
    }
};

ロック解除されたリスト LockFreeList:

template<typename T>
class LockFreeList
{
    
    
    atomic<Node<T> *> head;
public:
    void pushFront(const T &value)
    {
    
    
        auto *node = new Node<T>(value);
        node->next = head.load();
        while(!head.compare_exchange_weak(node->next, node)); //②
    }
};

コードからわかるように、ロックされたバージョンでは、①ロックが実行されます。ロックフリー版では、②はアトミックなCAS操作compare_exchange_weakを使用しており、この関数は保存が成功した場合にtrueを返しますが、同時に誤った失敗(元の値が期待値と等しい場合)を防ぐために、 、ストレージが成功しない可能性があります。これは主に、単一の比較および交換命令が欠如している場合に発生します。ハードウェア マシン)、通常は CAS をループに入れます。

ここでは、それぞれ 1,000,000 回のプッシュ操作を実行した、ロックされたバージョンとロックされていないバージョンの簡単なパフォーマンスの比較を示します。テストコードは次のとおりです。

int main()
{
    
    
    const int SIZE = 1000000;
    //有锁测试
    auto start = chrono::steady_clock::now();
    WithLockList<int> wlList;
    for(int i = 0; i < SIZE; ++i)
    {
    
    
        wlList.pushFront(i);
    }
    auto end = chrono::steady_clock::now();
    chrono::duration<double, std::micro> micro = end - start;
    cout << "with lock list costs micro:" << micro.count() << endl;

    //无锁测试
    start = chrono::steady_clock::now();
    LockFreeList<int> lfList;
    for(int i = 0; i < SIZE; ++i)
    {
    
    
        lfList.pushFront(i);
    }
    end = chrono::steady_clock::now();
    micro = end - start;
    cout << "free lock list costs micro:" << micro.count() << endl;

    return 0;
}

3回の出力は以下の通りで、ロックなし版とロック版の方がパフォーマンスが高いことがわかります。

ロックリスト付きのコストマイクロ:548118

無料のロック リストのコスト マイクロ:491570

ロックリスト付きのコストマイクロ:556037

無料のロック リストのコスト マイクロ:476045

ロックリスト付きのコストマイクロ:557451

無料のロック リストのコスト マイクロ:481470

2. ゼロコピー

ここでのコピーとは、プロセス空間でのメモリ コピーではなく、カーネル バッファとアプリケーション バッファ間のデータの直接送信を指します (もちろん、C++ での参照の渡しや移動操作など、この点でゼロ コピーを実現することもできます) )。ここで、ユーザーが特定のファイルをダウンロードできるサービスがあるとします。リクエストが来ると、サーバー ディスク上のデータをネットワークに送信します。このプロセスの擬似コードは次のとおりです。

filefd = open(...); //打开文件
sockfd = socket(...); //打开socket
buffer = new buffer(...); //创建buffer
read(filefd, buffer); //从文件内容读到buffer中
write(sockfd, buffer); //将buffer中的内容发送到网络

データコピーの手順は以下の通りです。

ここに画像の説明を挿入

上図の緑色の矢印はDMAコピーを示しており、DMA(Direct Memory Access)とはダイレクトメモリアクセスのことで、高速データ転送の仕組みであり、外部機器がシステムメモリを介さずに直接データをやり取りするインターフェース技術のことを指します。 CPUを介して。赤い矢印は CPU コピーを示します。DMA テクノロジを使用する場合でも、DMA コピー用に 2 つ、CPU コピー用に 2 つ、合計 4 つのコピーがあります。

2.1、メモリマッピング

メモリ マッピングは、ユーザー空間のメモリ領域の一部をカーネル空間にマッピングします。ユーザーによるこのメモリ領域の変更は、カーネル空間に直接反映されます。同様に、カーネル空間のこの領域の変更も、ユーザー空間に直接反映されます。このカーネルバッファを共有するためのスペース。

メモリマッピングを使用して書き換えた後の疑似コードは次のとおりです。

filefd = open(...); //打开文件
sockfd = socket(...); //打开socket
buffer = mmap(filefd); //将文件映射到进程空间
write(sockfd, buffer); //将buffer中的内容发送到网络

メモリ マッピングを使用した後のデータ コピー フローを次の図に示します。

ここに画像の説明を挿入

図からわかるように、メモリマッピングの採用によりデータコピーが3倍に減少し、カーネルバッファ内のデータがアプリケーションプログラムを介して直接ソケットバッファにコピーされることがなくなりました。高パフォーマンスのメッセージ ストレージを実現するために、RocketMQ はメモリ マッピング メカニズムを使用してストレージ ファイルを複数の固定サイズのファイルに分割し、メモリ マッピングに基づいて順次書き込みを実行します。

2.2、ゼロコピー

ゼロコピーは、CPU があるストレージから別のストレージにデータをコピーすることを防止し、それによってデータ転送の効率を効果的に向上させるテクノロジーです。Linux カーネル 2.4 以降では、DMA コレクションおよびコピー機能による送信をサポートし、カーネル ページ キャッシュ内のデータを直接パッケージ化してネットワークに送信します。疑似コードは次のとおりです。

filefd = open(...); //打开文件
sockfd = socket(...); //打开socket
sendfile(sockfd, filefd); //将文件内容发送到网络

ゼロコピーを使用した後のプロセスは次のとおりです。
ここに画像の説明を挿入

ゼロコピー

ゼロコピーの手順は次のとおりです。

1) DMA はデータを DMA エンジンのカーネル バッファにコピーします。

2) データの位置と長さ情報の記述子をソケット バッファに追加します。

3) DMA エンジンは、カーネル バッファからプロトコル エンジンにデータを直接転送します。

ゼロ コピーは実際にはコピーなしではなく、カーネル バッファの 2 つの DMA コピーがまだ存在しますが、カーネル バッファとユーザー バッファ間の CPU コピーは削除されていることがわかります。Linux の主なゼロコピー関数には、sendfile、splice、tee などが含まれます。下図はIBMの公式サイトに掲載されている通常送信とゼロコピー送信の性能比較ですが、ゼロコピーは通常送信に比べて約3倍高速であることがわかり、Kafkaもゼロコピー技術を採用しています。

ここに画像の説明を挿入

3. 連載

データをファイルに書き込んだり、ネットワークに送信したり、ストレージに書き込んだりする場合は、通常、シリアル化テクノロジが必要です。また、データを読み取る場合には、エンコード (エンコード) およびデコード (デコード) とも呼ばれる逆シリアル化 (逆シリアル化) が必要です。 。送信データの表現として、シリアル化はネットワーク フレームワークや通信プロトコルから切り離されます。たとえば、ネットワーク フレームワーク taf は jce、json、カスタム シリアル化をサポートし、HTTP プロトコルは XML、JSON、ストリーミング メディア送信などをサポートします。

シリアル化の方法は数多くありますが、データの送信や保存の基本として、適切なシリアル化の方法をどのように選択するかが特に重要です。

3.1. 分類

一般に、シリアル化技術は次の 3 種類に大別できます。

  • 組み込み型: Java の java.io.Serializable など、プログラミング言語によってサポートされる型を指します。このタイプは言語のバインディングにより汎用的ではなく、一般にパフォーマンスが低く、通常はローカルでのみ使用されます。

  • テキストタイプ: 一般に、XML や JSON などの標準化されたテキスト形式です。このタイプは読みやすく、クロスプラットフォームをサポートし、幅広いアプリケーションを備えています。主な欠点は、比較的肥大化し、ネットワーク送信に多くの帯域幅が消費されることです。

  • バイナリタイプ: バイナリエンコーディングが採用され、データ構成がよりコンパクトになり、多言語とマルチプラットフォームがサポートされます。一般的なものは、Protocol Buffer/Thrift/MessagePack/FlatBuffer などです。

3.2. パフォーマンス指標

シリアル化/逆シリアル化を測定するには、次の 3 つの主要な指標があります。

1) シリアル化後のバイト サイズ。

2) シリアル化/逆シリアル化の速度。

3) CPU とメモリの消費量。

次の図は、いくつかの一般的なシリアル化フレームワークのパフォーマンスの比較を示しています。
ここに画像の説明を挿入

シリアル化と逆シリアル化の速度の比較 シリアル化のバイト占有率の比較
ここに画像の説明を挿入

Protobuf がシリアル化速度とバイト比の点で最高と言えることがわかります。しかし、世界の外には人がいて、空の向こうには空があります。Protobuf より FlatBuffer の方が無敵だと聞きました。下の写真は Google の FlatBuffer と他のシリアル化パフォーマンスの比較です。データ FB を見てるだけです。この写真はPBを数秒で殺すように見えます。

ここに画像の説明を挿入

3.3、選択に関する考慮事項

シリアル化テクノロジを設計および選択する場合、考慮すべき多くの側面がありますが、主に次の側面があります。

1) パフォーマンス: CPU とバイト サイズがシリアル化の主なオーバーヘッドです。基本的な RPC 通信、ストレージ システム、および同時実行性の高いサービスには、高性能かつ高圧縮のバイナリ シリアル化を選択する必要があります。Web リクエストが少ない一部の内部サービスとアプリケーションはテキストの JSON を使用でき、ブラウザーは JSON 組み込みを直接サポートします。

2) 使いやすさ: 豊富なデータ構造と補助ツールにより、使いやすさが向上し、ビジネス コードの開発量が削減されます。現在、多くのシリアル化フレームワークは、リスト、マップ、その他の構造と読みやすい印刷をサポートしています。

3) 汎用性: 最新のサービスには複数の言語とプラットフォームが含まれることが多く、クロスプラットフォームおよびクロス言語の相互通信をサポートできるかどうかが、シリアル化選択の基本条件となります。

4) 互換性: 最新のサービスは高速に反復され、アップグレードされます。優れたシリアル化フレームワークは、良好な前方互換性を備え、フィールドの追加、削除、変更をサポートする必要があります。

5) スケーラビリティ: シリアル化フレームワークが低いしきい値のカスタム形式をサポートできるかどうかが重要な考慮事項となる場合があります。

4. ポンディゼーション

プーリングはおそらく最も一般的に使用されるテクノロジであり、その本質は、プールを作成することによってオブジェクトの再利用を向上させ、作成と破棄を繰り返すオーバーヘッドを削減することです。一般的に使用されるプーリング テクノロジには、メモリ プール、スレッド プール、接続プール、オブジェクト プールなどがあります。

4.1. メモリプール

C/C++ では、malloc/free と new/delete がそれぞれメモリの割り当てに使用され、基盤となるシステムが sbrk/brk を呼び出すことは誰もが知っています。メモリの割り当てや解放を行うシステムコールを頻繁に呼び出すと、パフォーマンスに影響を与えるだけでなく、メモリの断片化が起こりやすくなり、メモリプール技術はこれらの問題を解決することを目的としています。このような理由から、C/C++ におけるメモリ操作はシステムコールを直接呼び出すのではなく、独自のメモリ管理を実現しています。

1) ptmalloc: glibc の実装。

2) tcmalloc: Google の実装。

3) jemalloc: Facebook の実装。

以下は、インターネットで入手した 3 種類の malloc の比較表です。tcmalloc と jemalloc のパフォーマンスは似ていますが、ptmalloc のパフォーマンスは 2 つに劣ります。ニーズに応じて、より適切な malloc を選択できます。たとえば、redis と mysl は、どの malloc を使用するかを指定できます。3 つの実装と違いについては、オンラインで確認できます。

ここに画像の説明を挿入

標準ライブラリの実装により、オペレーティング システムのメモリ管理にメモリ管理の層が追加されますが、アプリケーションは通常、参照カウントや小さなオブジェクトの割り当てなどのために、独自の特定のメモリ プールも実装します。したがって、メモリ管理には通常 3 つのレベルがあるようです。
ここに画像の説明を挿入

4.2、スレッドプール

スレッドの作成にはリソースを割り当てる必要があるため、一定のオーバーヘッドが発生し、タスクを処理するためにスレッドを作成すると、システムのパフォーマンスに影響を与えることは避けられません。スレッド プールは、作成および再利用されるスレッドの数を制限できるため、システムのパフォーマンスが向上します。

スレッド プールは分類またはグループ化でき、異なるタスクは異なるスレッド グループを使用でき、相互影響を避けるために分離できます。分類として、コアと非コアに分けることができます。コア スレッド プールは常に存在し、リサイクルされません。非コアは一定期間アイドル状態になった後にスレッドをリサイクルすることがあり、それによってシステム リソースが節約されます。必要に応じて、スレッドが再利用されます。オンデマンドで作成され、プールに配置されます。

4.3、接続プール

一般的に使用される接続プールには、データベース接続プール、Redis 接続プール、TCP 接続プールなどが含まれます。主な目的は、多重化によって接続の作成と解放のオーバーヘッドを削減することです。接続プールの実装では通常、次の問題を考慮する必要があります。

1) 初期化: スタートアップは初期化と遅延初期化です。スタートアップの初期化により、一部のロック操作が軽減され、必要なときに直接使用できるようになりますが、サービスの起動が遅くなったり、起動後にタスクが処理されなくなったりして、リソースが無駄に消費される可能性があるという欠点があります。遅延初期化とは、本当に必要なときに作成することです。この方法は、リソースの使用量を減らすのに役立ちますが、突然のタスク要求に直面して、瞬時に大量の接続を作成すると、システムの応答が遅くなったり、応答エラーが発生したりする可能性があります。起動時に初期化する方法を使用します。

2) 接続数: 必要な接続数のバランスをとります。接続数が少なすぎると、タスクの処理が遅くなる可能性があります。接続数が多すぎると、タスクの処理が遅くなるだけでなく、消費電力も高くなります。システムリソースが過剰になります。

3) 接続の削除: 接続プールに使用可能な接続がない場合、使用可能な接続ができるまで待つか、新しい一時接続を割り当てるか。

4) 接続を入れる: 接続が使い果たされ、接続プールがいっぱいでない場合は、接続を接続プール (3 で作成した一時接続を含む) に入れます。それ以外の場合は、接続を閉じます。

5) 接続の検出: 長時間アイドル状態の接続と無効な接続は閉じて、接続プールから削除する必要があります。一般的に使用される検出方法には、使用中の検出と定期的な検出があります。

4.4、オブジェクトプール

厳密に言うと、前の 3 つのバディを含め、すべての種類のプールはオブジェクト プール パターンの適用です。オブジェクト プールは、さまざまなプールと同様に、インスタンスの数を制限しながら、同じタイプのオブジェクトが多数作成されることを避けるために一部のオブジェクトをキャッシュします。たとえば、redis 内の 0 ~ 9999 の整数オブジェクトは、オブジェクト プールを使用して共有されます。オブジェクト プール モードはゲーム開発でよく使用され、たとえばマップ上にモンスターや NPC が表示される場合、毎回再作成されるのではなく、オブジェクト プールから取得されます。

5. 同時実行性

5.1. リクエストの同時実行性

タスクが複数のサブタスクを処理する必要がある場合、依存関係のないサブタスクを並列化できます。このシナリオはバックグラウンド開発では非常に一般的です。たとえば、リクエストで 3 つのデータをクエリする必要がある場合、それぞれ T1、T2、T3 の時間がかかります (シリアル呼び出しに時間がかかる場合、T=T1+T2+T3)。3 つのタスクを同時に実行すると、合計時間 T=max(T1,T 2,T3) がかかります。書き込み操作についても同様です。同じ種類のリクエストについては、バッチマージを同時に実行して、RPC 呼び出しの数を減らすこともできます。

5.2. 冗長なリクエスト

冗長リクエストとは、複数の同一のリクエストをバックエンド サービスに同時に送信することを指し、すぐに応答した人が使用され、その他は破棄されます。この戦略はクライアントの待機時間を短縮しますが、システム コールの量も増加します。一般に、初期化やリクエストがほとんどないシナリオに適用できます。同社の WNS の競馬モジュールは実際にこのメカニズムであり、長い接続を迅速に確立するために、競馬モジュールはバックグラウンドで同時に複数の IP/ポートへのリクエストを開始し、これを素早く使用する人は特に便利ですネットワークが弱いモバイル デバイスの場合、待機タイムアウトを使用する場合、再試行メカニズムにより、ユーザーの待機時間が大幅に増加することは間違いありません。

6. 非同期

時間のかかるタスクを処理する場合、同期待ちの方法を採用するとシステムのスループットが大幅に低下しますが、非同期化することで解決できます。さまざまなレベルで非同期の概念にはいくつかの違いがあるため、ここでは非同期 I/O については説明しません。

6.1. 非同期呼び出し

時間のかかる RPC 呼び出しまたはタスク処理を行う場合、一般的に使用される非同期メソッドは次のとおりです。

  • コールバック: 非同期コールバックは、コールバック関数を登録し、非同期タスクを開始し、タスクが完了すると、ユーザーが登録したコールバック関数がコールバックされるため、呼び出し側の待ち時間が短縮されます。この方法ではコードが分散して保守が難しくなり、問題を特定するのが比較的困難になります。

  • Future: ユーザーがタスクを送信すると、すぐに Future が返され、タスクは非同期に実行され、後で Future を通じて実行結果を取得できます。1.4.1 のリクエストの同時実行性については、Future を使用して実装できます。疑似コードは次のとおりです。

//异步并发任务
  Future<Response> f1 = Executor.submit(query1);
  Future<Response> f2 = Executor.submit(query2);
  Future<Response> f3 = Executor.submit(query3);

  //处理其他事情
  doSomething();

  //获取结果
  Response res1 = f1.getResult();
  Response res2 = f2.getResult();
  Response res3 = f3.getResult();
  • CPS

(継続渡しスタイル) では、複数の非同期プログラミングを配置してより複雑な非同期処理を形成し、同期コード呼び出しの形で非同期効果を実現できます。CPS は後続の処理ロジックをパラメータとして then に渡し、最終的に例外をキャッチできるため、散在する非同期コールバック コードと困難な例外追跡の問題が解決されます。Java および C++ PPL の CompletableFuture は基本的にこの機能をサポートします。一般的な呼び出しは次のとおりです。

void handleRequest(const Request &req)
{
    
    
  return req.Read().Then([](Buffer &inbuf){
    
    
      return handleData(inbuf);
  }).Then([](Buffer &outbuf){
    
    
      return handleWrite(outbuf);
  }).Finally(){
    
    
      return cleanUp();
  });
}

6.2. プロセスの非同期

ビジネス プロセスには長い呼び出しチェーンと多くの事後依存関係が伴うことが多く、これによりシステムの可用性と同時処理能力が同時に低下します。重要ではない依存関係の非同期解決を使用できます。たとえば、ペンギン E スポーツ放送サービスでは、番組の放送と書き込みストレージに加えて、番組情報を SHIELD レコメンデーション プラットフォーム、アプリのホームページ、セカンダリ ページなどに同期する必要もあります。外部との同期はブロードキャストの主要なロジックではなく、一貫性の要件はそれほど高くないため、これらの同期後の操作は非同期にすることができ、次のようにストレージへの書き込み後に応答がアプリに返されます。形:

ここに画像の説明を挿入

7. キャッシュ

シングルコア CPU から分散システムまで、フロントエンドからバックエンドまで、キャッシュはあらゆる場所にあります。古代には、朱元璋が「ゆっくりと王になる」ことで最終的に世界を勝ち取りましたが、現在では、チップメーカーもインターネット企業も「ゆっくりと王になる」(キャッシュが王様)という同じ方針を採用して地位を占めています。キャッシュは元のデータのコピー セットであり、その本質は、主に高同時読み取りを解決するために、空間を時間と交換することです。

7.1、キャッシュ使用シナリオ

キャッシュは空間を時間と交換する技術であり、キャッシュを使用するとシステムのパフォーマンスを向上させることができます。「濃いワインも良いけど、欲張らないでね。」 キャッシュを使う目的はコストパフォーマンスの向上であり、コストに関係なくいわゆる性能向上のためにキャッシュを使うのではなく、シーンによって異なります。

キャッシュの使用に適したシナリオとして、私が参加したプロジェクト Penguin E-sports を例に挙げます。

1) 一度生成されると基本的に変更されないデータ: ペンギン E スポーツのゲーム リストなど、バックグラウンドでゲームを作成した後はほとんど変更がなく、ゲーム リスト全体を直接キャッシュできます。

2) 読み取り集中型またはホットスポットのあるデータ: 通常、これは、ペンギン E スポーツのホームページ ライブ リストなど、さまざまなアプリのホームページです。

3) 計算コストの高いデータ: ペンギン E スポーツのトップ ホット リスト ビデオなど、7 日間のリストがキャッシュされ、毎日の早朝にさまざまな指標に従って計算された後にリストが並べ替えられます。

4) 片面データ: ペンギン E スポーツのトップ トレンド ビデオでもあり、ソートされたリスト全体がキャッシュされることに加えて、データの最初の N ページを組み立てた後の最終的な返結果がページごとに直接キャッシュされます。プロセス;

キャッシュに適さないシナリオ:

1) 書く量を増やし、読む量を減らし、頻繁に更新します。

2) データの一貫性に関する厳格な要件。

7.2、キャッシュの分類

  • プロセスレベルのキャッシュ: キャッシュされたデータはプロセスのアドレス空間に直接あり、これがキャッシュにアクセスする最も速くて簡単な方法となります。主な欠点は、プロセス空間のサイズによって制限され、キャッシュできるデータ量が制限されており、プロセスが再起動されるとキャッシュされたデータが失われることです。通常、キャッシュされたデータの量がそれほど多くないシナリオで使用されます。

  • 集中キャッシュ: キャッシュされたデータは、共有メモリなどの 1 台のマシンに集中されます。このタイプのキャッシュの容量は主にマシンのメモリのサイズによって制限され、プロセスの再起動後にデータが失われることはありません。一般的に使用される集中型キャッシュ ミドルウェアには、スタンドアロン バージョンの redis、memcache などが含まれます。

  • 分散キャッシュ: キャッシュされたデータは複数のマシンに分散されます。通常、データ シャーディングには特定のアルゴリズム (ハッシュなど) を使用し、大量のキャッシュ データを各マシン ノードに均等に分散する必要があります。一般的に使用されるコンポーネントは、Memcache (クライアントの断片化)、Codis (プロキシの断片化)、Redis Cluster (クラスターの断片化) です。

  • マルチレベル キャッシュ: アクセス効率を向上させ、バックエンド ストレージへの影響を軽減するために、システム内のさまざまなレベルでデータをキャッシュすることを指します。以下の図は、Penguin Gaming の多層キャッシュ アプリケーションを示しています。現在のネットワーク統計によると、一次キャッシュのヒット率は 94% に達しており、食料品店に到達するリクエストの数は非常に少ないです。

ここに画像の説明を挿入

全体的なワークフローは次のとおりです。

1) リクエストがホームページまたはライブ ブロードキャスト ルームのサービスに到着した後、ローカル キャッシュにヒットした場合は直接戻りますが、そうでない場合は、次のレベルのキャッシュ コア ストレージからクエリを実行し、ローカル キャッシュを更新します。

2) フロントエンド サービスのキャッシュはヒットせず、コア ストレージ サービスに浸透します。ヒットした場合は直接フロントエンド サービスに戻ります。ヒットしなかった場合は、ストレージ層の食料品店を要求してキャッシュを更新します。

3) キャッシュの最初の 2 レベルは、ストレージ層の食料品店には影響しません。

7.3、キャッシュモード

キャッシュの使用に関しては、いくつかのモデルがまとめられており、主に Cache-Aside と Cache-As-SoR の 2 つのカテゴリに分類されています。このうち、SoR(system-of-record):レコードのシステム、つまりデータソースを示し、キャッシュはSoRのレプリカセットです。

キャッシュアサイド: キャッシュをバイパスします。これは最も一般的なキャッシュ モードです。読み取りの場合は、まずキャッシュからデータを読み取り、ヒットしなかった場合はソース SoR に読み戻してキャッシュを更新します。書き込み操作の場合は、最初に SoR を書き込み、次にキャッシュを書き込みます。このモードの構造図は次のとおりです。
ここに画像の説明を挿入

ロジックコード:

//读操作
data = Cache.get(key);
if(data == NULL)
{
    
    
    data = SoR.load(key);
    Cache.set(key, data);
}

//写操作
if(SoR.save(key, data))
{
    
    
    Cache.set(key, data);
}

このモードは使い方が簡単ですが、アプリケーション層に対して不透明であり、読み取りおよび書き込みロジックを完了するにはビジネス コードが必要です。同時に、書き込みの場合、データ ソースの書き込みとキャッシュの書き込みはアトミックな操作ではないため、次のような状況では、この 2 つの間でデータの不整合が発生する可能性があります。

1) 同時書き込み中、データの不整合が発生する可能性があります。次の図に示すように、user1 と user2 はほぼ同時に読み取りと書き込みを行います。ユーザー 1 は t1 に db に書き込み、ユーザー 2 は t2 にデータベースに書き込み、次にユーザー 2 は t3 にキャッシュに書き込み、ユーザー 1 は t4 にキャッシュに書き込みます。この場合、db は user2 のデータ、cache は user1 のデータであり、両者は矛盾しています。
ここに画像の説明を挿入

キャッシュアサイドの同時読み取りと書き込み

2) データ ソースへの最初の書き込みは成功しますが、その後のキャッシュへの書き込みは失敗し、2 つの間のデータに一貫性がありません。これら 2 つのケースでは、ビジネスが耐えられない場合は、最初にキャッシュを削除してからデータベースに書き込むだけで解決できますが、コストは次の読み取りリクエストのキャッシュ ミスです。

Cache-As-SoR: キャッシュがデータ ソースです。このモードでは、キャッシュが SoR とみなされ、読み取りおよび書き込み操作はすべてキャッシュに対して行われ、キャッシュは読み取りおよび書き込み操作を SoR に委託します。つまり、キャッシュはプロキシです。以下に示すように:
ここに画像の説明を挿入

Cache-As-SoR の構造図

Cache-As-SoR には 3 つの実装があります。

1) リードスルー: 読み取り操作が発生すると、まずキャッシュがクエリされ、ミスがあった場合、キャッシュはソースの SoR に戻ります。つまり、ストレージ エンドはビジネスの代わりにキャッシュ アサイドを実装します。

2) ライトスルー: ライトスルー モードと呼ばれ、ビジネスは最初に書き込み操作を呼び出し、次にキャッシュがキャッシュと SoR への書き込みを担当します。

3) ライトビハインド: ライトバック モードと呼ばれます。書き込み操作が発生すると、ビジネスはキャッシュを更新するだけですぐに戻り、その後 SoR を非同期で書き込みます。これにより、書き込み/バッチ書き込みを組み合わせて使用​​することでパフォーマンスが向上します。

7.4. キャッシュのリサイクル戦略

スペースが限られている場合、ホットスポット アクセスが低頻度である場合、またはアクティブな更新通知がない場合は、キャッシュされたデータをリサイクルする必要があります。一般的に使用されるリサイクル戦略は次のとおりです。

1) 時間ベース: 時間ベースの戦略は、主に 2 つのタイプに分類できます。

  • TTL (Time To Live): キャッシュへのアクセスの有無に関係なく、キャッシュ データの作成から指定された有効期限までの存続期間に基づきます。RedisのEXPIREなど。

  • TTI (Time To Idle): アイドル期間に基づいて、指定された時間内にアクセスされない場合、キャッシュはリサイクルされます。

2) スペースベース: キャッシュはストレージスペースの上限を設定し、上限に達すると、特定の戦略に従ってデータが削除されます。

3) 容量ベース: キャッシュはストレージ エントリの上限を設定し、上限に達すると、特定の戦略に従ってデータが削除されます。

4) 参照ベース: 参照カウントまたは強参照と弱参照のいくつかの戦略に基づくリサイクル。

キャッシュの一般的なリサイクル アルゴリズムは次のとおりです。

  • FIFO (先入れ先出し): 先入れ先出しの原則に基づき、最初にキャッシュに入ったデータが最初に削除されます。

  • LRU (Least Recent Used): 最も局所性の原理に基づくもので、最近使用されたデータは将来も使用される可能性が高く、逆に長期間使用されていないデータは、現時点では、将来的に使用される可能性は比較的低いです。

  • LFU: (Least Frequently Used): 最も使用頻度の低いデータから順に削除、つまり各オブジェクトの使用回数をカウントし、削除が必要な場合には最も使用頻度の低いデータを選択して削除します。

7.5. キャッシュのクラッシュと修復

不十分な設計、リクエスト攻撃 (必ずしも悪意のある攻撃ではない) などによって引き起こされるキャッシュの問題のため、一般的なキャッシュの問題と解決策を以下に示します。

キャッシュの侵入: クエリに多数の存在しないキーが使用されると、キャッシュがヒットせず、これらのリクエストがバックエンド ストレージに侵入し、最終的にはバックエンド ストレージに過剰な負荷が発生したり、場合によっては圧倒する。この状況の原因は一般に、ストレージ内にデータが存在しないことが原因であり、解決策は主に 2 つあります。

1) 空の値またはデフォルト値を設定します。ストレージにデータがない場合は、次のリクエストがバックエンド ストレージに侵入しないように、空の値またはデフォルト値をキャッシュに設定します。ただし、この状況で悪意のある攻撃に遭遇した場合、クエリのために異なるキーを偽造し続けるとうまく対処できなくなるため、リクエストをフィルタリングするためのセキュリティ ポリシーを導入する必要があります。

2) ブルーム フィルター: ブルーム フィルターを使用して、考えられるすべてのデータを十分な大きさのビットマップにハッシュします。存在してはいけないデータはこのビットマップによってインターセプトされ、基礎となるデータベースのクエリ圧力への影響が回避されます。

キャッシュなだれ: 一定期間内に多数のキャッシュが集合的に障害を起こし、バックエンドのストレージ負荷が瞬時に増加したり、超過したりすることを指します。通常、次のことが原因で発生します。

1) キャッシュの有効期限が特定の期間に集中している場合、キーごとに異なる有効期限を使用し、元の基本有効期限に基づいて異なるランダム時間を追加することができます。

2) モジュロ機構を使用するキャッシュ インスタンスがダウンした場合、障害のあるインスタンスが削除された後に大量のキャッシュ ミスが発生します。解決策は 2 つあります: ① マスター/スレーブのバックアップを作成し、マスター ノードに障害が発生した場合にマスター インスタンスをスレーブ インスタンスに直接置き換える; ② モジュロの代わりに一貫性のあるハッシュを使用して、インスタンスがクラッシュした場合でも、インスタンスのごく一部だけが復旧するようにするキャッシュミス。

キャッシュ ホットスポット: キャッシュ システム自体は高性能ですが、一部のホット データの同時アクセスに耐えることができず、キャッシュ サービス自体が過負荷になってしまいます。Weibo がユーザー ID をハッシュ キーとして使用し、ある日突然、妹の Zhiling が結婚を発表したとします。彼女の Weibo コンテンツがユーザー ID に従ってノードにキャッシュされている場合、彼女の何千人ものファンが彼女の Weibo を閲覧すると、必然的にそれが行われます。キーが熱すぎるため、キャッシュ ノードに負荷がかかります。この場合、複数のキャッシュを異なるノード上に生成でき、各キャッシュの内容は同じであるため、単一ノードのアクセスに対するプレッシャーが軽減されます。

7.6. キャッシュの優れた実践例

1) 動的と静的の分離: キャッシュ オブジェクトの場合、多くの種類の属性に分割することができ、そのうちのいくつかは静的で、いくつかは動的です。キャッシュするときは動的と静的を分離するのが最善です。例えば、Penguin E-sportsの動画詳細は、タイトル、再生時間、解像度、カバーURL、いいね数、コメント数などに分かれています。このうち、タイトル、再生時間などは静的な属性であり、基本的には反映されません。 「いいね!」やコメントの数は頻繁に変化しますが、動的な属性が変更されるたびにビデオ キャッシュ全体を取り出して更新する (非常にコストがかかる) ことがないよう、キャッシュ中に 2 つの部分が分離されます。

2) 大きなオブジェクトの使用には注意してください。キャッシュ オブジェクトが大きすぎると、特に Redis などのシングルスレッド アーキテクチャでは、各読み取りと書き込みのオーバーヘッドが非常に高くなり、他のリクエストがスタックする可能性があります。典型的な状況は、値フィールドに大量のリストをぶら下げたり、境界のないリストを格納したりする場合であり、この場合、データ構造を再設計するか、クライアントで値を分割して集計する必要があります。

3) 有効期限の設定: ダーティデータとストレージの使用量を減らすために有効期限を設定するようにしてください。ただし、有効期限が一定期間に集中できないことに注意してください。

4) タイムアウト設定: データ アクセスを高速化する手段として、通常、キャッシュにはタイムアウト期間を設定する必要がありますが、タイムアウト期間は長すぎてはなりません (約 100 ミリ秒など)。そうしないと、リクエスト全体がタイムアウトしてしまい、源に戻ります。

5) キャッシュの分離: まず、競合や相互上書きを防ぐために、サービスごとに異なるキーが使用されます。第 2 に、コア サービスと非コア サービスは、異なるキャッシュ インスタンスを通じて物理的に分離されます。

6) 障害によるダウングレード: キャッシュを使用するには、特定のダウングレード プランが必要です。キャッシュは通常、特にコア サービスの場合、重要なロジックではありません。キャッシュの一部が失敗または失敗した場合、リターンを直接中断するのではなく、ソースに戻り続ける必要があります。

7) 容量制御: キャッシュ、特にローカル キャッシュを使用する場合、容量制御が必要です。キャッシュが多すぎてメモリが不足すると、ストレージ領域のスワップや GC 操作が頻繁に発生し、応答速度が低下します。

8) ビジネス指向: ビジネス指向であり、キャッシュのためのキャッシュを行わないでください。パフォーマンス要件が高くなく、リクエスト量も大きくなく、分散キャッシュまたはデータベースで十分な場合は、ローカル キャッシュを増やす必要はありません。そうでない場合は、データ ノード レプリケーションと冪等処理ロジックの導入が適切ではない可能性があります。ろうそくの価値があります。

9) 監視と警告: 姉妹紙が常に正しいのと同じように、あなたが間違うことは決してありません。大きなオブジェクト、遅いクエリ、メモリ使用量などを監視します。

8. 断片化

フラグメンテーションとは、大きな部分を複数の小さな部分に分割することですが、ここではデータシャーディングとタスクシャーディングに分けます。データ シャーディングに関して、このホワイト ペーパーでは、さまざまなシステムの分割に関する専門用語 (リージョン、シャード、vnode、パーティションなど) を総称してシャーディングと呼びます。フラグメンテーションは、大規模なデータセットをより多くのノードに分散し、1 点の読み書き負荷も複数のノードに分散し、スケーラビリティと可用性を向上させる一石三鳥の技術と言えます。

データ シャーディングは、プログラミング言語の標準ライブラリのコレクションから分散ミドルウェアに至るまで、至る所で行われています。たとえば、さまざまなオブジェクトを格納するスレッドセーフなコンテナを作成したとき、ロックの競合を減らすために、コンテナはセグメント化され、各セグメントにロックがあり、オブジェクトはハッシュまたはモジュラスに従って特定のセグメントに配置されました。 Java の ConcurrentHashMap などもセグメンテーション メカニズムを採用しています。分散メッセージ ミドルウェア Kafka でも、トピックは複数のパーティションに分割されており、各パーティションは互いに独立しており、同時に読み取りと書き込みが可能です。

8.1. 断片化戦略

シャーディングするときは、すべてのノードにデータを均等に分散して負荷を分散するようにしてください。分布が不均一だと傾きが生じ、システム全体のパフォーマンスが低下します。一般的なシャーディング戦略は次のとおりです。

  • 範囲の断片化

    連続キーワードに基づく断片化はソートを維持し、範囲検索に適しており、壊れた断片の読み取りと書き込みを軽減します。インターバルシャーディングの欠点は、不均一なデータ分散とホットスポットが発生しやすいことです。たとえば、ライブ ブロードキャスト プラットフォームで ID によって間隔セグメント化が実行される場合、通常は短い桁の ID が大きなアンカーとなり、100 ~ 1,000 桁の ID は 10 桁を超える ID よりも頻繁にアクセスする必要があります。時間範囲ごとにシャードすることも一般的であり、通常、最新の期間の読み取りおよび書き込み操作は、長い期間の読み取りおよび書き込み操作よりも頻繁に行われます。

ここに画像の説明を挿入

  • ランダムシャーディング

    特定の方法 (ハッシュ モジュロなど) に従ってシャードすると、データの分布が比較的均一になり、ホット スポットや同時実行のボトルネックが発生しにくくなります。欠点は、範囲クエリによって複数のノードへのリクエストが開始される場合など、秩序ある隣接関係の機能が失われることです。

ここに画像の説明を挿入

  • 結合シャーディングは、
    間隔シャーディングとランダム シャーディングの間の妥協点であり、2 つの方法を組み合わせて採用されます。複合キーは複数のキーで構成され、最初のキーはランダム ハッシュに使用され、残りのキーは間隔ソートに使用されます。ライブ ブロードキャスト プラットフォームがアンカー ID + ライブ時間 (anchor_id、live_time) を組み合わせキーとして使用する場合、特定の期間内の特定のアンカーの開始レコードを効率的にクエリできます。WeChat モーメント、QQ Talk、Weibo などのソーシャル シナリオでは、ユーザー ID と公開時刻 (user_id、pub_time) の組み合わせを使用して、一定期間のユーザーの公開レコードを検索します。

8.2. 二次インデックス

セカンダリ インデックスは通常、特定の値の検索を高速化するために使用され、レコードを一意に識別することはできません。セカンダリ インデックスを使用するには、二次検索が必要です。リレーショナル データベースと一部の KV データベースは、mysql の補助インデックス (非クラスター化インデックス) などのセカンダリ インデックスをサポートし、ES 転置インデックスは用語ごとにドキュメントを検索します。

  • ローカルインデックス

インデックスはキーワードと同じパーティションに格納されます。つまり、インデックスとレコードは同じパーティションにあるため、すべての書き込み操作はパーティション間操作を行わずに 1 つのパーティションで実行されます。ただし、読み取り操作の場合は、他のパーティション上のデータを集約する必要があります。たとえば、「Glory of Kings」の短いビデオを例にとると、ビデオ ビデオがキー インデックスとして使用され、ビデオ タグ (5 キル、3 キル、李白、A Ke など) が二次インデックスとして使用されます。ローカル インデックスを次の図に示します。
ここに画像の説明を挿入

ローカルインデックス

  • グローバルインデックス

キーとは独立して、インデックス値自体によってパーティション化します。このように、インデックスのデータの読み取りはすべて 1 つのパーティションで実行されますが、書き込み操作の場合は複数のパーティションにまたがる必要があります。上記の例を例として、グローバル インデックスを次の図に示します。
ここに画像の説明を挿入

グローバルインデックス

8.3. ルーティング戦略

ルーティング ポリシーは、シャード調整されたルートなど、指定されたノードにデータ リクエストを送信する方法を決定します。通常、クライアント ルーティング、プロキシ ルーティング、クラスター ルーティングの 3 つの方法があります。

  • クライアントルーティング

クライアントはシャーディング ロジックを直接操作し、シャードとノードの割り当て関係を認識し、ターゲット ノードに直接接続します。Memcache は、次の図に示すように、この方法で分散されます。
ここに画像の説明を挿入

Memcache クライアントのルーティング

  • プロキシ層ルーティング

クライアントのリクエストはプロキシ層に送信され、プロキシ層はリクエストを対応するデータ ノードに転送します。Redis に基づく業界の分散ストレージ codis (codis-プロキシ層) や、CMEM (アクセス アクセス層)、DCache (プロキシ + ルーター) などの企業など、多くの分散システムがこのアプローチを採用しています。以下の CMEM アーキテクチャ図に示されているように、赤いボックス内のアクセス層はルーティング プロキシ層です。

CMEM アクセス層ルーティング

  • クラスタールーティング

フラグメント ルーティングはクラスタによって実装され、クライアントは任意のノードに接続します。ノードに要求されたフラグメントがある場合は処理されます。そうでない場合、要求は適切なノードに転送されるか、クライアントはターゲットにリダイレクトされます。ノード。たとえば、redis クラスターと同社の CKV+ はこの方式を採用しており、CKV + クラスターのルーティングとフォワーディングは次の図のようになります。
ここに画像の説明を挿入

CKV + クラスタールーティング

上記 3 つのルーティング方法にはそれぞれ長所と短所があり、クライアント ルーティングの実装は比較的単純ですが、ビジネス侵入に対しては比較的強力です。プロキシ レイヤ ルーティングはビジネスに対して透過的ですが、ネットワーク伝送のレイヤが追加されるため、パフォーマンスに一定の影響があり、導入とメンテナンスが比較的複雑になります。クラスター ルーティングはビジネスに対して透過的であり、プロキシ ルーティングよりも構造が 1 つ少ないため、コストが節約されますが、実装はより複雑で、不合理な戦略により複数のネットワーク送信が増加します。

8.4、ダイナミックバランス

バイナリ ツリーと赤黒ツリーのバランスを学ぶとき、データの挿入と削除によってバランスが崩れることは誰もが知っています。ツリーのバランスを維持するために、挿入と削除後に、ツリーの高さを左手と右手で動的に調整し、再バランスを維持します。分散データ ストレージも再バランスする必要がありますが、主に次の側面で不均衡を引き起こす要因は他にもあります。

1) 読み取りおよび書き込みの負荷が増加し、より多くの CPU が必要になります。

2) データの規模が増大し、より多くのディスクとメモリが必要になります。

3) データ ノードに障害が発生し、他のノードに置き換える必要がある。

業界や企業の多くの製品は、Redis クラスターのリシャーディングや HDFS/kafka のリバランスなどの動的なバランス調整もサポートしています。一般的な方法は次のとおりです。

  • 固定パーティション

ノード数をはるかに超える数のパーティションを作成し、各ノードに複数のパーティションを割り当てます。新しいノードが追加された場合、次の図に示すように、バランスを保つために既存のノードから複数のパーティションを均等に削除できます。ノードを削除する場合はその逆も同様です。典型的なものはコンシステント ハッシュであり、2^32-1 個の仮想ノード (vnode) を作成し、それらを物理ノードに分散します。このモードは比較的シンプルで、作成時にパーティションの数を決定する必要がありますが、設定が小さすぎると、データが急速に拡大した場合にリバランスのコストが非常に高くなります。パーティションの数が多く設定されている場合、管理のオーバーヘッドが発生します。
ここに画像の説明を挿入

パーティションのリバランスを修正

  • 動的パーティション

パーティションの数を自動的に増減し、パーティション データが一定のしきい値に達すると、パーティションが分割されます。パーティション データが特定のしきい値まで縮小すると、パーティションはマージされます。B+ ツリーの分割削除操作に似ています。HBase リージョンの分割と結合、TDSQL の Set Shard など、多くのストレージ コンポーネントがこの方式を採用しています。この方法の利点は、データ量に自動的に適応し、優れた拡張性があることです。このタイプのパーティションを使用する場合の注意点は、初期化されたパーティションが 1 つだけの場合、オンラインになった直後に大量のリクエストがあった場合に単一点の負荷が高くなるということです。通常、複数のパーティションが事前に初期化されます。この問題を解決するには、HBase の事前分割などを使用します。

8.5. サブデータベースとサブテーブル

データベースの 1 つのテーブル/マシンに大量のデータがある場合、パフォーマンスのボトルネックが発生します。データベースへの負荷を分散し、読み取りおよび書き込みのパフォーマンスを向上させるには、分割統治戦略を採用する必要があります。データベースとテーブルを分割します。通常、サブデータベースのサブテーブルは次の状況で必要になります。

1) 1 つのテーブル内のデータ量が一定のレベル (mysql など、通常は数千万) に達すると、読み取りおよび書き込みのパフォーマンスが低下します。このときインデックスが大きくなりすぎてパフォーマンスが悪くなるため、単一テーブルを分解する必要があります。

2) データベースのスループットがボトルネックに達し、データの読み取りと書き込みの負荷を分散するためにさらにデータベース インスタンスを追加する必要があります。

サブデータベースおよびサブテーブルは、特定の条件に従ってデータを複数のデータベースおよびテーブルに分散します。これは、垂直分割と水平分割の 2 つのモードに分けることができます。

  • 垂直分割: ビジネス タイプやモジュール タイプなどの特定のルールに従って、1 つのデータベース内の複数のテーブルが異なるデータベースに分散されます。ライブ ブロードキャスト プラットフォームを例にとると、次の図に示すように、ライブ番組データ、ビデオ オン デマンド データ、およびユーザーの注目データは異なるデータベースに保存されます。

ここに画像の説明を挿入

アドバンテージ:

1) セグメント化ルールが明確であり、事業部門が明確である。

2)業務の種類や重要度に応じたコスト管理ができ、拡張性も便利。

3) データのメンテナンスが簡単です。

欠点:

1) 異なるテーブルが異なるライブラリに分割されており、テーブル接続を使用して結合することはできません。しかし、実際の業務設計では基本的に結合操作は行われず、2回のクエリや書き込みによってマッピングテーブルを構築し、より高性能なストレージシステムにデータを構築して格納するのが一般的です。

2) トランザクション処理が複雑になり、トランザクション内での同一ライブラリの異なるテーブルの本来の操作はサポートされなくなりました。たとえば、ライブ ブロードキャストの終了時にライブ プログラムを更新し、ライブ オンデマンド再生を生成することは、サブデータベース後の 1 つのトランザクションで完了することはできません。このとき、フレキシブル トランザクションまたはその他の分散トランザクション ソリューションを使用できます。

水平シャーディング: ハッシュやモジュロなどの特定のルールに従って、同じテーブル内のデータを複数のデータベースに分割します。単純に行単位で分割すると理解でき、分割後のテーブル構造は同じです。たとえば、ライブブロードキャストシステムのブロードキャストレコードは時間の経過とともに蓄積され、テーブルはますます大きくなり、アンカーIDまたはブロードキャスト日付に従って水平に分割して、異なるデータベースインスタンスに保存できます。メリット: 1) 分割後もテーブル構造が同じで業務コードの変更が不要; 2) 単一テーブルのデータ量を制御できるため、パフォーマンス向上につながる; デメリット: 1) などの課題結合、カウント、レコードのマージ、ソート、およびページングにはクロスノード処理が必要であるため、2) 比較的複雑であり、ルーティング戦略を実装する必要があります。要約すると、垂直分割と水平分割にはそれぞれ長所と短所があり、通常は次のとおりです。 2 つのモードが一緒に使用されます。

8.6. タスクのシャーディング

子供の頃、新しい本を配ったのを覚えています。先生は新しい本の山を教室に持ってきて、数人のクラスメートに一緒に配るように頼みました。何人かは中国語を、何冊かは数学を、何冊かは自然を配りました。工場の組立ラインは各工程を並列化した後、最終的に最終製品を合成するが、これも一種のタスクスライシングである。

タスク シャーディングでは、タスクを複数のサブタスクに分割して並列処理し、タスクの実行を高速化します。これには通常、データ シャーディングが含まれます。たとえば、マージ ソートでは、最初にデータが複数のサブシーケンスに分割され、最初に各サブシーケンスが並べ替えられ、最後に順序付けされたシーケンスが合成されます。ビッグ データ処理では、Map/Reduce はデータ シャーディングとタスク シャーディングの古典的な組み合わせです。

9. 保管

シングルコア CPU から分散型、フロントエンドからバックエンドに至るまで、あらゆるシステムでさまざまな機能やロジックを実装するには、読み取りと書き込みの 2 つの操作のみが必要です。各システムのビジネス特性は異なる場合があります。読み取りに重点を置くもの、書き換えに重点を置くもの、両方を組み合わせるものがあります。このセクションでは主に、さまざまなビジネス シナリオでの保存と読み取りのためのいくつかの方法論について説明します。

9.1、読み取りと書き込みの分離

ほとんどのビジネスでは読み取りが多くなり、書き込みが少なくなりますが、システムの処理能力を向上させるために、次の図に示すように、マスター ノードを書き込みに使用し、スレーブ ノードを読み取りに使用することができます。

ここに画像の説明を挿入

読み取り/書き込み分離アーキテクチャには次の特徴があります: 1) データベース サービスはマスター/スレーブ アーキテクチャであり、1 つのマスターと 1 つのスレーブ、または 1 つのマスターと複数のスレーブにすることができます; 2) マスター ノードは書き込み操作を担当します。スレーブノードは読み取り操作を担当します; 3) マスターノードはデータをスレーブノードにコピーします; 基本アーキテクチャに基づいて、マスター-マスター-スレーブ、マスターなど、さまざまな読み取り-書き込み分離アーキテクチャを変更できます。 -奴隷-奴隷。マスター ノードとスレーブ ノードは、mysql+redis などの異なるストレージにすることもできます。

読み書き分離したマスター/スレーブアーキテクチャは一般に非同期レプリケーションを採用しており、データレプリケーションの遅延が問題となるため、高いデータ一貫性を必要としないビジネスに適しています。レプリケーションの遅延によって生じる問題を最小限に抑える方法がいくつかあります。

1) 読み取り後の書き込みの一貫性: つまり、自分の書き込みを読み取ります。これは、書き込み操作後にリアルタイムの更新を必要とするユーザーに適しています。典型的なシナリオは、ユーザーがアカウントの登録またはアカウントのパスワードを変更した後にログインする場合ですが、このとき、スレーブノードに読み取りリクエストが送信されると、データがまだ同期されていない可能性があるため、ユーザーはログインに失敗します。それは容認できないことです。この場合、独自の読み取りリクエストをマスター ノードに送信できますが、他のユーザー情報を表示するリクエストはスレーブ ノードに送信されます。

2) セカンダリ読み取り: 最初にスレーブ ノードを読み取り、読み取りが失敗した場合、または追跡更新時間が特定のしきい値未満の場合はマスター ノードから読み取ります。

3) キー ビジネスはマスター ノードの読み取りと書き込みを行い、非キー ビジネスは個別に読み取りと書き込みを行います。

4) 単調読み取り: ロールバックを回避するために、ユーザーの読み取りリクエストがすべて同じスレーブ ノードに送信されるようにします。たとえば、ユーザーが M マスター ノードの情報を更新すると、データはすぐにスレーブ ノード S1 に同期され、ユーザーが問い合わせるとリクエストが S1 に送信され、更新された情報が表示されます。このとき、データの同期が完了していないスレーブノードS2にリクエストが送信されるため、ユーザーからは更新情報が消えた、つまりデータがロールバックされただけの現象が見られます。

9.2、静的分離と動的分離

動的および静的分離により、頻繁に更新されるデータと更新頻度の低いデータが分離されます。CDN で最も一般的なのは、通常、Web ページは静的リソース (picture/js/css など) と動的リソース (JSP、PHP など) に分割され、静的リソースは動的リソースと静的リソースを使用して CDN エッジ ノードにキャッシュされます。つまり、ネットワーク伝送とサービス負荷が軽減されます。

7.6 で述べたビデオ オン デマンド キャッシュの動的および静的分離など、動的分離をデータベースと KV ストレージに採用することもできます。データベースでは、動的フィールドと静的フィールドの分離は垂直セグメンテーションに似ており、動的フィールドと静的フィールドを異なるデータベース テーブルに格納し、データベース ロックの粒度を減らし、同時に異なるデータベース リソースを割り当てることで使用率を合理的に向上させることができます。

9.3. ホットとコールドの分離

ホットとコールドの分離は、あらゆるストレージ製品や大規模ビジネスにとって必須の機能であると言え、Mysql、ElasticSearch、CMEM、Grocery などはすべて、直接的または間接的にホットとコールドの分離をサポートしています。ホット データをよりパフォーマンスの高いストレージ デバイスに配置し、コールド データを安価なディスクにシンクすることでコストを節約します。Tencent Cloud のコストを節約するために、Penguin E スポーツもアンカー ファンの数と時間に応じてライブ再生にホットとコールドの分離を採用しています。次の図は、ES のコールドとホットの分離の実装アーキテクチャ図です。
ここに画像の説明を挿入

ES コールドとホットの分離アーキテクチャ図

9.4. 書き換えと軽い読み取り

ライトの書き換えに関する私の個人的な理解には、次の 2 つの意味があると考えられます。1) クリティカルな書き込み、非同期レプリケーションなどの読み取りの重要性を軽減し、マスター ノードが正常に書き込みを行うことを保証すること、およびスレーブ ノードの読み取りが同期遅延を許容できること。2) より多くのロジックを書き込み、より少ないロジックの読み取りを行い、計算ロジックを読み取りから書き込みに転送します。これは、読み取りリクエスト中に計算を実行する必要があるシナリオに適しています。たとえば、共通リーダーボードは、リクエストの読み取り時ではなく書き込み時に構築されます。

WeiboやMomentsなどのソーシャルプロダクトのシナリオでは、フォローや友達と同様の機能があります。例として友達の輪のシミュレーションを考えてみましょう (友達の輪が具体的にどのように行うのかはわかりません)。友達の輪に入ったときにユーザーに表示される友達メッセージのリストが、ユーザーの新しいメッセージをたどることによって組み立てられているとします。リクエストするときに友達を指定し、時間ごとに並べ替えるのは、友達のサークルからのこれほど多くのリクエストに応えるのは明らかに困難です。リライトして軽く読むという方法をとって、モーメントに投稿するときにリストを構築して、それを直接読むという方法もあります。

アクター モデルに従って、ユーザーのメールボックスを作成します。ユーザーは、モーメントに投稿した後に自分のメールボックスを作成した後、戻り、メッセージを友人のメールボックスに非同期にプッシュします。これにより、友人が自分のメールボックスを読んだときに、そのメッセージが彼のモーメントのメッセージリスト。以下に示すように:
ここに画像の説明を挿入

リライトライト読み取り処理

上の図は軽く書き換えるアイデアを示しているだけで、実際の応用には他にもいくつかの問題があります。1) 拡散書き込み: これは拡散書き込み行為です。大家族に友達が多い場合、拡散書き込みコストも非常に高くつき、人によっては何千年もモーメントを読まなかったり、友達をブロックしたりすることもあります。友達の数が一定の範囲内にある場合のみ、数が多すぎる場合はプッシュプルの組み合わせを使用したり、アクティブなインジケーターを分析したりするなど、他の戦略を採用する必要があります。2) メールボックスの容量: 一般に、モーメントを表示する場合、下にスクロールして表示することはありません。このとき、メールボックス ストレージのエントリ数を制限する必要があり、超過したエントリは他のストレージからクエリされます。

9.5. データの異質性

データの異質性は主に、クエリを高速化するためにさまざまな次元に従ってインデックス関係を確立することです。たとえば、JD.com や Tmall などのオンライン ショッピング モールは、通常、注文番号に応じてデータベースとテーブルを分割します。注文番号は同じテーブル内にないため、購入者または販売者の注文リストを照会するには、すべてのサブデータベースを照会してデータ集約を実行する必要があります。異種インデックスを構築でき、注文生成と同時にバイヤー、マーチャントから注文までのインデックステーブルを作成でき、このテーブルはユーザーIDに応じてデータベースとテーブルに分割できます。

10. 待ち行列

システム アプリケーションでは、すべてのタスクとリクエストをリアルタイムで処理する必要はありません。多くの場合、データには強い一貫性は必要ありませんが、最終的な一貫性を維持することだけが必要です。システム モジュール間の依存関係を知る必要がない場合もあります。これらのシナリオでは、データに強い一貫性は必要ありません。 、キュー テクノロジーが提供するものはたくさんあります。

10.1. アプリケーションシナリオ

キューには幅広いアプリケーション シナリオがあり、次のように要約できます。

  • 非同期処理: 通常、業務リクエストの処理プロセスは多数ありますが、このリクエストではすぐに処理する必要のない処理もあり、このときに非同期処理を使用できます。たとえば、ライブ ブロードキャスト プラットフォームでは、アンカーがブロードキャストを開始した後、ブロードキャスト通知をファンに送信する必要があります。ブロードキャスト イベントをメッセージ キューに書き込むと、特別なデーモンがブロードキャスト通知を処理して送信します。これにより、ブロードキャストの応答速度が向上します。

  • トラフィック クリッピング: 同時実行性の高いシステムのパフォーマンスのボトルネックは、通常、データベースの読み取りおよび書き込みなどの I/O 操作にあります。突然のトラフィックに直面した場合は、メッセージ キューをキューイングとバッファリングに使用できます。ペンギン E スポーツを例に挙げると、時々、Menglei のような大きなアンカーが登場します。この時点では、多数のユーザーがアンカーを購読しているため、購読プロセスには複数の書き込み操作が必要ですが、この時点では、ユーザーがどのアンカーをフォローして保存したかのみを書き込みます。次に、一時ストレージのメッセージ キューに入り、アンカーが誰をフォローしているか、その他のストレージを書き込みます。

  • システムの分離: 一部の基本サービスは、ペンギン E スポーツの検索、推奨、イベントをブロードキャストするために必要なその他のシステムなど、他の多くのサービスに依存しています。ブロードキャスト サービス自体は、誰がデータを必要とするかは気にせず、ブロードキャストを処理するだけで済みます。依存サービス (最初のポイントで説明したブロードキャスト通知を送信するデーモンを含む) は、ブロードキャスト イベントのメッセージ キューをサブスクライブして分離できます。 。

  • データの同期: メッセージ キューは、特にシステム間でデータを同期する場合に、データ バスとして機能します。私が以前開発に参加した分散キャッシュシステムを例に挙げると、Mysqlを書く際にRabbitMQを介してRedisにデータを同期することで、結果整合性のある分散キャッシュを実現しています。

  • 柔軟なトランザクション: 従来の分散トランザクションは、2 フェーズ プロトコルまたはその最適化されたバリアントを使用して実装されます。トランザクションが実行されると、ロック リソースを競合して待機する必要があります。同時実行性の高いシナリオでは、システムのパフォーマンスとスループットが大幅に低下します。デッドロックが発生することもあります。インターネットの中核は高い同時実行性と高可用性であり、一般に従来のトランザクションの問題を柔軟なトランザクションに変換します。次の図は、Ali でのメッセージ キューに基づく分散トランザクションの実装です (詳細については、「エンタープライズ IT アーキテクチャ変革の方法」、「アリババ台湾中部の戦略思考とアーキテクチャの実戦」、「WeChat Reading」を参照してください。電子版があります)。

ここに画像の説明を挿入

その中心となる原則とプロセスは次のとおりです。

1) 分散トランザクション イニシエーターは最初のローカル トランザクションを実行する前に、トランザクション メッセージを MQ に送信してサーバーに保存します。MQ コンシューマーはメッセージを認識して消費することができません①②。

2) トランザクション メッセージが正常に送信された後、スタンドアロン トランザクション操作③を開始します。

a) ローカルトランザクションが正常に実行された場合、MQ サーバーのトランザクションメッセージを通常の状態に更新します④。

b) ローカル トランザクションの実行中のダウンタイムやネットワークの問題により、ローカル トランザクションが MQ サーバーにタイムリーにフィードバックされない場合、前のトランザクション メッセージは常に MQ に保存されます。MQ サーバーはトランザクション メッセージを定期的にスキャンし、メッセージの保存時間が一定のしきい値を超えていることを検出すると、MQ 運用端末にトランザクションの実行状況を確認するリクエストを送信します5。

c) ローカルトランザクション結果⑥を確認した後、トランザクションが正常に実行された場合は、以前に保存されたトランザクションメッセージを通常の状態に更新します。そうでない場合は、MQ サーバーにトランザクションメッセージを破棄するように通知します。

3) コンシューマはトランザクション メッセージを取得し、通常の状態に設定した後、2 番目のローカル トランザクション ⑧ を実行します。実行が失敗した場合、MQ 送信者は最初のローカル トランザクションをロールバックまたはフォワード補正するように通知されます。

10.2. アプリケーションの分類

  • バッファキュー: キューの基本的な機能は、TCP の送信バッファなどのバッファキューイングであり、アプリケーション層のバッファは通常、ネットワークフレームワークに追加されます。バーストトラフィックを処理するためにバッファキューを使用すると、処理がスムーズになり、システムが保護されます。12306 でチケットを購入した人はそれを理解しています。

ここに画像の説明を挿入

ビッグ データ ログ システムでは、通常、ログ収集システムとログ解析システムの間にログ バッファ キューを追加して、解析システムがブロックされたり、解析システムの負荷が低いときにログが破棄されたりするのを防ぐ必要があります。負荷を軽減し、独自のアップグレードとメンテナンスを容易にします。下図のTianji Pavilionのデータ収集システムでは、ログバッファキューとしてKafkaが使用されています。
ここに画像の説明を挿入

  • リクエスト キュー: ユーザー リクエストをキューに入れます。ネットワーク フレームワークには一般にリクエスト キューがあります。たとえば、spp にはプロキシ プロセスとワーク プロセスの間に共有メモリ キューがあり、taf にもネットワーク スレッドとサーヴァント スレッドの間にキューがあります。フロー制御、過負荷保護、タイムアウト破棄などに使用されます。

ここに画像の説明を挿入

  • タスク キュー: 非同期実行のためにタスクをキューに送信します。最も一般的なのはスレッド プールのタスク キューです。

  • メッセージキュー

メッセージ配信には、主にポイントツーポイントとパブリッシュ/サブスクライブの 2 つのモードがあります。一般的なものは、RabbitMQ、RocketMQ、Kafka などです。次の表は、一般的に使用されるメッセージ キューの比較です。

比較項目 アクティブMQ ラビットMQ ロケットMQ カフカ
コミュニティ/会社 アパッチ Mozilla パブリック ライセンス アリババ アパッチ
成熟度と認可 成熟した/オープンソース 成熟した/オープンソース 比較的成熟した/オープンソース 成熟した/オープンソース
開発言語 ジャワ アーラン ジャワ スカラ&Java
クライアントがサポートする言語 Java、C/C++、Python、PHP、Perl、.netなど Erlang、Java、Ruby などを正式にサポートしており、コミュニティは多言語 API を作成し、一般的に使用されているほぼすべての言語をサポートしています。 Java、C++ Java を正式にサポートしており、オープンソース コミュニティには PHP、Python、Go、C/C++、Ruby などの複数の言語バージョンがあります。
プロトコルのサポート OpenWire、STOMP、REST、XMPP、AMQP マルチプロトコルのサポート: AMQP、XMPP、SMTP、STOMP 自分で定義したセット (コミュニティが提供する JMS – 未成熟) 独自のプロトコル、コミュニティは HTTP プロトコルのサポートをカプセル化します
ZooKeeper + LevelDB に基づくマスター/スレーブの実装 マスター/スレーブ モード。マスターはサービスを提供し、スレーブはバックアップ専用 (コールド スタンバイ) マルチマスターモード、マルチマスターおよびマルチスレーブモード、非同期レプリケーションモード、同期二重書き込みをサポート レプリカ メカニズムをサポートします。リーダーに障害が発生すると、バックアップが自動的にリーダーを置き換え、リーダーを再選出します (Zookeeper に基づく)
データの信頼性 マスター/スレーブ、データ損失の可能性が低い データが失われないことを保証でき、バックアップ用のスレーブもあります 非同期リアルタイム フラッシュ、同期フラッシュ、同期レプリケーション、および非同期レプリケーションをサポート データは信頼性が高く、フォールト トレランスと災害復旧機能を備えたレプリカ メカニズムが存在します。
スタンドアロンのスループット 10,000レベル 10,000レベル 100,000レベル、高スループットをサポート 100,000 レベル、高スループット、リアルタイム データ計算、ログ収集、その他のシナリオのためにビッグ データ システムと連携
メッセージの遅延 ミリ秒 マイクロ秒レベル ミリ秒 ミリ秒以内
フロー制御 クレジット ベースのアルゴリズムに基づいており、プロデューサー レベルで動作する、受動的にトリガーされる内部保護メカニズムです。 クライアント レベルとユーザー レベルをサポートしており、アクティブな設定を通じてフロー制御をプロデューサーまたはコンシューマーに適用できます。
持久化能力 デフォルトのメモリ。正常に閉じられた場合、メモリ内の未処理のメッセージはファイルに永続化され、JDBC 戦略が使用されている場合はデータベースに保存されます。 メモリ、ファイル、データ蓄積をサポートします。しかし、蓄積はスループットに影響を与えます ディスクファイル ディスクファイル。ディスク容量が十分であれば無制限にメッセージを蓄積可能
負荷分散 サポート サポート サポート サポート
管理インターフェース 一般的 より良い コマンドラインインターフェース 公式にはコマンドラインバージョンのみを提供しており、yahooは独自のWeb管理インターフェイスをオープンソース化しています
導入方法と難易度 独立/簡単 独立/簡単 独立/簡単 独立/簡単
機能サポート MQ フィールドの関数は比較的完成されています Erlang に基づいて開発されており、強力な同時実行機能、優れたパフォーマンス、低遅延を備えています。 MQ 機能は比較的完全、つまり分散されており、優れたスケーラビリティを備えています。 関数は比較的単純で、主に単純な MQ 関数をサポートしており、ビッグデータ分野のリアルタイム コンピューティングやログ収集に広く使用されています。

要約する

この記事では、高パフォーマンスのサービスのバックグラウンド開発と設計のための一般的な方法とテクノロジについて説明および要約し、マインド マップを通じて一連の方法論を要約します。もちろん、これは高性能だけがすべてというわけではありません。ネットワークプログラミングのI/OモデルやC10K問題、ビジネスロジックのデータ構造やアルゴリズム設計、各種ミドルウェアのパラメータチューニングなど、特定の分野にはそれぞれ独自の高性能化の方法があります。記事にはいくつかのプロジェクトの実践例も記載されていますが、無理な点やもっと良い解決策があればご教示ください。

おすすめ

転載: blog.csdn.net/u011397981/article/details/131372137