ASP.NETのキャッシュインフラストラクチャの使用、続き

以前の投稿で、 ASP.NETのキャッシュインフラストラクチャを使用してサイトのパフォーマンスを向上させる方法について説明しました。 数行のコードを追加することで、ホームページのパフォーマンスを5倍向上させることができました。 今回は、さまざまなハックに頼らずに、さらにパフォーマンスを絞り込みましょう。



たとえば、私はまだMvc Music Storeプロジェクトを使用しいます。

以前の投稿を読んでいない場合は、ホームページがどのように高速化されているかを見てみましょう。



すべての最適化はホームページに関するものでしたが、今は内部ページに行きます。



負荷試験



検証のために、次のシナリオで25人の「仮想ユーザー」に対してVisual Studioで負荷テストを行いました。

1)ホームページのリクエスト

2)リクエストページのジャンル(カタログ)

3)アルバムページのリクエスト(製品)

忠実のために、彼は訪問がカタログ\製品の同じページではなく、異なるページにランダム化されるようにしました。

また、新規ユーザーの割合を80%に増やしました。これは事実です。



結果は、1秒あたり42スクリプトです。



キャッシングの追加-簡単なアプローチ



ASP.NETでは、属性を設定し、データベースの依存関係でキャッシュできます。

これを行うには、いくつかの簡単な手順に従う必要があります。

1. web.configにキャッシュオプションを入力します


<system.web> <caching> <sqlCacheDependency enabled="true" pollTime="1000"> <databases> <add name="MusicStore" connectionStringName="MusicStoreEntities" /> </databases> </sqlCacheDependency> <outputCacheSettings> <outputCacheProfiles> <add name="Catalog" sqlDependency="MusicStore:Genres;MusicStore:Albums" duration="86400" location="ServerAndClient" varyByParam="Genre" enabled="true" /> </outputCacheProfiles> </outputCacheSettings> </caching> </system.web>
      
      





sqlCacheDependency



要素は、データベースのキャッシュ依存関係のパラメーターを定義します。 データベース依存関係は、 pollTime



間隔(この場合は1000ミリ秒(1秒))で変更をチェックします。

outputCacheProfiles要素は、異なるアクションに対して同じ設定を繰り返さないようにプロファイルを設定します。 さらに、プロジェクトを再構築せずにキャッシュを管理できます。



2.データベーススキーマを変更して、依存関係が機能するようにします


これを行うには、アプリケーションの起動時に、次のコード行を呼び出します

 String connStr = System.Configuration.ConfigurationManager.ConnectionStrings["MusicStoreEntities"].ConnectionString; System.Web.Caching.SqlCacheDependencyAdmin.EnableNotifications(connStr); System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, "Genres"); System.Web.Caching.SqlCacheDependencyAdmin.EnableTableForNotifications(connStr, "Albums");
      
      







3.属性を追加する


 [OutputCache(CacheProfile = "Catalog")] public ActionResult Browse(string genre) { //... } [OutputCache(CacheProfile = "Catalog")] public ActionResult Details(int id) { //... }
      
      







テストを再度実行します-1秒あたり60スクリプト。 つまり、この場合、速度をほぼ50%上げることができました。



コードから依存関係をインストールする



WebAPIを使用する場合、キャッシュ属性を使用できません。 ただし、この場合、 SqlCacheDependency



クラスが役立ちます。 使い方は非常に簡単です。コンストラクターでweb.configからデータベース名とテーブル名を指定します。 SqlCacheDependencyインスタンスを使用して、ローカルキャッシュ要素の依存関係を指定できます。



したがって、すべてのページをキャッシュすることが有益でない場合は、ナビゲーションキャッシュを実行できます。

 [ChildActionOnly] public ActionResult GenreMenu() { var cacheKey = "Nav"; var genres = this.HttpContext.Cache.Get(cacheKey); if (genres == null) { genres = storeDB.Genres.ToList(); this.HttpContext.Cache.Insert(cacheKey, genres, new SqlCacheDependency("MusicStore","Genres")); } return PartialView(genres); }
      
      







SqlCacheDependency



を受け入れる別のSqlCacheDependency



コンストラクターがあります。 これは、SQL Server Service Brokerからのアラートに基づいた、まったく異なるデータベース変更追跡エンジンです。 これらのアラートを使用しようとしましたが、すべてのリクエストに対して機能しません。 さらに、リクエストが「間違っている」場合、エラーは発生せず、作成後すぐに通知が届きます。 さらに、アラートは非常に遅くなります。 私の測定によると、テーブルへの書き込みが8倍遅くなります。



コインの裏側



データベースの依存関係はまったく解放されておらず、その作業のために、レコードの作成、変更、削除をトリガーするトリガーが作成されます。 これらのトリガーは、サービステーブル内の、どのテーブルがいつ変更されたかに関する情報を更新します。 そして、アプリケーション側のスレッドは定期的にテーブルを読み取り、依存関係を通知します。



変更の量が頻繁に発生しない場合、トリガーのオーバーヘッドはわずかです。 また、頻繁に変更が発生すると、キャッシュの効率が低下します。 Mvc Music Storeの例では、アルバムを変更すると、カタログ全体のキャッシュ全体がリセットされます。



どうする?



同じサーバー内にとどまる場合、Mvc Music Storeでバスケットをキャッシュするために使用されるのと同じアプローチが使用されます-個々の要素またはデータサンプルをキャッシュに保存し、記録するときにキャッシュをリセットします( 以前の投稿で詳しく説明します)。 キャッシュの粒度とフラッシュの適切な選択により、高いキャッシュ効率を達成できます。



しかし、複数のサーバーに拡張する場合、このアプローチはほとんど常に機能しません。 バスケットの場合、同じクライアントが同じサーバーに到達するときに、クライアントアフィニティの場合にのみ機能します。 最新のNLBはこれを提供しますが、たとえば商品をキャッシュする場合、クライアントアフィニティはもはや役に立ちません。



分散キャッシュが役立ちます。


要求を処理するために複数のWebサーバーを既にインストールしている場合は、分散キャッシュについて検討する必要があります。

今日の最適なオプションの1つはRedisです。 オンプレミスとMicrosoft Azureクラウドの両方で利用できます。



RedisをASP.NETプロジェクトに追加するには、パッケージマネージャーコンソールを開き、いくつかのコマンドを実行します

 Install-Package Redis-64 Install-Package StackExchange.Redis
      
      







Redisは、優れた機能、いわゆるキースペース通知(http://redis.io/topics/notifications)をサポートしています。 これにより、別のサーバーで変更が発生した場合でも、アイテムがいつ変更されたかを追跡できます。



この機能をASP.NETに統合するために、小さなクラスを作成しました。

 class RedisCacheDependency: CacheDependency { public RedisCacheDependency(string key):base() { Redis.Client.GetSubscriber().Subscribe("__keyspace@0__:" + key, (c, v) => { this.NotifyDependencyChanged(new object(), EventArgs.Empty ); }); } }
      
      





このクラスは、RedisのCacheDependencyを実装します。



そして今、クライアント自体:

 public static class Redis { public static readonly ConnectionMultiplexer Client = ConnectionMultiplexer.Connect("localhost"); public static CacheDependency CreateDependency(string key) { return new RedisCacheDependency(key); } public static T GetCached<T>(string key, Func<T> getter) where T:class { var localCache = HttpRuntime.Cache; var result = (T) localCache.Get(key); if (result != null) return result; var redisDb = Client.GetDatabase(); var value = redisDb.StringGet(key); if (!value.IsNullOrEmpty) { result = Json.Decode<T>(value); localCache.Insert(key, result, CreateDependency(key)); return result; } result = getter(); redisDb.StringSet(key, Json.Encode(result)); localCache.Insert(key, result, CreateDependency(key)); return result; } public static void DeleteKey(string key) { HttpRuntime.Cache.Remove(key); var redisDb = Client.GetDatabase(); redisDb.KeyDelete(key); } }
      
      





GetCachedメソッドは、結果をローカルASP.NETキャッシュに保存します。 ローカルキャッシュは非常に高速で、キャッシュ内のアイテムのチェックにはナノ秒かかります。 これは、Redis + serialization-deserializationへのリモート要求よりもはるかに高速です。



これで、Redisキャッシュ内のアイテムをページキャッシュにバインドできます。

 public ActionResult Browse(string genre) { var cacheKey = "catalog-" + genre; var genreModel = Redis.GetCached(cacheKey, () => (from g in storeDB.Genres where g.Name == genre select new GenreBrowse { Name = g.Name, Albums = from a in g.Albums select new AlbumSummary { Title = a.Title, AlbumId = a.AlbumId, AlbumArtUrl = a.AlbumArtUrl } } ).Single() ); this.Response.AddCacheItemDependency(cacheKey); this.Response.Cache.SetLastModifiedFromFileDependencies(); this.Response.Cache.AppendCacheExtension("max-age=0"); this.Response.Cache.VaryByParams["genre"] = true; this.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate); return View(genreModel); }
      
      





標準のOutputCache属性は削除する必要があります。削除しないと、依存関係に応答しません。 必要に応じて、コードをコピーして貼り付けないように、キャッシュ用にActionFilterを作成できます。



キャッシュをリセットするには、データを変更するメソッドでRedis.DeleteKeyを呼び出す必要があります。



2番目の負荷テストでは、1秒あたり52個のスクリプトが生成されました。 これはRedisを使用しない場合よりも少なくなりますが、テーブル内のレコード数が増えてもパフォーマンスが著しく低下することはありません。



Redisで他に何ができますか?


キャッシュにデータを手動で配置することに加えて、NuGetパッケージMicrosoft.Web.RedisSessionStateProviderおよびMicrosoft.Web.RedisOutputCacheProviderを使用して、セッション状態とページキャッシュをRedisに配置できます。 残念ながら、カスタムOutputCacheProviderはCacheDependencyの使用を制限して出力キャッシュをフラッシュします。



おわりに



ASP.NETには多くのキャッシュ機能があり、この一連の投稿でレビューした機能に加えて、ファイルとディレクトリにリンクするキャッシュ検証コールバックもあります。 しかし、まだ話していない落とし穴があります。 ASP.NETでのWebアプリケーションの最適化に関連するすべてに興味がある場合は、私のセミナー-gandjustas.timepad.ru/event/150915にアクセスしてください。



シリーズのすべての投稿




ソースコードとテストはGitHubで入手できます-github.com/gandjustas/McvMusicStoreCache



All Articles