awesome-everything EN
↑ Обратно к восхождению

Кеширование

Cache stampede: чтение кода

Суть Читаем реальные cache-aside сниппеты — lock-on-miss, request coalescing, XFetch — предсказываем поведение и выбираем фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги stampede прячутся в самом cache-aside коде: lock с неправильным EX, коалесцер, регистрирующий in-flight промис на строку позже нужного, правило XFetch, срабатывающее слишком рано. Читайте каждый сниппет так, как читали бы на ревью, и выбирайте фикс, который сеньор делает первым.

Цель

Потренируйтесь читать сам код митигаций — lock-on-miss, request coalescing и probabilistic early expiration — и находить дефект, который рано или поздно вскроет нагрузочный тест.

Сниппет 1 — lock-on-miss

func get(ctx context.Context, key string) ([]byte, error) {
    if v, ok := cache.Get(key); ok {
        return v, nil
    }
    // miss: try to become the rebuilder
    locked := redis.SetNX(ctx, "lock:"+key, uuid, 30*time.Second).Val()
    if !locked {
        // someone else is rebuilding
        return rebuild(ctx, key) // <-- rebuild anyway
    }
    v, err := rebuild(ctx, key)
    if err == nil {
        cache.Set(key, v, 60*time.Second)
        redis.Del(ctx, "lock:"+key)
    }
    return v, nil
}
Викторина

Lock через SetNX берётся корректно, но нагрузочный тест всё равно даёт N одновременных rebuild'ов в БД. Где баг?

Сниппет 2 — request coalescing

const inflight = new Map(); // key -> Promise

async function getCoalesced(key) {
  const cached = await cache.get(key);
  if (cached !== null) return cached;

  const fresh = await rebuild(key);   // (A) await the rebuild...
  inflight.set(key, fresh);           // (B) ...then record it
  const value = await fresh;
  cache.set(key, value, 60);
  inflight.delete(key);
  return value;
}
Викторина

Этот код должен коалесцировать одновременные промахи по одному ключу в один rebuild, но не коалесцирует никогда. Что не так?

Сниппет 3 — probabilistic early expiration (XFetch)

def should_refresh(delta, beta, ttl_remaining):
    # delta = typical rebuild seconds, ttl_remaining = seconds to expiry
    return (-beta * delta * math.log(random.random())) >= ttl_remaining
Викторина

Оператор хочет меньше лишних ранних rebuild'ов на тёплых ключах и поднимает beta с 1.0 до 4.0. Каков эффект на горячем ключе против более холодного?

Сниппет 4 — запись с fencing-token

def rebuild_and_write(key, my_token):
    value = rebuild(key)               # may take longer than the lock EX
    if redis.get("lock:" + key) != my_token:
        return                          # we lost the lock — abort the write
    redis.set("cache:" + key, value, ex=60)
Викторина

Проверка fencing 'GET lock, затем SET cache' защищает от медленного rebuild, пережившего свой lock. Какая гонка остаётся и что её закрывает?

Итог

Каждая защита от stampede живёт в коде, который легко тонко испортить: lock помогает, только если проигравшие ждут и перечитывают, а не делают rebuild всё равно; коалесцер помогает, только если in-flight промис регистрируется синхронно до любого await и читается на входе; beta в XFetch двигает окно раннего обновления в сторону, противоположную интуиции большинства (больший beta — раньше и чаще); а проверка fencing-token безопасна, только когда чтение-затем-запись атомарны или подкреплены монотонной версией. Прочитайте митигацию, проведите через неё два конкурентных вызова — и баг обычно проявит себя ещё до любого нагрузочного теста.

Продолжить восхождение ↑Cache stampede: собрать и укротить стадо
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.