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

Кеширование

Dogpile: чтение кода и блокировок

Суть Читай реальные сниппеты single-flight, distributed lock и продления lease, предскажи поведение dogpile и выбери фикс с наибольшим рычагом, который senior сделал бы первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Dogpile живёт в пути пересчёта: обработчик промаха, захват блокировки, TTL, освобождение. Прочитай каждый сниппет, найди, где коллизия открывается снова или блокировка дедлочит, и выбери фикс, который senior сделал бы первым.

Цель

Потренируй чтение кода коалесинга и блокировок так, как читаешь его в инциденте — проследи конкурентный путь, заметь пропущенный таймаут, потерянный lease или незафенсенную запись, и чини механизм, а не симптом.

Сниппет 1 — локальный single-flight

var group singleflight.Group

func getFeed(ctx context.Context, key string) ([]byte, error) {
    if v, ok := cache.Get(key); ok {
        return v, nil
    }
    // коалесим одновременные промахи по ключу на один пересчёт
    v, err, _ := group.Do(key, func() (any, error) {
        out, err := recomputeFeed(ctx) // 200ms-агрегация в БД
        if err != nil {
            return nil, err
        }
        cache.Set(key, out, 5*time.Minute)
        return out, nil
    })
    if err != nil {
        return nil, err
    }
    return v.([]byte), nil
}
Викторина

Это работает на 20 инстансах за балансировщиком. Сколько пересчётов в БД произойдёт на момент истечения и какое изменение с наибольшим рычагом приведёт к одному?

Сниппет 2 — distributed-блокировка без TTL

func getFeedLocked(ctx context.Context, key string) ([]byte, error) {
    if v, ok := cache.Get(key); ok {
        return v, nil
    }
    lockKey := "lock:" + key
    // захват: SET lock:key <token> NX   (БЕЗ аргумента истечения)
    ok, _ := rdb.SetNX(ctx, lockKey, token, 0).Result()
    if !ok {
        time.Sleep(50 * time.Millisecond) // кто-то другой пересчитывает
        return getFeedLocked(ctx, key)     // повтор чтения
    }
    defer rdb.Del(ctx, lockKey)            // освобождение при выходе
    out, err := recomputeFeed(ctx)
    if err != nil {
        return nil, err
    }
    cache.Set(key, out, 5*time.Minute)
    return out, nil
}
Викторина

SetNX использует истечение 0 (без TTL); освобождение через defer Del. Какой production-сбой и какой фикс?

Сниппет 3 — фиксированный TTL, слишком короткий для хвоста

const lockTTL = 5 * time.Second // пересчёт p50 ~2s, p99 ~25s

func recomputeUnderLock(ctx context.Context, key, token string) error {
    ok, _ := rdb.SetNX(ctx, "lock:"+key, token, lockTTL).Result()
    if !ok {
        return errLockHeld // вызывающий отдаёт stale и ретраит позже
    }
    out, err := recomputeFeed(ctx) // до 25s на холодном шарде
    if err != nil {
        return err
    }
    // блокировка тут уже могла истечь на медленном пересчёте
    cache.Set(key, out, 5*time.Minute)
    rdb.Del(ctx, "lock:"+key) // безусловное удаление
    return nil
}
Викторина

С TTL блокировки 5s и пересчётом p99 25s назови оба бага и принципиальный фикс.

Сниппет 4 — держатель, продлевающий lease

func recomputeWithLease(ctx context.Context, key, token string) error {
    if ok, _ := rdb.SetNX(ctx, "lock:"+key, token, 10*time.Second).Result(); !ok {
        return errLockHeld
    }
    // heartbeat: продлеваем lease каждые 3s, пока работаем
    stop := make(chan struct{})
    go func() {
        t := time.NewTicker(3 * time.Second)
        defer t.Stop()
        for {
            select {
            case <-stop:
                return
            case <-t.C:
                // PEXPIRE lock:key 10000, если токен ещё наш
                renewIfOwner(ctx, "lock:"+key, token, 10*time.Second)
            }
        }
    }()
    out, err := recomputeFeed(ctx)
    close(stop)
    if err != nil {
        return err
    }
    cache.Set(key, out, 5*time.Minute)
    releaseIfOwner(ctx, "lock:"+key, token) // CAS-удаление
    return nil
}
Викторина

Lease продлевается каждые 3s при TTL 10s и освобождается через CAS-по-токену. Длинная stop-the-world GC-пауза (≥10s) бьёт по этому воркеру в середине пересчёта. Что всё ещё может пойти не так и что от этого защищает?

Итог

Каждый фикс dogpile читается в пути блокировки. Локальный single-flight капит пересчёты числом инстансов, а не одним — distributed lock координирует флот. Блокировка без TTL дедлочит всех читателей, если держатель умер, поэтому всегда SET NX PX. Фиксированный TTL короче худшего пересчёта открывает стадо снова и рискует удалить чужую блокировку, поэтому продлевай lease по heartbeat и освобождай условно по токену. И даже продлеваемый lease может лапситься при длинной STW-паузе или partition, поэтому фенси запись монотонным токеном (или версионным CAS), чтобы возобновившийся держатель не перезаписал свежие данные. Чини жизненный цикл блокировки, потом перепроверь под сценарием медленного пересчёта и инъекции паузы.

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

Trademarks belong to their respective owners. Editorial reference only.