Zlepšení výkonu pomocí mezipaměti s flexibilním serverem Azure Database for PostgreSQL

Při vytváření aplikace na Azure Database for PostgreSQL flexibilním serveru je přidání vrstvy ukládání do mezipaměti jedním z nejúčinnějších způsobů, jak zlepšit dobu odezvy, snížit zatížení databáze a zvýšit odolnost. Díky tomu, že vaše aplikace obsluhuje často čtená data z mezipaměti v paměti, odesílá do PostgreSQL méně dotazů. To znamená nižší využití procesoru a IOPS, takže můžete běžet na menší výpočetní úrovni, škálovat čtení bez vertikálního navýšení kapacity serveru a absorbovat špičky provozu. Mezipaměť může také přidat odolnost. Pokud má PostgreSQL krátké přerušení, požadavky, které narazí na data, která už jsou v mezipaměti, můžou být úspěšné, takže cesty ke čtení zůstanou dostupné, dokud se databáze obnoví.

Tento článek vám pomůže rozhodnout, kdy ukládání do mezipaměti pomůže a jaký vzor vyhovuje vaší aplikaci. Poté implementuje čtyři vzory práce s mezipamětí v jazyce Python (cache-aside, předběžné načítání referenčních dat, write-through a řízení událostmi) pomocí Azure Managed Redis, knihoven redis a psycopg a ověřování Microsoft Entra ID.

Kdy přidat mezipaměť

PostgreSQL už ukládá často používané datové stránky do mezipaměti vyrovnávací paměti a také využívá mezipaměť souborů operačního systému. Tyto mezipaměti zkrátí opakovaný přístup, ale sdílejí paměť přidělenou databázovému serveru s prováděním dotazů a dalšími procesy. Jejich obsah je také potřeba znovu zahřát po některých operacích údržby a převzetí služeb při selhání.

Kapacitu mezipaměti databáze můžete získat škálováním na výpočetní možnost s větší pamětí. Můžete také vyladit nastavení paměti v PostgreSQL, ale pokud přidělíte více paměti vyrovnávací paměti, zbude jí méně pro provádění dotazů a operační systém. Pečlivě otestujte změny v paměti, abyste se vyhnuli stavům nedostatku paměti.

Azure Managed Redis doplňuje tyto nativní mezipaměti. Ukládá vybraná data aplikací a výsledky dotazů mimo databázový server, poskytuje přístup k cestám čtení citlivým na čas s nižší latencí a může uchovávat čtení uložená v mezipaměti, zatímco PostgreSQL obnoví nebo zahřeje mezipaměť. Tuto funkci použijte, pokud tyto výhody odůvodňují přidanou logiku aplikace a jinou službu pro provoz. Nenahrazuje PostgreSQL jako zdroj pravdy.

Přidání externí mezipaměti, jako je Azure Managed Redis, pomáhá nejvíce, když má vaše úloha tyto charakteristiky:

  • Vzorce přístupu s převahou čtení Stejné řádky se čtou mnohem častěji, než se mění, například katalogy produktů, profily uživatelů, konfigurační data nebo referenční tabulky.
  • Nákladné nebo opakované dotazy. Agregace, spojení nebo vypočítané výsledky, které jsou nákladné pro výrobu, ale stabilní v krátkých časových oknech.
  • Koncové body citlivé na latenci Uživatelsky orientované operace, kdy čtení v paměti (v milisekundách) je vhodnější než doba odezvy databáze.
  • Předvídatelné špičky. Sezónní provoz nebo provoz řízený událostmi, kdy ukládání do mezipaměti absorbuje zatížení, které by jinak vynutilo škálování výpočetních prostředků.
  • Citlivost na údržbu a přepnutí při selhání. Operace čtení, které vyžadují stabilní dobu odezvy během obnovy instance PostgreSQL nebo naplňování její mezipaměti po údržbě nebo převzetí služeb při selhání.

Ukládání do mezipaměti je méně přínosné pro úlohy náročné na zápis, data, která musí být vždy transakčně konzistentní, nebo dotazy, které jsou již rychlé a opakují se jen zřídka.

Vzory ukládání do mezipaměti

Tento článek používá jako průběžný příklad maloobchodní prodejnu. Různé části aplikace využívají různé vzory ukládání do mezipaměti. Následující části implementují první čtyři vzory v Python. Článek popisuje vzorce snižování zátěže relací a stavu a ukládání do mezipaměti ve více oblastech, ale neposkytuje pro ně implementace kódu.

Pattern Jak to funguje V prodejním obchodě
Cache-aside (líné načítání) Aplikace nejprve zkontroluje mezipaměť. Při chybě se načte z PostgreSQL a pak naplní mezipaměť. Katalog produktů a detailní stránky produktů, kde několik oblíbených položek generuje většinu zobrazení.
Předběžné načtení referenčních dat Stabilní data jsou do mezipaměti načtena předem a obnovována při změně zdroje, namísto při minutí mezipaměti. Kategorie, značky a konfigurace expedice
Průběžný zápis Aplikace zapisuje do mezipaměti a PostgreSQL ve stejné operaci a udržuje je konzistentní. Aktualizace cen a zásob, které musí být viditelné okamžitě.
Zneplatnění řízené událostmi Položky mezipaměti se aktualizují nebo zneplatňují v reakci na události změn dat, nikoli na základě časovače. Stav objednávky v průběhu jejího vyřizování.
Odlehčení relace a stavu Přechodný stav se nachází v mezipaměti místo databáze. Nákupní košíky a uživatelské relace.
Ukládání do mezipaměti ve více regionech Mezipaměť v každé oblasti obsluhuje místní čtení, která jsou synchronizovaná s aktivní geografickou replikací. Globální výkladní skříň obsluhující nakupující v několika oblastech

Předpoklady

Tip

Kompletní, nasaditelná verze této ukázky, včetně infrastruktury jako kódu a všech čtyř vzorů, najdete v úložišti amr-caching-pattern-samples na GitHub.

Krok 1: Instalace klientských knihoven

Nainstalujte klientské knihovny Redis a PostgreSQL spolu s knihovnou identit Azure pro ověřování Microsoft Entra.

pip install "redis>=5.0,<6.0" "psycopg[binary]>=3.1,<4.0" "azure-identity>=1.17,<2.0"

Krok 2: Připojení pomocí Microsoft Entra ID

Místo přístupových klíčů používejte ověřování Microsoft Entra ID. Microsoft Entra ID eliminuje potřebu ukládat tajné kódy do aplikace a umožňuje centrálně spravovat přístup.

Následující kód vytvoří klienta Redis a připojení PostgreSQL, které se ověřuje pomocí spravované identity nebo přihlašovacích údajů vývojáře.DefaultAzureCredential Příklad používá klienta RedisCluster, který podporuje práci s clusterem a odpovídá zásadě clusterování OSS, kterou tato ukázka používá. Pokud mezipaměť používá zásady clusteringu Enterprise, použijte místo toho standardního redis.Redis klienta.

import os
import json
import redis
from redis.cluster import RedisCluster
import psycopg
from azure.identity import DefaultAzureCredential

REDIS_HOST = os.environ["REDIS_HOST"]  # for example, mycache.eastus.redis.azure.net
REDIS_PORT = 10000
PG_HOST = os.environ["PG_HOST"]        # for example, myserver.postgres.database.azure.com
PG_DATABASE = os.environ["PG_DATABASE"]

credential = DefaultAzureCredential()

# Acquire a token for Azure Managed Redis and use it as the password.
redis_token = credential.get_token("https://redis.azure.com/.default")

# The username is the object ID of the Microsoft Entra identity.
redis_client = RedisCluster(
    host=REDIS_HOST,
    port=REDIS_PORT,
    ssl=True,
    ssl_check_hostname=False,  # cluster nodes are reached by IP; the certificate chain is still validated
    username=os.environ["REDIS_USER_OBJECT_ID"],
    password=redis_token.token,
    decode_responses=True,
)

# Acquire a token for Azure Database for PostgreSQL and use it as the password.
pg_token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default")

pg_conn = psycopg.connect(
    host=PG_HOST,
    dbname=PG_DATABASE,
    user=os.environ["PG_USER"],
    password=pg_token.token,
    sslmode="require",
)

Note

Přístupovým tokenům Microsoft Entra vyprší platnost obvykle přibližně po jedné hodině. U dlouho běžících aplikací obnovte token před vypršením jeho platnosti a znovu se připojte k Azure Managed Redis i Azure Database for PostgreSQL, nebo použijte pomocnou rutinu, která transparentně znovu získává tokeny. Podrobnosti najdete v tématu Použití Microsoft Entra ID pro ověřování s Azure Managed Redis.

Krok 3: Strategie cache-aside

Vzor Cache-aside je nejběžnější a v této ukázce se používá při čtení produktů. Aplikace nejprve zkontroluje Redis, a pokud v mezipaměti položku nenajde, dotáže se PostgreSQL a uloží výsledek do mezipaměti s dobou života (TTL). Několik oblíbených produktů generuje většinu zobrazení, takže míra zásahů je vysoká.

Protože je většina požadavků na čtení obsluhována z paměti, strategie cache-aside snižuje trvalou zátěž PostgreSQL při čtení. Toto snížení znamená méně spojení, menší zatížení vyrovnávací paměti a nižší využití procesoru i nižší počet IOPS. Špičky čtení můžete absorbovat bez vertikálního navýšení kapacity serveru nebo přidávání replik pro čtení. Na PostgreSQL se dotazujete pouze při chybě cache (při prvním přístupu nebo po vypršení TTL). Podívejte se, co se má ukládat do mezipaměti, abyste identifikovali dotazy, které stojí za ukládání do mezipaměti.

CACHE_TTL_SECONDS = 300  # 5 minutes

def get_product(product_id: int) -> dict | None:
    cache_key = f"product:{product_id}"

    # 1. Try the cache first.
    cached = redis_client.get(cache_key)
    if cached is not None:
        return json.loads(cached)

    # 2. On a miss, read from PostgreSQL.
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
        row = cursor.fetchone()
    if row is None:
        return None

    product = {"id": row[0], "name": row[1], "price": float(row[2])}

    # 3. Populate the cache with a TTL, then return.
    redis_client.set(cache_key, json.dumps(product), ex=CACHE_TTL_SECONDS)
    return product

Krok 4: Předběžné načtení referenčních dat

Stabilní data, která se čtou neustále, ale mění se jen zřídka (například kategorie, značky nebo konfigurace dopravy), nemusejí čekat na minutí mezipaměti. Načtěte ho do mezipaměti předem a aktualizujte ho, když se zdroj změní. Ve schématu PostgreSQL jsou to obvykle malé vyhledávací tabulky a tabulky dimenzí, které se připojují k mnoha dotazům. Jejich poskytování z paměti eliminuje v databázi velké množství opakovaného spojování tabulek a vyhledávání. Na rozdíl od cache-aside zde nedochází k minutí mezipaměti při jednotlivých požadavcích ani ke kolizím souvisejícím s TTL. Při změně se aktualizujete, takže čtení je vždy teplé.

def prefetch_categories() -> None:
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name FROM categories ORDER BY name")
        categories = [{"id": r[0], "name": r[1]} for r in cursor.fetchall()]
    redis_client.set("ref:categories", json.dumps(categories))  # no TTL; refreshed on change

def get_categories() -> list[dict]:
    cached = redis_client.get("ref:categories")
    return json.loads(cached) if cached else []

Krok 5: Průchozí zápis

Pokud musí být změna viditelná okamžitě, místo čekání na vypršení platnosti hodnoty TTL nebo zrušení platnosti klíče zapište PostgreSQL a mezipaměť ve stejné operaci. Tento model se například používá k aktualizaci informací o cenách v ukázce. PostgreSQL zůstává zdrojem pravdy. Změny se nejprve potvrdí a mezipaměť se aktualizuje, takže čtení po zápisu vrátí novou hodnotu.

Při zápisu do dvou systémů, které nesdílejí transakci, může potvrzení databáze proběhnout úspěšně, zatímco aktualizace mezipaměti selže a neexistuje žádná jednoduchá oprava. Následující fragment kódu ukazuje ideální scénář a vynechává zpracování chyb. V produkčním prostředí se sami rozhodnete, jak naložit s neúspěšným obnovením, například zda ho při zdánlivě přechodném selhání zkusit znovu, nebo zneplatnit klíč, aby se při dalším čtení data znovu načetla z PostgreSQL. PostgreSQL v obou směrech uchovává správnou hodnotu, takže zastaralá nebo chybějící položka mezipaměti je vždy obnovitelná. Pokud je nutné, aby se aktualizace mezipaměti spolehlivě provedla, řiďte ji místo toho ze streamu změn databáze (viz zneplatnění řízené událostmi).

def update_price(product_id: int, new_price: float) -> None:
    # 1. Write to PostgreSQL, the source of truth.
    with pg_conn.cursor() as cursor:
        cursor.execute("UPDATE products SET price = %s WHERE id = %s", (new_price, product_id))
    pg_conn.commit()

    # 2. Refresh the cached entry so reads see the new price right away.
    with pg_conn.cursor() as cursor:
        cursor.execute("SELECT id, name, price FROM products WHERE id = %s", (product_id,))
        row = cursor.fetchone()
    if row is not None:
        product = {"id": row[0], "name": row[1], "price": float(row[2])}
        redis_client.set(f"product:{product_id}", json.dumps(product), ex=CACHE_TTL_SECONDS)

Krok 6: Zneplatnění řízené událostmi

Zneplatnění řízené událostmi udržuje mezipaměť konzistentní s databází tím, že reaguje na události změny dat. Aktualizuje nebo zneplatňuje položky, jakmile se změní, namísto toho, aby jejich platnost vypršela po uplynutí časovače. Zapisovače připojují události k odolnému streamu Redis, protokolu jen pro připojení a jeden nebo více příjemců tyto události přečte a aktualizuje mezipaměť. Vzhledem k tomu, že stream přetrvává, události přežijí restartování příjemce. Skupina příjemců doručuje každou událost jednomu pracovníkovi, sleduje potvrzení, takže se nic neztratí nebo zpracuje dvakrát, a umožňuje škálovat zpracování napříč pracovními procesy.

Tento vzor použijte, když je hodnota v mezipaměti odvozena z dat, která se mění na jiném místě (stav, projekce nebo agregace), kde by TTL buď vedla k poskytování zastaralých dat, nebo vynucovala neustálý přepočet. V obchodě tento model řídí stav objednávky prostřednictvím plnění. Při zadání objednávky se objednávka zapíše do PostgreSQL, její počáteční stav se uloží do mezipaměti a do streamu se přidá událost placed:

ORDER_STREAM = "orders:events"

def place_order(product_id: int, quantity: int) -> int:
    with pg_conn.cursor() as cursor:
        cursor.execute(
            "INSERT INTO orders (product_id, quantity, status) VALUES (%s, %s, 'placed') RETURNING id",
            (product_id, quantity),
        )
        order_id = cursor.fetchone()[0]
    pg_conn.commit()

    redis_client.set(f"order:{order_id}:status", "placed", ex=86400)
    redis_client.xadd(ORDER_STREAM, {"order_id": order_id, "status": "placed"}, maxlen=10000, approximate=True)
    return order_id

Pracovník plnění spustí skupinu příjemců: přečte nové události, přepojí každou objednávku v PostgreSQL, aktualizuje projekci uloženou order:{id}:status v mezipaměti a potvrdí událost. Stránka objednávky přečte danou projekci, takže kontroly stavu zůstanou rychlé a nikdy se nedotknou databáze. Hodnota zůstane správná, protože události ji udržují aktuální.

GROUP = "fulfillment"

def process_orders() -> None:
    try:
        redis_client.xgroup_create(ORDER_STREAM, GROUP, id="0", mkstream=True)
    except redis.exceptions.ResponseError:
        pass  # group already exists

    while True:
        events = redis_client.xreadgroup(GROUP, "worker-1", {ORDER_STREAM: ">"}, count=10, block=5000)
        for _stream, entries in events or []:
            for event_id, fields in entries:
                order_id = int(fields["order_id"])
                with pg_conn.cursor() as cursor:
                    cursor.execute("UPDATE orders SET status = 'shipped' WHERE id = %s", (order_id,))
                pg_conn.commit()
                redis_client.set(f"order:{order_id}:status", "shipped", ex=86400)
                redis_client.xack(ORDER_STREAM, GROUP, event_id)

Zdrojem událostí v této ukázce je aplikace, která zapisuje PostgreSQL a připojí událost do stejné cesty. PostgreSQL může také generovat změny samotné: LISTEN/NOTIFY pro odlehčená oznámení nebo logické dekódování (zachytávání dat změn) pro trvalý stream změn na úrovni řádků. Řízení mezipaměti z vlastního streamu změn PostgreSQL znamená, že reaguje na každou potvrzenou změnu, a to i zápisy, které aplikaci obcházejí.

Osvědčené postupy

Dodržujte tyto postupy, aby vaše mezipaměť byla správně nastavená, efektivní a cenově výhodná.

  • Nastavte hodnotu TTL všude, kde je neakutnost rizikem. TTL omezuje zastaralost dat při selhání invalidace. Nastavte hodnotu TTL podle maximální míry zastaralosti dat, kterou může aplikace akceptovat. Pro referenční data používejte dlouhou hodnotu TTL nebo žádnou hodnotu TTL, pouze pokud existují spolehlivé procesy zneplatnění a aktualizace.
  • Použijte konzistentní schéma pojmenování klíčů. Klíče v oboru názvů podle služby, entity a identifikátoru, například product:42 nebo user:1001:profile. Pokud se může změnit formát klíče nebo schéma hodnot, přidejte verzi.
  • Ukažte správnou členitost. Jednotlivé entity nebo malé sady výsledků ukládejte do mezipaměti, pokud je často znovu používáte a lze je snadno zneplatnit. Neuložujte data, která mají málo opakovaného použití. Nadměrné ukládání do mezipaměti plýtvá pamětí a může snížit míru zásahů.
  • Zpracujte korektně stavy, kdy data nejsou v mezipaměti, i výpadky. Zachází s mezipamětí jako s optimalizací, nikoli jako se zdrojem pravdy. Pokud je Redis nedostupný, použijte ohraničenou záložní verzi do PostgreSQL. Přidejte časové limity, jističe, omezení backoffu a požadavků pro ochranu PostgreSQL. Pokud má PostgreSQL krátké přerušení, můžete dál obsluhovat čtení dat, která už jsou v mezipaměti, zatímco zápisy čekají na obnovení databáze.
  • Zabránit zahlcení mezipaměti. Když vyprší platnost oblíbeného klíče, může databáze najednou narazit na mnoho požadavků. Použijte TTL jitter, slučování požadavků, stale-while-revalidate nebo krátký distribuovaný zámek, aby jeden požadavek znovu naplnil položku.
  • Nastavení správné velikosti mezipaměti Sledujte míru zásahů, využití paměti, míru vypuzování, míru expirace, latenci, frekventované klíče a kardinalitu klíčů. Nízká míra zásahů může znamenat, že mezipaměť je příliš malá, dochází k nadměrnému vytěsňování, jsou zvoleny nevhodné klíče nebo je použit nevhodný vzor přístupu. Pokyny pro určení velikosti najdete v tématu Pokyny k výběru úrovně Azure Managed Redis.
  • Zvolte zásadu vyřazení, která odpovídá vašim klíčům. Pro databázi jen pro mezipaměť začněte s allkeys-lru nebo allkeys-lfu. Zásady volatile-* používejte pouze v případech, kdy stejná databáze obsahuje klíče mezipaměti s vypršenou platností a chráněné klíče, jejichž platnost nevypršela. Volatilní zásada může přestat vyřazovat položky, pokud žádné klíče nemají nastavenou hodnotu TTL. Pokud je to možné, oddělte data mezipaměti od chráněných dat.
  • Serializujte efektivně. JSON je čitelný a přenosný. U cest s vysokou propustností otestujte kompaktní binární formát, abyste snížili režii paměti a sítě. Než změníte formát, proveďte srovnávací testy paměti, procesoru, latence, vývoje schématu a ladění.

Zjistit, co ukládat do mezipaměti

Nejúčinnějšími cíli mezipaměti jsou dotazy, které vaše aplikace spouští nejčastěji s daty, která se mění nejméně. Namísto odhadování, které dotazy odpovídají tomuto popisu, porovnejte historické a aktuální pohledy v telemetrii dotazů, kterou může Azure Database for PostgreSQL shromažďovat po povolení příslušných funkcí:

  • Query Store zachovají statistiky provádění dotazů pro historickou analýzu. K vyhledání dotazů, které konzistentně dominují zatížení databáze během delších období, použijte počty volání a celkovou a průměrnou dobu provádění. Viz Monitorování výkonu pomocí Query Store.
  • Query Performance Insight vizualizuje Query Store data na portálu Azure, takže můžete odhalit časté dotazy náročné na prostředky a porovnat jejich chování v průběhu času. Viz Query Performance Insight.
  • pg_stat_statements zveřejňuje kumulativní statistiky pro jednotlivé příkazy v databázi pro přímý pohled na aktuální okno pozorování. Její statistiky je možné resetovat, takže použijte Query Store, když potřebujete zachovat historii napříč okny pozorování.

Před výběrem kandidáta mezipaměti použijte obě zobrazení. Krátkodobá špička nemusí představovat normální chování úlohy, zatímco historický průměr může skrýt aktuální regresi. Upřednostnit dotazy, které jsou časté, nákladné a stabilní, což znamená vysoký počet volání, vysoký celkový čas provádění a výsledky, které se u každého požadavku nemění. Tyto dotazy poskytují nejvyšší rychlost dosažení mezipaměti a největší pokles zatížení databáze. Dotaz, který se spouští neustále, ale vrací stejné řádky pro minuty, jako je výpis produktů, strom kategorií nebo cenová tabulka, je ideálním kandidátem.