Önbelleksizlik kötü modeliNo Caching antipattern

Çok sayıda eş zamanlı istek işleyen bir bulut uygulamasında aynı verilerin tekrar tekrar getirilmesi, performansı ve ölçeklenebilirliği düşürebilir.In a cloud application that handles many concurrent requests, repeatedly fetching the same data can reduce performance and scalability.

Sorun açıklamasıProblem description

Veriler önbelleğe alınmazsa aşağıdakiler gibi istenmeyen bazı davranışlar ortaya çıkabilir:When data is not cached, it can cause a number of undesirable behaviors, including:

  • G/Ç ek yükü veya gecikme süresi açısından erişilmesi pahalı bir kaynaktan aynı bilgileri tekrar tekrar getirme.Repeatedly fetching the same information from a resource that is expensive to access, in terms of I/O overhead or latency.
  • Birden çok istek için tekrar tekrar aynı nesneleri veya veri yapılarını oluşturma.Repeatedly constructing the same objects or data structures for multiple requests.
  • Hizmet kotası olan ve belirli bir sınırı aşan istemcileri kısıtlayan bir uzak hizmete aşırı sayıda çağrı yapma.Making excessive calls to a remote service that has a service quota and throttles clients past a certain limit.

Bu sorunlar uzun yanıt sürelerine, veri deposunda çekişmenin artmasına ve ölçeklenebilirliğin düşmesine yol açabilir.In turn, these problems can lead to poor response times, increased contention in the data store, and poor scalability.

Aşağıdaki örnekte, Entity Framework kullanarak bir veritabanına bağlanılmaktadır.The following example uses Entity Framework to connect to a database. Birden çok istek tam olarak aynı verileri getiriyor olsa bile her istemci isteği için veritabanına bir çağrı yapılır.Every client request results in a call to the database, even if multiple requests are fetching exactly the same data. Tekrarlanan isteklerin G/Ç ek yükü ve veri erişimi ücretleri açısından maliyeti birikerek hızla artabilir.The cost of repeated requests, in terms of I/O overhead and data access charges, can accumulate quickly.

public class PersonRepository : IPersonRepository
{
    public async Task<Person> GetAsync(int id)
    {
        using (var context = new AdventureWorksContext())
        {
            return await context.People
                .Where(p => p.Id == id)
                .FirstOrDefaultAsync()
                .ConfigureAwait(false);
        }
    }
}

Örneğin tamamını burada bulabilirsiniz.You can find the complete sample here.

Bu kötü model genellikle aşağıdaki nedenlerden ortaya çıkar:This antipattern typically occurs because:

  • Önbellek kullanılmamasını uygulamak daha kolaydır ve yükler düşük olduğunda sorunsuz çalışır.Not using a cache is simpler to implement, and it works fine under low loads. Önbelleğe alma kodu daha karmaşık hale getirir.Caching makes the code more complicated.
  • Önbellek kullanmanın avantajları ve dezavantajları net olarak anlaşılmamıştır.The benefits and drawbacks of using a cache are not clearly understood.
  • Önbelleğe alınan verilerin doğruluk ve güncelliklerini korumaktan doğacak ek yük konusunda endişeler vardır.There is concern about the overhead of maintaining the accuracy and freshness of cached data.
  • Uygulama, ağdaki gecikme sürelerinin sorun olmadığı ve performansı yüksek, pahalı donanımlar üzerinde çalıştırılan şirket içi bir sistemden geçirilmiştir. Bu nedenle, önbelleğe alma özgün tasarımda dikkate alınmamıştır.An application was migrated from an on-premises system, where network latency was not an issue, and the system ran on expensive high-performance hardware, so caching wasn't considered in the original design.
  • Geliştiriciler, belli bir senaryoda önbelleğe alma olanaklarının bulunduğunun farkında değildir.Developers aren't aware that caching is a possibility in a given scenario. Örneğin, geliştiriciler bir web API'si uygularken ETag'leri kullanmayı akıl edemeyebilir.For example, developers may not think of using ETags when implementing a web API.

Sorunun çözümüHow to fix the problem

En popüler önbelleğe alma stratejisi, isteğe bağlı veya edilgen önbellek stratejisidir.The most popular caching strategy is the on-demand or cache-aside strategy.

  • Uygulama, okuma sırasında verileri önbellekten okumaya çalışır.On read, the application tries to read the data from the cache. Veriler önbellekte değilse uygulama bunları veri kaynağından alır ve önbelleğe ekler.If the data isn't in the cache, the application retrieves it from the data source and adds it to the cache.
  • Uygulama, yazma sırasında değişikliği doğrudan veri kaynağına yazar ve eski değeri önbellekten kaldırır.On write, the application writes the change directly to the data source and removes the old value from the cache. Bir daha gerekli olduğunda bu veri kaynaktan alınır ve önbelleğe eklenir.It will be retrieved and added to the cache the next time it is required.

Bu yaklaşım, sık sık değişen veriler için uygundur.This approach is suitable for data that changes frequently. Önceki örnek Edilgen Önbellek düzenini kullanacak şekilde güncelleştirilmiş haliyle aşağıdadır.Here is the previous example updated to use the Cache-Aside pattern.

public class CachedPersonRepository : IPersonRepository
{
    private readonly PersonRepository _innerRepository;

    public CachedPersonRepository(PersonRepository innerRepository)
    {
        _innerRepository = innerRepository;
    }

    public async Task<Person> GetAsync(int id)
    {
        return await CacheService.GetAsync<Person>("p:" + id, () => _innerRepository.GetAsync(id)).ConfigureAwait(false);
    }
}

public class CacheService
{
    private static ConnectionMultiplexer _connection;

    public static async Task<T> GetAsync<T>(string key, Func<Task<T>> loadCache, double expirationTimeInMinutes)
    {
        IDatabase cache = Connection.GetDatabase();
        T value = await GetAsync<T>(cache, key).ConfigureAwait(false);
        if (value == null)
        {
            // Value was not found in the cache. Call the lambda to get the value from the database.
            value = await loadCache().ConfigureAwait(false);
            if (value != null)
            {
                // Add the value to the cache.
                await SetAsync(cache, key, value, expirationTimeInMinutes).ConfigureAwait(false);
            }
        }
        return value;
    }
}

GetAsync yönteminin artık doğrudan veritabanını çağırmak yerine CacheService sınıfını çağırdığına dikkat edin.Notice that the GetAsync method now calls the CacheService class, rather than calling the database directly. CacheService sınıfı, öğeyi ilk olarak Azure Redis Cache’ten almaya çalışır.The CacheService class first tries to get the item from Azure Redis Cache. Değer, Redis Cache’te bulunamazsa CacheService kendisine çağıran tarafından geçirilmiş olan bir lambda işlevini çağırır.If the value isn't found in Redis Cache, the CacheService invokes a lambda function that was passed to it by the caller. Lambda işlevi, verileri veritabanından getirmekten sorumludur.The lambda function is responsible for fetching the data from the database. Bu uygulama, depo ile kullanılan önbellek çözümünü ve CacheService ile veritabanını birbirinden ayırır.This implementation decouples the repository from the particular caching solution, and decouples the CacheService from the database.

Dikkat edilmesi gerekenlerConsiderations

  • Belki de geçici bir hata nedeniyle önbellek kullanılamıyorsa istemciye hata döndürmeyin.If the cache is unavailable, perhaps because of a transient failure, don't return an error to the client. Bunun yerine, verileri özgün veri kaynağından getirin.Instead, fetch the data from the original data source. Ancak, önbellek kurtarılırken özgün veri deposuna çok sayıda istek gidebileceğini, bu yüzden zaman aşımları ve bağlantı hataları yaşanabileceğini unutmayın.However, be aware that while the cache is being recovered, the original data store could be swamped with requests, resulting in timeouts and failed connections. (Zaten, önbellek kullanmanın başlıca amaçlarından biri de bu durumları önlemektir.) Veri kaynağına aşırı yüklenilmesini önlemek için Devre Kesici düzeni gibi bir teknik kullanın.(After all, this is one of the motivations for using a cache in the first place.) Use a technique such as the Circuit Breaker pattern to avoid overwhelming the data source.

  • Statik olmayan verileri önbelleğe alan uygulamalar, nihai tutarlılığı destekleyecek şekilde tasarlanmalıdır.Applications that cache nonstatic data should be designed to support eventual consistency.

  • Web API'leri için, istek ve yanıt iletilerine bir Cache-Control üst bilgisi ekleyerek ve ETag'ler aracılığıyla nesne sürümlerini tanımlayarak istemci tarafında önbelleğe almayı destekleyebilirsiniz.For web APIs, you can support client-side caching by including a Cache-Control header in request and response messages, and using ETags to identify versions of objects. Daha fazla bilgi için bkz. API uygulaması.For more information, see API implementation.

  • Varlıkların tamamını önbelleğe almanız gerekmez.You don't have to cache entire entities. Varlığın büyük bir kısmı statikse ve yalnızca küçük bir parçası sık sık değişiyorsa statik öğeleri önbelleğe alın ve dinamik öğeleri veri kaynağından alın.If most of an entity is static but only a small piece changes frequently, cache the static elements and retrieve the dynamic elements from the data source. Bu yaklaşım, veri kaynağına yönelik olarak gerçekleştirilen G/Ç hacminin azaltılmasına yardımcı olabilir.This approach can help to reduce the volume of I/O being performed against the data source.

  • Bazı durumlarda, geçici veriler kısa süreli ise bunları önbelleğe almak kullanışlı olabilir.In some cases, if volatile data is short-lived, it can be useful to cache it. Örneğin, sürekli olarak durum güncelleştirmeleri gönderen bir cihaz düşünelim.For example, consider a device that continually sends status updates. Bu bilgileri gelir gelmez önbelleğe almak ve asla kalıcı bir depoya yazmamak mantıklı olabilir.It might make sense to cache this information as it arrives, and not write it to a persistent store at all.

  • Birçok önbellek çözümü, verilerin eskimesini engellemek için yapılandırılabilir sona erme sürelerini destekler. Böylece veriler, belirtilen bir sürenin ardından önbellekten otomatik olarak kaldırılır.To prevent data from becoming stale, many caching solutions support configurable expiration periods, so that data is automatically removed from the cache after a specified interval. Sona erme süresini senaryonuza uygun olarak ayarlamanız gerekebilir.You may need to tune the expiration time for your scenario. Son derece statik olan veriler, daha hızlı eskiyebilecek olan geçici verilere göre daha uzun süre önbellekte kalabilir.Data that is highly static can stay in the cache for longer periods than volatile data that may become stale quickly.

  • Önbelleğe alma çözümü yerleşik bir sona erme özelliği sağlamıyorsa önbelleğin sınırsızca büyümesini önlemek için, ara sıra önbelleği temizleyen bir arka plan işlemi uygulamanız gerekebilir.If the caching solution doesn't provide built-in expiration, you may need to implement a background process that occasionally sweeps the cache, to prevent it from growing without limits.

  • Dış veri kaynaklarından gelen verileri önbelleğe almanın yanı sıra karmaşık hesaplamaların sonuçlarını kaydetmek için de önbelleğe alma işlemini kullanabilirsiniz.Besides caching data from an external data source, you can use caching to save the results of complex computations. Ancak, bunu yapmadan önce uygulamanın gerçekten CPU tarafından sınırlanıp sınırlanmadığını belirlemek için uygulamayı izleyin.Before you do that, however, instrument the application to determine whether the application is really CPU bound.

  • Uygulama başlatıldığında önbelleği hazırlamak faydalı olabilir.It might be useful to prime the cache when the application starts. Önbelleğe kullanılma olasılığı en yüksek olan verileri doldurun.Populate the cache with the data that is most likely to be used.

  • Her zaman, istenen verilerin önbellekte bulunduğu ve bulunmadığı durumları algılayan izleme işlevi ekleyin.Always include instrumentation that detects cache hits and cache misses. Bu bilgilerden yararlanarak hangi verilerin önbelleğe alınacağı, verilerin süresi dolmadan önce ne kadar süre önbellekte tutulacağı gibi önbelleğe alma ilkelerini ayarlayın.Use this information to tune caching policies, such what data to cache, and how long to hold data in the cache before it expires.

  • Önbelleğe alma özelliğinin olmaması bir performans sorunu oluşturuyorsa, önbelleğe almanın eklenmesi de istek hacmini web ön ucunun aşırı yüklenmesine neden olacak kadar artırabilir.If the lack of caching is a bottleneck, then adding caching may increase the volume of requests so much that the web front end becomes overloaded. İstemciler, HTTP 503 (Hizmet Kullanılamıyor) hataları almaya başlayabilir.Clients may start to receive HTTP 503 (Service Unavailable) errors. Bu durum, ön ucun ölçeğinin genişletilmesi gerektiğine işaret eder.These are an indication that you should scale out the front end.

Sorunu algılamaHow to detect the problem

Önbelleğe alma özelliği olmamasının performans sorunlarına neden olup olmadığını belirlemenize yardımcı olması için aşağıdaki adımları gerçekleştirebilirsiniz:You can perform the following steps to help identify whether lack of caching is causing performance problems:

  1. Uygulama tasarımını gözden geçirin.Review the application design. Uygulamanın kullandığı tüm veri depolarının bir envanterini alın.Take an inventory of all the data stores that the application uses. Her bir veri deposu için, uygulamanın önbellek kullanıp kullanmadığını belirleyin.For each, determine whether the application is using a cache. Mümkünse verilerin ne sıklıkta değiştiğini belirleyin.If possible, determine how frequently the data changes. Önbelleğe alma konusunda ilk olarak düşünülebilecek adaylar, sık sık değişmeyen veriler ve sık okunan statik başvuru verileridir.Good initial candidates for caching include data that changes slowly, and static reference data that is read frequently.

  2. Uygulamayı ve canlı sistemi izleyerek uygulamanın ne sıklıkta veri aldığını veya bilgi hesapladığını öğrenin.Instrument the application and monitor the live system to find out how frequently the application retrieves data or calculates information.

  3. Bir test ortamında uygulamanın profilini oluşturarak veri erişim işlemlerinin veya sık yapılan diğer hesaplamaların getirdiği ek yükle ilgili alt düzey ölçümleri yakalayın.Profile the application in a test environment to capture low-level metrics about the overhead associated with data access operations or other frequently performed calculations.

  4. Bir test ortamında yük testi gerçekleştirerek sistemin normal bir iş yükü ve ağır yük altında nasıl yanıt verdiğini belirleyin.Perform load testing in a test environment to identify how the system responds under a normal workload and under heavy load. Yük testi, gerçekçi iş yükleri kullanarak üretim ortamında gözlenen veri erişimi kalıplarının benzetimini yapmalıdır.Load testing should simulate the pattern of data access observed in the production environment using realistic workloads.

  5. Temel veri depolarının veri erişim istatistiklerini inceleyin ve aynı veri isteklerinin ne sıklıkta tekrarlandığını gözden geçirin.Examine the data access statistics for the underlying data stores and review how often the same data requests are repeated.

Örnek tanılamaExample diagnosis

Aşağıdaki bölümlerde, bu adımlar yukarıda açıklanan örnek uygulamaya uygulanmaktadır.The following sections apply these steps to the sample application described earlier.

Uygulamayı ve canlı sistemi izlemeInstrument the application and monitor the live system

Uygulamayı izleyerek uygulama üretimdeyken kullanıcıların yaptığı belirli istekler hakkında bilgi edinin.Instrument the application and monitor it to get information about the specific requests that users make while the application is in production.

Aşağıdaki resimde, bir yük testi sırasında New Relic tarafından yakalanan izleme verileri gösterilmektedir.The following image shows monitoring data captured by New Relic during a load test. Bu durumda gerçekleştirilen tek HTTP GET işlemi, Person/GetAsync işlemidir.In this case, the only HTTP GET operation performed is Person/GetAsync. Ancak canlı bir üretim ortamında her bir isteğin hangi sıklıkta yapıldığının bilinmesi, hangi kaynakların önbelleğe alınması gerektiği hakkında size bir fikir verebilir.But in a live production environment, knowing the relative frequency that each request is performed can give you insight into which resources should be cached.

CachingDemo uygulaması için sunucu isteklerini gösteren New Relic

Daha ayrıntılı bir analiz gerekiyorsa bir test ortamında (üretim sisteminde değil) alt düzey performans verilerini yakalamak için bir profil oluşturucu kullanabilirsiniz.If you need a deeper analysis, you can use a profiler to capture low-level performance data in a test environment (not the production system). G/Ç istek hızları, bellek kullanımı ve CPU kullanımı gibi ölçümlere bakın.Look at metrics such as I/O request rates, memory usage, and CPU utilization. Bu ölçümler, bir veri deposu veya hizmete çok sayıda istek yapıldığını ya da aynı hesaplamayı gerçekleştiren yinelenen işlemler olduğunu gösterebilir.These metrics may show a large number of requests to a data store or service, or repeated processing that performs the same calculation.

Uygulamanın yük testini yapmaLoad test the application

Aşağıdaki grafikte, örnek uygulamanın yük testinin sonuçları gösterilmektedir.The following graph shows the results of load testing the sample application. Yük testi, tipik bir dizi işlem gerçekleştiren 800 adede kadar kullanıcının aşamalı artan yükünün benzetimini yapmaktadır.The load test simulates a step load of up to 800 users performing a typical series of operations.

Önbelleksiz senaryo için performans yük testi sonuçları

Saniye başına gerçekleştirilen başarılı testlerin sayısı bir platoya ulaşmakta ve bunun sonucunda ek istekler yavaşlamaktadır.The number of successful tests performed each second reaches a plateau, and additional requests are slowed as a result. Ortalama test süresi, iş yükü arttıkça düzenli olarak artmaktadır.The average test time steadily increases with the workload. Kullanıcı yükü en üst noktaya ulaştıktan sonra yanıt süresi sabit bir hal almaktadır.The response time levels off once the user load peaks.

Veri erişim istatistiklerini incelemeExamine data access statistics

Bir veri deposu tarafından sunulan veri erişim istatistikleri ve diğer bilgiler, hangi sorguların en sık yinelendiği gibi yararlı bilgiler sağlayabilir.Data access statistics and other information provided by a data store can give useful information, such as which queries are repeated most frequently. Örneğin, Microsoft SQL Server'daki sys.dm_exec_query_stats yönetim görünümünde yakın zamanda yürütülen sorgulara ilişkin istatistiksel bilgiler vardır.For example, in Microsoft SQL Server, the sys.dm_exec_query_stats management view has statistical information for recently executed queries. Her sorgunun metni sys.dm_exec-query_plan görünümünde bulunabilir.The text for each query is available in the sys.dm_exec-query_plan view. SQL Server Management Studio gibi bir araç kullanıp aşağıdaki SQL sorgusunu çalıştırarak sorguların ne sıklıkta gerçekleştirildiğini belirleyebilirsiniz.You can use a tool such as SQL Server Management Studio to run the following SQL query and determine how frequently queries are performed.

SELECT UseCounts, Text, Query_Plan
FROM sys.dm_exec_cached_plans
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
CROSS APPLY sys.dm_exec_query_plan(plan_handle)

Sonuçlardaki UseCount sütunu her bir sorgunun ne sıklıkta çalıştırıldığını gösterir.The UseCount column in the results indicates how frequently each query is run. Aşağıdaki resimde, üçüncü sorgunun diğer tüm sorgulardan çok daha fazla şekilde, 250.000 kereden fazla çalıştırıldığı gösterilmektedir.The following image shows that the third query was run more than 250,000 times, significantly more than any other query.

SQL Server Management Server'daki dinamik yönetim görünümlerini sorgulama sonuçları

Bu denli çok sayıda veritabanı isteğine neden olan SQL sorgusu şudur:Here is the SQL query that is causing so many database requests:

(@p__linq__0 int)SELECT TOP (2)
[Extent1].[BusinessEntityId] AS [BusinessEntityId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName]
FROM [Person].[Person] AS [Extent1]
WHERE [Extent1].[BusinessEntityId] = @p__linq__0

Bu sorgu, yukarıda gösterilen GetByIdAsync yönteminde Entity Framework tarafından oluşturulur.This is the query that Entity Framework generates in GetByIdAsync method shown earlier.

Çözümü uygulama ve sonucu doğrulamaImplement the solution and verify the result

Bir önbellek ekledikten sonra yük testlerini yineleyin ve sonuçları önbellek olmadan gerçekleştirilen önceki yük testleriyle karşılaştırın.After you incorporate a cache, repeat the load tests and compare the results to the earlier load tests without a cache. Örnek uygulamaya bir önbellek ekledikten sonra yapılan yük testlerinin sonuçları aşağıdadır.Here are the load test results after adding a cache to the sample application.

Önbellekli senaryo için performans yük testi sonuçları

Başarılı testlerin hacmi yine bir platoya ulaşıyor, ancak bu plato daha yüksek bir kullanıcı yükünde gerçekleşiyor.The volume of successful tests still reaches a plateau, but at a higher user load. Bu yükteki istek hızı önceki duruma göre çok daha yüksektir.The request rate at this load is significantly higher than earlier. Ortalama test süresi hala yük ile artmaktadır, ancak en yüksek yanıt süresi, önceki 1 ms ile karşılaştırıldığında 20 kat gelişme göstererek 0,05 ms olmaktadır.Average test time still increases with load, but the maximum response time is 0.05 ms, compared with 1 ms earlier—a 20× improvement.