Bir Laravel projesi yerel geliştirme ortamında oldukça hızlı çalışırken canlı sunucuya taşındığında belirgin biçimde yavaşlayabilir. Sayfalar geç açılır, formlar bekletir, yönetim panelindeki tablolar ağırlaşır ve daha önce fark edilmeyen küçük gecikmeler kullanıcı deneyimini bozmaya başlar.
Böyle bir durumda ilk şüpheli genellikle sunucu olur.
“RAM yetmiyor olabilir”, “İşlemci zayıf”, “Hosting yavaş” veya “Laravel zaten ağır bir framework” gibi yorumlar yapılır. Bazen gerçekten sorun sunucu kaynaklarıdır. Fakat çoğu projede doğrudan daha güçlü bir sunucuya geçmek, asıl problemi yalnızca bir süreliğine gizler.
Çünkü yerel ortam ile canlı sunucu arasındaki fark sadece donanımdan ibaret değildir.
Yerelde birkaç kullanıcı, sınırlı veri ve sıfıra yakın trafik vardır. Veritabanındaki kayıt sayısı düşüktür. E-posta, görsel işleme veya harici API bağlantıları çoğu zaman gerçek koşullarda çalışmaz. Canlı ortamda ise bütün bu yükler aynı isteğin içine girebilir.
Aynı kod çalışıyor olabilir ama çalıştığı şartlar aynı değildir.
Önce Gerçekten Nerede Yavaşladığını Bulmak Gerekir
“Site yavaş” ifadesi tek başına teşhis için yeterli değil.
Ana sayfa mı yavaş? Yalnızca giriş yaptıktan sonra mı sorun yaşanıyor? Listeleme sayfaları hızlıyken detay sayfası mı bekletiyor? İlk açılış mı uzun sürüyor, yoksa her istek mi aynı şekilde gecikiyor?
Bunların her biri farklı bir soruna işaret edebilir.
Örneğin ilk istek yavaş, sonraki istekler hızlıysa cache, OPcache veya uygulamanın ilk yüklenme maliyeti incelenebilir. Listeleme sayfası veri arttıkça ağırlaşıyorsa veritabanı sorguları ve indexler öne çıkar. Form gönderimi uzun sürüyorsa aynı istek içinde e-posta, görsel işleme veya harici servis çağrısı yapılıyor olabilir.
Bu nedenle optimizasyona başlamadan önce şu ayrımı yapmak gerekir:
Sunucu ilk baytı geç mi gönderiyor?
HTML hızlı geliyor ama görseller ve JavaScript dosyaları mı geç yükleniyor?
Belirli bir veritabanı sorgusu mu bekletiyor?
Harici bir API cevabı mı gecikiyor?
Queue ile çalışması gereken bir işlem web isteğinin içinde mi yapılıyor?
Sorun bütün kullanıcılarda mı, yalnızca belirli bir işlemde mi görülüyor?
Bu sorulara cevap vermeden yapılan optimizasyonlar genellikle tahmine dayanır. Birkaç cache komutu çalıştırılır, PHP ayarları değiştirilir ve sonuç beklenir. Sorun düzelmezse bu kez sunucu paketi yükseltilir.
Benim tercih ettiğim yol, önce gecikmenin hangi katmanda oluştuğunu bulmak.
Basit bir başlangıç için sayfanın yanıt süresi terminalden ölçülebilir:
curl -o /dev/null -s \
-w "DNS: %{time_namelookup}\nConnect: %{time_connect}\nTTFB: %{time_starttransfer}\nTotal: %{time_total}\n" \
https://example.comBuradaki TTFB, sunucunun ilk baytı göndermeye başlamasının ne kadar sürdüğünü gösterir. Değer yüksekse uygulama, veritabanı veya sunucu tarafına bakmak gerekir. İlk bayt hızlı geldiği hâlde sayfanın tamamlanması uzun sürüyorsa görseller, fontlar, JavaScript dosyaları ve üçüncü taraf kaynaklar ayrıca incelenmelidir.
Tek bir ölçümle karar vermek de doğru olmaz. Aynı adresi birkaç kez test etmek ve sorun yaşanan sayfaları ayrı ayrı karşılaştırmak gerekir.
Yerelde Görünmeyen Veritabanı Maliyeti
Laravel projelerinde canlı ortam performansını bozan en yaygın konulardan biri veritabanı sorgularıdır.
Yerel veritabanında 20 araç, 10 blog yazısı veya 50 kullanıcı varken birçok sorgu hızlı çalışır. Aynı tablo canlı ortamda yüz binlerce kayda ulaştığında daha önce önemsiz görünen kararlar ciddi gecikmeler oluşturmaya başlar.
Özellikle ilişkili verilerin yanlış yüklenmesi beklenenden çok daha fazla sorgu üretebilir.
Şöyle bir kullanım düşünelim:
$vehicles = Vehicle::query()->get();
foreach ($vehicles as $vehicle) {
echo $vehicle->brand->name;
}İlk sorgu araçları getirir. Ardından her araç için marka ilişkisi ayrı ayrı sorgulanabilir. Kayıt sayısı arttıkça toplam sorgu sayısı da artar.
İlişki gerçekten kullanılacaksa eager loading uygulanmalıdır:
$vehicles = Vehicle::query()
->with('brand')
->get();Ancak burada da diğer uca savrulmak mümkün.
“Tekrar sorgu oluşmasın” düşüncesiyle ihtiyaç duyulan veya duyulmayan bütün ilişkileri with() içine eklemek sorgu sayısını azaltırken taşınan veri miktarını büyütebilir. Özellikle görseller, kullanıcılar, kategoriler, özellikler ve çok sayıda alt ilişki aynı anda yüklendiğinde tek bir sayfa gereğinden fazla veri çekebilir.
Doğru yaklaşım, gerçekten kullanılacak ilişkileri ve alanları yüklemektir:
$vehicles = Vehicle::query()
->select(['id', 'brand_id', 'name', 'slug', 'price'])
->with('brand:id,name')
->latest()
->paginate(24);Bir listedeki kayıtların tamamını get() ile almak da canlı ortamda sorun çıkarabilir. Yerelde 50 kayıtla çalışan yapı, canlıda 50 bin kaydı belleğe taşımaya çalışabilir.
Listeleme sayfasında bütün kayıtların gerçekten gerekli olup olmadığı sorulmalıdır. Çoğu durumda paginate(), simplePaginate() veya uygun senaryolarda cursorPaginate() kullanmak daha doğru olur.
Sayısal bilgi göstermek için ilişkili kayıtların tamamını yüklemek de gereksizdir:
$brands = Brand::query()
->withCount('vehicles')
->get();Bu yaklaşım yalnızca araç sayısı gerekiyorsa bütün araç kayıtlarını belleğe almaktan daha temizdir.
Laravel, sorguları izlemek için DB::listen() ve belirli bir eşik üzerinde veritabanında geçirilen süreyi takip etmek için whenQueryingForLongerThan() gibi araçlar sunuyor. Bunlar ölçülü kullanıldığında hangi sorguların uygulamayı ağırlaştırdığını bulmayı kolaylaştırır. Laravel veritabanı dokümantasyonu
Her where Alanına Index Eklemek de Çözüm Değil
Yavaş sorgular görüldüğünde bir diğer yaygın tepki, sorguda kullanılan bütün kolonlara index eklemektir.
Indexler doğru kullanıldığında ciddi fark oluşturabilir. Ancak her kolona rastgele index eklemek veritabanını otomatik olarak hızlandırmaz. Gereksiz indexler depolama alanını büyütür ve kayıt ekleme ya da güncelleme işlemlerinin maliyetini artırır.
Index kararı sorgunun gerçek kullanımına göre verilmelidir.
Örneğin sık kullanılan bir sorgu şu yapıdaysa:
Vehicle::query()
->where('is_active', true)
->where('status', '!=', 'sold')
->where('condition', 'used')
->latest('published_at')
->paginate(24);Hangi indexin fayda sağlayacağını yalnızca kolonlara bakarak tahmin etmek yerine sorgu planını incelemek gerekir. MySQL veya MariaDB tarafında EXPLAIN, veritabanının hangi indexi kullandığını ve kaç satırı taramayı beklediğini gösterir.
Sorun bazen index eksikliği değil, sorgunun kendisidir. Fonksiyon uygulanmış kolonlar, başında joker bulunan LIKE aramaları, gereksiz OR koşulları veya çok büyük join işlemleri index kullanımını sınırlayabilir.
Bu nedenle veritabanı optimizasyonunda doğru sıra şudur: Önce yavaş sorguyu bulmak, ardından sorgu planını incelemek ve son olarak gerçekten gerekli indexi eklemek.
Tahminle değil, ölçümle ilerlemek gerekiyor.
Uygulama Üretim Ortamına Hazır mı?
Yerelde geliştirme kolaylığı için kullandığımız bazı ayarlar üretim ortamında bırakıldığında hem performans hem de güvenlik açısından sorun oluşturabilir.
İlk kontrol edilmesi gereken ayarlar şunlardır:
APP_ENV=production
APP_DEBUG=falseAPP_DEBUG=true değerinin canlı ortamda bırakılması yalnızca performans konusu değildir. Hata ekranlarında uygulama yapısı, dosya yolları, sorgular veya hassas olabilecek başka bilgiler görüntülenebilir.
Üretim ortamında yapılandırma, route ve view cache işlemleri de deploy sürecinin parçası olmalıdır.
php artisan config:cache
php artisan route:cache
php artisan view:cacheLaravel’in güncel deployment dokümantasyonu, production deploy sırasında yapılandırma cache’inin kullanılmasını öneriyor. Route ve view cache de uygulamanın her istekte aynı yapıları tekrar hazırlamasını azaltıyor. Laravel deployment dokümantasyonu
Bunları tek komutla çalıştırmak için şu komut da kullanılabilir:
php artisan optimizeAncak cache komutlarını çalıştırmak her problemi çözmez. Yanlış yapılandırılmış bir projeyi optimize komutuyla hızlı hâle getiremeyiz.
Ayrıca config:cache sonrasında .env dosyasındaki değerlerin uygulama içinde doğrudan env() ile okunmaması gerekir. env() kullanımı config dosyalarıyla sınırlandırılmalı, uygulamanın diğer bölümlerinde config() üzerinden erişilmelidir.
Örneğin:
// Uygulama içinde bunu kullanmak yerine:
env('PAYMENT_API_URL');
// Önce config/services.php içinde tanımlayıp:
'payment' => [
'url' => env('PAYMENT_API_URL'),
],
// Daha sonra bunu kullanmak gerekir:
config('services.payment.url');Cache temizlemek gerektiğinde bütün komutları alışkanlıkla arka arkaya çalıştırmak yerine hangi cache’in neden temizlendiğini bilmek daha sağlıklıdır.
php artisan optimize:clearBu komut geliştirme veya sorun giderme sırasında faydalı olabilir. Fakat üretim ortamında cache’leri temizledikten sonra uygun optimizasyon komutlarının tekrar çalıştırılması unutulmamalıdır.
Web İsteği Her İşi Kendi İçinde Yapmamalı
Bir kullanıcı formu gönderdiğinde uygulama aynı istek içinde e-posta gönderiyor, büyük görselleri yeniden boyutlandırıyor, PDF hazırlıyor ve harici API’den cevap bekliyorsa sayfanın geç dönmesi normaldir.
Bu işlemlerin bazıları başarılı biçimde tamamlanmış olsa bile kullanıcı birkaç saniye boş ekrana bakar.
Kullanıcının anında sonuç görmesi gerekmeyen işler queue üzerinden arka planda çalıştırılmalıdır. Örneğin e-posta gönderimi, toplu bildirim, rapor hazırlama, görsel dönüştürme ve üçüncü taraf servislere veri gönderme gibi işlemler job olarak ele alınabilir.
ProcessVehicleImages::dispatch($vehicle->id);E-posta da doğrudan istek içinde gönderilmek yerine kuyruğa alınabilir:
Mail::to($user)->queue(new ApplicationReceived($application));Fakat kodu queue uyumlu hâle getirmek işin yalnızca bir bölümüdür. Canlı sunucuda worker çalışmıyorsa işler kuyrukta beklemeye devam eder.
Production ortamında queue:work sürecinin Supervisor gibi bir süreç yöneticisi tarafından izlenmesi gerekir. Worker sonlandığında otomatik olarak yeniden başlatılmalı, deploy sonrasında ise eski kodla çalışmaya devam etmemesi için yeniden yüklenmelidir:
php artisan queue:restartLaravel dokümantasyonu da queue:work süreçlerinin uzun süre çalışan süreçler olduğunu ve bir process monitor tarafından yönetilmesi gerektiğini belirtiyor. Laravel queue dokümantasyonu
Queue kullanmak uygulamayı sihirli biçimde daha az işlem yapar hâle getirmez. Yalnızca kullanıcının beklemesi gerekmeyen işi web isteğinin dışına taşır. Toplam işlem yükü yine vardır ve worker sayısı, timeout, retry ayarları ile kuyruk altyapısı doğru planlanmalıdır.
Harici Servisler Uygulamanın Hızını Belirleyebilir
Bazen Laravel tarafındaki sorgular hızlıdır ve sunucu kaynaklarında sorun görünmez. Buna rağmen belirli bir işlem sürekli bekler.
Bu durumda harici bağlantılar incelenmelidir.
Ödeme sistemleri, kargo servisleri, e-posta sunucuları, döviz kuru servisleri, harita API’leri veya başka bir uygulamaya yapılan HTTP istekleri sayfa yanıtını doğrudan etkileyebilir.
Harici isteklerde timeout belirlenmemişse uygulama servis cevap verene kadar uzun süre bekleyebilir:
$response = Http::timeout(10)
->connectTimeout(3)
->get($url);Her servisin aynı timeout değerine ihtiyacı yoktur. Önemli olan bağlantının sonsuza kadar beklememesidir.
Başarısız bir isteği yeniden denemek gerekiyorsa retry() kullanılabilir. Fakat kullanıcı isteğinin içinde yapılan agresif tekrarlar sayfanın daha da geç dönmesine neden olabilir. Kritik olmayan entegrasyonları kuyruğa taşımak çoğu zaman daha doğru olur.
Ayrıca her sayfa açılışında aynı harici veriyi tekrar çekmek gerekmeyebilir. Döviz kuru, kategori listesi veya sık değişmeyen bir servis cevabı belirli süreyle cache’lenebilir.
Burada cache süresi verinin niteliğine göre belirlenmelidir. Saatlik güncellenmesi gereken bir bilgiyi bir hafta cache’lemek ne kadar yanlışsa, gün boyunca değişmeyen bir veriyi her istekte tekrar çekmek de o kadar gereksizdir.
Cache Kullanmak ile Her Şeyi Cache’lemek Aynı Şey Değil
Cache, performans sorunlarında ilk akla gelen çözümlerden biri. Doğru yerde kullanıldığında veritabanı ve harici servis yükünü ciddi biçimde azaltabilir.
Örneğin sık kullanılan ama seyrek değişen bir veri şu şekilde saklanabilir:
$brands = Cache::remember(
'active-brands',
now()->addHour(),
fn () => Brand::query()
->where('is_active', true)
->orderBy('name')
->get()
);Fakat cache’in ne zaman temizleneceği düşünülmediyse eski veriler gösterilmeye başlanır. Marka güncellenir ama kullanıcı hâlâ eski adı görür. Yeni bir kayıt eklenir fakat listeye yansımaz.
Bu nedenle cache eklerken şu üç sorunun cevabı belli olmalıdır:
Veri ne kadar süre geçerli kalacak?
Hangi işlem gerçekleştiğinde cache temizlenecek?
Aynı veri kullanıcıya, dile veya şirkete göre değişiyor mu?
Çok kiracılı veya çok dilli yapılarda herkese aynı cache anahtarını kullanmak yanlış sonuçlar doğurabilir. Anahtarın firma, dil veya ilgili diğer kapsam bilgilerini içermesi gerekebilir.
Cache, yavaş sorguyu gizlemek için de kullanılmamalıdır. Önce sorgunun neden yavaş olduğu anlaşılmalı, ardından gerçekten tekrar tekrar üretilmesi gerekmeyen sonuçlar cache’e alınmalıdır.
PHP-FPM ve OPcache Tarafını Atlamamak Gerekir
Uygulama ve veritabanı tarafı düzgün olduğu hâlde performans sorunu devam ediyorsa PHP çalışma ortamı incelenmelidir.
Laravel projelerinde PHP-FPM pool ayarları, aynı anda kaç isteğin işlenebileceğini doğrudan etkiler. pm.max_children değeri sunucunun kaldırabileceğinden çok düşükse yoğunluk sırasında istekler sıraya girer. Gereğinden yüksekse bu kez PHP süreçleri RAM’i tüketebilir ve sunucu swap kullanmaya başlayabilir.
Burada internette bulunan tek bir değeri kopyalamak doğru değil. Her PHP sürecinin ortalama bellek tüketimi ve sunucunun kullanılabilir RAM’i ölçülerek karar verilmelidir.
OPcache’in etkin olması da önemlidir. OPcache kapalıysa PHP dosyaları her istekte yeniden derlenir.
Aktif PHP binary’sine göre durum şu komutla kontrol edilebilir:
php -i | grep -E "opcache.enable|opcache.memory_consumption"cPanel veya CloudLinux kullanılan sunucularda terminaldeki php komutu ile sitenin PHP-FPM üzerinden kullandığı sürüm aynı olmayabilir. Terminalde PHP 8.5 görünürken site PHP 8.3-FPM ile çalışıyor olabilir.
Bu yüzden kontrol yalnızca CLI üzerinden yapılmamalı. Alan adının bağlı olduğu PHP-FPM pool ve ilgili php.ini ayarları ayrıca doğrulanmalıdır.
Aksi hâlde doğru ayarı yanlış PHP sürümünde değiştirip hiçbir sonuç alamamak mümkündür.
Sunucu Kaynaklarına Bakmadan da Olmaz
“Her şey uygulamadan kaynaklanır” demek, “her şey sunucudan kaynaklanır” demek kadar yanlış olur.
Uygulama tarafındaki kontrollerden sonra sunucu kaynakları incelenmelidir.
Başlangıç için şu komutlar yeterli fikir verebilir:
topfree -mdf -huptimeCPU kullanımı sürekli yüksek mi? RAM tükenmiş mi? Swap kullanılıyor mu? Disk dolmak üzere mi? Load average sunucunun kapasitesine göre normal mi?
Disk doluluğu özellikle gözden kaçabiliyor. Log dosyaları, eski yedekler, cache dosyaları veya kullanıcı yüklemeleri diski doldurduğunda yalnızca Laravel değil; veritabanı, e-posta ve diğer sunucu servisleri de etkilenebilir.
Disk I/O sorunu varsa işlemci ve RAM boş görünmesine rağmen uygulama yavaş çalışabilir. iostat kuruluysa daha ayrıntılı inceleme yapılabilir:
iostat -xz 1
Paylaşımlı veya sanallaştırılmış ortamlarda komşu hesapların tüketimi de performansı etkileyebilir. Özellikle CPU limiti, I/O limiti veya eş zamanlı işlem sınırı bulunan hosting ortamlarında uygulama kendi kaynaklarını aşmasa bile hesap düzeyinde kısıtlama görebilir.
Sunucunun fiziksel olarak kullanıcı kitlesinden çok uzakta bulunması da ağ gecikmesini artırabilir. Fakat saniyeler süren bir backend cevabını yalnızca sunucu lokasyonuna bağlamak genellikle doğru değildir. Coğrafi gecikme çoğu zaman milisaniye düzeyindedir; uzun beklemelerin arkasında uygulama veya altyapı tarafında başka bir neden bulunur.
Görseller ve Frontend Tarafı da “Laravel Yavaş” Hissi Oluşturabilir
Backend hızlı cevap verdiği hâlde sayfa kullanıcıya geç açılıyor gibi görünebilir.
Büyük JPEG veya PNG görseller, gereksiz yüksek çözünürlük, çok sayıda font dosyası, render-blocking JavaScript ve kullanılmayan CSS sayfanın tamamlanmasını geciktirebilir.
Örneğin kart içinde 400 piksel genişliğinde gösterilen bir görselin 6000 piksel olarak yüklenmesi backend sorunu değildir. Fakat kullanıcı bunu “site yavaş” şeklinde tarif eder.
Görseller ihtiyaç duyulan ölçülerde hazırlanmalı, uygun kaliteyle WebP veya desteklenen başka modern formatlara dönüştürülmeli ve ekran dışında kalanlar lazy loading ile yüklenmelidir.
Aynı görselin her yerde tek boyutla kullanılması yerine kullanım alanına göre farklı sürümler üretilebilir. Küçük bir blog kartıyla detay sayfasındaki geniş kapak görselinin aynı dosyayı yüklemesi gerekmiyor.
Vite production build’inin alınmamış olması, geliştirme dosyalarının yanlış kullanılması veya üçüncü taraf scriptlerin kontrolsüz biçimde eklenmesi de frontend performansını etkileyebilir.
Bu yüzden yalnızca Laravel Debugbar’daki süreye bakarak bütün sayfanın hızlı olduğuna karar veremeyiz. Tarayıcının Network ve Performance sekmeleri de incelenmelidir.
Ben Hangi Sırayla Kontrol Ederim?
Performans sorunu yaşandığında her ayarı aynı anda değiştirmek, neyin işe yaradığını anlamayı zorlaştırır.
Ben teşhisi genel olarak şu sırayla ele alırım:
Sorunu tekrarlarım: Hangi sayfanın, hangi işlemde ve hangi kullanıcı koşulunda yavaşladığını netleştiririm.
Yanıt süresini ayırırım: Backend cevabı mı geç geliyor, yoksa tarayıcı kaynakları mı bekliyor?
Sorguları incelerim: Sorgu sayısı, yavaş sorgular, N+1 ilişkileri, taşınan veri ve sorgu planlarına bakarım.
Ağır işlemleri kontrol ederim: E-posta, görsel işleme, raporlama ve harici API çağrılarının request içinde yapılıp yapılmadığını araştırırım.
Production ayarlarını doğrularım: Debug, cache, deploy komutları, PHP sürümü ve OPcache durumunu kontrol ederim.
Queue süreçlerine bakarım: Worker gerçekten çalışıyor mu, işler birikiyor mu ve Supervisor doğru PHP binary’sini mi kullanıyor?
Sunucu kaynaklarını ölçerim: CPU, RAM, swap, disk, I/O ve hesap limitlerini incelerim.
Frontend yükünü değerlendiririm: Görseller, fontlar, CSS, JavaScript ve üçüncü taraf kaynakları kontrol ederim.
Tek tek değişiklik yaparım: Her düzenlemeden sonra aynı senaryoyu yeniden ölçerim.
Bu sıra her projede birebir aynı olmak zorunda değil. Belirtiler hangi katmana işaret ediyorsa oradan başlamak daha mantıklı olabilir. Önemli olan, aynı anda beş farklı ayarı değiştirip hangisinin sonucu etkilediğini kaybetmemek.
Daha Güçlü Sunucu Her Zaman Daha Hızlı Uygulama Demek Değil
Yavaş çalışan bir Laravel projesi daha güçlü bir sunucuya taşındığında elbette hızlanabilir. Daha fazla işlemci, RAM ve hızlı disk uygulamaya nefes aldırır.
Ancak bir sayfa yüzlerce gereksiz sorgu çalıştırıyorsa, bütün kayıtları belleğe yüklüyorsa veya kullanıcı isteği içinde harici servislerin cevabını bekliyorsa yeni sunucu yalnızca aynı hataları daha hızlı çalıştırır.
Veri ve trafik arttığında problem yeniden ortaya çıkar.
Bu nedenle sunucu yükseltmeden önce uygulamanın ne yaptığına bakmak gerekir. Ardından mevcut yük için kaynakların gerçekten yeterli olup olmadığı değerlendirilmelidir.
İyi performans tek bir komutla veya tek bir sunucu ayarıyla elde edilmiyor. Uygulama kodu, veritabanı, queue sistemi, PHP-FPM, web sunucusu ve frontend birlikte çalışıyor. Yavaşlık da çoğu zaman bu katmanlardan yalnızca birinde değil, birkaçındaki küçük sorunların birleşmesiyle oluşuyor.
Laravel projesi yerelde hızlıyken canlıda yavaşlıyorsa ilk soru “Sunucuyu ne kadar büyütmeliyim?” olmamalı.
Önce şu soruyu sormak daha doğru:
Uygulama zamanını ve kaynaklarını gerçekte nerede harcıyor?
Bu sorunun cevabı bulunduğunda çözüm de genellikle çok daha net hâle geliyor.
Yaman HACIOĞLU
Yorumlar (0)
Henüz yorum yapılmamış. İlk yorumu siz bırakın.
Yorum bırak