← 中文资源

简体中文版本

源站护盾(Origin Shield)

origin shield 是 CDN 与源站之间的额外缓存层——一个护盾区域收拢全部边缘节点——成倍加深有效缓存深度,大幅削减源站负载。

阅读英文原文

概念定义

CDN 各边缘独立缓存:同一长尾对象在每个 POP 都 miss 一遍。护盾横亘在所有边缘与源站之间,对象只回源一次,此后各 POP 都从护盾取——源站从几十个客户端变成一个。

工作机制

边缘被配置为向护盾回源;护盾维护大容量缓存、拥有独立 TTL 与陈旧策略,通常在回源点施加请求合并。护盾区域应靠近源站而非用户,因为全部边缘填充流量都经过它。

使用场景

带限速或计算昂贵的源站、各 POP 缓存重度 miss 的长尾目录、以及想要单一源站保护闸门的 DR 场景。源站故障期间护盾还是集中提供陈旧服务的那个点。

常见误区

“护盾给每个请求加延迟”不对:只有边缘 miss 才多一跳,边缘命中不受影响。“每大洲一个护盾”反而重建了它本该收拢的扇入——每源站恰一个护盾点才是要义。护盾是新的单点:它故障时边缘填充直冲源站或请求失败;动态不可缓存流量必须绕开护盾,否则单区域跳数把一切串行化。

代码示例

import threading
from collections import OrderedDict
from collections.abc import Callable, Hashable
from functools import wraps
from typing import Generic, TypeVar

K = TypeVar("K", bound=Hashable)
V = TypeVar("V")


class LRUCache(Generic[K, V]):
    def __init__(self, capacity: int) -> None:
        if capacity < 1:
            raise ValueError("capacity must be at least 1")
        self.capacity = capacity
        self._items: OrderedDict[K, V] = OrderedDict()
        self._lock = threading.RLock()
        self.hits = 0
        self.misses = 0

    def get(self, key: K) -> V | None:
        with self._lock:
            if key not in self._items:
                self.misses += 1
                return None
            self.hits += 1
            self._items.move_to_end(key)
            return self._items[key]

    def put(self, key: K, value: V) -> None:
        with self._lock:
            self._items[key] = value
            self._items.move_to_end(key)
            while len(self._items) > self.capacity:
                self._items.popitem(last=False)

    def stats(self) -> dict[str, int | float]:
        total = self.hits + self.misses
        return {
            "size": len(self._items),
            "hits": self.hits,
            "misses": self.misses,
            "hit_rate": round(self.hits / total, 3) if total else 0.0,
        }


def memoize(
    cache: LRUCache[K, V],
) -> Callable[[Callable[[K], V]], Callable[[K], V]]:
    def decorator(func: Callable[[K], V]) -> Callable[[K], V]:
        @wraps(func)
        def wrapper(key: K) -> V:
            cached = cache.get(key)
            if cached is not None:
                return cached
            value = func(key)
            cache.put(key, value)
            return value

        return wrapper

    return decorator


def main() -> None:
    cache: LRUCache[str, int] = LRUCache(capacity=2)
    calls = 0

    @memoize(cache)
    def slow_square(text: str) -> int:
        nonlocal calls
        calls += 1
        return int(text) ** 2

    print(slow_square("4"), slow_square("4"))
    print(slow_square("5"), slow_square("6"))
    print(slow_square("4"))
    print(cache.stats(), "calls:", calls)


if __name__ == "__main__":
    main()
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"
)

type Store struct {
	mu    sync.RWMutex
	cache map[string]entry
	group singleflight.Group
}

type entry struct {
	value   string
	expires time.Time
}

func NewStore(ttl time.Duration) *Store {
	return &Store{cache: make(map[string]entry)}
}

func (s *Store) Get(ctx context.Context, key string, ttl time.Duration) (string, error) {
	s.mu.RLock()
	e, ok := s.cache[key]
	s.mu.RUnlock()

	if ok && time.Now().Before(e.expires) {
		return e.value, nil // 命中:新鲜数据直接返回
	}

	v, err, _ := s.group.Do(key, func() (interface{}, error) {
		value, loadErr := slowLoad(ctx, key) // 同 key 并发只执行一次
		if loadErr != nil {
			return nil, loadErr
		}
		s.mu.Lock()
		s.cache[key] = entry{value: value, expires: time.Now().Add(ttl)}
		s.mu.Unlock()
		return value, nil
	})
	if err != nil {
		if ok {
			return e.value, nil // 回源失败时降级旧值
		}
		return "", err
	}
	return v.(string), nil
}

func slowLoad(_ context.Context, key string) (string, error) {
	time.Sleep(50 * time.Millisecond)
	return "value-for-" + key, nil
}

func main() {
	store := NewStore(time.Minute)

	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			v, _ := store.Get(context.Background(), "config", time.Minute)
			fmt.Println(v)
		}()
	}
	wg.Wait()
}