Azure Database for PostgreSQL 유연한 서버에서 애플리케이션을 빌드할 때 캐싱 계층을 추가하는 것은 응답 시간을 개선하고 데이터베이스의 부하를 줄이며 복원력을 높이는 가장 효과적인 방법 중 하나입니다. 애플리케이션은 메모리 내 캐시에서 자주 읽는 데이터를 제공하여 PostgreSQL에 더 적은 쿼리를 보냅니다. 즉, CPU 및 IOPS 사용량이 감소하므로 더 작은 컴퓨팅 계층에서 실행하고, 서버를 확장하지 않고 읽기 크기를 조정하고, 트래픽 급증을 흡수할 수 있습니다. 캐시는 복원력을 추가할 수도 있습니다. PostgreSQL에 짧은 중단이 있는 경우 캐시에 이미 있는 데이터를 적중하는 요청은 계속 성공할 수 있으므로 데이터베이스가 복구되는 동안 읽기 경로를 계속 사용할 수 있습니다.
이 문서는 캐싱이 도움이 되는 시기와 애플리케이션에 맞는 패턴을 결정하는 데 도움이 됩니다. 그런 다음 Azure Managed Redis, 라이브러리 및 Microsoft Entra ID 인증을 사용하여 Python 4개의 캐싱 패턴(캐시 배제, 참조 데이터 프리페치, redispsycopg 쓰기 및 이벤트 기반)을 구현합니다.
캐시를 추가해야 하는 경우
PostgreSQL은 이미 자주 액세스하는 데이터 페이지를 버퍼 캐시에 캐시하고 운영 체제의 파일 캐시를 활용합니다. 이러한 캐시를 사용하면 반복 액세스 속도가 빨라지지만 데이터베이스 서버에 할당된 메모리를 쿼리 실행 및 기타 프로세스와 공유합니다. 해당 콘텐츠도 일부 유지 관리 및 장애 조치(failover) 작업 후에 다시 워밍업되어야 합니다.
메모리가 더 많은 컴퓨팅 옵션으로 확장하여 더 많은 데이터베이스 캐시 용량을 얻을 수 있습니다. PostgreSQL 메모리 설정을 튜닝할 수도 있지만, 버퍼 캐시에 더 많은 메모리를 할당하면 쿼리 실행 및 운영 체제에 대한 메모리가 줄어듭니다. 메모리 부족 조건을 방지하기 위해 메모리 변경 내용을 신중하게 테스트합니다.
Azure Managed Redis는 이러한 네이티브 캐시를 보완합니다. 선택한 애플리케이션 데이터 및 쿼리 결과를 데이터베이스 서버 외부에 저장하고, 시간이 중요한 읽기 경로에 대한 대기 시간이 짧으며, PostgreSQL이 캐시를 복구하거나 웜하는 동안 캐시된 읽기를 계속 사용할 수 있습니다. 이러한 이점은 추가된 애플리케이션 논리 및 다른 서비스가 작동하도록 정당화할 때 사용합니다. PostgreSQL을 진실의 근원으로 대체하지는 않습니다.
Azure Managed Redis와 같은 외부 캐시를 추가하면 워크로드에 다음과 같은 특성이 있는 경우 가장 도움이 됩니다.
- 읽기 중심의 액세스 패턴. 동일한 행은 제품 카탈로그, 사용자 프로필, 구성 데이터 또는 참조 테이블과 같이 변경되는 것보다 훨씬 더 자주 읽습니다.
- 비용이 많이 들거나 반복되는 쿼리입니다. 생성 비용이 많이 들지만 짧은 시간 동안 안정적인 집계, 조인 또는 계산된 결과입니다.
- 지연 시간에 민감한 엔드포인트. 메모리 내 읽기(밀리초 미만)가 데이터베이스 왕복보다 바람직한 사용자 연결 작업입니다.
- 예측 가능한 급증. 캐싱이 부하를 흡수하여 그렇지 않으면 컴퓨팅 리소스를 확장해야 하는 계절적 또는 이벤트성 트래픽.
- 유지 관리 및 장애 조치 민감도 PostgreSQL 인스턴스가 유지 관리 또는 장애 조치 작업 후 복구 중이거나 캐시를 예열하는 동안에도 일관된 응답 시간이 필요한 읽기 작업 경로.
캐싱은 쓰기 집약적 워크로드, 항상 트랜잭션 수준의 일관성이 보장되어야 하는 데이터, 또는 이미 빠르고 거의 반복되지 않는 쿼리에는 효과가 덜합니다.
캐싱 패턴
이 문서에서는 소매 상점을 실행 예제로 사용합니다. 앱의 여러 부분에서 다양한 캐싱 패턴의 이점을 누릴 수 있습니다. 다음 섹션에서는 Python 처음 네 가지 패턴을 구현합니다. 이 문서에서는 세션 및 상태 오프로드 및 다중 지역 캐싱 패턴을 설명하지만 이를 위한 코드 구현은 제공하지 않습니다.
| 패턴 | 작동 방식 | 소매 매장에서 |
|---|---|---|
| 캐시 어사이드 (지연 로딩) | 애플리케이션은 먼저 캐시를 확인합니다. 캐시 미스가 발생하면 PostgreSQL에서 읽어 캐시에 채웁니다. | 제품 카탈로그 및 세부 정보 페이지- 인기 있는 몇 가지 항목이 대부분의 읽기를 구동합니다. |
| 참조 데이터 미리 가져오기 | 안정적인 데이터는 캐시에 미리 로드되며, 캐시 미스가 발생했을 때가 아니라 소스가 변경될 때 새로 고쳐집니다. | 범주, 브랜드 및 배송 구성. |
| 쓰기-통과 | 애플리케이션은 동일한 작업에서 캐시 및 PostgreSQL에 기록하여 일관성을 유지합니다. | 즉시 표시되어야 하는 가격 및 인벤토리 업데이트입니다. |
| 이벤트 기반 무효화 | 캐시 항목은 타이머 대신 데이터 변경 이벤트에 대한 응답으로 업데이트되거나 무효화됩니다. | 주문이 처리 이행 과정을 거치면서 변경되는 주문 상태입니다. |
| 세션 및 상태 오프로드 | 임시 상태는 데이터베이스 대신 캐시에 있습니다. | 쇼핑 카트 및 사용자 세션. |
| 다중 지역 캐싱 | 각 지역의 캐시는 활성 지역 복제와 동기화된 상태로 유지되는 로컬 읽기를 제공합니다. | 여러 지역의 쇼핑객에게 서비스를 제공하는 글로벌 상점. |
사전 요구 사항
- 활성 구독이 있는 Azure 계정. 무료로 계정을 만듭니다.
- Microsoft Entra 인증을 사용하도록 설정된 Azure Database for PostgreSQL 유연한 서버 인스턴스입니다. 만들려면 Azure Database for PostgreSQL 유연한 서버 만들기를 참조하세요.
- Azure Managed Redis 인스턴스. 만들려면 Azure Managed Redis 인스턴스 만들기를 참조하세요. 대기 시간을 최소화하기 위해 PostgreSQL 서버(및 프로덕션의 경우 동일한 가상 네트워크)와 동일한 지역에 만듭니다.
- 두 서비스 모두에서 개발자 또는 애플리케이션 ID에 대한 데이터 액세스: PostgreSQL 서버에서 Microsoft Entra 관리자 또는 역할, 그리고 캐시에서 Redis 액세스 정책 할당. Azure Database for PostgreSQL Microsoft Entra 인증을 참조하고 Azure Managed Redis 인증에 Microsoft Entra ID 사용합니다.
- Python 3.10 이상.
- Azure CLI. 설치하려면 Azure CLI 설치하는 방법을 참조하세요.
팁 (조언)
코드로서의 인프라 및 네 가지 패턴을 포함하여 이 샘플의 배포 가능한 전체 버전은 GitHub amr-caching-pattern-samples 리포지토리를 참조하세요.
1단계: 클라이언트 라이브러리 설치
Microsoft Entra 인증을 위해 Azure ID 라이브러리와 함께 Redis 및 PostgreSQL 클라이언트 라이브러리를 설치합니다.
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 애플리케이션에 비밀을 저장할 필요가 없으며 중앙에서 액세스를 관리할 수 있습니다.
다음 코드는 관리 ID 또는 개발자 자격 증명을 통해 DefaultAzureCredential인증된 Redis 클라이언트 및 PostgreSQL 연결을 만듭니다. 이 예제에서는 이 샘플에서 사용하는 OSS 클러스터링 정책과 일치하는 클러스터 인식 RedisCluster 클라이언트를 사용합니다. 캐시에서 엔터프라이즈 클러스터링 정책을 사용하는 경우 대신 표준 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",
)
메모
Microsoft Entra 액세스 토큰은 일반적으로 약 1시간 후에 만료됩니다. 장기 실행 애플리케이션의 경우 만료되기 전에 토큰을 새로 고치고 Azure Managed Redis 및 Azure Database for PostgreSQL 모두에 대해 다시 연결하거나 토큰을 투명하게 다시 가져오는 도우미를 사용합니다. 자세한 내용은 Azure Managed Redis 인증에 Microsoft Entra ID 사용을 참조하세요.
3단계: 캐시 배제
캐시 배제는 가장 일반적인 패턴이며 이 샘플의 제품 읽기에 사용됩니다. 애플리케이션은 Redis를 먼저 확인하고 누락 시 PostgreSQL을 쿼리하고 캐시를 TTL(Time-to-Live)으로 채웁니다. 인기 있는 몇 가지 제품은 대부분의 읽기를 구동하므로 적중률이 높습니다.
대부분의 읽기 요청이 메모리에서 처리되므로 캐시 어사이드는 PostgreSQL의 지속적인 읽기 부하를 덜어줍니다. 이렇게 하면 연결이 줄어들고 버퍼 캐시 변동이 줄어들며 CPU 및 IOPS가 줄어듭니다. 서버를 확장하거나 읽기 복제본을 추가하지 않고 읽기 급증을 흡수할 수 있습니다. 캐시 미스가 발생한 경우에만(첫 번째 액세스 시 또는 TTL이 만료된 후) PostgreSQL을 조회합니다. 캐싱할 가치가 있는 쿼리를 식별하려면 캐시할 항목 찾기 를 참조하세요.
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 스키마에서는 일반적으로 여러 쿼리에 조인되는 작은 조회 및 차원 테이블입니다. 메모리에서 처리하면 데이터베이스에서 많은 양의 반복 조인 및 조회가 제거됩니다. 캐시 배제와 달리 요청당 누락이 없고 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단계: 쓰기
변경 내용을 즉시 표시해야 하는 경우 TTL이 만료될 때까지 기다리거나 키를 무효화하는 대신 PostgreSQL 및 캐시를 동일한 작업에 작성합니다. 예를 들어 이 패턴은 샘플에서 가격 정보를 업데이트하는 데 사용됩니다. 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로 사용합니다. 시간 제한, 회로 차단기, 백오프 및 요청 제한을 추가하여 PostgreSQL을 보호합니다. PostgreSQL에 잠시 중단이 있는 경우 쓰기가 데이터베이스가 복구되기를 기다리는 동안 캐시에 이미 있는 데이터에 대한 읽기를 계속 처리할 수 있습니다.
- 캐시 스탬피드를 방지하세요. 인기 있는 키가 만료되면 많은 요청이 한 번에 데이터베이스에 적중할 수 있습니다. TTL 지터, 요청 병합, stale-while-revalidate 또는 짧은 분산 잠금을 사용하여 한 요청만 항목을 다시 채우게 합니다.
- 캐시 크기를 조정합니다. 적중률, 메모리 사용량, 제거 속도, 만료율, 대기 시간, 핫 키 및 키 카디널리티를 모니터링합니다. 적중률이 낮을 경우 캐시가 너무 작거나 과도한 제거, 잘못된 키 선택 또는 잘못된 액세스 패턴을 나타낼 수 있습니다. 크기 조정 지침은 Azure Managed Redis 계층 선택 지침을 참조하세요.
- 키에 맞는 제거 정책을 선택합니다. 캐시 전용 데이터베이스의 경우 allkeys-lru 또는 allkeys-lfu로 시작합니다. 동일한 데이터베이스에 만료 캐시 키와 보호된 만료되지 않는 키가 포함된 경우에만 volatile-* 정책을 사용합니다. 키에 TTL이 없는 경우 일시적 정책은 제거를 중지할 수 있습니다. 가능한 경우 캐시 데이터를 보호된 데이터와 분리합니다.
- 효율적으로 직렬화합니다. JSON은 읽을 수 있고 이식 가능합니다. 높은 처리량 경로의 경우 압축 이진 형식을 테스트하여 메모리 및 네트워크 오버헤드를 줄입니다. 형식을 변경하기 전에 메모리, CPU, 대기 시간, 스키마 진화 및 디버깅 영향을 벤치마킹합니다.
캐시할 내용 찾기
가장 효과적인 캐시 대상은 가장 적게 변경되는 데이터에 대해 애플리케이션이 가장 자주 실행하는 쿼리입니다. 이 설명에 맞는 쿼리를 추측하는 대신 관련 기능을 사용하도록 설정할 때 Azure Database for PostgreSQL 수집할 수 있는 쿼리 원격 분석의 기록 및 현재 뷰를 비교합니다.
- 쿼리 저장소 기록 분석을 위해 쿼리 실행 통계를 유지합니다. 호출 수와 총 및 평균 실행 시간을 사용하여 더 긴 기간 동안 데이터베이스 부하를 일관되게 지배하는 쿼리를 찾습니다. 쿼리 저장소 사용하여 성능 모니터링을 참조하세요.
- Query Performance Insight는 Azure 포털에서 쿼리 저장소 데이터를 시각화하므로 자주 사용되는 리소스 집약적 쿼리를 발견하고 시간에 따른 동작을 비교할 수 있습니다. Query Performance Insight를 참조하세요.
-
pg_stat_statements는 현재 관찰 창을 직접 보기 위해 데이터베이스 내의 누적 문별 통계를 노출합니다. 통계를 다시 설정할 수 있으므로 관찰 기간 동안 보존된 기록이 필요한 경우 쿼리 저장소 사용합니다.
캐시 후보를 선택하기 전에 두 보기를 모두 사용합니다. 단기 급증은 워크로드의 정상적인 동작을 나타내지 않을 수 있지만 기록 평균은 현재 회귀를 숨길 수 있습니다. 자주, 비용이 많이 들고, 안정적이며, 높은 호출 수, 높은 총 실행 시간 및 모든 요청에서 변경되지 않는 결과를 의미하는 쿼리의 우선 순위를 지정합니다. 이러한 쿼리는 가장 높은 캐시 적중률과 가장 큰 데이터베이스 로드 감소율을 제공합니다. 제품 목록, 범주 트리 또는 가격 책정 테이블과 같이 지속적으로 실행되지만 몇 분 동안 동일한 행을 반환하는 쿼리는 이상적인 후보입니다.