Повысьте производительность с помощью кэша в База данных Azure для PostgreSQL — Flexible Server

При создании приложения на гибком сервере База данных Azure для PostgreSQL добавление слоя кэширования является одним из наиболее эффективных способов повышения времени отклика, снижения нагрузки на базу данных и повышения устойчивости. Обслуживая часто считываемые данные из кэша в памяти, приложение отправляет меньше запросов в PostgreSQL. Это означает снижение потребления ЦП и операций ввода-вывода в секунду, чтобы вы могли работать на меньшем уровне вычислений, масштабировать операции чтения без увеличения масштаба сервера и поглощать пики трафика. Кэш также может добавить устойчивость. Если PostgreSQL имеет краткое прерывание, запросы, которые уже попали в кэш, могут выполняться успешно, поэтому пути чтения остаются доступными во время восстановления базы данных.

Эта статья поможет вам решить, когда кэширование помогает и какой шаблон соответствует вашему приложению. Затем в Python реализуются четыре шаблона кэширования (cache-aside, предварительная загрузка справочных данных, сквозная запись и событийно-управляемое кэширование) с помощью Azure Managed Redis, библиотек redis и psycopg, а также аутентификации Microsoft Entra ID.

Когда добавлять кэш

PostgreSQL уже кэширует часто используемые страницы данных в своем буферном кеше, а также использует файловый кеш операционной системы. Эти кэши упрощают повторный доступ, но они совместно используют память, выделенную серверу базы данных, с выполнением запросов и другими процессами. Их содержимое также должно снова прогреться после некоторых операций обслуживания и аварийного переключения.

Вы можете получить больше емкости кэша базы данных путем масштабирования до параметра вычислений с большим объемом памяти. Вы также можете настроить параметры памяти PostgreSQL, но выделение большего объема памяти в буферный кэш оставляет меньше для выполнения запросов и операционной системы. Тщательно тестируйте изменения, связанные с памятью, чтобы избежать ситуаций с нехваткой памяти.

Azure Managed Redis дополняет эти собственные кэши. Он хранит выбранные данные приложения и результаты запросов вне сервера базы данных, обеспечивает доступ с меньшей задержкой для критичных к задержкам операций чтения и может сохранять доступность кэшированных данных для чтения, пока PostgreSQL восстанавливается или прогревает свой кэш. Используйте это, когда эти преимущества оправдывают дополнительную логику приложения и необходимость поддерживать в работе ещё один сервис. Он не заменяет PostgreSQL источником истины.

Добавление внешнего кэша, например Azure Managed Redis, помогает в большинстве случаев, когда рабочая нагрузка имеет следующие характеристики:

  • Характер доступа с преобладанием операций чтения. Те же строки считываются гораздо чаще, чем они изменяются, например каталоги продуктов, профили пользователей, данные конфигурации или справочные таблицы.
  • Дорогостоящие или повторяющиеся запросы. Агрегаты, соединения или вычисляемые результаты, которые являются дорогостоящими для производства, но стабильными с течением времени.
  • Конечные точки, чувствительные к задержкам. Операции, выполняемые пользователем, при которых чтение в памяти (под миллисекунда) предпочтительнее круговой передачи базы данных.
  • Прогнозируемые пики. Сезонный или управляемый событиями трафик, в котором кэширование поглощает нагрузку, которая в противном случае приведет к масштабированию вычислительных ресурсов.
  • Чувствительность обслуживания и переключения при отказе Обрабатывайте запросы на чтение, требующие стабильного времени отклика, пока экземпляр PostgreSQL восстанавливается или прогревает свой кэш после обслуживания или переключения при отказе.

Кэширование менее эффективно для нагрузок с интенсивной записью, данных, которые всегда должны быть транзакционно согласованными, или запросов, которые и так выполняются быстро и редко повторяются.

Кэширование шаблонов

В этой статье в качестве примера используется розничный магазин. Различные части приложения получают преимущества от различных шаблонов кэширования. В следующих разделах реализованы первые четыре шаблона в Python. В этой статье описываются выгрузка сеансов и состояния, а также шаблоны кэширования для нескольких регионов, но не содержатся их реализации в виде кода.

Рисунок Принцип работы На витрине магазина
cache-aside (ленивая загрузка) Приложение сначала проверяет кэш. Если в кэше нет данных, он читает их из PostgreSQL, а затем записывает в кэш. Страницы каталога товаров и страницы сведений о товаре, где небольшое число популярных товаров обеспечивает большую часть просмотров.
Предварительная выборка ссылочных данных Стабильные данные заранее загружаются в кэш и обновляются при изменении источника, а не при промахе кэша. Категории, бренды и конфигурация доставки.
Сквозная запись Приложение записывает данные в кэш и PostgreSQL в рамках одной операции, сохраняя их согласованность. Обновления цен и складских остатков, которые должны отображаться немедленно.
Событийно-управляемая инвалидация Записи кэша обновляются или инвалидируются в ответ на события изменения данных, а не по таймеру. Статус заказа по мере его выполнения.
Выгрузка сеанса и состояния Временное состояние находится в кэше вместо базы данных. Корзины покупок и сеансы пользователей.
Мультирегиональное кэширование Кэш в каждом регионе обслуживает локальные операции чтения, которые синхронизируются с активной георепликацией. Глобальный магазин, обслуживающий покупателей в нескольких регионах.

Необходимые условия

Tip

Полная развернутая версия этого примера, включая инфраструктуру в виде кода и всех четырех шаблонов, см. в репозитории amr-caching-pattern-samples на GitHub.

Шаг 1. Установка клиентских библиотек

Установите клиентские библиотеки Redis и PostgreSQL вместе с библиотекой удостоверений Azure для проверки подлинности Microsoft Entra.

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

Шаг 2. Подключение к Microsoft Entra ID

Используйте Microsoft Entra ID проверку подлинности вместо ключей доступа. Microsoft Entra ID удаляет необходимость хранения секретов в приложении и позволяет централизованно управлять доступом.

Следующий код создаёт клиент Redis и подключение к PostgreSQL, использующие проверку подлинности с помощью управляемого удостоверения или учетных данных разработчика через DefaultAzureCredential. В этом примере используется клиент RedisCluster с поддержкой кластеров, который соответствует политике кластеризации OSS, используемой в этом примере. Если кэш использует политику кластеризации Enterprise, используйте вместо этого стандартный redis.Redis клиент.

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

Срок действия маркеров доступа Microsoft Entra обычно истекает примерно через час. Для длительных приложений обновите маркер до истечения срока действия и повторно подключитесь как для Azure Managed Redis, так и для База данных Azure для PostgreSQL или используйте вспомогательный компонент, который прозрачно повторно запрашивает маркеры. Дополнительные сведения см. в разделе "Использование Microsoft Entra ID для проверки подлинности с помощью Azure Managed Redis".

Шаг 3: Стратегия cache-aside

Паттерн cache-aside — наиболее распространённый, и в этом примере он используется при чтении данных о продуктах. Приложение сначала проверяет Redis, и, если в кэше данных нет, обращается к PostgreSQL и заполняет кэш, задавая время жизни (TTL). Несколько популярных продуктов обеспечивают большую часть операций чтения, поэтому частота попаданий высока.

Поскольку большинство операций чтения выполняется из памяти, стратегия cache-aside снимает с PostgreSQL устойчивую нагрузку на чтение. Это сокращение означает меньшее число подключений, меньшую интенсивность обновления буферного кэша, а также более низкие нагрузку на ЦП и IOPS. Вы можете справляться с пиковыми нагрузками на чтение, не масштабируя сервер и не добавляя реплики для чтения. Запрос к PostgreSQL выполняется только при промахе кэша (при первом обращении или после истечения TTL). См. раздел «Что кэшировать», чтобы определить запросы, которые стоит кэшировать.

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

Шаг 4: Предзагрузка справочных данных

Стабильным данным, которые постоянно читаются, но редко изменяются (например, категории, бренды или конфигурация доставки), не нужно ждать промаха кэша. Загрузите его в кэш впереди и обновите его при изменении источника. В схеме PostgreSQL это обычно небольшие справочные таблицы и таблицы измерений, которые подключаются ко многим запросам через JOIN. Их обслуживание из памяти избавляет от большого количества повторяющихся JOIN-операций и поисковых запросов к базе данных. В отличие от кэша, нет промаха по запросу и нет гонки TTL. Вы обновляете изменения, поэтому операции чтения всегда теплы.

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 []

Шаг 5. Запись

Когда изменение должно быть видно немедленно, записывайте данные в PostgreSQL и в кэш в рамках одной операции, вместо того чтобы ждать истечения TTL или инвалидировать ключ. Например, этот шаблон используется для обновления сведений о ценах в примере. PostgreSQL остается источником истины. Сначала изменения фиксируются, а затем кэш обновляется, поэтому чтение после записи возвращает новое значение.

При записи в две системы, которые не участвуют в одной транзакции, подтверждение транзакции в базе данных может пройти успешно, а обновление кэша — завершиться сбоем, и простого решения здесь нет. Следующий фрагмент кода показывает основной сценарий и не рассматривает обработку ошибок. В рабочей среде вам нужно решить, как поступать со сбоем обновления, например повторить попытку, если сбой выглядит временным, или инвалидировать ключ, чтобы при следующем чтении данные заново загрузились из PostgreSQL. В любом случае PostgreSQL содержит правильное значение, поэтому устаревшие или отсутствующие записи кэша всегда можно восстановить. Если обновление кэша должно надежно приземлиться, перенесите его из потока изменений базы данных (см. недопустимое изменение на основе событий).

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)

Шаг 6: Инвалидация на основе событий

Недопустимость на основе событий обеспечивает согласованность кэша с базой данных путем реагирования на события изменения данных. Он обновляет или отменяет записи по мере их изменения вместо истечения срока действия таймера. Процессы записи добавляют события в устойчивый поток Redis — журнал, в который данные можно только дописывать, — а один или несколько потребителей считывают эти события и обновляют кэш. Поскольку поток является постоянным, события не теряются при перезапуске потребителя. Группа потребителей передаёт каждое событие одному обработчику, отслеживает подтверждения, чтобы события не терялись и не обрабатывались повторно, и позволяет масштабировать обработку между несколькими обработчиками.

Используйте этот шаблон, когда кэшированное значение вычисляется на основе данных, которые изменяются в другом месте системы (например, состояние, проекция или агрегат), и при этом TTL либо будет приводить к выдаче устаревших данных, либо вынуждать к постоянному пересчёту. На витрине магазина этот шаблон определяет статус заказа по мере его обработки. Размещение заказа записывает заказ в PostgreSQL, кэширует его начальное состояние и добавляет 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

Обработчик исполнения обслуживает группу потребителей: он считывает новые события, переводит каждый заказ на следующий этап в PostgreSQL, обновляет кэшированную проекцию order:{id}:status и подтверждает получение события. Страница заказа считывает эту проекцию, поэтому проверки состояния остаются быстрыми и никогда не касаются базы данных. Значение остается правильным, так как события сохраняют его в текущем состоянии.

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)

Источник событий в этом примере — это приложение, которое записывает PostgreSQL и добавляет событие в тот же путь. PostgreSQL также может выдавать изменения: LISTEN/NOTIFY для упрощенных уведомлений или логического декодирования (отслеживания измененных данных) для устойчивого потока изменений на уровне строк. Построение кэша на основе собственного потока изменений PostgreSQL означает, что кэш реагирует на каждое зафиксированное изменение, даже на записи, которые проходят в обход приложения.

Лучшие практики

Следуйте этим рекомендациям, чтобы сохранить кэш правильным, эффективным и экономичным.

  • Задавайте TTL везде, где есть риск устаревания данных. TTL ограничивает устаревание данных, если инвалидация не срабатывает. Сопоставьте TTL с максимальной степенью устаревания данных, которую допускает приложение. Используйте длительный TTL или не задавайте TTL для справочных данных только при наличии надежных процессов инвалидации и обновления.
  • Используйте согласованную схему именования ключей. Ключи пространства имен по службам, сущностям и идентификаторам, таким как product:42 или user:1001:profile. Добавьте версию, когда может измениться формат ключа или схема значений.
  • Кэшируйте правильную детализацию. Кэшируйте отдельные сущности или небольшие результирующие наборы, когда они часто используются и легко недопустимы. Не кэшируйте данные, которые не используются повторно. Чрезмерное кэширование приводит к неэффективному расходованию памяти и может снизить коэффициент попаданий.
  • Корректно обрабатывайте промахи кэша и сбои. Рассматривайте кэш как оптимизацию, а не источник истины. Если Redis недоступен, используйте ограниченный резервный вариант с PostgreSQL. Добавьте тайм-ауты, прерыватели сбоев, backoff и ограничения на запросы для защиты PostgreSQL. Если в работе PostgreSQL происходит кратковременный сбой, вы можете продолжать обрабатывать запросы на чтение для данных, уже находящихся в кэше, в то время как операции записи будут ждать восстановления работы базы данных.
  • Предотвращайте лавинообразную нагрузку на кэш. Когда срок действия популярного ключа истекает, многие запросы могут одновременно попасть в базу данных. Используйте джиттер TTL, объединение запросов, режим stale-while-revalidate или короткую распределенную блокировку, чтобы только один запрос заново заполнил этот элемент кэша.
  • Подберите оптимальный размер кэша. Отслеживайте скорость попадания, использование памяти, скорость вытеснения, скорость истечения срока действия, задержку, горячие ключи и кратность ключей. Низкая скорость попадания может указывать на кэш, который слишком мал, чрезмерные вытеснения, плохой выбор ключа или шаблон плохого доступа. Рекомендации по выбору уровня см. в рекомендациях по выбору уровня Azure Managed Redis.
  • Выберите политику вытеснения, подходящую для ваших ключей. Для базы данных, доступной только для кэша, начните с allkeys-lru или allkeys-lfu. Используйте политику переменных*, только если та же база данных содержит ключи кэша с истекающим сроком действия и защищенные ключи без истечения срока действия. Переменная политика может прекратить вытеснение, если ключи не имеют TTL. По возможности отделите данные кэша от защищенных данных.
  • Эффективно выполняйте сериализацию. JSON можно читать и переносить. Для путей с высокой пропускной способностью проверьте компактный двоичный формат, чтобы уменьшить нагрузку на память и сеть. Прежде чем менять формат, оцените использование памяти, загрузку процессора, задержку, эволюцию схемы и влияние на отладку.

Определите, что кэшировать

Наиболее эффективными целевыми объектами кэша являются запросы, которые приложение выполняет чаще всего с данными, которые изменяются наименее. Вместо того чтобы угадать, какие запросы соответствуют этому описанию, сравните исторические и текущие представления в телеметрии запроса, которые База данных Azure для PostgreSQL могут собираться при включении соответствующих функций:

  • хранилище запросов сохраняет статистику выполнения запросов для исторического анализа. Используйте количество вызовов и общее и среднее время выполнения, чтобы найти запросы, которые постоянно доминируют в нагрузке базы данных в течение более длительных периодов. См. статью "Мониторинг производительности с помощью хранилище запросов".
  • Аналитика производительности запросов визуализирует данные хранилище запросов на портале Azure, чтобы вы могли обнаружить частые и ресурсоемкие запросы и сравнить их поведение с течением времени. См. аналитические сведения о производительности запросов.
  • pg_stat_statements предоставляет совокупную статистику для каждой инструкции в базе данных для прямого просмотра текущего окна наблюдения. Его статистику можно сбросить, поэтому используйте хранилище запросов, если вам нужно сохранить историю между периодами наблюдения.

Используйте оба представления, прежде чем выбрать кандидат для кэширования. Краткосрочный пик может не представлять нормальное поведение рабочей нагрузки, в то время как исторический средний может скрыть текущую регрессию. Отдавайте приоритет запросам, которые часто выполняются, являются ресурсоёмкими и дают стабильные результаты, то есть имеют большое число вызовов, большое суммарное время выполнения, а их результаты не меняются при каждом запросе. Эти запросы обеспечивают наибольший коэффициент попаданий в кэш и наибольшее снижение нагрузки на базу данных. Запрос, который выполняется постоянно, но возвращает те же строки в течение нескольких минут, например список продуктов, дерево категорий или таблицу цен, является идеальным кандидатом.