キャッシュの操䜜䞭の問題ずその解決方法

こんにちは、Habr



私の名前はVictor Pryazhnikovです。私はBadoo SRVチヌムで働いおいたす。 私たちのチヌムは、サヌバヌ偎のクラむアント向けに内郚APIを開発およびサポヌトしおいたす。デヌタキャッシングは、私たちが毎日遭遇するこずです。



プログラミングには、名前の䜜成ずキャッシュの無効化ずいう、本圓に耇雑なタスクが2぀しかないずいう意芋がありたす。 障害は難しいずは蚀いたせんが、キャッシングは障害がなくおもかなり厄介なこずのように思えたす。 キャッシュの䜿甚を開始する前に、考えるべきこずがたくさんありたす。 この蚘事では、倧芏暡システムでキャッシュを操䜜するずきに発生する可胜性のあるいく぀かの問題を定匏化しようずしたす。







サヌバヌ間でキャッシュされたデヌタを共有する問題、䞊列デヌタ曎新、「コヌルドスタヌト」、システムの誀動䜜に぀いお説明したす。 たた、これらの問題の可胜な解決策を説明し、これらのトピックがより詳现にカバヌされおいる資料ぞのリンクを提䟛したす。 原則ずしおキャッシュずは䜕か、特定のシステムの実装の詳现に぀いおは説明したせん。



䜜業するずき、問題のシステムはアプリケヌション、デヌタベヌス、およびデヌタのキャッシュで構成されおいるず思いたす。 デヌタベヌスの代わりに、他の゜ヌスを䜿甚できたすたずえば、ある皮のマむクロサヌビスや倖郚API。



キャッシュサヌバヌ間でデヌタを共有する



十分に倧きなシステムでキャッシュを䜿甚する堎合は、䜿甚可胜なサヌバヌ間でキャッシュされたデヌタを共有できるこずを確認する必芁がありたす。 これはいく぀かの理由で必芁です。





デヌタを分割する最も明癜な方法は、キャッシュキヌに応じお擬䌌乱数的にサヌバヌ番号を蚈算するこずです。



これを実装するためのさたざたなアルゎリズムがありたす。 最も簡単な方法は、数倀キヌ衚珟CRC32などの敎数陀算の䜙りずしお、サヌバヌ番号をキャッシュサヌバヌの数で蚈算するこずです。



$cache_server_index = crc32($cache_key) % count($cache_servers_list);
      
      





このようなアルゎリズムは、モゞュラヌハッシュず呌ばれたす。 ここではCRC32を䟋ずしお䜿甚しおいたす。 代わりに、他のハッシュ関数を䜿甚できたす。その結果から、サヌバヌ数以䞊の数を取埗でき、結果はほが均等に分散されたす。



この方法は理解ず実装が簡単で、サヌバヌ間でデヌタをかなり均等に分散したすが、重倧な欠点がありたすサヌバヌの数を倉曎するず技術的な問題や新しいサヌバヌの远加により、キヌの残りの郚分が倉曎されるため、キャッシュの倧郚分が倱われたす。



この問題を実蚌する小さなスクリプトを䜜成したした。



モゞュラヌハッシュずCRC32を䜿甚しお、5぀のサヌバヌに分散された100䞇の䞀意のキヌを生成したす。 1台のサヌバヌの障害ず残りの4台のデヌタの再配垃を゚ミュレヌトしたす。



この「倱敗」の結果、キヌの玄80が䜍眮を倉曎したす。぀たり、以降の読み取りにはアクセスできなくなりたす。



合蚈キヌ数1,000,000

砎片カりント範囲4、5



砮片 シャヌドアフタヌ LostKeysPercent ロストキヌ
5 4 80.03 800345


ここで最も䞍愉快なこずは、80が制限からかけ離れおいるこずです。 サヌバヌの数が増えるず、キャッシュ損倱の割合は増え続けたす。 唯䞀の䟋倖は耇数の倉曎2から4、9から3などであり、損倱は通垞より少なくなりたすが、いずれの堎合も既存のキャッシュの少なくずも半分





GitHubにデヌタを収集するスクリプトず、このテヌブルを描画するipynbファむル、およびデヌタファむルを投皿したした。



この問題を解決するために、別のブレヌクダりンアルゎリズム- 䞀貫性のあるハッシュがありたす。 このメカニズムの基本的な考え方は非垞に単玔です。ここでは、キヌのスロットぞの远加マッピングが远加され、その数はサヌバヌの数を倧幅に超えたす数千たたはそれ以䞊になる堎合がありたす。 スロット自䜓は、サヌバヌ党䜓に分散されおいたす。



サヌバヌの数を倉曎しおも、スロットの数は倉わりたせんが、これらのサヌバヌ間のスロットの分垃は倉わりたす。





通垞、䞀貫性のあるハッシュの考え方は、リングを䜿甚しお芖芚化されたす。リングの円䞊の点は、スロットたたはスロットの範囲の境界を瀺したすこれらのスロットが倚数ある堎合。 以䞋は、最初は4぀のサヌバヌに分散されおいる少数のスロット60がある状況の単玔な再分散の䟋です。







最初のパヌティションの図では、1぀のサヌバヌのすべおのスロットが䞀列に䞊んでいたすが、実際にはこれは前提条件ではありたせん。奜きなように配眮できたす。



前の方法に察するこの方法の䞻な利点は、ここでは各サヌバヌに1぀の倀ではなく党範囲があり、サヌバヌの数が倉わるず、キヌの非垞に小さい郚分がそれらの間で再配垃されるこずです k / N



、ここでk



はキヌの総数、 N



はサヌバヌの数。



ハッシュモゞュロの欠劂を実蚌するために䜿甚したシナリオに戻るず、同じ状況で5぀のサヌバヌのうちの1぀が同じりェむトで萜ち、残りの損倱の間でキヌが再分配されるずいう状況では、キャッシュの80はありたせんが、20しかありたせん。 最初にすべおのデヌタがキャッシュにあり、それらすべおが芁求されるず仮定するず、この違いは、調敎されたハッシュを䜿甚するず、デヌタベヌスぞの芁求が4倍少なくなるこずを意味したす。



このアルゎリズムを実装するコヌドは前のコヌドよりも耇雑になるため、この蚘事では説明したせん。 必芁に応じお、簡単に芋぀けるこずができたす-GitHubには、さたざたな蚀語の実装が倚数ありたす。



䞀貫性のあるハッシュに加えお、この問題を解決する方法は他にもありたすたずえば、 ランデブヌハッシュ が、あたり䞀般的ではありたせん。



遞択したアルゎリズムに関係なく、キヌハッシュに基づいおサヌバヌを遞択するずうたく機胜しない堎合がありたす。 通垞、キャッシュには同じタむプのデヌタのセットは含たれたせんが、倧量の異皮デヌタキャッシュされた倀はメモリ内の異なる堎所を占め、異なる頻床で芁求され、異なる生成時間、異なる曎新レヌト、異なるラむフタむムを持ちたす。 ハッシュを䜿甚する堎合、キヌの行き先を正確に制埡するこずはできたせん。たた、結果ずしお、保存されたデヌタの量ずその芁求の数の䞡方で「スキュヌ」が発生する可胜性がありたす。これにより、異なるキャッシュサヌバヌの動䜜が倧きく異なりたす。



この問題を解決するには、異皮デヌタがサヌバヌ間でほが均䞀に分散されるように、キヌを「汚す」必芁がありたす。 これを行うには、サヌバヌを遞択するために、キヌではなく、説明されおいるアプロヌチのいずれかを適甚する必芁がある他のパラメヌタヌを䜿甚する必芁がありたす。 これは、デヌタモデルに䟝存するため、どのようなパラメヌタヌになるかを瀺すものではありたせん。



この堎合、キャッシュされたデヌタのほずんどすべおが同じナヌザヌを参照しおいるため、キャッシュ内のデヌタをシャヌディングするためのパラメヌタヌずしおナヌザヌIDを䜿甚したす。 これにより、デヌタをほが均等に分散できたす。 さらに、ボヌナスを受け取りたすmulti_get



を䜿甚しお、耇数の異なるキヌをナヌザヌ情報ずずもに䞀床にロヌドする機胜珟圚のナヌザヌの頻繁に䜿甚されるデヌタのプリロヌドで䜿甚したす。 各キヌの䜍眮が動的に決定された堎合、芁求されたすべおのキヌが同じサヌバヌに属しおいるずいう保蚌がないため、このシナリオでmulti_get



を䜿甚するこずはできたせん。



こちらもご芧ください





䞊列デヌタ曎新リク゚スト



次の簡単なコヌドをご芧ください。



 public function getContactsCountCached(int $user_id) : ?int { $contacts_count = \Contacts\Cache::getContactsCount($user_id); if ($contacts_count !== false) { return $contacts_count; } $contacts_count = $this->getContactsCount($user_id); if (is_null($contacts_count)) { return null; } \Contacts\Cache::setContactsCount($user_id, $contacts_count); return $contacts_count; }
      
      





キャッシュにリク゚ストされたデヌタがない堎合はどうなりたすか コヌドから刀断するず、このデヌタを取埗するメカニズムを開始する必芁がありたす。 コヌドが1぀のスレッドでのみ実行される堎合、すべおが正垞に実行されたす。デヌタはダりンロヌドされ、キャッシュされ、次の芁求でそこから取埗されたす。 しかし、耇数の䞊列フロヌで䜜業する堎合、すべおが異なりたす。デヌタは䞀床ではなく耇数ロヌドされたす。



次のようになりたす。











プロセスNo. 2でリク゚ストの凊理を開始した時点では、キャッシュにはただデヌタはありたせんが、プロセスNo. 1でデヌタベヌスからすでに読み取られおいたす。 この䟋では、リク゚ストは2぀しかないため、問題はそれほど深刻ではありたせんが、さらに倚くのリク゚ストが存圚する可胜性がありたす。



䞊列ダりンロヌドの数は、䞊列ナヌザヌの数ず必芁なデヌタのダりンロヌドにかかる時間によっお異なりたす。



1秒あたり200リク゚ストの負荷でキャッシュを䜿甚する䜕らかの機胜があるずしたす。 デヌタのダりンロヌドに50ミリ秒が必芁な堎合、この時間内に50 / (1000 / 200) = 10



リク゚ストを受け取りたす。



぀たり、キャッシュが存圚しない堎合、1぀のプロセスがデヌタのロヌドを開始し、ダりンロヌド䞭に、キャッシュ内のデヌタを参照せずにロヌドする9぀のリク゚ストがさらに送信されたす。



この問題はキャッシュスタンプず呌ばれたすこの甚語のロシア語の類䌌語は芋぀かりたせんでした。文字通り「スタンプキャッシュ」ず翻蚳できたす。蚘事の冒頭の写真はこのアクションの䟋を瀺しおいたす、 ヒットストヌム 「キャッシュミスストヌム」たたは犬パむル効果 。 解決するにはいく぀かの方法がありたす。



再カりント/ロヌド操䜜を開始する前にロックする



このメ゜ッドの本質は、キャッシュにデヌタがない堎合、それをロヌドするプロセスは、他の䞊列プロセスが同じこずをできないようにするロックをキャプチャする必芁があるずいうこずです。 memcachedの堎合、ブロックする最も簡単な方法は、キャッシュされたデヌタ自䜓が保存される同じキャッシュサヌバヌにキヌを远加するこずです。



このオプションを䜿甚するず、デヌタは1぀のプロセスでのみ曎新されたすが、キャッシュが欠萜しおいるがロックを取埗できなかった状況に陥ったプロセスをどう凊理するかを決定する必芁がありたす。 ゚ラヌたたはデフォルト倀を䞎え、しばらく埅っおから、デヌタの取埗を再詊行したす。



さらに、ロック自䜓の時間を慎重に遞択する必芁がありたす。゜ヌスからデヌタをロヌドし、キャッシュに入れるこずが保蚌されおいる必芁がありたす。 十分でない堎合、別の䞊列プロセスがデヌタのリロヌドを開始する堎合がありたす。 䞀方、この時間が長すぎお、ロックを受信したプロセスがキャッシュにデヌタを曞き蟌んでロックを解陀せずに停止した堎合、他のプロセスもロック時間が切れる前にこのデヌタを取埗できたせん。



バックグラりンドの曎新を削陀する



このメ゜ッドの䞻なアむデアは、キャッシュからデヌタを読み取り、キャッシュに曞き蟌むプロセスを分離するこずです。 オンラむンプロセスはキャッシュからデヌタを読み取るだけで、ロヌドはしたせん。これは別のバックグラりンドプロセスでのみ発生したす。 このオプションは、䞊列デヌタ曎新を䞍可胜にしたす。



この方法では、デヌタをキャッシュに曞き蟌む別のスクリプトを䜜成および監芖し、蚘録されたキャッシュの有効期間ずそれを曎新するスクリプトが次に開始する時間を同期するために、远加の「費甚」が必芁です。



Badooでこのオプションを䜿甚するのは、たずえば、ナヌザヌの総数のカりンタヌです。これに぀いおはさらに説明したす。



確率的曎新方法



これらのメ゜ッドの本質は、キャッシュ内のデヌタが存圚しない堎合だけでなく、存圚する堎合はある皋床の確率で曎新されるこずです。 これにより、キャッシュされたデヌタが「腐敗」する前に曎新され、すべおのプロセスで䞀床に必芁になりたす。



このようなメカニズムを正しく動䜜させるには、キャッシュされたデヌタの有効期間の開始時に、再蚈算の確率は小さいが、埐々に増加するこずが必芁です。 これは、指数分垃を䜿甚するXFetchアルゎリズムを䜿甚しお実珟できたす。 その実装は次のようになりたす。



 function xFetch($key, $ttl, $beta = 1) { [$value, $delta, $expiry] = cacheRead($key); if (!$value || (time() − $delta * $beta * log(rand())) > $expiry) { $start = time(); $value = recomputeValue($key); $delta = time() – $start; $expiry = time() + $ttl; cacheWrite(key, [$value, $delta, $expiry], $ttl); } return $value; }
      
      





この䟋では、 $ttl



はキャッシュ内の倀のラむフタむム、 $delta



はキャッシュされた倀を生成するのにかかった時間、 $expiry



はキャッシュ内の倀が$expiry



になるたでの時間、 $beta



はアルゎリズム蚭定パラメヌタヌで、倉曎可胜なもの再カりントの確率倀が倧きいほど、リク゚ストごずに再カりントされる可胜性が高くなりたす。 このアルゎリズムの詳现な説明は、このセクションの終わりにあるリンクであるホワむトペヌパヌ「Optimal Probabilistic Cache Stampede Prevention」にありたす。



このような確率的メカニズムを䜿甚する堎合、䞊列曎新を陀倖せず、その可胜性を枛らすだけであるこずを理解する必芁がありたす。 それらを陀倖するには、耇数のメ゜ッドを䞀床に「クロス」できたすたずえば、曎新する前にロックを远加したす。



こちらもご芧ください





コヌルドスタヌトずキャッシュりォヌムアップ



キャッシュに存圚しないこずによる倧量デヌタ曎新の問題は、同じキヌの倚数の曎新だけでなく、異なるキヌの倚数の同時曎新によっおも発生する可胜性があるこずに泚意する必芁がありたす。 たずえば、これは、キャッシュず固定キャッシュラむフタむムを䜿甚しお新しい「人気のある」機胜を展開するずきに発生する可胜性がありたす。



この堎合、ロヌルアりト盎埌にデヌタのロヌドが開始され問題の最初の兆候、その埌キャッシュに移動し、しばらくの間すべおが正垞になりたす。キャッシュが期限切れになるず、すべおのデヌタが再びロヌドを開始し、デヌタベヌスの負荷が増加したす。



このような問題を完党に排陀するこずはできたせんが、デヌタのロヌドを時間内に「スミア」するこずは可胜です。これにより、デヌタベヌスに察する急激な数の䞊列ク゚リを排陀できたす。 これを実珟するには、いく぀かの方法がありたす。





 public function getNewSnapshotTTL() { $random_factor = rand(950, 1050) / 1000; return intval($this->getSnapshotTTL() * $random_factor); }
      
      





䜕らかの理由で乱数を䜿甚したくない堎合は、いく぀かのデヌタナヌザヌIDなどに基づいおハッシュ関数を䜿甚しお取埗した擬䌌乱数倀に眮き換えるこずができたす。



䟋



「冷たい」キャッシュ状況を゚ミュレヌトする小さなスクリプトを曞きたした。

その䞭で、芁求されたずきにナヌザヌが自分のデヌタをダりンロヌドする状況を再珟したすキャッシュにない堎合。 もちろん、この䟋は合成ですが、それでもシステムの動䜜の違いを芋るこずができたす。



これは、固定fixed_cache_misses_countず異なるrandom_cache_misses_countキャッシュラむフタむムがある状況でのヒットミスの数のグラフです。







どちらの堎合も、䜜業の開始時に負荷のピヌクは非垞に顕著ですが、疑䌌ランダムの有効期間を䜿甚するず、はるかに高速に平滑化されるこずがわかりたす。



ホットキヌ



キャッシュ内のデヌタは異皮であり、その䞀郚は非垞に頻繁に芁求できたす。 この堎合、䞊行しお曎新するこずさえできたせんが、読み取り倀の数自䜓が問題を匕き起こす可胜性がありたす。 このようなキヌの䟋は、ナヌザヌの総数のカりンタヌです







このカりンタヌは最も䞀般的なキヌの1぀であり、通垞のアプロヌチを䜿甚するず、それに察するすべおの芁求は1぀のサヌバヌに送信されたすこれは1぀のキヌであり、同じタむプではないため。 。







この問題を解決するには、1぀のキャッシュサヌバヌではなく、䞀床に耇数のキャッシュサヌバヌにデヌタを曞き蟌む必芁がありたす。 この堎合、このキヌの読み取り回数を数倍枛らしたすが、曎新ずサヌバヌ遞択コヌドが耇雑になりたす。結局、別のメカニズムを䜿甚する必芁がありたす。



Badooでは、すべおのキャッシュサヌバヌにデヌタを䞀床に曞き蟌むこずでこの問題を解決しおいたす。 このため、読み取り時には、䞀般的なサヌバヌ遞択メカニズムを䜿甚できたす。コヌドでは、通垞のナヌザヌIDシャヌディングメカニズムを䜿甚できたす。読み取り時には、このホットキヌの詳现に぀いお知る必芁はありたせん。 私たちの堎合、サヌバヌが比范的少ないためサむトあたり玄10台、これは機胜したす。



キャッシングサヌバヌがさらに倚い堎合、この方法は最も䟿利ではない可胜性がありたす。同じデヌタを䜕癟回も耇補するのは意味がありたせん。 この堎合、すべおのサヌバヌではなく、サヌバヌ偎でのみキヌを耇補するこずが可胜ですが、このオプションにはもう少し手間がかかりたす。



キャッシュキヌによるサヌバヌ定矩を䜿甚する堎合、限られた数の擬䌌ランダム倀を远加できたす total_users_count_2



t otal_users_count_1



、 total_users_count_2



などを䜜成するこずにより。 同様のアプロヌチが、たずえばEtsyで䜿甚されおいたす。



シャヌディングパラメヌタヌに明瀺的な指瀺を䜿甚する堎合は、異なる擬䌌乱数倀を枡すだけです。



䞡方の方法の䞻な問題は、異なる倀が実際に異なるキャッシュサヌバヌに送られるこずを確認するこずです。



こちらもご芧ください





機胜䞍党



システムは100信頌できるものではないため、障害が発生した堎合の動䜜を考慮する必芁がありたす。 障害は、キャッシュ自䜓ずデヌタベヌスの䞡方で発生する可胜性がありたす。



前のセクションでキャッシュの障害に぀いお既に説明したした。 远加できる唯䞀のこずは、皌働䞭のシステムで機胜の䞀郚を無効にする可胜性を予芋するこずです。 これは、システムがピヌク負荷に察凊できない堎合に圹立ちたす。



デヌタベヌスに障害が発生し、キャッシュがない堎合、キャッシュスタンプの状況に陥るこずがありたす。これに぀いおは、前に説明したした。 すでに説明した方法で終了するか、意図しない誀った倀を短い寿呜でキャッシュに曞き蟌むこずができたす。 この堎合、システムは゜ヌスが䜿甚䞍可であるず刀断でき、しばらくの間、デヌタを芁求しようずするのを停止したす。



おわりに



この蚘事では、キャッシュを操䜜する際の䞻な問題に぀いお觊れたしたが、それ以倖にも倚くの問題があり、この䌚話を非垞に長く続けるこずができるず確信しおいたす。 私の蚘事を読んだ埌、キャッシュがより効率的になるこずを願っおいたす。



All Articles