Antipattern neexistujícího ukládání do mezipaměti

Anti-vzory jsou běžné chyby návrhu, které mohou narušit váš software nebo aplikace v situacích stresu a neměly by být přehlédnuty. K žádnému antipatternu ukládání do mezipaměti nedojde, když cloudová aplikace, která zpracovává mnoho souběžných požadavků, opakovaně načítá stejná data. To může snížit výkon a škálovatelnost.

Když se data neukládají do mezipaměti, může to způsobovat množství nežádoucího chování, mimo jiné:

  • Opakované načítání stejných informací z prostředku s nákladným přístupem z hlediska latence nebo režie související se vstupně-výstupními operacemi.
  • Opakované vytváření stejných objektů nebo datových struktur pro více požadavků.
  • Provádění nadměrného volání vzdálených služeb, které mají kvótu a po překročení určitého limitu omezují klienty.

Tyto problémy pak můžou vést ke zhoršení doby odezvy, zvýšení množství kolizí v úložišti dat a zhoršení škálovatelnosti.

Příklady antipatternu ukládání do mezipaměti

Následující příklad se pomocí rozhraní Entity Framework připojí k databázi. Výsledkem každého požadavku klienta je volání databáze, i když několik požadavků načítá naprosto stejná data. Náklady na opakované požadavky, z hlediska režie související se vstupně-výstupními operacemi a poplatky za přístup k datům, můžou rychle narůstat.

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);
        }
    }
}

Kompletní ukázku najdete tady.

Možné důvody vzniku tohoto antipatternu:

  • Implementace bez použití mezipaměti je jednodušší a při nízké zátěži funguje bez problémů. Ukládání do mezipaměti komplikuje kód.
  • Nejasné porozumění výhodám a nevýhodám použití mezipaměti.
  • Existuje obava o režii související se zachováním přesnosti a aktuálnosti dat uložených v mezipaměti.
  • Aplikace se migrovala z místního systému, kde latence sítě nepředstavovala problém, a tento systém běžel na drahém a vysoce výkonném hardwaru, takže se ukládání do mezipaměti v původním návrhu nezohlednilo.
  • Vývojáři si nejsou vědomi možnosti využití ukládání do mezipaměti v daném scénáři. Vývojáři například nemusí vzít v úvahu použití značek entit při implementaci webového rozhraní API.

Jak opravit antipattern bez ukládání do mezipaměti

Nejoblíbenější strategií ukládání do mezipaměti je strategie na vyžádání nebo s doplňováním mezipaměti.

  • Při čtení se aplikace pokusí přečíst data z mezipaměti. Pokud data v mezipaměti nejsou, aplikace je načte ze zdroje dat a přidá je do mezipaměti.
  • Při zápisu aplikace zapíše změny přímo do zdroje dat a z mezipaměti odebere staré hodnoty. Data se načtou a přidají do mezipaměti při dalším vyžádání.

Tento přístup je vhodný pro data, která se často mění. Tady je předchozí příklad aktualizovaný tak, aby používal princip s doplňováním mezipaměti.

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;
    }
}

Všimněte si, že metoda GetAsync teď volá třídu CacheService místo toho, aby volala přímo databázi. Třída CacheService se nejprve pokusí získat položku ze služby Azure Cache for Redis. Pokud se hodnota v mezipaměti nenachází, třída CacheService vyvolá funkci lambda, kterou jí předala volající metoda. Funkce lambda zodpovídá za načtení dat z databáze. Tato implementace odděluje úložiště od konkrétního řešení ukládání do mezipaměti a třídu CacheService od databáze.

Důležité informace o strategii ukládání do mezipaměti

  • Pokud je mezipaměť nedostupná, například kvůli přechodnému selhání, nevracejte do klienta chybu. Místo toho načtěte data z původního zdroje dat. Pamatujte však, že při obnovování mezipaměti může být původní úložiště dat zahlcené požadavky, což může způsobit vypršení časového limitu a selhání připojení. (Koneckonců, to je jedna z motivací k použití mezipaměti na prvním místě.) Pokud chcete zabránit zahlcení zdroje dat, použijte techniku, jako je model Jistič.

  • Aplikace, které do mezipaměti ukládají dynamická data, by měly být navržené pro podporu konečné konzistence.

  • V případě webových rozhraní API můžete zajistit podporu ukládání do mezipaměti na straně klienta zahrnutím hlavičky Cache-Control v požadavku a zprávách s odezvami a použitím značek entit k identifikaci verzí objektů. Další informace najdete v tématu Implementace rozhraní API.

  • Do mezipaměti nemusíte ukládat celé entity. Pokud je většina entity statická, ale jenom malá část se často mění, uložte do mezipaměti statické elementy a dynamické elementy načítejte ze zdroje dat. Tento přístup může pomoct snížit objem prováděných vstupně-výstupních operací se zdrojem dat.

  • V některých případech je užitečné ukládat do mezipaměti nestálá a krátkodobá data. Představte si například zařízení, které průběžně odesílá aktualizace stavu. Tyto informace může být vhodné při přijetí ukládat do mezipaměti, a do trvalého úložiště je vůbec nezapisovat.

  • Z důvodu zabránění zastarávání dat řada řešení ukládání do mezipaměti podporuje konfigurovatelné doby vypršení platnosti, aby se data po uplynutí zadaného časového intervalu z mezipaměti automaticky odebrala. Čas vypršení platnosti možná budete muset pro účely vašeho scénáře vyladit. Vysoce statická data můžou v mezipaměti zůstat déle než nestálá data, která můžou rychle zastarávat.

  • Pokud řešení ukládání do mezipaměti neposkytuje integrované vypršení platnosti, možná budete muset implementovat proces na pozadí, který občas mezipaměť pročistí, aby se zabránilo jejímu neomezenému zvětšování.

  • Kromě ukládání dat z externích zdrojů dat můžete ukládání do mezipaměti použít k ukládání výsledků složitých výpočtů. Předtím, než to uděláte, ale aplikaci instrumentujte, abyste určili, jestli je skutečně závislá na procesoru.

  • Mezipaměť může být vhodné při spuštění aplikace vymazat. Naplňte mezipaměť daty s největší pravděpodobností používání.

  • Vždy zahrňte instrumentaci, která rozpozná úspěšné a neúspěšné přístupy k mezipaměti. Pomocí těchto informací můžete ladit zásady ukládání do mezipaměti, například jaká data se mají ukládat a jak dlouho se mají držet v mezipaměti, než vyprší jejich platnost.

  • Pokud je chybějící ukládání do mezipaměti kritickým bodem, pak může přidání ukládání do mezipaměti zvýšit počet požadavků takovým způsobem, že dojde k přetížení webového front-endu. Klienti můžou začít dostávat chyby HTTP 503 (Služba není dostupná). Ty jsou znamením, že byste měli front-end škálovat na více instancí.

Zjištění antipatternu bez ukládání do mezipaměti

Následující postup vám pomůže identifikovat, jestli chybějící ukládání do mezipaměti způsobuje problémy s výkonem:

  1. Zkontrolujte návrh aplikace. Proveďte inventarizaci všech úložišť dat, která aplikace používá. U každého určete, jestli aplikace používá mezipaměť. Pokud je to možné, určete, jak často se data mění. Mezi vhodné počáteční kandidáty pro ukládání do mezipaměti patří pomalu se měnící data a často čtená statická referenční data.

  2. Instrumentujte aplikaci a monitorujte systém za provozu, abyste zjistili, jak často aplikace načítá data nebo vypočítává informace.

  3. Profilujte aplikaci v testovacím prostředí, abyste zachytili nízkoúrovňové metriky o režii související s operacemi přístupu k datům nebo jinými často prováděnými výpočty.

  4. Proveďte zátěžové testování v testovacím prostředí, abyste identifikovali, jak systém reaguje při normálním zatížení a při velkém zatížení. Zátěžové testování by mělo simulovat vzorec přístupu k datům vypozorovaný v produkčním prostředí s využitím realistických úloh.

  5. Prozkoumejte statistiky přístupu k datům pro základní úložiště dat a zkontrolujte, jak často se opakují požadavky na stejná data.

Ukázková diagnostika

V následujících částech se tento postup použije u ukázkové aplikace popsané výše.

Instrumentace aplikace a monitorování systému za provozu

Instrumentujte aplikaci a monitorujte ji, abyste získali informace o konkrétních požadavcích, které uživatelé provádějí, když je aplikace v produkčním prostředí.

Následující obrázek ukazuje monitorování dat zachycených přes New Relic během zátěžového testu. V tomto případě je jedinou provedenou operací HTTP GET Person/GetAsync. V produkčním prostředí vám však znalost relativní četnosti provádění jednotlivých požadavků může poskytnout přehled o tom, které prostředky by se měly ukládat do mezipaměti.

New Relic showing server requests for the CachingDemo application

Pokud potřebujete hlubší analýzu, můžete pomocí profileru zachytávat nízkoúrovňová data o výkonu v testovacím prostředí (ne v produkčním systému). Podívejte se na metriky, jako je frekvence požadavků na vstupně-výstupní operace a využití paměti a procesoru. Tyto metriky můžou ukázat velké množství požadavků na úložiště dat nebo službu nebo opakované zpracování provádějící stejný výpočet.

Zátěžový test aplikace

Následující graf ukazuje výsledky zátěžového testování ukázkové aplikace. Zátěžový test simuluje krokové zatížení až 800 uživatelů, kteří provádějí obvyklé řady operací.

Performance load test results for the uncached scenario

Počet úspěšně provedených testů každou sekundu dosahuje stabilní úrovně a výsledkem je zpomalování dalších požadavků. Průměrný čas testu se plynule prodlužuje s nárůstem zatížení. Doba odezvy se ustálí, jakmile uživatelské zatížení dosáhne vrcholu.

Zkoumání statistik přístupu k datům

Statistiky přístupu k datům a další informace získané z úložiště dat můžou poskytovat užitečné informace například o tom, jaké dotazy se opakují nejčastěji. Například zobrazení správy sys.dm_exec_query_stats v Microsoft SQL Serveru obsahuje statistické informace o nedávno provedených dotazech. Texty jednotlivých dotazů jsou k dispozici v zobrazení sys.dm_exec-query_plan. Pomocí nástroje, jako je SQL Server Management Studio, můžete spustit následující příkaz jazyka SQL a určit, jak často se příkazy provádějí.

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)

Sloupec UseCount ve výsledcích udává, jak často se jednotlivé dotazy spouští. Následující obrázek ukazuje, že se třetí dotaz spustil více než 250 000krát, což je výrazně více než jakýkoli jiný dotaz.

Results of querying the dynamic management views in SQL Server Management Server

Tady je příkaz jazyka SQL, který způsobuje tolik požadavků na databázi:

(@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

Jedná se o dotaz, který Entity Framework generuje v metodě GetByIdAsync popsané výše.

Implementace řešení strategie mezipaměti a ověření výsledku

Jakmile implementujete mezipaměť, zopakujte zátěžové testy a porovnejte výsledky s dřívějšími zátěžovými testy bez mezipaměti. Tady jsou výsledky zátěžového testu po přidání mezipaměti do ukázkové aplikace.

Performance load test results for the cached scenario

Množství úspěšných testů stále dosahuje stabilní úrovně, ale při větším uživatelském zatížení. Frekvence požadavků při tomto zatížení je výrazně vyšší než dříve. Průměrná doba testu se stále zvyšuje s zatížením, ale maximální doba odezvy je 0,05 ms ve srovnání s 1 ms dříve – vylepšením 20×.