この作品は、クリエイティブ・コモンズ表示-非営利目的-共有4.0国際ライセンス契約に基づいて同じ方法でライセンスされています。
この作品(LizhaoロングによってボーエンリーZhaolongのによって作成)李Zhaolongの確認は、著作権を明記してください。
記事のディレクトリ
前書き
キャッシュはプログラマーにとって明らかに非常に重要なものですが、多くの人は実際にはほとんどの場合その存在を無視しています。これは本当に非常に残念なことです。今日はRedis、memcache、Tairについては説明しません。このような分散キャッシュはそうではありません。 VFSの4レベルキャッシュなど、オペレーティングシステムの論理キャッシュについて説明します[1]。CPUのキャッシュにある2つの問題、缓存一致性
合計のfalse sharing
問題について話しているだけです。
私が思うこれらの2つの問題は、ほとんどの場合、人々はそれらにあまり注意を払わないでしょう。なぜなら、そのような低レベルのものは私たちのプログラミングとどのくらい関係があるのでしょうか?答えは密接に関連しており、コードの効率に不注意に影響を与える可能性がありますが、わからないため、それらを学習する必要があるようです。しかし、心配しないでください。この問題は、コード表示の助けを借りて簡単に回避できます。
偽共有
これは、CPU内の複数のコアでのキャッシュの分散です。同じCPU内にありますが、独自のプライベートキャッシュがあることがわかります。図は、L1およびL2レベルのキャッシュを示しています。通常の状況では、キャッシュが多いほどヒット率(時間的局所性と空間的局所性)が高くなるため、これは問題ではないようです。もちろん、これは正常です。マルチスレッドを導入すると、プロセス内のすべてのスレッドが1つになります。多级页表
合計mm_struct
で、彼らはほぼすべてのデータを共有することができますが、スレッドはまた、彼らは別のコア上で実行できることを意味し、スケジューリングの単位、[2]または異なるCPUであるため、変更は当然異なって書き込まれます。キャッシュ。
この時点cache line
で書いたばかりの場合は、他のcore
変更が加えられており[2]、この時点で導入され缓存一致性
ます。もちろん、まだ話していません。当面の間、その機能は確実にすることであることがわかっています。特定のアドレスの読み取り操作によって返される値は、そのアドレスの最新の値である必要があります。これは、異なるコアの複数のキャッシュラインがデータを同期する必要があることを意味します。これは、明らかに小さなオーバーヘッドではありません。
このとき、2つのスレッドが同じキャッシュライン(同じキャッシュラインを含む)の変数を絶えず変更しているとしたら、どうなるでしょうか。つまり、2つのキャッシュラインが無効になり、継続的に再ロードされます。
ウィキのfalse sharing
定義を見てみましょう:
コンピュータサイエンスでは、偽共有は、キャッシュメカニズムによって管理される最小のリソースブロックのサイズで分散されたコヒーレントキャッシュを備えたシステムで発生する可能性のある、パフォーマンスを低下させる使用パターンです。システム参加者が、他の当事者によって変更されることのないデータに定期的にアクセスしようとしたが、それらのデータが変更されたデータとキャッシュブロックを共有している場合、論理的な必要性がないにもかかわらず、キャッシュプロトコルによって最初の参加者がユニット全体をリロードする必要があります。 。キャッシングシステムはこのブロック内のアクティビティを認識せず、最初の参加者に、リソースの真の共有アクセスに必要なキャッシングシステムのオーバーヘッドを負担させます。
実際、それは私たちが上で説明したものです。
確認するコードを書いてみましょう4 x Intel® Core i5-7200U CPU @ 2.50GHz
。私のマシン構成は次のとおりです。コードは実際には非常に単純です。2つのグローバル変数を適用して、同じキャッシュラインに緊密に配置できるようにしてから、2つのスレッドをそれぞれ10億回変更します。時間の消費を見てください。 2つのスレッドが同じコアで実行されないようにするために、CPUのアフィニティを設定できます。具体的なコードは次のとおりです。
bool SetCPUaffinity(int param){
cpu_set_t mask; // CPU核的集合
CPU_ZERO(&mask); // 置空
CPU_SET(param,&mask); // 设置亲和力值,把cpu加到集合中 https://man7.org/linux/man-pages/man3/CPU_SET.3.html
// 第一个参数为零的时候默认为调用线程
if (sched_setaffinity(0, sizeof(mask), &mask) == -1){
// 设置线程CPU亲和力
return false;
// 看起来五种errno没有必要处理;
} else {
return true;
}
}
int num0;
int num1;
void thread0(int index){
SetCPUaffinity(index);
int count = 100000000; // 1亿
while(count--){
num0++;
}
return;
}
void thread1(int index){
SetCPUaffinity(index);
int count = 100000000;
while(count--){
num1++;
}
return;
}
int main(){
vector<std::thread> pools;
pools.push_back(thread(thread0, 0));
pools.push_back(thread(thread1, 1));
for_each(pools.begin(), pools.end(), std::mem_fn(&std::thread::join));
return 0;
}
実行結果は以下のとおりです。
コードに少し変更を加えています。つまり、2つのスレッドをシリアルにするために、main
関数部分を変更するだけで済みます。コードは次のとおりです。
int main(){
vector<std::thread> pools;
pools.emplace_back(thread(thread0, 0));
pools[0].join();
pools.emplace_back(thread(thread1, 1));
pools[1].join();
return 0;
}
ショックを受けた、違いのほぼ4倍。
もちろん、これら2つの変数を同じキャッシュラインにない限り、そのような問題は発生しません。変更を加えて、2つのスレッドによって変更された変数を64ビットで整列させることができます。もちろん、ここでの64は修正されておらず、独自のマシン構成に応じて変更する必要があります。実行してcat /proc/cpuinfo
そのうちの1つを表示するか、cache_alignment
直接実行cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
して表示することができます[4]。
int num0 __attribute__ ((aligned(64)));
int num1 __attribute__ ((aligned(64)));
もちろん、次のように書くこともできます。
int num0;
char arr[60]; // 可设置一些标记位
int num1;
消費量が再び減少していることがわかります。
次に、2つのスレッドを同じCPUに割り当てて、効果を確認します。CPUアフィニティを設定するときは、同じパラメータを設定します。メインはおそらく次のとおりです。
int num0;
int num1;
int main(){
vector<std::thread> pools;
pools.emplace_back(thread(thread0, 1));
pools.emplace_back(thread(thread1, 1));
for_each(pools.begin(), pools.end(), std::mem_fn(&std::thread::join));
return 0;
}
結果が期待どおりであることがわかります。
キャッシュコヒーレンシ
false sharing
この問題の理由は、缓存一致性
各CPUが最新の値を読み取れるように、キャッシュ間の整合性を同期させる必要があるためです。この質問に最初に興味を持ったのは、アトミック操作を実現する方法を考えていたときでした。当時、一般的なCAS操作はCMPXCHG(Intel x86)
命令に基づいて行う必要があることを知っていました。次のステップはバスロックとキャッシュコヒーレンシです。前者はシステムバスを発行するためのもので、#lock
シグナル、他のCPUは現時点ではシステムバスを使用できず、動作は排他的な方法でアトミックであることが保証されています。もちろん、LOCK#
CPUとメモリ間の通信をロックするために使用されるため、このロックの粒度が大きすぎることもわかります。これにより、他のプロセッサはロック中にメモリアドレスのデータを操作できなくなります。もちろん必ずしもアトミックである必要はありません。操作では#LOCK
シグナルを使用する必要があります。
P6ファミリプロセッサ以降、LOCKプレフィックスが命令のプレフィックスとして付けられ、アクセスされているメモリ領域がプロセッサの内部にキャッシュされている場合、通常、LOCK#信号はアサートされません。代わりに、プロセッサのキャッシュのみがロックされます。ここで、プロセッサのキャッシュコヒーレンシメカニズムは、メモリに関して操作がアトミックに実行されることを保証します。キャッシュのロックの詳細については、インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアル第3A巻の第8章の「内部プロセッサーキャッシュに対するロック操作の影響」を参照してください。
Intel486およびPentiumプロセッサの場合、ロックされているメモリ領域がプロセッサにキャッシュされている場合でも、LOCK#信号はLOCK操作中に常にバス上で宣言されます。
P6以降のプロセッサシリーズでは、LOCK操作中にロックされたメモリ領域がLOCK操作を実行するプロセッサにライトバックメモリとしてキャッシュされ、キャッシュラインに完全に含まれている場合、プロセッサはバス上でLOCK#を宣言しない場合があります。信号。代わりに、メモリの場所を内部的に変更し、キャッシュコヒーレンシメカニズムを使用して、操作がアトミックに実行されるようにします。この操作は「キャッシュロック」と呼ばれます。キャッシュコヒーレンシメカニズムは、同じメモリ領域をキャッシュした2つ以上のプロセッサが、その領域のデータを同時に変更するのを自動的に防ぎます。
詳細については、[6]、[7]を参照してください。
続けましょう。メモリを変更した後、キャッシュに変更されたデータがある可能性があります。読み取るたびに最新のデータを読み取るには、これを行うためにキャッシュの整合性が必要です。ここでより古典的なのは間違いなくMESI
[8]プロトコルです。IntelプロセッサはMESIから進化したMESIF
[10]プロトコルを使用しますが、AMDはMOESI
[9]プロトコルを使用し、基本的MESI
にはステートマシンを使用して問題を解決します。
プロトコルの特定の実装については、これらについて述べた記事がたくさんあるので、話したくありません。私が話したいのは、これらのプロトコルの簡単な学習に基づいています[10] [11] [12]つまり、分散システムでキャッシュの一貫性を維持する方法です。
実際、この質問が提起されるとすぐに、次のシーンが頭に浮かびました。
- Chubbyのクライアント側キャッシュの一貫性
- ぽっちゃりのキープライブメカニズム
- GFSのクライアントチャンク情報のキャッシュ整合性
- GFSのマスターとチャンクのリースメカニズム
- キャッシュデータベースとデータベースの整合性
実際、上記の5つのシナリオの1、3、および5は、私たちの理解ではキャッシュの一貫性である必要があります。それでは、なぜ2、4をその中に入れたのでしょうか。
まず缓存一致性
、Wiki [13]の定義を見てみましょう。
コンピュータアーキテクチャでは、キャッシュコヒーレンスは共有リソースデータの均一性であり、最終的には複数のローカルキャッシュに保存されます。システム内のクライアントが共通メモリリソースのキャッシュを維持している場合、インコヒーレントデータで問題が発生する可能性があります。これは特にマルチプロセッシングシステムのCPUの場合に当てはまります。
コンピュータアーキテクチャでは、キャッシュの一貫性は、最終的に複数のローカルキャッシュに格納される共有リソースデータの一貫性です。システム内のクライアントが一般的なメモリリソースのキャッシュを維持している場合、データの不整合により、特にマルチプロセッシングシステムのCPUで問題が発生する可能性があります。
明らかに、キャッシュの定義が呼び出されていることがわかりますshared resource data
。これは、スコープが非常に広いことを意味します。したがって、両端間で維持されているリースをキャッシュと見なすことができますか?分散ロックなど、より詳細に言えば、両方の当事者によって維持されているロック情報をキャッシュとして使用できますか?「でのセキュリティと展望ChubbyGoデモ」私は最大の問題は、分散ロックを作ることであると思われるこの時点では、実際には、最大の問題を紹介して回避デッドロック分散ロック・タイムアウト機構にために分析lock server(锁服务器)
とclient(持锁者)
で、同じを見ます事実、あることを自然に思える缓存一致性
、でも私が思うに一貫我々は分散で議論されている特殊なキャッシュの一貫性はありませんか?もちろん個人的な意見ですが、間違いがあればハイハンにお願いします。
上記を考えた後、2、4を入れました。前者は分散ロックのタイムアウトメカニズムに対するChubbyのソリューションであり、後者はGFSmaster
でのchunk
サーバーメンテナンスのリースです。
5番目のシナリオソリューションは[14] [18]を参照できますが、これは分散トランザクション(強い一貫性)の問題だと思います。
1と3は、最も直感的なデータ整合性です。シナリオは[16] 2.7で説明されています。ソリューションは非常に単純です。書き込み要求を直接ブロックし、この変更されたデータキャッシュを持つすべてのクライアントにキャッシュ無効化情報を送信します。すべての応答の後書き込み操作を終了すると、この時点でキャッシュの整合性は明らかに満たされますが、書き込み効率も明らかに非常に低くなります。3つ目については、[15] 3.2の2番目の段落で説明されています。非常に簡潔ですがchunk
、無効化後、リースを保持しなくなるように復元できるという非常に重要な点も明らかになります。これにより、書き込み操作のオーバーヘッドを実際に大幅に削減できます。上記の2つのポイントは、それぞれ2つの段階でこの問題を解決しました。
2と4に関しては、2つのシステムによる両方のシステムのメンテナンスlease
のためのソリューションです。[15]には詳細な説明はありませんが、[16] 2.8の説明keepalive机制
は非常に明確です。本質はタイムアウトを維持することです。クライアントのタイムアウト後、リソース(キャッシュ)が無効になり、リースが再維持されると有効になると見なされます。また、2者間の時差と通信の不確実性のため、クライアントには猶予があります。詳細に理解できる期間[16]。この問題を回避するためにChubbyGoで使用したfence
方法。
明らかに、非常に興味深い場所を見つけることができます。分散システムのソリューションでは、MESI
メッセージパッシングに基づくステートマシン遷移に似た、オペレーティングシステムキャッシュのようなキャッシュコヒーレンス戦略を採用していません。
どうして?その理由は、システムバスとはネットワーク通信が違うためだと思います。前者の遅延は最大ではなく(例えば、唯一のルートのルーターが破損している)、バス通信は基本的に信頼できます。基本的に無駄な時間がないので、そのようにメッセージングのバスの中でそうであるが、確かに非常に効率的である(2、4、メンテナンスのタイムアウトで、分散トランザクションにメッセージングのいくつかのラウンド2PC
、 3PC
)、各ステップが必要です。分散システムでこれを行うと、リクエストが長時間遅れる可能性があるため(1つが到着せず、残りは成功し、システムのフォールトトレランスが非常に高くなるため、操作が長時間ブロックされる可能性があります。貧しい)。
総括する
このことから、特に分散システムでは、通信遅延の信頼性が低く、多くの問題がはるかに困難になるため、同じ問題でもさまざまな制約の下でさまざまな解決策が得られることがわかります。
私はまだ20歳ですが、私の生涯で、最新の通信技術によって、データが特定の時間遅延(より短い時間)内に確実に配信されることを確認できることを心から願っています。
参照:
- 「LinuxIOについてもう一度話してください」
- 「(コンセプト)マルチCPUとマルチコアCPUとハイパースレッディング(ハイパースレッディング)」
- https://en.wikipedia.org/wiki/False_sharing
- 「CPUキャッシュがパフォーマンスに与える影響」
- https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu
- 「CPUのLOCK命令について話してください」
- 「IntelLOCKプレフィックスコマンド」
- https://en.wikipedia.org/wiki/MESI_protocol
- https://en.wikipedia.org/wiki/MOESI_protocol
- https://www.realworldtech.com/common-system-interface/5/
- 「「ビッグトークプロセッサ」キャッシュコヒーレンシプロトコルMESI」
- 「キャッシュコヒーレンスプロトコルについて話す」
- キャッシュコヒーレンスウィキ
- 「分散キャッシュ(一貫性)」
- 論文「Googleファイルシステム」
- 论文《疎結合分散システムのぽっちゃりロックサービス》
- 论文《リース:分散ファイルキャッシュの一貫性のための効率的なフォールトトレラントメカニズム》
- 「分散データベースとキャッシュの二重書き込み整合性スキームの分析」