多くの人々は、キャッシュ、さらには分散キャッシュでさえ、すべてをより速く、よりクールにする簡単な方法だと信じています。 しかし、実践が示すように、キャッシュの不適切な使用は、常にではないにしても、しばしば事態を悪化させます。
カットの下では、キャッシュが誤ってねじ込まれたためにパフォーマンスが低下したことについて、いくつかのストーリーがあります。
- この記事で説明するイベントはすべて架空のものであり、実際の展開との偶然の一致はランダムです。
- この記事で説明されているApache Igniteロジックは意図的に簡略化されており、重要な詳細は省略されています。
- 公式のドキュメントを慎重に読むことなく、本番環境でこれを繰り返さないでください!
- 説明されている設計パターンのいくつかはアンチパターンです。実装前にこれを覚えておいてください:)
私のデータベースは遅くなり、あなたのキャッシュでさらに遅くなります:(
平均的なWebサービスはどのようなものですか?
⟷
データベースが対処を停止したら、何をしますか?
そう! 新しい鉄を買う!
いつそれが役に立たないのですか?
そう! キャッシュを設定し、できれば分散してください!
⟷ ⟷
速くなりますか? 事実ではありません!
なんで? 信じられないほどの並行性を達成し、同時に数千の要求を受け入れることができるとしましょう。 しかし、それらはすべて、1つのキー-最初のページからのホットオファー-に従って提供されます。
さらに、すべてのスレッドは同じロジックに従います:「キャッシュに値がありません。データベースにアクセスします」。
結果として何が起こりますか? 各スレッドはデータベースにアクセスし、キャッシュ内の値を更新します。 その結果、システムは、キャッシュが原則的になかった場合よりも多くの時間を費やします。
そして、問題の解決策は非常に簡単です-同じキーに対するリクエストの同期。
Apache Igniteは、同期をサポートするSpring Cachingを介した単純なキャッシュを提供します。
@Cacheable("dynamicCache") public String cacheable(Integer key) { // :( return longOp(key); } @Cacheable(value = "dynamicCache", sync = true) public String cacheableSync(Integer key) { // , , // , , // , longOp(key) return longOp(key); }
バージョン2.1で Apache Ignite に同期機能が追加されました。
早く! しかし、まだゆっくり:(
このストーリーは、前のストーリーの直接の続きであり、キャッシュ開発者も常に正しく使用するとは限らないことを示すことを目的としています。
そのため、キャッシュ同期を追加する修正は実稼働環境で行われることが判明しました...助けにはなりませんでした。
最初のページからホットグッズを急いだ人はいませんでした。また、1000のフローすべてが同時に異なるキーに送られ、同期メカニズムがボトルネックになりました。
以前の記事で、 Apache Igniteで同期ツールがどのように機能するかについて前に話しました。
すべての同期ツールに関する情報は、以前にMap<String, DataStructureInfo>
形式のDATA_STRUCTURES_KEY
キーでignite-sys-cache
に保存され、シンクロナイザーの各追加は次のようになりました。
// lock(cache, DATA_STRUCTURES_KEY); // Map<String, DataStructureInfo> map = cache.get(DATA_STRUCTURES_KEY); map.put("Lock785", dsInfo); cache.put(DATA_STRUCTURES_KEY, map); // unlock(cache, DATA_STRUCTURES_KEY)
合計すると、必要なシンクロナイザーを作成するときに、すべてのスレッドが同じキーで値を変更しようとしました。
再び速い! しかし、それは信頼できますか?
Apache Igniteはバージョン2.1で「最も重要な」キーを取り除き、シンクロナイザーに関する情報を個別に保存し始めました。 ユーザーは実際のシナリオで9000%以上を獲得しました。
そして、ライトが点滅し、途切れない電源に障害が発生し、ユーザーが加熱されたキャッシュを失うまで、すべてが正常でした。
バージョン2.1以降、 Apache IgniteにはPersistenceの独自の実装があります。
ただし、それ自体では、永続性は再起動中に一貫した状態を保証しません。 データは定期的に同期(ディスクにフラッシュ)され、リアルタイムではありません。 Persistenceの主な目的は、1台のコンピューターでメモリに収まるよりも多くのデータを格納し、効率的に処理する機能です。
一貫性の保証は、 先行書き込みロギングアプローチを使用して実現されます。 簡単に言えば、データは最初に論理演算としてディスクに書き込まれ、次に分散キャッシュに既に保存されています。
ディスク上のサーバーを再起動すると、ある時点で最新の状態になります。 WALをこの状態にロールするだけで十分であり、システムは再び効率的で一貫しています(フルACID)。 回復には数秒、最悪の場合は数分かかりますが、現在のリクエストでキャッシュをウォームアップするのに必要な時間や日数はかかりません。
信頼できるようになりました! 速いですか?
永続性はシステムの速度を低下させます(読み取りではなく書き込み)。 WALを有効にすると、システムの速度がさらに低下します。これは信頼性の代価です。
さまざまな保証を提供するいくつかのレベルのロギングがあります。
-デフォルト-任意の負荷レベルでのデータストレージの完全な保証
-LOG_ONLY-オペレーティングシステムに障害が発生した場合を除き、完全保証
-背景-保証はありませんが、Apache Igniteは次のことを試みます。
-NONE-操作ログは保持されません。
加重平均システムでのDEFAULTとNONEの速度の差は10倍に達します。
状況に戻りましょう。 NONEの3倍遅いBackgroundモードを選択し、ウォームキャッシュを失うことを恐れていないと仮定します(クラッシュの数分前から操作を失うことができますが、それ以上はできません)。
このモードでは、数か月間働き、すべてが起こり、クラッシュ後にシステムを簡単に復元しました。 周りの誰もが幸せで幸せです。
1つの「しかし」ではなかった場合、12月20日の販売のピーク時に、サーバーの負荷は80%であり、負荷がかかってクラッシュしようとしていることがわかりました。
WALをオフにすると(NONEに変換すると)3倍の負荷が軽減されますが、そのためには、Apache Igniteクラスター全体を再起動し、何かが発生した場合にそのようなクラスターで復旧する必要があります-「 高速で信頼性の低い 」項目に戻りますか?
バージョン2.4以降、 Apache Igniteにはクラスターを再起動せずにWALを無効にし、すべての保証を復元して有効にする機能があります。
SQL
// ALTER TABLE tableName NOLOGGING // ALTER TABLE tableName LOGGING
Java
// ignite.cluster().isWalEnabled(cacheName); // ignite.cluster().disableWal(cacheName); // ignite.cluster().enableWal(cacheName);
今すぐに迅速かつ確実に、しかしそれなら-オプションがあります...
さて、ロギングを無効にする必要がある場合(保証のために、主なことは負荷に耐えることです!)、WALを一時的に無効にして、いつでもオンにすることができます。
また、システムの開始時に膨大な履歴データをすばやくダウンロードします。
システム全体ではなく、特定のキャッシュのみのフレームワーク内で切断が可能であることにも言及する価値があります。 この場合、シャットダウンが実行されなかったキャッシュ内のデータは影響を受けません。
この場合、WALをオンにした後、システムは選択されたWALモードに従って動作を保証します。
キャッシュは、分散されていても万能薬ではありません!
どんなに高度で高度な技術であっても、単一の技術ではすべての問題を解決することはできません。 不適切に使用された技術はそれを悪化させる可能性が高く、正しく使用されてもすべてのギャップを埋めることはできません。
分散を含むキャッシュは、適切な使用と思慮深い構成でのみ高速化を提供するメカニズムです。 プロジェクトに導入する前にこれを覚えておいて、すべての場合に関連する前後に測定を行ってください...そしてパフォーマンスがあなたと一緒になるかもしれません!