Laravel Üretim İçin Mimari Desenler — Bölüm 2
~9 dakikalık okuma · Kuyruk tasarımı · İş mimarisi · Arka planda işlem yapma
Arka planda çalışan işler, çalışmaya başladığından itibaren “çözüldü” hissini veren özelliklerdendir. Görev gönderirsiniz, arka planda çalışır, hayat güzel. Framework bununla ilgilenir.
Bu illüzyon, ölçek büyüdüğünde bozulur. Bir şifre sıfırlama e-postası, 90 saniyelik bir video sıkıştırma işinin arkasında bekler. Daha önceki bir API’ye bağlı olan bir iş arızalanır, yeniden dener, yine arızalanır — gerçek işler beklerken işçi süreçlerini harcar. Bir toplu işlemde 5,000 iş aynı anda gönderildiğinde kuyruk sistemi bu patlamaya dayanamaz. Bir dosya işlemi yarıda kalır ve üç gün boyunca fark etmediğiniz bozuk verilere yol açar.
Bunların hiçbiri framework hatası değildir. Bunlar, açıkça verilmemiş tasarım hatalarıdır — yanlış kararlarla sonuçlanan durumlar.
Bu makale, kararlar ve bu kararların arkasındaki mantık hakkında.
Neler Öğreneceksiniz
Neler Öğreneceksiniz
- Eloquent model gönderiminin iş yapmak için neden yanlış olduğu — ve onun yerine neyi göndermelisiniz
- Bir kuyruk topolojisini, bir sorunla karşılaşmadan önce nasıl tasarlayacağınızı
- Geçici, oran sınırlı ve kalıcı hatalar için hangi yeniden deneme stratejisini kullanmalısınız
- Bir işin ortasında çökme durumunda bozuk bir durum bırakmayacak şekilde dosya işlemlerini nasıl yazmalısınız
Zihinsel Model: Bir Kuyruk, Bir Yapılacaklar Listesi Değildir
Zihinsel Model: Bir Kuyruk, Bir Yapılacaklar Listesi Değildir
Çoğu kuyruk sorunu, bir kuyruk kavramını basit bir liste olarak ele almaktan kaynaklanır — girişler gelir, çıkışlar olur, sırayla. Bu model, az sayıda iş için sorun çıkarmıyor gibi görünse de, baskı altındayken bozulur.
Daha doğru zihinsel model: kuyruk, sisteminizin bir şeyi gerçekleştirmesi gerektiğini bilen kısmı ile bunu gerçekleştirecek kısmı arasında, zaman, işlem yeniden başlatmaları ve hatalarla ayrılmış bir iş sözleşmesi‘dir. İş yükü, kendine yeterli bir talimattır; onu yaratan bağlamın hâlâ mevcut olduğunu varsayamaz.
Bu değişim — “görev listesi” yerine “kendine yeterli iş sözleşmesi” — makalenin geri kalan kararlarını mantıklı hale getirir.
Bir İş Yapıcısına Ne Koymalısınız — Ne Koymamalısınız
Bir İş Yapıcısına Ne Koymalısınız — Ne Koymamalısınız
Kuyrukta en sık yapılan hata, bir Eloquent modelini iş yapıcısına göndermektir:
// Bu doğru görünüyor. Ama değil.
CompressVideo::dispatch($media);
Bir iş gönderildiğinde, yapıcı argümanları kuyruk yüküne serileştirilir. Laravel’nin SerializesModels özelliği, bu durumu modelin sınıfını ve birincil anahtarı saklayarak ve iş çalıştırıldığında modeli yeniden alarak halleder. Bu doğru gibi görünüyor, fakat düşünün: iş 30 saniye sonra çalıştırılabilir. Ya da kuyruk geciktiğinde üç saat sonra. Bu süre zarfında kayıt güncellenmiş, soft-delete edilmiş ya da durumu değişmiş olabilir.
Eğer iş, serileştirilmiş bir model taşırsa, iş gönderildiği zamandaki görüntüyle çalışır. Eğer iş, bir kimlik taşırsa, yürütme zamanında güncel veriyi alır.
class CompressMediaVideo implements ShouldQueue
{
public function __construct(private readonly int $mediaId) {}
public function handle(VideoCompressorService $compressor): void
{
// Yürütme zamanında taze çekin, gönderim zamanında güncel olmayanla değil
$media = Media::findOrFail($this->mediaId);
if (!str_starts_with($media->mime_type, 'video/')) {
return; // Durum gönderimden beri değişmiş — nazikçe ele alın
}
$compressor->compress($media);
}
}
Kimlik gönderin, modelleri değil. İşin sorumluluğu, ilk ilkelerden başlayarak bir iş birimini gerçekleştirmektedir — başka bir yerde başlatılmış olan bir konuşmaya devam etmek değildir.
Tek Sorumluluk İlkesi: İşlere Uygulandı
Tek Sorumluluk İlkesi: İşlere Uygulandı
Bir iş sınıfı sadece bir iş yapmalıdır. Bu görünüşte bariz bir durumdur. Ancak, sık sık ihlal edilir.
Birden fazla şeyi yapan işlerin sorunu: bir parça başarısız olduğunda, tüm iş başarısız olur. Eğer bir iş bir videoyu sıkıştırıyor, veritabanını güncelliyor, bir bildirim gönderiyor ve dış bir sisteme senkronize ediyorsa ve dış senkronizasyon zaman aşımına uğrarsa — sıkıştırma gerçekleştirilmiştir, ancak iş yeniden deneme yaparken sıkıştırmayı tekrar çalıştırır. Yeniden deneme senkronizasyonu yeniden denemek için tasarlanmıştır. CPU israfına neden olur, potansiyel olarak bir çift bildirim oluşturur ve işin yeniden deneme davranışını öngörülemez hale getirir.
Daha temiz tasarım: her sorumluluk için bir iş, zincirleme. Unutmayın ki her sınıfta yapıcı hala gereklidir:
class CompressMediaVideo implements ShouldQueue
{
public function __construct(private readonly int $mediaId) {}
public function handle(VideoCompressorService $compressor): void
{
$media = Media::findOrFail($this->mediaId);
$compressor->compress($media);
// Sıkıştırma tamamlandı — sıradaki işi bağımsız olarak gönderin
UpdateMediaMetadata::dispatch($this->mediaId)->onQueue('default');
}
}
Her iş bağımsız olarak yeniden denenebilir. Metadata güncellemesi başarısız olursa sıkıştırmayı yeniden çalıştırmaz. Belirli iş türleri için izleme ekleyebilir ve işçileri bağımsız olarak ölçeklendirebilirsiniz.
Kuyruk Topolojisi: Kimsenin Yapması Gereken Karar
Kuyruk Topolojisi: Kimsenin Yapması Gereken Karar
Tek bir default kuyruğu, çalışana kadar işleri görebilir. Ancak, hem zaman kritik operasyonlar (şifre sıfırlamaları, OTP’ler) hem de uzun süren işlemler (rapor üretimi, toplu sıkıştırma) aynı kuyrukta bulunduğunda, kuyruk kendi başına çözemeyeceği bir öncelik problemi ortaya çıkar.
Topoloji kararı — hangi kuyrukların bulunduğu ve her birinde neyin çalıştığı — ilk iş dağıtılmadan önce verilmelidir, kullanıcıların “OTP’m neden gelmedi?” diye şikayet etmesi değil.
Pratik bir topoloji:
| Kuyruk | Amaç | Kabul Edilebilir Gecikme |
|---|---|---|
critical | Şifre sıfırlama, OTP’ler, güvenlik uyarıları | Hiç — gecikme, kullanıcıların sisteme erişememesi demektir |
default | Bildirimler, durum güncellemeleri, hafif görevler | Birkaç saniye |
media | Video sıkıştırma, resim işleme | Dakikalar — yavaş ve CPU yoğun |
sync | Dış API senkronizasyon işleri | Değişken — üçüncü taraf hatalarını izole et |
reports | Toplu dışa aktarımlar, büyük sorgular | Dakikalar alabilir — diğer kuyrukları asla aç bırakmamalıdır |
İşçiler, öncelik sırasına göre kuyrukları dinler:
// Kritik kuyruğun kendi özel işçisi var — başka bir şey rekabet etmez
php artisan queue:work redis --queue=critical --timeout=30
// Bu işçi önce varsayılanı işler, ardından varsayılan boşsa medyayı işler
php artisan queue:work redis --queue=default,media --timeout=120
// Senkron işçi — izole, böylece üçüncü taraf hataları kendi kuyruklarınızı etkilemez
php artisan queue:work redis --queue=sync --timeout=60
// Raporlar izole — yavaş zaman aşımından diğer işçileri etkilemez
php artisan queue:work redis --queue=reports --timeout=600
Neden sync izole olmalı? Dış API güvenilirliği kontrolünüzün dışındadır. Eğer üçüncü taraf bir hata alır ve siz saldırgan bir şekilde yeniden deniyorsanız, bu yeniden deneme işçilik pozisyonlarında birikmeye başlar. Paylaşımlı bir kuyrukta, sizin işlerinizle rekabet ederler. İzole bir sync kuyruğu, üçüncü taraf bir aksama olduğunda sadece senkron işçileri etkiler — uygulamanızın kendi arka plan çalışması etkilenmeden devam eder.
Redis üzerinde iseniz ve bir pano çalıştırabiliyorsanız, Laravel Horizon kuyruk derinliği, verimlilik ve hatalı iş izleme gibi özellikleri hazır olarak sunar. Bir veritabanı sürücüsünde ya da kilitlenmiş bir ortamda iseniz, bir php artisan queue:failed kaydı, saatlik bir kontrol ile temel bilgileri kaplayabilir.
Yeniden Deneme Stratejisi: Tüm Hatalar Eşit Değildir
Yeniden Deneme Stratejisi: Tüm Hatalar Eşit Değildir
Varsayılan yeniden deneme davranışı — N kez dene, sonra pes et — bazı işler için doğru iken diğerleri için yanlıştır. Hata modlarını düşünmek, iş yazmadan önce sizi yanlış varsayımlardan kurtarır.
Geçici Hatalar
Geçici Hatalar
Veritabanı aksamaları, kısa süreli ağ zaman aşımı — yeniden denedeki olası başarılar:
public int $tries = 3;
public int $backoff = 5; // Yeniden denemeler arasında 5 saniye bekleyin
Oran Sınırlı ya da Hizmet Dışı Hatalar
Oran Sınırlı ya da Hizmet Dışı Hatalar
Bir dış API mevcut değil, oran limiti aşılmış — artan bekleme süreleriyle geri çekil:
public int $tries = 5;
public function backoff(): array
{
// 30s, 60s, 120s, 240s, 480s arasında bekleyin
return [30, 60, 120, 240, 480];
}
public function retryUntil(): \DateTime
{
// Denemelerden bağımsız olarak, 24 saat sonra pes edin
// Uzun süreli arızalar sırasında işlerin birikmesini önler
return now()->addHours(24);
}
Kalıcı Hatalar
Kalıcı Hatalar
Yanlış formatta giriş, asla var olmayacak bir kayıt — hiç yeniden denemeyin:
public int $tries = 1;
public function failed(\Throwable $e): void
{
// Hemen uyar — bu, insan müdahalesi gerektirir, yeniden deneme değil
logger()->critical('Kalıcı iş hatası', [
'job' => static::class,
'error' => $e->getMessage(),
]);
}
Neden timeout ve tries farklı şeyler ve neden bu önemlidir: Bir iş $timeout ile karşılaşırsak, öldürülüp kuyrukta geri döner — bu, bir yeniden deneme olarak sayılır. Bir iş $tries değerini aşarsa failed_jobs alanına düşer. Eğer bir işin timeout = 120 ve tries = 3 olarak ayarlanırsa ve bu iş sürekli olarak 130 saniye alırsa, üç kez zaman aşımına uğrayacak ve failed_jobs alanına düşecektir — asla tamamlamaz ve bu süre zarfında üç işçi pozisyonunu tüketir.
Yaygın hata:
$timeoutdeğerini işin tipik yürütme süresinden daha düşük ayarlamak. Her seferinde iş çalıştığında, tamamlanmadan önce öldürülür, yeniden deneme olarak sayılır ve asla başarılı olmadan$triesdeğerini tüketir. Eğer bir iş 90 saniye gerektiriyorsa, en azından 120 verin. Zaman aşımı bir güvenlik ağıdır, hedef değildir.
Büyük Backlog’ları İşleme: Artisan Komut Deseni
Büyük Backlog’ları İşleme: Artisan Komut Deseni
Binlerce mevcut kayıt için iş göndermeniz gerektiğinde — bir backlog sıkıştırması, veri göçü, toplu yeniden hesaplama — gönderim mekanizması, işin kendisi kadar önemlidir.
Desen: veritabanını parça parça gezinen ve işleri kademeli olarak gönderen bir Artisan komutu. Gönderim yapmadan önce bir --dry-run seçeneği:
class ProcessBacklogCommand extends Command
{
protected $signature = 'media:compress-videos
{--id= : Belirli bir kaydı ID ile işleme}
{--dry-run : Hiçbir şey göndermeden ne olacağını göster}
{--chunk=200: İşlenmesi gereken kayıtlar her parçada}';
public function handle(): void
{
// Eğer --id sağlanmışsa, sorguyu o tek kayda sınırla
$query = Media::query()
->where('mime_type', 'like', 'video/%')
->whereNull('compressed_at')
->when($this->option('id'), fn($q) => $q->where(, $this->option()));
$total = $query->count();
$this->info("İşlenecek {$total} kayıt bulundu.");
if ($this->option()) {
$this->info();
return;
}
$dispatched = 0;
$query->chunkById((int) $this->option(), function($batch) use ($dispatched) {
foreach($batch as $record) {
CompressMediaVideo::dispatch($record->id)->onQueue();
$dispatched++;
}
$this->info("Şu ana kadar {$dispatched} iş gönderildi...");
});
$this->info("Tamam. Toplam gönderilen: {$dispatched}");
}
}
Neden chunkById yerine chunk? Laravel’in chunk() yöntemi SQL OFFSET kullanıyor — bu, veritabanının N satırı sayması ve atlaması gerektiği anlamına gelir. Bir milyon satırlık bir tabloda, 500. parça 100,000 satırı atlamak zorundadır. Bu, giderek yavaşlamaktadır. chunkById, WHERE id > $lastSeen LIMIT $chunk ifadesini kullanır — bir anahtar kümesi imleci. Sorgu süresi, konumdan bağımsız olarak sabittir. Büyük tablolarda, bu, birkaç dakikada tamamlanan bir komut ile zaman aşımına uğrayacak bir komut arasındaki farktır.
Neden --dry-run ilk sınıf bir seçenek olarak? Üretim verilerine karşı bir toplu gönderim gerçekleştirmeden önce ne olacağını görmek istersiniz. --dry-run eklemek bir maliyet oluşturmaz ve “40,000 iş gönderileceğini bilmiyordum” gibi birçok durumu önler.
Atomik Dosya İşlemleri: Çökmeye Göre Tasarlamak
Atomik Dosya İşlemleri: Çökmeye Göre Tasarlamak
Herhangi bir dosya yazan iş, yazma işlemi yarıda kalır diye varsaymalıdır. Ölçeklendiğinde, bu olur. Sorun, sistemin durumu ne olacağıdır.
Hedef yola doğrudan yazmak: bir çökme, orijinal dosya yerinde kısmi, bozuk bir dosya bırakır. Zarar kalıcıdır ve manuel müdahale gerektirir.
Önce geçici bir dosyaya yazın, ardından yeniden adlandırın:
$inputPath = $media->full_path;
$tempPath = $inputPath . . uniqid();
// Tüm yazımlar buraya gider — burada bir çökme, orijinal dosyayı etkilenmeden bırakır
$this->runFFmpeg($inputPath, $tempPath);
// Çıktı gerçekten daha iyi ise yalnızca değiştirin
if (filesize($tempPath) >= filesize($inputPath)) {
@unlink($tempPath);
return; // Orijinal zaten optimaldi — bunu değiştirmeyin
}
// rename() POSIX dosya sistemlerinde atomiktir — bu ya tamamlanır
// veya gerçekleşmez. Yarı yol durumu yok.
rename($tempPath, $inputPath);
Neden boyutları değiştirmeden önce karşılaştırıyorsunuz? Zaten sıkıştırılmış bir dosyayı yeniden kodlamak daha büyük bir çıktı üretebilir — kodlama yükü tasarrufu aşabilir. Her zaman çıktının iyileştirilmiş olduğunu doğrulayın ve ardından orijinalini silin.
Neden rename() atomik? Linux ve macOS üzerinde, rename() tek bir dosya sistemi sistem çağrısıdır. Çekirdek, dosyayı taşımanın ya da taşımamanın garantisini verir — bir ara durum yoktur; var olan dosya, yeni dosya tamamen yerinde olmadan duramaz.
İyi Tasarlanmış Bir İşin Yapısı
İyi Tasarlanmış Bir İşin Yapısı
Tüm bunları bir araya getirerek:
class CompressMediaVideo implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public int $backoff = 60;
// Bir ID alır, model değil
public function __construct(private readonly int $mediaId) {}
public function handle(VideoCompressorService $compressor): void
{
// Yürütme zamanında taze çeker
$media = Media::findOrFail($this->mediaId);
// Gönderimden beri durum değişikliklerini saklar
if (!str_starts_with($media->mime_type, )) {
return;
}
$compressor->compress($media);
}
// Hata işleme sonradan gelmemeli
public function failed(\Throwable $exception): void
{
logger()->error(, [
=> $this->mediaId,
=> $exception->getMessage(),
]);
}
}
Bu iş, bir kimlik alır, model değil, yürütme zamanında taze verileri alır, gönderimden beri değişikliklerin saklanmasını içerir, anlamlı bir failed() işleyiciye sahiptir ve zaman aşımı, yeniden deneme ve geri alma değerleri, framework varsayımlarından değil, gerçek iş özelliklerinden yansıtır.
Bu makalenin ana keşfi: Kuyruk hataları neredeyse asla framework hatası değildir — bunlar tasarım hatalarıdır. Kimlik göndermek yerine modelleri göndermek, açık zaman aşımı ve yeniden deneme değerleri ayarlamak ve işleri kuyruk türüne göre ayırmak, ilk iş dağıtılmadan önce alınması gereken kararlardır, ilk durumu yaşadıktan sonra değil.
Göndermeden Önce — Kontrol Listesi
Göndermeden Önce — Kontrol Listesi
- [ ] Kuyruk topolojisi tanımlandı — her şey
defaultiçin değil - [ ] Her iş yapıcısı bir ID alıyor, bir Eloquent model değil
- [ ] Her iş
$tries,$timeout, ve$backoffdeğerleri açıkça ayarlanmıştır — framework varsayımlarına bırakılmamıştır - [ ] Her işin anlamlı bir bağlamı kaydeden bir
failed()yöntemi vardır - [ ]
$timeoutdeğerleri işin tipik yürütme süresinden rahat bir şekilde daha yüksek ayarlanmıştır - [ ] Toplu gönderim komutları
chunkByIdkullanır, değilchunk - [ ] Toplu komutlar ile bir
--dry-runseçeneği vardır - [ ] Dosya yazan her iş önce
.tmpdosyasına yazar, ardındanrename()ile değiştirir — asla doğrudan hedef yola yazmaz
Önceki: Bölüm 1 — Denetim İzi
Kaynak: Orijinal Makale
- Neler Öğreneceksiniz
- Zihinsel Model: Bir Kuyruk, Bir Yapılacaklar Listesi Değildir
- Bir İş Yapıcısına Ne Koymalısınız — Ne Koymamalısınız
- Tek Sorumluluk İlkesi: İşlere Uygulandı
- Kuyruk Topolojisi: Kimsenin Yapması Gereken Karar
- Yeniden Deneme Stratejisi: Tüm Hatalar Eşit Değildir
- Büyük Backlog’ları İşleme: Artisan Komut Deseni
- Atomik Dosya İşlemleri: Çökmeye Göre Tasarlamak
- İyi Tasarlanmış Bir İşin Yapısı
- Göndermeden Önce — Kontrol Listesi


