Önbelleğe Alma ile Web Çiftliğinde ASP.NET Uygulama Performansı Nasıl İyileştirilir
Yayınlanan: 2022-03-11Bilgisayar Biliminde sadece iki zor şey vardır: önbellek geçersiz kılma ve bir şeyleri adlandırma.
- Yazar: Phil Karlton
Önbelleğe Almaya Kısa Bir Giriş
Önbelleğe alma, basit bir numarayla performansı artırmak için güçlü bir tekniktir: Bir sonuca her ihtiyacımız olduğunda pahalı işler (karmaşık bir hesaplama veya karmaşık veritabanı sorgusu gibi) yapmak yerine, sistem bu çalışmanın sonucunu depolayabilir veya önbelleğe alabilir ve basitçe bir dahaki sefer istendiğinde, bu işi yeniden gerçekleştirmeye gerek kalmadan sağlayın (ve bu nedenle, çok daha hızlı yanıt verebilir).
Tabii ki, önbelleğe almanın ardındaki tüm fikir, yalnızca önbelleğe aldığımız sonuç geçerli kaldığı sürece çalışır. Ve burada sorunun asıl zor kısmına geliyoruz: Önbelleğe alınmış bir öğenin geçersiz hale geldiğini ve yeniden oluşturulması gerektiğini nasıl belirleriz?
ve dağıtılmış web çiftliği önbelleğe alma sorununu çözmek için mükemmel.
Genellikle, tipik bir web uygulaması, yazma isteklerinden çok daha yüksek hacimli okuma istekleriyle uğraşmak zorundadır. Bu nedenle, yüksek bir yükü işlemek üzere tasarlanmış tipik bir web uygulaması, ölçeklenebilir ve dağıtılabilir olacak şekilde tasarlanmıştır ve genellikle bir çiftlik olarak adlandırılan bir dizi web katmanı düğümü olarak dağıtılır. Tüm bu gerçeklerin önbelleğe almanın uygulanabilirliği üzerinde etkisi vardır.
Bu makalede, yüksek bir yükün üstesinden gelmek için tasarlanmış web uygulamalarının yüksek verim ve performansının sağlanmasında önbelleğe almanın oynayabileceği role odaklanacağız ve projelerimden birindeki deneyimi kullanacağım ve ASP.NET tabanlı bir çözüm sağlayacağım. bir örnek olarak.
Yüksek Yük Taşıma Problemi
Çözmem gereken asıl problem orijinal değildi. Benim görevim, bir ASP.NET MVC monolitik web uygulaması prototipini yüksek bir yükü kaldırabilecek hale getirmekti.
Monolitik bir web uygulamasının çıktı kapasitesini geliştirmeye yönelik gerekli adımlar şunlardır:
- Web uygulamasının birden çok kopyasını bir yük dengeleyicinin arkasında paralel olarak çalıştırmasını ve tüm eşzamanlı istekleri etkin bir şekilde sunmasını (yani, ölçeklenebilir hale getirmesini) etkinleştirin.
- Mevcut performans darboğazlarını ortaya çıkarmak ve bunları optimize etmek için uygulamanın profilini çıkarın.
- Genellikle genel uygulama yükünün önemli bir bölümünü oluşturduğundan, okuma isteği verimini artırmak için önbelleğe almayı kullanın.
Önbelleğe alma stratejileri, önbelleğe alınan değerleri depolamak için genellikle Memcached veya Redis gibi bazı ara yazılım önbelleğe alma sunucularının kullanılmasını içerir. Yüksek oranda benimsenmelerine ve kanıtlanmış uygulanabilirliklerine rağmen, bu yaklaşımların aşağıdakiler de dahil olmak üzere bazı olumsuz yönleri vardır:
- Ayrı önbellek sunucularına erişimle sağlanan ağ gecikmeleri, veritabanının kendisine erişme gecikmeleriyle karşılaştırılabilir.
- Web katmanının veri yapıları, kullanıma hazır serileştirme ve seri durumdan çıkarma için uygun olmayabilir. Önbellek sunucularını kullanmak için bu veri yapılarının, sürekli ek geliştirme çabası gerektiren serileştirmeyi ve seri durumdan çıkarmayı desteklemesi gerekir.
- Serileştirme ve seri durumdan çıkarma, performans üzerinde olumsuz bir etkiyle çalışma zamanı ek yükü ekler.
Tüm bu konular benim durumumda alakalıydı, bu yüzden alternatif seçenekleri araştırmak zorunda kaldım.
Yerleşik ASP.NET bellek içi önbelleği ( System.Web.Caching.Cache
) son derece hızlıdır ve hem geliştirme sırasında hem de çalışma zamanında serileştirme ve seriyi kaldırma ek yükü olmadan kullanılabilir. Ancak, ASP.NET bellek içi önbelleğin de kendi dezavantajları vardır:
- Her web katmanı düğümü, önbelleğe alınmış değerlerin kendi kopyasına ihtiyaç duyar. Bu, düğüm soğuk başlatma veya geri dönüşüm sırasında daha yüksek veritabanı katmanı tüketimine neden olabilir.
- Her web katmanı düğümü, başka bir düğüm, güncellenmiş değerler yazarak önbelleğin herhangi bir bölümünü geçersiz kıldığında bilgilendirilmelidir. Önbellek dağıtıldığı ve uygun senkronizasyon olmadığı için, düğümlerin çoğu tipik olarak kabul edilemez olan eski değerleri döndürür.
Ek veritabanı katmanı yükü kendi başına bir darboğaza yol açmazsa, düzgün dağıtılmış bir önbellek uygulamak, ele alınması kolay bir görev gibi görünüyor, değil mi? Pekala, bu kolay bir iş değil, ama mümkün . Benim durumumda, kıyaslamalar, çalışmaların çoğu web katmanında gerçekleştiğinden, veritabanı katmanının bir sorun olmaması gerektiğini gösterdi. Bu yüzden, ASP.NET bellek içi önbelleği kullanmaya ve uygun senkronizasyonu uygulamaya odaklanmaya karar verdim.
ASP.NET Tabanlı Çözümün Tanıtılması
Açıklandığı gibi, çözümüm özel önbelleğe alma sunucusu yerine ASP.NET bellek içi önbelleği kullanmaktı. Bu, web çiftliğinin her düğümünün kendi önbelleğine sahip olmasını, veritabanını doğrudan sorgulamasını, gerekli hesaplamaları yapmasını ve sonuçları bir önbellekte saklamasını gerektirir. Bu şekilde, önbelleğin bellek içi yapısı sayesinde tüm önbellek işlemleri çok hızlı olacaktır. Tipik olarak, önbelleğe alınmış öğelerin net bir ömrü vardır ve bazı değişiklikler veya yeni verilerin yazılmasıyla eski hale gelir. Bu nedenle, web uygulaması mantığından, önbellek öğesinin ne zaman geçersiz kılınması gerektiği genellikle açıktır.
Burada kalan tek sorun, düğümlerden biri kendi önbelleğindeki bir önbellek öğesini geçersiz kıldığında, başka hiçbir düğümün bu güncellemeyi bilmeyecek olmasıdır. Bu nedenle, diğer düğümler tarafından hizmet verilen sonraki istekler, eski sonuçlar verecektir. Bunu ele almak için her düğüm, önbellek geçersiz kılmalarını diğer düğümlerle paylaşmalıdır. Böyle bir geçersiz kılma aldıktan sonra, diğer düğümler önbelleğe alınmış değerlerini bırakabilir ve bir sonraki istekte yeni bir tane alabilir.
Burada Redis devreye girebilir. Diğer çözümlere kıyasla Redis'in gücü, Pub/Sub özelliklerinden gelir. Bir Redis sunucusunun her istemcisi bir kanal oluşturabilir ve bu kanalda bazı veriler yayınlayabilir. Herhangi bir başka istemci, olaya dayalı herhangi bir sisteme çok benzer şekilde, o kanalı dinleyebilir ve ilgili verileri alabilir. Bu işlev, düğümler arasında önbellek geçersiz kılma mesajlarını değiş tokuş etmek için kullanılabilir, böylece tüm düğümler gerektiğinde önbelleklerini geçersiz kılabilir.
ASP.NET'in bellek içi önbelleği bazı yönlerden basit, bazı yönlerden karmaşıktır. Özellikle, anahtar/değer çiftlerinin bir haritası olarak çalıştığı için basittir, ancak geçersiz kılma stratejileri ve bağımlılıklarıyla ilgili çok fazla karmaşıklık vardır.
Neyse ki, tipik kullanım durumları yeterince basittir ve tüm öğeler için varsayılan bir geçersiz kılma stratejisi kullanmak mümkündür, bu da her bir önbellek öğesinin en fazla yalnızca tek bir bağımlılığa sahip olmasını sağlar. Benim durumumda, önbelleğe alma hizmetinin arayüzü için aşağıdaki ASP.NET koduyla bitirdim. (Basitlik ve özel lisans uğruna bazı ayrıntıları atladığım için bunun gerçek kod olmadığını unutmayın.)
public interface ICacheKey { string Value { get; } } public interface IDataCacheKey : ICacheKey { } public interface ITouchableCacheKey : ICacheKey { } public interface ICacheService { int ItemsCount { get; } T Get<T>(IDataCacheKey key, Func<T> valueGetter); T Get<T>(IDataCacheKey key, Func<T> valueGetter, ICacheKey dependencyKey); }
Burada önbellek hizmeti temelde iki şeye izin verir. İlk olarak, bazı değer alıcı işlevlerinin sonucunun iş parçacığı güvenli bir şekilde saklanmasını sağlar. İkincisi, o zaman geçerli olan değerin istendiğinde her zaman döndürülmesini sağlar. Önbellek öğesi eskidiğinde veya önbellekten açıkça çıkarıldığında, geçerli bir değeri almak için değer alıcı yeniden çağrılır. Önbellek anahtarı, esas olarak uygulamanın her yerinde önbellek anahtarı dizelerinin sabit kodlanmasını önlemek için ICacheKey
arabirimi tarafından soyutlandı.

Önbellek öğelerini geçersiz kılmak için şuna benzeyen ayrı bir hizmet sundum:
public interface ICacheInvalidator { bool IsSessionOpen { get; } void OpenSession(); void CloseSession(); void Drop(IDataCacheKey key); void Touch(ITouchableCacheKey key); void Purge(); }
Yalnızca bağımlı veri öğelerine sahip olan verili öğeleri bırakma ve tuşlara dokunma temel yöntemlerinin yanı sıra, bir tür “oturum” ile ilgili birkaç yöntem vardır.
Web uygulamamız, bağımlılık yönetimi için kontrolün tersine çevrilmesi (IoC) tasarım modelinin bir uygulaması olan bağımlılık enjeksiyonu için Autofac'ı kullandı. Bu özellik, geliştiricilerin bağımlılıklar konusunda endişelenmelerine gerek kalmadan sınıflarını oluşturmalarına olanak tanır, çünkü IoC kapsayıcısı onlar için bu yükü yönetir.
Önbellek hizmeti ve önbellek geçersiz kılıcı, IoC ile ilgili olarak büyük ölçüde farklı yaşam döngülerine sahiptir. Önbellek hizmeti tekil (tüm istemciler arasında paylaşılan bir örnek) olarak kaydedilirken, önbellek geçersiz kılıcı istek başına bir örnek olarak kaydedildi (gelen her istek için ayrı bir örnek oluşturuldu). Niye ya?
Cevap, ele almamız gereken ek bir incelikle ilgili. Web uygulaması, esas olarak UI ve mantık endişelerinin ayrılmasına yardımcı olan bir Model-View-Controller (MVC) mimarisi kullanıyor. Bu nedenle, tipik bir denetleyici eylemi, ActionFilterAttribute
öğesinin bir alt sınıfına sarılır. ASP.NET MVC çerçevesinde, bu tür C# öznitelikleri, denetleyicinin eylem mantığını bir şekilde süslemek için kullanılır. Bu özel öznitelik, yeni bir veritabanı bağlantısı açmaktan ve eylemin başında bir işlem başlatmaktan sorumluydu. Ayrıca, eylemin sonunda, işlemin başarılı olması durumunda işlemin gerçekleştirilmesinden ve başarısızlık durumunda geri alınmasından filtre özniteliği alt sınıfı sorumluydu.
Önbellek geçersiz kılma işlemin tam ortasında gerçekleştiyse, o düğüme yapılan bir sonraki isteğin eski (hala diğer işlemler tarafından görülebilir) değeri başarıyla önbelleğe geri koyacağı bir yarış durumu olabilir. Bunu önlemek için, tüm geçersiz kılmalar işlem tamamlanana kadar ertelenir. Bundan sonra, önbellek öğelerinin çıkarılması güvenlidir ve bir işlem hatası durumunda, önbellek değişikliğine hiç gerek yoktur.
Önbellek geçersiz kılıcıdaki "oturum" ile ilgili bölümlerin tam amacı buydu. Ayrıca, ömrünün amacı talebe bağlı olmaktır. ASP.NET kodu şöyle görünüyordu:
class HybridCacheInvalidator : ICacheInvalidator { ... public void Drop(IDataCacheKey key) { if (key == null) throw new ArgumentNullException("key"); if (!IsSessionOpen) throw new InvalidOperationException("Session must be opened first."); _postponedRedisMessages.Add(new Tuple<string, string>("drop", key.Value)); } ... public void CloseSession() { if (!IsSessionOpen) return; _postponedRedisMessages.ForEach(m => PublishRedisMessageSafe(m.Item1, m.Item2)); _postponedRedisMessages = null; } ... }
Buradaki PublishRedisMessageSafe
yöntemi, mesajın (ikinci argüman) belirli bir kanala (ilk argüman) gönderilmesinden sorumludur. Aslında, bırakma ve dokunma için ayrı kanallar vardır, bu nedenle her birinin mesaj işleyicisi tam olarak ne yapacağını biliyordu - alınan mesaj yüküne eşit tuşa bırak/dokun.
Zor kısımlardan biri, Redis sunucusuna olan bağlantıyı düzgün bir şekilde yönetmekti. Sunucunun herhangi bir nedenle çökmesi durumunda uygulama düzgün şekilde çalışmaya devam etmelidir. Redis tekrar çevrimiçi olduğunda, uygulama sorunsuz bir şekilde yeniden kullanmaya başlamalı ve diğer düğümlerle tekrar mesaj alışverişinde bulunmalıdır. Bunu başarmak için StackExchange.Redis kitaplığını kullandım ve ortaya çıkan bağlantı yönetimi mantığı şu şekilde uygulandı:
class HybridCacheService : ... { ... public void Initialize() { try { Multiplexer = ConnectionMultiplexer.Connect(_configService.Caching.BackendServerAddress); ... Multiplexer.ConnectionFailed += (sender, args) => UpdateConnectedState(); Multiplexer.ConnectionRestored += (sender, args) => UpdateConnectedState(); ... } catch (Exception ex) { ... } } private void UpdateConnectedState() { if (Multiplexer.IsConnected && _currentCacheService is NoCacheServiceStub) { _inProcCacheInvalidator.Purge(); _currentCacheService = _inProcCacheService; _logger.Debug("Connection to remote Redis server restored, switched to in-proc mode."); } else if (!Multiplexer.IsConnected && _currentCacheService is InProcCacheService) { _currentCacheService = _noCacheStub; _logger.Debug("Connection to remote Redis server lost, switched to no-cache mode."); } } }
Burada, ConnectionMultiplexer
, temel alınan Redis ile şeffaf çalışmadan sorumlu olan StackExchange.Redis kitaplığından bir türdür. Buradaki önemli kısım, belirli bir düğümün Redis ile bağlantısını kaybettiğinde, hiçbir isteğin eski verileri almamasını sağlamak için önbellek yok moduna geri dönmesidir. Bağlantı geri yüklendikten sonra düğüm yeniden bellek içi önbelleği kullanmaya başlar.
Önbellek hizmeti ( SomeActionWithoutCaching
) kullanılmadan yapılan eylem örnekleri ve onu kullanan özdeş bir işlem ( SomeActionUsingCache
):
class SomeController : Controller { public ISomeService SomeService { get; set; } public ICacheService CacheService { get; set; } ... public ActionResult SomeActionWithoutCaching() { return View( SomeService.GetModelData() ); } ... public ActionResult SomeActionUsingCache() { return View( CacheService.Get( /* Cache key creation omitted */, () => SomeService.GetModelData() ); ); } }
Bir ISomeService
uygulamasından bir kod parçacığı şöyle görünebilir:
class DefaultSomeService : ISomeService { public ICacheInvalidator _cacheInvalidator; ... public SomeModel GetModelData() { return /* Do something to get model data. */; } ... public void SetModelData(SomeModel model) { /* Do something to set model data. */ _cacheInvalidator.Drop(/* Cache key creation omitted */); } }
Kıyaslama ve Sonuçlar
Önbelleğe alma ASP.NET kodunun tamamı ayarlandıktan sonra, onu mevcut web uygulaması mantığında kullanma zamanı gelmiştir ve önbelleğe almayı kullanmak için kodu yeniden yazma çabalarının çoğunu nereye harcayacağınıza karar vermek için kıyaslama kullanışlı olabilir. Kıyaslama yapılacak operasyonel olarak en yaygın veya kritik birkaç kullanım örneğini seçmek çok önemlidir. Bundan sonra, Apache jMeter gibi bir araç iki şey için kullanılabilir:
- Bu anahtar kullanım durumlarını HTTP istekleri aracılığıyla kıyaslamak için.
- Test edilen web düğümü için yüksek yükü simüle etmek.
Bir performans profili elde etmek için, IIS çalışan işlemine eklenebilen herhangi bir profil oluşturucu kullanılabilir. Benim durumumda JetBrains dotTrace Performance kullandım. Doğru jMeter parametrelerini (eşzamanlılık ve istek sayısı gibi) belirlemek için deney yapmak için harcanan bir sürenin ardından, etkin noktaları ve darboğazları belirlemede çok yardımcı olan performans anlık görüntülerini toplamaya başlamak mümkün hale gelir.
Benim durumumda, bazı kullanım durumları, veritabanı okumalarında bariz darboğazlarla yaklaşık %15-%45 toplam kod yürütme süresinin harcandığını gösterdi. Önbelleğe alma uyguladıktan sonra, çoğu için performans neredeyse iki katına çıktı (yani iki kat daha hızlıydı).
Çözüm
Görebileceğiniz gibi, benim durumum genellikle “tekerleği yeniden icat etmek” olarak adlandırılan şeye bir örnek gibi görünebilir: Zaten yaygın olarak uygulanan en iyi uygulamalar varken neden yeni bir şey yaratmaya çalışmakla uğraşalım ki? Sadece bir Memcached veya Redis kurun ve bırakın.
En iyi uygulamaların kullanılmasının genellikle en iyi seçenek olduğuna kesinlikle katılıyorum. Ancak herhangi bir en iyi uygulamayı körü körüne uygulamadan önce, kişinin kendine şu soruyu sorması gerekir: Bu “en iyi uygulama” ne kadar uygulanabilir? Benim durumuma iyi uyuyor mu?
Bana göre, önemli bir karar verirken uygun seçenekler ve ödünleşim analizi bir zorunluluktur ve sorun o kadar kolay olmadığı için seçtiğim yaklaşım buydu. Benim durumumda, dikkate alınması gereken birçok faktör vardı ve eldeki sorun için doğru yaklaşım olmayabileceğinden, herkese uyan tek bir çözüm almak istemedim.
Sonunda, uygun önbelleğe alma işlemiyle, ilk çözüme göre neredeyse %50 performans artışı elde ettim.