AI ํธ๋ํฝ ์คํ์ดํฌ ์์กด๊ธฐ: Redis Write-Buffer ํจํด
๋ฐ์ด๋ด AI ์์ด์ ํธ ๋ฐ์นญ์์ Postgres๋ฅผ row-level lock ์ง์ฅ์ผ๋ก๋ถํฐ ๊ตฌํ Redis List ํ๋์ ์ด์ผ๊ธฐ.
AI ํธ๋ํฝ ๊ธ์ฆ ์ ์ปค๋ฅ์
ํ ๊ณ ๊ฐ(Connection Pool Exhaustion)์ ํ๋ก๋์
๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กฐ์ฉํ ์ด์ธ์์
๋๋ค. ์ ์คํ ๊ฒฝ๊ณ ๋ฐ์ ์์ต๋๋ค. ์๋ฒฝ 3์์ CONNECTION_REFUSED ์๋ฌ์ ์ฐ์ ๋ฒฝ, ๋ถํ๋ Slack ์ฑ๋, ๊ทธ๋ฆฌ๊ณ row-level contention์ด ๋๋ฌด ์ฌํด ํฌ์ค์ฒดํฌ๋ง์ ํ์์์๋๋ PostgreSQL ์ธ์คํด์ค๋ฅผ ๋ง์ฃผํ๊ฒ ๋ฉ๋๋ค.
์ฐ๋ฆฌ๋ ์ด๊ฑธ ๋ผ์ ๋ฆฌ๊ฒ ๋ฐฐ์ ์ต๋๋ค.
Row-Level Lock์ ์์ธํ ๋ฌผ๋ฆฌํ: 2,000๊ฐ์ ๋์ AI ์์ด์ ํธ ์์ฒญ์ด ๊ฐ๊ฐ ๊ฐ์ PostgreSQL ํ ์ด๋ธ์
INSERT๋ฅผ ์๋ํ๋ฉด, ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋จ์ํ ๋๋ ค์ง๋ ๊ฒ ์๋๋๋ค โ ๋ฐ๋๋ฝ์ ๋น ์ง๋๋ค. ๊ฐ ํธ๋์ญ์ ์ row-level lock์ ์ก๊ณ , ์ปค๋ฅ์ ํ์ ๊ธฐ๋ค๋ฆฌ๋ฉฐ, ๋ฆฌ์์ค๋ฅผ ์ธ์ง๋ก ์ก์ต๋๋ค. 5,000 RPS์์ 100๊ฐ์ง๋ฆฌ ์ปค๋ฅ์ ํ์ ๋ ์ด์ ํ์ด ์๋๋๋ค. ์ฃผ์ฐจ์ฅ์ ๋๋ค.
๐ฅ ๋ฌธ์ : PostgreSQL์ Write Storm์ฉ์ผ๋ก ๋ง๋ค์ด์ง์ง ์์๋ค
AI ์์ด์ ํธ๊ฐ ๋ฐ์ด๋ด์ ํ๊ณ ๋ชจ๋ ์ถ๋ก ์ฝ๋ฐฑ์ด Postgres์ ์ง์ ์ฐ๊ธฐ๋ฅผ ์๋ํ ๋ ๋ฒ์ด์ง๋ ์ผ:
| ์งํ | ์ง์ ์ฐ๊ธฐ | Redis Write-Buffer |
|---|---|---|
| ์ต๋ ์ง์ RPS | ~500 | 5,000+ |
| p99 ๋ ์ดํด์ | 800ms โ ํ์์์ | 15ms (ํ๋ซ) |
| ์ปค๋ฅ์ ํ ์ฌ์ฉ๋ฅ | 100% (๊ณ ๊ฐ) | 8โ12% |
| 2,000 RPS ์๋ฌ์จ | 35โ78% | 0% |
| Row-level lock ๊ฒฝํฉ | ์น๋ช ์ | ์์ |
| ๋ฐ์ดํฐ ์ ์ค ์ํ | ๋์ (๊ฑฐ๋ถ๋ ์ฐ๊ธฐ) | ์ ๋ก (Redis ์์ํ) |
PostgreSQL์ MVCC ์์ง์ **์ผ๊ด์ฑ(Consistency)**์ ์ต์ ํ๋์ด ์์ง, **์ฒ๋ฆฌ๋(Throughput)**์ ์ต์ ํ๋ ๊ฒ ์๋๋๋ค. ์์ฒ ๊ฐ์ ๋์ INSERT ๋ฌธ์ ๋์ง๋ฉด, ๊ฐ ํธ๋์ญ์
์ด SELECT ... FOR UPDATE ๋๋ ์๋ฌต์ insert lock์ ํตํด row-level lock์ ํ๋ํฉ๋๋ค. ์ปค๋ฅ์
ํ์ด ๋ณ๋ชฉ์ด ๋๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ lock contention์ ์ฃฝ์์ ์์ฉ๋์ด์ ์ง์
ํฉ๋๋ค.
์ด๋ ๊ฒ ์๊ฐํ์ธ์: PostgreSQL์ **๊ธ๊ณ (The Vault)**์ ๋๋ค โ ๊ผผ๊ผผํ๊ฒ ์์ ํ๊ณ , ํธ๋์ญ์ ์ ์ผ๋ก ์๋ฒฝํ์ง๋ง, ๋ชจ๋ ๋ฐฉ๋ฌธ์์ ์ ๋ถ์ฆ์ ํ์ธํ๋ ๊ฒฝ๋น์์ด ์ ์๋ ๋ฌธ์ด ํ๋๋ฟ์ ๋๋ค. Redis๋ **๋น ๋ฅธ ์บ์ (The Fast Cashier)**์ ๋๋ค โ ์ฆ์ ์ฃผ๋ฌธ์ ๋ฐ๊ณ , ํฐ์ผ์ ์ ๊ณ , ๋ช ์ด๋ง๋ค ๊ธ๊ณ ์ ํฐ์ผ์ ์ผ๊ด ์ ๋ฌํฉ๋๋ค. ํ๋์๋ชน์ด ์ค๋ฉด, ์บ์ ๊ฐ ์ค์ ๊ณ์ ์์ง์ ๋๋ค. ๊ธ๊ณ ๋ ์์ง๊ฐ ์์๋์ง์กฐ์ฐจ ๋ชจ๋ฆ ๋๋ค.
๐งช ์ง์ ์ฒดํ: ํธ๋ํฝ ์์ง ์๋ฎฌ๋ ์ดํฐ
์ํคํ ์ฒ๋ฅผ ํ๊ณ ๋ค๊ธฐ ์ ์, ์ฐจ์ด๋ฅผ ์ง์ ๊ฒฝํํด ๋ณด์ธ์. RPS ์ฌ๋ผ์ด๋๋ฅผ 5,000๊น์ง ๋์ด์ฌ๋ฆฌ๊ณ Direct Writes vs. Redis Write-Buffer์ ์ฐจ์ด๋ฅผ ํ์ธํ์ธ์:
๐ก TIP "Direct to Postgres"๋ฅผ 1,000+ RPS์์ ๋จผ์ ์์ํ์ฌ ์ปค๋ฅ์ ํ์ด ํญ๋ฐํ๋ ๊ฒ์ ๊ด์ฐฐํ์ธ์. ๊ทธ ๋ค์ "Redis Write-Buffer"๋ก ์ ํํ๊ณ 5,000 RPS๊น์ง ์ฌ๋ ค๋ณด์ธ์ โ ๋ ์ดํด์๊ฐ ~15ms์์ ํ๋ซํ๊ฒ ์ ์ง๋๊ณ ์๋ฌ์จ์ด 0%๋ฅผ ์ ์งํ๋ ๊ฒ์ ํ์ธํ์ธ์.
๐ ์ํคํ ์ฒ: Redis Stream-to-Bulk ํ์ดํ๋ผ์ธ
ํต์ฌ ํจํด์ 3๋จ๊ณ ๋์ปคํ๋ง ํ์ดํ๋ผ์ธ์ ๋๋ค:
- Hot Path (์์ด์ ํธ โ Redis): ๋ชจ๋ ์ฐ๊ธฐ๋ Redis List์ ๋ํ ๋จ์ผ LPUSH. ์๋ธ๋ฐ๋ฆฌ์ด. Lock ์์.
- Worker Path (Redis โ Worker): GCP Cloud Run ์์ปค๊ฐ ๋ฃจํ์์ BRPOP ํธ์ถ, ๋ฉ์์ง ๋ฐฐ์น.
- Cold Path (Worker โ PostgreSQL): ์์ปค๊ฐ ๋ฐฐ์น๋น 200ํ์ผ๋ก ๋จ์ผ
INSERT ... VALUES์คํ.
ํต์ฌ ์ธ์ฌ์ดํธ: PostgreSQL์ ์คํ์ดํฌ๋ฅผ ์ ๋ ๋ณด์ง ๋ชปํฉ๋๋ค. ์ ์คํธ๋ฆผ ํธ๋ํฝ์ด 100 RPS์ด๋ 50,000 RPS์ด๋ ๊ด๊ณ์์ด ์์ ์ ์ด๊ณ ์์ธก ๊ฐ๋ฅํ ๋ฒํฌ ์ธ์ํธ ์คํธ๋ฆผ์ ์์ ํฉ๋๋ค. Redis List๊ฐ ์ถฉ๊ฒฉ ํก์๊ธฐ ์ญํ ์ ํฉ๋๋ค.
๐ Clean Code: Redis Stream Append ํจํด
ํ๋ก๋์: ์๋ธ๋ฐ๋ฆฌ์ด ์ฐ๊ธฐ ๊ฒฝ๋ก
import redis import json import time r = redis.Redis(host="redis-cluster.internal", port=6379, db=0) def buffer_event(event: dict) -> None: """ Redis List์ ์ด๋ฒคํธ ์ถ๊ฐ. ํธ์ถ๋น ~0.2ms. PostgreSQL์ ๋ํ ์ปค๋ฅ์ ํ ์๋ ฅ ์ ๋ก. """ payload = json.dumps({ **event, "buffered_at": time.time(), }) r.lpush("event_buffer", payload)
๋ชจ๋ AI ์์ด์ ํธ ์ฝ๋ฐฑ์ Postgres๋ฅผ ์ง์ ์น๋ ๋์ buffer_event()๋ฅผ ํธ์ถํฉ๋๋ค. LPUSH ์ฐ์ฐ์ O(1)์ด๋ฉฐ ~0.2ms์ ์๋ฃ๋ฉ๋๋ค โ ๋ถํ ์ํ์์ ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ์ํธ์ 15โ800ms ๋ฒ์์ ๋น๊ต๋ฉ๋๋ค.
์ปจ์๋จธ: ๋ฒํฌ ํ๋ฌ์ ์์ปค
import psycopg2 from psycopg2.extras import execute_values BATCH_SIZE = 200 POLL_TIMEOUT = 1 # seconds def flush_worker(): """ Redis์์ BRPOP ๋ฐฐ์น๋ฅผ ๊ฐ์ ธ์ PostgreSQL์ ๋ฒํฌ ์ธ์ํธํ๋ ์ฅ๊ธฐ ์คํ ์์ปค. """ pg = psycopg2.connect(dsn="postgresql://...") batch = [] while True: result = r.brpop("event_buffer", timeout=POLL_TIMEOUT) if result: _, raw = result batch.append(json.loads(raw)) if len(batch) >= BATCH_SIZE or (batch and not result): with pg.cursor() as cur: execute_values( cur, """ INSERT INTO events (user_id, event_type, payload, buffered_at) VALUES %s """, [ (e["user_id"], e["event_type"], json.dumps(e["payload"]), e["buffered_at"]) for e in batch ], ) pg.commit() batch.clear()
BRPOP ํธ์ถ์ ๋ฐ์ดํฐ๊ฐ ์ฌ์ฉ ๊ฐ๋ฅํด์ง ๋๊น์ง ๋ธ๋กํนํ ํ, ์์ปค๊ฐ ์ต๋ 200๊ฐ ์ด๋ฒคํธ๋ฅผ ๋์ ํ๊ณ ํ๋ฌ์ํฉ๋๋ค. 200ํ์ ๋จ์ผ execute_values ํธ์ถ์ 200๊ฐ ๊ฐ๋ณ INSERT ๋ฌธ๋ณด๋ค ๊ทน์ ์ผ๋ก ๋น ๋ฆ
๋๋ค โ PostgreSQL์ด 200๊ฐ๊ฐ ์๋ ํ๋์ ํธ๋์ญ์
lock๋ง ํ๋ํฉ๋๋ค.
๐ ๋น๊ต: Before vs. After
| ์ฐจ์ | Before: ์ง์ DB ์ฐ๊ธฐ | After: Redis Write-Buffer |
|---|---|---|
| ์ฐ๊ธฐ ๋ ์ดํด์ (p99) | 800ms โ 5,000ms (ํ์์์) | 0.2ms (Redis) + 15ms (๋ฐฐ์น ํ๋ฌ์) |
| Postgres ์ปค๋ฅ์ | 100/100 (ํฌํ) | 8โ12/100 (์์ ) |
| Row-level lock ๊ฒฝํฉ | ์ฐ์ ๋ฐ๋๋ฝ | ์ ๋ก โ ๋จ์ผ ๋ฒํฌ ํธ๋์ญ์ |
| 5,000 RPS ์๋ฌ์จ | 78%+ CONNECTION_REFUSED | 0% |
| ๋ฐ์ดํฐ ๋ด๊ตฌ์ฑ | ์ ์ค๋ ์ฐ๊ธฐ (๊ฑฐ๋ถ๋จ) | Redis AOF + ๋ฐฐ์น ํ์ธ |
| ์ํ ํ์ฅ | Postgres ๋ ํ๋ฆฌ์นด ์ถ๊ฐ ($$$) | Cloud Run ์์ปค ์ถ๊ฐ ($) |
๐ ๋ด๊ตฌ์ฑ ๋ณด์ฅ
ํํ ๋ฐ๋ก : "ํ์ง๋ง Redis๋ ์ธ๋ฉ๋ชจ๋ฆฌ์์์. ํฌ๋์ํ๋ฉด?"
์ธ ๊ฐ์ง ๋ณดํธ ๊ณ์ธต:
-
Redis AOF Persistence:
appendonly yes์appendfsync everysec์ค์ ์ผ๋ก Redis๋ 1์ด ์ด๋ด์ ๋ชจ๋ ์ฐ๊ธฐ๋ฅผ ๋์คํฌ์ ์์ํํฉ๋๋ค. -
๋ฐฐ์น ํ์ธ: ์์ปค๋ PostgreSQL
COMMIT์ฑ๊ณต ํ์๋ง Redis์์ ์ด๋ฒคํธ๋ฅผ ์ ๊ฑฐํฉ๋๋ค. -
Dead Letter Queue: 3ํ ์ฌ์๋ ํ ์คํจํ ์ด๋ฒคํธ๋
event_buffer_dlqList๋ก ์ด๋ํ์ฌ ์๋ ๊ฒ์ฌ๋ฅผ ๊ธฐ๋ค๋ฆฝ๋๋ค.
MAX_RETRIES = 3 def safe_flush(batch: list) -> None: for attempt in range(MAX_RETRIES): try: bulk_insert(batch) return except psycopg2.OperationalError: time.sleep(2 ** attempt) # Dead Letter Queue โ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ์์ง ์๋๋ค for event in batch: r.lpush("event_buffer_dlq", json.dumps(event))
๐ง ๊ฒฐ๋ก : ๊ธ๊ณ ๋ฅผ ๋ณดํธํ๊ณ , ์บ์ ๋ฅผ ์ค์ผ์ผํ๋ผ
์ธํ๋ผ ์์ง๋์ด๋ง์์ ๊ฐ์ฅ ์ด๋ ค์ด ๊ตํ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ API๊ฐ ์๋๋ผ๋ ๊ฒ์ ๋๋ค. PostgreSQL์ ACID ํธ๋์ญ์ , ๋ณต์กํ ์ฟผ๋ฆฌ, ๊ด๊ณ์ ๋ฌด๊ฒฐ์ฑ์์ ํ์ํฉ๋๋ค. ํ์ง๋ง ์์ฒ ๊ฐ์ ๋์ AI ์์ด์ ํธ๋ฅผ ์ํ ๊ณ ์ฒ๋ฆฌ๋ ์ฐ๊ธฐ ์๋ํฌ์ธํธ๋ก ์ค๊ณ๋ ์ ์ ์์ต๋๋ค.
Redis Write-Buffer๊ฐ ์๋ ฅ์ ์ญ์ ์ํต๋๋ค:
- ํก์ โ O(1) LPUSH ์ฐ๊ธฐ๋ก ํธ๋ํฝ ์คํ์ดํฌ ํก์.
- ๋ฐฐ์น โ PostgreSQL์ด ํธ์ํ๊ฒ ์ํํ ์ ์๋ ์ฒญํฌ๋ก ์ด๋ฒคํธ ์ผ๊ด ์ฒ๋ฆฌ.
- ํ๋ฌ์ โ ๋จ์ผ ๋ฒํฌ ํธ๋์ญ์ ์ผ๋ก โ ํ๋์ lock, 200ํ, ๋.
- ์ค์ผ์ผ โ ๋น์ผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ํ๋ฆฌ์นด๊ฐ ์๋ ์ํ ์๋ ์์ปค ์ถ๊ฐ.
๊ฒฐ๊ณผ? ๋ฐ์นญ ์ ์ ๋ก ๋ค์ดํ์. ์ปค๋ฅ์ ํ ๊ณ ๊ฐ ์ ๋ก. ์ ์ค๋ ์ฐ๊ธฐ ์ ๋ก. Redis๊ฐ ํญํ์ ์ฒ๋ฆฌํ๋ ๋์ Postgres๋ 8% ์ปค๋ฅ์ ํ์ฉ๋ฅ ๋ก ํ์จํฉ๋๋ค.
AI ๊ธฐ๋ฐ ์ ํ์ ์ถ์ํ๋ ์ํฐํ๋ผ์ด์ฆ ํ์๊ฒ, ์ด๊ฒ์ ์ต์ ํ๊ฐ ์๋๋๋ค โ ์์กด ํจํด์ ๋๋ค.
์ต๊ณ ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํคํ ์ฒ๋ ๊ฐ์ฅ ๋น์ผ ์ปดํฌ๋ํธ๊ฐ ํธ๋ํฝ ์คํ์ดํฌ๊ฐ ์์๋์ง์กฐ์ฐจ ๋ชจ๋ฅด๋ ์ํคํ ์ฒ์ ๋๋ค.
Updated 5/9/2026