แคช, CDN และการกระจายโหลด
เพิ่มชั้นแคช Redis ส่งทรัพยากรคงที่ไปยัง CDN และกระจายทราฟฟิกไปยังแบบจำลองด้วยตัวกระจายโหลดแบบวนรอบและแบบแฮชสอดคล้อง
แคช, CDN และการกระจายโหลด เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดการแคชจึงจำเป็นเมื่อระบบมีขนาดใหญ่
การแคชจัดเก็บสำเนาข้อมูลที่มีการเข้าถึงบ่อยไว้ในชั้นจัดเก็บข้อมูลที่เร็วกว่า เพื่อให้คำขอในอนาคตได้รับการตอบสนองโดยไม่ต้องเข้าถึงแหล่งจัดเก็บข้อมูลเบื้องหลังที่ช้ากว่า (ฐานข้อมูล, API ภายนอก) เมื่อระบบมีขนาดใหญ่ รายการยอดนิยมจำนวนไม่มากจะได้รับคำขอส่วนใหญ่เป็นจำนวนมาก — มักใช้ กฎ 80/20 (หลักการพาเรโต) อธิบายได้ว่า รายการ 20% คิดเป็นปริมาณการใช้งาน 80%
แคชที่เก็บรายการยอดนิยม 20% ไว้ในหน่วยความจำได้ จะรองรับภาระงานของฐานข้อมูล 80% นี่เป็นเหตุผลที่การเพิ่มแคช Redis มักลดการใช้ CPU ของฐานข้อมูลได้ 70–90% และลดความหน่วง p99 จาก 10ms เหลือต่ำกว่า 1ms สำหรับคำขอที่พบข้อมูลในแคช โดยแทบไม่ต้องเปลี่ยนแปลงฐานข้อมูลหรือตรรกะของแอปพลิเคชัน
# Demonstrating the 80/20 caching benefit
import random
# Simulate 1000 requests to 100 items with Zipf-like distribution
def zipf_sample(n_items, n_requests):
access_counts = {}
weights = [1.0 / (i + 1) for i in range(n_items)] # Zipf: item 0 most popular
total = sum(weights)
probs = [w / total for w in weights]
for _ in range(n_requests):
item = random.choices(range(n_items), weights=probs)[0]
access_counts[item] = access_counts.get(item, 0) + 1
return access_counts
random.seed(42)
counts = zipf_sample(100, 10000)
top_20_items = sorted(counts, key=counts.get, reverse=True)[:20]
top_20_requests = sum(counts[i] for i in top_20_items)
print(f'Top 20% of items ({20} of 100) handle {top_20_requests/100:.1f}% of requests')รูปแบบแคชแบบแยกจากแหล่งข้อมูล (การโหลดเมื่อจำเป็น)
รูปแบบ แคชแบบแยกจากแหล่งข้อมูล (เรียกอีกอย่างว่าการโหลดเมื่อจำเป็น) เป็นกลยุทธ์การทำแคชที่ใช้กันมากที่สุด โค้ดของแอปพลิเคชันมีหน้าที่จัดการแคช: เมื่ออ่านข้อมูล ให้ตรวจสอบแคชก่อน หาก พบข้อมูลในแคช ให้ส่งคืนทันที หาก ไม่พบข้อมูลในแคช ให้ดึงข้อมูลจากฐานข้อมูล เขียนข้อมูลลงแคช แล้วจึงส่งคืน เมื่อเขียนข้อมูล ให้อัปเดตฐานข้อมูลและทำให้รายการแคชใช้ไม่ได้ (delete) เพื่อให้การอ่านครั้งถัดไปโหลดข้อมูลใหม่
รูปแบบนี้ทำให้แคชเก็บเฉพาะข้อมูลที่มีการร้องขอจริง (ไม่โหลดข้อมูลล่วงหน้าโดยไม่จำเป็น) และยังคงสอดคล้องกับฐานข้อมูลผ่านการทำให้รายการแคชใช้ไม่ได้ ข้อแลกเปลี่ยนคือ การเข้าถึงครั้งแรกหลังจากไม่พบข้อมูลในแคชต้องเสียค่าใช้จ่ายในการเข้าถึงฐานข้อมูลเต็มจำนวน (การเริ่มต้นแบบเย็น)
# Cache-aside pattern in Python
class CacheAsideService:
def __init__(self, db, cache):
self.db = db
self.cache = cache # e.g., Redis client
def get_user(self, user_id):
cache_key = f'user:{user_id}'
# 1. Check cache
cached = self.cache.get(cache_key)
if cached:
return cached # cache hit
# 2. Cache miss: fetch from DB
user = self.db.query('SELECT * FROM users WHERE id=%s', user_id)
# 3. Write to cache with TTL
self.cache.set(cache_key, user, ttl=3600) # 1 hour TTL
return user
def update_user(self, user_id, data):
# 1. Write to DB
self.db.execute('UPDATE users SET ... WHERE id=%s', user_id, data)
# 2. Invalidate cache (delete, not update)
self.cache.delete(f'user:{user_id}')
# Next read will re-populate cache from DB
print('Cache-aside: READ from cache, miss? load from DB + write cache')
print(' WRITE to DB, then DELETE from cache (invalidate)')การแคชแบบเขียนผ่านและเขียนเบื้องหลัง
เขียนผ่าน: ในการเขียนทุกครั้ง ให้อัปเดตทั้งฐานข้อมูลและแคชพร้อมกัน แคชจะมีข้อมูลล่าสุดอยู่เสมอ ข้อแลกเปลี่ยนคือ การเขียนจะช้าลง (ต้องดำเนินการสองครั้ง) และแคชจะเต็มไปด้วยข้อมูลที่อาจไม่มีการอ่านอีกเลย
เขียนเบื้องหลัง (เขียนย้อนกลับ): เมื่อเขียนข้อมูล ให้อัปเดตเฉพาะแคช แล้วส่งข้อมูลลงฐานข้อมูลแบบอะซิงโครนัสในภายหลัง วิธีนี้ทำให้การเขียนรวดเร็วมาก แต่เสี่ยงต่อการสูญหายของข้อมูลหากแคชล้มเหลวก่อนส่งข้อมูลลงฐานข้อมูล เหมาะกับปริมาณงานที่เน้นการเขียนและยอมรับการสูญหายของข้อมูลบางส่วนได้ (เช่น ตัวนับจำนวนการดูและการวิเคราะห์ข้อมูล)
# Write-through vs Write-behind comparison
strategies = {
'Cache-aside (Lazy)': {
'read': 'Check cache; miss => DB + populate cache',
'write': 'Write DB; delete from cache (invalidate)',
'consistency': 'Strong (invalidation ensures freshness)',
'write_latency': 'Fast (one DB write)',
'risk': 'Cache stampede on popular key expiry',
},
'Write-through': {
'read': 'Always check cache; miss => DB',
'write': 'Write DB AND cache atomically',
'consistency': 'Strong (cache always has latest)',
'write_latency': 'Slower (two writes per operation)',
'risk': 'Cache polluted with rarely-read data',
},
'Write-behind': {
'read': 'Check cache; miss => DB',
'write': 'Write cache only; async flush to DB',
'consistency': 'Eventual (flush may be delayed)',
'write_latency': 'Very fast (cache write only)',
'risk': 'Data loss if cache crashes before flush',
},
}
for name, info in strategies.items():
print(f'\n{name}:')
for k, v in info.items(): print(f' {k}: {v}')นโยบายการนำข้อมูลออกจากแคช
เมื่อแคชเต็ม นโยบายการนำข้อมูลออกจะเป็นตัวตัดสินใจว่าควรนำรายการใดออก นโยบายที่ใช้กันมากที่สุดมีดังนี้:
- LRU (ใช้งานล่าสุดน้อยที่สุด): นำรายการที่ไม่ได้เข้าถึงมาเป็นเวลานานที่สุดออก เหมาะกับปริมาณงานที่ข้อมูลที่เพิ่งเข้าถึงมีแนวโน้มถูกเข้าถึงซ้ำ Redis ใช้นโยบายนี้เป็นค่าเริ่มต้น
- LFU (ใช้งานน้อยครั้งที่สุด): นำรายการที่ถูกเข้าถึงน้อยครั้งที่สุดออก เหมาะกว่ากับปริมาณงานที่มีบางรายการได้รับความนิยมอย่างถาวร แต่ LRU อาจตรวจจับลักษณะนี้ไม่ได้
- FIFO (เข้าก่อนออกก่อน): นำรายการที่ถูกเพิ่มเข้ามาก่อนที่สุดออก เป็นวิธีที่เรียบง่าย แต่มีประสิทธิภาพต่ำสำหรับปริมาณงานเว็บทั่วไป
- สุ่ม: นำรายการแบบสุ่มออก ในทางปฏิบัติ วิธีนี้ให้ผลที่แข่งขันกับ LRU ได้อย่างน่าประหลาดใจเมื่อแคชมีขนาดใหญ่มาก
# Implementing LRU cache
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict() # maintains insertion/access order
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict LRU (oldest)
cache = LRUCache(3)
for k, v in [('a',1),('b',2),('c',3)]:
cache.put(k, v)
print('Get a:', cache.get('a')) # 1 (a now most recently used)
cache.put('d', 4) # evicts 'b' (LRU)
print('Get b:', cache.get('b')) # -1 (evicted)
print('Get c:', cache.get('c')) # 3เครือข่ายส่งมอบเนื้อหา (CDN)
CDN คือเครือข่ายเซิร์ฟเวอร์ที่กระจายอยู่ตามพื้นที่ทางภูมิศาสตร์ ซึ่งเรียกว่า เซิร์ฟเวอร์ขอบเครือข่าย (จุดให้บริการ, PoPs) ทำหน้าที่แคชเนื้อหาแบบคงที่และแบบไดนามิกไว้ใกล้ผู้ใช้ปลายทาง แทนที่จะให้คำขอของผู้ใช้ทุกคนเดินทางไปยังเซิร์ฟเวอร์ต้นทางในศูนย์ข้อมูลแห่งเดียว โหนดขอบเครือข่ายของ CDN จะส่งเนื้อหาจาก PoP ที่ใกล้ที่สุด ซึ่งช่วยลดเวลาแฝงจากประมาณ 200 มิลลิวินาที (ข้ามทวีป) เหลือประมาณ 5 มิลลิวินาที (PoP ที่อยู่ใกล้เคียง)
CDN มีความสำคัญสำหรับทรัพยากร assets แบบคงที่ (รูปภาพ, CSS, JS) การสตรีมวิดีโอ (ส่วนของ HLS) และคำตอบจากส่วนติดต่อโปรแกรมประยุกต์กับ HTML ที่สร้างจากเซิร์ฟเวอร์มากขึ้นเรื่อย ๆ CDN จะตรวจสอบแคชที่ขอบเครือข่าย หากไม่พบข้อมูลในแคช ก็จะดึงข้อมูลจากต้นทางและแคชไว้สำหรับคำขอในอนาคต
# CDN architecture flow
cdn_flow = [
'User requests https://example.com/image.jpg',
'DNS resolves to the nearest CDN PoP (e.g., Frankfurt for EU users)',
'CDN edge checks its local cache:',
' HIT: Return cached image directly (5ms latency)',
' MISS: Fetch from origin server (e.g., AWS S3 in us-east-1)',
' Cache image at edge with Cache-Control: max-age=86400',
' Future requests for this image served from edge (HIT)',
'Cache-Control headers control CDN behaviour:',
' max-age=31536000 s-maxage=31536000 -- cache 1 year',
' no-cache -- always revalidate',
' private -- CDN must not cache (user-specific)',
]
for step in cdn_flow:
print(step)
print('\nCDN providers: Cloudflare, AWS CloudFront, Fastly, Akamai')การกระจายโหลด: การกระจายปริมาณการรับส่งข้อมูล
ตัวจัดสรรโหลด จะกระจายคำขอขาเข้าไปยังเซิร์ฟเวอร์แบ็กเอนด์หลายเครื่อง เพื่อป้องกันไม่ให้เซิร์ฟเวอร์เครื่องใดเครื่องหนึ่งกลายเป็นคอขวด นอกจากนี้ยังช่วยให้เกิด ความพร้อมใช้งานสูง หากเซิร์ฟเวอร์เครื่องหนึ่งขัดข้อง ตัวจัดสรรโหลดจะกำหนดเส้นทางปริมาณการรับส่งข้อมูลไปยังเซิร์ฟเวอร์ที่ยังทำงานได้โดยอัตโนมัติ (ตรวจสอบสถานะทุก 5–30 วินาที)
ตัวจัดสรรโหลดทำงานได้ใน OSI หลายชั้น ได้แก่ ชั้นที่ 4 (การขนส่ง — กำหนดเส้นทางตาม IP/พอร์ตและทำงานได้เร็วมาก) และชั้นที่ 7 (แอปพลิเคชัน — กำหนดเส้นทางตามเส้นทาง URL ส่วนหัว และคุกกี้ ทำให้กำหนดเส้นทางได้อย่างชาญฉลาดยิ่งขึ้น) AWS ALB, เอ็นจินเอ็กซ์ และเอชเอพร็อกซีเป็นตัวจัดสรรโหลดชั้นที่ 7 ที่ใช้กันทั่วไป ส่วน AWS NLB เป็นตัวจัดสรรโหลดชั้นที่ 4
# Load balancing algorithms
algorithms = {
'Round Robin': {
'how': 'Rotate through servers in sequence',
'best_for': 'Stateless servers with similar capacity',
'weakness': 'Does not account for server load or response time',
},
'Weighted Round Robin': {
'how': 'Round robin but servers with more capacity get more requests',
'best_for': 'Heterogeneous server fleet',
'weakness': 'Static weights; does not adapt to runtime load',
},
'Least Connections': {
'how': 'Send to server with fewest active connections',
'best_for': 'Long-lived connections (WebSocket, streaming)',
'weakness': 'More complex tracking of connection state',
},
'Consistent Hashing': {
'how': 'Hash request key (user_id, session) to server',
'best_for': 'Sticky sessions, cache locality per server',
'weakness': 'Uneven distribution if hash space is not balanced',
},
'Random': {
'how': 'Choose server at random',
'best_for': 'Simple stateless workloads',
'weakness': 'No guarantee of load balance in short windows',
},
}
for alg, info in algorithms.items():
print(f'{alg}: {info["how"]}')การแฮชแบบสอดคล้องกัน: การเพิ่มและนำโหนดออก
การแฮชแบบสอดคล้องกัน แก้ปัญหาการกระจายคีย์แคชใหม่เมื่อมีการเพิ่มหรือนำเซิร์ฟเวอร์ออก ในการแฮชแบบโมดูโลอย่างง่าย (server = hash(key) % n) เมื่อค่า n เปลี่ยน คีย์เกือบทั้งหมดจะถูกจับคู่ใหม่ ซึ่งทำให้เกิด cache stampede การแฮชแบบสอดคล้องกันจะจับคู่ทั้งคีย์และเซิร์ฟเวอร์ลงบนวงแหวน โดยแต่ละคีย์จะให้บริการโดยเซิร์ฟเวอร์ที่อยู่ถัดไปตามทิศทางตามเข็มนาฬิกา การเพิ่มเซิร์ฟเวอร์หนึ่งเครื่องจะจับคู่ใหม่เฉพาะคีย์ที่อยู่ระหว่างเซิร์ฟเวอร์ใหม่กับเซิร์ฟเวอร์ก่อนหน้า ซึ่งคิดเป็นประมาณ 1/n ของคีย์ทั้งหมด
โหนดเสมือนช่วยปรับปรุงการกระจายโหลด โดยเซิร์ฟเวอร์จริงแต่ละเครื่องจะได้รับหลายตำแหน่งบนวงแหวน ทำให้คีย์กระจายตัวได้สม่ำเสมอมากขึ้น แม้จะมีเซิร์ฟเวอร์จำนวนน้อย
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, replicas=100):
self.replicas = replicas # virtual nodes per server
self.ring = {}
self.sorted_keys = []
def add_server(self, server):
for i in range(self.replicas):
key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
self.ring[key] = server
bisect.insort(self.sorted_keys, key)
def remove_server(self, server):
for i in range(self.replicas):
key = int(hashlib.md5(f'{server}:{i}'.encode()).hexdigest(), 16)
del self.ring[key]
self.sorted_keys.remove(key)
def get_server(self, item):
key = int(hashlib.md5(item.encode()).hexdigest(), 16)
idx = bisect.bisect(self.sorted_keys, key) % len(self.sorted_keys)
return self.ring[self.sorted_keys[idx]]
ring = ConsistentHashRing()
for s in ['server-1', 'server-2', 'server-3']:
ring.add_server(s)
for item in ['user:1', 'user:2', 'product:abc', 'session:xyz']:
print(f'{item} => {ring.get_server(item)}')ปัญหาแคช stampede และแนวทางแก้ไข
ปัญหา cache stampede (หรือปัญหาฝูงชนแห่เข้าพร้อมกัน) เกิดขึ้นเมื่อรายการแคชที่ได้รับความนิยมหมดอายุ แล้วคำขอจำนวนมากที่ทำงานพร้อมกันไม่พบข้อมูลในแคชในเวลาเดียวกัน ส่งผลให้ฐานข้อมูลต้องรับคำค้นหาเดียวกันจำนวนมาก แนวทางแก้ไขมีดังนี้:
- การล็อกแบบ Mutex: มีเพียงคำขอเดียวที่คำนวณค่า ส่วนคำขออื่นรอ
- การหมดอายุล่วงหน้าแบบสุ่ม: ก่อน TTL จะหมดอายุเล็กน้อย คำขอหนึ่งจะตัดสินใจแบบสุ่มเพื่อรีเฟรชแคช ป้องกันไม่ให้หมดอายุพร้อมกัน
- การส่งข้อมูลเก่าระหว่างตรวจสอบใหม่: ส่งเนื้อหาเก่าให้ผู้ใช้ทันที ขณะรีเฟรชแคชแบบอะซิงโครนัส
- การรีเฟรชเบื้องหลัง: กระบวนการแยกต่างหากจะรีเฟรชคีย์ยอดนิยมก่อนหมดอายุ
import time, threading, random
# Probabilistic early expiry (XFetch algorithm)
class ProbabilisticCache:
def __init__(self):
self._cache = {}
def get(self, key, ttl, recompute_fn, beta=1.0):
if key in self._cache:
value, expiry, delta = self._cache[key]
# XFetch: decide to refresh early with probability proportional to delta/TTL
remaining = expiry - time.time()
if remaining > 0:
early_refresh_score = delta * beta * (-1) * (remaining / ttl)
if random.random() > (1 - early_refresh_score): # simplified
pass # could trigger async refresh here
return value
# Cache miss or expired
start = time.time()
value = recompute_fn()
delta = time.time() - start # computation time
expiry = time.time() + ttl
self._cache[key] = (value, expiry, delta)
return value
print('XFetch: refresh probabilistically before expiry based on computation cost')
print('High-cost computations => refresh earlier to avoid stampede')
print('Low-cost computations => refresh closer to TTL')การทำให้แคชของ CDN ใช้ไม่ได้
การทำให้แคชใช้ไม่ได้เป็นเรื่องที่ขึ้นชื่อว่ายาก: “ปัญหาที่ยากที่สุดในวิทยาการคอมพิวเตอร์มีเพียงสองอย่าง ได้แก่ การทำให้แคชใช้ไม่ได้และการตั้งชื่อสิ่งต่าง ๆ” เมื่อเนื้อหาที่ต้นทางเปลี่ยนแปลง โหนดขอบเครือข่ายของ CDN ต้องส่งเวอร์ชันใหม่ กลยุทธ์ที่ใช้มีดังนี้:
- การหมดอายุตาม TTL: ปล่อยให้เนื้อหาหมดอายุตามธรรมชาติ (เรียบง่ายแต่มีช่วงเวลาที่ข้อมูลเก่า)
- การกำหนดรุ่นในที่อยู่เว็บ: ฝังแฮชของเนื้อหาไว้ในที่อยู่เว็บ (เช่น
main.a3f2b.js) เนื้อหาใหม่ = ที่อยู่เว็บใหม่ จึงไม่จำเป็นต้องทำให้แคชใช้ไม่ได้ - การล้างแคช CDN: ล้างที่อยู่เว็บอย่างชัดเจนผ่านการเรียกส่วนติดต่อโปรแกรมประยุกต์หลังการนำไปใช้งาน (รวดเร็วแต่ต้องผสานการทำงานกับส่วนติดต่อโปรแกรมประยุกต์ของ CDN)
# Cache invalidation strategies for CDN/browser
strategies = [
{
'name': 'Long TTL + URL versioning (best for static assets)',
'example': '<script src="/app.a3f2b1c.js"></script>',
'ttl': 'Cache-Control: max-age=31536000 (1 year)',
'how': 'Content hash in filename; new deploy = new URL; old URL cached forever (OK)',
},
{
'name': 'Short TTL (for frequently changing content)',
'example': '/api/v1/config',
'ttl': 'Cache-Control: max-age=60 (1 minute)',
'how': 'Simple; content is at most 60s stale; no invalidation needed',
},
{
'name': 'CDN API purge (for news / social media)',
'example': '/news/breaking-story.html',
'ttl': 'Cache-Control: s-maxage=3600',
'how': 'On publish, call CDN.purge(url); edge serves new version immediately',
},
]
for s in strategies:
print(f'{s["name"]}:')
print(f' Example: {s["example"]}')
print(f' TTL: {s["ttl"]}')
print(f' Strategy: {s["how"]}\n')สถาปัตยกรรม: การนำทุกอย่างมารวมกัน
ชั้นแอปพลิเคชันเว็บที่รองรับการขยายขนาดอย่างเต็มรูปแบบใช้เทคนิคทั้งสามร่วมกัน ได้แก่ ตัวจัดสรรโหลดกระจายปริมาณการรับส่งข้อมูล CDN รองรับคำขอทรัพยากรแบบคงที่และคำขอจากส่วนติดต่อโปรแกรมประยุกต์ที่แคชได้ ส่วนรีดิสแคชข้อมูลไดนามิก ฐานข้อมูลจะเห็นเฉพาะคำขอที่ไม่พบข้อมูลในแคช ซึ่งโดยทั่วไปมีเพียง 5–20% ของคำขอทั้งหมด
ลำดับการทำงานทั่วไปของส่วนติดต่อโปรแกรมประยุกต์ที่เน้นการอ่านมีดังนี้: ผู้ใช้ → DNS → ขอบเครือข่าย CDN (พบข้อมูลในแคช: ส่งให้ทันที) → ไม่พบข้อมูลใน CDN → ตัวจัดสรรโหลด → กลุ่มเซิร์ฟเวอร์แอปพลิเคชัน → แคชรีดิส (พบข้อมูล: ตอบกลับใน 1 มิลลิวินาที) → ไม่พบข้อมูลในรีดิส → ฐานข้อมูล (10–50 มิลลิวินาที) → แคชคำตอบไว้ในรีดิสและใน CDN หากต้องการ → ผู้ใช้ แต่ละชั้นช่วยลดภาระของฐานข้อมูลได้อย่างมาก
# Request flow with cache hit rates
request_flow = [
('Browser Cache', '10%', '0ms', 'Browser caches GET responses per Cache-Control'),
('CDN Edge Cache', '60%', '5ms', 'CloudFront/Fastly caches cacheable API responses'),
('Load Balancer', None, '1ms', 'Routes to healthy app server replica'),
('App Server', None, '2ms', 'Business logic, auth check'),
('Redis Cache', '25%', '1ms', 'Caches computed data, hot DB rows'),
('Database Read Replica','5%', '10ms', 'Cache miss: query read replica'),
('Database Primary', '0.1%', '15ms', 'Cache+replica miss: query primary (rare for reads)'),
]
print(f'{'Layer':30s} {'Hit Rate':10s} {'Latency':10s} {'Notes'}')
print('-'*80)
for layer, hit_rate, latency, note in request_flow:
hr = hit_rate if hit_rate else '-'
print(f'{layer:30s} {hr:10s} {latency:10s} {note}')
print('\nResult: DB sees ~5% of requests; Redis sees ~25%; CDN absorbs 60%; browser 10%')เคล็ดลับการสัมภาษณ์: การแคชและการกระจายโหลด
เมื่ออภิปรายเรื่องการแคชในการสัมภาษณ์ด้านการออกแบบระบบ ควรกล่าวถึงประเด็นต่อไปนี้เสมอ: จะแคชอะไร (ข้อมูลยอดนิยม การคำนวณที่ใช้ทรัพยากรมาก), จะแคชที่ใด (เบราว์เซอร์ CDN แอปพลิเคชัน แคชคำค้นหาฐานข้อมูล), จะทำให้แคชใช้ไม่ได้เมื่อใด (เมื่อเขียนข้อมูล เมื่อ TTL หมดอายุ หรือผ่านการรีเฟรชเบื้องหลัง) และ ยอมรับการรับประกันความสอดคล้องแบบใดได้บ้าง แคชทำให้เกิดช่วงเวลาที่ข้อมูลอาจไม่สอดคล้องกัน จึงควรระบุเรื่องนี้อย่างชัดเจน
สำหรับการกระจายโหลด ควรกล่าวถึงการเลือกอัลกอริทึม การตรวจสอบสถานะ เซสชันแบบยึดติด (หากจำเป็น) และความเป็นไปได้ในการขยายขนาดตามแนวนอนของเซิร์ฟเวอร์แอปพลิเคชันที่ไม่มีสถานะ หากแอปพลิเคชันมีสถานะ เช่น การเชื่อมต่อ WebSocket หรือเซสชัน ควรอธิบายวิธีจัดการสถานะนั้นข้ามชุดจำลองหลายชุด
# Caching design questions checklist
cache_checklist = [
'What data to cache? (read-heavy, expensive to compute, rarely updated)',
'Cache layer: client-side / CDN / app-level / DB query cache?',
'Cache invalidation strategy: TTL / event-driven / write-through?',
'Eviction policy: LRU / LFU?',
'Cache key design: ensure uniqueness, avoid hotspots',
'Consistency window: acceptable staleness in seconds?',
'Cache stampede prevention: mutex / stale-while-revalidate?',
'Cache capacity: how much RAM needed for hot set?',
]
lb_checklist = [
'Layer 4 vs Layer 7: routing by IP or by URL/headers?',
'Algorithm: round robin / least-connections / consistent hashing?',
'Health checks: interval, failure threshold, recovery',
'Session stickiness: needed? Use cookie-based affinity or external session store',
'Auto-scaling: scale out when CPU > 70%; scale in when < 30%',
]
print('Cache checklist:')
for item in cache_checklist: print(f' [ ] {item}')
print('\nLoad balancer checklist:')
for item in lb_checklist: print(f' [ ] {item}')ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจของคุณเกี่ยวกับแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า การแคชจะเก็บข้อมูลยอดนิยมไว้ในชั้นหน่วยความจำความเร็วสูง (รีดิส, CDN) เพื่อรองรับการอ่านส่วนใหญ่และลดภาระของฐานข้อมูล รูปแบบที่แอปพลิเคชันจัดการแคชเองเป็นรูปแบบที่ใช้กันมากที่สุด — เมื่อไม่พบข้อมูลให้โหลดจากฐานข้อมูล เมื่อพบข้อมูลให้ส่งกลับทันที และเมื่อเขียนข้อมูลให้ลบข้อมูลออกจากแคช และ การแฮชแบบสอดคล้องกันจะกระจายคีย์แคชไปยังโหนดต่าง ๆ ดังนั้นการเพิ่มหรือนำโหนดออกจะจับคู่ใหม่เพียงประมาณ 1/n ของคีย์ แทนที่จะจับคู่ใหม่ทั้งหมด บทถัดไปเราจะออกแบบตัวจำกัดอัตราและฟีดทวิตเตอร์ เพื่อนำแนวคิดการออกแบบระบบทั้งหมดไปประยุกต์ใช้กับปัญหาแบบครบตั้งแต่ต้นจนจบ
คำถามที่พบบ่อย
บทเรียน “แคช, CDN และการกระจายโหลด” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “แคช, CDN และการกระจายโหลด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “แคช, CDN และการกระจายโหลด”
เพิ่มชั้นแคช Redis ส่งทรัพยากรคงที่ไปยัง CDN และกระจายทราฟฟิกไปยังแบบจำลองด้วยตัวกระจายโหลดแบบวนรอบและแบบแฮชสอดคล้อง คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “แคช, CDN และการกระจายโหลด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- กรอบการสัมภาษณ์ด้านการออกแบบระบบ
- การจัดเก็บข้อมูลที่ขยายขนาดได้: SQL กับ NoSQL
- แคช, CDN และการกระจายโหลด
- การออกแบบตัวจำกัดอัตราและฟีด Twitter