Məzmuna keç
AçBağlaEsc
Bloq

6 dəq oxunuş

Laravel-də cache stampede-i dayandırmaq

Yüklü saytda isti keş açarı bitəndə nə baş verir və iki kiçik həll: bir sorğunun yenidən qurması üçün atomik kilid və heç kimin gözləməməsi üçün köhnə məlumatı vermək.

Keşləmə həll olunmuş problem kimi görünür, ta ki bir gün olmayana qədər. Yavaş sorğunu Cache::remember ilə bükürsən, səhifə sürətlənir, hamı razıdır. Sonra bir gün verilənlər bazası heç bir açıq səbəb olmadan bir neçə saniyəlik sıçrayış edir və bu, müntəzəm aralıqlarla təkrarlanır. Bunu görmüsənsə, böyük ehtimalla cache stampede ilə üzləşmisən.

Bu, onun nə olduğu və Laravel-də bununla necə davrandığım haqqında qısa yazıdır. Həll mürəkkəb deyil, amma səni vurana qədər gözdən qaçırmaq asandır.

Problem

Deyək ki, ana səhifədə statistika vidceti var. Rəqəmlər ağır sorğudan gəlir, ona görə 10 dəqiqəlik keşləyirsən:

public function getStats(): array
{
    return Cache::remember('homepage.stats', now()->addMinutes(10), function () {
        return $this->expensiveQuery(); // takes ~2 seconds
    });
}

Bu, çox vaxt yaxşı işləyir. Bir sorğu sorğunu icra edir, qalanları növbəti 10 dəqiqə keşdən oxuyur.

Problem açar bitdiyi anda başlayır. remember əslində sadəcə "dəyəri götür, yoxdursa callback-i icra et və saxla" deməkdir. İndi yüklü saytda açarın bitdiyi saniyədə nə olduğunu təsəvvür et:

  • A sorğusu gəlir, keş boşdur, 2 saniyəlik sorğunu başladır.
  • B sorğusu 50 ms sonra gəlir, keş hələ də boşdur (A bitirməyib), eyni sorğunu başladır.
  • C-dən Z-yə qədər sorğular eyni şeyi edir.

İndi birinin əvəzinə onlarla eyni ağır sorğu eyni anda işləyir. Keş səni qorumadı, sadəcə bütün yükü bir ana yığıb daha da pisləşdirdi. Stampede budur.

Həll: bir sorğu yenidən qursun, qalanları gözləsin

Fikir sadədir: keş boş olanda yalnız bir sorğuya onu yenidən qurmağa icazə verilməlidir. Qalanlar onun bitməsini gözləyir, sonra təzə dəyəri oxuyur.

Laravel-də məhz bunun üçün atomik kilidlər var:

public function getStats(): array
{
    $key = 'homepage.stats';
 
    $value = Cache::get($key);
    if ($value !== null) {
        return $value;
    }
 
    // Cache is empty. Try to take the lock and rebuild.
    // block(5) waits up to 5 seconds for the lock to become free.
    return Cache::lock($key . ':lock', 10)->block(5, function () use ($key) {
        // We got the lock. But maybe another request already rebuilt
        // the cache while we were waiting, so check again first.
        $value = Cache::get($key);
        if ($value !== null) {
            return $value;
        }
 
        $value = $this->expensiveQuery();
        Cache::put($key, $value, now()->addMinutes(10));
 
        return $value;
    });
}

İndi sorğunu yalnız bir sorğu icra edir. Qalanları block(5)-də oturub bir az gözləyir və içəri girəndə dəyər artıq yerindədir. Əllinin əvəzinə bir sorğu.

Bilməli olduğun kiçik bir şey: block verdiyin vaxt ərzində kilidi ala bilməsə, LockTimeoutException atır. 2 saniyəlik sorğu və 5 saniyəlik gözləmə ilə bu, az ehtimaldır, amma yenidən qurma yavaşdırsa, ya gözləməni artırmaq, ya da onu tutub məntiqli bir şey qaytarmaq istəyəcəksən.

Atomik kilidlər onları dəstəkləyən keş drayveri tələb edir. Redis və Memcached dəstəkləyir, database və file drayverləri də. Əksər layihələr onsuz da Redis-dədir, ona görə adətən əlavə heç nə qurmaq lazım gəlmir.

Daha yaxşı versiya: gözləmək əvəzinə bir az köhnə məlumat vermək

Kilidli həll yaxşıdır, amma hələ bir kələ-kötür tərəfi var. Kilidi qazanmayan sorğular yenidən qurmanı gözləməli olur. Sorğun 2 saniyə çəkirsə, həmin istifadəçilər 2 saniyə yüklənmə səhifəsinə baxır.

Statistika vidceti kimi bir şey üçün rəqəmlərin 30 saniyə köhnə olması heç kimi maraqlandırmır. Ona görə daha yaxşı mübadilə belədir: bir sorğu arxa planda sakitcə təzələyərkən köhnə dəyəri verməyə davam et. Heç kim gözləmir.

Fənd iki şeyi saxlamaqdır: məlumatı və öz "bu köhnəlir" vaxt möhürümüzü, keşə isə təzəlik pəncərəsindən xeyli uzun real ömür vermək:

public function getStats(): array
{
    $key = 'homepage.stats';
    $cached = Cache::get($key);
 
    if ($cached) {
        // Still fresh? Just return it.
        if ($cached['fresh_until'] > now()->timestamp) {
            return $cached['data'];
        }
 
        // Stale. Try to grab the lock to refresh.
        // If we get it, we refresh. If we don't, someone else is already
        // on it, so we just hand back the old data and move on.
        $lock = Cache::lock($key . ':refresh', 10);
        if ($lock->get()) {
            try {
                return $this->rebuildStats($key);
            } finally {
                $lock->release();
            }
        }
 
        return $cached['data']; // serve stale, no waiting
    }
 
    // Truly empty (first load, or it got evicted). Here we do block,
    // because we have nothing old to fall back on.
    return Cache::lock($key . ':refresh', 10)->block(5, function () use ($key) {
        $fresh = Cache::get($key);
        return $fresh ? $fresh['data'] : $this->rebuildStats($key);
    });
}
 
private function rebuildStats(string $key): array
{
    $data = $this->expensiveQuery();
 
    Cache::put($key, [
        'data'        => $data,
        'fresh_until' => now()->addMinutes(10)->timestamp, // soft expiry
    ], now()->addHour()); // hard expiry, much longer
 
    return $data;
}

İndi axın belədir:

  • Təzə məlumat → qaytar.
  • Bir az köhnə məlumat → bir sorğu təzələyir, qalanları köhnə nüsxəni dərhal alır.
  • Heç nə yoxdur → gözləməyə qayıt, çünki veriləcək köhnə nüsxə yoxdur.

"Sərt" ömür (bir saat) sadəcə "təzə" pəncərədən (10 dəqiqə) xeyli uzun olmalıdır ki, təzələmə gedərkən həmişə veriləcək köhnə dəyər yerində olsun.

Bu nə vaxt dəyər?

Hər keşə bu lazım deyil. Açarı yenidən qurmaq ucuzdursa və ya səhifəyə az trafik gəlirsə, adi remember tamamilə kifayətdir və lazım olmayan mexanizm əlavə etməməlisən.

Bu, iki şey eyni anda doğru olanda əhəmiyyət kəsb edir: dəyəri qurmaq bahadır və açar o qədər istidir ki, çoxlu sorğu eyni anda keşi boş tapa bilər. Ana səhifə məlumatı, bahalı panellər, çoxlu istifadəçinin eyni anda paylaşdığı hər şey. Bitən bir açarın verilənlər bazasını qısa müddətə yerə sərə bildiyi yer məhz oradır.

Yaxşı xəbər odur ki, həll kiçikdir. Yalnız bir sorğunun yenidən qurması üçün kilid və istəyə görə heç kimin gözləməməsi üçün bir az köhnə məlumat vermək. İki fikir, və keşin arabir səni incidən şey olmaqdan çıxır.