์บ์ ์ฑ๋ฅ ํฅ์๊ธฐ (Improving Cache Speed at Scale)
์บ์ ์ฑ๋ฅ ํฅ์๊ธฐ (Improving Cache Speed at Scale) ๊ด๋ จ
Improving Cache Speed at Scale
์๋ ํ์ธ์! ๋ฐ์ดํฐ์ด์ํ ๊น๊ฐ๋ฆผ์ ๋๋ค.๐๐ปโโ๏ธ ์ฌํด ๋ ๋์ค ์ปจํผ๋ฐ์ค๋ ์ฝ๋ก๋๋ก ์ธํด ์จ๋ผ์ธ์ผ๋ก ์งํ๋์์ต๋๋ค. ์ค๋์ ๊ทธ ์ค ์ ๊ฐ ๊ฐ์ฅ ํฅ๋ฏธ๋กญ๊ฒ ๋ค์๋ ์ธ์ ์ ๋ด์ฉ์ ๊ณต์ ํด๋ณด๋ ค๊ณ ํฉ๋๋ค. ์ ์ ๋ชฉ์ Improving Cache Speed at Scale ์ด๋ฉฐ, ์ธ์ ์์์ ์ ํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
Cache Stampede
๋ ๋์ค๋ฅผ ์บ์๋ก ์ฌ์ฉํ ๋ ๋ฐ์ดํฐ์ ๊ฐฑ์ ์ ์ํด ๋๋ถ๋ถ์ ์๋น์ค์์๋ ํค์ ๋ํด Expire time(TTL)
์ ์ค์ ํฉ๋๋ค. AWS๋ ElastiCache์ ์บ์ฑ ์ ๋ต ๋ฌธ์์์ ๋ฐ์ดํฐ๋ฅผ ์ต์ ์ํ๋ก ์ ์งํจ๊ณผ ๋์์ ๋ณต์ก์ฑ์ ์ค์ด๊ธฐ ์ํด TTL์ ์ถ๊ฐํ ๊ฒ์ ๊ถ์ฅํ๊ณ ์์ต๋๋ค.
ํ์ง๋ง ๋๊ท๋ชจ ํธ๋ํฝ ํ๊ฒฝ์์ ์ด TTL๊ฐ์ ์์์น ๋ชปํ ๋ฌธ์ ๋ฅผ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.
์ด ๊ตฌ์กฐ์์ ๋ ๋์ค๋ ์บ์๋ก, DB ์๋จ์์ ๋ถ์ฐ๋ ์๋ฒ๋ค์ ์์ฒญ์ ๋ฐ๊ณ ์์ต๋๋ค. ํค๊ฐ ๋ง๋ฃ๋๋ ์์ ์ ์๊ฐํด๋ณผ๊น์? read-thorugh ๊ตฌ์กฐ์์ ๋ ๋์ค์ ๋ฐ์ดํฐ๊ฐ ์์ ๋ ์๋ฒ๋ค์ ์ง์ DB์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ ๋ ๋์ค์ ์ ์ฅํฉ๋๋ค. ํค๊ฐ ๋ง๋ฃ๋๋ ์๊ฐ ๋ง์ ์๋ฒ์์ ์ด ํค๋ฅผ ์ฐธ์กฐํ๋ ์์ ์ด ๊ฒน์น๊ฒ ๋ฉ๋๋ค. ๋ชจ๋ ์๋ฒ๋ค์ด DB์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ง์ํ๋ duplicate read
์ ๊ทธ ๊ฐ์ ๋ฐ๋ณต์ ์ผ๋ก ๋ ๋์ค์ writeํ๋ duplicate write
๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
- ์ด๋ก์: ์ ์์ ์ธ ์๋ต
- ๋นจ๊ฐ์: ๋ ๋์ค Key miss
- ํ๋์: ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ง์
PER(Probablistic Early Recomputation)
์ด ํ์์ ํด๊ฒฐํ๊ธฐ ์ํด PER(Probablistic Early Recomputation) ์๊ณ ๋ฆฌ์ฆ์ ๋์ ํ ์ ์์ต๋๋ค. ์ด ์๊ณ ๋ฆฌ์ฆ์ ํค์ TTL์ด ์ค์ ๋ก ๋ง๋ฃ๋๊ธฐ ์ ์ ์ผ์ ํ๋ฅ ๋ก ์บ์๋ฅผ ๊ฐฑ์ ํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํค๊ฐ ์์ ํ ๋ง๋ฃ๋๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋จผ์ ์ฝ์ด์ค๊ฒ ํจ์ผ๋ก์จ Cache Stampede ํ์์ ๋ง์ ์ ์์ต๋๋ค.
def fetch_aot(key, expiry_gap_ms):
ttl_ms = redis.pttl(key) # pttl์ millisecond ๋จ์
if ttl_ms - (random() * expiry_gap_ms) > 0:
return redis.get(key)
return None
# Usage
fetch_aot('foo', 2000)
์ด ๋ฐฉ์์ ์๋ VLDB๋ผ๋ ๊ตญ์ ํ์ ๋ํ์์ ๋ฐํ๋ ๋ฐฉ๋ฒ์ด๋ฉฐ, ์ธํฐ๋ท์ ๊ด๋ จ ๋ ผ๋ฌธ์ด ๊ณต๊ฐ๋์ด ์์ต๋๋ค. ์ ์ด ํ๋ฅ ๋ถํฌ๊ฐ ์ฌ์ฉ๋๋์ง, beta๊ฐ์ ์ด๋ป๊ฒ ์ ํด์ผ ํ๋์ง ๋ฑ์ ๋ด์ฉ๋ ํฅ๋ฏธ๋ก์ฐ๋ ๊ด์ฌ์์ผ์ ๋ถ๋ค์ ๋ ผ๋ฌธ์ ์ฝ์ด๋ณด์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
Debouncing
We took inspiration from frontend world (debounce) and exploited promises(deferred)
ํน์, Cache Stampede ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ํ๋ก ํธ์๋-์๋๐ ์์ ๋๋ฐ์ด์ฑ์ด๋ผ๋ ์์ด๋์ด๋ฅผ ์ฑ์ฉํด ์ฌ ์ ์์ต๋๋ค.
โ ๋๋ฐ์ด์ฑ์ ๋จ์์๊ฐ ๋ด์์ ํธ์ถ๋๋ ๋ง์ง๋ง ํจ์๋ง ํธ์ถํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
์ธํฐ๋ฒ์ด 100ms๋ผ๋ฉด 100ms๋์์ ๋ชจ๋ ์ด๋ฒคํธ๋ ๋ฌด์๋๊ณ , ๋ง์ง๋ง ์ด๋ฒคํธ๋ง ๋์ํ๋ ๋ฐฉ๋ฒ์
๋๋ค. (์ถ์ฒ: FE๊ฐ๋ฐ๋ฉ ํ์ )
์ด ์์ด๋์ด๋ฅผ ๋์ ํ๋ฉด ์ดํ๋ฆฌ์ผ์ด์ ์์ ํน์ ํค miss๊ฐ ๋ฐ์ํด๋ ๋ฐ๋ก DB์ ์ง์ํ์ง ์์ต๋๋ค. ์ด ํค ID์ ๋ํด debouncer๋ฅผ ์์ฑํด์, ์ฒซ๋ฒ์งธ reader๊ฐ ์ด ํจ์๋ฅผ ๋ฐํํ ๋๊น์ง ๋ค๋ฅธ reader๋ค์ ๊ธฐ๋ค๋ฆฝ๋๋ค. ์ด ๋๋ฐ์ด์์ ์ฝ๋๋ ์๋์ ๊ฐ๊ณ , ์๋ฎฌ๋ ์ดํฐ ๋งํฌ์์ ์ด๋ค ์์ผ๋ก ๋์ํ๋์ง ํ์ธํ์ค ์ ์์ต๋๋ค.
const debouncer = new Debouncer();
async function menuItemLoader(key) {
//Read from Redis/DB
}
const menu = await debouncer.debounce(
'menu-${id}', menuItemLoader
);
class Debouncer {
construnctor() {
this.pendingBoard = {};
}
async debounce(id, callback) {
if(this.pendingBoard(id) !== undefined) {
return await this.pendingBorad(id);
}
this.pendingBoard(id) = callback(id);
try {
return await this.pendingBoard(id);
} finally {
delete this.pendingBoard(id);
}
}
}
Cache Stampede ํ์์ด ๋ฐ์ํ์ ๋์ Key Miss ๊ทธ๋ํ๋ ์์ฒ๋ผ Spiky nature ํฉ๋๋ค. (๐๋ญ๊ฐ๊ฐ expire ๋์ด์ everybody rushes into read ํ๋๋ด!) ๋ฐ๋ฉด์ ์๋ ๊ทธ๋ํ๋ ํธ๋ํฝ์ด ํจ์ฌ ๊ฐ์ํ์์ ์ ์ ์์ต๋๋ค. ์ธ๋ฐ์๋ set์ ์ค์ด๋ ์ ์ฒด์ ์ธ round trip time์ด ๊ฐ์๋๊ณ , latency๋ ์ค์ด๋ญ๋๋ค. ์ค์ ๋ก LINE์์ ์ฑ๋ฅํ ์คํธ๋ฅผ ํ์ ๋, ์ด ์๊ณ ๋ฆฌ์ฆ์ ๋์ ํ๋ฉด ์ฝ ์ธ๋ฐฐ ์ ๋์ ์๋ต ์๊ฐ ๊ฐ์ ์ด ์์๋ค๊ณ ํฉ๋๋ค.
Typical caching setup
ํธ๋ํฝ์ด ๋์ ์๋น์ค์์๋ ๋๋ถ๋ถ ์ด๋ฐ์์ผ๋ก ์บ์๋ฅผ ๊ตฌ์ฑํ๊ณ ์์ต๋๋ค. L1์ ์ดํ๋ฆฌ์ผ์ด์ ์บ์(ex. Ehcache), L2๋ ๋ ๋์ค๋ก ์๊ฐํ ์ ์๊ฒ ์ฃ ? ์์์ ๋งํ๋ ๋ ๋์ค์ DB์ฌ์ด์ Stampede ์ด์๋ L1์ L2์ฌ์ด์์๋ ๋ฐ๋ณต๋ ์ ์์ต๋๋ค.
Under high traffic load similar cache stampede/miss-storm can be observed between L1&L2 cache (and so on)
Hot Keys
ํ๋์ ํค์ ๋ํ ์ ๊ทผ์ด ๋๋ฌด ๋ง์ ๋์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉฐ, ์ด ํ์ ๋ํ ์บ์ ์ฑ๋ฅ์ ์ ํ์ํฌ ์ ์์ต๋๋ค.
Hot Key ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด, ๊ฐ์ฅ ์ฝ๊ฒ ์๊ฐ ํ ์ ์๋ ๋์์ ์ฝ๊ธฐ ๋ถ์ฐ์ ๋๋ค. ํ๋์ ๋ง์คํฐ์ ์ฌ๋ฌ๊ฐ์ ์ฌ๋ ์ด๋ธ๋ฅผ ์ถ๊ฐํ๊ณ , ์ดํ๋ฆฌ์ผ์ด์ ์์๋ ์ฌ๋ฌ๋์ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ค๋ ๋ฐฉ์์ ๋๋ค. ํ์ง๋ง ์ด๋ฐ ๊ตฌ์ฑ์์ ์ฅ์ ๊ฐ ๋ฐ์ํด์ ํ์ผ์ค๋ฒ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค๋ฉด ์ํฉ์ด ๋ณต์กํด์ง๋๋ค. ์๊ฐ์ง๋ ๋ชปํ ์ฅ์ ์ ๋ณ๋ชฉ์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์กด์ฌํฉ๋๋ค.
์ด ์ธ์ ์์๋ ํค์ ๋ณต์ ๋ณธ์ ๋ง๋๋ ๋ฐฉ์์ ์ ์ํ๊ณ ์์ต๋๋ค.
def write_keys(key, copies):
return ["{{copy{}}}-{}".format(i,key) for i in range(copies)]
def read_key(key, copies):
r = randrange(0, copies)
return "{{copy{}}}-{}".format(r, key)
hot key๋ฅผ ์ ์ฅํ ๋์๋ ์์ prefix๋ฅผ ๋ถํ์ ์ฌ๋ฌ๊ฐ์ ํค๋ฅผ ๋ง๋ค์ด๋ ๋๋ค. ํค๋ฅผ ์ฝ์ ๋์๋ ๊ทธ prefix๋ฅผ ์ฌ์ฉํด์ ๋๋คํ๊ฒ ์ ๊ทผํ๋ ๋ก์ง์ ์ถ๊ฐํฉ๋๋ค.
Compression
๋ ๋์ค์์ ๋จธ์ ๋ฌ๋ ๋ชจ๋ธ, HTTP ํ์ด์ง ๋ฑ์ ๋ค๋ฃจ๊ฑฐ๋, ๋ฉ์์ง ํ ๋ฑ์ผ๋ก ์ฌ์ฉํ๋ฉฐ ํฌ๊ธฐ์นด ํฐ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋์ ์บ์ ์ฑ๋ฅ ์ ํ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ์ด ๋์๋ ์์ถ์ ๊ณ ๋ คํ ์ ์์ต๋๋ค. ์์ถ์ ํ ๋์๋ ๋ค์ ์ธ ๊ฐ์ง ์ฌํญ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
์ฒซ ๋ฒ์งธ๋ ์ ์ ํ ์์ถ ๋น์จ(Compression Ratio) ์ ๋๋ค. ๋์ ์์ถ๋ฅ ์ด ์ค์ํ ๊ฒ์ด ์๋๋ผ, ์ ์ ํ ์์ถ๋ฅ ์ ์ฐพ์์ผ ํฉ๋๋ค. ์๋ํ๋ฉด ์์ถ์ ํ ๋์ CPU ์ฑ๋ฅ ์ ์๊ฐํด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ์์ ์ฑ์ ๋ฌผ๋ก ํ์์ ๋๋ค.
Cache Stampede๋ ์ค์ ๋ก ์ด๋ค ์ํฅ์ ๋ผ์น ๊น์
๊ธ์ ๋ง์น๊ธฐ ์ ์ ๊ฐ ๋ด๋นํ๊ณ ์๋ ํ ์๋น์ค์์ Cache Stampede ํ์์ด ๋ฐ์ํ ์ฌ๋ก๋ฅผ ๊ณต์ ๋๋ฆฌ๊ณ ์ ํฉ๋๋ค. ์ด ์๋น์ค์์๋ ๋ ๋์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก TTL๊ฐ์ 300์ด๋ก ์ ์ฅํ๊ณ ์์ผ๋ฉฐ, ์ด๋ ์ผ๋ฐ์ ์ธ ์ํฉ์์ ์๋ฌด๋ฐ ๋ฌธ์ ๊ฐ ๋์ง ์๊ณ ์์์ต๋๋ค. ํ์ง๋ง ํธ๋ํฝ์ด ๊ณผ๋ํ๊ฒ ๋ชฐ๋ฆฌ๋ ์ํฉ์์ ์บ์์๋ฒ์ ์์ฒญ๋ ๋ถํ๊ฐ ๋ฐ์ํ๊ณ ์ด๋ฅผ ๋ถ์ํ๋ ๊ณผ์ ์์ ์๋ ๋ก๊ทธ๋ฅผ ํ์ธํ ์ ์์์ต๋๋ค.
DB์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์จ ๋ค ๋ ๋์ค์ SET ํ๋ ๊ณผ์ ์์ ์์ฒญ๋๊ฒ ์ค๋ ์๊ฐ์ด ์์๋จ์ ๋ณผ ์ ์๊ณ , ๋ํ ์ด๋ฐ ํ๋ก์ธ์ค๊ฐ ํน์ ์๊ฐ๋์ ์ฌ๋ฌ ๊ฐ ๋ฐ๊ฒฌ๋์์ต๋๋ค. ์ด๋ ๋ถ๋ช ํ ๋ ๋์ค์ ์ฑ๋ฅ์ ์ ํ์ํจ๋ค๋ ๊ฒ์ ์์ํ ์ ์์ต๋๋ค.