{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Muhammet Şafak — Mühendislik",
  "home_page_url": "https://muhammetsafak.com/tr/muhendislik/",
  "feed_url": "https://muhammetsafak.com/tr/muhendislik/feed.json",
  "description": "Üretim dersleri, mimari kararlar, veritabanı, sunucu ve uygulama patternleri ve AI-destekli mühendislik üzerine notlar, sistem incelemeleri, denemeler ve tarayıcıda çalışan geliştirici araçları.",
  "language": "tr-TR",
  "authors": [
    {
      "name": "Muhammet Şafak",
      "url": "https://muhammetsafak.com"
    }
  ],
  "items": [
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/upgrade-tetikleyicileri/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/upgrade-tetikleyicileri/",
      "title": "Upgrade Tetikleyicileri: Zorunlu, Kilitleyen, Fırsat",
      "summary": "Dependabot PR'ı, EOL duyurusu, deprecation uyarısı ve CVE bülteni aynı kuyruğa düşer ama aynı aciliyette değildir. EOL ve etkilendiğiniz CVE zorunludur, bağımlılık kilidi ve deprecation kilitleyendir, özellik ve performans fırsattır. Her türe bir tepki süresi yazmak, hatırlatmaya koşmayı ve kilide geç kalmayı önler.",
      "content_html": "<p>Dependabot’un açtığı PR’ların çoğu zorunluluk değil, hatırlatmadır.</p>\n<p>Kuyrukta dört tür sinyal birbirine karışıyor: Dependabot PR’ları, deprecation uyarıları, EOL duyuruları ve CVE bültenleri. Her biri farklı bir kaynaktan, farklı bir nedenle doğuyor ama hepsi aynı listeye, aynı tonda düşüyor. Ekran aynı görünüyor, iş aynı büyüklükte değil.</p>\n<p>Zorunlu olan başka yerden gelir: bir EOL tarihinden, bir CVE’den ya da sizi eski sürüme kilitleyen bir bağımlılıktan. Üçüne aynı tepkiyi veren ekip ya geç kalır ya da boşuna koşar.</p>\n<h2 id=\"kuyruğa-iki-ayrı-saat-düşer\">Kuyruğa iki ayrı saat düşer</h2>\n<p>Bir PR’ın ne kadar acil olduğu başlığında yazmaz; onu hangi saatin açtığında yazar: biri bir advisory’ye, öteki <code>dependabot.yml</code>’deki takvime bağlıdır.</p>\n<p>GitHub’ın belgelerine göre security updates açıldığında, güvenlik açığı olan bir bağımlılık bulunduğunda yeni PR açılır. Version updates ise yeni bir sürüm çıktığında açılır ve <code>dependabot.yml</code>’de tanımladığınız sıklıkla çalışır. Birincisini dış dünyada olan bir şey tetikler, ikincisini sizin yazdığınız takvim.</p>\n<p>Bir ayrıntı daha: <code>cooldown</code> ayarı yalnız version updates için PR açılışını geciktirir; security updates’e uygulanmaz. Yani PR gürültüsünü kısmak için kurduğunuz ayar, güvenlik PR’ını susturmaz.</p>\n<p>Pratik sonuç şu: haftalık takvimle gelen version update PR’ı, bir advisory’den doğan PR ile aynı kuyrukta durur ama aynı saatin işi değildir. Birini bu hafta, ötekini bu çeyrek ele alabilirsiniz.</p>\n<p>Bir PR’ı açmadan önce iki şeye bakın: kim açtı, hangi etiketle geldi. Security update ise soru artık sürüm farkı değil, etkilenip etkilenmediğiniz. Version update ise soru, o sürümün bir şeyi çözüp çözmediği. İkincisinin cevabı çoğu zaman “hayır, yeni bir sürüm çıkmış, hepsi bu” olacak.</p>\n<h2 id=\"eol-bir-tarih-cve-bir-olay\">EOL bir tarih, CVE bir olay</h2>\n<p>EOL önceden bilinir. <a href=\"https://www.php.net/supported-versions.php\">php.net</a> her dalın security desteğinin bitiş tarihini yayımlıyor ve politika basit: yayından itibaren iki yıl aktif destek, ardından iki yıl yalnız güvenlik düzeltmesi. PHP 8.2’nin security desteği <strong>31 Aralık 2026</strong>’da bitiyor. Bu yazının yayın tarihinden yaklaşık 9,5 hafta sonra.</p>\n<p>Framework katmanı da kendi saatini tutuyor. Laravel’in <a href=\"https://laravel.com/docs/12.x/releases\">12.x sürüm notları</a> şöyle diyor: bug fix’ler 18 ay, security fix’ler 2 yıl sağlanır. Laravel 12 için bu, bug fix’lerin 13 Ağustos 2026’da bittiği, security fix’lerin 24 Şubat 2027’ye kadar sürdüğü anlamına geliyor. Yani bu yazının yayımlandığı gün Laravel 12 çalıştıran bir ekip, bug fix desteğini çoktan kaybetmiş, security desteğini henüz kaybetmemiş durumda.</p>\n<p>CVE ise takvime uymaz. Salı sabahı çıkabilir, hangi sürümü çalıştırdığınıza bakmaz.</p>\n<p>EOL’ün sonrasını da abartmamak gerekiyor. Upstream desteğinin bitmesi, düzeltme gelmeyeceği anlamına gelmez; gelmesi sizin kontrolünüzden çıkar. Ubuntu’nun <a href=\"https://ubuntu.com/security/notices/USN-6305-2\">USN-6305-2</a> bülteni bunun örneği: PHP 7.4’ün upstream desteği Kasım 2022’de bitmişti; 27 Şubat 2024’te Ubuntu, CVE-2023-3823 ve CVE-2023-3824 için PHP paketlerine düzeltme yayımladı. 20.04’teki <code>php7.4</code> bunu standart depodan aldı; 18.04’teki <code>php7.2</code> ve 16.04’teki <code>php7.0</code> için ESM ve Ubuntu Pro gerekti.</p>\n<p>Bu bir çıkış yolu olabilir. Ama bedeli bir satıcıya bağlı kalmak, düzeltmenin hangi paketle ve hangi hızla geleceğini onun takvimine bırakmak.</p>\n<p>EOL tarihinin değeri öngörülebilir olmasında. Tarihi geriye doğru hesaplayabilirsiniz: bitiş gününden önce production’da olmak istediğiniz sınırı koyun, oradan staging’e, oradan test işine geri sayın. Aşağıdaki önerimi 8.2’ye uygularsam sınır 30 Eylül 2026 olurdu. Bu yazıyı okurken hâlâ 8.2 çalıştırıyorsanız o sınır geçmiş durumda; bu bir suçlama değil, planı bugün yazmanın nedeni.</p>\n<h2 id=\"kilitleyen-tetikleyici-görünmez\">Kilitleyen tetikleyici görünmez</h2>\n<p>Kilitleyen tetikleyici ne bir tarih taşır ne bir bülten. Bir paket eski runtime’ı desteklemeyi bıraktığında yeni sürümü kurulamaz hâle gelir, kimse de size haber vermez.</p>\n<p>PHP 7.1 vakasında tetikleyici EOL değildi, bağımlılık kilidiydi; ayrıntısı <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/\">Upgrade’i Ertelemenin Maliyeti</a> yazısında.</p>\n<p>Kilidin bir EOL gibi tarihi olmadığı için ölçmeniz gerekir: bekleyen paket güncellemelerinden kaçı, runtime sürümünüz yüzünden kurulamıyor? Sıfırdan büyük her sayı, bir kapının kapandığını gösterir.</p>\n<p>Kilit sessiz büyür. Eski runtime’da her şey çalışmaya devam ettiği için hiçbir alarm üretmez; ilk görünür sinyal, güncellemek istediğiniz paketin kurulamadığı gündür. O gün tür artık fırsat değil, kilitleyendir ve çıkışın tek parça olması muhtemeldir.</p>\n<h2 id=\"üç-tür-üç-tepki-süresi\">Üç tür, üç tepki süresi</h2>\n<p>Sinyalleri üç türe ayırıyorum:</p>\n<ul>\n<li><strong>Zorunlu:</strong> dışarıdan gelen ve tarihi ya da riski sizin tercihinizden bağımsız olan sinyal (EOL, etkilendiğiniz CVE).</li>\n<li><strong>Kilitleyen:</strong> bugün bir şey bozmayan ama yarın seçeneğinizi daraltan sinyal.</li>\n<li><strong>Fırsat:</strong> yapmazsanız hiçbir şeyin bozulmadığı, yaparsanız kazanç sağlayan sinyal.</li>\n</ul>\n<p>Aşağıdaki tepki süreleri benim önerim. Sektör standardı değil; kendi ekibinizin kapasitesine göre kaydırın, ama bir süre yazın. Süresi olmayan sinyal kuyrukta bekler, kuyrukta bekleyen sinyal de sonunda “acil” olarak geri döner.</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"Tablo 1\"><table><thead><tr><th>Sinyal</th><th>Tür</th><th>Tepki süresi (önerim)</th></tr></thead><tbody><tr><td>Etkilendiğiniz bir CVE</td><td>Zorunlu</td><td>Triage aynı iş günü, yama günler içinde</td></tr><tr><td>Etkilenmediğiniz bir CVE</td><td>Fırsat</td><td>Normal döngü; etkilenmediğinizin kaydı tutulur</td></tr><tr><td>EOL tarihi</td><td>Zorunlu</td><td>Tarih takvime girer; production’da bitiş tarihinden en az 3 ay önce</td></tr><tr><td>Bağımlılık kilidi</td><td>Kilitleyen</td><td>Bir sonraki çeyrek planına girer</td></tr><tr><td>Deprecation uyarısı</td><td>Kilitleyen</td><td>Bir sonraki major’dan önce</td></tr><tr><td>Özellik ya da performans</td><td>Fırsat</td><td>Backlog; ölçülmüş bir kazanç yoksa tarih yok</td></tr></tbody></table></div>\n<p>Tablonun iki satırı ayrıca durmayı hak ediyor. Önce CVE: aynı bülten, sizin kullandığınız kod yoluna dokunuyorsa zorunlu, dokunmuyorsa fırsattır. Aciliyeti bültenin başlığı değil, sizin sisteminiz belirler. Bunu belirlemek için iki soru yeter: etkilenen bileşeni kullanıyor muyum, etkilenen kod yoluna dışarıdan erişilebiliyor mu? İkisine de “evet” diyorsanız tablodaki ilk satırdasınız; düzeltme henüz sizin sürüm dalınıza gelmediyse bu, süreyi uzatmaz, işi yamadan geçici önleme çevirir. Cevaplardan biri “hayır” ise bunu kayda geçirin; kayıt, bir ay sonra aynı bülteni yeniden tartışmanızı engeller. Sonra performans: “yeni sürüm daha hızlı” cümlesi bir fırsattır, bir zorunluluk değil. Ölçmeden o cümleyle takvim açmayın.</p>\n<p>Kilitleyen satırlarda süre neden gün değil çeyrek? Çünkü kilit bugün bir şey bozmuyor; ama her çeyrek bir paket daha eski runtime’ı bırakırsa çıkışın maliyeti büyür. Çeyrek, bunu fark etmek ve bir sonraki planlamaya yazmak için yeterince kısa bir süre.</p>\n<blockquote><p><strong>Tan:</strong> Türü ve tepki süresini sinyalin durduğu yere yazmak, tabloyu ayrı bir sayfada tutmaktan daha çok işe yarar: issue etiketi ya da PR açıklamasında tek satır yeter. Kuyruğa bakan kişi kararı listeden okur, wiki'yi açmak zorunda kalmaz. \"Etkilenmiyoruz\" kaydı da aynı yerde durur; bülten bir ay sonra yeniden düştüğünde cevap zaten oradadır.</p></blockquote>\n<h2 id=\"bu-çerçevenin-sınırı\">Bu çerçevenin sınırı</h2>\n<p>Bu sınıflandırma karar matrisinin yerini tutmaz. Tür, aciliyetin üst sınırını söyler; hangi sistemde, hangi riskle, hangi sırayla yapılacağını söylemez.</p>\n<p>Aynı CVE bir iç araçta fırsat, ödeme akışında zorunlu olabilir. O ayrımı sistemin kritikliği yapar. Karar matrisi serinin Bölüm 3’ünde.</p>\n<p>Tür ayrıca sabit değildir. Bir fırsat, ertelendikçe kilitleyene döner; bir kilit, EOL tarihi yaklaştıkça zorunluya. Sınıflandırmayı bir kez yapıp kuyruğa gömmeyin, her planlama turunda yeniden bakın.</p>\n<h2 id=\"kuyruğa-geri-dön\">Kuyruğa geri dön</h2>\n<p>Başa dönelim: o Dependabot PR’ı. Hangi saat açtı onu? Bir advisory mi, takvim mi? Bir kilidi mi çözüyor, yoksa yalnızca yeni bir sürümün çıktığını mı söylüyor? Üç soru, çoğu PR’ı fırsat ya da hatırlatma sınıfına yollar ve geriye gerçekten bu hafta bakılacak birkaç iş kalır.</p>\n<p>Sınıflandırmanın amacı daha az upgrade yapmak değil; doğru olanı doğru zamanda yapmak. Hatırlatmayı zorunluluk gibi karşılayan ekip boşuna koşar, kilidi hatırlatma sanan ekip geç kalır. İkisinin de bedeli aynı kuyruktan çıkar.</p>\n<p>Burada sinyalleri sınıflandırdım. Hangi sistemleri, hangi katmanlarda takip edeceğinizi serinin Bölüm 2’sinde, hedef sürümü nasıl seçeceğinizi Bölüm 4’te, upgrade etmemenin ne zaman doğru karar olduğunu <span data-scheduled=\"/tr/muhendislik/denemeler/upgrade-etmeme-karari/\">Bölüm 8</span>’de ele alacağım.</p>\n<p>Kuyruğunuzdaki son 10 upgrade sinyalini zorunlu, kilitleyen ve fırsat diye ayırın; her birine bir tepki süresi yazın.</p>",
      "content_text": "> **Kısaca:** Dependabot PR'ı, EOL duyurusu, deprecation uyarısı ve CVE bülteni aynı kuyruğa düşer ama aynı aciliyette değildir. EOL ve etkilendiğiniz CVE zorunludur, bağımlılık kilidi ve deprecation kilitleyendir, özellik ve performans fırsattır. Her türe bir tepki süresi yazmak, hatırlatmaya koşmayı ve kilide geç kalmayı önler.\n\nDependabot'un açtığı PR'ların çoğu zorunluluk değil, hatırlatmadır.\n\nKuyrukta dört tür sinyal birbirine karışıyor: Dependabot PR'ları, deprecation uyarıları, EOL duyuruları ve CVE bültenleri. Her biri farklı bir kaynaktan, farklı bir nedenle doğuyor ama hepsi aynı listeye, aynı tonda düşüyor. Ekran aynı görünüyor, iş aynı büyüklükte değil.\n\nZorunlu olan başka yerden gelir: bir EOL tarihinden, bir CVE'den ya da sizi eski sürüme kilitleyen bir bağımlılıktan. Üçüne aynı tepkiyi veren ekip ya geç kalır ya da boşuna koşar.\n\n## Kuyruğa iki ayrı saat düşer\n\nBir PR'ın ne kadar acil olduğu başlığında yazmaz; onu hangi saatin açtığında yazar: biri bir advisory'ye, öteki `dependabot.yml`'deki takvime bağlıdır.\n\nGitHub'ın belgelerine göre security updates açıldığında, güvenlik açığı olan bir bağımlılık bulunduğunda yeni PR açılır. Version updates ise yeni bir sürüm çıktığında açılır ve `dependabot.yml`'de tanımladığınız sıklıkla çalışır. Birincisini dış dünyada olan bir şey tetikler, ikincisini sizin yazdığınız takvim.\n\nBir ayrıntı daha: `cooldown` ayarı yalnız version updates için PR açılışını geciktirir; security updates'e uygulanmaz. Yani PR gürültüsünü kısmak için kurduğunuz ayar, güvenlik PR'ını susturmaz.\n\nPratik sonuç şu: haftalık takvimle gelen version update PR'ı, bir advisory'den doğan PR ile aynı kuyrukta durur ama aynı saatin işi değildir. Birini bu hafta, ötekini bu çeyrek ele alabilirsiniz.\n\nBir PR'ı açmadan önce iki şeye bakın: kim açtı, hangi etiketle geldi. Security update ise soru artık sürüm farkı değil, etkilenip etkilenmediğiniz. Version update ise soru, o sürümün bir şeyi çözüp çözmediği. İkincisinin cevabı çoğu zaman \"hayır, yeni bir sürüm çıkmış, hepsi bu\" olacak.\n\n## EOL bir tarih, CVE bir olay\n\nEOL önceden bilinir. [php.net](https://www.php.net/supported-versions.php) her dalın security desteğinin bitiş tarihini yayımlıyor ve politika basit: yayından itibaren iki yıl aktif destek, ardından iki yıl yalnız güvenlik düzeltmesi. PHP 8.2'nin security desteği **31 Aralık 2026**'da bitiyor. Bu yazının yayın tarihinden yaklaşık 9,5 hafta sonra.\n\nFramework katmanı da kendi saatini tutuyor. Laravel'in [12.x sürüm notları](https://laravel.com/docs/12.x/releases) şöyle diyor: bug fix'ler 18 ay, security fix'ler 2 yıl sağlanır. Laravel 12 için bu, bug fix'lerin 13 Ağustos 2026'da bittiği, security fix'lerin 24 Şubat 2027'ye kadar sürdüğü anlamına geliyor. Yani bu yazının yayımlandığı gün Laravel 12 çalıştıran bir ekip, bug fix desteğini çoktan kaybetmiş, security desteğini henüz kaybetmemiş durumda.\n\nCVE ise takvime uymaz. Salı sabahı çıkabilir, hangi sürümü çalıştırdığınıza bakmaz.\n\nEOL'ün sonrasını da abartmamak gerekiyor. Upstream desteğinin bitmesi, düzeltme gelmeyeceği anlamına gelmez; gelmesi sizin kontrolünüzden çıkar. Ubuntu'nun [USN-6305-2](https://ubuntu.com/security/notices/USN-6305-2) bülteni bunun örneği: PHP 7.4'ün upstream desteği Kasım 2022'de bitmişti; 27 Şubat 2024'te Ubuntu, CVE-2023-3823 ve CVE-2023-3824 için PHP paketlerine düzeltme yayımladı. 20.04'teki `php7.4` bunu standart depodan aldı; 18.04'teki `php7.2` ve 16.04'teki `php7.0` için ESM ve Ubuntu Pro gerekti.\n\nBu bir çıkış yolu olabilir. Ama bedeli bir satıcıya bağlı kalmak, düzeltmenin hangi paketle ve hangi hızla geleceğini onun takvimine bırakmak.\n\nEOL tarihinin değeri öngörülebilir olmasında. Tarihi geriye doğru hesaplayabilirsiniz: bitiş gününden önce production'da olmak istediğiniz sınırı koyun, oradan staging'e, oradan test işine geri sayın. Aşağıdaki önerimi 8.2'ye uygularsam sınır 30 Eylül 2026 olurdu. Bu yazıyı okurken hâlâ 8.2 çalıştırıyorsanız o sınır geçmiş durumda; bu bir suçlama değil, planı bugün yazmanın nedeni.\n\n## Kilitleyen tetikleyici görünmez\n\nKilitleyen tetikleyici ne bir tarih taşır ne bir bülten. Bir paket eski runtime'ı desteklemeyi bıraktığında yeni sürümü kurulamaz hâle gelir, kimse de size haber vermez.\n\nPHP 7.1 vakasında tetikleyici EOL değildi, bağımlılık kilidiydi; ayrıntısı [Upgrade'i Ertelemenin Maliyeti](/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/) yazısında.\n\nKilidin bir EOL gibi tarihi olmadığı için ölçmeniz gerekir: bekleyen paket güncellemelerinden kaçı, runtime sürümünüz yüzünden kurulamıyor? Sıfırdan büyük her sayı, bir kapının kapandığını gösterir.\n\nKilit sessiz büyür. Eski runtime'da her şey çalışmaya devam ettiği için hiçbir alarm üretmez; ilk görünür sinyal, güncellemek istediğiniz paketin kurulamadığı gündür. O gün tür artık fırsat değil, kilitleyendir ve çıkışın tek parça olması muhtemeldir.\n\n## Üç tür, üç tepki süresi\n\nSinyalleri üç türe ayırıyorum:\n\n- **Zorunlu:** dışarıdan gelen ve tarihi ya da riski sizin tercihinizden bağımsız olan sinyal (EOL, etkilendiğiniz CVE).\n- **Kilitleyen:** bugün bir şey bozmayan ama yarın seçeneğinizi daraltan sinyal.\n- **Fırsat:** yapmazsanız hiçbir şeyin bozulmadığı, yaparsanız kazanç sağlayan sinyal.\n\nAşağıdaki tepki süreleri benim önerim. Sektör standardı değil; kendi ekibinizin kapasitesine göre kaydırın, ama bir süre yazın. Süresi olmayan sinyal kuyrukta bekler, kuyrukta bekleyen sinyal de sonunda \"acil\" olarak geri döner.\n\n| Sinyal | Tür | Tepki süresi (önerim) |\n|---|---|---|\n| Etkilendiğiniz bir CVE | Zorunlu | Triage aynı iş günü, yama günler içinde |\n| Etkilenmediğiniz bir CVE | Fırsat | Normal döngü; etkilenmediğinizin kaydı tutulur |\n| EOL tarihi | Zorunlu | Tarih takvime girer; production'da bitiş tarihinden en az 3 ay önce |\n| Bağımlılık kilidi | Kilitleyen | Bir sonraki çeyrek planına girer |\n| Deprecation uyarısı | Kilitleyen | Bir sonraki major'dan önce |\n| Özellik ya da performans | Fırsat | Backlog; ölçülmüş bir kazanç yoksa tarih yok |\n\nTablonun iki satırı ayrıca durmayı hak ediyor. Önce CVE: aynı bülten, sizin kullandığınız kod yoluna dokunuyorsa zorunlu, dokunmuyorsa fırsattır. Aciliyeti bültenin başlığı değil, sizin sisteminiz belirler. Bunu belirlemek için iki soru yeter: etkilenen bileşeni kullanıyor muyum, etkilenen kod yoluna dışarıdan erişilebiliyor mu? İkisine de \"evet\" diyorsanız tablodaki ilk satırdasınız; düzeltme henüz sizin sürüm dalınıza gelmediyse bu, süreyi uzatmaz, işi yamadan geçici önleme çevirir. Cevaplardan biri \"hayır\" ise bunu kayda geçirin; kayıt, bir ay sonra aynı bülteni yeniden tartışmanızı engeller. Sonra performans: \"yeni sürüm daha hızlı\" cümlesi bir fırsattır, bir zorunluluk değil. Ölçmeden o cümleyle takvim açmayın.\n\nKilitleyen satırlarda süre neden gün değil çeyrek? Çünkü kilit bugün bir şey bozmuyor; ama her çeyrek bir paket daha eski runtime'ı bırakırsa çıkışın maliyeti büyür. Çeyrek, bunu fark etmek ve bir sonraki planlamaya yazmak için yeterince kısa bir süre.\n\n\n> **Tan:** Türü ve tepki süresini sinyalin durduğu yere yazmak, tabloyu ayrı bir sayfada tutmaktan daha çok işe yarar: issue etiketi ya da PR açıklamasında tek satır yeter. Kuyruğa bakan kişi kararı listeden okur, wiki'yi açmak zorunda kalmaz. \"Etkilenmiyoruz\" kaydı da aynı yerde durur; bülten bir ay sonra yeniden düştüğünde cevap zaten oradadır.\n\n\n## Bu çerçevenin sınırı\n\nBu sınıflandırma karar matrisinin yerini tutmaz. Tür, aciliyetin üst sınırını söyler; hangi sistemde, hangi riskle, hangi sırayla yapılacağını söylemez.\n\nAynı CVE bir iç araçta fırsat, ödeme akışında zorunlu olabilir. O ayrımı sistemin kritikliği yapar. Karar matrisi serinin Bölüm 3'ünde.\n\nTür ayrıca sabit değildir. Bir fırsat, ertelendikçe kilitleyene döner; bir kilit, EOL tarihi yaklaştıkça zorunluya. Sınıflandırmayı bir kez yapıp kuyruğa gömmeyin, her planlama turunda yeniden bakın.\n\n## Kuyruğa geri dön\n\nBaşa dönelim: o Dependabot PR'ı. Hangi saat açtı onu? Bir advisory mi, takvim mi? Bir kilidi mi çözüyor, yoksa yalnızca yeni bir sürümün çıktığını mı söylüyor? Üç soru, çoğu PR'ı fırsat ya da hatırlatma sınıfına yollar ve geriye gerçekten bu hafta bakılacak birkaç iş kalır.\n\nSınıflandırmanın amacı daha az upgrade yapmak değil; doğru olanı doğru zamanda yapmak. Hatırlatmayı zorunluluk gibi karşılayan ekip boşuna koşar, kilidi hatırlatma sanan ekip geç kalır. İkisinin de bedeli aynı kuyruktan çıkar.\n\nBurada sinyalleri sınıflandırdım. Hangi sistemleri, hangi katmanlarda takip edeceğinizi serinin Bölüm 2'sinde, hedef sürümü nasıl seçeceğinizi Bölüm 4'te, upgrade etmemenin ne zaman doğru karar olduğunu Bölüm 8'de ele alacağım.\n\nKuyruğunuzdaki son 10 upgrade sinyalini zorunlu, kilitleyen ve fırsat diye ayırın; her birine bir tepki süresi yazın.\n\n## Kaynaklar\n\n- [Supported Versions](https://www.php.net/supported-versions.php) — PHP\n- [Laravel 12.x Release Notes](https://laravel.com/docs/12.x/releases) — Laravel\n- [Optimizing PR creation for version updates](https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/optimizing-pr-creation-version-updates) — GitHub\n- [USN-6305-2: PHP vulnerabilities](https://ubuntu.com/security/notices/USN-6305-2) — Canonical",
      "date_published": "2026-10-25T00:00:00.000Z",
      "tags": [
        "dependencies",
        "decisions",
        "security",
        "production",
        "Production'da Upgrade"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/integration-vs-unit-test/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/integration-vs-unit-test/",
      "title": "Production Hatasını Hangi Test Engeller?",
      "summary": "Pratikte production hatalarının birçoğu bileşenlerin arasında doğar ve izole bir unit test tam da kırılan bağlantıyı mock'lar. Unit test saf, mantık yoğun çekirdekte parlar; integration test glue code'u gerçek veritabanı ve HTTP katmanıyla sınar. Test bütçesini hataların doğduğu yere ayırın, mock'u yalnızca ödeme sağlayıcısı gibi dış bağımlılıklara saklayın.",
      "content_html": "<p>%100 unit test coverage’ı olan bir özellik, production’da ilk gün kırıldı. Hata hiçbir birimin içinde değildi; iki bileşenin birbiriyle konuşma biçimindeydi. Her birim tek tek, izole, kusursuz test edilmişti. İkisinin arasındaki dikişi ise hiçbir test görmemişti.</p>\n<p>“Hangi test bu hatayı engellerdi?” sorusu, test stratejisinin asıl sorusudur.</p>\n<h2 id=\"gerçek-hatalar-nerede-doğar\">Gerçek hatalar nerede doğar</h2>\n<p>Production hatalarını dürüstçe geriye doğru izlerseniz, birçoğunun aynı yerde buluştuğunu görürsünüz: bileşenlerin <strong>arası</strong>. Yanlış serialize edilmiş bir payload, uyuşmayan bir kontrat, yanlış çizilmiş bir transaction sınırı, ortamlar arası bir config farkı.</p>\n<p>İzole unit test, tam da kırılan o parçayı mock’lar. Veritabanını mock’layan bir test, yanlış yazılmış bir sorguyu göremez. HTTP katmanını mock’layan bir test, yanlış bir status kodunu yakalayamaz. Test, gerçeğin yerine koyduğu sahte şey kadar iyidir.</p>\n<h2 id=\"unit-test-ne-işe-yarar\">Unit test ne işe yarar</h2>\n<p>Bu, unit test’e karşı bir yazı değil. Unit test’in net bir değeri var: karmaşık, saf mantık üzerinde hızlı geri bildirim. Bir hesaplama, bir algoritma, bir durum makinesi, bir sürü edge case — bunları izole, hızlı testlerle sınamak doğru hamledir. Saniyeler içinde yüzlerce senaryo.</p>\n<p>Unit test, <strong>mantık</strong> zengin olduğunda parlar.</p>\n<h2 id=\"integration-test-ne-işe-yarar\">Integration test ne işe yarar</h2>\n<p>Integration test farklı bir soruyu cevaplar: bileşenler gerçekten birlikte çalışıyor mu? Gerçek veritabanıyla, gerçek HTTP katmanıyla, gerçek kuyrukla. Bir endpoint’e istek atıp veritabanında doğru satırın oluştuğunu doğrulamak — mock’ların gizleyeceği her şeyi açığa çıkarır.</p>\n<p>Tipik bir web uygulaması çoğunlukla <strong>glue code</strong>’dur: veriyi al, doğrula, kaydet, döndür. Bu kodun içinde az mantık, çok bağlantı vardır. Bağlantıyı ancak integration test sınar.</p>\n<h2 id=\"test-bütçesi-nereye\">Test bütçesi nereye</h2>\n<p>Klasik test piramidi en alta çok sayıda unit test koyar. Ama tipik bir web uygulaması için, hata önleme açısından en yüksek getiriyi integration testler verir — çünkü hatalar bağlantıda doğuyor.</p>\n<p>Pratik dağılım: glue code için integration test; mantık yoğun çekirdek için unit test. Glue code’a izole unit test yığmak, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/sade-test-kulturu/\">coverage sayısını</a> yükseltir ama production’ı korumaz.</p>\n<blockquote><p><strong>Tan:</strong> Bir hata production'a kaçtığında, onu hangi testin yakalayacağını yazın: unit mi, integration mı? Birkaç olay sonra test bütçesinin nereye gitmesi gerektiği tartışma değil, bir kayıt olur. Yazının başındaki soru, her post-mortem'in son maddesi olabilir.</p></blockquote>\n<h2 id=\"mockun-tuzağı\">Mock’un tuzağı</h2>\n<p>En sık hata, kırılan şeyi mock’lamaktır. Bir sorgunun yanlış olduğunu, veritabanını mock’layan bir test asla söyleyemez — çünkü o test, sorgunun gittiği yeri sahteyle değiştirmiştir. <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">Veri yoğunluklu sistemlerdeki</a> N+1 gibi bir sorgu hatası, ancak gerçek veritabanına giden bir test ile yakalanır.</p>\n<p>Mock’u, kontrol edemediğiniz ve yavaş olan dış şeyler için saklayın — bir ödeme sağlayıcısı gibi. Kendi veritabanınızı mock’lamak, çoğu zaman testi gerçeklikten koparmaktır.</p>\n<hr/>\n<p>“Bütün testler yeşil” cümlesi, “production güvende” demek değildir. Hangi testin hangi hatayı engellediğini bilmiyorsanız, test sayınız bir güven kaynağı değil, bir histir.</p>\n<p>Test bütçenizi, hataların gerçekten doğduğu yere — bileşenlerin arasına — ayırın.</p>",
      "content_text": "> **Kısaca:** Pratikte production hatalarının birçoğu bileşenlerin arasında doğar ve izole bir unit test tam da kırılan bağlantıyı mock'lar. Unit test saf, mantık yoğun çekirdekte parlar; integration test glue code'u gerçek veritabanı ve HTTP katmanıyla sınar. Test bütçesini hataların doğduğu yere ayırın, mock'u yalnızca ödeme sağlayıcısı gibi dış bağımlılıklara saklayın.\n\n%100 unit test coverage'ı olan bir özellik, production'da ilk gün kırıldı. Hata hiçbir birimin içinde değildi; iki bileşenin birbiriyle konuşma biçimindeydi. Her birim tek tek, izole, kusursuz test edilmişti. İkisinin arasındaki dikişi ise hiçbir test görmemişti.\n\n\"Hangi test bu hatayı engellerdi?\" sorusu, test stratejisinin asıl sorusudur.\n\n## Gerçek hatalar nerede doğar\n\nProduction hatalarını dürüstçe geriye doğru izlerseniz, birçoğunun aynı yerde buluştuğunu görürsünüz: bileşenlerin **arası**. Yanlış serialize edilmiş bir payload, uyuşmayan bir kontrat, yanlış çizilmiş bir transaction sınırı, ortamlar arası bir config farkı.\n\nİzole unit test, tam da kırılan o parçayı mock'lar. Veritabanını mock'layan bir test, yanlış yazılmış bir sorguyu göremez. HTTP katmanını mock'layan bir test, yanlış bir status kodunu yakalayamaz. Test, gerçeğin yerine koyduğu sahte şey kadar iyidir.\n\n## Unit test ne işe yarar\n\nBu, unit test'e karşı bir yazı değil. Unit test'in net bir değeri var: karmaşık, saf mantık üzerinde hızlı geri bildirim. Bir hesaplama, bir algoritma, bir durum makinesi, bir sürü edge case — bunları izole, hızlı testlerle sınamak doğru hamledir. Saniyeler içinde yüzlerce senaryo.\n\nUnit test, **mantık** zengin olduğunda parlar.\n\n## Integration test ne işe yarar\n\nIntegration test farklı bir soruyu cevaplar: bileşenler gerçekten birlikte çalışıyor mu? Gerçek veritabanıyla, gerçek HTTP katmanıyla, gerçek kuyrukla. Bir endpoint'e istek atıp veritabanında doğru satırın oluştuğunu doğrulamak — mock'ların gizleyeceği her şeyi açığa çıkarır.\n\nTipik bir web uygulaması çoğunlukla **glue code**'dur: veriyi al, doğrula, kaydet, döndür. Bu kodun içinde az mantık, çok bağlantı vardır. Bağlantıyı ancak integration test sınar.\n\n## Test bütçesi nereye\n\nKlasik test piramidi en alta çok sayıda unit test koyar. Ama tipik bir web uygulaması için, hata önleme açısından en yüksek getiriyi integration testler verir — çünkü hatalar bağlantıda doğuyor.\n\nPratik dağılım: glue code için integration test; mantık yoğun çekirdek için unit test. Glue code'a izole unit test yığmak, [coverage sayısını](/tr/muhendislik/denemeler/sade-test-kulturu/) yükseltir ama production'ı korumaz.\n\n\n> **Tan:** Bir hata production'a kaçtığında, onu hangi testin yakalayacağını yazın: unit mi, integration mı? Birkaç olay sonra test bütçesinin nereye gitmesi gerektiği tartışma değil, bir kayıt olur. Yazının başındaki soru, her post-mortem'in son maddesi olabilir.\n\n\n## Mock'un tuzağı\n\nEn sık hata, kırılan şeyi mock'lamaktır. Bir sorgunun yanlış olduğunu, veritabanını mock'layan bir test asla söyleyemez — çünkü o test, sorgunun gittiği yeri sahteyle değiştirmiştir. [Veri yoğunluklu sistemlerdeki](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) N+1 gibi bir sorgu hatası, ancak gerçek veritabanına giden bir test ile yakalanır.\n\nMock'u, kontrol edemediğiniz ve yavaş olan dış şeyler için saklayın — bir ödeme sağlayıcısı gibi. Kendi veritabanınızı mock'lamak, çoğu zaman testi gerçeklikten koparmaktır.\n\n---\n\n\"Bütün testler yeşil\" cümlesi, \"production güvende\" demek değildir. Hangi testin hangi hatayı engellediğini bilmiyorsanız, test sayınız bir güven kaynağı değil, bir histir.\n\nTest bütçenizi, hataların gerçekten doğduğu yere — bileşenlerin arasına — ayırın.\n\n## Kaynaklar\n\n- [TestPyramid](https://martinfowler.com/bliki/TestPyramid.html) — martinfowler.com",
      "date_published": "2026-10-24T00:00:00.000Z",
      "tags": [
        "testing",
        "quality",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/",
      "title": "Upgrade'i Ertelemenin Maliyeti",
      "summary": "Upgrade'i ertelemek risk almamak değil, riski görünmez biçimde biriktirmektir. Paketler eski runtime desteğini tek tek kestikçe bağımlılık kilidi sessizce kurulur; 1–2 günlük bir iş, tek seferde çözülmesi gereken onlarca breaking change'e ve aylara yayılan bir bug kuyruğuna dönüşür. Erteleme ancak tarihi ve gerekçesi yazıldığında bir karardır.",
      "content_html": "<p>PHP 7.1’den stabil 8.2’ye geçmeyi önerdiğimde cevap kısaydı: “Çalışıyor, ne gerek var?” Cevap doğruydu — o gün hiçbir şey bozuk değildi. Bozulmaya başlayan şey görünmüyordu: paketler eski PHP desteğini tek tek kesiyor, uygulama 7.1’e kilitleniyordu.</p>\n<p>“Çalışıyor” cümlesi bugünü anlatır. Faturayı ise sonraki günler keser.</p>\n<p>İtiraz mantıksız değil, o yüzden hafife almıyorum. Bozuk olmayan bir şeye zaman ayırmak, hiçbir şey kazanılmıyormuş gibi görünür. Haklı bir cümleyi çürütmek için cümleyle tartışmak yetmez; sayıyla gelmek gerekir. Aşağıdaki, o cümlenin bana neye mal olduğunun hesabı: hangi mekanizmayla, hangi kalemlerle ve hangi kısmı ölçebildiğimle.</p>\n<h2 id=\"kilit-nasıl-kuruluyor\">Kilit Nasıl Kuruluyor</h2>\n<p>Kilit tek bir günde kurulmadı; her paket güncellemesinde bir kapı daha kapandı.</p>\n<p>Mekanizma sıradan. Bir paket yeni sürümünde eski runtime’ı desteklemeyi bırakır. Runtime yükseltilmediği için o sürüm kurulamaz, eski sürümde kalınır. Aynı şey bir sonraki pakette olur, ardından bir sonrakinde. Hiçbiri tek başına alarm üretmez: eski sürüm çalışmaya devam eder.</p>\n<p>Bir noktadan sonra güncellenebilen paket kalmaz. Buna <strong>bağımlılık kilidi</strong> diyorum: uygulama runtime’a, paketler uygulamaya, hepsi birlikte 7.1’e bağlanmış durumda. Çıkış tek parça. Runtime’ı yükseltmeden hiçbir paketi ileri alamıyordum; paketleri ileri almadan da yükseltmenin neye mal olacağını göremiyordum.</p>\n<p>Kilit bu yüzden fark edilmeden kuruluyor. Her kapanan kapı tek başına ertelenebilir bir ayrıntı; hiçbir paket güncellemesi “upgrade’i bugün yapmalıyız” dedirtecek kadar büyük görünmüyor. Kararı ertelemek her seferinde makul. Makul kararların toplamı ise bir köşeye sıkışmak oluyor.</p>\n<p>Kilidin en kötü yanı sessiz olması. Hiçbir hata mesajı “bugün bir kapı daha kapandı” demiyor. Bunu ancak bir paketi güncellemeye çalıştığınız gün, güncellemenin neden kurulmadığını okurken fark ediyorsunuz.</p>\n<p>Kendi sisteminizde bakılacak işaret basit: Bekleyen paket güncellemelerinden kaçı, runtime sürümü yüzünden kurulamıyor? Sıfırdan büyük her sayı, bir kapının kapanmış olduğunu gösterir.</p>\n<h2 id=\"elimdeki-tek-rakam\">Elimdeki Tek Rakam</h2>\n<p>Önerdiğim gün iş, 1–2 günlük bir upgrade’di. Kilit kurulurken ise her paket güncellemesi birkaç saat tutuyordu. İkisi de küçük bir iş gibi duruyordu ve bu yüzden ertelemek kolaydı.</p>\n<p>Küçük iş, ertelemek için iyi bir adaydır; “önce acil olanı bitirelim” cümlesi her takvimde yer bulur.</p>\n<p>Ertelenince iş yalnız büyümedi, biriktiği için tek parça hâline geldi. Sürüm atlamak zorunda kaldım: 7.1’den 8.2’ye adım adım değil, aradaki her şeyi tek seferde geçerek. Önüme onlarca breaking change ve deprecated metot çıktı ve hepsi aynı anda çözülmek zorundaydı. Parça parça ödenebilecek bir borç, tek bir tahsilata dönüştü.</p>\n<p>Rakamın asıl öğrettiği, tutarın kendisi değil iş türünün değişmesi. Birkaç saatlik paket güncellemeleri istenen sırayla, istenen günde yapılabilen parçalı işlerdi. Onlarca breaking change ise bölünemeyen, başlayınca bitmesi gereken tek bir iş. Ertelemek işi büyütmekle kalmadı, esnekliğini de aldı.</p>\n<p>Bu, elimdeki tek rakam: 1–2 günlük iş, artı her paket güncellemesinde birkaç saat. Ertelemenin toplam bedelini bunun yanına koyabilecek bir ölçümüm yok. Koymayacağım da; ölçmediğim bir sayıyı yazmak, “çalışıyor ya” cevabından daha kötü bir argüman olur.</p>\n<p>Yine de bu tek rakam bile bir konuşmayı başlatmaya yeter. “Çalışıyor ya” karşısına “hiçbir şey kazanmıyoruz” diye değil, “şimdi 1–2 gün, sonra tek seferde onlarca değişiklik” diye gidebilirsiniz. İkisi arasındaki farkı yöneticiye açıklamak için ölçüm şart değil, yalnız karşılaştırmanın yönü yeter.</p>\n<p>Rakamı nasıl çıkaracağınız serinin 7. bölümünde; hangi upgrade’in önce geleceği 3. bölümde. Burada yalnız şunu söylüyorum: bu karşılaştırmayı o gün masaya koyabilseydim, “çalışıyor” cevabı tek başına yetmezdi.</p>\n<h2 id=\"faturanın-asıl-kalemi\">Faturanın Asıl Kalemi</h2>\n<p>Breaking change’leri temizledik; beklemediğimiz şey, ondan sonra gelen bug’lardı.</p>\n<p>Breaking change’in iyi bir özelliği var: hata mesajı onu gösterir. Listeyi çıkarırsınız, tek tek kapatırsınız, biter. Bu kalem uzun ama bellidir: büyüklüğünü kabaca kestirir, takvime yerleştirir, yöneticiye de söyleyebilirsiniz.</p>\n<p>Bug’lar öyle değildi. Bir kısmı yalnız tek bir case’de ortaya çıkıyordu — sık tetiklenmeyen bir yol, belirli bir veri, belirli bir sıra. Böyle bir bug’ı önce görmek, sonra yeniden üretmek, sonra nedenini bulmak gerekiyor. Her biri ayrı bir araştırma. Çözümleri aylara yayıldı. Bir bug kapanıyor, başka bir case’de bir başkası çıkıyordu.</p>\n<p>Planlama açısından ikisini ayrı yazmak gerekiyor. Breaking change tahmin edilebilir bir kalem; bug’lar edilemez. Bütçeyi yalnız birincisine göre kurarsanız, ikincisi bütçenin dışında, takvimin içinde yaşanır.</p>\n<p>Asıl maliyet orada: <strong>uzun kuyruk</strong>. Toplu geçişin bedeli takvime sığar, kuyruğun bedeli sığmaz. Üstelik onlarca değişikliği bir arada taşıyınca her bug’ın kaynağı onlarca adaydan biri oldu; hangisinin hangi değişiklikten geldiğini ayırmak ayrı bir iş çıkardı.</p>\n<h2 id=\"çalışıyor-bugünü-anlatır\">Çalışıyor Bugünü Anlatır</h2>\n<p>“Çalışıyor ya” itirazına “teknik borç var” demek yetmiyor; karşınızdaki bunu bir görüş olarak duyuyor. İş diliyle cevap vermek için riski kalemlere ayırmak gerekiyor. Ertelenen her upgrade dört kalem biriktirir:</p>\n<ul>\n<li><strong>Destek bitişi:</strong> runtime ya da framework’ün bakımı bitince yeni düzeltme gelmez. Takvimde belli bir gün, bilinen bir tarih.</li>\n<li><strong>CVE’ye açık kalma süresi:</strong> desteğin bittiği günden sonra duyurulan her CVE için resmi yama gelmez; dağıtımınız ya da ücretli bir uzatılmış destek yamalamıyorsa açık, siz yükseltene kadar sistemde durur. Sayaç desteğin bittiği gün başlar, upgrade günü durur.</li>\n<li><strong>Bağımlılık kilidi:</strong> paketler eski sürüme sabitlendikçe güncelleme seçenekleri daralır. Kilit kurulduktan sonra tek çıkış, her şeyi birlikte taşımak.</li>\n<li><strong>Büyüyen sürüm farkı:</strong> aradaki sürüm sayısı arttıkça tek seferde çözülecek breaking change ve deprecated metot sayısı artar. Benim vakamda bu, onlarca kalemdi.</li>\n</ul>\n<p>Bunların hiçbiri bugün bir hata üretmiyor ve izleme panelinde görünmüyor; hepsi yarın ödenecek. Yönetici açısından da fark bu: bozuk bir şey için bütçe istemek kolay, bozulmamış bir şeyin neden bozulacağını anlatmak zor. Kalemleri adlandırmak bu yüzden işe yarıyor, çünkü her biri bir tarihe, bir sayaca ya da bir sayıya bağlanabilir.</p>\n<p>Dört kalemin özeti şu: <strong>çalışıyor bugünü anlatır, risk yarının bilanço kalemidir.</strong> Upgrade’in bedeli bellidir ve ödemeyi siz zamanlarsınız. Ertelemenin bedeli ise belirsizdir ve tahsilat zamanını siz seçemezsiniz.</p>\n<blockquote><p><strong>Tan:</strong> Yazı ertelemenin bedelini açıkça gösteriyor. Benim takıldığım yer, ispat yükünün hep upgrade'i önerende kalması. Yükseltmek için gerekçe isteniyor, beklemek için gerekçe soran pek çıkmıyor. İki soru da aynı masaya konsaydı “çalışıyor” cevabı bu kadar kolay kazanır mıydı?</p></blockquote>\n<h2 id=\"ne-zaman-bırakırsınız\">Ne Zaman Bırakırsınız?</h2>\n<p>Upgrade etmemek de meşru bir karar olabilir; hangi durumlarda olduğunu serinin <span data-scheduled=\"/tr/muhendislik/denemeler/upgrade-etmeme-karari/\">8. bölümüne</span> bırakıyorum. Buradaki itirazım ertelemeye değil, ertelemenin bir karar olarak yazılmamasına.</p>\n<p>Ertelemek bilinçli bir karar olabilir; ama yazılı olmayan karar, bir gün sürpriz olarak geri gelir. Tarihi ve gerekçesi yazılmış bir erteleme risk yönetimidir, yazılmamış olan ise yalnızca unutmaktır.</p>\n<p>Bugün yapabileceğiniz tek bir şey var: kullandığınız runtime ve framework sürümlerinin destek bitiş tarihlerine bakın, bir yere yazın. Sistemin envanteri yoksa bile iki satır yeter: ne çalışıyor, desteği ne zaman bitiyor. Bir sonraki “neden?” sorusunda elinizde en azından bir tarih olur. Tarihin yanına, sistemin o güne kadar kaç sürüm geride kalacağını da yazarsanız, soru “çalışıyor mu” olmaktan çıkıp “ne zaman ödeyeceğiz” olur; konuşma da orada başlar.</p>\n<p>Kendinize sorun: bu sistemi bugün yükseltmek 1–2 günlük bir iş olsaydı yapar mıydınız? Cevap evetse, bekleyerek elde edeceğiniz şey yok; yalnız iş büyüyor.</p>\n<p>Seri buradan devam ediyor: 1. bölümde <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/upgrade-tetikleyicileri/\">upgrade’i tetikleyen durumlar</a>, 2. bölümde neyin yükseltileceğini görmek için <span data-scheduled=\"/tr/muhendislik/notlar/upgrade-envanteri/\">envanter ve katmanlar</span>, 3. bölümde <span data-scheduled=\"/tr/muhendislik/notlar/upgrade-karar-matrisi/\">karar matrisi</span>; 8. bölüme kadar <span data-scheduled=\"/tr/muhendislik/notlar/upgrade-hedef-surum/\">hedef sürüm</span>, <span data-scheduled=\"/tr/muhendislik/notlar/upgrade-on-kosullar/\">ön koşullar</span>, <span data-scheduled=\"/tr/muhendislik/notlar/upgrade-rollout/\">rollout</span> ve <span data-scheduled=\"/tr/muhendislik/denemeler/upgrade-organizasyonu/\">organizasyon</span>.</p>\n<p>Upgrade’i ertelediğinizde riski kapatmazsınız; faturayı bir sonraki güne yazarsınız.</p>",
      "content_text": "> **Kısaca:** Upgrade'i ertelemek risk almamak değil, riski görünmez biçimde biriktirmektir. Paketler eski runtime desteğini tek tek kestikçe bağımlılık kilidi sessizce kurulur; 1–2 günlük bir iş, tek seferde çözülmesi gereken onlarca breaking change'e ve aylara yayılan bir bug kuyruğuna dönüşür. Erteleme ancak tarihi ve gerekçesi yazıldığında bir karardır.\n\nPHP 7.1'den stabil 8.2'ye geçmeyi önerdiğimde cevap kısaydı: \"Çalışıyor, ne gerek var?\" Cevap doğruydu — o gün hiçbir şey bozuk değildi. Bozulmaya başlayan şey görünmüyordu: paketler eski PHP desteğini tek tek kesiyor, uygulama 7.1'e kilitleniyordu.\n\n\"Çalışıyor\" cümlesi bugünü anlatır. Faturayı ise sonraki günler keser.\n\nİtiraz mantıksız değil, o yüzden hafife almıyorum. Bozuk olmayan bir şeye zaman ayırmak, hiçbir şey kazanılmıyormuş gibi görünür. Haklı bir cümleyi çürütmek için cümleyle tartışmak yetmez; sayıyla gelmek gerekir. Aşağıdaki, o cümlenin bana neye mal olduğunun hesabı: hangi mekanizmayla, hangi kalemlerle ve hangi kısmı ölçebildiğimle.\n\n## Kilit Nasıl Kuruluyor\n\nKilit tek bir günde kurulmadı; her paket güncellemesinde bir kapı daha kapandı.\n\nMekanizma sıradan. Bir paket yeni sürümünde eski runtime'ı desteklemeyi bırakır. Runtime yükseltilmediği için o sürüm kurulamaz, eski sürümde kalınır. Aynı şey bir sonraki pakette olur, ardından bir sonrakinde. Hiçbiri tek başına alarm üretmez: eski sürüm çalışmaya devam eder.\n\nBir noktadan sonra güncellenebilen paket kalmaz. Buna **bağımlılık kilidi** diyorum: uygulama runtime'a, paketler uygulamaya, hepsi birlikte 7.1'e bağlanmış durumda. Çıkış tek parça. Runtime'ı yükseltmeden hiçbir paketi ileri alamıyordum; paketleri ileri almadan da yükseltmenin neye mal olacağını göremiyordum.\n\nKilit bu yüzden fark edilmeden kuruluyor. Her kapanan kapı tek başına ertelenebilir bir ayrıntı; hiçbir paket güncellemesi \"upgrade'i bugün yapmalıyız\" dedirtecek kadar büyük görünmüyor. Kararı ertelemek her seferinde makul. Makul kararların toplamı ise bir köşeye sıkışmak oluyor.\n\nKilidin en kötü yanı sessiz olması. Hiçbir hata mesajı \"bugün bir kapı daha kapandı\" demiyor. Bunu ancak bir paketi güncellemeye çalıştığınız gün, güncellemenin neden kurulmadığını okurken fark ediyorsunuz.\n\nKendi sisteminizde bakılacak işaret basit: Bekleyen paket güncellemelerinden kaçı, runtime sürümü yüzünden kurulamıyor? Sıfırdan büyük her sayı, bir kapının kapanmış olduğunu gösterir.\n\n## Elimdeki Tek Rakam\n\nÖnerdiğim gün iş, 1–2 günlük bir upgrade'di. Kilit kurulurken ise her paket güncellemesi birkaç saat tutuyordu. İkisi de küçük bir iş gibi duruyordu ve bu yüzden ertelemek kolaydı.\n\nKüçük iş, ertelemek için iyi bir adaydır; \"önce acil olanı bitirelim\" cümlesi her takvimde yer bulur.\n\nErtelenince iş yalnız büyümedi, biriktiği için tek parça hâline geldi. Sürüm atlamak zorunda kaldım: 7.1'den 8.2'ye adım adım değil, aradaki her şeyi tek seferde geçerek. Önüme onlarca breaking change ve deprecated metot çıktı ve hepsi aynı anda çözülmek zorundaydı. Parça parça ödenebilecek bir borç, tek bir tahsilata dönüştü.\n\nRakamın asıl öğrettiği, tutarın kendisi değil iş türünün değişmesi. Birkaç saatlik paket güncellemeleri istenen sırayla, istenen günde yapılabilen parçalı işlerdi. Onlarca breaking change ise bölünemeyen, başlayınca bitmesi gereken tek bir iş. Ertelemek işi büyütmekle kalmadı, esnekliğini de aldı.\n\nBu, elimdeki tek rakam: 1–2 günlük iş, artı her paket güncellemesinde birkaç saat. Ertelemenin toplam bedelini bunun yanına koyabilecek bir ölçümüm yok. Koymayacağım da; ölçmediğim bir sayıyı yazmak, \"çalışıyor ya\" cevabından daha kötü bir argüman olur.\n\nYine de bu tek rakam bile bir konuşmayı başlatmaya yeter. \"Çalışıyor ya\" karşısına \"hiçbir şey kazanmıyoruz\" diye değil, \"şimdi 1–2 gün, sonra tek seferde onlarca değişiklik\" diye gidebilirsiniz. İkisi arasındaki farkı yöneticiye açıklamak için ölçüm şart değil, yalnız karşılaştırmanın yönü yeter.\n\nRakamı nasıl çıkaracağınız serinin 7. bölümünde; hangi upgrade'in önce geleceği 3. bölümde. Burada yalnız şunu söylüyorum: bu karşılaştırmayı o gün masaya koyabilseydim, \"çalışıyor\" cevabı tek başına yetmezdi.\n\n## Faturanın Asıl Kalemi\n\nBreaking change'leri temizledik; beklemediğimiz şey, ondan sonra gelen bug'lardı.\n\nBreaking change'in iyi bir özelliği var: hata mesajı onu gösterir. Listeyi çıkarırsınız, tek tek kapatırsınız, biter. Bu kalem uzun ama bellidir: büyüklüğünü kabaca kestirir, takvime yerleştirir, yöneticiye de söyleyebilirsiniz.\n\nBug'lar öyle değildi. Bir kısmı yalnız tek bir case'de ortaya çıkıyordu — sık tetiklenmeyen bir yol, belirli bir veri, belirli bir sıra. Böyle bir bug'ı önce görmek, sonra yeniden üretmek, sonra nedenini bulmak gerekiyor. Her biri ayrı bir araştırma. Çözümleri aylara yayıldı. Bir bug kapanıyor, başka bir case'de bir başkası çıkıyordu.\n\nPlanlama açısından ikisini ayrı yazmak gerekiyor. Breaking change tahmin edilebilir bir kalem; bug'lar edilemez. Bütçeyi yalnız birincisine göre kurarsanız, ikincisi bütçenin dışında, takvimin içinde yaşanır.\n\nAsıl maliyet orada: **uzun kuyruk**. Toplu geçişin bedeli takvime sığar, kuyruğun bedeli sığmaz. Üstelik onlarca değişikliği bir arada taşıyınca her bug'ın kaynağı onlarca adaydan biri oldu; hangisinin hangi değişiklikten geldiğini ayırmak ayrı bir iş çıkardı.\n\n## Çalışıyor Bugünü Anlatır\n\n\"Çalışıyor ya\" itirazına \"teknik borç var\" demek yetmiyor; karşınızdaki bunu bir görüş olarak duyuyor. İş diliyle cevap vermek için riski kalemlere ayırmak gerekiyor. Ertelenen her upgrade dört kalem biriktirir:\n\n- **Destek bitişi:** runtime ya da framework'ün bakımı bitince yeni düzeltme gelmez. Takvimde belli bir gün, bilinen bir tarih.\n- **CVE'ye açık kalma süresi:** desteğin bittiği günden sonra duyurulan her CVE için resmi yama gelmez; dağıtımınız ya da ücretli bir uzatılmış destek yamalamıyorsa açık, siz yükseltene kadar sistemde durur. Sayaç desteğin bittiği gün başlar, upgrade günü durur.\n- **Bağımlılık kilidi:** paketler eski sürüme sabitlendikçe güncelleme seçenekleri daralır. Kilit kurulduktan sonra tek çıkış, her şeyi birlikte taşımak.\n- **Büyüyen sürüm farkı:** aradaki sürüm sayısı arttıkça tek seferde çözülecek breaking change ve deprecated metot sayısı artar. Benim vakamda bu, onlarca kalemdi.\n\nBunların hiçbiri bugün bir hata üretmiyor ve izleme panelinde görünmüyor; hepsi yarın ödenecek. Yönetici açısından da fark bu: bozuk bir şey için bütçe istemek kolay, bozulmamış bir şeyin neden bozulacağını anlatmak zor. Kalemleri adlandırmak bu yüzden işe yarıyor, çünkü her biri bir tarihe, bir sayaca ya da bir sayıya bağlanabilir.\n\nDört kalemin özeti şu: **çalışıyor bugünü anlatır, risk yarının bilanço kalemidir.** Upgrade'in bedeli bellidir ve ödemeyi siz zamanlarsınız. Ertelemenin bedeli ise belirsizdir ve tahsilat zamanını siz seçemezsiniz.\n\n\n> **Tan:** Yazı ertelemenin bedelini açıkça gösteriyor. Benim takıldığım yer, ispat yükünün hep upgrade'i önerende kalması. Yükseltmek için gerekçe isteniyor, beklemek için gerekçe soran pek çıkmıyor. İki soru da aynı masaya konsaydı “çalışıyor” cevabı bu kadar kolay kazanır mıydı?\n\n\n## Ne Zaman Bırakırsınız?\n\nUpgrade etmemek de meşru bir karar olabilir; hangi durumlarda olduğunu serinin 8. bölümüne bırakıyorum. Buradaki itirazım ertelemeye değil, ertelemenin bir karar olarak yazılmamasına.\n\nErtelemek bilinçli bir karar olabilir; ama yazılı olmayan karar, bir gün sürpriz olarak geri gelir. Tarihi ve gerekçesi yazılmış bir erteleme risk yönetimidir, yazılmamış olan ise yalnızca unutmaktır.\n\nBugün yapabileceğiniz tek bir şey var: kullandığınız runtime ve framework sürümlerinin destek bitiş tarihlerine bakın, bir yere yazın. Sistemin envanteri yoksa bile iki satır yeter: ne çalışıyor, desteği ne zaman bitiyor. Bir sonraki \"neden?\" sorusunda elinizde en azından bir tarih olur. Tarihin yanına, sistemin o güne kadar kaç sürüm geride kalacağını da yazarsanız, soru \"çalışıyor mu\" olmaktan çıkıp \"ne zaman ödeyeceğiz\" olur; konuşma da orada başlar.\n\nKendinize sorun: bu sistemi bugün yükseltmek 1–2 günlük bir iş olsaydı yapar mıydınız? Cevap evetse, bekleyerek elde edeceğiniz şey yok; yalnız iş büyüyor.\n\nSeri buradan devam ediyor: 1. bölümde [upgrade'i tetikleyen durumlar](/tr/muhendislik/notlar/upgrade-tetikleyicileri/), 2. bölümde neyin yükseltileceğini görmek için envanter ve katmanlar, 3. bölümde karar matrisi; 8. bölüme kadar hedef sürüm, ön koşullar, rollout ve organizasyon.\n\nUpgrade'i ertelediğinizde riski kapatmazsınız; faturayı bir sonraki güne yazarsınız.\n\n## Sık sorulanlar\n\n### Çalışıyor, ne gerek var?\n\n\"Çalışıyor\" cümlesi bugünü anlatır; risk ise yarının bilanço kalemidir. Ertelenen her upgrade dört kalem biriktirir: destek bitişi, CVE'ye açık kalma süresi, bağımlılık kilidi ve büyüyen sürüm farkı. Hiçbiri bugün hata üretmez, hepsi yarın ödenir. Upgrade'in bedeli bellidir ve ödemeyi siz zamanlarsınız; ertelemenin bedeli belirsizdir ve tahsilat zamanını siz seçemezsiniz.\n\n### Bekleyen paket güncellemelerinden kaçı, runtime sürümü yüzünden kurulamıyor?\n\nBağımlılık kilidinin kurulup kurulmadığını gösteren işaret bu sayıdır. Paketler eski runtime desteğini tek tek keser; hiçbiri alarm üretmez, eski sürüm çalışmaya devam eder. Sıfırdan büyük her sayı bir kapının kapandığını gösterir. Bir noktadan sonra güncellenebilen paket kalmaz ve tek çıkış her şeyi birlikte taşımaktır.",
      "date_published": "2026-10-18T00:00:00.000Z",
      "tags": [
        "technical-debt",
        "decisions",
        "dependencies",
        "opinion",
        "Production'da Upgrade"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/sade-test-kulturu/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/sade-test-kulturu/",
      "title": "%100 Coverage Peşinde İşlevsiz Test Yazmak",
      "summary": "Coverage yalnızca satırın çalıştığını ölçer, doğrulandığını değil; hedefe dönüştüğünde sayıyı en ucuz yoldan yükselten, hiçbir hatayı yakalamayan testler yazılır. Testin değeri yakaladığı hatadadır: önemli davranışı test edin, coverage yan ürün olarak gelsin. Test bütçesini karmaşık mantığa, para işleyen koda ve daha önce hata çıkmış yerlere harcayın.",
      "content_html": "<p>Bir pull request gördüm: kırk yeni test, hepsi tek bir amaç içindi — coverage’ı %100’e çıkarmak. Testlerin çoğu bir getter’ı çağırıp, az önce set edilen değerin geri döndüğünü doğruluyordu. Coverage yeşile döndü. O kırk testin yakalayacağı hata sayısı: sıfır.</p>\n<p>Test coverage yararlı bir sayıdır — bir hedefe dönüştürülene kadar.</p>\n<h2 id=\"coverage-neyi-ölçer-neyi-ölçmez\">Coverage neyi ölçer, neyi ölçmez</h2>\n<p>Coverage tek bir şeyi ölçer: testler çalışırken kodun hangi satırlarının <strong>çalıştığını</strong>. Ölçmediği şey çok daha önemli: o satırların gerçekten <strong>doğrulanıp doğrulanmadığını</strong>.</p>\n<p>Bir satır, hiçbir anlamlı <code>assert</code> içermeyen bir testle de “kapsanmış” sayılır. Kod çalışır, test yeşildir, ama kodun doğru davranıp davranmadığını kimse kontrol etmemiştir. Coverage, çalışan kod ile doğrulanan kod arasındaki farkı göremez.</p>\n<h2 id=\"yanlış-metrik-yanlış-testi-teşvik-eder\">Yanlış metrik yanlış testi teşvik eder</h2>\n<p>Coverage bir hedef hâline geldiğinde insan davranışı tahmin edilebilir: sayıyı en ucuz yoldan yükseltecek testler yazılır.</p>\n<ul>\n<li>Getter/setter testleri — mantığı olmayan kodu test etmek.</li>\n<li>Mock’un çağrıldığını doğrulayan testler — gerçek bir sonuç değil, bir etkileşim.</li>\n<li><code>assert</code>’ı anlamsız, sadece “patlamadı” diyen testler.</li>\n</ul>\n<p>Bunlar coverage sayısını yükseltir ve hiçbir hatayı yakalamaz. Daha kötüsü: yeşil bir %100, ekibe yanlış bir güven verir. Test edilmiş sanılan kod, aslında sadece çalıştırılmıştır.</p>\n<h2 id=\"i̇yi-testin-ölçüsü\">İyi testin ölçüsü</h2>\n<p>Bir testin değeri, kapsadığı satırda değil, yakaladığı hatadadır. Basit bir sınama: kodun mantığını kasten bozun — bir <code>+</code>’yı <code>-</code> yapın, bir koşulu ters çevirin. İyi bir test kırmızıya döner. Dönmüyorsa, o test bir şey doğrulamıyordur.</p>\n<p>Bu fikrin sistematik hâli mutation testing’dir: araç kodu küçük yollarla bozar ve testlerinizin bunu yakalayıp yakalamadığını ölçer. Coverage “satır çalıştı mı” der; mutation testing “satır gerçekten test edildi mi” der.</p>\n<h2 id=\"coverage-bir-yan-üründür\">Coverage bir yan üründür</h2>\n<p>Doğru sıralama tersinedir: önce önemli davranışı test edin, coverage da bir <strong>yan ürün</strong> olarak yükselsin. Sayıyı kovaladığınızda işlevsiz test yazarsınız; davranışı kovaladığınızda coverage zaten gelir — ve gelen coverage gerçektir.</p>\n<p>%100 bir hedef değildir. Hiç test edilmemiş kritik bir yol, %100’lük bir kod tabanında bile olabilir; tersine, %75 coverage’lı bir sistem her riskli yolu sağlam test ediyor olabilir.</p>\n<h2 id=\"test-bütçesini-nereye\">Test bütçesini nereye</h2>\n<p>Test yazmak da kod yazmaktır; sınırlı bir bütçesi vardır. O bütçeyi riske göre harcayın:</p>\n<ul>\n<li>Karmaşık mantık, para işleyen kod, hesaplamalar.</li>\n<li>Edge case’ler — sınır değerler, boş girdi, eşzamanlılık.</li>\n<li>Daha önce bir kez hata çıkmış yerler — hata bir kez oradan çıktıysa, yine çıkar.</li>\n</ul>\n<p>Önemsiz, mantıksız kodu test etmek bütçeyi boşa harcar. <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/gereginden-karmasik-sistem-sinyalleri/\">Gereğinden karmaşık sistem sinyallerinde</a> değindiğim “test kurulumu testin kendisinden uzun” işareti de buraya bağlanır: bir testi yazmak çok zorsa, sorun testte değil, test edilen kodun fazla bağımlı olmasındadır.</p>\n<hr/>\n<p>Coverage yüzdesi bir güven kaynağı değildir; yalnızca bir gözlemdir. Güveni testlerin yakaladığı hatalar verir — kovaladığı sayı değil.</p>\n<p>Sayıyı değil, kod yanlış olduğunda kırmızıya dönecek testi hedefleyin.</p>",
      "content_text": "> **Kısaca:** Coverage yalnızca satırın çalıştığını ölçer, doğrulandığını değil; hedefe dönüştüğünde sayıyı en ucuz yoldan yükselten, hiçbir hatayı yakalamayan testler yazılır. Testin değeri yakaladığı hatadadır: önemli davranışı test edin, coverage yan ürün olarak gelsin. Test bütçesini karmaşık mantığa, para işleyen koda ve daha önce hata çıkmış yerlere harcayın.\n\nBir pull request gördüm: kırk yeni test, hepsi tek bir amaç içindi — coverage'ı %100'e çıkarmak. Testlerin çoğu bir getter'ı çağırıp, az önce set edilen değerin geri döndüğünü doğruluyordu. Coverage yeşile döndü. O kırk testin yakalayacağı hata sayısı: sıfır.\n\nTest coverage yararlı bir sayıdır — bir hedefe dönüştürülene kadar.\n\n## Coverage neyi ölçer, neyi ölçmez\n\nCoverage tek bir şeyi ölçer: testler çalışırken kodun hangi satırlarının **çalıştığını**. Ölçmediği şey çok daha önemli: o satırların gerçekten **doğrulanıp doğrulanmadığını**.\n\nBir satır, hiçbir anlamlı `assert` içermeyen bir testle de \"kapsanmış\" sayılır. Kod çalışır, test yeşildir, ama kodun doğru davranıp davranmadığını kimse kontrol etmemiştir. Coverage, çalışan kod ile doğrulanan kod arasındaki farkı göremez.\n\n## Yanlış metrik yanlış testi teşvik eder\n\nCoverage bir hedef hâline geldiğinde insan davranışı tahmin edilebilir: sayıyı en ucuz yoldan yükseltecek testler yazılır.\n\n- Getter/setter testleri — mantığı olmayan kodu test etmek.\n- Mock'un çağrıldığını doğrulayan testler — gerçek bir sonuç değil, bir etkileşim.\n- `assert`'ı anlamsız, sadece \"patlamadı\" diyen testler.\n\nBunlar coverage sayısını yükseltir ve hiçbir hatayı yakalamaz. Daha kötüsü: yeşil bir %100, ekibe yanlış bir güven verir. Test edilmiş sanılan kod, aslında sadece çalıştırılmıştır.\n\n## İyi testin ölçüsü\n\nBir testin değeri, kapsadığı satırda değil, yakaladığı hatadadır. Basit bir sınama: kodun mantığını kasten bozun — bir `+`'yı `-` yapın, bir koşulu ters çevirin. İyi bir test kırmızıya döner. Dönmüyorsa, o test bir şey doğrulamıyordur.\n\nBu fikrin sistematik hâli mutation testing'dir: araç kodu küçük yollarla bozar ve testlerinizin bunu yakalayıp yakalamadığını ölçer. Coverage \"satır çalıştı mı\" der; mutation testing \"satır gerçekten test edildi mi\" der.\n\n## Coverage bir yan üründür\n\nDoğru sıralama tersinedir: önce önemli davranışı test edin, coverage da bir **yan ürün** olarak yükselsin. Sayıyı kovaladığınızda işlevsiz test yazarsınız; davranışı kovaladığınızda coverage zaten gelir — ve gelen coverage gerçektir.\n\n%100 bir hedef değildir. Hiç test edilmemiş kritik bir yol, %100'lük bir kod tabanında bile olabilir; tersine, %75 coverage'lı bir sistem her riskli yolu sağlam test ediyor olabilir.\n\n## Test bütçesini nereye\n\nTest yazmak da kod yazmaktır; sınırlı bir bütçesi vardır. O bütçeyi riske göre harcayın:\n\n- Karmaşık mantık, para işleyen kod, hesaplamalar.\n- Edge case'ler — sınır değerler, boş girdi, eşzamanlılık.\n- Daha önce bir kez hata çıkmış yerler — hata bir kez oradan çıktıysa, yine çıkar.\n\nÖnemsiz, mantıksız kodu test etmek bütçeyi boşa harcar. [Gereğinden karmaşık sistem sinyallerinde](/tr/muhendislik/denemeler/gereginden-karmasik-sistem-sinyalleri/) değindiğim \"test kurulumu testin kendisinden uzun\" işareti de buraya bağlanır: bir testi yazmak çok zorsa, sorun testte değil, test edilen kodun fazla bağımlı olmasındadır.\n\n---\n\nCoverage yüzdesi bir güven kaynağı değildir; yalnızca bir gözlemdir. Güveni testlerin yakaladığı hatalar verir — kovaladığı sayı değil.\n\nSayıyı değil, kod yanlış olduğunda kırmızıya dönecek testi hedefleyin.\n\n## Kaynaklar\n\n- [Test Coverage](https://martinfowler.com/bliki/TestCoverage.html) — martinfowler.com",
      "date_published": "2026-10-17T00:00:00.000Z",
      "tags": [
        "testing",
        "quality",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/ucuncu-parti-paket-bagimliligi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/ucuncu-parti-paket-bagimliligi/",
      "title": "Her Şey İçin Bir Paket Kurmanın Bedeli",
      "summary": "Küçük bir fonksiyon için paket kurmak, kodunuza kendi yetkilerinizle çalışan başkasının kodunu sokmaktır: güvenlik yüzeyi, bakım ve yükseltme sürtünmesi kurulumla bitmeyen bir fatura keser. Kripto, HTTP istemcisi ve veritabanı sürücüsü gibi zor, güvenlik-kritik işlerde paket kazanır; küçük, durağan ve iyi anlaşılan işlerde o birkaç satırı kendiniz yazmak daha ucuzdur.",
      "content_html": "<p>Bir projenin <code>composer.lock</code>’unda 180 paket vardı. Doğrudan kullanılan, elle sayınca, on ikiydi. Geri kalanı — yüzden fazla paket — çoğu zaman küçük bir fonksiyon için kurulmuş paketlerin sürüklediği bağımlılıklardı.</p>\n<p><code>composer require</code> yazmak bedava hissettirir. Değil. Her bağımlılık, kod tabanınızda başkasının koduna açılan bir kapıdır — ve o kapı bir daha tam kapanmaz.</p>\n<h2 id=\"her-bağımlılık-bir-kapıdır\">Her bağımlılık bir kapıdır</h2>\n<p>Bir paket kurduğunuzda, o paketin kodu sizin uygulamanızın yetkileriyle, sizin sunucunuzda çalışır. Veritabanınıza erişebilir, dosya sisteminizi görür, ortam değişkenlerinizi okur.</p>\n<p>Bu, paketin kötü niyetli olduğu anlamına gelmez — ama bir gün ele geçirilirse, sizin uygulamanız da ele geçirilmiş olur. Supply-chain saldırısı denen şey budur: zayıf halka sizin yazdığınız kod değil, güvendiğiniz koddur. Yüzlerce pakete güveniyorsanız, güvenmeniz gereken yüzlerce ekip var demektir.</p>\n<h2 id=\"faturanın-kalemleri\">Faturanın kalemleri</h2>\n<p>Bir bağımlılığın maliyeti kurulumla bitmez:</p>\n<ul>\n<li><strong>Güvenlik yüzeyi.</strong> Her paket, denetlemediğiniz bir kod parçasıdır. Transitive bağımlılıklarla birlikte, gerçekte neye güvendiğinizi kimse tam bilmez.</li>\n<li><strong>Bakım.</strong> Paket terk edilir, yeni bir PHP sürümünde kırılır, başka bir paketle çakışır. Onun sorunu artık sizin sorununuzdur.</li>\n<li><strong><a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/\">Yükseltme sürtünmesi</a>.</strong> Framework’ü yükseltmek istediğinizde, en eski ve en bakımsız bağımlılığınız kadar hızlısınız.</li>\n</ul>\n<p>Bu kalemler küçük bir paket için de, büyük bir paket için de ödenir.</p>\n<h2 id=\"küçük-iş-büyük-kapı\">Küçük iş, büyük kapı</h2>\n<p>Asıl mesele asimetri. Bir slug üreten, bir tarih biçimleyen, birkaç satırlık bir yardımcı — bunlar için bir paket kurmak, küçük bir kazanç için geniş bir kapı açmaktır. Kazanç birkaç satır kod; bedel ise yukarıdaki faturanın tamamı.</p>\n<p>Ünlü bir örnek var: JavaScript dünyasında, bir string’i sola dolduran on bir satırlık bir paket bir gün yayından kalktı ve binlerce projenin build’i çöktü. On bir satır için hepsi, denetlemediği bir kapıya bağlanmıştı.</p>\n<h2 id=\"ne-zaman-paket-ne-zaman-kendiniz\">Ne zaman paket, ne zaman kendiniz</h2>\n<p>Kural ideolojik değil, ekonomik:</p>\n<ul>\n<li><strong>Paket kullanın:</strong> zor, iyi test edilmiş, güvenlik-kritik işlerde. Kriptografi, HTTP istemcileri, veritabanı sürücüleri, framework’ün kendisi. Kendi kripto kodunuzu <strong>asla</strong> yazmayın — burası paketin tartışmasız kazandığı yer.</li>\n<li><strong>Kendiniz yazın:</strong> küçük, durağan ve iyi anladığınız işlerde. Birkaç satırlık bir yardımcı, koca bir bağımlılık ağacı miras almaktan ucuzdur.</li>\n</ul>\n<p>Ölçü şu: paketin çözdüğü problem, paketin getirdiği faturadan büyük mü? Büyükse kurun. Değilse, o birkaç satırı yazın ve sahiplenin.</p>\n<h2 id=\"bağımlılığı-seçerken\">Bağımlılığı seçerken</h2>\n<p>Paket kurmaya karar verdiyseniz, hangisini seçeceğinize bakın: aktif bakılıyor mu, son sürümü ne zaman çıkmış, kaç transitive bağımlılık sürüklüyor, lisansı uygun mu, topluluğu ne kadar geniş. Bir bağımlılık seçmek, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/neden-boring-architecture/\">boring architecture</a> yazısındaki innovation token mantığıyla aynı: olgun, öngörülebilir, terk edilmeyecek olanı seçin.</p>\n<hr/>\n<p>Bağımlılık eklemek, kod yazmamanın bir yolu gibi görünür. Aslında kodu başka birine yazdırıp sorumluluğunu kendinizde tutmaktır.</p>\n<p>Küçük bir iş için koca bir kapı açmadan önce sorun: bu birkaç satırı yazmak gerçekten daha mı pahalı?</p>",
      "content_text": "> **Kısaca:** Küçük bir fonksiyon için paket kurmak, kodunuza kendi yetkilerinizle çalışan başkasının kodunu sokmaktır: güvenlik yüzeyi, bakım ve yükseltme sürtünmesi kurulumla bitmeyen bir fatura keser. Kripto, HTTP istemcisi ve veritabanı sürücüsü gibi zor, güvenlik-kritik işlerde paket kazanır; küçük, durağan ve iyi anlaşılan işlerde o birkaç satırı kendiniz yazmak daha ucuzdur.\n\nBir projenin `composer.lock`'unda 180 paket vardı. Doğrudan kullanılan, elle sayınca, on ikiydi. Geri kalanı — yüzden fazla paket — çoğu zaman küçük bir fonksiyon için kurulmuş paketlerin sürüklediği bağımlılıklardı.\n\n`composer require` yazmak bedava hissettirir. Değil. Her bağımlılık, kod tabanınızda başkasının koduna açılan bir kapıdır — ve o kapı bir daha tam kapanmaz.\n\n## Her bağımlılık bir kapıdır\n\nBir paket kurduğunuzda, o paketin kodu sizin uygulamanızın yetkileriyle, sizin sunucunuzda çalışır. Veritabanınıza erişebilir, dosya sisteminizi görür, ortam değişkenlerinizi okur.\n\nBu, paketin kötü niyetli olduğu anlamına gelmez — ama bir gün ele geçirilirse, sizin uygulamanız da ele geçirilmiş olur. Supply-chain saldırısı denen şey budur: zayıf halka sizin yazdığınız kod değil, güvendiğiniz koddur. Yüzlerce pakete güveniyorsanız, güvenmeniz gereken yüzlerce ekip var demektir.\n\n## Faturanın kalemleri\n\nBir bağımlılığın maliyeti kurulumla bitmez:\n\n- **Güvenlik yüzeyi.** Her paket, denetlemediğiniz bir kod parçasıdır. Transitive bağımlılıklarla birlikte, gerçekte neye güvendiğinizi kimse tam bilmez.\n- **Bakım.** Paket terk edilir, yeni bir PHP sürümünde kırılır, başka bir paketle çakışır. Onun sorunu artık sizin sorununuzdur.\n- **[Yükseltme sürtünmesi](/tr/muhendislik/denemeler/upgrade-ertelemenin-maliyeti/).** Framework'ü yükseltmek istediğinizde, en eski ve en bakımsız bağımlılığınız kadar hızlısınız.\n\nBu kalemler küçük bir paket için de, büyük bir paket için de ödenir.\n\n## Küçük iş, büyük kapı\n\nAsıl mesele asimetri. Bir slug üreten, bir tarih biçimleyen, birkaç satırlık bir yardımcı — bunlar için bir paket kurmak, küçük bir kazanç için geniş bir kapı açmaktır. Kazanç birkaç satır kod; bedel ise yukarıdaki faturanın tamamı.\n\nÜnlü bir örnek var: JavaScript dünyasında, bir string'i sola dolduran on bir satırlık bir paket bir gün yayından kalktı ve binlerce projenin build'i çöktü. On bir satır için hepsi, denetlemediği bir kapıya bağlanmıştı.\n\n## Ne zaman paket, ne zaman kendiniz\n\nKural ideolojik değil, ekonomik:\n\n- **Paket kullanın:** zor, iyi test edilmiş, güvenlik-kritik işlerde. Kriptografi, HTTP istemcileri, veritabanı sürücüleri, framework'ün kendisi. Kendi kripto kodunuzu **asla** yazmayın — burası paketin tartışmasız kazandığı yer.\n- **Kendiniz yazın:** küçük, durağan ve iyi anladığınız işlerde. Birkaç satırlık bir yardımcı, koca bir bağımlılık ağacı miras almaktan ucuzdur.\n\nÖlçü şu: paketin çözdüğü problem, paketin getirdiği faturadan büyük mü? Büyükse kurun. Değilse, o birkaç satırı yazın ve sahiplenin.\n\n## Bağımlılığı seçerken\n\nPaket kurmaya karar verdiyseniz, hangisini seçeceğinize bakın: aktif bakılıyor mu, son sürümü ne zaman çıkmış, kaç transitive bağımlılık sürüklüyor, lisansı uygun mu, topluluğu ne kadar geniş. Bir bağımlılık seçmek, [boring architecture](/tr/muhendislik/denemeler/neden-boring-architecture/) yazısındaki innovation token mantığıyla aynı: olgun, öngörülebilir, terk edilmeyecek olanı seçin.\n\n---\n\nBağımlılık eklemek, kod yazmamanın bir yolu gibi görünür. Aslında kodu başka birine yazdırıp sorumluluğunu kendinizde tutmaktır.\n\nKüçük bir iş için koca bir kapı açmadan önce sorun: bu birkaç satırı yazmak gerçekten daha mı pahalı?\n\n## Kaynaklar\n\n- [kik, left-pad, and npm](https://blog.npmjs.org/post/141577284765/kik-left-pad-and-npm) — npm",
      "date_published": "2026-10-10T00:00:00.000Z",
      "tags": [
        "dependencies",
        "security",
        "simplicity",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/cqrs-ne-zaman-gerekli/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/cqrs-ne-zaman-gerekli/",
      "title": "CQRS: Ne Zaman Gerekli, Ne Zaman Gereksiz Yük?",
      "summary": "CQRS yazma modelini okuma modelinden ayırır; bu mutlaka iki veritabanı ya da read replica demek değildir. Yazma ile okumanın şekli gerçekten ayrışan karmaşık domain'lerde netlik kazandırır, basit CRUD'da ise senkron tutulacak ikinci bir model ve iki kat koddan başka bir şey getirmez. Hafif CQRS, ayrı bir sorgu sınıfıyla başlar.",
      "content_html": "<p>Basit bir CRUD uygulamasında “CQRS uyguladık” denen bir kod tabanı gördüm. Tek bir “kullanıcı oluştur” işlemi için bir Command, bir CommandHandler, ayrı bir okuma modeli ve bir projection vardı. Bütün bu tören, sonunda tek bir satır <code>INSERT</code> ediyordu.</p>\n<p>CQRS bazı sistemlerde gerçek bir netlik kazandırır. Bu uygulamada kazandırdığı tek şey, iki kat koddu.</p>\n<h2 id=\"cqrs-nedir-kısaca\">CQRS nedir, kısaca</h2>\n<p>CQRS — Command Query Responsibility Segregation — tek bir fikre dayanır: <strong>yazma modeli</strong> ile <strong>okuma modelini</strong> birbirinden ayırmak.</p>\n<p>Klasik yaklaşımda tek bir model hem yazılır hem okunur — aynı <code>Order</code> sınıfıyla hem sipariş oluşturulur hem ekranda gösterilir. CQRS bunu ikiye böler: yazma tarafı kuralları zorlayan, normalize bir model; okuma tarafı ekrana göre şekillenmiş, denormalize bir model.</p>\n<p>Dikkat: bu mutlaka iki veritabanı demek değildir. Çoğu zaman iki <strong>model</strong> demektir — aynı veritabanında bile olabilir.</p>\n<h2 id=\"read-replica-ile-karıştırmayın\">Read replica ile karıştırmayın</h2>\n<p>Sık yapılan bir karışıklık: CQRS ile read replica aynı şey sanılır. Değiller.</p>\n<ul>\n<li><strong>Read replica</strong> (<a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/read-write-splitting/\">read/write splitting</a>) bir altyapı kararıdır: aynı model, farklı sunucu. Okuma yükünü ölçeklemek için.</li>\n<li><strong>CQRS</strong> bir modelleme kararıdır: farklı model, aynı ya da farklı yer. Okuma ile yazmanın <strong>şekli</strong> ayrıştığı için.</li>\n</ul>\n<p>Birini diğeri olmadan kullanabilirsiniz. Read replica’nız olur ama tek modeliniz vardır; ya da CQRS yaparsınız ama tek sunucuda.</p>\n<h2 id=\"cqrs-neyi-kazandırır\">CQRS neyi kazandırır</h2>\n<p>CQRS, yazma modeli ile okuma modeli <strong>gerçekten</strong> ayrıştığında değer katar. Bu, karmaşık domain’lerde olur: yazdığınız şekil ile okuduğunuz şekil birbirinden uzaklaşır.</p>\n<p>Yazma tarafı normalize, değişmezleri (invariant) zorlayan, iş kurallarıyla yüklü bir modeldir. Okuma tarafı ise ekranların ihtiyacına göre düzleştirilmiş, önceden hesaplanmış, hızlı sorgulanan bir modeldir. İkisini tek bir model olmaya zorlamak, ikisini de orta karar yapar.</p>\n<h2 id=\"basit-crudda-gereksiz-yük\">Basit CRUD’da gereksiz yük</h2>\n<p>Sorun şu: basit bir CRUD uygulamasında yazma modeli ile okuma modeli <strong>zaten aynıdır</strong> — ikisi de o satırın kendisi. Orada CQRS uygulamak, var olmayan bir ayrımı modellemektir.</p>\n<p>Sonuç: iki model, senkron tutulması gereken iki yapı, iki kat kod — ve karşılığında sıfır kazanç. Bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> desen ölçeğindeki hâlidir: bugün olmayan bir karmaşıklık için bugünden bedel ödemek.</p>\n<h2 id=\"hafif-cqrs\">Hafif CQRS</h2>\n<p>CQRS “yapmak” için event sourcing’e, ayrı veritabanlarına, projection altyapısına ihtiyacınız yok. Bunlar CQRS’in en ağır biçimidir, zorunlu hâli değil.</p>\n<p>Hafif bir başlangıç: yazma tarafını normal Eloquent modelleriyle bırakın, okuma tarafı için ekrana göre şekillenmiş bir DTO döndüren ayrı bir sorgu sınıfı yazın. Yazma ile okuma yolu kodda ayrılmıştır — bu zaten CQRS’in özüdür. Ağır altyapıya ancak gerçekten gerektiğinde geçin.</p>\n<blockquote><p><strong>Tan:</strong> Okuma modeli yazmanın ardından ayrı bir akışla güncelleniyorsa, kullanıcı az önce kaydettiğini ekranda göremeyebilir. CQRS'e karar vermeden önce bu gecikmenin ürün tarafında kabul edilebilir olup olmadığını sorun. Hafif CQRS bu soruyu çoğu zaman gündemden düşürür.</p></blockquote>\n<h2 id=\"ne-zaman-gerçekten-gerekli\">Ne zaman gerçekten gerekli</h2>\n<p>CQRS’i ciddiye almak için şu sinyaller olmalı:</p>\n<ul>\n<li>Domain karmaşıklığı yüksek — yazma tarafı ciddi iş kuralları taşıyor (<a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/katmanli-mimari-mi-clean-architecture-mi/\">Clean Architecture’ın</a> değer kattığı türden bir domain).</li>\n<li>Okuma ile yazmanın şekli ve yükü belirgin biçimde farklı.</li>\n<li>Çoğu zaman <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/event-driven-mimari-ne-zaman/\">event-driven mimari</a> ile birlikte — okuma modeli, yazma tarafının yaydığı event’lerle güncelleniyor.</li>\n</ul>\n<p>Bu sinyaller yoksa, tek model ve gerektiğinde ayrı bir okuma sorgusu fazlasıyla yeter.</p>\n<hr/>\n<p>CQRS bir olgunluk rozeti değil, belirli bir problemin — okuma ile yazmanın gerçekten ayrışması — çözümüdür. O problem sizde yoksa, kazandırdığı tek şey ikinci bir modeli senkron tutma zahmetidir.</p>\n<p>Modeli ayırmadan önce, okuma ile yazmanızın gerçekten ayrı olup olmadığına bakın.</p>",
      "content_text": "> **Kısaca:** CQRS yazma modelini okuma modelinden ayırır; bu mutlaka iki veritabanı ya da read replica demek değildir. Yazma ile okumanın şekli gerçekten ayrışan karmaşık domain'lerde netlik kazandırır, basit CRUD'da ise senkron tutulacak ikinci bir model ve iki kat koddan başka bir şey getirmez. Hafif CQRS, ayrı bir sorgu sınıfıyla başlar.\n\nBasit bir CRUD uygulamasında \"CQRS uyguladık\" denen bir kod tabanı gördüm. Tek bir \"kullanıcı oluştur\" işlemi için bir Command, bir CommandHandler, ayrı bir okuma modeli ve bir projection vardı. Bütün bu tören, sonunda tek bir satır `INSERT` ediyordu.\n\nCQRS bazı sistemlerde gerçek bir netlik kazandırır. Bu uygulamada kazandırdığı tek şey, iki kat koddu.\n\n## CQRS nedir, kısaca\n\nCQRS — Command Query Responsibility Segregation — tek bir fikre dayanır: **yazma modeli** ile **okuma modelini** birbirinden ayırmak.\n\nKlasik yaklaşımda tek bir model hem yazılır hem okunur — aynı `Order` sınıfıyla hem sipariş oluşturulur hem ekranda gösterilir. CQRS bunu ikiye böler: yazma tarafı kuralları zorlayan, normalize bir model; okuma tarafı ekrana göre şekillenmiş, denormalize bir model.\n\nDikkat: bu mutlaka iki veritabanı demek değildir. Çoğu zaman iki **model** demektir — aynı veritabanında bile olabilir.\n\n## Read replica ile karıştırmayın\n\nSık yapılan bir karışıklık: CQRS ile read replica aynı şey sanılır. Değiller.\n\n- **Read replica** ([read/write splitting](/tr/muhendislik/notlar/read-write-splitting/)) bir altyapı kararıdır: aynı model, farklı sunucu. Okuma yükünü ölçeklemek için.\n- **CQRS** bir modelleme kararıdır: farklı model, aynı ya da farklı yer. Okuma ile yazmanın **şekli** ayrıştığı için.\n\nBirini diğeri olmadan kullanabilirsiniz. Read replica'nız olur ama tek modeliniz vardır; ya da CQRS yaparsınız ama tek sunucuda.\n\n## CQRS neyi kazandırır\n\nCQRS, yazma modeli ile okuma modeli **gerçekten** ayrıştığında değer katar. Bu, karmaşık domain'lerde olur: yazdığınız şekil ile okuduğunuz şekil birbirinden uzaklaşır.\n\nYazma tarafı normalize, değişmezleri (invariant) zorlayan, iş kurallarıyla yüklü bir modeldir. Okuma tarafı ise ekranların ihtiyacına göre düzleştirilmiş, önceden hesaplanmış, hızlı sorgulanan bir modeldir. İkisini tek bir model olmaya zorlamak, ikisini de orta karar yapar.\n\n## Basit CRUD'da gereksiz yük\n\nSorun şu: basit bir CRUD uygulamasında yazma modeli ile okuma modeli **zaten aynıdır** — ikisi de o satırın kendisi. Orada CQRS uygulamak, var olmayan bir ayrımı modellemektir.\n\nSonuç: iki model, senkron tutulması gereken iki yapı, iki kat kod — ve karşılığında sıfır kazanç. Bu, [spekülatif genelliğin](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) desen ölçeğindeki hâlidir: bugün olmayan bir karmaşıklık için bugünden bedel ödemek.\n\n## Hafif CQRS\n\nCQRS \"yapmak\" için event sourcing'e, ayrı veritabanlarına, projection altyapısına ihtiyacınız yok. Bunlar CQRS'in en ağır biçimidir, zorunlu hâli değil.\n\nHafif bir başlangıç: yazma tarafını normal Eloquent modelleriyle bırakın, okuma tarafı için ekrana göre şekillenmiş bir DTO döndüren ayrı bir sorgu sınıfı yazın. Yazma ile okuma yolu kodda ayrılmıştır — bu zaten CQRS'in özüdür. Ağır altyapıya ancak gerçekten gerektiğinde geçin.\n\n\n> **Tan:** Okuma modeli yazmanın ardından ayrı bir akışla güncelleniyorsa, kullanıcı az önce kaydettiğini ekranda göremeyebilir. CQRS'e karar vermeden önce bu gecikmenin ürün tarafında kabul edilebilir olup olmadığını sorun. Hafif CQRS bu soruyu çoğu zaman gündemden düşürür.\n\n\n## Ne zaman gerçekten gerekli\n\nCQRS'i ciddiye almak için şu sinyaller olmalı:\n\n- Domain karmaşıklığı yüksek — yazma tarafı ciddi iş kuralları taşıyor ([Clean Architecture'ın](/tr/muhendislik/denemeler/katmanli-mimari-mi-clean-architecture-mi/) değer kattığı türden bir domain).\n- Okuma ile yazmanın şekli ve yükü belirgin biçimde farklı.\n- Çoğu zaman [event-driven mimari](/tr/muhendislik/notlar/event-driven-mimari-ne-zaman/) ile birlikte — okuma modeli, yazma tarafının yaydığı event'lerle güncelleniyor.\n\nBu sinyaller yoksa, tek model ve gerektiğinde ayrı bir okuma sorgusu fazlasıyla yeter.\n\n---\n\nCQRS bir olgunluk rozeti değil, belirli bir problemin — okuma ile yazmanın gerçekten ayrışması — çözümüdür. O problem sizde yoksa, kazandırdığı tek şey ikinci bir modeli senkron tutma zahmetidir.\n\nModeli ayırmadan önce, okuma ile yazmanızın gerçekten ayrı olup olmadığına bakın.\n\n## Kaynaklar\n\n- [CQRS](https://martinfowler.com/bliki/CQRS.html) — martinfowler.com",
      "date_published": "2026-10-03T00:00:00.000Z",
      "tags": [
        "architecture",
        "cqrs",
        "patterns"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/arsivleme-stratejisi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/arsivleme-stratejisi/",
      "title": "Arşivleme Stratejisi: Eski Veriyi Yavaşlatmadan Ayırma",
      "summary": "Sıcak ve soğuk veri aynı tabloda yaşarsa soğuk veri her sorguyu yavaşlatır: indeksler şişer, autovacuum yetişemez. Arşivleme bir temizlik işi değil, ilk gün tasarlanan bir yaşam döngüsüdür. Zamana göre partition'lanmış tabloda eski partition DETACH ile ayrılır, soğuk depoya taşınır ve erişim kapısı kapanmaz.",
      "content_html": "<p>Sekiz yıllık veri biriktiren bir <code>orders</code> tablosu gördüm. Sorguların %95’i son 90 güne bakıyordu — ama her sorgu, kalan sekiz yılın bedelini indeks boyutunda ve vacuum süresinde ödüyordu. Sıcak veri, soğuk verinin ağırlığını taşıyordu.</p>\n<p>Hiçbir tablo sonsuza dek büyüyemez. Arşivleme, bunu kabul edip baştan planlamaktır.</p>\n<h2 id=\"tablo-sonsuza-dek-büyüyemez\">Tablo sonsuza dek büyüyemez</h2>\n<p>Çoğu tablonun doğal bir sıcak/soğuk ayrımı vardır. Aktif veri — bu haftanın siparişleri, açık biletler — sürekli okunur ve yazılır. Tarihsel veri — üç yıl önce kapanmış kayıtlar — neredeyse hiç değişmez, nadiren okunur. Ama ikisi aynı tabloda yaşadığında, soğuk veri sıcak verinin her sorgusunu yavaşlatır: indeksler şişer, autovacuum yetişemez, working set yapay olarak büyür.</p>\n<h2 id=\"arşivleme-bir-yaşam-döngüsüdür\">Arşivleme bir yaşam döngüsüdür</h2>\n<p>Arşivleme, tablo paniğe yol açacak kadar büyüdüğünde akla gelen bir temizlik işi değildir. Verinin bir yaşam döngüsüdür ve ilk gün tasarlanmalıdır: veri sıcak doğar, zamanla ılınır, sonra soğur. Soru “arşivleyecek miyiz” değil, “soğuyan veri nereye gidecek”.</p>\n<h2 id=\"partition-tabanlı-arşivleme\">Partition tabanlı arşivleme</h2>\n<p>En temiz yöntem, tabloyu zamana göre partition’lamaktır — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerin</a> üçüncü kırılma noktasında anlatılan yapı. Append ağırlıklı veride doğal anahtar zamandır ve partition’lar arşivlemeyi neredeyse bedava kılar.</p>\n<p>Eski bir partition’ı silmek yerine <strong>ayırırsınız</strong>:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ALTER</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders DETACH </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">PARTITION</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders_2023_01;</span></span></code></pre>\n<p><code>DETACH</code> ile partition, ana tablodan kopar ama veritabanında ayrı bir tablo olarak durmaya devam eder. Artık onu istediğiniz gibi taşıyabilirsiniz — silmeden, dondurmadan.</p>\n<h2 id=\"soğuk-depolama-seçenekleri\">Soğuk depolama seçenekleri</h2>\n<p>Ayrılan partition’lar, maliyet ve erişim ihtiyacına göre farklı yerlere gidebilir:</p>\n<ul>\n<li><strong>Ayrı bir arşiv şeması</strong> — aynı veritabanında ama <code>archive</code> şemasında; sorgulanabilir, sadece sıcak yoldan uzakta.</li>\n<li><strong>Daha ucuz disk</strong> — partition’ın dosyalarını farklı bir tablespace’e taşımak.</li>\n<li><strong>Nesne depolama</strong> — partition’ı sıkıştırılmış bir dökümle ya da Parquet olarak S3 benzeri bir depoya almak; en ucuzu, en yavaş erişileni.</li>\n<li><strong>Ayrı bir arşiv veritabanı</strong> — canlı sistemin kapasitesinden tamamen bağımsız.</li>\n</ul>\n<h2 id=\"arşivlerken-erişimi-korumak\">Arşivlerken erişimi korumak</h2>\n<p>İşin zor kısmı budur: arşivlenen veri çoğu zaman tamamen ölü değildir. Bir denetim, bir destek talebi, yasal bir zorunluluk onu yeniden gerektirebilir. Arşivlemeyi “erişilemez yapmak” ile karıştırmayın.</p>\n<p>Pratik dengeler: arşiv tablolarını sorgulanabilir tutmak (sadece ucuz depoda); nesne depolamadaki veriye foreign data wrapper ile erişmek; ya da açıkça “arşiv sorgusu” diye bilinen, yavaş ama çalışan ayrı bir yol bırakmak. Hangisini seçerseniz seçin, soğuk veriye bir kapı bırakın — yeter ki o kapı sıcak yoldan ayrı olsun.</p>\n<h2 id=\"silmek-de-bir-arşiv-kararı\">Silmek de bir arşiv kararı</h2>\n<p>Bazen doğru arşiv, silmektir. Her verinin saklanması gerekmez; bir saklama (retention) politikası, “bu veri ne kadar süre yaşar” sorusuna baştan cevap verir. KVKK/GDPR gibi düzenlemeler bazı veriler için silmeyi zaten zorunlu kılar. Silmeyi de yaşam döngüsünün bir adımı olarak tasarlayın — sonradan akla gelen bir iş olarak değil.</p>\n<hr/>\n<p>Arşivleme stratejisi, tablonun büyüyeceğini ilk gün kabul etmekle başlar. Sıcak veriyi hızlı tutmanın yolu, soğuk veriyi ona yük olmaktan çıkarmaktır.</p>\n<p>Tablonuz büyüyecek; tek soru, bunu sizin mi planladığınız, yoksa production’ın mı size hatırlattığı.</p>",
      "content_text": "> **Kısaca:** Sıcak ve soğuk veri aynı tabloda yaşarsa soğuk veri her sorguyu yavaşlatır: indeksler şişer, autovacuum yetişemez. Arşivleme bir temizlik işi değil, ilk gün tasarlanan bir yaşam döngüsüdür. Zamana göre partition'lanmış tabloda eski partition DETACH ile ayrılır, soğuk depoya taşınır ve erişim kapısı kapanmaz.\n\nSekiz yıllık veri biriktiren bir `orders` tablosu gördüm. Sorguların %95'i son 90 güne bakıyordu — ama her sorgu, kalan sekiz yılın bedelini indeks boyutunda ve vacuum süresinde ödüyordu. Sıcak veri, soğuk verinin ağırlığını taşıyordu.\n\nHiçbir tablo sonsuza dek büyüyemez. Arşivleme, bunu kabul edip baştan planlamaktır.\n\n## Tablo sonsuza dek büyüyemez\n\nÇoğu tablonun doğal bir sıcak/soğuk ayrımı vardır. Aktif veri — bu haftanın siparişleri, açık biletler — sürekli okunur ve yazılır. Tarihsel veri — üç yıl önce kapanmış kayıtlar — neredeyse hiç değişmez, nadiren okunur. Ama ikisi aynı tabloda yaşadığında, soğuk veri sıcak verinin her sorgusunu yavaşlatır: indeksler şişer, autovacuum yetişemez, working set yapay olarak büyür.\n\n## Arşivleme bir yaşam döngüsüdür\n\nArşivleme, tablo paniğe yol açacak kadar büyüdüğünde akla gelen bir temizlik işi değildir. Verinin bir yaşam döngüsüdür ve ilk gün tasarlanmalıdır: veri sıcak doğar, zamanla ılınır, sonra soğur. Soru \"arşivleyecek miyiz\" değil, \"soğuyan veri nereye gidecek\".\n\n## Partition tabanlı arşivleme\n\nEn temiz yöntem, tabloyu zamana göre partition'lamaktır — [veri yoğunluklu sistemlerin](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) üçüncü kırılma noktasında anlatılan yapı. Append ağırlıklı veride doğal anahtar zamandır ve partition'lar arşivlemeyi neredeyse bedava kılar.\n\nEski bir partition'ı silmek yerine **ayırırsınız**:\n\n```sql\nALTER TABLE orders DETACH PARTITION orders_2023_01;\n```\n\n`DETACH` ile partition, ana tablodan kopar ama veritabanında ayrı bir tablo olarak durmaya devam eder. Artık onu istediğiniz gibi taşıyabilirsiniz — silmeden, dondurmadan.\n\n## Soğuk depolama seçenekleri\n\nAyrılan partition'lar, maliyet ve erişim ihtiyacına göre farklı yerlere gidebilir:\n\n- **Ayrı bir arşiv şeması** — aynı veritabanında ama `archive` şemasında; sorgulanabilir, sadece sıcak yoldan uzakta.\n- **Daha ucuz disk** — partition'ın dosyalarını farklı bir tablespace'e taşımak.\n- **Nesne depolama** — partition'ı sıkıştırılmış bir dökümle ya da Parquet olarak S3 benzeri bir depoya almak; en ucuzu, en yavaş erişileni.\n- **Ayrı bir arşiv veritabanı** — canlı sistemin kapasitesinden tamamen bağımsız.\n\n## Arşivlerken erişimi korumak\n\nİşin zor kısmı budur: arşivlenen veri çoğu zaman tamamen ölü değildir. Bir denetim, bir destek talebi, yasal bir zorunluluk onu yeniden gerektirebilir. Arşivlemeyi \"erişilemez yapmak\" ile karıştırmayın.\n\nPratik dengeler: arşiv tablolarını sorgulanabilir tutmak (sadece ucuz depoda); nesne depolamadaki veriye foreign data wrapper ile erişmek; ya da açıkça \"arşiv sorgusu\" diye bilinen, yavaş ama çalışan ayrı bir yol bırakmak. Hangisini seçerseniz seçin, soğuk veriye bir kapı bırakın — yeter ki o kapı sıcak yoldan ayrı olsun.\n\n## Silmek de bir arşiv kararı\n\nBazen doğru arşiv, silmektir. Her verinin saklanması gerekmez; bir saklama (retention) politikası, \"bu veri ne kadar süre yaşar\" sorusuna baştan cevap verir. KVKK/GDPR gibi düzenlemeler bazı veriler için silmeyi zaten zorunlu kılar. Silmeyi de yaşam döngüsünün bir adımı olarak tasarlayın — sonradan akla gelen bir iş olarak değil.\n\n---\n\nArşivleme stratejisi, tablonun büyüyeceğini ilk gün kabul etmekle başlar. Sıcak veriyi hızlı tutmanın yolu, soğuk veriyi ona yük olmaktan çıkarmaktır.\n\nTablonuz büyüyecek; tek soru, bunu sizin mi planladığınız, yoksa production'ın mı size hatırlattığı.\n\n## Kaynaklar\n\n- [PostgreSQL 18: Table Partitioning](https://www.postgresql.org/docs/18/ddl-partitioning.html) — PostgreSQL",
      "date_published": "2026-09-26T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "performance",
        "production"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/buyuk-veri-sendromu/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/buyuk-veri-sendromu/",
      "title": "Verin Gerçekten Büyük mü, Yoksa Kötü mü Modellenmiş?",
      "summary": "Verimiz büyük diyen ekip çoğu zaman bir ölçüme değil, bir yavaşlık hissine dayanır; 12 GB'lık bir tablo modern bir PostgreSQL için küçüktür. Veri gerçekten büyük olmadan dağıtık sisteme geçmek en pahalı yanlış dönüştür; vergisi peşin ödenir ve karşılığında çözülen bir şey olmaz. Sayı yoksa ortada büyük veri değil, ölçülmemiş veri vardır.",
      "content_html": "<p>Bir toplantıda şu cümle kuruldu: “Verimiz çok büyüdü, artık dağıtık bir veritabanına geçmemiz lazım.” Sonra asıl tabloya baktık: 12 GB. Modern bir sunucudaki PostgreSQL için 12 GB büyük değil — küçük.</p>\n<p>“Büyük veri” en sık, hiç de büyük olmayan veriler hakkında söylenir. Ve bu cümle bir teşhis değil, çoğu zaman bir bahanedir.</p>\n<h2 id=\"büyük-bir-his-bir-ölçüm-değil\">”Büyük” bir his, bir ölçüm değil</h2>\n<p>“Verimiz büyük” cümlesi neredeyse hiçbir zaman bir sayıya dayanmaz. Bir yavaşlık hissine, bir korkuya, bir konferans konuşmasına dayanır. Oysa modern bir PostgreSQL, sıradan bir sunucuda yüzlerce GB’ı, dikkatli kurulmuşsa düşük TB’ları rahatça taşır.</p>\n<p>Bir verinin büyük olup olmadığı hissedilmez; ölçülür. Ölçmeden verilen “büyük” kararı, yanlış bir mimariye atılan ilk adımdır.</p>\n<h2 id=\"çoğu-büyük-veri-problemi-aslında-ne\">Çoğu “büyük veri” problemi aslında ne</h2>\n<p>Büyük veri sanılan yavaşlığın gerçek sebebi neredeyse her zaman şu üçünden biridir:</p>\n<ul>\n<li><strong>Eksik indeks.</strong> Sequential scan yapan bir sorgu, 5 GB’lık bir tabloyu 500 GB gibi hissettirir. Sorun boyut değil, taranan boyuttur.</li>\n<li><strong>Kötü modelleme.</strong> Normalize edilmemiş tablolar, her satıra gömülü dev JSON blob’ları, append ağırlıklı bir tablonun hiç partition’lanmamış olması. Veri “büyük” değil, kötü yerleştirilmiş.</li>\n<li><strong>Yanlış erişim deseni.</strong> N+1 sorgular, tek kolon yeterken tüm satırın çekilmesi, döngü içinde sorgu. Uygulama, küçük veriyi büyük kullanıyor.</li>\n</ul>\n<p>Üçü de boyutla değil, <strong>disiplinle</strong> çözülür — ve üçü de dağıtık bir veritabanı gerektirmez.</p>\n<h2 id=\"gerçek-büyüklüğü-ölçmek\">Gerçek büyüklüğü ölçmek</h2>\n<p>“Büyük mü” sorusuna cevap vermek için somut sorular sorun:</p>\n<ul>\n<li>Tablo gerçekte ne kadar yer kaplıyor? <code>pg_total_relation_size</code> ile bakın — tahminle değil.</li>\n<li>Sık erişilen veri (working set) RAM’e sığıyor mu? <code>pg_stat_database</code>’in cache hit ratio’su %99’un altına düştü mü?</li>\n<li>Büyüme hızı ne? Ayda 1 GB ile ayda 100 GB bambaşka kararlardır.</li>\n<li>Hangi sorgu yavaş ve <strong>neden</strong>? <code>EXPLAIN ANALYZE</code> çıktısı ne diyor?</li>\n</ul>\n<p>Bu soruların cevaplarıyla gerçek bir kırılma noktası bulduysanız, sırayla ne yapılacağı — indeks, read replica, partitioning — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerin kırılma noktaları</a> yazısında. Bu yazı, o adımlardan önce gelen ölçüm hakkında.</p>\n<h2 id=\"dağıtık-sistem-vergisi\">Dağıtık sistem vergisi</h2>\n<p>Veri gerçekten büyük olmadan dağıtık bir sisteme geçmek, en pahalı yanlış dönüştür. Çünkü maliyetleri <strong>peşinen</strong> ödersiniz — elle sharding veya <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/nosql-tuzagi/\">dağıtık NoSQL</a>’de tablolar arası <code>JOIN</code>’i kaybetmek ve eventual consistency; dağıtık SQL’de bile iki kat operasyon yükü ve dağıtık debugging — ve karşılığında çözülen hiçbir şey olmaz. Var olmayan bir problem için gerçek bir vergi.</p>\n<p><a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/\">PostgreSQL her şeye yeter mi</a> yazısındaki kural burada da geçerli: çoğu ekip PostgreSQL’in <code>jsonb</code>, partitioning, BRIN ve indeks yeteneklerinin küçük bir kısmını kullanıp “yetmiyor” sonucuna varır.</p>\n<h2 id=\"gerçekten-büyükse\">Gerçekten büyükse</h2>\n<p>Dürüst olalım: gerçek büyük veri vardır. Tek bir büyük makinenin yazma kapasitesini sürdürülebilir biçimde aşan throughput, ya da gerçekten çok-TB’lık sıcak bir working set — bunlar gerçek sınırlardır. Oraya ulaşan bir sistem, o karmaşıklığı finanse edecek bir ölçeğe de ulaşmıştır; o noktada dağıtık mimari bir borç değil, kazanılmış bir bedeldir.</p>\n<p>Mesele dağıtık sistemlere karşı olmak değil; onlara, gerçekten gerekmeden — yani ölçmeden — geçmemektir.</p>\n<hr/>\n<p>“Verimiz büyük” cümlesini duyduğunuzda istenecek tek şey bir sayıdır. Sayı yoksa, ortada büyük veri değil, ölçülmemiş veri vardır.</p>\n<p>Gerçek büyüklük ölçülür; hissedilen büyüklük çoğu zaman eksik bir indekstir.</p>",
      "content_text": "> **Kısaca:** Verimiz büyük diyen ekip çoğu zaman bir ölçüme değil, bir yavaşlık hissine dayanır; 12 GB'lık bir tablo modern bir PostgreSQL için küçüktür. Veri gerçekten büyük olmadan dağıtık sisteme geçmek en pahalı yanlış dönüştür; vergisi peşin ödenir ve karşılığında çözülen bir şey olmaz. Sayı yoksa ortada büyük veri değil, ölçülmemiş veri vardır.\n\nBir toplantıda şu cümle kuruldu: \"Verimiz çok büyüdü, artık dağıtık bir veritabanına geçmemiz lazım.\" Sonra asıl tabloya baktık: 12 GB. Modern bir sunucudaki PostgreSQL için 12 GB büyük değil — küçük.\n\n\"Büyük veri\" en sık, hiç de büyük olmayan veriler hakkında söylenir. Ve bu cümle bir teşhis değil, çoğu zaman bir bahanedir.\n\n## \"Büyük\" bir his, bir ölçüm değil\n\n\"Verimiz büyük\" cümlesi neredeyse hiçbir zaman bir sayıya dayanmaz. Bir yavaşlık hissine, bir korkuya, bir konferans konuşmasına dayanır. Oysa modern bir PostgreSQL, sıradan bir sunucuda yüzlerce GB'ı, dikkatli kurulmuşsa düşük TB'ları rahatça taşır.\n\nBir verinin büyük olup olmadığı hissedilmez; ölçülür. Ölçmeden verilen \"büyük\" kararı, yanlış bir mimariye atılan ilk adımdır.\n\n## Çoğu \"büyük veri\" problemi aslında ne\n\nBüyük veri sanılan yavaşlığın gerçek sebebi neredeyse her zaman şu üçünden biridir:\n\n- **Eksik indeks.** Sequential scan yapan bir sorgu, 5 GB'lık bir tabloyu 500 GB gibi hissettirir. Sorun boyut değil, taranan boyuttur.\n- **Kötü modelleme.** Normalize edilmemiş tablolar, her satıra gömülü dev JSON blob'ları, append ağırlıklı bir tablonun hiç partition'lanmamış olması. Veri \"büyük\" değil, kötü yerleştirilmiş.\n- **Yanlış erişim deseni.** N+1 sorgular, tek kolon yeterken tüm satırın çekilmesi, döngü içinde sorgu. Uygulama, küçük veriyi büyük kullanıyor.\n\nÜçü de boyutla değil, **disiplinle** çözülür — ve üçü de dağıtık bir veritabanı gerektirmez.\n\n## Gerçek büyüklüğü ölçmek\n\n\"Büyük mü\" sorusuna cevap vermek için somut sorular sorun:\n\n- Tablo gerçekte ne kadar yer kaplıyor? `pg_total_relation_size` ile bakın — tahminle değil.\n- Sık erişilen veri (working set) RAM'e sığıyor mu? `pg_stat_database`'in cache hit ratio'su %99'un altına düştü mü?\n- Büyüme hızı ne? Ayda 1 GB ile ayda 100 GB bambaşka kararlardır.\n- Hangi sorgu yavaş ve **neden**? `EXPLAIN ANALYZE` çıktısı ne diyor?\n\nBu soruların cevaplarıyla gerçek bir kırılma noktası bulduysanız, sırayla ne yapılacağı — indeks, read replica, partitioning — [veri yoğunluklu sistemlerin kırılma noktaları](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) yazısında. Bu yazı, o adımlardan önce gelen ölçüm hakkında.\n\n## Dağıtık sistem vergisi\n\nVeri gerçekten büyük olmadan dağıtık bir sisteme geçmek, en pahalı yanlış dönüştür. Çünkü maliyetleri **peşinen** ödersiniz — elle sharding veya [dağıtık NoSQL](/tr/muhendislik/denemeler/nosql-tuzagi/)'de tablolar arası `JOIN`'i kaybetmek ve eventual consistency; dağıtık SQL'de bile iki kat operasyon yükü ve dağıtık debugging — ve karşılığında çözülen hiçbir şey olmaz. Var olmayan bir problem için gerçek bir vergi.\n\n[PostgreSQL her şeye yeter mi](/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/) yazısındaki kural burada da geçerli: çoğu ekip PostgreSQL'in `jsonb`, partitioning, BRIN ve indeks yeteneklerinin küçük bir kısmını kullanıp \"yetmiyor\" sonucuna varır.\n\n## Gerçekten büyükse\n\nDürüst olalım: gerçek büyük veri vardır. Tek bir büyük makinenin yazma kapasitesini sürdürülebilir biçimde aşan throughput, ya da gerçekten çok-TB'lık sıcak bir working set — bunlar gerçek sınırlardır. Oraya ulaşan bir sistem, o karmaşıklığı finanse edecek bir ölçeğe de ulaşmıştır; o noktada dağıtık mimari bir borç değil, kazanılmış bir bedeldir.\n\nMesele dağıtık sistemlere karşı olmak değil; onlara, gerçekten gerekmeden — yani ölçmeden — geçmemektir.\n\n---\n\n\"Verimiz büyük\" cümlesini duyduğunuzda istenecek tek şey bir sayıdır. Sayı yoksa, ortada büyük veri değil, ölçülmemiş veri vardır.\n\nGerçek büyüklük ölçülür; hissedilen büyüklük çoğu zaman eksik bir indekstir.",
      "date_published": "2026-09-19T00:00:00.000Z",
      "tags": [
        "database",
        "scaling",
        "architecture",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/",
      "title": "PostgreSQL Kuyruk Tablosunu Canlı Kümeye Göre Kurmak",
      "summary": "Bir kuyruk tablosunda iş gören küme bekleyen satırlardır, tablo ise tamamlanmış satırlarla sonsuza kadar büyür. Tablonun boyutuyla ölçeklenen her şey yanlış ölçeklenir: 10 milyon satırda composite index 310 MB'a çıkarken partial index 7,6 MB'ta kaldı, varsayılan autovacuum eşiği ise 15 dakikada biriken 1,75 milyon dead tuple'a hiç ulaşmadı.",
      "content_html": "<p>On milyon satırlık bir <code>jobs</code> tablosunda kuyrukta bekleyen iş 5.000 taneydi; claim sorgusunun kullandığı index ise 310 MB tutuyordu. Aynı işi 7,6 MB’lık bir partial index görüyordu ve ikisinin arasındaki hız farkı yüzde 6,9’du.</p>\n<p>Yani mesele hız değildi. Mesele, neyin tablonun boyutuyla birlikte büyüdüğüydü.</p>\n<h2 id=\"kuyruk-tablosu-iki-tablodur\">Kuyruk tablosu iki tablodur</h2>\n<p>Bir kuyruk tablosunda iki ayrı küme yaşar:</p>\n<ul>\n<li><strong>Canlı küme</strong> — <code>pending</code> durumundaki satırlar. Worker’ların aradığı, claim’lerin kilitlediği, gerçekten iş gören kısım. Boyutu throughput ile üretim hızı arasındaki dengeye bağlıdır; sağlıklı bir sistemde kabaca sabit durur.</li>\n<li><strong>Arşiv</strong> — <code>done</code> durumundaki satırlar. Kimse onları sorgulamaz, ama silinmedikleri sürece tablo onlarla birlikte sonsuza kadar büyür.</li>\n</ul>\n<p>PostgreSQL bu ayrımı bilmez. MVCC açısından tamamlanmış bir satır <strong>canlı bir tuple’dır</strong>; <code>pg_class.reltuples</code> onu sayar, planner istatistikleri onu sayar, autovacuum eşiği onu sayar. Ölçtüğüm tabloda <code>pending</code> satır sayısı 5.000’di, <code>n_live_tup</code> ise 10.005.726.</p>\n<p>Bu yüzden kuyruk tablosunda iki yanlış ölçekleme kendiliğinden olur: index tabloyla büyür, vacuum eşiği de tabloyla büyür. İkisi de işi yapan kümeye değil, arşive bakar.</p>\n<p>Aşağıdaki sayılar <a href=\"https://github.com/muhammetsafak/pg-queue-bench\">pg-queue-bench</a> üzerinden geliyor: PostgreSQL 17, tek container, <code>shared_buffers</code> 1 GB, 8 client, <code>FOR UPDATE SKIP LOCKED</code> ile claim. Her pgbench çıktısı repoda duruyor.</p>\n<h2 id=\"indexi-tabloya-değil-kuyruğa-kurun\">Index’i tabloya değil kuyruğa kurun</h2>\n<p>Canlı küme her boyutta 5.000 satırda sabit tutuldu; değişen tek şey etrafındaki tamamlanmış satır sayısıydı. Saniye başına claim, üç tekrarın medyanı:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"Tablo 1\"><table><thead><tr><th>Strateji</th><th>100 bin</th><th>1 milyon</th><th>10 milyon</th><th>Index boyutu (10 milyon)</th></tr></thead><tbody><tr><td>index yok</td><td>2.001</td><td>248</td><td>8</td><td>—</td></tr><tr><td><code>(status)</code></td><td>6.500</td><td>6.417</td><td>6.426</td><td>66,1 MB</td></tr><tr><td><code>(status, created_at)</code></td><td>12.415</td><td>11.203</td><td>10.796</td><td>310,4 MB</td></tr><tr><td>partial <code>(created_at) WHERE status = &#39;pending&#39;</code></td><td>13.041</td><td>11.708</td><td>11.538</td><td>7,6 MB</td></tr></tbody></table></div>\n<p>Index’siz satır, neden bu sorunun var olduğunu gösteriyor: 10 milyon satırın içinden 5.000 satırı sequential scan ile bulmak saniyede yalnızca 7,6 kez yapılabiliyor.</p>\n<p>Composite ile partial arasındaki throughput farkı yüzde 6,9. Bu, partial index’i seçmek için bir gerekçe değil. Gerekçe boyut:</p>\n<ul>\n<li>Composite index tabloyla büyüdü: 11,2 → 40,3 → 310,4 MB.</li>\n<li>Partial index 6,5 → 7,6 → 7,6 MB’ta durdu. İndekslediği şey tablo değil, <strong>kuyruk</strong>; kuyruk sabit olduğu için index de sabit.</li>\n</ul>\n<p>On milyon satırda fark <strong>41 kat</strong>. 310 MB’lık bir index’i <code>shared_buffers</code> içinde tutmak ile 7,6 MB’lık bir index’i tutmak aynı karar değil.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">CREATE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> INDEX</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CONCURRENTLY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> jobs_pending_created_at</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    ON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> jobs (created_at)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\"> WHERE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pending&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p><code>ORDER BY created_at</code> sıralamayı index’ten bedava alır; claim sorgusu predicate’i index’le <strong>birebir aynı yazmalıdır</strong>. Canlı tabloda index’i <code>CONCURRENTLY</code> ile kurmak, yazmaları durdurmamanın yoludur; <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/zero-downtime-veritabani-gocleri/\">kesintisiz şema değişikliğinin geri kalanı</a> ayrı bir yazıda.</p>\n<h3 id=\"parametreyle-sorarsanız\">Parametreyle sorarsanız</h3>\n<p>PostgreSQL bir partial index’i ancak sorgunun <code>WHERE</code> koşulunun index predicate’ini kapsadığını <strong>planlama anında</strong> kanıtlayabilirse kullanır. Generic plan <code>$1</code>’in ne olduğunu bilmeden kurulur; <code>status = $1</code> ifadesinin <code>status = &#39;pending&#39;</code> olduğunu kanıtlayamaz ve partial index’i elemek zorunda kalır.</p>\n<p><code>force_custom_plan</code> altında saniyede 11.752 olan claim, <code>force_generic_plan</code> ile zorlandığında 7,2’ye düştü; ortalama gecikme 0,68 ms’den 1.113 ms’ye çıktı — yaklaşık <strong>1.635 kat</strong>. Index’in tarama sayacı sıfırda kaldı. Aynı koşulda composite index etkilenmedi (saniyede 11.417), çünkü onun kanıtlanacak bir predicate’i yok; <code>status</code> index’in içindeki bir kolon.</p>\n<p>Uçurum gerçek ama çitle çevrili. Varsayılan <code>auto</code> modda PostgreSQL ilk beş çalıştırmayı custom plan ile yapar, sonra generic planın tahmini maliyetini custom planların ortalamasıyla karşılaştırır. Tek oturumda 40 çalıştırmanın <strong>40’ında</strong> custom plan kullanıldı; planner generic plana hiç geçmedi. Plan maliyetlerini kaydetmedim; en olası açıklama, partial index’i kullanamayan generic planın tahmini maliyetinin yüksek çıkması. Yine de tek neden bu olmayabilir: predicate’siz <code>(status)</code> index’i de 40 çalıştırmanın 40’ında custom planda kaldı. Composite index ise altıncı çalıştırmada generic plana geçti ve bundan bir şey kaybetmedi.</p>\n<p>Bu korumanın bir bedeli var: partial index’e giden sorgu <strong>her çalıştırmada yeniden planlanır</strong>. Bu ölçekte fark ölçülemeyecek kadar küçük (son beş çalıştırmanın ortalaması: partial 0,20 ms, composite 0,21 ms). Çok tablolu bir join’de ya da uzun bir <code>IN</code> listesinde aynı şey söylenemez.</p>\n<p>Kural şu: <strong>predicate’li bir index’e parametreyle ulaşmayın.</strong> <code>status = &#39;pending&#39;</code> literal olarak yazılsın; <code>plan_cache_mode</code>’a dokunmayın.</p>\n<h2 id=\"küçük-index-daha-az-vacuum-demek-değildir\">Küçük index daha az vacuum demek değildir</h2>\n<p>Otuz saniyelik koşularda iki index neredeyse eşitti. On beş dakikalık sürekli yükte tablo değişti. Bu koşuda claim edilen satır <code>done</code> yapıldı ve yanında saniyede 2.000 iş üreten bir producer çalıştı.</p>\n<p>Partial index <strong>0,125 MB’tan 38,2 MB’a</strong> çıktı — yaklaşık 305 kat. Canlı küme koşunun büyük kısmında 3.000 ile 5.000 satır arasında kaldı; şişen şey index’teki dead entry’lerdi.</p>\n<p>Mekanizma tek cümle: <code>pending → done</code> geçişi satırı partial index’in kapsamından çıkarır, ama index’teki girdi vacuum gelene kadar orada kalır. Partial index’in <strong>küçüklüğü canlı kümeden</strong>, <strong>şişme hızı throughput’tan</strong> gelir; ikisi arasında bir bağ yok. Küçük bir index vacuum’u ucuzlatır, seyrekleştirmez.</p>\n<h2 id=\"autovacuum-eşiği-arşive-bakar\">Autovacuum eşiği arşive bakar</h2>\n<p>On beş dakikada 1.753.949 dead tuple birikti. Autovacuum sayacı: <strong>0</strong>.</p>\n<p>Varsayılan eşik şöyle hesaplanır:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>vacuum eşiği = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples</span></span>\n<span class=\"line\"><span>             = 50 + 0,2 × ~10.000.000</span></span>\n<span class=\"line\"><span>             ≈ 2.000.000 dead tuple</span></span></code></pre>\n<p><code>reltuples</code> tamamlanmış satırları da saydığı için eşik arşivle birlikte büyür. İş gören küme sabit kalır, üretilen dead tuple hızı throughput’a bağlıdır; ama vacuum’un gelmesi için gereken sayı tablonun her yeni <code>done</code> satırıyla biraz daha uzağa kayar. Tablo ne kadar büyükse vacuum o kadar geç gelir.</p>\n<p>Bunun bedelini composite index ödedi. Aynı on beş dakikada 301 MB’tan 427 MB’a çıktı ve hedefin gerisine düştü:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"Tablo 2\"><table><thead><tr><th>Strateji</th><th>Başta</th><th>900. saniyede</th><th>Bekleyen iş</th></tr></thead><tbody><tr><td>partial</td><td>2.024 tps · 1,7 ms</td><td>2.016 tps · 4.906 ms</td><td>5.003 → 14.364</td></tr><tr><td>composite</td><td>2.000 tps · 0,52 ms</td><td>1.204 tps · 61.533 ms</td><td>5.000 → 126.024</td></tr></tbody></table></div>\n<p>Gecikme pgbench’in zamanlama gecikmesini de içeriyor; hedef hızı tutturamayan client borcunu buraya yazar. Partial index de son 45 saniyede geriledi (855. saniyede 1.740 tps, bekleyen iş 4.549’dan 14.364’e), ama koşuyu hedef hızda bitirdi. Composite index 660. saniyeden sonra bir daha toparlanamadı.</p>\n<h2 id=\"eşiği-tablodan-koparın\">Eşiği tablodan koparın</h2>\n<p>Global varsayılan bu erişim deseni için tasarlanmamış. Eşiği tablonun boyutundan koparıp sabit bir sayıya bağlayın:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ALTER</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> jobs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">SET</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    autovacuum_vacuum_scale_factor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    autovacuum_vacuum_threshold    </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 50000</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span></code></pre>\n<p><code>scale_factor = 0</code> eşiği <code>reltuples</code>’tan bağımsız yapar; geriye yalnızca sabit taban kalır. Hangi sayının doğru olduğunu tablo boyutu değil <strong>throughput</strong> belirler: saniyede 2.000 claim işleyen bir kuyruk 50.000 dead tuple’ı yaklaşık 25 saniyede üretir.</p>\n<p>Bu ayarı ölçmedim — ölçümdeki koşu varsayılan ayarlarla yapıldı ve autovacuum hiç çalışmadığı için vacuum’un <strong>maliyeti</strong> de ölçülmedi. 50.000 bir başlangıç noktası; kendi throughput’unuza ve vacuum’un kendi tablonuzdaki süresine göre ayarlayın.</p>\n<h2 id=\"kendi-tablonuzu-ölçün\">Kendi tablonuzu ölçün</h2>\n<p>Önce iki kümenin oranına bakın:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> jobs</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\"> GROUP BY</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Sonra index’lerin boyutuna ve gerçekten kullanılıp kullanılmadığına:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> indexrelname,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">       pg_size_pretty(pg_relation_size(indexrelid)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">AS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> boyut,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">       idx_scan</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pg_stat_user_indexes</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\"> WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> relname </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;jobs&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Son olarak dead tuple sayısını global eşikle yan yana koyun:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">n_dead_tup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">       s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">autovacuum_count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">       s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">last_autovacuum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">       current_setting(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;autovacuum_vacuum_threshold&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)::</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">int</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">         +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_setting(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;autovacuum_vacuum_scale_factor&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)::float8 </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> c</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">reltuples</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">         AS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> global_esik,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">       c</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">reloptions</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pg_stat_user_tables s</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  JOIN</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pg_class c </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">ON</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> c</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">oid</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">relid</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\"> WHERE</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">relname</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;jobs&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p><code>reltuples</code> -1 ise tablo henüz analiz edilmemiştir ve hesap anlamsızdır. <code>reloptions</code> boşsa tablo global eşiği kullanıyor demektir. <code>n_dead_tup</code> saatler boyunca <code>global_esik</code>’in altında kalıp büyüyorsa ve <code>autovacuum_count</code> yerinde sayıyorsa, vacuum’unuz arşive bakıyor.</p>\n<h2 id=\"ne-zaman-bu-kalıbı-bırakırsınız\">Ne zaman bu kalıbı bırakırsınız?</h2>\n<ul>\n<li><strong>Tamamlanan satırları hemen siliyor ya da <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/arsivleme-stratejisi/\">arşiv tablosuna taşıyorsanız</a>.</strong> Tablo zaten canlı kümeye yakın kalır; composite index’in büyümesi de varsayılan eşiğin kayması da küçük kalır.</li>\n<li><strong>ORM ya da sürücünüz <code>status</code>’u parametre olarak bağlıyor ve generic plan zorlanıyorsa.</strong> Partial index’i kullanamazsınız; composite index daha güvenli.</li>\n<li><strong>Tabloyu zamana göre partition’lıyorsanız.</strong> Eski partition’ları düşürmek hem index’i hem vacuum yükünü tablo boyutundan ayırır; bu yazının çözdüğü problemi başka bir yoldan çözer.</li>\n</ul>\n<p>Sayılar tek makineden, PostgreSQL 17’den ve tek bir erişim deseninden geliyor. Otuz saniyelik ve on beş dakikalık koşular gerçek bir kuyruğun aylarını temsil etmez; yön aynı kalır, büyüklükler iş yükünüze aittir.</p>\n<hr/>\n<p>Kuyruk tablosunda tablo boyutu bir gürültüdür. Index’i ve vacuum’u, işi yapan kümeye göre kurun.</p>",
      "content_text": "> **Kısaca:** Bir kuyruk tablosunda iş gören küme bekleyen satırlardır, tablo ise tamamlanmış satırlarla sonsuza kadar büyür. Tablonun boyutuyla ölçeklenen her şey yanlış ölçeklenir: 10 milyon satırda composite index 310 MB'a çıkarken partial index 7,6 MB'ta kaldı, varsayılan autovacuum eşiği ise 15 dakikada biriken 1,75 milyon dead tuple'a hiç ulaşmadı.\n\nOn milyon satırlık bir `jobs` tablosunda kuyrukta bekleyen iş 5.000 taneydi; claim sorgusunun kullandığı index ise 310 MB tutuyordu. Aynı işi 7,6 MB'lık bir partial index görüyordu ve ikisinin arasındaki hız farkı yüzde 6,9'du.\n\nYani mesele hız değildi. Mesele, neyin tablonun boyutuyla birlikte büyüdüğüydü.\n\n## Kuyruk tablosu iki tablodur\n\nBir kuyruk tablosunda iki ayrı küme yaşar:\n\n- **Canlı küme** — `pending` durumundaki satırlar. Worker'ların aradığı, claim'lerin kilitlediği, gerçekten iş gören kısım. Boyutu throughput ile üretim hızı arasındaki dengeye bağlıdır; sağlıklı bir sistemde kabaca sabit durur.\n- **Arşiv** — `done` durumundaki satırlar. Kimse onları sorgulamaz, ama silinmedikleri sürece tablo onlarla birlikte sonsuza kadar büyür.\n\nPostgreSQL bu ayrımı bilmez. MVCC açısından tamamlanmış bir satır **canlı bir tuple'dır**; `pg_class.reltuples` onu sayar, planner istatistikleri onu sayar, autovacuum eşiği onu sayar. Ölçtüğüm tabloda `pending` satır sayısı 5.000'di, `n_live_tup` ise 10.005.726.\n\nBu yüzden kuyruk tablosunda iki yanlış ölçekleme kendiliğinden olur: index tabloyla büyür, vacuum eşiği de tabloyla büyür. İkisi de işi yapan kümeye değil, arşive bakar.\n\nAşağıdaki sayılar [pg-queue-bench](https://github.com/muhammetsafak/pg-queue-bench) üzerinden geliyor: PostgreSQL 17, tek container, `shared_buffers` 1 GB, 8 client, `FOR UPDATE SKIP LOCKED` ile claim. Her pgbench çıktısı repoda duruyor.\n\n## Index'i tabloya değil kuyruğa kurun\n\nCanlı küme her boyutta 5.000 satırda sabit tutuldu; değişen tek şey etrafındaki tamamlanmış satır sayısıydı. Saniye başına claim, üç tekrarın medyanı:\n\n| Strateji | 100 bin | 1 milyon | 10 milyon | Index boyutu (10 milyon) |\n| --- | --- | --- | --- | --- |\n| index yok | 2.001 | 248 | 8 | — |\n| `(status)` | 6.500 | 6.417 | 6.426 | 66,1 MB |\n| `(status, created_at)` | 12.415 | 11.203 | 10.796 | 310,4 MB |\n| partial `(created_at) WHERE status = 'pending'` | 13.041 | 11.708 | 11.538 | 7,6 MB |\n\nIndex'siz satır, neden bu sorunun var olduğunu gösteriyor: 10 milyon satırın içinden 5.000 satırı sequential scan ile bulmak saniyede yalnızca 7,6 kez yapılabiliyor.\n\nComposite ile partial arasındaki throughput farkı yüzde 6,9. Bu, partial index'i seçmek için bir gerekçe değil. Gerekçe boyut:\n\n- Composite index tabloyla büyüdü: 11,2 → 40,3 → 310,4 MB.\n- Partial index 6,5 → 7,6 → 7,6 MB'ta durdu. İndekslediği şey tablo değil, **kuyruk**; kuyruk sabit olduğu için index de sabit.\n\nOn milyon satırda fark **41 kat**. 310 MB'lık bir index'i `shared_buffers` içinde tutmak ile 7,6 MB'lık bir index'i tutmak aynı karar değil.\n\n```sql\nCREATE INDEX CONCURRENTLY jobs_pending_created_at\n    ON jobs (created_at)\n WHERE status = 'pending';\n```\n\n`ORDER BY created_at` sıralamayı index'ten bedava alır; claim sorgusu predicate'i index'le **birebir aynı yazmalıdır**. Canlı tabloda index'i `CONCURRENTLY` ile kurmak, yazmaları durdurmamanın yoludur; [kesintisiz şema değişikliğinin geri kalanı](/tr/muhendislik/notlar/zero-downtime-veritabani-gocleri/) ayrı bir yazıda.\n\n### Parametreyle sorarsanız\n\nPostgreSQL bir partial index'i ancak sorgunun `WHERE` koşulunun index predicate'ini kapsadığını **planlama anında** kanıtlayabilirse kullanır. Generic plan `$1`'in ne olduğunu bilmeden kurulur; `status = $1` ifadesinin `status = 'pending'` olduğunu kanıtlayamaz ve partial index'i elemek zorunda kalır.\n\n`force_custom_plan` altında saniyede 11.752 olan claim, `force_generic_plan` ile zorlandığında 7,2'ye düştü; ortalama gecikme 0,68 ms'den 1.113 ms'ye çıktı — yaklaşık **1.635 kat**. Index'in tarama sayacı sıfırda kaldı. Aynı koşulda composite index etkilenmedi (saniyede 11.417), çünkü onun kanıtlanacak bir predicate'i yok; `status` index'in içindeki bir kolon.\n\nUçurum gerçek ama çitle çevrili. Varsayılan `auto` modda PostgreSQL ilk beş çalıştırmayı custom plan ile yapar, sonra generic planın tahmini maliyetini custom planların ortalamasıyla karşılaştırır. Tek oturumda 40 çalıştırmanın **40'ında** custom plan kullanıldı; planner generic plana hiç geçmedi. Plan maliyetlerini kaydetmedim; en olası açıklama, partial index'i kullanamayan generic planın tahmini maliyetinin yüksek çıkması. Yine de tek neden bu olmayabilir: predicate'siz `(status)` index'i de 40 çalıştırmanın 40'ında custom planda kaldı. Composite index ise altıncı çalıştırmada generic plana geçti ve bundan bir şey kaybetmedi.\n\nBu korumanın bir bedeli var: partial index'e giden sorgu **her çalıştırmada yeniden planlanır**. Bu ölçekte fark ölçülemeyecek kadar küçük (son beş çalıştırmanın ortalaması: partial 0,20 ms, composite 0,21 ms). Çok tablolu bir join'de ya da uzun bir `IN` listesinde aynı şey söylenemez.\n\nKural şu: **predicate'li bir index'e parametreyle ulaşmayın.** `status = 'pending'` literal olarak yazılsın; `plan_cache_mode`'a dokunmayın.\n\n## Küçük index daha az vacuum demek değildir\n\nOtuz saniyelik koşularda iki index neredeyse eşitti. On beş dakikalık sürekli yükte tablo değişti. Bu koşuda claim edilen satır `done` yapıldı ve yanında saniyede 2.000 iş üreten bir producer çalıştı.\n\nPartial index **0,125 MB'tan 38,2 MB'a** çıktı — yaklaşık 305 kat. Canlı küme koşunun büyük kısmında 3.000 ile 5.000 satır arasında kaldı; şişen şey index'teki dead entry'lerdi.\n\nMekanizma tek cümle: `pending → done` geçişi satırı partial index'in kapsamından çıkarır, ama index'teki girdi vacuum gelene kadar orada kalır. Partial index'in **küçüklüğü canlı kümeden**, **şişme hızı throughput'tan** gelir; ikisi arasında bir bağ yok. Küçük bir index vacuum'u ucuzlatır, seyrekleştirmez.\n\n## Autovacuum eşiği arşive bakar\n\nOn beş dakikada 1.753.949 dead tuple birikti. Autovacuum sayacı: **0**.\n\nVarsayılan eşik şöyle hesaplanır:\n\n```\nvacuum eşiği = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor × reltuples\n             = 50 + 0,2 × ~10.000.000\n             ≈ 2.000.000 dead tuple\n```\n\n`reltuples` tamamlanmış satırları da saydığı için eşik arşivle birlikte büyür. İş gören küme sabit kalır, üretilen dead tuple hızı throughput'a bağlıdır; ama vacuum'un gelmesi için gereken sayı tablonun her yeni `done` satırıyla biraz daha uzağa kayar. Tablo ne kadar büyükse vacuum o kadar geç gelir.\n\nBunun bedelini composite index ödedi. Aynı on beş dakikada 301 MB'tan 427 MB'a çıktı ve hedefin gerisine düştü:\n\n| Strateji | Başta | 900. saniyede | Bekleyen iş |\n| --- | --- | --- | --- |\n| partial | 2.024 tps · 1,7 ms | 2.016 tps · 4.906 ms | 5.003 → 14.364 |\n| composite | 2.000 tps · 0,52 ms | 1.204 tps · 61.533 ms | 5.000 → 126.024 |\n\nGecikme pgbench'in zamanlama gecikmesini de içeriyor; hedef hızı tutturamayan client borcunu buraya yazar. Partial index de son 45 saniyede geriledi (855. saniyede 1.740 tps, bekleyen iş 4.549'dan 14.364'e), ama koşuyu hedef hızda bitirdi. Composite index 660. saniyeden sonra bir daha toparlanamadı.\n\n## Eşiği tablodan koparın\n\nGlobal varsayılan bu erişim deseni için tasarlanmamış. Eşiği tablonun boyutundan koparıp sabit bir sayıya bağlayın:\n\n```sql\nALTER TABLE jobs SET (\n    autovacuum_vacuum_scale_factor = 0,\n    autovacuum_vacuum_threshold    = 50000\n);\n```\n\n`scale_factor = 0` eşiği `reltuples`'tan bağımsız yapar; geriye yalnızca sabit taban kalır. Hangi sayının doğru olduğunu tablo boyutu değil **throughput** belirler: saniyede 2.000 claim işleyen bir kuyruk 50.000 dead tuple'ı yaklaşık 25 saniyede üretir.\n\nBu ayarı ölçmedim — ölçümdeki koşu varsayılan ayarlarla yapıldı ve autovacuum hiç çalışmadığı için vacuum'un **maliyeti** de ölçülmedi. 50.000 bir başlangıç noktası; kendi throughput'unuza ve vacuum'un kendi tablonuzdaki süresine göre ayarlayın.\n\n## Kendi tablonuzu ölçün\n\nÖnce iki kümenin oranına bakın:\n\n```sql\nSELECT status, count(*)\n  FROM jobs\n GROUP BY status;\n```\n\nSonra index'lerin boyutuna ve gerçekten kullanılıp kullanılmadığına:\n\n```sql\nSELECT indexrelname,\n       pg_size_pretty(pg_relation_size(indexrelid)) AS boyut,\n       idx_scan\n  FROM pg_stat_user_indexes\n WHERE relname = 'jobs';\n```\n\nSon olarak dead tuple sayısını global eşikle yan yana koyun:\n\n```sql\nSELECT s.n_dead_tup,\n       s.autovacuum_count,\n       s.last_autovacuum,\n       current_setting('autovacuum_vacuum_threshold')::int\n         + current_setting('autovacuum_vacuum_scale_factor')::float8 * c.reltuples\n         AS global_esik,\n       c.reloptions\n  FROM pg_stat_user_tables s\n  JOIN pg_class c ON c.oid = s.relid\n WHERE s.relname = 'jobs';\n```\n\n`reltuples` -1 ise tablo henüz analiz edilmemiştir ve hesap anlamsızdır. `reloptions` boşsa tablo global eşiği kullanıyor demektir. `n_dead_tup` saatler boyunca `global_esik`'in altında kalıp büyüyorsa ve `autovacuum_count` yerinde sayıyorsa, vacuum'unuz arşive bakıyor.\n\n## Ne zaman bu kalıbı bırakırsınız?\n\n- **Tamamlanan satırları hemen siliyor ya da [arşiv tablosuna taşıyorsanız](/tr/muhendislik/notlar/arsivleme-stratejisi/).** Tablo zaten canlı kümeye yakın kalır; composite index'in büyümesi de varsayılan eşiğin kayması da küçük kalır.\n- **ORM ya da sürücünüz `status`'u parametre olarak bağlıyor ve generic plan zorlanıyorsa.** Partial index'i kullanamazsınız; composite index daha güvenli.\n- **Tabloyu zamana göre partition'lıyorsanız.** Eski partition'ları düşürmek hem index'i hem vacuum yükünü tablo boyutundan ayırır; bu yazının çözdüğü problemi başka bir yoldan çözer.\n\nSayılar tek makineden, PostgreSQL 17'den ve tek bir erişim deseninden geliyor. Otuz saniyelik ve on beş dakikalık koşular gerçek bir kuyruğun aylarını temsil etmez; yön aynı kalır, büyüklükler iş yükünüze aittir.\n\n---\n\nKuyruk tablosunda tablo boyutu bir gürültüdür. Index'i ve vacuum'u, işi yapan kümeye göre kurun.\n\n## Kaynaklar\n\n- [Routine Vacuuming: the autovacuum daemon and its thresholds](https://www.postgresql.org/docs/17/routine-vacuuming.html) — PostgreSQL\n- [Partial Indexes](https://www.postgresql.org/docs/17/indexes-partial.html) — PostgreSQL\n- [PREPARE: generic and custom plans](https://www.postgresql.org/docs/17/sql-prepare.html) — PostgreSQL\n- [pg-queue-bench: raw pgbench runs behind every number in this note](https://github.com/muhammetsafak/pg-queue-bench) — GitHub\n\n## Sık sorulanlar\n\n### Kuyruk tablosunda partial index daha mı hızlı?\n\nBelirgin biçimde değil. 10 milyon tamamlanmış satırlı tabloda partial index saniyede 11.538 claim, (status, created_at) composite index 10.796 claim taşıdı; fark yüzde 6,9. Asıl fark boyutta: partial index 7,6 MB'ta sabit kaldı, composite index 310 MB'a çıktı.\n\n### Autovacuum kuyruk tablosuna neden geç gelir?\n\nVarsayılan eşik 50 + 0,2 × reltuples'tır ve reltuples tamamlanmış satırları da sayar. 10 milyon satırlık tabloda eşik yaklaşık 2 milyon dead tuple'dır; ölçümde 15 dakikada 1,75 milyon dead tuple birikti ve autovacuum bir kez bile çalışmadı. Tablo büyüdükçe eşik de büyür.",
      "date_published": "2026-09-16T00:00:00.000Z",
      "date_modified": "2026-10-07T00:00:00.000Z",
      "tags": [
        "postgresql",
        "queue",
        "performance",
        "indexing",
        "production"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/orm-mu-native-sql-mi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/orm-mu-native-sql-mi/",
      "title": "ORM mi, Native SQL mi?",
      "summary": "ORM, CRUD için doğru varsayılan: sorguları parametreli yazar ve tekrarı azaltır. Tehlikesi SQL'i gözden gizlemesinde — N+1, gereksiz kolonlar, unutulmuş bir where. Çok tablolu raporlar, agregasyonlar ve toplu işlemlerde düz SQL daha hızlı ve okunur; güvenliği araç değil parametre bağlama sağlar.",
      "content_html": "<p>Bir yönetim panelinin tek bir sayfası 1.400 sorgu atıyordu. Sebep ekranda görünmüyordu: bir döngü içinde masum görünen <code>$order-&gt;customer-&gt;name</code> çağrısı, her sipariş için ayrı bir sorgu açıyordu. ORM aslında hiçbir şeyi gizlememişti — sadece geliştiricinin hangi sorgunun çalıştığını düşünmeyi bırakmasına izin vermişti.</p>\n<p>ORM bizi tembelleştiriyor mu, yoksa koruyor mu? Cevap: ikisi de — ve hangisinin baskın olduğunu ekip belirler.</p>\n<h2 id=\"orm-ne-satın-alır\">ORM ne satın alır</h2>\n<p>ORM’in gerçek getirileri var ve bunlar küçümsenmemeli:</p>\n<ul>\n<li><strong>Güvenlik.</strong> Sorgular varsayılan olarak parametrelidir; sıradan kullanımda SQL injection yüzeyi kapalıdır.</li>\n<li><strong>Okunabilirlik.</strong> CRUD işlemleri için <code>User::create($data)</code>, elle yazılmış <code>INSERT</code>’ten kısa ve nettir.</li>\n<li><strong>Daha az tekrar.</strong> İlişkiler, migration’lar, model event’leri — hepsi tek yerden.</li>\n</ul>\n<p>Projelerin büyük çoğunluğu özünde CRUD’dur ve bu işler için ORM doğru varsayılandır. Buna itirazım yok.</p>\n<h2 id=\"ormin-tembelleştirdiği-yer\">ORM’in tembelleştirdiği yer</h2>\n<p>Tehlike ORM’in ne yaptığında değil, ne <strong>gizlediğinde</strong>. ORM, SQL’i gözden kaldırır; geliştirici SQL’i görmeyince hangi sorgunun çalıştığını düşünmeyi bırakır.</p>\n<p>Sonuç tanıdık: yazının başındaki N+1; tek bir kolon yetecekken tüm satırı belleğe çekmek; bir <code>where</code> unutup farkında olmadan tüm tabloyu yüklemek. Bunların hiçbirini ORM “yapmaz”; hepsini, ORM’in ne ürettiğine bakmayan geliştirici yapar.</p>\n<p><a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">Veri yoğunluklu sistemler</a> yazısındaki yanlış kırılma noktaları tablosunun ilk satırı tam da budur: “veritabanı yavaş” sanılan şey, çoğu zaman uygulamanın 200 sorgu atmasıdır.</p>\n<h2 id=\"orm-nerede-biter\">ORM nerede biter</h2>\n<p>ORM, transactional CRUD’da parlar: bir kaydı oluştur, güncelle, ilişkileriyle oku. Zorlandığı yer de bellidir:</p>\n<ul>\n<li>Çok tablolu, çok adımlı raporlar.</li>\n<li>Agregasyonlar, window function’lar, <code>GROUP BY</code> üstüne karmaşık koşullar.</li>\n<li>Toplu işlemler — milyonlarca satırı tek tek model olarak gezmek yerine tek bir <code>UPDATE</code>.</li>\n</ul>\n<p>Bu işlerde ORM ile ısrar etmek hem daha yavaş hem daha az okunur kod üretir. Düz SQL burada hem daha hızlı hem daha nettir. Doğru indeksleri seçmek de bu işin parçası — <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/indeks-yonetimi/\">indeks yönetimi</a> notu o tarafı anlatıyor.</p>\n<h2 id=\"karar-orm-varsayılan-sql-araç\">Karar: ORM varsayılan, SQL araç</h2>\n<p>İkisi rakip değil. Doğru kullanım kabaca şu:</p>\n<ul>\n<li>İşlerin %90’ı — CRUD, basit listeler — ORM ile. Hızlı, güvenli, okunur.</li>\n<li>Kalan %10 — ağır raporlama, agregasyon, toplu işlem — query builder veya düz SQL ile.</li>\n</ul>\n<p>Kıdem göstergesi, ORM’i bilmek değil; <strong>nerede bittiğini</strong> bilmektir. ORM’i her şeye zorlayan ekip onu bir performans tuzağına çevirir; ORM’i hiç kullanmayıp her şeyi elle yazan ekip ise güvenlik ve okunabilirlikten boş yere ödün verir.</p>\n<h2 id=\"i̇kisi-de-güvenli-yazılabilir\">İkisi de güvenli yazılabilir</h2>\n<p>Yaygın bir yanlış: “düz SQL = güvensiz”. Güvenliği sağlayan ORM değil, <strong>parametre bağlamadır</strong>. Bağlı parametrelerle yazılmış düz bir sorgu, ORM kadar injection’a kapalıdır:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">select</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> id, total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">FROM</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> orders </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ?&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;pending&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]);</span></span></code></pre>\n<p>Tehlikeli olan düz SQL değil; string birleştirmeyle kullanıcı girdisini sorguya gömmektir — ve bunu ORM’in <code>whereRaw</code>’ında da yapabilirsiniz. Araç değil, alışkanlık korur.</p>\n<hr/>\n<p>ORM bir tembellik makinesi de olabilir, bir güvenlik ağı da — farkı, ekibin ona nasıl baktığı yaratır. Onu kullanın, ama ne ürettiğini görmeyi hiç bırakmayın.</p>\n<p>İyi geliştirici ORM ile yazar; ne zaman ORM’i bırakacağını da bilir.</p>",
      "content_text": "> **Kısaca:** ORM, CRUD için doğru varsayılan: sorguları parametreli yazar ve tekrarı azaltır. Tehlikesi SQL'i gözden gizlemesinde — N+1, gereksiz kolonlar, unutulmuş bir where. Çok tablolu raporlar, agregasyonlar ve toplu işlemlerde düz SQL daha hızlı ve okunur; güvenliği araç değil parametre bağlama sağlar.\n\nBir yönetim panelinin tek bir sayfası 1.400 sorgu atıyordu. Sebep ekranda görünmüyordu: bir döngü içinde masum görünen `$order->customer->name` çağrısı, her sipariş için ayrı bir sorgu açıyordu. ORM aslında hiçbir şeyi gizlememişti — sadece geliştiricinin hangi sorgunun çalıştığını düşünmeyi bırakmasına izin vermişti.\n\nORM bizi tembelleştiriyor mu, yoksa koruyor mu? Cevap: ikisi de — ve hangisinin baskın olduğunu ekip belirler.\n\n## ORM ne satın alır\n\nORM'in gerçek getirileri var ve bunlar küçümsenmemeli:\n\n- **Güvenlik.** Sorgular varsayılan olarak parametrelidir; sıradan kullanımda SQL injection yüzeyi kapalıdır.\n- **Okunabilirlik.** CRUD işlemleri için `User::create($data)`, elle yazılmış `INSERT`'ten kısa ve nettir.\n- **Daha az tekrar.** İlişkiler, migration'lar, model event'leri — hepsi tek yerden.\n\nProjelerin büyük çoğunluğu özünde CRUD'dur ve bu işler için ORM doğru varsayılandır. Buna itirazım yok.\n\n## ORM'in tembelleştirdiği yer\n\nTehlike ORM'in ne yaptığında değil, ne **gizlediğinde**. ORM, SQL'i gözden kaldırır; geliştirici SQL'i görmeyince hangi sorgunun çalıştığını düşünmeyi bırakır.\n\nSonuç tanıdık: yazının başındaki N+1; tek bir kolon yetecekken tüm satırı belleğe çekmek; bir `where` unutup farkında olmadan tüm tabloyu yüklemek. Bunların hiçbirini ORM \"yapmaz\"; hepsini, ORM'in ne ürettiğine bakmayan geliştirici yapar.\n\n[Veri yoğunluklu sistemler](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) yazısındaki yanlış kırılma noktaları tablosunun ilk satırı tam da budur: \"veritabanı yavaş\" sanılan şey, çoğu zaman uygulamanın 200 sorgu atmasıdır.\n\n## ORM nerede biter\n\nORM, transactional CRUD'da parlar: bir kaydı oluştur, güncelle, ilişkileriyle oku. Zorlandığı yer de bellidir:\n\n- Çok tablolu, çok adımlı raporlar.\n- Agregasyonlar, window function'lar, `GROUP BY` üstüne karmaşık koşullar.\n- Toplu işlemler — milyonlarca satırı tek tek model olarak gezmek yerine tek bir `UPDATE`.\n\nBu işlerde ORM ile ısrar etmek hem daha yavaş hem daha az okunur kod üretir. Düz SQL burada hem daha hızlı hem daha nettir. Doğru indeksleri seçmek de bu işin parçası — [indeks yönetimi](/tr/muhendislik/notlar/indeks-yonetimi/) notu o tarafı anlatıyor.\n\n## Karar: ORM varsayılan, SQL araç\n\nİkisi rakip değil. Doğru kullanım kabaca şu:\n\n- İşlerin %90'ı — CRUD, basit listeler — ORM ile. Hızlı, güvenli, okunur.\n- Kalan %10 — ağır raporlama, agregasyon, toplu işlem — query builder veya düz SQL ile.\n\nKıdem göstergesi, ORM'i bilmek değil; **nerede bittiğini** bilmektir. ORM'i her şeye zorlayan ekip onu bir performans tuzağına çevirir; ORM'i hiç kullanmayıp her şeyi elle yazan ekip ise güvenlik ve okunabilirlikten boş yere ödün verir.\n\n## İkisi de güvenli yazılabilir\n\nYaygın bir yanlış: \"düz SQL = güvensiz\". Güvenliği sağlayan ORM değil, **parametre bağlamadır**. Bağlı parametrelerle yazılmış düz bir sorgu, ORM kadar injection'a kapalıdır:\n\n```php\nDB::select('SELECT id, total FROM orders WHERE status = ?', ['pending']);\n```\n\nTehlikeli olan düz SQL değil; string birleştirmeyle kullanıcı girdisini sorguya gömmektir — ve bunu ORM'in `whereRaw`'ında da yapabilirsiniz. Araç değil, alışkanlık korur.\n\n---\n\nORM bir tembellik makinesi de olabilir, bir güvenlik ağı da — farkı, ekibin ona nasıl baktığı yaratır. Onu kullanın, ama ne ürettiğini görmeyi hiç bırakmayın.\n\nİyi geliştirici ORM ile yazar; ne zaman ORM'i bırakacağını da bilir.\n\n## Kaynaklar\n\n- [Database: Query Builder (Laravel 12.x)](https://laravel.com/docs/12.x/queries) — Laravel",
      "date_published": "2026-09-12T00:00:00.000Z",
      "tags": [
        "database",
        "orm",
        "sql",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/indeks-yonetimi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/indeks-yonetimi/",
      "title": "İndeksler: Eksik Yavaşlatır, Fazlası Yazmayı Öldürür",
      "summary": "İndeks bedava okuma hızı değil, yazmayla ödenmiş okuma hızıdır. Her INSERT tablonun her indeksine bir girdi yazar; her UPDATE de indeksli bir kolona dokunduğu anda aynısını yapar. HOT güncellemesi ve kısmi indeks istisnadır, varsayılan değil. Bu yüzden indeksi gerçek bir sorgu planı istediği için ekleyin, pg_stat_user_indexes idx_scan = 0 gösterdiği için silin.",
      "content_html": "<p>İki karşıt ekip gördüm. Biri tabloya hiç indeks koymamıştı — her sorgu bir sequential scan, her liste sayfası saniyeler sürüyordu. Diğeri tam tersini yapmıştı: “ne olur ne olmaz” diye her kolona bir indeks açmıştı ve şimdi her <code>INSERT</code> ağır aksak ilerliyordu.</p>\n<p>İkisi de aynı yanılgının iki ucu: indeksi ücretsiz bir hız kaynağı sanmak.</p>\n<h2 id=\"i̇ndeks-ücretsiz-hız-değildir\">İndeks ücretsiz hız değildir</h2>\n<p>Bir indeks okumayı hızlandırır, çünkü veritabanı tüm tabloyu taramak yerine sıralı bir yapıya bakar. Ama o sıralı yapı kendi kendine güncel kalmaz: tablodaki bir <code>INSERT</code>, o tablonun <strong>her indeksine</strong> bir girdi yazar; bir <code>UPDATE</code> de indeksli bir kolona dokunduğu anda aynısını yapar. İstisnalar var — indeksli hiçbir kolonu değiştirmeyen HOT güncellemesi indekslere dokunmaz, kısmi indeks yüklemi dışında kalan satırlar için güncellenmez, <code>DELETE</code> ise indeks girdilerini temizlemeyi <code>VACUUM</code>’a bırakır — ama bunlar istisna, varsayılan değil.</p>\n<p>Yani her indeks bir takastır: okuma hızını, yazma maliyetiyle satın alırsınız. Beş indeksli bir tabloya tek satır eklemek, altı yapıyı birden güncellemektir. İndeks “bedava okuma hızı” değil, “yazmayla ödenmiş okuma hızı”dır.</p>\n<h2 id=\"eksik-indeks-tahmin-değil-ölçüm\">Eksik indeks: tahmin değil, ölçüm</h2>\n<p>Eksik indeksi sorgu planından bulun, tahminden değil:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">EXPLAIN ANALYZE</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pending&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Çıktıda büyük bir tabloda <code>Seq Scan</code> görüyorsanız ve sorgu satırların küçük bir kısmını döndürüyorsa, muhtemelen bir indeks eksiktir. <code>pg_stat_user_tables</code> tablosunda <code>seq_scan</code> sayısı yüksek olan büyük tabloları tarayın — bunlar adaylarınızdır.</p>\n<p>İndeksi gerçek, yavaş bir sorgu için ekleyin. “İleride lazım olabilir” diye eklenen indeks, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> veritabanı katmanındaki hâlidir.</p>\n<h2 id=\"fazla-indeks-kullanılmayanı-bul\">Fazla indeks: kullanılmayanı bul</h2>\n<p>Ters yönde, hiç kullanılmayan indeksler her yazmayı boş yere pahalılaştırır. Onları da ölçümle bulun:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> indexrelname, idx_scan</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pg_stat_user_indexes</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> idx_scan </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p><code>idx_scan = 0</code> olan bir indeks hiç okuma hızlandırmıyor ama her yazmaya maliyet ekliyor demektir. (Primary key ve unique kısıtların indekslerini elbette koruyun.) Bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerin</a> yazma kırılma noktasında sayılan ilk temizliktir.</p>\n<h2 id=\"doğru-indeksi-nasıl-seçersiniz\">Doğru indeksi nasıl seçersiniz?</h2>\n<p>İndeks “var/yok”tan ibaret değildir; doğru türü ve şekli seçmek önemlidir:</p>\n<ul>\n<li><strong>Bileşik indekste kolon sırası.</strong> <code>(a, b)</code> indeksi, <code>a</code>’da eşitlik + <code>b</code>’de aralık sorgusuna hizmet eder; <code>b</code>’de tek başına yapılan bir arama da bu indeksi kullanabilir ama taranan indeks bölümünü daraltmaz — PostgreSQL 18 öncesinde indeksin tamamı taranır, 18’den itibaren skip scan optimizasyonu bunu daraltır. Eşitlikle filtrelenen kolonu başa, aralıkla filtreleneni sona koyun.</li>\n<li><strong>Kısmi indeks (partial).</strong> Sorgu hep aynı alt kümeye bakıyorsa indeksi de o alt kümeyle sınırlayın: <code>CREATE INDEX ... WHERE status = &#39;active&#39;</code>. Daha küçük indeks, daha ucuz bakım.</li>\n<li><strong>Covering indeks.</strong> <code>INCLUDE</code> ile sık okunan kolonları indekse ekleyip veritabanının tabloya hiç gitmemesini sağlayabilirsiniz.</li>\n<li><strong>B-tree dışı türler.</strong> Eşitlik/sıralama için B-tree; <code>jsonb</code> ve tam metin için GIN; saf sıralı, append ağırlıklı veri için küçük ve ucuz BRIN.</li>\n</ul>\n<h2 id=\"duplikat-ve-örtüşen-indeksler\">Duplikat ve örtüşen indeksler</h2>\n<p><code>(a)</code> üzerindeki bir indeks, zaten <code>(a, b)</code> indeksiniz varsa gereksizdir — bileşik indeks <code>a</code> ile başlayan sorgulara da hizmet eder. Bu tür örtüşmeleri periyodik olarak temizleyin; her biri sessiz bir yazma vergisidir.</p>\n<h2 id=\"denge-sorgu-planıyla-kurulur\">Denge sorgu planıyla kurulur</h2>\n<p>Tek kural: indeksi ne tahminle ekleyin ne tahminle silin. Bir indeks, gerçek bir sorgu planı onu istediği için eklenir; bir indeks, istatistikler onu kimsenin kullanmadığını gösterdiği için silinir. Eksik ile fazla arasındaki denge, hislerle değil, <code>EXPLAIN ANALYZE</code> ve <code>pg_stat_user_indexes</code> ile kurulur.</p>\n<hr/>\n<p>İndeks yönetimi, “daha çok indeks daha iyi” sanısından kurtulmakla başlar. Her indeks bir okuma kazancı ve bir yazma maliyetidir; iyi yönetim, bu ikisini ölçerek dengede tutmaktır.</p>\n<p>Eksik indeks sorguyu yavaşlatır; fazlası ise tüm tabloyu. İkisinden de ölçüm korur.</p>",
      "content_text": "> **Kısaca:** İndeks bedava okuma hızı değil, yazmayla ödenmiş okuma hızıdır. Her INSERT tablonun her indeksine bir girdi yazar; her UPDATE de indeksli bir kolona dokunduğu anda aynısını yapar. HOT güncellemesi ve kısmi indeks istisnadır, varsayılan değil. Bu yüzden indeksi gerçek bir sorgu planı istediği için ekleyin, pg_stat_user_indexes idx_scan = 0 gösterdiği için silin.\n\nİki karşıt ekip gördüm. Biri tabloya hiç indeks koymamıştı — her sorgu bir sequential scan, her liste sayfası saniyeler sürüyordu. Diğeri tam tersini yapmıştı: \"ne olur ne olmaz\" diye her kolona bir indeks açmıştı ve şimdi her `INSERT` ağır aksak ilerliyordu.\n\nİkisi de aynı yanılgının iki ucu: indeksi ücretsiz bir hız kaynağı sanmak.\n\n## İndeks ücretsiz hız değildir\n\nBir indeks okumayı hızlandırır, çünkü veritabanı tüm tabloyu taramak yerine sıralı bir yapıya bakar. Ama o sıralı yapı kendi kendine güncel kalmaz: tablodaki bir `INSERT`, o tablonun **her indeksine** bir girdi yazar; bir `UPDATE` de indeksli bir kolona dokunduğu anda aynısını yapar. İstisnalar var — indeksli hiçbir kolonu değiştirmeyen HOT güncellemesi indekslere dokunmaz, kısmi indeks yüklemi dışında kalan satırlar için güncellenmez, `DELETE` ise indeks girdilerini temizlemeyi `VACUUM`'a bırakır — ama bunlar istisna, varsayılan değil.\n\nYani her indeks bir takastır: okuma hızını, yazma maliyetiyle satın alırsınız. Beş indeksli bir tabloya tek satır eklemek, altı yapıyı birden güncellemektir. İndeks \"bedava okuma hızı\" değil, \"yazmayla ödenmiş okuma hızı\"dır.\n\n## Eksik indeks: tahmin değil, ölçüm\n\nEksik indeksi sorgu planından bulun, tahminden değil:\n\n```sql\nEXPLAIN ANALYZE\nSELECT * FROM orders WHERE status = 'pending';\n```\n\nÇıktıda büyük bir tabloda `Seq Scan` görüyorsanız ve sorgu satırların küçük bir kısmını döndürüyorsa, muhtemelen bir indeks eksiktir. `pg_stat_user_tables` tablosunda `seq_scan` sayısı yüksek olan büyük tabloları tarayın — bunlar adaylarınızdır.\n\nİndeksi gerçek, yavaş bir sorgu için ekleyin. \"İleride lazım olabilir\" diye eklenen indeks, [spekülatif genelliğin](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) veritabanı katmanındaki hâlidir.\n\n## Fazla indeks: kullanılmayanı bul\n\nTers yönde, hiç kullanılmayan indeksler her yazmayı boş yere pahalılaştırır. Onları da ölçümle bulun:\n\n```sql\nSELECT indexrelname, idx_scan\nFROM pg_stat_user_indexes\nWHERE idx_scan = 0;\n```\n\n`idx_scan = 0` olan bir indeks hiç okuma hızlandırmıyor ama her yazmaya maliyet ekliyor demektir. (Primary key ve unique kısıtların indekslerini elbette koruyun.) Bu, [veri yoğunluklu sistemlerin](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) yazma kırılma noktasında sayılan ilk temizliktir.\n\n## Doğru indeksi nasıl seçersiniz?\n\nİndeks \"var/yok\"tan ibaret değildir; doğru türü ve şekli seçmek önemlidir:\n\n- **Bileşik indekste kolon sırası.** `(a, b)` indeksi, `a`'da eşitlik + `b`'de aralık sorgusuna hizmet eder; `b`'de tek başına yapılan bir arama da bu indeksi kullanabilir ama taranan indeks bölümünü daraltmaz — PostgreSQL 18 öncesinde indeksin tamamı taranır, 18'den itibaren skip scan optimizasyonu bunu daraltır. Eşitlikle filtrelenen kolonu başa, aralıkla filtreleneni sona koyun.\n- **Kısmi indeks (partial).** Sorgu hep aynı alt kümeye bakıyorsa indeksi de o alt kümeyle sınırlayın: `CREATE INDEX ... WHERE status = 'active'`. Daha küçük indeks, daha ucuz bakım.\n- **Covering indeks.** `INCLUDE` ile sık okunan kolonları indekse ekleyip veritabanının tabloya hiç gitmemesini sağlayabilirsiniz.\n- **B-tree dışı türler.** Eşitlik/sıralama için B-tree; `jsonb` ve tam metin için GIN; saf sıralı, append ağırlıklı veri için küçük ve ucuz BRIN.\n\n## Duplikat ve örtüşen indeksler\n\n`(a)` üzerindeki bir indeks, zaten `(a, b)` indeksiniz varsa gereksizdir — bileşik indeks `a` ile başlayan sorgulara da hizmet eder. Bu tür örtüşmeleri periyodik olarak temizleyin; her biri sessiz bir yazma vergisidir.\n\n## Denge sorgu planıyla kurulur\n\nTek kural: indeksi ne tahminle ekleyin ne tahminle silin. Bir indeks, gerçek bir sorgu planı onu istediği için eklenir; bir indeks, istatistikler onu kimsenin kullanmadığını gösterdiği için silinir. Eksik ile fazla arasındaki denge, hislerle değil, `EXPLAIN ANALYZE` ve `pg_stat_user_indexes` ile kurulur.\n\n---\n\nİndeks yönetimi, \"daha çok indeks daha iyi\" sanısından kurtulmakla başlar. Her indeks bir okuma kazancı ve bir yazma maliyetidir; iyi yönetim, bu ikisini ölçerek dengede tutmaktır.\n\nEksik indeks sorguyu yavaşlatır; fazlası ise tüm tabloyu. İkisinden de ölçüm korur.\n\n## Kaynaklar\n\n- [CREATE INDEX: CONCURRENTLY, INCLUDE, partial indexes and index methods](https://www.postgresql.org/docs/current/sql-createindex.html) — PostgreSQL\n- [Multicolumn Indexes: column order and the skip scan optimization](https://www.postgresql.org/docs/current/indexes-multicolumn.html) — PostgreSQL\n- [PostgreSQL 18 Release Notes: allow skip scans of btree indexes](https://www.postgresql.org/docs/release/18.0/) — PostgreSQL\n- [Heap-Only Tuples (HOT)](https://www.postgresql.org/docs/current/storage-hot.html) — PostgreSQL\n\n## Sık sorulanlar\n\n### Her INSERT tablodaki her indeksi güncellemek zorunda mıdır?\n\nINSERT için evet: yeni satır her indekste bir girdi alır. UPDATE farklıdır. İndeksli hiçbir kolonu değiştirmeyen HOT güncellemesi indekslere dokunmaz, kısmi indeks ise yüklemi dışında kalan satırlar için güncellenmez. DELETE ise indeks girdilerini temizlemeyi VACUUM'a bırakır.\n\n### Kullanılmayan indeksler nasıl bulunur?\n\npg_stat_user_indexes tablosunda idx_scan = 0 olanları sorgulayın. Bu indeksler hiçbir okumayı hızlandırmıyor ama her yazmaya maliyet ekliyordur. Primary key ve unique kısıtları taşıyan indeksleri her hâlükârda koruyun.",
      "date_published": "2026-09-05T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "performance",
        "indexing",
        "PostgreSQL Üretimde"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/zero-downtime-veritabani-gocleri/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/zero-downtime-veritabani-gocleri/",
      "title": "Canlıda Sıfır Kesintiyle Şema Değiştirmek",
      "summary": "Tehlikeli olan şema değişikliği değil, onu tek adımda yapmak. Okumaları da kuyruğa sokan yalnızca ACCESS EXCLUSIVE lock alan işlemlerdir ve 50 milyon satırlık bir tabloda o kuyruğun adı kesintidir. Değişikliği expand-contract adımlarına bölün: kısıtı NOT VALID ile ekleyip VALIDATE'i ayrı adımda çalıştırın, indeksleri CONCURRENTLY kurun ve migration oturumuna kısa bir lock_timeout verin.",
      "content_html": "<p>Bir deploy, 50 milyon satırlık bir tabloda tek bir <code>ALTER TABLE</code> çalıştırdı ve tabloyu dört dakika kilitledi. Dört dakika boyunca o tabloya değen her istek bekledi; site fiilen çöktü. Migration’ın kendisi doğruydu — sorun, onun tek adımda yapılmasıydı.</p>\n<p>Tehlikeli olan şema değişikliği değildir. Tehlikeli olan, şema değişikliğini tek seferde, geri uyumluluğu düşünmeden yapmaktır.</p>\n<h2 id=\"locku-kim-alır\">Lock’u kim alır?</h2>\n<p>Her şema değişikliği aynı maliyette değildir. Modern PostgreSQL’de sabit <code>DEFAULT</code> ile kolon eklemek bir metadata işlemidir — hızlıdır. Asıl tehlike, tabloyu uzun süre kilitleyen işlemlerdedir:</p>\n<ul>\n<li><code>CREATE INDEX</code> — <code>CONCURRENTLY</code> olmadan tabloyu yazmaya kapatır.</li>\n<li>Tabloyu yeniden yazan <code>ALTER COLUMN ... TYPE</code> değişiklikleri.</li>\n<li>Tüm tabloyu tarayan <code>NOT NULL</code>, <code>CHECK</code> veya foreign key eklemeleri.</li>\n</ul>\n<p>Bu işlemler güçlü bir lock alır; <code>ACCESS EXCLUSIVE</code> alanlar — tablo yeniden yazımı ya da <code>NOT NULL</code>/<code>CHECK</code> eklemesi — tabloya değen her sorguyu, okuma dahil, kuyruğa sokar. Geri kalanı yalnızca yazmaları bloklar. Tablo büyükse kuyruk uzar.</p>\n<h2 id=\"tehlikeli-olan-tek-adımdır\">Tehlikeli olan tek adımdır</h2>\n<p>Çözüm migration yapmamak değil; her tehlikeli migration’ı, her biri tek başına güvenli ve geri uyumlu olan küçük adımlara bölmektir. Bunun adı <strong>expand-contract</strong> (genişlet–daralt) kalıbıdır.</p>\n<p>Bir kolonu yeniden adlandırmak istediğinizi düşünün. Tek adımda <code>RENAME COLUMN</code>, eski kolonu okuyan çalışan kodu anında kırar. Bunun yerine <span data-scheduled=\"/tr/muhendislik/sistemler/sifir-kesintili-dagitim/\">üç deploy</span>:</p>\n<ol>\n<li><strong>Genişlet.</strong> Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, hâlâ eskiden okusun.</li>\n<li><strong>Geçiş.</strong> Eski veriyi yeni kolona batch’ler hâlinde taşı. Kod artık yeniden okusun.</li>\n<li><strong>Daralt.</strong> Eski kolonu sil.</li>\n</ol>\n<p>Her adım, hem bir önceki sürümdeki kod hem de yeni kod ile çalışır. Hiçbir an, çalışan kod ile şema uyumsuz değildir.</p>\n<h2 id=\"güvenli-reçeteler\">Güvenli reçeteler</h2>\n<p>Sık yapılan değişikliklerin kesintisiz hâlleri:</p>\n<p><strong>Yeni <code>NOT NULL</code> kolon.</strong> Tek adımda <code>NOT NULL</code> eklemek tabloyu tarar. Bölün:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- 1. Önce nullable ekle</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ALTER</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">ADD</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> COLUMN </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> text</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- 2. Mevcut satırları batch&#39;ler hâlinde doldur (uygulama tarafında)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- 3. Kısıtı önce doğrulamadan ekle, sonra ayrı adımda doğrula</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ALTER</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">ADD</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> CONSTRAINT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders_status_not_null</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    CHECK</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> IS NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NOT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VALID;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ALTER</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders VALIDATE </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">CONSTRAINT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders_status_not_null;</span></span></code></pre>\n<p><code>NOT VALID</code> ile eklenen kısıt yeni satırlar için hemen geçerlidir ama mevcut satırları taramaz; <code>VALIDATE</code> ise tabloyu yalnızca <code>SHARE UPDATE EXCLUSIVE</code> lock ile tarar — yazmaları engellemez.</p>\n<p><strong><a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/indeks-yonetimi/\">İndeks</a>.</strong> Her zaman <code>CONCURRENTLY</code>:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">CREATE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> INDEX</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CONCURRENTLY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> idx_orders_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">ON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> orders (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span></code></pre>\n<p><strong>Foreign key.</strong> Aynı iki adımlı kalıp: önce <code>ADD CONSTRAINT ... NOT VALID</code>, sonra <code>VALIDATE CONSTRAINT</code>.</p>\n<p><strong><code>lock_timeout</code>.</strong> Bir migration, uzun süren bir sorgunun arkasında lock için beklerken kendisi de arkasındaki her şeyi bloklar. Bunu önlemek için migration oturumuna kısa bir <code>lock_timeout</code> verin — lock hemen alınamıyorsa migration beklemek yerine başarısız olsun, siz tekrar deneyin.</p>\n<blockquote><p><strong>Tan:</strong> Expand-contract'ın bedeli takvimde ödeniyor: yeniden adlandırma örneğindeki gibi tek bir değişiklik üç deploy'a yayılıyor. Daralt adımı unutulursa eski kolon kalıcı hâle gelir. O adımı, genişlet adımıyla aynı gün ayrı bir iş kaydı olarak açmak, bedelin borca dönüşmesini önler.</p></blockquote>\n<h2 id=\"kod-ve-şema-birlikte-uyumlu-olmalı\">Kod ve şema birlikte uyumlu olmalı</h2>\n<p>Expand-contract’ın özü tek bir kuralda: her ara adımda, hem <strong>çalışmakta olan eski kod</strong> hem de <strong>yeni kod</strong> mevcut şema ile çalışabilmeli.</p>\n<p>Veritabanı şeması, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">ileride lazım olur</a> yazısında “tek yönlü kapı” dediğim şeydir — geri almak pahalıdır. Bu yüzden değişikliği büyük ve geri dönülmez tek bir adım olarak değil, her biri ayrı ayrı geri alınabilir küçük adımlar olarak tasarlayın.</p>\n<hr/>\n<p>Sıfır kesintili migration bir araç değil, bir disiplindir: her şema değişikliğini, çalışan kodun hiç fark etmeyeceği kadar küçük adımlara bölmek.</p>\n<p>Tehlikeli olan değişikliğin kendisi değil, onu tek nefeste yapmaktır.</p>",
      "content_text": "> **Kısaca:** Tehlikeli olan şema değişikliği değil, onu tek adımda yapmak. Okumaları da kuyruğa sokan yalnızca ACCESS EXCLUSIVE lock alan işlemlerdir ve 50 milyon satırlık bir tabloda o kuyruğun adı kesintidir. Değişikliği expand-contract adımlarına bölün: kısıtı NOT VALID ile ekleyip VALIDATE'i ayrı adımda çalıştırın, indeksleri CONCURRENTLY kurun ve migration oturumuna kısa bir lock_timeout verin.\n\nBir deploy, 50 milyon satırlık bir tabloda tek bir `ALTER TABLE` çalıştırdı ve tabloyu dört dakika kilitledi. Dört dakika boyunca o tabloya değen her istek bekledi; site fiilen çöktü. Migration'ın kendisi doğruydu — sorun, onun tek adımda yapılmasıydı.\n\nTehlikeli olan şema değişikliği değildir. Tehlikeli olan, şema değişikliğini tek seferde, geri uyumluluğu düşünmeden yapmaktır.\n\n## Lock'u kim alır?\n\nHer şema değişikliği aynı maliyette değildir. Modern PostgreSQL'de sabit `DEFAULT` ile kolon eklemek bir metadata işlemidir — hızlıdır. Asıl tehlike, tabloyu uzun süre kilitleyen işlemlerdedir:\n\n- `CREATE INDEX` — `CONCURRENTLY` olmadan tabloyu yazmaya kapatır.\n- Tabloyu yeniden yazan `ALTER COLUMN ... TYPE` değişiklikleri.\n- Tüm tabloyu tarayan `NOT NULL`, `CHECK` veya foreign key eklemeleri.\n\nBu işlemler güçlü bir lock alır; `ACCESS EXCLUSIVE` alanlar — tablo yeniden yazımı ya da `NOT NULL`/`CHECK` eklemesi — tabloya değen her sorguyu, okuma dahil, kuyruğa sokar. Geri kalanı yalnızca yazmaları bloklar. Tablo büyükse kuyruk uzar.\n\n## Tehlikeli olan tek adımdır\n\nÇözüm migration yapmamak değil; her tehlikeli migration'ı, her biri tek başına güvenli ve geri uyumlu olan küçük adımlara bölmektir. Bunun adı **expand-contract** (genişlet–daralt) kalıbıdır.\n\nBir kolonu yeniden adlandırmak istediğinizi düşünün. Tek adımda `RENAME COLUMN`, eski kolonu okuyan çalışan kodu anında kırar. Bunun yerine üç deploy:\n\n1. **Genişlet.** Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, hâlâ eskiden okusun.\n2. **Geçiş.** Eski veriyi yeni kolona batch'ler hâlinde taşı. Kod artık yeniden okusun.\n3. **Daralt.** Eski kolonu sil.\n\nHer adım, hem bir önceki sürümdeki kod hem de yeni kod ile çalışır. Hiçbir an, çalışan kod ile şema uyumsuz değildir.\n\n## Güvenli reçeteler\n\nSık yapılan değişikliklerin kesintisiz hâlleri:\n\n**Yeni `NOT NULL` kolon.** Tek adımda `NOT NULL` eklemek tabloyu tarar. Bölün:\n\n```sql\n-- 1. Önce nullable ekle\nALTER TABLE orders ADD COLUMN status text;\n\n-- 2. Mevcut satırları batch'ler hâlinde doldur (uygulama tarafında)\n\n-- 3. Kısıtı önce doğrulamadan ekle, sonra ayrı adımda doğrula\nALTER TABLE orders ADD CONSTRAINT orders_status_not_null\n    CHECK (status IS NOT NULL) NOT VALID;\nALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null;\n```\n\n`NOT VALID` ile eklenen kısıt yeni satırlar için hemen geçerlidir ama mevcut satırları taramaz; `VALIDATE` ise tabloyu yalnızca `SHARE UPDATE EXCLUSIVE` lock ile tarar — yazmaları engellemez.\n\n**[İndeks](/tr/muhendislik/notlar/indeks-yonetimi/).** Her zaman `CONCURRENTLY`:\n\n```sql\nCREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);\n```\n\n**Foreign key.** Aynı iki adımlı kalıp: önce `ADD CONSTRAINT ... NOT VALID`, sonra `VALIDATE CONSTRAINT`.\n\n**`lock_timeout`.** Bir migration, uzun süren bir sorgunun arkasında lock için beklerken kendisi de arkasındaki her şeyi bloklar. Bunu önlemek için migration oturumuna kısa bir `lock_timeout` verin — lock hemen alınamıyorsa migration beklemek yerine başarısız olsun, siz tekrar deneyin.\n\n\n> **Tan:** Expand-contract'ın bedeli takvimde ödeniyor: yeniden adlandırma örneğindeki gibi tek bir değişiklik üç deploy'a yayılıyor. Daralt adımı unutulursa eski kolon kalıcı hâle gelir. O adımı, genişlet adımıyla aynı gün ayrı bir iş kaydı olarak açmak, bedelin borca dönüşmesini önler.\n\n\n## Kod ve şema birlikte uyumlu olmalı\n\nExpand-contract'ın özü tek bir kuralda: her ara adımda, hem **çalışmakta olan eski kod** hem de **yeni kod** mevcut şema ile çalışabilmeli.\n\nVeritabanı şeması, [ileride lazım olur](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) yazısında \"tek yönlü kapı\" dediğim şeydir — geri almak pahalıdır. Bu yüzden değişikliği büyük ve geri dönülmez tek bir adım olarak değil, her biri ayrı ayrı geri alınabilir küçük adımlar olarak tasarlayın.\n\n---\n\nSıfır kesintili migration bir araç değil, bir disiplindir: her şema değişikliğini, çalışan kodun hiç fark etmeyeceği kadar küçük adımlara bölmek.\n\nTehlikeli olan değişikliğin kendisi değil, onu tek nefeste yapmaktır.\n\n## Adımlar\n\n**Expand–contract ile bir kolonu kesintisiz değiştirmek** — Production'da kesinti yaratmadan migration almak: geri uyumlu adımlar, çok aşamalı kolon değişiklikleri ve tablo kilitlenmesini önleme.\n\n1. **Genişlet** — Yeni kolonu ekle. Kod hem eskiye hem yeniye yazsın, okumayı hâlâ eskiden yapsın.\n2. **Geçiş** — Eski veriyi yeni kolona batch’ler hâlinde taşı; taşıma bitince kod artık yeniden okusun.\n3. **Daralt** — Eski kolonu sil. Her adım hem bir önceki sürümün kodu hem yeni kod ile çalışır; hiçbir an şema ile çalışan kod uyumsuz değildir.\n\n## Kaynaklar\n\n- [ALTER TABLE: NOT VALID constraints, VALIDATE CONSTRAINT and ADD COLUMN with a DEFAULT](https://www.postgresql.org/docs/current/sql-altertable.html) — PostgreSQL\n- [Explicit Locking: table-level lock modes](https://www.postgresql.org/docs/current/explicit-locking.html) — PostgreSQL\n- [CREATE INDEX: building indexes concurrently](https://www.postgresql.org/docs/current/sql-createindex.html) — PostgreSQL\n- [Client Connection Defaults: lock_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html) — PostgreSQL\n\n## Sık sorulanlar\n\n### Hangi şema değişiklikleri yazmayı değil, okumayı da bloklar?\n\nACCESS EXCLUSIVE lock alanlar: tablo yeniden yazımı ya da NOT NULL veya CHECK eklemesi. Düz bir SELECT'i yalnızca bu lock modu engeller. CONCURRENTLY olmadan çalışan CREATE INDEX daha zayıf bir lock alır; tabloyu yazmaya kapatır ama okumaya açık bırakır.\n\n### Kısıtı neden önce NOT VALID ile eklemek, sonra ayrı adımda doğrulamak gerekir?\n\nNOT VALID ile eklenen kısıt tabloyu taramadan yeni satırlar için hemen geçerli olur, yani anında commit eder. Ayrı adımdaki VALIDATE CONSTRAINT ise tabloyu yalnızca SHARE UPDATE EXCLUSIVE lock ile tarar ve yazmaları engellemez.",
      "date_published": "2026-08-29T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "migrations",
        "production",
        "PostgreSQL Üretimde"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/read-write-splitting/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/read-write-splitting/",
      "title": "Read/Write Splitting: Okuma ve Yazma Yükünü Ayırmak",
      "summary": "Read replica okumayı ölçekler, yazma kapasitesine hiçbir şey katmaz; bu yüzden takas bedava değildir, bedeli replication lag'dir. Laravel'in sticky seçeneği read-after-write'ı yalnızca tek bir istek içinde onarır; iki istek arasında kullanıcı az önce güncellediği profili hâlâ bayat görür. Önce indeks disiplini: replica çoğu zaman tek bir CREATE INDEX'in çözeceği şeyi pahalıya satın almaktır.",
      "content_html": "<p>Primary veritabanının CPU’su sürekli tavandaydı. İlginç olan şuydu: yazma oranı düşüktü. Yükün neredeyse tamamı okumaydı — rapor sayfaları, listeleme endpoint’leri, arama. Tek bir primary, kendisine hiç ihtiyaç duymayan bir sürü okumayı taşımaya çalışıyordu.</p>\n<p>Okuma ve yazma yükünü ayırmak — read/write splitting — bu tablonun bilinen çözümü. Ama doğru zamanda ve doğru tuzakların farkında uygulanmazsa, çözdüğünden çok problem getirir. Okuma ve yazma <em>modelini</em> ayırmak ise ayrı bir karar; <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/cqrs-ne-zaman-gerekli/\">CQRS</a> ile read replica aynı şey değil.</p>\n<h2 id=\"çoğu-yük-okuma-ağırlıklıdır\">Çoğu yük okuma ağırlıklıdır</h2>\n<p>Trafiğin ne kadarının okuma olduğu iş yüküne bağlıdır: standart OLTP benchmark’larının ölçümünde TPC-E %90,69 okumadır, TPC-C ise %65,71’de kalır — yani kendi oranınızı varsaymak yerine ölçün. Her sipariş bir kez yazılır, ama onlarca kez okunur: listede, detayda, raporda, panelde. Bu asimetri, read/write splitting’i cazip kılan şeydir — çünkü ölçeklemeniz gereken taraf bellidir.</p>\n<h2 id=\"önce-bu-gerçekten-bir-kapasite-sorunu-mu\">Önce: bu gerçekten bir kapasite sorunu mu?</h2>\n<p>Replica eklemeden önce durun. Primary’nin dolu olması her zaman “kapasite bitti” demek değildir. Çoğu zaman tek bir eksik indeks, primary’yi olduğundan kat kat meşgul gösterir.</p>\n<p>Bir replica eklemek — yeni bir sunucu, replication kurulumu, lag izleme — aslında bir <code>CREATE INDEX</code>’in çözeceği bir problemi pahalıya satın almak olabilir. <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">Veri yoğunluklu sistemlerin kırılma noktaları</a> yazısındaki sıra nettir: önce indeks disiplini, sonra replica. Bu sırayı atlamayın.</p>\n<p>Sorgularınızı <code>EXPLAIN ANALYZE</code> ile ölçün. Primary, doğru indekslenmiş sorgular altında gerçekten doyuyorsa — işte o zaman replica.</p>\n<h2 id=\"replica-okumayı-ölçekler-yazmayı-değil\">Replica okumayı ölçekler, yazmayı değil</h2>\n<p>Net olalım: bir read replica, yazma kapasitenize <strong>hiçbir şey</strong> katmaz. Aynı yazmalar her replica’da yeniden oynanır. Replica okuma yükü problemini çözer; yazma yükü probleminiz varsa replica yanlış araçtır.</p>\n<h2 id=\"laravelde-kurulum\">Laravel’de kurulum</h2>\n<p>Laravel, okuma/yazma bağlantı ayrımını doğal destekler:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// config/database.php</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;pgsql&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;driver&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pgsql&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;read&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">   =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;host&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;10.0.0.2&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]],   </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// replica</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;write&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">  =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;host&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;10.0.0.1&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]],   </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// primary</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;sticky&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    // ...ortak ayarlar</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span></code></pre>\n<p><code>SELECT</code>’ler replica’ya, <code>INSERT/UPDATE/DELETE</code>’ler primary’ye gider. Replica’nın kendi bağlantı havuzu olur — primary’ninkinden ayrı; <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/pgbouncer-auth-query/\">pgBouncer</a> kullanıyorsanız ikisi ayrı havuzdur.</p>\n<h2 id=\"replication-lag-asıl-fatura\">Replication lag: asıl fatura</h2>\n<p>Replica, primary’nin birkaç milisaniye — yük altında birkaç saniye — gerisindedir. Bu gecikme, read/write splitting’in gerçek bedelidir ve adı <strong>read-after-write</strong> problemidir.</p>\n<p><code>sticky =&gt; true</code> bunu kısmen çözer: bir istek içinde yazma yaptıysanız, aynı istekteki sonraki okumalar primary’ye gider. Ama <code>sticky</code> yalnızca <strong>tek bir istek</strong> sınırında çalışır.</p>\n<p>Sınırın dışı hâlâ açıktır: kullanıcı profilini günceller (istek 1, primary’ye yazıldı), sonraki sayfaya geçer (istek 2, replica’dan okur) ve henüz replica’da oynanmamış eski profilini görür. Kullanıcı kendi yazdığı veriyi bayat görür. Bu bir bug gibi görünür ama aslında mimarinin kabul ettiği bir ödündür — bilerek kabul edilmesi gereken bir ödün.</p>\n<h2 id=\"sorgu-sınıflandırma-disiplini\">Sorgu sınıflandırma disiplini</h2>\n<p>Read/write splitting’i kurmak, her okumanın şu soruya cevap vermesini gerektirir: <strong>bu sorgu bayat veri okuyabilir mi?</strong></p>\n<ul>\n<li><strong>Replica’dan okunabilir:</strong> listeler, raporlar, arama sonuçları, paneller. Birkaç saniye gecikme önemsiz.</li>\n<li><strong>Primary’den okunmalı:</strong> hesap bakiyesi, stok adedi, yetki kontrolü, bir yazma kararının dayandığı her okuma.</li>\n</ul>\n<p>Bu sınıflandırma artık mimarinin bir parçasıdır ve dokümante edilmesi gerekir. Yeni gelen bir geliştiricinin “bu sorgu nereden okumalı” sorusuna bakacak bir yeri olmalı — yoksa sınıflandırma sessizce çürür.</p>\n<h2 id=\"ne-zaman-gerçekten-gerekir\">Ne zaman gerçekten gerekir?</h2>\n<p>Read/write splitting şu üç koşul birlikte sağlandığında doğru hamledir:</p>\n<ol>\n<li>İndeks disiplini tamamlanmış; sorgular doğru indeksli ve hâlâ primary doyuyor.</li>\n<li>Yük ölçülmüş biçimde okuma ağırlıklı.</li>\n<li>Bayat-okuma sınıflandırmasını yapıp dokümante edecek disiplin var.</li>\n</ol>\n<p>Bunlardan biri eksikse, replica’yı ertelemek daha ucuzdur.</p>\n<hr/>\n<p>Read/write splitting, dikey büyümeden ucuz bir ölçek hamlesidir — ama bedava değildir. Bedeli replication lag’dir ve onu görmezden gelmek, kullanıcıya kendi verisini eski göstermekle ödenir.</p>\n<p>Okumayı ayırmadan önce, hangi okumanın bayatlığa dayanabileceğini bilin.</p>",
      "content_text": "> **Kısaca:** Read replica okumayı ölçekler, yazma kapasitesine hiçbir şey katmaz; bu yüzden takas bedava değildir, bedeli replication lag'dir. Laravel'in sticky seçeneği read-after-write'ı yalnızca tek bir istek içinde onarır; iki istek arasında kullanıcı az önce güncellediği profili hâlâ bayat görür. Önce indeks disiplini: replica çoğu zaman tek bir CREATE INDEX'in çözeceği şeyi pahalıya satın almaktır.\n\nPrimary veritabanının CPU'su sürekli tavandaydı. İlginç olan şuydu: yazma oranı düşüktü. Yükün neredeyse tamamı okumaydı — rapor sayfaları, listeleme endpoint'leri, arama. Tek bir primary, kendisine hiç ihtiyaç duymayan bir sürü okumayı taşımaya çalışıyordu.\n\nOkuma ve yazma yükünü ayırmak — read/write splitting — bu tablonun bilinen çözümü. Ama doğru zamanda ve doğru tuzakların farkında uygulanmazsa, çözdüğünden çok problem getirir. Okuma ve yazma *modelini* ayırmak ise ayrı bir karar; [CQRS](/tr/muhendislik/notlar/cqrs-ne-zaman-gerekli/) ile read replica aynı şey değil.\n\n## Çoğu yük okuma ağırlıklıdır\n\nTrafiğin ne kadarının okuma olduğu iş yüküne bağlıdır: standart OLTP benchmark'larının ölçümünde TPC-E %90,69 okumadır, TPC-C ise %65,71'de kalır — yani kendi oranınızı varsaymak yerine ölçün. Her sipariş bir kez yazılır, ama onlarca kez okunur: listede, detayda, raporda, panelde. Bu asimetri, read/write splitting'i cazip kılan şeydir — çünkü ölçeklemeniz gereken taraf bellidir.\n\n## Önce: bu gerçekten bir kapasite sorunu mu?\n\nReplica eklemeden önce durun. Primary'nin dolu olması her zaman \"kapasite bitti\" demek değildir. Çoğu zaman tek bir eksik indeks, primary'yi olduğundan kat kat meşgul gösterir.\n\nBir replica eklemek — yeni bir sunucu, replication kurulumu, lag izleme — aslında bir `CREATE INDEX`'in çözeceği bir problemi pahalıya satın almak olabilir. [Veri yoğunluklu sistemlerin kırılma noktaları](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) yazısındaki sıra nettir: önce indeks disiplini, sonra replica. Bu sırayı atlamayın.\n\nSorgularınızı `EXPLAIN ANALYZE` ile ölçün. Primary, doğru indekslenmiş sorgular altında gerçekten doyuyorsa — işte o zaman replica.\n\n## Replica okumayı ölçekler, yazmayı değil\n\nNet olalım: bir read replica, yazma kapasitenize **hiçbir şey** katmaz. Aynı yazmalar her replica'da yeniden oynanır. Replica okuma yükü problemini çözer; yazma yükü probleminiz varsa replica yanlış araçtır.\n\n## Laravel'de kurulum\n\nLaravel, okuma/yazma bağlantı ayrımını doğal destekler:\n\n```php\n// config/database.php\n'pgsql' => [\n    'driver' => 'pgsql',\n    'read'   => ['host' => ['10.0.0.2']],   // replica\n    'write'  => ['host' => ['10.0.0.1']],   // primary\n    'sticky' => true,\n    // ...ortak ayarlar\n],\n```\n\n`SELECT`'ler replica'ya, `INSERT/UPDATE/DELETE`'ler primary'ye gider. Replica'nın kendi bağlantı havuzu olur — primary'ninkinden ayrı; [pgBouncer](/tr/muhendislik/notlar/pgbouncer-auth-query/) kullanıyorsanız ikisi ayrı havuzdur.\n\n## Replication lag: asıl fatura\n\nReplica, primary'nin birkaç milisaniye — yük altında birkaç saniye — gerisindedir. Bu gecikme, read/write splitting'in gerçek bedelidir ve adı **read-after-write** problemidir.\n\n`sticky => true` bunu kısmen çözer: bir istek içinde yazma yaptıysanız, aynı istekteki sonraki okumalar primary'ye gider. Ama `sticky` yalnızca **tek bir istek** sınırında çalışır.\n\nSınırın dışı hâlâ açıktır: kullanıcı profilini günceller (istek 1, primary'ye yazıldı), sonraki sayfaya geçer (istek 2, replica'dan okur) ve henüz replica'da oynanmamış eski profilini görür. Kullanıcı kendi yazdığı veriyi bayat görür. Bu bir bug gibi görünür ama aslında mimarinin kabul ettiği bir ödündür — bilerek kabul edilmesi gereken bir ödün.\n\n## Sorgu sınıflandırma disiplini\n\nRead/write splitting'i kurmak, her okumanın şu soruya cevap vermesini gerektirir: **bu sorgu bayat veri okuyabilir mi?**\n\n- **Replica'dan okunabilir:** listeler, raporlar, arama sonuçları, paneller. Birkaç saniye gecikme önemsiz.\n- **Primary'den okunmalı:** hesap bakiyesi, stok adedi, yetki kontrolü, bir yazma kararının dayandığı her okuma.\n\nBu sınıflandırma artık mimarinin bir parçasıdır ve dokümante edilmesi gerekir. Yeni gelen bir geliştiricinin \"bu sorgu nereden okumalı\" sorusuna bakacak bir yeri olmalı — yoksa sınıflandırma sessizce çürür.\n\n## Ne zaman gerçekten gerekir?\n\nRead/write splitting şu üç koşul birlikte sağlandığında doğru hamledir:\n\n1. İndeks disiplini tamamlanmış; sorgular doğru indeksli ve hâlâ primary doyuyor.\n2. Yük ölçülmüş biçimde okuma ağırlıklı.\n3. Bayat-okuma sınıflandırmasını yapıp dokümante edecek disiplin var.\n\nBunlardan biri eksikse, replica'yı ertelemek daha ucuzdur.\n\n---\n\nRead/write splitting, dikey büyümeden ucuz bir ölçek hamlesidir — ama bedava değildir. Bedeli replication lag'dir ve onu görmezden gelmek, kullanıcıya kendi verisini eski göstermekle ödenir.\n\nOkumayı ayırmadan önce, hangi okumanın bayatlığa dayanabileceğini bilin.\n\n## Kaynaklar\n\n- [Log-Shipping Standby Servers: streaming replication and hot standby](https://www.postgresql.org/docs/current/warm-standby.html) — PostgreSQL\n- [TPC-E vs. TPC-C: Characterizing the New TPC-E Benchmark via an I/O Comparison Study](https://www.cs.cmu.edu/~chensm/papers/TPCE-sigmod-record10.pdf) — ACM SIGMOD Record 39(3), 2010\n- [Database: Read and Write Connections](https://laravel.com/docs/12.x/database#read-and-write-connections) — Laravel\n\n## Sık sorulanlar\n\n### Read replica eklemek yazma kapasitesini arttırır mı?\n\nHayır. Aynı yazmalar her replica'da yeniden oynanır; replica yazma throughput'una hiçbir şey katmaz. Okuma yükü problemini çözer; problem yazma tarafındaysa replica yanlış araçtır.\n\n### sticky seçeneği neyi çözer, neyi çözmez?\n\nsticky açıkken aynı istek içinde yazmadan sonra gelen okumalar primary'ye gider; kullanıcı tek bir istek içinde kendi yazdığını bayat okumaz. İstekler arasında ise hiçbir şey yapmaz: sonraki sayfa hâlâ yazmayı henüz oynamamış bir replica'dan okuyabilir.",
      "date_published": "2026-08-22T00:00:00.000Z",
      "date_modified": "2026-10-07T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "scaling",
        "performance",
        "PostgreSQL Üretimde"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/dogru-cache-stratejisi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/dogru-cache-stratejisi/",
      "title": "Redis Koymadan Önce: Doğru Cache Stratejisi",
      "summary": "Cache hiçbir zaman daha hızlı bir veritabanı değildir; kabul edilmiş bayatlık karşılığında satın alınmış hızdır. Bu yüzden ilk soru teknik değil: bu veri ne kadar bayat olabilir? TTL sıkıcı varsayılandır, çünkü her yazma yolunu bilmeyi gerektirmez ve biri unutulduğunda sessizce bozulmaz. Açık invalidation ancak her yolu eksiksiz yakalarsanız kesindir. Ve cache okuma yükünü azaltır, yazma yükünü asla.",
      "content_html": "<p>Bir ekip “site yavaş” diye Redis cache ekledi. Bir hafta sonra destek kuyruğu doldu: “Fiyatı güncelledim ama eski fiyat görünüyor.” Cache yavaşlığı çözmüştü — ve sessizce yeni bir problem yaratmıştı.</p>\n<p>Cache eklemek bedava bir performans kazancı değildir. Tazeliği hıza takas etmektir; bu takası bilerek yapmadıysanız, fatura bir bug olarak geri gelir.</p>\n<h2 id=\"cache-bir-tutarlılık-ödünüdür\">Cache bir tutarlılık ödünüdür</h2>\n<p>Bir değeri cache’lediğiniz an şunu kabul etmiş olursunuz: birileri, bir süreliğine, eski veriyi görebilir. Cache hiçbir zaman “daha hızlı veritabanı” değildir; “kabul edilmiş bayatlık karşılığında hız”dır.</p>\n<p>Bu yüzden ilk soru teknik değil: <strong>bu veri ne kadar bayat olabilir?</strong> Cevabı “hiç” olan bir veri — bir hesap bakiyesi, bir stok adedi — cache’lenmeden önce iki kez düşünülmelidir.</p>\n<h2 id=\"önce-sorulacak-sorular\">Önce sorulacak sorular</h2>\n<p>Redis’i açmadan önce:</p>\n<ul>\n<li><strong>Bu gerçekten bir okuma darboğazı mı?</strong> “Site yavaş” bir hipotezdir, ölçüm değil. Yavaşlığın kaynağını görmeden cache eklemek, bilinmeyen bir şeyin üstünü örtmektir.</li>\n<li><strong>Yavaşlık aslında eksik bir indeks mi?</strong> Cache, kötü bir sorgunun üstünü örter ama onu çözmez. Bir <code>CREATE INDEX</code>’in çözeceği şeyi cache’le gizlemek, problemi taşımaktır — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> yanlış kırılma noktası tam olarak budur.</li>\n<li><strong>Bu veri ne kadar bayat kalabilir?</strong> Cevap, hangi stratejiyi seçeceğinizi belirler.</li>\n</ul>\n<h2 id=\"invalidation-neden-zor\">Invalidation neden zor?</h2>\n<p>Yazılımın iki zor probleminden biri olarak anılır cache invalidation — ve abartı değil. Cache’i koymak kolaydır; onu doğru anda <strong>temizlemek</strong> zordur.</p>\n<p>Çünkü bir cache değeri, onu etkileyen her yazma yolunda temizlenmelidir. Ürün fiyatı yalnızca “ürünü düzenle” ekranından değil; toplu fiyat güncellemesinden, bir indirim işinden, bir admin script’inden de değişir. Bu yolların <strong>hepsini</strong> bulup cache’i temizlemek — işte zor olan bu. Bir tanesini kaçırırsanız, yazının başındaki destek talebini alırsınız.</p>\n<h2 id=\"i̇ki-strateji\">İki strateji</h2>\n<p>Pratikte iki yol var:</p>\n<ul>\n<li><strong>TTL tabanlı.</strong> Değere bir ömür biçersiniz — 60 saniye, 5 dakika. Süre dolunca cache kendini tazeler. Basit, sağlam, her yazma yolunu bilmeyi gerektirmez. Karşılığında: TTL süresi kadar bayatlık kabul edersiniz.</li>\n<li><strong>Açık invalidation.</strong> Veri değiştiğinde cache’i elle silersiniz. Kesin ve tazedir — ama yalnızca her yazma yolunu eksiksiz yakalarsanız.</li>\n</ul>\n<p>Varsayılanım TTL’dir. Sıkıcı, öngörülebilir ve unutulan bir yazma yolu yüzünden sessizce bozulmaz. Açık invalidation’a yalnızca bayatlık gerçekten kabul edilemezken ve yazma yollarının sayısı sınırlı ve bilinirken geçerim. Çoğu zaman ikisi birlikte kullanılır: kısa bir TTL bir güvenlik ağı olarak, açık invalidation hız için.</p>\n<h2 id=\"cache-yazmayı-azaltmaz\">Cache yazmayı azaltmaz</h2>\n<p>Sık yapılan bir hata: yazma yükü altındaki bir sistemi cache ekleyerek kurtarmaya çalışmak. Cache <strong>okuma</strong> yükünü azaltır; yazma yüküne hiçbir etkisi yoktur — aksine, invalidation’ın kendisi ek yazma işidir. Probleminiz yazma tarafındaysa, cache yanlış araçtır.</p>\n<blockquote><p><strong>Tan:</strong> Bayatlık süresini tek başına mühendis seçmesin. Fiyat güncellendiğinde müşterinin eski fiyatı ne kadar süre görebileceği, ürün tarafının cevaplaması gereken bir soru. Cevabı yazılı alın: \"güncelleme en geç şu kadar sürede görünür\" cümlesi, destek kuyruğu dolmadan önce elinizdeki sözleşme olur.</p></blockquote>\n<h2 id=\"pratik-kalıplar\">Pratik kalıplar</h2>\n<ul>\n<li><strong>Cache-aside.</strong> Oku: önce cache’e bak, yoksa veritabanından al ve cache’e yaz. En yaygın ve en anlaşılır kalıp.</li>\n<li><strong>Stampede koruması.</strong> Popüler bir anahtarın TTL’i dolduğu an yüzlerce istek aynı anda veritabanına gider. Cache miss’te kısa bir lock ile yalnızca bir isteğin veritabanına gitmesini sağlayın.</li>\n<li><strong>Anahtar disiplini.</strong> Cache anahtarlarını namespace’leyin — <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/shared-redis-namespace-izolasyonu/\">shared Redis namespace izolasyonu</a> notundaki kalıp, cache anahtarları için de geçerlidir.</li>\n</ul>\n<hr/>\n<p>Cache, yavaş bir sistemin çözümü değil; ölçülmüş bir okuma darboğazının, bilinçli bir tutarlılık ödünüyle hafifletilmesidir. Invalidation planı olmadan eklenen cache, hızlandırdığından fazlasını borç olarak geri alır.</p>\n<p>Cache koymadan önce sorun: bu veri ne kadar eski olabilir — ve onu kim temizleyecek?</p>",
      "content_text": "> **Kısaca:** Cache hiçbir zaman daha hızlı bir veritabanı değildir; kabul edilmiş bayatlık karşılığında satın alınmış hızdır. Bu yüzden ilk soru teknik değil: bu veri ne kadar bayat olabilir? TTL sıkıcı varsayılandır, çünkü her yazma yolunu bilmeyi gerektirmez ve biri unutulduğunda sessizce bozulmaz. Açık invalidation ancak her yolu eksiksiz yakalarsanız kesindir. Ve cache okuma yükünü azaltır, yazma yükünü asla.\n\nBir ekip \"site yavaş\" diye Redis cache ekledi. Bir hafta sonra destek kuyruğu doldu: \"Fiyatı güncelledim ama eski fiyat görünüyor.\" Cache yavaşlığı çözmüştü — ve sessizce yeni bir problem yaratmıştı.\n\nCache eklemek bedava bir performans kazancı değildir. Tazeliği hıza takas etmektir; bu takası bilerek yapmadıysanız, fatura bir bug olarak geri gelir.\n\n## Cache bir tutarlılık ödünüdür\n\nBir değeri cache'lediğiniz an şunu kabul etmiş olursunuz: birileri, bir süreliğine, eski veriyi görebilir. Cache hiçbir zaman \"daha hızlı veritabanı\" değildir; \"kabul edilmiş bayatlık karşılığında hız\"dır.\n\nBu yüzden ilk soru teknik değil: **bu veri ne kadar bayat olabilir?** Cevabı \"hiç\" olan bir veri — bir hesap bakiyesi, bir stok adedi — cache'lenmeden önce iki kez düşünülmelidir.\n\n## Önce sorulacak sorular\n\nRedis'i açmadan önce:\n\n- **Bu gerçekten bir okuma darboğazı mı?** \"Site yavaş\" bir hipotezdir, ölçüm değil. Yavaşlığın kaynağını görmeden cache eklemek, bilinmeyen bir şeyin üstünü örtmektir.\n- **Yavaşlık aslında eksik bir indeks mi?** Cache, kötü bir sorgunun üstünü örter ama onu çözmez. Bir `CREATE INDEX`'in çözeceği şeyi cache'le gizlemek, problemi taşımaktır — [veri yoğunluklu sistemlerdeki](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) yanlış kırılma noktası tam olarak budur.\n- **Bu veri ne kadar bayat kalabilir?** Cevap, hangi stratejiyi seçeceğinizi belirler.\n\n## Invalidation neden zor?\n\nYazılımın iki zor probleminden biri olarak anılır cache invalidation — ve abartı değil. Cache'i koymak kolaydır; onu doğru anda **temizlemek** zordur.\n\nÇünkü bir cache değeri, onu etkileyen her yazma yolunda temizlenmelidir. Ürün fiyatı yalnızca \"ürünü düzenle\" ekranından değil; toplu fiyat güncellemesinden, bir indirim işinden, bir admin script'inden de değişir. Bu yolların **hepsini** bulup cache'i temizlemek — işte zor olan bu. Bir tanesini kaçırırsanız, yazının başındaki destek talebini alırsınız.\n\n## İki strateji\n\nPratikte iki yol var:\n\n- **TTL tabanlı.** Değere bir ömür biçersiniz — 60 saniye, 5 dakika. Süre dolunca cache kendini tazeler. Basit, sağlam, her yazma yolunu bilmeyi gerektirmez. Karşılığında: TTL süresi kadar bayatlık kabul edersiniz.\n- **Açık invalidation.** Veri değiştiğinde cache'i elle silersiniz. Kesin ve tazedir — ama yalnızca her yazma yolunu eksiksiz yakalarsanız.\n\nVarsayılanım TTL'dir. Sıkıcı, öngörülebilir ve unutulan bir yazma yolu yüzünden sessizce bozulmaz. Açık invalidation'a yalnızca bayatlık gerçekten kabul edilemezken ve yazma yollarının sayısı sınırlı ve bilinirken geçerim. Çoğu zaman ikisi birlikte kullanılır: kısa bir TTL bir güvenlik ağı olarak, açık invalidation hız için.\n\n## Cache yazmayı azaltmaz\n\nSık yapılan bir hata: yazma yükü altındaki bir sistemi cache ekleyerek kurtarmaya çalışmak. Cache **okuma** yükünü azaltır; yazma yüküne hiçbir etkisi yoktur — aksine, invalidation'ın kendisi ek yazma işidir. Probleminiz yazma tarafındaysa, cache yanlış araçtır.\n\n\n> **Tan:** Bayatlık süresini tek başına mühendis seçmesin. Fiyat güncellendiğinde müşterinin eski fiyatı ne kadar süre görebileceği, ürün tarafının cevaplaması gereken bir soru. Cevabı yazılı alın: \"güncelleme en geç şu kadar sürede görünür\" cümlesi, destek kuyruğu dolmadan önce elinizdeki sözleşme olur.\n\n\n## Pratik kalıplar\n\n- **Cache-aside.** Oku: önce cache'e bak, yoksa veritabanından al ve cache'e yaz. En yaygın ve en anlaşılır kalıp.\n- **Stampede koruması.** Popüler bir anahtarın TTL'i dolduğu an yüzlerce istek aynı anda veritabanına gider. Cache miss'te kısa bir lock ile yalnızca bir isteğin veritabanına gitmesini sağlayın.\n- **Anahtar disiplini.** Cache anahtarlarını namespace'leyin — [shared Redis namespace izolasyonu](/tr/muhendislik/notlar/shared-redis-namespace-izolasyonu/) notundaki kalıp, cache anahtarları için de geçerlidir.\n\n---\n\nCache, yavaş bir sistemin çözümü değil; ölçülmüş bir okuma darboğazının, bilinçli bir tutarlılık ödünüyle hafifletilmesidir. Invalidation planı olmadan eklenen cache, hızlandırdığından fazlasını borç olarak geri alır.\n\nCache koymadan önce sorun: bu veri ne kadar eski olabilir — ve onu kim temizleyecek?\n\n## Kaynaklar\n\n- [RFC 9111: HTTP Caching — freshness lifetime, stale responses and invalidation](https://www.rfc-editor.org/rfc/rfc9111.html) — IETF\n- [Distributed Locks with Redis](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/) — Redis\n\n## Sık sorulanlar\n\n### TTL mi kullanmalı, açık invalidation mı?\n\nGüvenli varsayılan TTL'dir: her yazma yolunu bilmeyi gerektirmez, biri unutulduğunda sessizce bozulmaz; karşılığında TTL süresi kadar bayatlık kabul edersiniz. Açık invalidation'a yalnızca bayatlık gerçekten kabul edilemezken ve yazma yolları sınırlı ve bilinirken geçin. Çoğu zaman ikisi birlikte çalışır.\n\n### Cache yazma yükünü azaltır mı?\n\nHayır. Cache okuma yükünü azaltır, yazma yüküne hiçbir etkisi yoktur; aksine invalidation'ın kendisi ek yazma işidir. Problem yazma tarafındaysa cache yanlış araçtır.",
      "date_published": "2026-08-15T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "caching",
        "redis",
        "performance",
        "production"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/nosql-tuzagi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/nosql-tuzagi/",
      "title": "NoSQL Tuzağı: \"Şeması Yok\" Diye Başlamak",
      "summary": "Şemasız veri diye bir şey yoktur; yalnızca şeklin nerede tanımlandığı ve onu kimin zorladığı sorusu vardır. Bir doküman veritabanını şema istemediğiniz için seçmek şemayı silmez; onu her okumaya, her yazmaya ve ekibin kafasına, hiçbir şeyin zorlamadığı bir yere taşır. NoSQL yerini yatay yazma ölçeğiyle ya da gerçekten heterojen belgelerle hak eder, şemadan kaçmakla değil.",
      "content_html": "<p>“Şeması yok, hızlı ilerleriz” diyerek MongoDB seçen bir projeyi iki yıl sonra gördüm. Her okuma savunmacıydı: <code>data?.user?.address?.city ?? null</code>. Sebep basitti — hiçbir belge bir diğeriyle tam olarak aynı şekle sahip değildi. Şema kaybolmamıştı; sadece veritabanından çıkıp her okuma kodu satırına dağılmıştı.</p>\n<h2 id=\"şemasız-bir-yanılgı\">”Şemasız” bir yanılgı</h2>\n<p>Şemasız veri diye bir şey yoktur. Verinin her zaman bir şekli vardır: bir kullanıcının e-postası, bir siparişin kalemleri, bir faturanın tarihi. Soru, şekil olup olmadığı değil; o şeklin <strong>nerede tanımlandığı</strong>.</p>\n<p>İki seçenek var. Şema ya veritabanında durur ve veritabanı tarafından zorlanır; ya da uygulama kodunda durur ve herkesin ona uyduğu varsayılır. “Şemasız” demek, ikincisini seçmektir — şemayı silmek değil, onu zorlanmayan bir yere taşımak.</p>\n<h2 id=\"şema-nereye-taşınır\">Şema nereye taşınır?</h2>\n<p>Veritabanı şemayı zorlamayı bıraktığında, o iş ortadan kalkmaz; şu yerlere dağılır:</p>\n<ul>\n<li><strong>Her okumaya.</strong> Kod, gelen belgenin beklenen alanlara sahip olduğunu artık varsayamaz; her alanı savunmacı biçimde kontrol eder.</li>\n<li><strong>Her yazmaya.</strong> Doğru şekli yazdığınızın garantisi yoktur; yalnızca umut vardır.</li>\n<li><strong>Dağınık doğrulama koduna.</strong> Veritabanının tek satırda yaptığı <code>NOT NULL</code>, tip kontrolü, foreign key — hepsi elle yazılmış kontrollere dönüşür.</li>\n<li><strong>Ekibin kafasına.</strong> “O alan bazen string, bazen array” bilgisi, dokümante edilmemiş bir kabile bilgisi olur.</li>\n</ul>\n<p>Şema bürokrasi değildir; veritabanının sizin yerinize ücretsiz yaptığı doğrulamadır. Onu reddetmek faturayı silmek değil, başka birine kesmektir.</p>\n<h2 id=\"çarpılan-krizler\">Çarpılan krizler</h2>\n<p>“Şemasız” rahatlığıyla başlayan projeler er ya da geç şunlara çarpar:</p>\n<ul>\n<li><strong>Veri drift’i.</strong> Zamanla belgenin on farklı sürümü aynı koleksiyonda yaşar. 2023’te yazılan kayıt, 2026’da yazılanla aynı şekilde değildir ve hiçbir migration bunu zorlamamıştır.</li>\n<li><strong>Referans bütünlüğü yok.</strong> Var olmayan bir kullanıcıya işaret eden bir sipariş, veritabanı tarafından engellenmez. Yetim kayıtlar sessizce birikir.</li>\n<li><strong>Migration bir uygulama işine dönüşür.</strong> İlişkisel dünyada tek bir <code>ALTER TABLE</code> ile yapılan şey, burada milyonlarca belgeyi tek tek gezen, batch’lenmesi gereken bir script olur.</li>\n</ul>\n<p>Bunlar küçük projede görünmez; tam da projenin büyüdüğü, ekibin değiştiği, kimsenin “o alan neden bazen yok” sorusunu cevaplayamadığı anda patlar.</p>\n<h2 id=\"nosqlin-gerçekten-doğru-olduğu-yerler\">NoSQL’in gerçekten doğru olduğu yerler</h2>\n<p>Bu, NoSQL’e karşı bir yazı değil. NoSQL’in gerçek ve haklı kullanımları var:</p>\n<ul>\n<li>Tek bir primary’nin yazma kapasitesini aşan, gerçekten yatay ölçek gerektiren yükler — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> en zor kırılma noktası.</li>\n<li>Doğası gereği gerçekten heterojen belgeler.</li>\n<li>Belirli erişim desenleri — saf key-value, wide-column, graph.</li>\n</ul>\n<p>Tuzak NoSQL değil; NoSQL’i bu gerçek güçlerinden biri için değil, sadece “şema istemiyorum” diye seçmektir. Doğru araç, yanlış gerekçe.</p>\n<h2 id=\"esneklik-istiyorsanız-postgresql-zaten-veriyor\">Esneklik istiyorsanız PostgreSQL zaten veriyor</h2>\n<p>“Bazı alanlar esnek olsun” ihtiyacı gerçekse, bunun için tüm bütünlüğü feda etmeniz gerekmez. PostgreSQL’in <code>jsonb</code> sütunu, ilişkisel tablonun bütünlüğü içinde esnek bir belge alanı verir — <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/\">PostgreSQL her şeye yeter mi</a> yazısında değindiğim gibi. Sabit olan alanlar şema olarak kalır, gerçekten değişken kısım <code>jsonb</code>’de durur. İkisinin en iyisi, hem de tek sistemde.</p>\n<hr/>\n<p>Şema bir yük değil, bir kalkandır. Onu veritabanından kaldırdığınızda kaybolmaz — yalnızca sizi koruduğu yerden çıkıp, sizi koruyamayacağı yere taşınır.</p>\n<p>“Şemasız” diye bir şey yoktur; yalnızca “şemayı kim zorluyor” sorusu vardır.</p>",
      "content_text": "> **Kısaca:** Şemasız veri diye bir şey yoktur; yalnızca şeklin nerede tanımlandığı ve onu kimin zorladığı sorusu vardır. Bir doküman veritabanını şema istemediğiniz için seçmek şemayı silmez; onu her okumaya, her yazmaya ve ekibin kafasına, hiçbir şeyin zorlamadığı bir yere taşır. NoSQL yerini yatay yazma ölçeğiyle ya da gerçekten heterojen belgelerle hak eder, şemadan kaçmakla değil.\n\n\"Şeması yok, hızlı ilerleriz\" diyerek MongoDB seçen bir projeyi iki yıl sonra gördüm. Her okuma savunmacıydı: `data?.user?.address?.city ?? null`. Sebep basitti — hiçbir belge bir diğeriyle tam olarak aynı şekle sahip değildi. Şema kaybolmamıştı; sadece veritabanından çıkıp her okuma kodu satırına dağılmıştı.\n\n## \"Şemasız\" bir yanılgı\n\nŞemasız veri diye bir şey yoktur. Verinin her zaman bir şekli vardır: bir kullanıcının e-postası, bir siparişin kalemleri, bir faturanın tarihi. Soru, şekil olup olmadığı değil; o şeklin **nerede tanımlandığı**.\n\nİki seçenek var. Şema ya veritabanında durur ve veritabanı tarafından zorlanır; ya da uygulama kodunda durur ve herkesin ona uyduğu varsayılır. \"Şemasız\" demek, ikincisini seçmektir — şemayı silmek değil, onu zorlanmayan bir yere taşımak.\n\n## Şema nereye taşınır?\n\nVeritabanı şemayı zorlamayı bıraktığında, o iş ortadan kalkmaz; şu yerlere dağılır:\n\n- **Her okumaya.** Kod, gelen belgenin beklenen alanlara sahip olduğunu artık varsayamaz; her alanı savunmacı biçimde kontrol eder.\n- **Her yazmaya.** Doğru şekli yazdığınızın garantisi yoktur; yalnızca umut vardır.\n- **Dağınık doğrulama koduna.** Veritabanının tek satırda yaptığı `NOT NULL`, tip kontrolü, foreign key — hepsi elle yazılmış kontrollere dönüşür.\n- **Ekibin kafasına.** \"O alan bazen string, bazen array\" bilgisi, dokümante edilmemiş bir kabile bilgisi olur.\n\nŞema bürokrasi değildir; veritabanının sizin yerinize ücretsiz yaptığı doğrulamadır. Onu reddetmek faturayı silmek değil, başka birine kesmektir.\n\n## Çarpılan krizler\n\n\"Şemasız\" rahatlığıyla başlayan projeler er ya da geç şunlara çarpar:\n\n- **Veri drift'i.** Zamanla belgenin on farklı sürümü aynı koleksiyonda yaşar. 2023'te yazılan kayıt, 2026'da yazılanla aynı şekilde değildir ve hiçbir migration bunu zorlamamıştır.\n- **Referans bütünlüğü yok.** Var olmayan bir kullanıcıya işaret eden bir sipariş, veritabanı tarafından engellenmez. Yetim kayıtlar sessizce birikir.\n- **Migration bir uygulama işine dönüşür.** İlişkisel dünyada tek bir `ALTER TABLE` ile yapılan şey, burada milyonlarca belgeyi tek tek gezen, batch'lenmesi gereken bir script olur.\n\nBunlar küçük projede görünmez; tam da projenin büyüdüğü, ekibin değiştiği, kimsenin \"o alan neden bazen yok\" sorusunu cevaplayamadığı anda patlar.\n\n## NoSQL'in gerçekten doğru olduğu yerler\n\nBu, NoSQL'e karşı bir yazı değil. NoSQL'in gerçek ve haklı kullanımları var:\n\n- Tek bir primary'nin yazma kapasitesini aşan, gerçekten yatay ölçek gerektiren yükler — [veri yoğunluklu sistemlerdeki](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) en zor kırılma noktası.\n- Doğası gereği gerçekten heterojen belgeler.\n- Belirli erişim desenleri — saf key-value, wide-column, graph.\n\nTuzak NoSQL değil; NoSQL'i bu gerçek güçlerinden biri için değil, sadece \"şema istemiyorum\" diye seçmektir. Doğru araç, yanlış gerekçe.\n\n## Esneklik istiyorsanız PostgreSQL zaten veriyor\n\n\"Bazı alanlar esnek olsun\" ihtiyacı gerçekse, bunun için tüm bütünlüğü feda etmeniz gerekmez. PostgreSQL'in `jsonb` sütunu, ilişkisel tablonun bütünlüğü içinde esnek bir belge alanı verir — [PostgreSQL her şeye yeter mi](/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/) yazısında değindiğim gibi. Sabit olan alanlar şema olarak kalır, gerçekten değişken kısım `jsonb`'de durur. İkisinin en iyisi, hem de tek sistemde.\n\n---\n\nŞema bir yük değil, bir kalkandır. Onu veritabanından kaldırdığınızda kaybolmaz — yalnızca sizi koruduğu yerden çıkıp, sizi koruyamayacağı yere taşınır.\n\n\"Şemasız\" diye bir şey yoktur; yalnızca \"şemayı kim zorluyor\" sorusu vardır.\n\n## Kaynaklar\n\n- [Designing Data-Intensive Applications, Chapter 2: schema-on-read vs. schema-on-write](https://dataintensive.net/) — Martin Kleppmann, O'Reilly Media, 2017\n- [MongoDB Manual: Schema Validation](https://www.mongodb.com/docs/manual/core/schema-validation/) — MongoDB\n- [JSON Types: jsonb](https://www.postgresql.org/docs/current/datatype-json.html) — PostgreSQL\n\n## Sık sorulanlar\n\n### Gerçekten şemasız veri var mıdır?\n\nHayır. Verinin her zaman bir şekli vardır; tek soru o şeklin nerede tanımlandığıdır. Şema ya veritabanında durur ve veritabanı tarafından zorlanır, ya da uygulama kodunda durur ve herkesin ona uyduğu varsayılır. Şemasız demek ikincisini seçmektir.\n\n### NoSQL gerçekten ne zaman doğru seçimdir?\n\nYük tek bir primary'nin yazma kapasitesini aşıp gerçekten yatay ölçek gerektirdiğinde, belgeler doğası gereği heterojen olduğunda ya da erişim deseni belirli olduğunda: saf key-value, wide-column, graph. Şema istememek bu gerekçelerden biri değildir.",
      "date_published": "2026-08-08T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "nosql",
        "database",
        "architecture",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/postgresql-her-seye-yeter-mi/",
      "title": "PostgreSQL Her Şeye Yeter mi?",
      "summary": "PostgreSQL'in yanına Elasticsearch, MongoDB ve RabbitMQ dizen listenin büyük kısmını uzun süre tek bir PostgreSQL taşır: arama için GIN index'li tsvector, doküman için jsonb, orta hacimli kuyruk için SELECT FOR UPDATE SKIP LOCKED. Sınırlar dürüst ve gerçek, ama ikinci veri sistemi bir histen ya da bir blog yazısından değil, ölçülmüş bir sınırdan gelmeli.",
      "content_html": "<p>Bir mimari planında, daha tek kullanıcı yokken şu liste vardı: kalıcı veri için PostgreSQL, arama için Elasticsearch, cache için Redis, esnek belgeler için MongoDB, kuyruk için RabbitMQ. Beş ayrı veri sistemi — beşinin de yedeği, monitoring’i, sürüm yükseltmesi ve kendine özgü hata modu.</p>\n<p>O listenin büyük kısmını, uzun bir süre boyunca, tek başına PostgreSQL taşır. Soru “PostgreSQL bunu yapabilir mi” değil; “bu beş sistemin operasyon yükünü gerçekten satın aldınız mı”.</p>\n<h2 id=\"postgresqlin-az-bilinen-genişliği\">PostgreSQL’in az bilinen genişliği</h2>\n<p>PostgreSQL’i sadece bir tablo-satır deposu sanmak, yeteneklerinin küçük bir kısmını kullanmaktır. Sık sık ayrı bir sistem getirten ihtiyaçların çoğu zaten içinde:</p>\n<ul>\n<li><strong>Tam metin arama.</strong> <code>tsvector</code>, <code>tsquery</code> ve GIN index ile gövdeli bir arama. Çoğu uygulamanın “ürün ara”, “yazı ara” ihtiyacını dilsel sapma, ağırlıklandırma ve sıralama dahil karşılar.</li>\n<li><strong>Doküman saklama.</strong> <code>jsonb</code> sütunu, GIN index’iyle birlikte sorgulanabilir bir belge deposudur. Şema esnekliği isteyen alanları, ilişkisel tablonun içinde tutabilirsiniz.</li>\n<li><strong>Kuyruk.</strong> <code>SELECT ... FOR UPDATE SKIP LOCKED</code> ve <code>LISTEN/NOTIFY</code>, orta hacimli bir iş kuyruğunu ayrı bir broker olmadan çalıştırır. Laravel’in <code>database</code> queue driver’ı bunun <code>SKIP LOCKED</code> tarafını kullanır — worker’ları <code>LISTEN/NOTIFY</code> sinyali beklemek yerine <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/\">kuyruk tablosunu</a> belirli aralıklarla yoklar.</li>\n<li><strong>Analitik sorgular.</strong> Window function’lar, CTE’ler, materialized view’lar — ayrı bir analitik veritabanına gitmeden ciddi raporlama.</li>\n<li><strong>Coğrafi veri.</strong> PostGIS eklentisiyle konum sorguları.</li>\n</ul>\n<p>Bu özellikler “var ama kullanılmıyor” değil; production’da güvenilir biçimde çalışan, olgun yeteneklerdir.</p>\n<h2 id=\"tek-sistemin-sessiz-getirisi\">Tek sistemin sessiz getirisi</h2>\n<p>Her yeni veri sistemi yalnızca bir kutu değil; bir bakım yükümlülüğüdür. Tek sistemde kalmanın getirisi:</p>\n<ul>\n<li><strong>Tek yedek hattı.</strong> Bir tane backup/restore prosedürü, bir tane kurtarma tatbikatı.</li>\n<li><strong>Tek izleme hedefi.</strong> Öğrenilecek bir metrik seti, kurulacak bir alarm düzeni.</li>\n<li><strong>Çapraz tutarlılık.</strong> En önemlisi bu. Arama indeksiniz ile veriniz aynı veritabanındaysa aralarında <strong>drift olamaz</strong> — ikisi aynı transaction’da güncellenir. Ayrı bir Elasticsearch ise eninde sonunda veriyle senkronizasyonu bozulur ve onu düzeltmek ayrı bir iş kalemine dönüşür.</li>\n<li><strong>Tek zihinsel model.</strong> Ekip tek bir sistemin tuhaflıklarını öğrenir, beş sistemin değil.</li>\n</ul>\n<p>Bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/neden-boring-architecture/\">boring architecture</a> yazısındaki innovation token mantığının veri katmanındaki hâlidir: her yeni sistem bir token ve token sınırlı.</p>\n<h2 id=\"sınır-nerede-başlar\">Sınır nerede başlar?</h2>\n<p>PostgreSQL her şeye yetmez — dürüst sınırları var ve onları görmezden gelmek de bir hata olur:</p>\n<ul>\n<li><strong>Arama.</strong> Yazım hatası toleransı, gelişmiş alaka düzeyi ayarı, faceted search ve çok dilli analiz ciddi ölçekte gerektiğinde, adanmış bir arama motoru gerçekten kazandırır.</li>\n<li><strong>Kuyruk.</strong> Çok yüksek throughput, karmaşık yönlendirme ya da çok tüketicili fan-out gerektiğinde gerçek bir message broker doğru araçtır. PostgreSQL kuyruğu orta hacimde rahat, uç hacimde değil.</li>\n<li><strong>Yazma throughput’u.</strong> Tek primary’nin fsync kapasitesini aşan sürekli yazma — bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">veri yoğunluklu sistemlerdeki</a> en zor kırılma noktası ve gerçek bir sınır.</li>\n</ul>\n<p>Bu sınırlardan birine çarptığınızda ek sistem getirmek doğru karardır. Çarpmadan getirmek ise yalnızca bir tahmin.</p>\n<h2 id=\"karar-önce-postgresql-sonra-ölçüm\">Karar: önce PostgreSQL, sonra ölçüm</h2>\n<p>Kural sade: yeni bir projede, PostgreSQL’in yapabildiği her şeyi PostgreSQL’e yaptırın. Arama, kuyruk, cache benzeri ihtiyaçların ilk durağı o olsun. Ayrı bir sistem, ancak <strong>ölçülmüş</strong> bir sınır aşıldığında gelsin — bir histen, bir blog yazısından ya da CV kaygısından değil.</p>\n<p>Çoğu ekip PostgreSQL’in belki %20’sini kullanır, sonra “yetmiyor” der. Yetmeyen çoğu zaman PostgreSQL değil, onu tanıma çabasıdır.</p>\n<hr/>\n<p>PostgreSQL her şeye yetmez — ama sandığınızdan çok daha fazlasına yeter. İkinci bir veri sistemi getirmeden önce, birincisinin gerçekten sonuna gelip gelmediğinizi sorun.</p>\n<p>Önce PostgreSQL; gerisi ölçümle gelsin.</p>",
      "content_text": "> **Kısaca:** PostgreSQL'in yanına Elasticsearch, MongoDB ve RabbitMQ dizen listenin büyük kısmını uzun süre tek bir PostgreSQL taşır: arama için GIN index'li tsvector, doküman için jsonb, orta hacimli kuyruk için SELECT FOR UPDATE SKIP LOCKED. Sınırlar dürüst ve gerçek, ama ikinci veri sistemi bir histen ya da bir blog yazısından değil, ölçülmüş bir sınırdan gelmeli.\n\nBir mimari planında, daha tek kullanıcı yokken şu liste vardı: kalıcı veri için PostgreSQL, arama için Elasticsearch, cache için Redis, esnek belgeler için MongoDB, kuyruk için RabbitMQ. Beş ayrı veri sistemi — beşinin de yedeği, monitoring'i, sürüm yükseltmesi ve kendine özgü hata modu.\n\nO listenin büyük kısmını, uzun bir süre boyunca, tek başına PostgreSQL taşır. Soru \"PostgreSQL bunu yapabilir mi\" değil; \"bu beş sistemin operasyon yükünü gerçekten satın aldınız mı\".\n\n## PostgreSQL'in az bilinen genişliği\n\nPostgreSQL'i sadece bir tablo-satır deposu sanmak, yeteneklerinin küçük bir kısmını kullanmaktır. Sık sık ayrı bir sistem getirten ihtiyaçların çoğu zaten içinde:\n\n- **Tam metin arama.** `tsvector`, `tsquery` ve GIN index ile gövdeli bir arama. Çoğu uygulamanın \"ürün ara\", \"yazı ara\" ihtiyacını dilsel sapma, ağırlıklandırma ve sıralama dahil karşılar.\n- **Doküman saklama.** `jsonb` sütunu, GIN index'iyle birlikte sorgulanabilir bir belge deposudur. Şema esnekliği isteyen alanları, ilişkisel tablonun içinde tutabilirsiniz.\n- **Kuyruk.** `SELECT ... FOR UPDATE SKIP LOCKED` ve `LISTEN/NOTIFY`, orta hacimli bir iş kuyruğunu ayrı bir broker olmadan çalıştırır. Laravel'in `database` queue driver'ı bunun `SKIP LOCKED` tarafını kullanır — worker'ları `LISTEN/NOTIFY` sinyali beklemek yerine [kuyruk tablosunu](/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/) belirli aralıklarla yoklar.\n- **Analitik sorgular.** Window function'lar, CTE'ler, materialized view'lar — ayrı bir analitik veritabanına gitmeden ciddi raporlama.\n- **Coğrafi veri.** PostGIS eklentisiyle konum sorguları.\n\nBu özellikler \"var ama kullanılmıyor\" değil; production'da güvenilir biçimde çalışan, olgun yeteneklerdir.\n\n## Tek sistemin sessiz getirisi\n\nHer yeni veri sistemi yalnızca bir kutu değil; bir bakım yükümlülüğüdür. Tek sistemde kalmanın getirisi:\n\n- **Tek yedek hattı.** Bir tane backup/restore prosedürü, bir tane kurtarma tatbikatı.\n- **Tek izleme hedefi.** Öğrenilecek bir metrik seti, kurulacak bir alarm düzeni.\n- **Çapraz tutarlılık.** En önemlisi bu. Arama indeksiniz ile veriniz aynı veritabanındaysa aralarında **drift olamaz** — ikisi aynı transaction'da güncellenir. Ayrı bir Elasticsearch ise eninde sonunda veriyle senkronizasyonu bozulur ve onu düzeltmek ayrı bir iş kalemine dönüşür.\n- **Tek zihinsel model.** Ekip tek bir sistemin tuhaflıklarını öğrenir, beş sistemin değil.\n\nBu, [boring architecture](/tr/muhendislik/denemeler/neden-boring-architecture/) yazısındaki innovation token mantığının veri katmanındaki hâlidir: her yeni sistem bir token ve token sınırlı.\n\n## Sınır nerede başlar?\n\nPostgreSQL her şeye yetmez — dürüst sınırları var ve onları görmezden gelmek de bir hata olur:\n\n- **Arama.** Yazım hatası toleransı, gelişmiş alaka düzeyi ayarı, faceted search ve çok dilli analiz ciddi ölçekte gerektiğinde, adanmış bir arama motoru gerçekten kazandırır.\n- **Kuyruk.** Çok yüksek throughput, karmaşık yönlendirme ya da çok tüketicili fan-out gerektiğinde gerçek bir message broker doğru araçtır. PostgreSQL kuyruğu orta hacimde rahat, uç hacimde değil.\n- **Yazma throughput'u.** Tek primary'nin fsync kapasitesini aşan sürekli yazma — bu, [veri yoğunluklu sistemlerdeki](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) en zor kırılma noktası ve gerçek bir sınır.\n\nBu sınırlardan birine çarptığınızda ek sistem getirmek doğru karardır. Çarpmadan getirmek ise yalnızca bir tahmin.\n\n## Karar: önce PostgreSQL, sonra ölçüm\n\nKural sade: yeni bir projede, PostgreSQL'in yapabildiği her şeyi PostgreSQL'e yaptırın. Arama, kuyruk, cache benzeri ihtiyaçların ilk durağı o olsun. Ayrı bir sistem, ancak **ölçülmüş** bir sınır aşıldığında gelsin — bir histen, bir blog yazısından ya da CV kaygısından değil.\n\nÇoğu ekip PostgreSQL'in belki %20'sini kullanır, sonra \"yetmiyor\" der. Yetmeyen çoğu zaman PostgreSQL değil, onu tanıma çabasıdır.\n\n---\n\nPostgreSQL her şeye yetmez — ama sandığınızdan çok daha fazlasına yeter. İkinci bir veri sistemi getirmeden önce, birincisinin gerçekten sonuna gelip gelmediğinizi sorun.\n\nÖnce PostgreSQL; gerisi ölçümle gelsin.\n\n## Kaynaklar\n\n- [Full Text Search: tsvector, tsquery, stemming and ranking](https://www.postgresql.org/docs/current/textsearch-intro.html) — PostgreSQL\n- [JSON Types: jsonb and GIN indexing](https://www.postgresql.org/docs/current/datatype-json.html) — PostgreSQL\n- [SELECT: the locking clause and SKIP LOCKED](https://www.postgresql.org/docs/current/sql-select.html) — PostgreSQL\n- [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) — PostgreSQL\n\n## Sık sorulanlar\n\n### PostgreSQL ayrı bir arama motorunun yerini tutar mı?\n\nÇoğu uygulama için evet. tsvector, tsquery ve GIN index; dilsel sapma, ağırlıklandırma ve sıralama dahil gövdeli bir tam metin araması verir. Adanmış bir motor; yazım hatası toleransı, gelişmiş alaka düzeyi ayarı, faceted search ve çok dilli analiz ciddi ölçekte gerektiğinde kazandırmaya başlar.\n\n### PostgreSQL iş kuyruğu olarak kullanılabilir mi?\n\nOrta hacimde evet. SELECT ... FOR UPDATE SKIP LOCKED, birden çok tüketicinin kuyruk benzeri bir tablodan lock çakışması olmadan iş çekmesini sağlar; LISTEN/NOTIFY ise tablonun değiştiğini haber verir. Çok yüksek throughput, karmaşık yönlendirme ya da çok tüketicili fan-out gerektiğinde doğru araç gerçek bir message broker olur.",
      "date_published": "2026-08-01T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "postgresql",
        "database",
        "architecture",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/denemeler/gereginden-karmasik-sistem-sinyalleri/",
      "url": "https://muhammetsafak.com/tr/muhendislik/denemeler/gereginden-karmasik-sistem-sinyalleri/",
      "title": "Gereğinden Karmaşık Bir Sistemin Verdiği Sinyaller",
      "summary": "Karmaşıklık tek bir kararla gelmez; tek tek savunulabilir adımlarla birikir, bu yüzden sadeleştirme bir hisle değil adı konmuş sinyallerle başlar. Sekiz somut sinyal, ardından dört adımlık bir çıkarma: ölü kod, tek implementasyonlu interface, erken dolaylılık ve en son net sınırlar. Hedef en az kod değil, en az gereksiz kod — özsel karmaşıklık silinmez, yalnızca daha kötü bir yere taşınır.",
      "content_html": "<p>Ekibe yeni katılan bir mühendis ilk haftasının sonunda sordu: “Tek bir alan eklemek neden iki gün sürüyor?” Soru masumdu, teşhis netti. Kimse o sistemi bir gün oturup “karmaşıklaştıralım” diye kararlaştırmamıştı. Karmaşıklık öylece birikmişti.</p>\n<p>Sorun da burada: karmaşıklık tek bir kararla gelmez, fark edilmeden birikir. Onu geri almak için önce onu <strong>görebilmek</strong> gerekir.</p>\n<h2 id=\"karmaşıklık-nasıl-birikir\">Karmaşıklık nasıl birikir?</h2>\n<p>Hiçbir karmaşık sistem, karmaşık olmaya karar vermez. Her adım tek başına makul görünür: “şimdilik bir interface açalım”, “bu duruma bir flag koyalım”, “araya bir katman daha”. Her biri küçük, her biri savunulabilir.</p>\n<p>Toplamları savunulabilir değil. Karmaşıklık bir olay değil, bir birikim — ve birikim, adı konmadığı sürece görünmez. Bu yüzden sadeleştirme bir his ile başlayamaz; somut sinyallerle başlar.</p>\n<h2 id=\"somut-sinyaller\">Somut sinyaller</h2>\n<p>Bir sistemin gereğinden karmaşık olduğunu söyleyen, tartışmaya kapalı kırmızı çizgiler:</p>\n<ul>\n<li><strong>Tek bir özelliği anlamak için çok sayıda dosya açıyorsunuz.</strong> Bir isteğin nereye gittiğini görmek altı dosya ve dört dolaylılık gerektiriyorsa, problem sizde değil, yapıdadır.</li>\n<li><strong>Tek satırlık bir değişiklik bir düzine dosyaya dokunuyor.</strong> Bir alan eklemek için migration, model, DTO, mapper, interface, factory, test fixture… Değişimin maliyeti, değişimin büyüklüğüyle orantılı değil.</li>\n<li><strong>Tek implementasyonlu interface’ler.</strong> “İhtimale karşı” açılmış, asla ikinci implementasyonu gelmemiş soyutlamalar. Bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> kod tabanında bıraktığı tortudur.</li>\n<li><strong>Onboarding haftalar sürüyor.</strong> Olgun ve sade bir sistemde yeni mühendis ilk günlerde üretkendir. Haftalar sürüyorsa, kod kendini anlatmıyordur.</li>\n<li><strong>Kimse sistemi tahtaya çizemiyor.</strong> “Şu kutu şuraya bağlı” diye anlatılamayan bir mimari, anlaşılmıyor demektir.</li>\n<li><strong><span data-scheduled=\"/tr/muhendislik/notlar/global-state-yonetimi/\">Test kurulumu testin kendisinden uzun</span>.</strong> Bir davranışı test etmek için kurulması gereken on mock, o davranışın çok fazla şeye bağlı olduğunun itirafıdır.</li>\n<li><strong>“Oraya dokunma, kırılır.”</strong> Bir kod bölgesi dokunulmaz ilan edilmişse, o bölge zaten kırıktır — sadece henüz patlamamıştır.</li>\n<li><strong>Kimsenin kullanmadığı config, flag ve seçenekler.</strong> Açılıp unutulmuş her seçenek, kodu her okuyanın hesaba katması gereken bir daldır.</li>\n</ul>\n<p>Bir sistemde bu sinyallerden ikisi-üçü varsa, tartışma “karmaşık mı” değil, “nereden başlayacağız”dır.</p>\n<h2 id=\"sadeleştirmeye-nereden-başlanır\">Sadeleştirmeye nereden başlanır?</h2>\n<p>Sadeleştirme bir yeniden yazım değildir; çoğu zaman bir <strong>çıkarma</strong> işlemidir. Sırayla:</p>\n<ol>\n<li><strong>Ölü kodu sil.</strong> Kullanılmayan flag, çağrılmayan method, ulaşılmayan branch. En düşük riskli, en yüksek getirili adım — silinen kodun bug’ı olmaz.</li>\n<li><strong>Tek implementasyonlu interface’leri çökert.</strong> Soyutlama gerçek bir ikinci kullanım kazanana kadar somut sınıfı doğrudan kullanın. Dikiş gerçekten gerektiğinde — <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">ihtiyaç kanıtlandığında</a> — birkaç saatlik bir refactor’la geri gelir.</li>\n<li><strong>Erken dolaylılığı satır içine al.</strong> Tek yerden çağrılan, sadece argümanı bir başka fonksiyona geçiren katmanları kaldırın.</li>\n<li><strong>Sınırları netleştir.</strong> Geriye kalan karmaşıklığı <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/\">modüler monolitteki</a> gibi net sınırların arkasına toplayın — böylece bir dahaki birikim erken görünür.</li>\n</ol>\n<p>Her adımdan sonra durun ve ölçün. Sadeleştirme de aşırıya kaçabilir; amaç “en az kod” değil, “en az gereksiz kod”.</p>\n<blockquote><p><strong>Tan:</strong> Yeni katılanların sorularını tek bir yerde biriktirin. Aynı soru tekrar tekrar geliyorsa, listedeki hangi sinyalin sizde en ağır bastığına o kayıt işaret edebilir. Bir sadeleştirme önerisini savunurken, bu kayıt yazıdaki sinyallere ekibinizin kendi sorularından bir kanıt ekler.</p></blockquote>\n<h2 id=\"karmaşıklık-her-zaman-kötü-değil\">Karmaşıklık her zaman kötü değil</h2>\n<p>Önemli bir ayrım: karmaşıklığın iki türü var. Fred Brooks’un kavramlarıyla — <strong>özsel (essential)</strong> ve <strong>tesadüfi (accidental)</strong>.</p>\n<p>Özsel karmaşıklık domain’in kendisinden gelir. Vergi hesabı karmaşıktır, çünkü vergi mevzuatı karmaşıktır; bunu silemezsiniz, sadece dürüstçe modelleyebilirsiniz. Tesadüfi karmaşıklık ise sizin eklediğinizdir: gereksiz katman, erken soyutlama, unutulmuş flag.</p>\n<p>Sadeleştirmenin hedefi tesadüfi olanı temizlemektir — özsel olanı değil. Bir sistemi “çok basit” yapıp domain’in gerçek karmaşıklığını görmezden gelmek de bir hatadır; o karmaşıklık silinmez, sadece yanlış yere — genelde çağıran kodun içine — taşınır.</p>\n<p>İyi mimari, domain ne kadar karmaşıksa o kadar karmaşıktır; daha fazlası değil.</p>\n<hr/>\n<p>Karmaşıklık fark edilmeden birikir; bu yüzden onu fark edilir kılan sinyalleri ezbere bilmek bir mühendislik becerisidir. Sinyali adlandıramıyorsanız, sadeleştirmeye de başlayamazsınız.</p>\n<p>Bir sistemi basit tutmak, onu basit kurmaktan daha çok emek ister — ve o emeğe değer.</p>",
      "content_text": "> **Kısaca:** Karmaşıklık tek bir kararla gelmez; tek tek savunulabilir adımlarla birikir, bu yüzden sadeleştirme bir hisle değil adı konmuş sinyallerle başlar. Sekiz somut sinyal, ardından dört adımlık bir çıkarma: ölü kod, tek implementasyonlu interface, erken dolaylılık ve en son net sınırlar. Hedef en az kod değil, en az gereksiz kod — özsel karmaşıklık silinmez, yalnızca daha kötü bir yere taşınır.\n\nEkibe yeni katılan bir mühendis ilk haftasının sonunda sordu: \"Tek bir alan eklemek neden iki gün sürüyor?\" Soru masumdu, teşhis netti. Kimse o sistemi bir gün oturup \"karmaşıklaştıralım\" diye kararlaştırmamıştı. Karmaşıklık öylece birikmişti.\n\nSorun da burada: karmaşıklık tek bir kararla gelmez, fark edilmeden birikir. Onu geri almak için önce onu **görebilmek** gerekir.\n\n## Karmaşıklık nasıl birikir?\n\nHiçbir karmaşık sistem, karmaşık olmaya karar vermez. Her adım tek başına makul görünür: \"şimdilik bir interface açalım\", \"bu duruma bir flag koyalım\", \"araya bir katman daha\". Her biri küçük, her biri savunulabilir.\n\nToplamları savunulabilir değil. Karmaşıklık bir olay değil, bir birikim — ve birikim, adı konmadığı sürece görünmez. Bu yüzden sadeleştirme bir his ile başlayamaz; somut sinyallerle başlar.\n\n## Somut sinyaller\n\nBir sistemin gereğinden karmaşık olduğunu söyleyen, tartışmaya kapalı kırmızı çizgiler:\n\n- **Tek bir özelliği anlamak için çok sayıda dosya açıyorsunuz.** Bir isteğin nereye gittiğini görmek altı dosya ve dört dolaylılık gerektiriyorsa, problem sizde değil, yapıdadır.\n- **Tek satırlık bir değişiklik bir düzine dosyaya dokunuyor.** Bir alan eklemek için migration, model, DTO, mapper, interface, factory, test fixture... Değişimin maliyeti, değişimin büyüklüğüyle orantılı değil.\n- **Tek implementasyonlu interface'ler.** \"İhtimale karşı\" açılmış, asla ikinci implementasyonu gelmemiş soyutlamalar. Bu, [spekülatif genelliğin](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) kod tabanında bıraktığı tortudur.\n- **Onboarding haftalar sürüyor.** Olgun ve sade bir sistemde yeni mühendis ilk günlerde üretkendir. Haftalar sürüyorsa, kod kendini anlatmıyordur.\n- **Kimse sistemi tahtaya çizemiyor.** \"Şu kutu şuraya bağlı\" diye anlatılamayan bir mimari, anlaşılmıyor demektir.\n- **Test kurulumu testin kendisinden uzun.** Bir davranışı test etmek için kurulması gereken on mock, o davranışın çok fazla şeye bağlı olduğunun itirafıdır.\n- **\"Oraya dokunma, kırılır.\"** Bir kod bölgesi dokunulmaz ilan edilmişse, o bölge zaten kırıktır — sadece henüz patlamamıştır.\n- **Kimsenin kullanmadığı config, flag ve seçenekler.** Açılıp unutulmuş her seçenek, kodu her okuyanın hesaba katması gereken bir daldır.\n\nBir sistemde bu sinyallerden ikisi-üçü varsa, tartışma \"karmaşık mı\" değil, \"nereden başlayacağız\"dır.\n\n## Sadeleştirmeye nereden başlanır?\n\nSadeleştirme bir yeniden yazım değildir; çoğu zaman bir **çıkarma** işlemidir. Sırayla:\n\n1. **Ölü kodu sil.** Kullanılmayan flag, çağrılmayan method, ulaşılmayan branch. En düşük riskli, en yüksek getirili adım — silinen kodun bug'ı olmaz.\n2. **Tek implementasyonlu interface'leri çökert.** Soyutlama gerçek bir ikinci kullanım kazanana kadar somut sınıfı doğrudan kullanın. Dikiş gerçekten gerektiğinde — [ihtiyaç kanıtlandığında](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) — birkaç saatlik bir refactor'la geri gelir.\n3. **Erken dolaylılığı satır içine al.** Tek yerden çağrılan, sadece argümanı bir başka fonksiyona geçiren katmanları kaldırın.\n4. **Sınırları netleştir.** Geriye kalan karmaşıklığı [modüler monolitteki](/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/) gibi net sınırların arkasına toplayın — böylece bir dahaki birikim erken görünür.\n\nHer adımdan sonra durun ve ölçün. Sadeleştirme de aşırıya kaçabilir; amaç \"en az kod\" değil, \"en az gereksiz kod\".\n\n\n> **Tan:** Yeni katılanların sorularını tek bir yerde biriktirin. Aynı soru tekrar tekrar geliyorsa, listedeki hangi sinyalin sizde en ağır bastığına o kayıt işaret edebilir. Bir sadeleştirme önerisini savunurken, bu kayıt yazıdaki sinyallere ekibinizin kendi sorularından bir kanıt ekler.\n\n\n## Karmaşıklık her zaman kötü değil\n\nÖnemli bir ayrım: karmaşıklığın iki türü var. Fred Brooks'un kavramlarıyla — **özsel (essential)** ve **tesadüfi (accidental)**.\n\nÖzsel karmaşıklık domain'in kendisinden gelir. Vergi hesabı karmaşıktır, çünkü vergi mevzuatı karmaşıktır; bunu silemezsiniz, sadece dürüstçe modelleyebilirsiniz. Tesadüfi karmaşıklık ise sizin eklediğinizdir: gereksiz katman, erken soyutlama, unutulmuş flag.\n\nSadeleştirmenin hedefi tesadüfi olanı temizlemektir — özsel olanı değil. Bir sistemi \"çok basit\" yapıp domain'in gerçek karmaşıklığını görmezden gelmek de bir hatadır; o karmaşıklık silinmez, sadece yanlış yere — genelde çağıran kodun içine — taşınır.\n\nİyi mimari, domain ne kadar karmaşıksa o kadar karmaşıktır; daha fazlası değil.\n\n---\n\nKarmaşıklık fark edilmeden birikir; bu yüzden onu fark edilir kılan sinyalleri ezbere bilmek bir mühendislik becerisidir. Sinyali adlandıramıyorsanız, sadeleştirmeye de başlayamazsınız.\n\nBir sistemi basit tutmak, onu basit kurmaktan daha çok emek ister — ve o emeğe değer.\n\n## Kaynaklar\n\n- [No Silver Bullet — Essence and Accidents of Software Engineering (TR86-020): essence, the difficulties inherent in the nature of software, versus accidents, those that attend its production but are not inherent](https://www.cs.unc.edu/techreports/86-020.pdf) — Frederick P. Brooks Jr., University of North Carolina\n- [Yagni: the cost of build, delay, carry and repair for a presumptive feature](https://martinfowler.com/bliki/Yagni.html) — Martin Fowler\n\n## Sık sorulanlar\n\n### Gereğinden karmaşık bir sistemle gerçekten zor bir domain nasıl ayırt edilir?\n\nBaktığınız karmaşıklığın türüne göre. Özsel karmaşıklık domain'den gelir — vergi hesabı karmaşıktır çünkü vergi mevzuatı karmaşıktır — ve yalnızca dürüstçe modellenebilir. Tesadüfi karmaşıklık ise sizin eklediğinizdir: fazladan katman, erken soyutlama, unutulmuş flag. Temizlemeye değer olan yalnızca ikincisidir.\n\n### Sadeleştirmeye nereden başlanır?\n\nSırayla: ölü kodu silin, tek implementasyonlu interface'leri çökertin, tek yerden çağrılıp yalnızca argümanı ileten dolaylılığı satır içine alın, sonra geriye kalanı net sınırların arkasına toplayın. Her adımdan sonra durup ölçün — sadeleştirme de aşırıya kaçabilir.\n\n### Tek implementasyonlu bir interface her zaman hata mıdır?\n\nO, ihtimale karşı açılmış bir soyutlamanın ikinci implementasyonu hiç gelmeyince geride bıraktığı tortudur. Soyutlama gerçek bir ikinci kullanım kazanana kadar somut sınıfı doğrudan kullanın; dikiş gerçekten gerektiğinde birkaç saatlik bir refactor'la geri gelir. Bu, onu yıllarca taşımaktan ucuzdur.",
      "date_published": "2026-07-25T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "architecture",
        "simplicity",
        "decisions",
        "opinion"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/circuit-breaker-dis-servis-entegrasyonu/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/circuit-breaker-dis-servis-entegrasyonu/",
      "title": "Dış Servis Entegrasyonlarında Circuit Breaker",
      "summary": "Yavaş bir dış servis, çökmüş birinden daha hızlı düşürür: çağrı process'i tutar, havuz dolar ve o servisle ilgisi olmayan endpoint'ler zaman aşımına uğramaya başlar. Sırayla üç savunma — her çağrıya açık ve kısa bir timeout, yalnızca exponential backoff ve jitter ile retry, ve hiç çağrı yapmamaya karar veren bir circuit breaker. Değerini, devre açıkken sunduğunuz fallback belirler.",
      "content_html": "<p>Bir SMS sağlayıcısı yavaşladı — çökmedi bile, sadece yanıt süresi 30 saniyeye çıktı. Yarım saat içinde bütün uygulama yanıt veremez hâle geldi; SMS ile hiç ilgisi olmayan endpoint’ler bile zaman aşımına uğruyordu. Tek bir yavaş dış servis, koca sistemi birlikte aşağı çekmişti.</p>\n<p>Bir dış servis er ya da geç çöker. Soru o değil. Soru şu: sizin sisteminiz onunla birlikte mi çökecek?</p>\n<h2 id=\"zincirleme-çöküş-nasıl-olur\">Zincirleme çöküş nasıl olur?</h2>\n<p>Mantık basit ve acımasız. Yavaş bir dış çağrı, onu yapan worker’ı — ya da PHP-FPM process’ini — kendine kilitler. Çağrı 30 saniye sürerse, o process 30 saniye boyunca başka hiçbir isteğe bakamaz.</p>\n<p>İstekler birikir, process havuzu dolar. Havuz dolunca — SMS ile hiç ilgisi olmayanlar dâhil — yeni istekler de bekler. Sağlıklı endpoint’leriniz, hasta bir bağımlılık yüzünden ölür. Buna <strong>cascading failure</strong> denir ve neredeyse her zaman bir yavaşlamayla başlar.</p>\n<h2 id=\"i̇lk-savunma-timeout\">İlk savunma: timeout</h2>\n<p>İlk ve en ucuz savunma, her dış çağrıya <strong>açık ve kısa</strong> bir timeout vermektir. Varsayılan timeout’lar — çoğu HTTP istemcisinde 30 saniye veya daha fazla — production için kabul edilemez.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">$response </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Http</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">timeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// toplam süre</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    -&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">connectTimeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)             </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// bağlantı kurma süresi</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    -&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">get</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;https://sms-provider.example/send&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span></code></pre>\n<p><a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/laravel-queue-production-yavaslama/\">Laravel queue yazısındaki</a> kural burada da geçerli: varsayılan 30 saniye değil, 5. Bir process’in bir bağımlılığı ne kadar süre bekleyeceğini şansa bırakmayın — bu sayı bir karar olmalı.</p>\n<h2 id=\"i̇kinci-savunma-retry--ama-dikkatli\">İkinci savunma: retry — ama dikkatli</h2>\n<p>Geçici hatalar için retry mantıklıdır, ama körlemesine retry zarar verir. Çöken bir servisi hemen yeniden denemek ona daha çok yük bindirir — buna <strong>retry storm</strong> denir; servisi tam ayağa kalkamadan tekrar devirir.</p>\n<p>Retry yapıyorsanız iki şey şart: exponential backoff (her denemede artan bekleme) ve jitter (rastgele sapma — tüm client’lar aynı anda denemesin).</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">Http</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">retry</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $attempt) {</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    // exponential backoff (200, 400, 800 ms) ve üstüne jitter</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 200</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> **</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($attempt </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> random_int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">100</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($exception) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $exception </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">instanceof</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ConnectionException</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">timeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">get</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($url);</span></span></code></pre>\n<p>Retry sayısını düşük tutun. Üç denemeden sonra hâlâ hata varsa, sorun geçici değildir; denemeye devam etmek yalnızca kendi process’inizi daha uzun bağlar.</p>\n<h2 id=\"üçüncü-savunma-circuit-breaker\">Üçüncü savunma: circuit breaker</h2>\n<p>Timeout tek bir çağrıyı sınırlar; circuit breaker <strong>çağrı yapmayı bırakmaya</strong> karar verir. Mantığı bir elektrik sigortasından gelir ve üç durumu vardır:</p>\n<ul>\n<li><strong>Closed</strong> — her şey normal, çağrılar geçer. Hatalar sayılır.</li>\n<li><strong>Open</strong> — son N çağrının çoğu başarısız oldu; devre açılır. Artık dış servise hiç gidilmez, çağrı <strong>anında</strong> hata döner.</li>\n<li><strong>Half-open</strong> — bir süre sonra devre tek bir deneme çağrısına izin verir. Başarılıysa <code>closed</code>’a döner, değilse tekrar <code>open</code>.</li>\n</ul>\n<p>Kazanç şu: dış servis hastayken siz onu beklemezsiniz. 5 saniyelik timeout bile process başına 5 saniyedir; <code>open</code> durumda bu süre 0’a iner. Fail-fast, yavaş-fail’den iyidir.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// Kavramsal akış — eşik, sayaç ve zamanlayıcıyı olgun bir pakete bırakın.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($breaker</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">isOpen</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;sms&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> $this</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">queueForLater</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($message);   </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// devre açık: hiç deneme yok</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $response </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Http</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">timeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">post</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($smsUrl, $payload);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $breaker</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">recordSuccess</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;sms&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">} </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\Throwable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $e) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $breaker</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">recordFailure</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;sms&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);          </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// eşik aşılırsa devre açılır</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> $this</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">queueForLater</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($message);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre>\n<p>Bu mantığı sıfırdan yazmak yerine olgun bir paket kullanın — durum saymayı, eşiği ve zamanlayıcıyı doğru kurmak göründüğünden inceliklidir.</p>\n<h2 id=\"fallback-devre-açıkken-ne-yapılır\">Fallback: devre açıkken ne yapılır?</h2>\n<p>Circuit breaker’ın değeri, <code>open</code> durumda ne yaptığınızla ortaya çıkar. <span data-scheduled=\"/tr/muhendislik/notlar/hata-yonetimi/\">“Hata döndür”</span> en zayıf seçenektir. Daha iyileri:</p>\n<ul>\n<li><strong>Kuyruğa al, sonra gönder.</strong> SMS hemen gitmek zorunda değilse, devre kapanınca işlenmek üzere <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/senkron-mu-asenkron-mu/\">kuyruğa</a> bırakın.</li>\n<li><strong>Bayat ama geçerli veriyi sun.</strong> Bir döviz kuru servisi çökmüşse, son bilinen kuru göstermek hiç göstermemekten iyidir.</li>\n<li><strong>Özelliği zarifçe kıs.</strong> “Öneriler şu an yüklenemiyor” mesajı, koca sayfayı çökertmekten iyidir.</li>\n</ul>\n<p>Hangi fallback’in doğru olduğu işe bağlıdır — ama “fallback yok” bir cevap değildir.</p>\n<h2 id=\"ne-zaman-bu-kadarına-gerek-yok\">Ne zaman bu kadarına gerek yok?</h2>\n<p>Her dış çağrı circuit breaker istemez. Timeout <strong>her zaman</strong> şart. Retry, yalnızca işlem idempotent ise. Circuit breaker ise asıl olarak çağrı sık ve servis kullanıcı yolundayken değer katar — günde birkaç kez çağrılan bir entegrasyon için timeout ve makul bir retry çoğu zaman yeter.</p>\n<hr/>\n<p>Dış servis çökmesi bir “ihtimal” değil, bir “ne zaman” sorusudur. Onunla birlikte çökmek ise bir tasarım tercihidir — ve değiştirilebilir bir tercih.</p>\n<p>Sisteminizi, en zayıf bağımlılığınız kadar kırılgan bırakmayın.</p>",
      "content_text": "> **Kısaca:** Yavaş bir dış servis, çökmüş birinden daha hızlı düşürür: çağrı process'i tutar, havuz dolar ve o servisle ilgisi olmayan endpoint'ler zaman aşımına uğramaya başlar. Sırayla üç savunma — her çağrıya açık ve kısa bir timeout, yalnızca exponential backoff ve jitter ile retry, ve hiç çağrı yapmamaya karar veren bir circuit breaker. Değerini, devre açıkken sunduğunuz fallback belirler.\n\nBir SMS sağlayıcısı yavaşladı — çökmedi bile, sadece yanıt süresi 30 saniyeye çıktı. Yarım saat içinde bütün uygulama yanıt veremez hâle geldi; SMS ile hiç ilgisi olmayan endpoint'ler bile zaman aşımına uğruyordu. Tek bir yavaş dış servis, koca sistemi birlikte aşağı çekmişti.\n\nBir dış servis er ya da geç çöker. Soru o değil. Soru şu: sizin sisteminiz onunla birlikte mi çökecek?\n\n## Zincirleme çöküş nasıl olur?\n\nMantık basit ve acımasız. Yavaş bir dış çağrı, onu yapan worker'ı — ya da PHP-FPM process'ini — kendine kilitler. Çağrı 30 saniye sürerse, o process 30 saniye boyunca başka hiçbir isteğe bakamaz.\n\nİstekler birikir, process havuzu dolar. Havuz dolunca — SMS ile hiç ilgisi olmayanlar dâhil — yeni istekler de bekler. Sağlıklı endpoint'leriniz, hasta bir bağımlılık yüzünden ölür. Buna **cascading failure** denir ve neredeyse her zaman bir yavaşlamayla başlar.\n\n## İlk savunma: timeout\n\nİlk ve en ucuz savunma, her dış çağrıya **açık ve kısa** bir timeout vermektir. Varsayılan timeout'lar — çoğu HTTP istemcisinde 30 saniye veya daha fazla — production için kabul edilemez.\n\n```php\n$response = Http::timeout(5)        // toplam süre\n    ->connectTimeout(2)             // bağlantı kurma süresi\n    ->get('https://sms-provider.example/send');\n```\n\n[Laravel queue yazısındaki](/tr/muhendislik/notlar/laravel-queue-production-yavaslama/) kural burada da geçerli: varsayılan 30 saniye değil, 5. Bir process'in bir bağımlılığı ne kadar süre bekleyeceğini şansa bırakmayın — bu sayı bir karar olmalı.\n\n## İkinci savunma: retry — ama dikkatli\n\nGeçici hatalar için retry mantıklıdır, ama körlemesine retry zarar verir. Çöken bir servisi hemen yeniden denemek ona daha çok yük bindirir — buna **retry storm** denir; servisi tam ayağa kalkamadan tekrar devirir.\n\nRetry yapıyorsanız iki şey şart: exponential backoff (her denemede artan bekleme) ve jitter (rastgele sapma — tüm client'lar aynı anda denemesin).\n\n```php\nHttp::retry(3, function (int $attempt) {\n    // exponential backoff (200, 400, 800 ms) ve üstüne jitter\n    return 200 * (2 ** ($attempt - 1)) + random_int(0, 100);\n}, function ($exception) {\n    return $exception instanceof ConnectionException;\n})->timeout(5)->get($url);\n```\n\nRetry sayısını düşük tutun. Üç denemeden sonra hâlâ hata varsa, sorun geçici değildir; denemeye devam etmek yalnızca kendi process'inizi daha uzun bağlar.\n\n## Üçüncü savunma: circuit breaker\n\nTimeout tek bir çağrıyı sınırlar; circuit breaker **çağrı yapmayı bırakmaya** karar verir. Mantığı bir elektrik sigortasından gelir ve üç durumu vardır:\n\n- **Closed** — her şey normal, çağrılar geçer. Hatalar sayılır.\n- **Open** — son N çağrının çoğu başarısız oldu; devre açılır. Artık dış servise hiç gidilmez, çağrı **anında** hata döner.\n- **Half-open** — bir süre sonra devre tek bir deneme çağrısına izin verir. Başarılıysa `closed`'a döner, değilse tekrar `open`.\n\nKazanç şu: dış servis hastayken siz onu beklemezsiniz. 5 saniyelik timeout bile process başına 5 saniyedir; `open` durumda bu süre 0'a iner. Fail-fast, yavaş-fail'den iyidir.\n\n```php\n// Kavramsal akış — eşik, sayaç ve zamanlayıcıyı olgun bir pakete bırakın.\nif ($breaker->isOpen('sms')) {\n    return $this->queueForLater($message);   // devre açık: hiç deneme yok\n}\n\ntry {\n    $response = Http::timeout(5)->post($smsUrl, $payload);\n    $breaker->recordSuccess('sms');\n} catch (\\Throwable $e) {\n    $breaker->recordFailure('sms');          // eşik aşılırsa devre açılır\n    return $this->queueForLater($message);\n}\n```\n\nBu mantığı sıfırdan yazmak yerine olgun bir paket kullanın — durum saymayı, eşiği ve zamanlayıcıyı doğru kurmak göründüğünden inceliklidir.\n\n## Fallback: devre açıkken ne yapılır?\n\nCircuit breaker'ın değeri, `open` durumda ne yaptığınızla ortaya çıkar. \"Hata döndür\" en zayıf seçenektir. Daha iyileri:\n\n- **Kuyruğa al, sonra gönder.** SMS hemen gitmek zorunda değilse, devre kapanınca işlenmek üzere [kuyruğa](/tr/muhendislik/notlar/senkron-mu-asenkron-mu/) bırakın.\n- **Bayat ama geçerli veriyi sun.** Bir döviz kuru servisi çökmüşse, son bilinen kuru göstermek hiç göstermemekten iyidir.\n- **Özelliği zarifçe kıs.** \"Öneriler şu an yüklenemiyor\" mesajı, koca sayfayı çökertmekten iyidir.\n\nHangi fallback'in doğru olduğu işe bağlıdır — ama \"fallback yok\" bir cevap değildir.\n\n## Ne zaman bu kadarına gerek yok?\n\nHer dış çağrı circuit breaker istemez. Timeout **her zaman** şart. Retry, yalnızca işlem idempotent ise. Circuit breaker ise asıl olarak çağrı sık ve servis kullanıcı yolundayken değer katar — günde birkaç kez çağrılan bir entegrasyon için timeout ve makul bir retry çoğu zaman yeter.\n\n---\n\nDış servis çökmesi bir \"ihtimal\" değil, bir \"ne zaman\" sorusudur. Onunla birlikte çökmek ise bir tasarım tercihidir — ve değiştirilebilir bir tercih.\n\nSisteminizi, en zayıf bağımlılığınız kadar kırılgan bırakmayın.\n\n## Kaynaklar\n\n- [Release It! Second Edition: Timeouts, Circuit Breaker, Bulkheads, Cascading Failures](https://pragprog.com/titles/mnee2/release-it-second-edition/) — Michael T. Nygard, Pragmatic Bookshelf\n- [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) — AWS Architecture Blog\n- [HTTP Client: timeout, connectTimeout and retry](https://laravel.com/docs/12.x/http-client) — Laravel\n\n## Sık sorulanlar\n\n### Tek bir yavaş dış servis bütün uygulamayı nasıl düşürür?\n\nYavaş çağrı, onu yapan worker'ı ya da PHP-FPM process'ini kilitler — 30 saniyelik bir çağrı, o process'in 30 saniye boyunca başka hiçbir şeye bakmaması demektir. İstekler birikir, process havuzu dolar ve havuz dolunca o servisle hiç ilgisi olmayan istekler de bekler. Cascading failure neredeyse her zaman bir çökmeyle değil, bir yavaşlamayla başlar.\n\n### Her dış çağrı circuit breaker ister mi?\n\nİstemez. Timeout her zaman şarttır. Retry yalnızca işlem idempotent ise ve yalnızca exponential backoff ile jitter eşliğinde yapılır. Circuit breaker ise asıl olarak çağrı sık ve servis kullanıcı yolundayken değer katar; günde birkaç kez çağrılan bir entegrasyon için timeout ve makul bir retry çoğu zaman yeter.",
      "date_published": "2026-07-18T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "resilience",
        "architecture",
        "api",
        "production"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/serverless-gecis-karari/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/serverless-gecis-karari/",
      "title": "Serverless'a Geçiş Kararı: Cold-Start, Vendor Lock-in",
      "summary": "Serverless bir mimari modası değil, bir iş yükü profili için bir araçtır: değişken, seyrek, patlamalı. Sabit trafik, kullanım başına ödemeyi her saniye ödemeye çevirir; düşük trafikli bir yolda ise cold-start istisna değil kuraldır ve kullanıcı onu neredeyse her istekte öder. Asıl bağ runtime değil, etrafındaki yapıştırıcıdır — event kaynakları, IAM, gateway — ve çıkışı bir taşımaya değil, yeniden yazıma çevirir.",
      "content_html": "<p>Bir ekip yeni servisini “serverless first” kararıyla AWS Lambda üzerine kurdu. Oysa trafik sabit ve öngörülebilirdi — gün boyu aşağı yukarı aynı. Ay sonunda <span data-scheduled=\"/tr/muhendislik/notlar/bulut-maliyet-optimizasyonu/\">fatura</span>, aynı işi yapan tek bir VPS’in birkaç katı çıktı; üstelik p95 latency de daha kötüydü.</p>\n<p>Serverless yanlış bir teknoloji değil. Yanlış iş yüküne uygulanmış bir teknolojiydi.</p>\n<h2 id=\"serverless-ne-satın-alır\">Serverless ne satın alır</h2>\n<p>FaaS’ın (Function as a Service) gerçek getirileri var:</p>\n<ul>\n<li><strong>Sunucu yönetimi yok.</strong> Yama, ölçekleme, kapasite planı sağlayıcıda.</li>\n<li><strong>Sıfıra inen ölçek.</strong> İstek yoksa çalışan hiçbir şey yok, ödenen hiçbir şey yok.</li>\n<li><strong>Kullanım başına ödeme.</strong> Atıl kapasite için para ödemezsiniz.</li>\n</ul>\n<p>Bu üçü, doğru iş yükünde gerçekten kazandırır. Mesele, o iş yükünü doğru teşhis etmek.</p>\n<h2 id=\"cold-startın-gerçek-maliyeti\">Cold-start’ın gerçek maliyeti</h2>\n<p>Bir fonksiyon bir süredir çağrılmadıysa, sağlayıcı onu sıfırdan ayağa kaldırır: runtime yüklenir, bağımlılıklar init olur, kod çalışır. Bu <strong>cold-start</strong>, çalışan bir process’e göre 100 ms’nin altından saniyelere kadar ek gecikmedir.</p>\n<p>Önemli ayrım: cold-start, düşük trafikli bir endpoint için <strong>istisna değil kural</strong>dır. Saatte birkaç çağrı alan bir fonksiyonda neredeyse her istek cold’dur. “Ölçeğe göre ödeme” cazip gelir, ama sıfıra yakın trafikte ödediğiniz şey, kullanıcının her seferinde gördüğü gecikmedir. Latency’ye duyarlı bir kullanıcı yolunda bu doğrudan bir UX maliyetidir.</p>\n<h2 id=\"vendor-lock-in\">Vendor lock-in</h2>\n<p>Lock-in yalnızca runtime değildir; asıl bağ etrafındaki yapıştırıcıdadır. Bir serverless uygulaması zamanla şunlara da bağlanır: sağlayıcının event kaynakları, kimlik/yetki modeli (IAM), yönetilen kuyruğu, API gateway’i, dağıtım araçları.</p>\n<p>Tek bir fonksiyonu taşımak kolaydır. Ama gerçek bir sistemde “serverless’tan çıkmak” çoğu zaman bir taşıma değil, bir yeniden yazımdır. Bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/neden-boring-architecture/\">boring architecture</a> yazısındaki innovation token mantığıyla doğrudan ilgili: tek bir karar gibi görünen şey, aslında geri alınması pahalı bir bağdır.</p>\n<h2 id=\"hangi-iş-yükünde-kazandırır\">Hangi iş yükünde kazandırır?</h2>\n<p>Serverless, yükün <strong>değişken ve seyrek</strong> olduğu yerde parlar:</p>\n<ul>\n<li>Zamanlanmış işler — gecelik raporlar, cron benzeri görevler.</li>\n<li>Webhook alıcıları — ne zaman, ne sıklıkta geleceği belirsiz.</li>\n<li>Düşük hacimli iç araçlar — günde birkaç kez kullanılan paneller.</li>\n<li>Ani patlamalı (bursty) batch işleri — saatte bir, kısa süreliğine yüksek paralellik.</li>\n</ul>\n<p>Bu yüklerde sıfıra inen ölçek gerçek para tasarrufudur ve cold-start çoğu zaman önemsizdir — kimse bir gece raporunun 800 ms geç başlamasını dert etmez.</p>\n<h2 id=\"hangi-iş-yükünde-kaybettirir\">Hangi iş yükünde kaybettirir?</h2>\n<p>Bunun tersi de net:</p>\n<ul>\n<li><strong><span data-scheduled=\"/tr/muhendislik/denemeler/on-premise-mi-cloud-mu/\">Sabit, sürekli trafik</span>.</strong> Her zaman çalışan bir API’de “kullanım başına ödeme”, “her saniye ödeme”ye dönüşür — ve genelde sürekli çalışan bir sunucudan pahalıdır.</li>\n<li><strong>Latency’ye duyarlı yollar.</strong> Cold-start, kullanıcının beklediği bir istekte kabul edilemez.</li>\n<li><strong>Uzun süren işler.</strong> FaaS’ın çalışma süresi sınırları vardır; uzun işler ya bölünür ya da uymaz.</li>\n</ul>\n<p>Bu profildeki bir iş yükü için sıkıcı bir VPS — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/tek-vps-coklu-proje-mimarisi/\">tek VPS üzerinde çoklu proje</a> yazısındaki kurulum gibi — hem daha ucuz hem daha öngörülebilirdir. Sabit yükün maliyeti, sabit bir sunucuyla eşleşir.</p>\n<h2 id=\"laravel-ve-serverless\">Laravel ve serverless</h2>\n<p>Laravel tarafında Laravel Vapor, uygulamayı Lambda üzerinde çalıştırmayı mümkün kılar ve iyi de çalışır. Ama çalışması, doğru karar olduğu anlamına gelmez: Vapor’la birlikte cold-start’ı ve AWS bağını da miras alırsınız.</p>\n<p>Karar yine iş yüküdür. Trafiği patlamalı, seyrek ya da öngörülemez bir Laravel uygulaması için Vapor mantıklı. Sürekli ve latency’ye duyarlı bir uygulama için, klasik bir sunucu hâlâ daha sade ve ucuz cevaptır.</p>\n<blockquote><p><strong>Tan:</strong> Hikâyedeki asıl hata Lambda'da değil, \"first\" kelimesindeydi: karar, iş yükü görülmeden verilmişti. Bir ilkeyi varsayılan yapmak, her yeni servis için sorulması gereken soruyu sizin yerinize cevaplamaktır. Varsayılanınız olsun, ama her servis için gerekçesini yeniden yazın.</p></blockquote>\n<h2 id=\"karışık-kullanın\">Karışık kullanın</h2>\n<p>Bu, ikili bir seçim değil. Sürekli yükü taşıyan sıkıcı bir sunucu + yalnızca seyrek/patlamalı işleri (webhook, gece raporu) serverless’a vermek çoğu zaman en dürüst dağılımdır. Her iş yükünü, kendi maliyet profiline ait olduğu yere koyun.</p>\n<hr/>\n<p>Serverless bir mimari modası değil, belirli bir iş yükü profili için bir araçtır. O profil sizde varsa kazandırır; yoksa cold-start ve lock-in olarak geri ödenir.</p>\n<p>Önce yükünüzü ölçün; araç sonra gelir.</p>",
      "content_text": "> **Kısaca:** Serverless bir mimari modası değil, bir iş yükü profili için bir araçtır: değişken, seyrek, patlamalı. Sabit trafik, kullanım başına ödemeyi her saniye ödemeye çevirir; düşük trafikli bir yolda ise cold-start istisna değil kuraldır ve kullanıcı onu neredeyse her istekte öder. Asıl bağ runtime değil, etrafındaki yapıştırıcıdır — event kaynakları, IAM, gateway — ve çıkışı bir taşımaya değil, yeniden yazıma çevirir.\n\nBir ekip yeni servisini \"serverless first\" kararıyla AWS Lambda üzerine kurdu. Oysa trafik sabit ve öngörülebilirdi — gün boyu aşağı yukarı aynı. Ay sonunda fatura, aynı işi yapan tek bir VPS'in birkaç katı çıktı; üstelik p95 latency de daha kötüydü.\n\nServerless yanlış bir teknoloji değil. Yanlış iş yüküne uygulanmış bir teknolojiydi.\n\n## Serverless ne satın alır\n\nFaaS'ın (Function as a Service) gerçek getirileri var:\n\n- **Sunucu yönetimi yok.** Yama, ölçekleme, kapasite planı sağlayıcıda.\n- **Sıfıra inen ölçek.** İstek yoksa çalışan hiçbir şey yok, ödenen hiçbir şey yok.\n- **Kullanım başına ödeme.** Atıl kapasite için para ödemezsiniz.\n\nBu üçü, doğru iş yükünde gerçekten kazandırır. Mesele, o iş yükünü doğru teşhis etmek.\n\n## Cold-start'ın gerçek maliyeti\n\nBir fonksiyon bir süredir çağrılmadıysa, sağlayıcı onu sıfırdan ayağa kaldırır: runtime yüklenir, bağımlılıklar init olur, kod çalışır. Bu **cold-start**, çalışan bir process'e göre 100 ms'nin altından saniyelere kadar ek gecikmedir.\n\nÖnemli ayrım: cold-start, düşük trafikli bir endpoint için **istisna değil kural**dır. Saatte birkaç çağrı alan bir fonksiyonda neredeyse her istek cold'dur. \"Ölçeğe göre ödeme\" cazip gelir, ama sıfıra yakın trafikte ödediğiniz şey, kullanıcının her seferinde gördüğü gecikmedir. Latency'ye duyarlı bir kullanıcı yolunda bu doğrudan bir UX maliyetidir.\n\n## Vendor lock-in\n\nLock-in yalnızca runtime değildir; asıl bağ etrafındaki yapıştırıcıdadır. Bir serverless uygulaması zamanla şunlara da bağlanır: sağlayıcının event kaynakları, kimlik/yetki modeli (IAM), yönetilen kuyruğu, API gateway'i, dağıtım araçları.\n\nTek bir fonksiyonu taşımak kolaydır. Ama gerçek bir sistemde \"serverless'tan çıkmak\" çoğu zaman bir taşıma değil, bir yeniden yazımdır. Bu, [boring architecture](/tr/muhendislik/denemeler/neden-boring-architecture/) yazısındaki innovation token mantığıyla doğrudan ilgili: tek bir karar gibi görünen şey, aslında geri alınması pahalı bir bağdır.\n\n## Hangi iş yükünde kazandırır?\n\nServerless, yükün **değişken ve seyrek** olduğu yerde parlar:\n\n- Zamanlanmış işler — gecelik raporlar, cron benzeri görevler.\n- Webhook alıcıları — ne zaman, ne sıklıkta geleceği belirsiz.\n- Düşük hacimli iç araçlar — günde birkaç kez kullanılan paneller.\n- Ani patlamalı (bursty) batch işleri — saatte bir, kısa süreliğine yüksek paralellik.\n\nBu yüklerde sıfıra inen ölçek gerçek para tasarrufudur ve cold-start çoğu zaman önemsizdir — kimse bir gece raporunun 800 ms geç başlamasını dert etmez.\n\n## Hangi iş yükünde kaybettirir?\n\nBunun tersi de net:\n\n- **Sabit, sürekli trafik.** Her zaman çalışan bir API'de \"kullanım başına ödeme\", \"her saniye ödeme\"ye dönüşür — ve genelde sürekli çalışan bir sunucudan pahalıdır.\n- **Latency'ye duyarlı yollar.** Cold-start, kullanıcının beklediği bir istekte kabul edilemez.\n- **Uzun süren işler.** FaaS'ın çalışma süresi sınırları vardır; uzun işler ya bölünür ya da uymaz.\n\nBu profildeki bir iş yükü için sıkıcı bir VPS — [tek VPS üzerinde çoklu proje](/tr/muhendislik/sistemler/tek-vps-coklu-proje-mimarisi/) yazısındaki kurulum gibi — hem daha ucuz hem daha öngörülebilirdir. Sabit yükün maliyeti, sabit bir sunucuyla eşleşir.\n\n## Laravel ve serverless\n\nLaravel tarafında Laravel Vapor, uygulamayı Lambda üzerinde çalıştırmayı mümkün kılar ve iyi de çalışır. Ama çalışması, doğru karar olduğu anlamına gelmez: Vapor'la birlikte cold-start'ı ve AWS bağını da miras alırsınız.\n\nKarar yine iş yüküdür. Trafiği patlamalı, seyrek ya da öngörülemez bir Laravel uygulaması için Vapor mantıklı. Sürekli ve latency'ye duyarlı bir uygulama için, klasik bir sunucu hâlâ daha sade ve ucuz cevaptır.\n\n\n> **Tan:** Hikâyedeki asıl hata Lambda'da değil, \"first\" kelimesindeydi: karar, iş yükü görülmeden verilmişti. Bir ilkeyi varsayılan yapmak, her yeni servis için sorulması gereken soruyu sizin yerinize cevaplamaktır. Varsayılanınız olsun, ama her servis için gerekçesini yeniden yazın.\n\n\n## Karışık kullanın\n\nBu, ikili bir seçim değil. Sürekli yükü taşıyan sıkıcı bir sunucu + yalnızca seyrek/patlamalı işleri (webhook, gece raporu) serverless'a vermek çoğu zaman en dürüst dağılımdır. Her iş yükünü, kendi maliyet profiline ait olduğu yere koyun.\n\n---\n\nServerless bir mimari modası değil, belirli bir iş yükü profili için bir araçtır. O profil sizde varsa kazandırır; yoksa cold-start ve lock-in olarak geri ödenir.\n\nÖnce yükünüzü ölçün; araç sonra gelir.\n\n## Kaynaklar\n\n- [Understanding the Lambda execution environment lifecycle: cold starts and latency](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html) — AWS\n- [Lambda quotas: function timeout is 900 seconds](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) — AWS\n\n## Sık sorulanlar\n\n### Serverless hangi iş yüklerinde gerçekten kazandırır?\n\nYükün değişken ve seyrek olduğu yerlerde: zamanlanmış işler, ne zaman geleceği belirsiz webhook alıcıları, düşük hacimli iç araçlar ve patlamalı batch işleri. Oralarda sıfıra inen ölçek gerçek para tasarrufudur ve cold-start çoğu zaman önemsizdir — kimse gece raporunun 800 ms geç başlamasını dert etmez.\n\n### Cold-start düşük trafikli bir endpoint için neden daha kötüdür?\n\nÇünkü orada istisna değil, kuraldır. Saatte birkaç çağrı alan bir fonksiyonda neredeyse her istek cold'dur; sıfıra yakın trafikte ödediğiniz şey, kullanıcının her seferinde gördüğü gecikmedir. Latency'ye duyarlı bir kullanıcı yolunda bu doğrudan bir UX maliyetidir.",
      "date_published": "2026-07-11T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "serverless",
        "architecture",
        "cloud"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/bff-backend-for-frontend/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/bff-backend-for-frontend/",
      "title": "BFF Pattern: Mobil ve Web İçin Ayrı API Katmanı",
      "summary": "Tek bir genel API'yi her istemciye uydurmanın bedeli over-fetching, chatty akış, koşullu alanlar ve web ile mobili tek kontrata kilitleyen bir versiyonlama düğümüdür. BFF bu faturayı istemci başına ince bir uyarlama katmanıyla keser — agregasyon ve yanıt şekillendirme, asla iş kuralı değil. Katmanı haklı çıkaran şey istemci sayısı değil, istemciler arasındaki mesafedir.",
      "content_html": "<p>Bir mobil uygulamanın tek bir ana ekranı çizmek için API’ye dokuz ayrı istek attığını gördüm. API aslında web için tasarlanmıştı; mobil, web’in böldüğü veriyi parça parça toplayıp ekranda birleştiriyordu. Her açılış dokuz round-trip — üstelik mobil ağ koşullarında.</p>\n<p>Bu, “tek API herkese yeter” varsayımının faturasıydı. BFF — Backend for Frontend — tam da bu faturayı kesmek için var.</p>\n<h2 id=\"bff-nedir\">BFF nedir</h2>\n<p>BFF, her istemci türü için ince ve ona özel bir API katmanıdır. Web’in kendi BFF’i, mobilin kendi BFF’i olur. Her biri arkadaki asıl servisleri/domain’i çağırır, ama yanıtı kendi istemcisinin ekranına göre şekillendirir.</p>\n<p>Asıl iş mantığı BFF’te değildir. BFF sadece bir <strong>uyarlama katmanı</strong>: agregasyon yapar, gereksiz alanı kırpar, istemcinin tek ekranda ihtiyaç duyduğu şeyi tek yanıtta toplar.</p>\n<h2 id=\"tek-apiyi-herkese-uydurmanın-gizli-maliyeti\">Tek API’yi herkese uydurmanın gizli maliyeti</h2>\n<p>Tek bir genel API, zamanla iki istemciyi de tam memnun edemez:</p>\n<ul>\n<li><strong>Over-fetching.</strong> Mobil, 40 alanlı bir kullanıcı nesnesi indirir, 4’ünü kullanır. Geri kalanı boşa giden bant genişliği ve pil.</li>\n<li><strong>Chatty akış.</strong> Web’in normal bulduğu “önce listeyi çek, sonra her satır için detay çek” akışı, mobil ağda dokuz round-trip’e dönüşür.</li>\n<li><strong>Koşullu alanlar.</strong> Yanıt <code>if mobile then ... else ...</code> ile dallanmaya başlar. Tek endpoint, iki istemcinin gereksinimini aynı anda taşımaya çalışır ve ikisinde de orta karar bir iş çıkarır.</li>\n<li><strong>Versiyonlama düğümü.</strong> Web için yapılması gereken bir değişiklik, mobilin yayınlanmış sürümünü kırma riski taşır. İki istemci tek kontrata kilitlenir.</li>\n</ul>\n<p>Bu maliyetler tek tek küçük görünür; toplamı bir mimari problemdir.</p>\n<h2 id=\"bff-ne-zaman-gerekli\">BFF ne zaman gerekli?</h2>\n<p>BFF, istemcilerin ihtiyacı <strong>gerçekten</strong> ayrıştığında gerekçelidir:</p>\n<ul>\n<li>Mobil ile web belirgin biçimde farklı ekranlara, farklı veri şekillerine ihtiyaç duyuyor.</li>\n<li>Mobil ağ ve pil kısıtları, round-trip sayısını ve payload boyutunu birinci sınıf bir kaygı yapıyor.</li>\n<li>İstemciler farklı tempoda yayınlanıyor — mobil sürümü mağaza onayına takılıyken web her gün deploy ediliyor; tek kontrat ikisini birbirine kilitliyor.</li>\n</ul>\n<p>Bu farklar varsa, her istemciye kendi BFF’ini vermek, tek API’yi sürekli ikisine birden uydurmaya çalışmaktan ucuzdur.</p>\n<h2 id=\"bff-ne-zaman-gereksiz\">BFF ne zaman gereksiz?</h2>\n<p>Tek bir istemciniz varsa BFF, çözdüğünden fazla katman ekler — sadece fazladan bir sıçrama.</p>\n<p>İki istemciniz var ama ikisi de neredeyse aynı veriyi aynı şekilde istiyorsa, yine gereksiz. BFF’i haklı çıkaran şey istemci sayısı değil, istemciler <strong>arasındaki farktır</strong>. Fark yoksa ayrı katman da olmamalı — bu, <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">spekülatif genelliğin</a> API katmanındaki hâli olur.</p>\n<h2 id=\"bff-mikroservis-değildir\">BFF mikroservis değildir</h2>\n<p>Sık karışan nokta: BFF eklemek sizi dağıtık mimariye sokmaz. BFF bir sunum/agregasyon katmanıdır, bir domain servisi değil. İçine iş kuralı koymak, kuralı istemci sayısı kadar kopyalamak demektir.</p>\n<p>BFF’in mikroservisle tek ilişkisi şu: ikisi de “ayrı bir deployable’a değer mi?” sorusunu sorar. Cevap çoğu zaman “hayır” — <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/\">mikroservise geçiş kararındaki</a> ölçülmüş-sinyal eşiği burada da geçerli.</p>\n<h2 id=\"hafif-başlayın\">Hafif başlayın</h2>\n<p>“BFF” demek, ayrı bir sunucu kurmak demek zorunda değil. Aynı monolit içinde:</p>\n<ul>\n<li>İstemci başına bir route namespace — <code>routes/web-api.php</code>, <code>routes/mobile-api.php</code> — her biri kendi controller’larıyla, kendi yanıt şekliyle.</li>\n<li>Ya da tek bir GraphQL katmanı: her istemci ihtiyacı kadarını sorgular, over-fetching kendiliğinden çözülür.</li>\n</ul>\n<p>Asıl fikir bir kutu değil, bir sınırdır: istemciye özel uyarlamayı tek bir yerde toplayın, domain’i ondan temiz tutun. Ayrı bir deployable’a ancak <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/\">ölçülmüş bir sinyal</a> zorladığında geçin.</p>\n<hr/>\n<p>BFF, “her istemci aynı API’yle idare etsin” inadının panzehiridir — ama yalnızca istemciler gerçekten ayrıştığında. Fark yoksa katman da olmamalı.</p>\n<p>Katmanı ekleyen şey istemci sayısı değil, aralarındaki mesafedir.</p>",
      "content_text": "> **Kısaca:** Tek bir genel API'yi her istemciye uydurmanın bedeli over-fetching, chatty akış, koşullu alanlar ve web ile mobili tek kontrata kilitleyen bir versiyonlama düğümüdür. BFF bu faturayı istemci başına ince bir uyarlama katmanıyla keser — agregasyon ve yanıt şekillendirme, asla iş kuralı değil. Katmanı haklı çıkaran şey istemci sayısı değil, istemciler arasındaki mesafedir.\n\nBir mobil uygulamanın tek bir ana ekranı çizmek için API'ye dokuz ayrı istek attığını gördüm. API aslında web için tasarlanmıştı; mobil, web'in böldüğü veriyi parça parça toplayıp ekranda birleştiriyordu. Her açılış dokuz round-trip — üstelik mobil ağ koşullarında.\n\nBu, \"tek API herkese yeter\" varsayımının faturasıydı. BFF — Backend for Frontend — tam da bu faturayı kesmek için var.\n\n## BFF nedir\n\nBFF, her istemci türü için ince ve ona özel bir API katmanıdır. Web'in kendi BFF'i, mobilin kendi BFF'i olur. Her biri arkadaki asıl servisleri/domain'i çağırır, ama yanıtı kendi istemcisinin ekranına göre şekillendirir.\n\nAsıl iş mantığı BFF'te değildir. BFF sadece bir **uyarlama katmanı**: agregasyon yapar, gereksiz alanı kırpar, istemcinin tek ekranda ihtiyaç duyduğu şeyi tek yanıtta toplar.\n\n## Tek API'yi herkese uydurmanın gizli maliyeti\n\nTek bir genel API, zamanla iki istemciyi de tam memnun edemez:\n\n- **Over-fetching.** Mobil, 40 alanlı bir kullanıcı nesnesi indirir, 4'ünü kullanır. Geri kalanı boşa giden bant genişliği ve pil.\n- **Chatty akış.** Web'in normal bulduğu \"önce listeyi çek, sonra her satır için detay çek\" akışı, mobil ağda dokuz round-trip'e dönüşür.\n- **Koşullu alanlar.** Yanıt `if mobile then ... else ...` ile dallanmaya başlar. Tek endpoint, iki istemcinin gereksinimini aynı anda taşımaya çalışır ve ikisinde de orta karar bir iş çıkarır.\n- **Versiyonlama düğümü.** Web için yapılması gereken bir değişiklik, mobilin yayınlanmış sürümünü kırma riski taşır. İki istemci tek kontrata kilitlenir.\n\nBu maliyetler tek tek küçük görünür; toplamı bir mimari problemdir.\n\n## BFF ne zaman gerekli?\n\nBFF, istemcilerin ihtiyacı **gerçekten** ayrıştığında gerekçelidir:\n\n- Mobil ile web belirgin biçimde farklı ekranlara, farklı veri şekillerine ihtiyaç duyuyor.\n- Mobil ağ ve pil kısıtları, round-trip sayısını ve payload boyutunu birinci sınıf bir kaygı yapıyor.\n- İstemciler farklı tempoda yayınlanıyor — mobil sürümü mağaza onayına takılıyken web her gün deploy ediliyor; tek kontrat ikisini birbirine kilitliyor.\n\nBu farklar varsa, her istemciye kendi BFF'ini vermek, tek API'yi sürekli ikisine birden uydurmaya çalışmaktan ucuzdur.\n\n## BFF ne zaman gereksiz?\n\nTek bir istemciniz varsa BFF, çözdüğünden fazla katman ekler — sadece fazladan bir sıçrama.\n\nİki istemciniz var ama ikisi de neredeyse aynı veriyi aynı şekilde istiyorsa, yine gereksiz. BFF'i haklı çıkaran şey istemci sayısı değil, istemciler **arasındaki farktır**. Fark yoksa ayrı katman da olmamalı — bu, [spekülatif genelliğin](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) API katmanındaki hâli olur.\n\n## BFF mikroservis değildir\n\nSık karışan nokta: BFF eklemek sizi dağıtık mimariye sokmaz. BFF bir sunum/agregasyon katmanıdır, bir domain servisi değil. İçine iş kuralı koymak, kuralı istemci sayısı kadar kopyalamak demektir.\n\nBFF'in mikroservisle tek ilişkisi şu: ikisi de \"ayrı bir deployable'a değer mi?\" sorusunu sorar. Cevap çoğu zaman \"hayır\" — [mikroservise geçiş kararındaki](/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/) ölçülmüş-sinyal eşiği burada da geçerli.\n\n## Hafif başlayın\n\n\"BFF\" demek, ayrı bir sunucu kurmak demek zorunda değil. Aynı monolit içinde:\n\n- İstemci başına bir route namespace — `routes/web-api.php`, `routes/mobile-api.php` — her biri kendi controller'larıyla, kendi yanıt şekliyle.\n- Ya da tek bir GraphQL katmanı: her istemci ihtiyacı kadarını sorgular, over-fetching kendiliğinden çözülür.\n\nAsıl fikir bir kutu değil, bir sınırdır: istemciye özel uyarlamayı tek bir yerde toplayın, domain'i ondan temiz tutun. Ayrı bir deployable'a ancak [ölçülmüş bir sinyal](/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/) zorladığında geçin.\n\n---\n\nBFF, \"her istemci aynı API'yle idare etsin\" inadının panzehiridir — ama yalnızca istemciler gerçekten ayrıştığında. Fark yoksa katman da olmamalı.\n\nKatmanı ekleyen şey istemci sayısı değil, aralarındaki mesafedir.\n\n## Kaynaklar\n\n- [Pattern: Backends For Frontends](https://samnewman.io/patterns/architectural/bff/) — Sam Newman\n- [Backends for Frontends pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends) — Microsoft Azure Architecture Center\n\n## Sık sorulanlar\n\n### BFF ne zaman gereksizdir?\n\nTek bir istemci varsa BFF çözdüğünden fazla katman ekler — sadece fazladan bir sıçrama. İki istemci varsa ama ikisi de neredeyse aynı veriyi aynı şekilde istiyorsa yine gereksizdir: BFF'i haklı çıkaran şey istemci sayısı değil, istemciler arasındaki farktır.\n\n### BFF bir mikroservis midir?\n\nDeğildir. BFF bir sunum ve agregasyon katmanıdır, bir domain servisi değil; eklemek sizi dağıtık mimariye sokmaz. İçine iş kuralı koymak, kuralı istemci sayısı kadar kopyalamak demektir. Mikroservisle tek ortak sorusu şudur: bu, ayrı bir deployable'a değer mi?",
      "date_published": "2026-07-04T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "architecture",
        "api",
        "bff"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/senkron-mu-asenkron-mu/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/senkron-mu-asenkron-mu/",
      "title": "Senkron mu Asenkron mu? HTTP ve Kuyruk Arasında Karar",
      "summary": "Senkron mu asenkron mu, bir performans sorusu değil, kim bekliyor sorusudur. Kullanıcı sonucu görmek zorundaysa iş istek içinde kalır; değilse kuyruğa gider ve istek 202 Accepted döner. Aynı çizgiyi hata keskinleştirir: senkron hata bir hata ekranıdır, asenkron hata bir retry'dır — kararsız bir üçüncü partinin senkron yolda durmamasının sebebi tam olarak budur.",
      "content_html": "<p>Bir kayıt formu sekiz saniye sürüyordu. İstek içinde sırayla şunlar oluyordu: kullanıcı kaydediliyor, hoş geldin e-postası gönderiliyor, bir PDF üretiliyor, bir CRM’e kayıt işleniyor. Kullanıcı bu sekiz saniye boyunca dönen bir spinner’a bakıyordu — oysa onu ilgilendiren tek şey, kaydının oluşup oluşmadığıydı.</p>\n<p>Bir işi HTTP isteği içinde anında mı yapmalı, kuyruğa mı atmalı? Çizgi göründüğünden net.</p>\n<h2 id=\"karar-çizgisi-kullanıcı-sonucu-bekliyor-mu\">Karar çizgisi: kullanıcı sonucu bekliyor mu?</h2>\n<p>Tek bir soru kararın çoğunu verir: <strong>kullanıcı bu işin sonucunu görmek zorunda mı?</strong></p>\n<ul>\n<li>Cevap “evet” ise iş senkron kalır — istek içinde, yanıt dönmeden biter.</li>\n<li>Cevap “hayır” ise iş kuyruğa gider — istek “kabul edildi” der ve döner.</li>\n</ul>\n<p>Yukarıdaki formda kullanıcı sadece “kaydım oluştu mu” cevabını bekliyor. E-posta, PDF, CRM — hiçbirinin sonucunu o ekranda görmesi gerekmiyor. Üçü de yanlış tarafta duruyordu.</p>\n<h2 id=\"hangi-işler-senkron-kalmalı\">Hangi işler senkron kalmalı?</h2>\n<p>Kullanıcının sonucunu doğrudan gördüğü işler istek içinde kalır:</p>\n<ul>\n<li>Girdi doğrulama — hata anında dönmeli.</li>\n<li>Asıl kaynağın yaratılması — kullanıcı oluşturma, sipariş oluşturma.</li>\n<li>Ödeme <strong>yetkilendirmesinin sonucu</strong> — kart geçti mi geçmedi mi, kullanıcı bunu o an bilmeli.</li>\n<li>Kullanıcının bir sonraki adımda göreceği veri.</li>\n</ul>\n<p>Bunları kuyruğa atmak, kullanıcıya “sonra bak” demektir — ve çoğu zaman kötü bir deneyimdir.</p>\n<h2 id=\"hangi-işler-kuyruğa-gitmeli\">Hangi işler kuyruğa gitmeli?</h2>\n<p>Kullanıcının sonucunu o an görmesi gerekmeyen her iş asenkrondur:</p>\n<ul>\n<li>E-posta ve bildirim.</li>\n<li>PDF ve rapor üretimi.</li>\n<li>Webhook gönderimi.</li>\n<li>Üçüncü parti senkronizasyonu — CRM, analitik, arama indeksi.</li>\n<li>Görsel işleme, dışa aktarma.</li>\n</ul>\n<p>Bunlar için doğru yanıt <code>202 Accepted</code>’dir: “isteğini aldım, işliyorum.” Kullanıcı beklemez, sistem işi kendi temposunda yapar.</p>\n<h2 id=\"çizgiyi-yanıt-süresi-keskinleştirir\">Çizgiyi yanıt süresi keskinleştirir</h2>\n<p>Her endpoint için bir p95 bütçeniz olmalı — diyelim 300 ms. Bir adım bu bütçeyi tek başına yiyorsa ve kullanıcı o adımın sonucunu görmek zorunda değilse, o adım kuyruğa gider.</p>\n<p>Senkron zincir, en yavaş halkası kadar hızlıdır. PDF üretimi 4 saniye sürüyorsa, kullanıcı oluşturma da 4 saniye sürer — PDF’in yanıtla hiçbir ilgisi olmasa bile.</p>\n<h2 id=\"hata-toleransı-asıl-ayrım\">Hata toleransı: asıl ayrım</h2>\n<p>Asıl mesele hız değil, hatadır. Senkron ve asenkron, başarısızlığı bambaşka karşılar:</p>\n<ul>\n<li><strong>Senkron hata</strong> = kullanıcı bir hata ekranı görür. İş kaybolur.</li>\n<li><strong>Asenkron hata</strong> = job retry edilir, gerekirse DLQ’ya düşer, <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/laravel-queue-production-yavaslama/\">kuyruk</a> sonra işler.</li>\n</ul>\n<p>Buradan tek bir kural çıkar: <strong>kararsız bir üçüncü parti asla senkron yolda durmamalı.</strong> O CRM çağrısı istek içindeyse, CRM her çöktüğünde sizin kayıt formunuz da çöker. Kuyruğa alınmış bir çağrı ise, CRM döndüğünde retry kendiliğinden tamamlanır. Üçüncü partinin uptime’ı sizin uptime’ınız olmaktan çıkar.</p>\n<h2 id=\"202-ve-durum-sorgusu-kalıbı\">202 ve durum sorgusu kalıbı</h2>\n<p>İş kuyruğa gidince kullanıcıya bir takip yolu bırakın:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>POST /exports          → 202 Accepted  { &quot;id&quot;: &quot;exp_1a2b&quot;, &quot;status&quot;: &quot;processing&quot; }</span></span>\n<span class=\"line\"><span>GET  /exports/exp_1a2b → 200 OK        { &quot;status&quot;: &quot;done&quot;, &quot;url&quot;: &quot;...&quot; }</span></span></code></pre>\n<p>Kullanıcı sonucu beklemiyordur ama bazen merak eder. Bir durum endpoint’i ya da hazır olunca gönderilen bir bildirim, bu boşluğu kapatır.</p>\n<blockquote><p><strong>Tan:</strong> İşi kuyruğa attığınızda hatayı kullanıcının ekranından alıp kimsenin bakmadığı bir yere taşımış olabilirsiniz. Kuyruğa giden her iş için, düşerse kimin göreceği ve kimin yeniden deneyeceği baştan belli olmalı. Bu sorunun cevabı yoksa, senkron yolun hatası en azından görünür bir yerde çıkar.</p></blockquote>\n<h2 id=\"sınır-nerede-kalkar\">Sınır nerede kalkar?</h2>\n<p>Bazı işler ikisinin arasındadır: kullanıcı sonucu görmek ister ama iş de yavaştır. Burada üçüncü bir yol var — işi senkron başlatın, ilk anlamlı sonucu hemen döndürün, gerisini kuyruğa devredin. Sipariş kaydını anında oluşturun (senkron), faturayı arkada üretin (asenkron).</p>\n<p>Çizgiyi her iş için ayrı çizin; “her şey kuyruğa” da “her şey senkron” kadar yanlıştır.</p>\n<hr/>\n<p>Senkron mu asenkron mu sorusu bir performans sorusu değil, bir “kim bekliyor” sorusudur. Kullanıcının beklemediği hiçbir iş, onun isteğini bekletmemeli.</p>\n<p>İsteği yalnızca, cevabını gerçekten istediği işle meşgul edin.</p>",
      "content_text": "> **Kısaca:** Senkron mu asenkron mu, bir performans sorusu değil, kim bekliyor sorusudur. Kullanıcı sonucu görmek zorundaysa iş istek içinde kalır; değilse kuyruğa gider ve istek 202 Accepted döner. Aynı çizgiyi hata keskinleştirir: senkron hata bir hata ekranıdır, asenkron hata bir retry'dır — kararsız bir üçüncü partinin senkron yolda durmamasının sebebi tam olarak budur.\n\nBir kayıt formu sekiz saniye sürüyordu. İstek içinde sırayla şunlar oluyordu: kullanıcı kaydediliyor, hoş geldin e-postası gönderiliyor, bir PDF üretiliyor, bir CRM'e kayıt işleniyor. Kullanıcı bu sekiz saniye boyunca dönen bir spinner'a bakıyordu — oysa onu ilgilendiren tek şey, kaydının oluşup oluşmadığıydı.\n\nBir işi HTTP isteği içinde anında mı yapmalı, kuyruğa mı atmalı? Çizgi göründüğünden net.\n\n## Karar çizgisi: kullanıcı sonucu bekliyor mu?\n\nTek bir soru kararın çoğunu verir: **kullanıcı bu işin sonucunu görmek zorunda mı?**\n\n- Cevap \"evet\" ise iş senkron kalır — istek içinde, yanıt dönmeden biter.\n- Cevap \"hayır\" ise iş kuyruğa gider — istek \"kabul edildi\" der ve döner.\n\nYukarıdaki formda kullanıcı sadece \"kaydım oluştu mu\" cevabını bekliyor. E-posta, PDF, CRM — hiçbirinin sonucunu o ekranda görmesi gerekmiyor. Üçü de yanlış tarafta duruyordu.\n\n## Hangi işler senkron kalmalı?\n\nKullanıcının sonucunu doğrudan gördüğü işler istek içinde kalır:\n\n- Girdi doğrulama — hata anında dönmeli.\n- Asıl kaynağın yaratılması — kullanıcı oluşturma, sipariş oluşturma.\n- Ödeme **yetkilendirmesinin sonucu** — kart geçti mi geçmedi mi, kullanıcı bunu o an bilmeli.\n- Kullanıcının bir sonraki adımda göreceği veri.\n\nBunları kuyruğa atmak, kullanıcıya \"sonra bak\" demektir — ve çoğu zaman kötü bir deneyimdir.\n\n## Hangi işler kuyruğa gitmeli?\n\nKullanıcının sonucunu o an görmesi gerekmeyen her iş asenkrondur:\n\n- E-posta ve bildirim.\n- PDF ve rapor üretimi.\n- Webhook gönderimi.\n- Üçüncü parti senkronizasyonu — CRM, analitik, arama indeksi.\n- Görsel işleme, dışa aktarma.\n\nBunlar için doğru yanıt `202 Accepted`'dir: \"isteğini aldım, işliyorum.\" Kullanıcı beklemez, sistem işi kendi temposunda yapar.\n\n## Çizgiyi yanıt süresi keskinleştirir\n\nHer endpoint için bir p95 bütçeniz olmalı — diyelim 300 ms. Bir adım bu bütçeyi tek başına yiyorsa ve kullanıcı o adımın sonucunu görmek zorunda değilse, o adım kuyruğa gider.\n\nSenkron zincir, en yavaş halkası kadar hızlıdır. PDF üretimi 4 saniye sürüyorsa, kullanıcı oluşturma da 4 saniye sürer — PDF'in yanıtla hiçbir ilgisi olmasa bile.\n\n## Hata toleransı: asıl ayrım\n\nAsıl mesele hız değil, hatadır. Senkron ve asenkron, başarısızlığı bambaşka karşılar:\n\n- **Senkron hata** = kullanıcı bir hata ekranı görür. İş kaybolur.\n- **Asenkron hata** = job retry edilir, gerekirse DLQ'ya düşer, [kuyruk](/tr/muhendislik/notlar/laravel-queue-production-yavaslama/) sonra işler.\n\nBuradan tek bir kural çıkar: **kararsız bir üçüncü parti asla senkron yolda durmamalı.** O CRM çağrısı istek içindeyse, CRM her çöktüğünde sizin kayıt formunuz da çöker. Kuyruğa alınmış bir çağrı ise, CRM döndüğünde retry kendiliğinden tamamlanır. Üçüncü partinin uptime'ı sizin uptime'ınız olmaktan çıkar.\n\n## 202 ve durum sorgusu kalıbı\n\nİş kuyruğa gidince kullanıcıya bir takip yolu bırakın:\n\n```\nPOST /exports          → 202 Accepted  { \"id\": \"exp_1a2b\", \"status\": \"processing\" }\nGET  /exports/exp_1a2b → 200 OK        { \"status\": \"done\", \"url\": \"...\" }\n```\n\nKullanıcı sonucu beklemiyordur ama bazen merak eder. Bir durum endpoint'i ya da hazır olunca gönderilen bir bildirim, bu boşluğu kapatır.\n\n\n> **Tan:** İşi kuyruğa attığınızda hatayı kullanıcının ekranından alıp kimsenin bakmadığı bir yere taşımış olabilirsiniz. Kuyruğa giden her iş için, düşerse kimin göreceği ve kimin yeniden deneyeceği baştan belli olmalı. Bu sorunun cevabı yoksa, senkron yolun hatası en azından görünür bir yerde çıkar.\n\n\n## Sınır nerede kalkar?\n\nBazı işler ikisinin arasındadır: kullanıcı sonucu görmek ister ama iş de yavaştır. Burada üçüncü bir yol var — işi senkron başlatın, ilk anlamlı sonucu hemen döndürün, gerisini kuyruğa devredin. Sipariş kaydını anında oluşturun (senkron), faturayı arkada üretin (asenkron).\n\nÇizgiyi her iş için ayrı çizin; \"her şey kuyruğa\" da \"her şey senkron\" kadar yanlıştır.\n\n---\n\nSenkron mu asenkron mu sorusu bir performans sorusu değil, bir \"kim bekliyor\" sorusudur. Kullanıcının beklemediği hiçbir iş, onun isteğini bekletmemeli.\n\nİsteği yalnızca, cevabını gerçekten istediği işle meşgul edin.\n\n## Kaynaklar\n\n- [RFC 9110, 15.3.3: 202 Accepted](https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted) — IETF\n- [Release It! Second Edition: stability patterns and antipatterns](https://pragprog.com/titles/mnee2/release-it-second-edition/) — Michael T. Nygard, Pragmatic Bookshelf\n- [Using dead-letter queues in Amazon SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html) — AWS\n\n## Sık sorulanlar\n\n### Hangi işler HTTP isteğinin içinde kalmalı?\n\nKullanıcının sonucunu doğrudan gördüğü işler: girdi doğrulama, asıl kaynağın yaratılması, ödeme yetkilendirmesinin sonucu ve kullanıcının bir sonraki adımda göreceği veri. Bunları kuyruğa atmak, kullanıcıya sonra bak demektir ve çoğu zaman kötü bir deneyimdir.\n\n### Kararsız bir üçüncü parti neden senkron yolda durmamalı?\n\nÇünkü onun kesintisi sizin kesintiniz olur. İstek içindeki bir CRM çağrısı, CRM her çöktüğünde kayıt formunu da düşürür. Kuyruğa alınmış aynı çağrı ise retry edilir ve CRM döndüğünde kendiliğinden tamamlanır; üçüncü partinin uptime'ı sizin uptime'ınız olmaktan çıkar.",
      "date_published": "2026-06-27T00:00:00.000Z",
      "date_modified": "2026-09-30T00:00:00.000Z",
      "tags": [
        "architecture",
        "queue",
        "async",
        "api"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/sistemler/transactional-outbox-dual-write-ve-idempotent-tuketim/",
      "url": "https://muhammetsafak.com/tr/muhendislik/sistemler/transactional-outbox-dual-write-ve-idempotent-tuketim/",
      "title": "Transactional Outbox: Dual-Write, İdempotent Tüketim",
      "summary": "Outbox dual-write'ı tek write'a indirir ama bedava garanti vermez, at-least-once verir. Relay polling mi CDC mi, SKIP LOCKED ile çoklu relay, ordering, processed_messages budama, exactly-once yanılsaması ve alan-seviyesi şifreleme.",
      "content_html": "<p>Belirti küçüktü: günde bir-iki fatura veritabanında duruyor ama consumer onları hiç görmüyordu. Kullanıcı “kestim” diyor, karşı tarafta hiçbir iz yok. Hep gece, hep bir deploy ya da network dalgalanması anında. Tabloyu netleştirince sebep ortaya çıktı — veritabanına <code>INSERT</code> ile kuyruğa <code>publish</code> iki ayrı sistemdi ve aralarında hiçbir garanti yoktu.</p>\n<p>Bu yazı o boşluğu kapatan <strong>transactional outbox</strong> kalıbının, kapatınca açılan <strong>at-least-once</strong> tekrarının ve son olarak event’in içinden geçen kişisel veriyi <strong>alan seviyesinde</strong> şifrelemenin sistem tarafı. Karar zincirinin günlük tarafını <a href=\"https://muhammetsafak.com/tr/blog/outbox-idempotent-tuketim-ve-alan-sifrelemesi/\">muhammetsafak.com’da ayrı yazdım</a>; burada kalıbın production’da dayanıklı olması için verilmesi gereken kararları topluyorum.</p>\n<h2 id=\"dual-write-bir-transactionı-iki-sisteme-yayamazsınız\">Dual-write: bir transaction’ı iki sisteme yayamazsınız</h2>\n<p>Sorunun özü tek cümle: yerel veritabanı transaction’ınız RabbitMQ’yu kapsamaz. Klasik kod <em>çoğu zaman</em> çalıştığı için tehlikeli:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">transaction</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">use</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($invoice) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $invoice</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">save</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();                 </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// 1) MySQL&#39;e yaz</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    $this</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">publishToRabbit</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($invoice); </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// 2) kuyruğa publish</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">});</span></span></code></pre>\n<p>İki ayrı arıza modu var:</p>\n<ul>\n<li><code>save()</code> başarılı, <code>publish</code> network hatasıyla düşer → DB’de fatura var, event yok. <strong>Kayıp event.</strong></li>\n<li><code>publish</code> başarılı, ardından transaction başka sebeple <code>rollback</code> olur → event gitti, DB’de karşılığı yok. <strong>Hayalet event.</strong></li>\n</ul>\n<p><code>DB::transaction</code> kurtarmaz, çünkü commit/rollback yalnızca MySQL tarafını sarar; broker o transaction’ın parçası değildir. İki sistemi tek atomik adımda tutmanın pratik yolu, yazmayı <strong>tek bir sisteme</strong> indirmek.</p>\n<h2 id=\"outbox-önce-tek-sisteme-yaz-sonra-ayrı-bir-süreç-yayımlasın\">Outbox: önce tek sisteme yaz, sonra ayrı bir süreç yayımlasın</h2>\n<p>Kalıbın fikri sade: mesajı kuyruğa doğrudan basmak yerine, <strong>aynı transaction içinde</strong> bir <code>outbox</code> tablosuna satır olarak yaz. Fatura ile event aynı commit’te ya birlikte var olur ya da birlikte yok olur — dual-write tek write’a iner.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">CREATE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> TABLE</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> outbox</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    id            </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">BINARY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">16</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)   </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- event id = idempotency anahtarı</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    aggregate_id  </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">BIGINT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">       NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- ordering ve partition anahtarı</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    topic         </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">VARCHAR</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">120</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    payload       </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">JSON</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">         NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ENUM(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;pending&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;published&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NOT NULL</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> DEFAULT</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pending&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    created_at    </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">DATETIME</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">6</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NOT NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    published_at  </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">DATETIME</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">6</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">NULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    PRIMARY KEY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (id),</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    KEY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> idx_dispatch (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, created_at)        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- relay taraması bu indeksten gider</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span></code></pre>\n<p>Yazma artık iş kodunun derdi değil:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">transaction</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">use</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($invoice) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $invoice</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">save</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    Outbox</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">write</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;invoice.issued&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, $invoice</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">id, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$this</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">payload</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($invoice));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">});</span></span></code></pre>\n<p>Kuyruğa basmayı ayrı bir <strong>relay</strong> yapar: <code>pending</code> satırları okur, RabbitMQ’ya <code>publish</code> eder, başarınca <code>published</code> damgalar. Burada kritik gerçek şu: relay “publish ettim ama <code>published</code> damgasını vuramadan çöktüm” durumuna düşebilir. Yani outbox dual-write’ı çözerken size bedava bir garanti <strong>vermez</strong> — <strong>at-least-once</strong> verir. Mesaj kaybolmaz ama tekrar edebilir. Geri kalan her şey bu gerçeğin etrafına dizilir.</p>\n<h2 id=\"relayi-nasıl-beslersiniz-polling-mi-cdc-mi\">Relay’i nasıl beslersiniz: polling mi, CDC mi?</h2>\n<p>Relay <code>pending</code> satırları iki yoldan görebilir.</p>\n<p><strong>Polling</strong> — relay tabloyu periyodik tarar:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id, topic, payload</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> outbox</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pending&#39;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">ORDER BY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> created_at</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">LIMIT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Basit, her veritabanında çalışır, operasyonel yükü düşük. Bedeli iki şey: tarama aralığı kadar <strong>gecikme</strong> (1 sn’lik poll = ~1 sn’lik kuyruk gecikmesi) ve boşta dönen sorgu. <code>idx_dispatch</code> indeksi olmadan bu tarama tablo büyüdükçe pahalılaşır; indeksle bile poll sıklığı ile DB yükü arasında pazarlık yaparsınız. Küçük-orta hacimde polling <strong>doğru</strong> cevap — gecikmeyi kabul edilebilir tutacak kadar sık, DB’yi yormayacak kadar seyrek (pratikte 200 ms–1 sn) ayarlayın.</p>\n<aside class=\"revision\" id=\"revision-2026-10-05\" data-kind=\"update\" data-refers=\"above\" aria-labelledby=\"revision-2026-10-05-label\"> <p class=\"revision__meta not-prose\" id=\"revision-2026-10-05-label\"> <span class=\"revision__label\"> <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"inline-block shrink-0\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 0 1-15.5 6.2L3 16\"/><path d=\"M3 12a9 9 0 0 1 15.5-6.2L21 8\"/><path d=\"M21 3v5h-5M3 21v-5h5\"/></svg> Update </span> <span class=\"revision__dot\" aria-hidden=\"true\">·</span> <time datetime=\"2026-10-05\">October 5, 2026</time>  <span class=\"revision__dot\" aria-hidden=\"true\">·</span> <span class=\"revision__refers\"> <span aria-hidden=\"true\">↑ </span> about the paragraph above </span>  </p> <div class=\"revision__body\"> <p>Aynı biçimdeki bir indeksi sonradan PostgreSQL 17’de ölçtüm. 10 milyon satırlık (5.000’i <code>pending</code>) bir kuyruk tablosunda <code>(status, created_at)</code> bileşik indeks 310,4 MB’a çıkarken <code>pending</code> satırlarına bağlı kısmi indeks 7,6 MB’ta kaldı — tek container (<a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/\">ölçümler</a>). Buradaki şema MySQL; onun <code>CREATE INDEX</code> ifadesinde <code>WHERE</code> yok ve orada ölçmedim.</p> </div> </aside>\n<p><strong>CDC</strong> (change data capture) — relay tabloyu değil, veritabanının <strong>WAL/binlog</strong>’unu dinler (Debezium tipik araç). <code>outbox</code>’a düşen her <code>INSERT</code> neredeyse anında bir event’e döner; polling gecikmesi ve boşa tarama ortadan kalkar. Bedeli operasyonel: binlog’a erişim, bir connector süreci, ek bir hareketli parça. CDC’yi gecikme gerçekten önemliyse (saniye altı) ya da hacim polling’i zorluyorsa açarım; aksi halde polling’in sadeliğini bir avantaj sayarım.</p>\n<blockquote>\n<p>Kural: gecikme bütçeniz ile operasyonel bütçeniz çatışır. CDC gecikmeyi satın alır, karşılığında bir altyapı parçası ödetir. Önce polling’le başlayın; CDC’ye ölçülmüş bir sebep çıkınca geçin.</p>\n</blockquote>\n<h2 id=\"çoklu-relay-skip-locked-ile-çakışmasız-dağıtım\">Çoklu relay: <code>SKIP LOCKED</code> ile çakışmasız dağıtım</h2>\n<p>Tek relay bir bottleneck ve tek arıza noktasıdır. Birden çok relay aynı tabloyu tarayınca yeni bir risk doğar: ikisi aynı satırı kapıp aynı mesajı iki kez yayımlar. Naif çözüm tabloyu kilitlemektir — ki bu paralelliği öldürür.</p>\n<p>Doğru araç <code>SELECT ... FOR UPDATE SKIP LOCKED</code>. Her relay bir batch satırı <strong>kilitleyerek</strong> claim eder; başka bir relay’in kilitlediği satırları sıraya girip beklemek yerine <strong>atlar</strong>:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">BEGIN</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  SELECT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id, topic, payload</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> outbox</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  WHERE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;pending&#39;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  ORDER BY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> created_at</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  LIMIT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  FOR</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> UPDATE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> SKIP</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> LOCKED;     </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- başka relay&#39;in tuttuğu satırları atla, bekleme</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  -- bu batch&#39;i publish et, sonra:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  UPDATE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> outbox </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">SET</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> &#39;published&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, published_at </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> NOW</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">6</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">IN</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (...);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">COMMIT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p><code>SKIP LOCKED</code> olmadan relay’ler aynı satırlar için kuyruğa girer ve paralellik fiilen seri yürür. <code>SKIP LOCKED</code> ile her relay disjoint bir küme çeker, hepsi birden ilerler. PostgreSQL ve MySQL 8+ bunu destekler.</p>\n<p>Bir incelik: <code>publish</code> ile <code>UPDATE ... published</code> arasında relay çökerse satır <code>pending</code> kalır ve <strong>yeniden yayımlanır</strong>. Bu kabul edilebilir — zaten at-least-once’tayız; çözüm tüketici tarafında, aşağıda. Tehlikeli olan tersidir: önce <code>published</code> damgalayıp sonra <code>publish</code> etmek. O zaman çökme <strong>kayıp event</strong> üretir ki tam kaçtığımız şey. Yani sıra <strong>değişmez</strong>: önce <code>publish</code>, sonra damga.</p>\n<h2 id=\"ordering-neyi-garanti-edebilirsiniz-neyi-edemezsiniz\">Ordering: neyi garanti edebilirsiniz, neyi edemezsiniz</h2>\n<p>“Event’ler yazıldığı sırada gelsin” sezgisel bir beklenti ama global ordering pahalı ve çoğu zaman gereksiz. Gerçekte ihtiyaç duyduğunuz şey <strong>aggregate başına</strong> sıradır: aynı faturanın <code>created</code> event’i <code>updated</code>’dan önce gelsin; iki farklı faturanın birbirine göre sırası kimsenin umurunda değil.</p>\n<p>Bunu <code>aggregate_id</code> ile partition’layarak alırsınız: aynı <code>aggregate_id</code>’ye sahip event’ler aynı kuyruk partition’ına (ya da RabbitMQ’da consistent hash exchange ile aynı queue’ya) gider; o partition’ı <strong>tek consumer</strong> sırayla işler. Farklı aggregate’ler paralel akar.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>outbox (created_at sırasıyla)</span></span>\n<span class=\"line\"><span>   │  hash(aggregate_id) % N</span></span>\n<span class=\"line\"><span>   ├── partition 0 ──► consumer-0   (aggregate A,D olayları — sıralı)</span></span>\n<span class=\"line\"><span>   ├── partition 1 ──► consumer-1   (aggregate B olayları   — sıralı)</span></span>\n<span class=\"line\"><span>   └── partition 2 ──► consumer-2   (aggregate C olayları   — sıralı)</span></span></code></pre>\n<p>İki şey ordering’i yine de bozar ve tasarımın bunlara hazır olması gerekir: çoklu relay <code>SKIP LOCKED</code> ile satırları farklı sırada yayımlayabilir ve at-least-once tekrarlar araya girebilir. Bu yüzden global sıraya bel bağlamak yerine, tüketiciyi <strong>sırasız ve tekrarlı</strong> event’e dayanıklı yazmak gerekir. Pratik kalkan: event’e monotonik bir <code>version</code>/<code>sequence</code> koyup tüketicide eskiyi (daha düşük versiyonu) düşürmek. Sıkı global ordering gerekiyorsa tek partition + tek consumer’a inersiniz — ki bu da throughput’u o tek consumer’a bağlar; çoğu sistem bu bedeli ödemek istemez.</p>\n<h2 id=\"at-least-once-gelince-tüketici-idempotent-olmalı\">At-least-once gelince: tüketici idempotent olmalı</h2>\n<p>Relay tekrar yayımlayabildiği için her event’in en az bir, bazen birden çok kez geleceğini varsaymak gerekir. Çözüm event’i tekil yapmaya çalışmak değil — <strong>tüketiciyi</strong> aynı event’i iki kez işlese de tek kez işlemiş gibi davranacak şekilde kurmak. İşlenen her event <code>id</code>’sini bir dedupe tablosuna yazıp işi <strong>aynı transaction içinde</strong> yapmak:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">transaction</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">use</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($event) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    $inserted </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> DB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">table</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;processed_messages&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">insertOrIgnore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">([</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#39;message_id&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">   =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;id&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#39;processed_at&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> now</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($inserted </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// tekrar gelmiş; hiçbir yan etki üretme</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    $this</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">applyBusinessEffect</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($event); </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// asıl iş — yalnızca bir kez</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">});</span></span></code></pre>\n<p><code>processed_messages.message_id</code> üstündeki <strong>unique</strong> kısıt işin kalbi: ikinci kez gelen aynı <code>id</code>, <code>insertOrIgnore</code> ile sessizce elenir. İş etkisini aynı transaction’a almak şart — yoksa “işledim ama damgalamadan çöktüm” boşluğu açılır. Dedupe anahtarının nereden geldiği ve çökmeden sağ çıkan dedupe tasarımı başlı başına bir konu; oraya <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/idempotency-ayni-mesaj-iki-kez/\">ayrı bir notta</a> girdim.</p>\n<p>Yan etki bir dış sistemse (e-fatura entegratörü, ödeme API’si) dedupe tek başına yetmez — çünkü yan etki transaction’ın dışındadır. Orada anahtarı <strong>dış servise taşımak</strong> gerekir: aynı <code>id</code>’yi entegratöre idempotency-key olarak geçmek, ya da göndermeden önce “bu zaten var mı?” diye sormak. Bu sınırı, numarayı dış çağrıdan önce mühürleyip recovery’yi bir lookup’a indiren <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/ardisik-numara-uretiminde-race-condition-ve-gap/\">JIT rezervasyon yazısında</a> ayrıntılı işledim.</p>\n<h2 id=\"processed_messages-sonsuza-kadar-büyüyemez\"><code>processed_messages</code> sonsuza kadar büyüyemez</h2>\n<p>Dedupe tablosu her event için bir satır biriktirir; budanmazsa kendisi bir performans sorununa döner. Anahtar gözlem: bir event <code>id</code>’sini sonsuza kadar tutmanız gerekmez — yalnızca <strong>bir tekrarın gelebileceği pencere</strong> kadar tutmanız yeter. Relay retry’ları ve broker redelivery’leri saatler mertebesindedir, günler değil.</p>\n<p>Pratikte sabit bir <strong>retention penceresi</strong> belirleyip (örneğin 7 gün — en uzun olası redelivery’nin rahatça üstünde) eskisini budarım:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"sql\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">DELETE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> processed_messages</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> processed_at </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&lt;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> NOW</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> INTERVAL </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">7</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> DAY</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">LIMIT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 10000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;   </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">-- tek seferde küçük batch&#39;ler, replica lag&#39;i ve kilidi kontrol altında</span></span></code></pre>\n<p><code>message_id</code> UUID ise tablo ve indeks büyür; budama bunu sınırlı tutar. Pencereyi seçerken kuralı tersinden kurun: <em>en uzun gecikmeli redelivery ne kadar sonra gelebilir?</em> Cevabın güvenli üstünü retention yapın. Pencereyi çok kısa tutarsanız geç gelen bir tekrar dedupe’a takılmaz ve yan etki ikinci kez çalışır.</p>\n<h2 id=\"exactly-once-neden-çoğu-zaman-bir-yanılsama\">Exactly-once neden çoğu zaman bir yanılsama</h2>\n<p>Dağıtık sistemde “tam bir kez teslim” cazip ama uçtan uca garanti edilemez. Sebep aynı dual-write’ın broker sınırındaki hali: tüketici işi yaptı ama <code>ack</code>’i broker’a ulaştıramadan çöktü mü, broker mesajı yeniden teslim eder — çünkü <code>ack</code> görmemiştir. “İşledim” ile “işlediğimi bildirdim” iki ayrı adımdır ve aralarına çökme girebilir. Bu, iki general probleminin pratikteki yüzü.</p>\n<p>Bu yüzden hedef exactly-once <strong>delivery</strong> değil, exactly-once <strong>effect</strong>. Bunu da iki ucuz garantiyi birleştirerek alırsınız:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>at-least-once delivery  +  idempotent consumer  =  effectively-once</span></span></code></pre>\n<p>Yani teslimi tam bir kez yapmaya çalışmayı bırakıp <strong>tekrarı zararsız</strong> kılarsınız. “Exactly-once” pazarlayan sistemlerin altında çoğu zaman tam olarak bu vardır: at-least-once + bir dedupe katmanı. Garantiyi teslimde değil, etkide arayın.</p>\n<h2 id=\"eventin-içinden-geçen-kişisel-veri-alan-seviyesi-şifreleme\">Event’in içinden geçen kişisel veri: alan seviyesi şifreleme</h2>\n<p>Outbox oturunca yeni bir yüzey çıkar: event’lerin payload’ında müşteri adı, vergi numarası, adres gibi kişisel veri var ve bu payload artık <strong>kalıcı</strong> bir tabloda (<code>outbox</code>) duruyor, üstelik broker’dan geçiyor. Veriyi yerinde bırakırsam, GDPR/KVKK kapsamındaki alanları şifresiz biçimde birden çok yere kopyalamış olurum.</p>\n<p>Bütün payload’ı şifrelemek istemedim — <code>topic</code>, <code>aggregate_id</code>, <code>invoice_id</code> gibi alanları relay yönlendirmesi ve gözlemlenebilirlik için açık görmem gerekiyordu. İhtiyaç <strong>alan seviyesi</strong> şifreleme: yalnızca hassas alanlar şifreli, gerisi açık. Hassas alanları şemada <code>x-gdpr-sensitive</code> ile işaretleyip serileştirmede yalnızca onları şifreledim:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">$schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;invoice_id&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">    =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;x-gdpr-sensitive&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;customer_name&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;x-gdpr-sensitive&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;tax_id&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">        =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;x-gdpr-sensitive&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#39;total&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">         =&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;x-gdpr-sensitive&#39;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =&gt;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">$payload </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> collect</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">($raw)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">fn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ($value, $field) =&gt;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ($schema[$field][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;x-gdpr-sensitive&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        ?</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Crypt</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">encryptString</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">string</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) $value)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// yalnızca hassas alan şifreli</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        :</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> $value</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">all</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span></code></pre>\n<p>Şemanın hassas alanı beyan etmesi tesadüf değil — bunu kuyruğun kenarında şema doğrulamasıyla birleştirince, “hangi alan kişisel veri ve doğru tipte mi?” sorularının ikisi de <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/semayi-kuyrugun-kenarinda-dogrulamak/\">kenarda</a> cevaplanır. Asıl mühendislik kararı şifrelemenin kendisinde değil, çevresindeki üç soruda:</p>\n<p><strong>Anahtar yönetimi ve rotasyon.</strong> Laravel <code>Crypt</code>, <code>APP_KEY</code> ile AES-256 kullanır. Tek bir anahtara bağlanırsanız rotasyon kabusa döner: eski anahtarla yazılmış outbox satırlarını yeni anahtar açamaz. Çözüm, şifreli değerin yanına <strong>anahtar versiyonu</strong> (key id) yazmak ve birden çok anahtarı aynı anda tanıyan bir keyring tutmak — yeni yazımlar güncel anahtarı kullanır, eski değerler kendi versiyonuyla açılır. Laravel’de <code>config/app.php</code>’deki <code>key</code> + <code>previous_keys</code> tam bunun içindir; decrypt sırasıyla eski anahtarları da dener. Rotasyonu bu yüzden veri taşımadan yapabilirsiniz.</p>\n<p><strong>Şifreli alanda arama.</strong> <code>Crypt::encryptString</code> her çağrıda farklı ciphertext üretir (rastgele IV), ki bu güvenlik için doğru ama <code>WHERE tax_id = ?</code> ile aramayı imkânsız kılar. Eşitlik araması gerekiyorsa <strong>blind index</strong> eklersiniz: aranabilir alanın deterministik bir HMAC’ini ayrı bir kolonda tutup onu sorgularsınız. Şifreli kolon gizliliği, blind index aranabilirliği taşır; ikisi ayrı kolondur çünkü iki ayrı işi vardır.</p>\n<p><strong>Anahtarı kaybetmek veriyi kaybetmektir.</strong> Alan şifrelemesi <code>APP_KEY</code>’i bir <strong>availability</strong> bağımlılığına çevirir: anahtar giderse şifreli alanlar kalıcı çöptür. Anahtar yedeği ve erişim kontrolü, şifrelemenin kendisi kadar tasarımın parçasıdır.</p>\n<h2 id=\"ne-zaman-bu-kalıbı-kurmazsınız\">Ne zaman bu kalıbı kurmazsınız?</h2>\n<p>Outbox + idempotent tüketim + alan şifrelemesi ucuz değil: bir tablo, bir relay süreci (ya da CDC connector), bir dedupe tablosu ve onun budama işi, bir de anahtar yönetimi getirir. Karşılığında ne aldığınızı bilmiyorsanız ödemeyin. Şu durumlarda kurmuyorum:</p>\n<ul>\n<li><strong>Event kaybı tolere edilebiliyorsa.</strong> Yayımladığınız şey best-effort bir bildirimse (cache invalidation, “yeni içerik var” sinyali) ve bir-iki kayıp önemsizse, doğrudan <code>publish</code> yeterli. Outbox’ı kayıp önemli olduğu için kurarsınız.</li>\n<li><strong>Yazma ile yayım aynı sistemdeyse.</strong> Hedefiniz de aynı veritabanıysa (ayrı bir broker yoksa), dual-write zaten yok; outbox’a gerek kalmaz.</li>\n<li><strong>Hacim tek consumer’a sığıyorsa ve ordering kritikse.</strong> Düşük hacimde tek tüketicili bir kuyruk hem sırayı hem tekilliği basitçe verir; çoklu relay ve partition makinesini kurmak olmayan bir problemi çözmektir.</li>\n</ul>\n<p>Kalıba ihtiyaç üç koşul <strong>birlikte</strong> doğunca başlar: event kaybı kabul edilemez, üretim/tüketim paralel ve veri ayrı bir sisteme (broker) gidiyor. Üçü birden yoksa daha sade bir çözüm vardır.</p>\n<hr/>\n<p>Üç parça tek bir disipline iner: <strong>veriyi tek bir sisteme yaz, tekrarı tüketicide bastır, hassas alanı yola çıkmadan mühürle.</strong> Outbox kuyruğa olan güveni, idempotent tüketim teslime olan güveni, alan şifrelemesi de payload’a olan güveni gereksiz kılar — her biri bir garantiyi koddan dışarı, tasarımın içine taşır.</p>",
      "content_text": "> **Kısaca:** Outbox dual-write'ı tek write'a indirir ama bedava garanti vermez, at-least-once verir. Relay polling mi CDC mi, SKIP LOCKED ile çoklu relay, ordering, processed_messages budama, exactly-once yanılsaması ve alan-seviyesi şifreleme.\n\nBelirti küçüktü: günde bir-iki fatura veritabanında duruyor ama consumer onları hiç görmüyordu. Kullanıcı \"kestim\" diyor, karşı tarafta hiçbir iz yok. Hep gece, hep bir deploy ya da network dalgalanması anında. Tabloyu netleştirince sebep ortaya çıktı — veritabanına `INSERT` ile kuyruğa `publish` iki ayrı sistemdi ve aralarında hiçbir garanti yoktu.\n\nBu yazı o boşluğu kapatan **transactional outbox** kalıbının, kapatınca açılan **at-least-once** tekrarının ve son olarak event'in içinden geçen kişisel veriyi **alan seviyesinde** şifrelemenin sistem tarafı. Karar zincirinin günlük tarafını [muhammetsafak.com'da ayrı yazdım](/tr/blog/outbox-idempotent-tuketim-ve-alan-sifrelemesi/); burada kalıbın production'da dayanıklı olması için verilmesi gereken kararları topluyorum.\n\n## Dual-write: bir transaction'ı iki sisteme yayamazsınız\n\nSorunun özü tek cümle: yerel veritabanı transaction'ınız RabbitMQ'yu kapsamaz. Klasik kod *çoğu zaman* çalıştığı için tehlikeli:\n\n```php\nDB::transaction(function () use ($invoice) {\n    $invoice->save();                 // 1) MySQL'e yaz\n    $this->publishToRabbit($invoice); // 2) kuyruğa publish\n});\n```\n\nİki ayrı arıza modu var:\n\n- `save()` başarılı, `publish` network hatasıyla düşer → DB'de fatura var, event yok. **Kayıp event.**\n- `publish` başarılı, ardından transaction başka sebeple `rollback` olur → event gitti, DB'de karşılığı yok. **Hayalet event.**\n\n`DB::transaction` kurtarmaz, çünkü commit/rollback yalnızca MySQL tarafını sarar; broker o transaction'ın parçası değildir. İki sistemi tek atomik adımda tutmanın pratik yolu, yazmayı **tek bir sisteme** indirmek.\n\n## Outbox: önce tek sisteme yaz, sonra ayrı bir süreç yayımlasın\n\nKalıbın fikri sade: mesajı kuyruğa doğrudan basmak yerine, **aynı transaction içinde** bir `outbox` tablosuna satır olarak yaz. Fatura ile event aynı commit'te ya birlikte var olur ya da birlikte yok olur — dual-write tek write'a iner.\n\n```sql\nCREATE TABLE outbox (\n    id            BINARY(16)   NOT NULL,        -- event id = idempotency anahtarı\n    aggregate_id  BIGINT       NOT NULL,        -- ordering ve partition anahtarı\n    topic         VARCHAR(120) NOT NULL,\n    payload       JSON         NOT NULL,\n    status        ENUM('pending','published') NOT NULL DEFAULT 'pending',\n    created_at    DATETIME(6)  NOT NULL,\n    published_at  DATETIME(6)  NULL,\n    PRIMARY KEY (id),\n    KEY idx_dispatch (status, created_at)        -- relay taraması bu indeksten gider\n);\n```\n\nYazma artık iş kodunun derdi değil:\n\n```php\nDB::transaction(function () use ($invoice) {\n    $invoice->save();\n    Outbox::write('invoice.issued', $invoice->id, $this->payload($invoice));\n});\n```\n\nKuyruğa basmayı ayrı bir **relay** yapar: `pending` satırları okur, RabbitMQ'ya `publish` eder, başarınca `published` damgalar. Burada kritik gerçek şu: relay \"publish ettim ama `published` damgasını vuramadan çöktüm\" durumuna düşebilir. Yani outbox dual-write'ı çözerken size bedava bir garanti **vermez** — **at-least-once** verir. Mesaj kaybolmaz ama tekrar edebilir. Geri kalan her şey bu gerçeğin etrafına dizilir.\n\n## Relay'i nasıl beslersiniz: polling mi, CDC mi?\n\nRelay `pending` satırları iki yoldan görebilir.\n\n**Polling** — relay tabloyu periyodik tarar:\n\n```sql\nSELECT id, topic, payload\nFROM outbox\nWHERE status = 'pending'\nORDER BY created_at\nLIMIT 100;\n```\n\nBasit, her veritabanında çalışır, operasyonel yükü düşük. Bedeli iki şey: tarama aralığı kadar **gecikme** (1 sn'lik poll = ~1 sn'lik kuyruk gecikmesi) ve boşta dönen sorgu. `idx_dispatch` indeksi olmadan bu tarama tablo büyüdükçe pahalılaşır; indeksle bile poll sıklığı ile DB yükü arasında pazarlık yaparsınız. Küçük-orta hacimde polling **doğru** cevap — gecikmeyi kabul edilebilir tutacak kadar sık, DB'yi yormayacak kadar seyrek (pratikte 200 ms–1 sn) ayarlayın.\n\n> **Güncelleme · 2026-10-05** (yukarıdaki paragraf için) — Aynı biçimdeki bir indeksi sonradan PostgreSQL 17'de ölçtüm. 10 milyon satırlık (5.000'i `pending`) bir kuyruk tablosunda `(status, created_at)` bileşik indeks 310,4 MB'a çıkarken `pending` satırlarına bağlı kısmi indeks 7,6 MB'ta kaldı — tek container ([ölçümler](/tr/muhendislik/notlar/kuyruk-tablosunu-canli-kumeye-gore-kurmak/)). Buradaki şema MySQL; onun `CREATE INDEX` ifadesinde `WHERE` yok ve orada ölçmedim.\n\n**CDC** (change data capture) — relay tabloyu değil, veritabanının **WAL/binlog**'unu dinler (Debezium tipik araç). `outbox`'a düşen her `INSERT` neredeyse anında bir event'e döner; polling gecikmesi ve boşa tarama ortadan kalkar. Bedeli operasyonel: binlog'a erişim, bir connector süreci, ek bir hareketli parça. CDC'yi gecikme gerçekten önemliyse (saniye altı) ya da hacim polling'i zorluyorsa açarım; aksi halde polling'in sadeliğini bir avantaj sayarım.\n\n> Kural: gecikme bütçeniz ile operasyonel bütçeniz çatışır. CDC gecikmeyi satın alır, karşılığında bir altyapı parçası ödetir. Önce polling'le başlayın; CDC'ye ölçülmüş bir sebep çıkınca geçin.\n\n## Çoklu relay: `SKIP LOCKED` ile çakışmasız dağıtım\n\nTek relay bir bottleneck ve tek arıza noktasıdır. Birden çok relay aynı tabloyu tarayınca yeni bir risk doğar: ikisi aynı satırı kapıp aynı mesajı iki kez yayımlar. Naif çözüm tabloyu kilitlemektir — ki bu paralelliği öldürür.\n\nDoğru araç `SELECT ... FOR UPDATE SKIP LOCKED`. Her relay bir batch satırı **kilitleyerek** claim eder; başka bir relay'in kilitlediği satırları sıraya girip beklemek yerine **atlar**:\n\n```sql\nBEGIN;\n  SELECT id, topic, payload\n  FROM outbox\n  WHERE status = 'pending'\n  ORDER BY created_at\n  LIMIT 100\n  FOR UPDATE SKIP LOCKED;     -- başka relay'in tuttuğu satırları atla, bekleme\n\n  -- bu batch'i publish et, sonra:\n  UPDATE outbox SET status = 'published', published_at = NOW(6)\n  WHERE id IN (...);\nCOMMIT;\n```\n\n`SKIP LOCKED` olmadan relay'ler aynı satırlar için kuyruğa girer ve paralellik fiilen seri yürür. `SKIP LOCKED` ile her relay disjoint bir küme çeker, hepsi birden ilerler. PostgreSQL ve MySQL 8+ bunu destekler.\n\nBir incelik: `publish` ile `UPDATE ... published` arasında relay çökerse satır `pending` kalır ve **yeniden yayımlanır**. Bu kabul edilebilir — zaten at-least-once'tayız; çözüm tüketici tarafında, aşağıda. Tehlikeli olan tersidir: önce `published` damgalayıp sonra `publish` etmek. O zaman çökme **kayıp event** üretir ki tam kaçtığımız şey. Yani sıra **değişmez**: önce `publish`, sonra damga.\n\n## Ordering: neyi garanti edebilirsiniz, neyi edemezsiniz\n\n\"Event'ler yazıldığı sırada gelsin\" sezgisel bir beklenti ama global ordering pahalı ve çoğu zaman gereksiz. Gerçekte ihtiyaç duyduğunuz şey **aggregate başına** sıradır: aynı faturanın `created` event'i `updated`'dan önce gelsin; iki farklı faturanın birbirine göre sırası kimsenin umurunda değil.\n\nBunu `aggregate_id` ile partition'layarak alırsınız: aynı `aggregate_id`'ye sahip event'ler aynı kuyruk partition'ına (ya da RabbitMQ'da consistent hash exchange ile aynı queue'ya) gider; o partition'ı **tek consumer** sırayla işler. Farklı aggregate'ler paralel akar.\n\n```\noutbox (created_at sırasıyla)\n   │  hash(aggregate_id) % N\n   ├── partition 0 ──► consumer-0   (aggregate A,D olayları — sıralı)\n   ├── partition 1 ──► consumer-1   (aggregate B olayları   — sıralı)\n   └── partition 2 ──► consumer-2   (aggregate C olayları   — sıralı)\n```\n\nİki şey ordering'i yine de bozar ve tasarımın bunlara hazır olması gerekir: çoklu relay `SKIP LOCKED` ile satırları farklı sırada yayımlayabilir ve at-least-once tekrarlar araya girebilir. Bu yüzden global sıraya bel bağlamak yerine, tüketiciyi **sırasız ve tekrarlı** event'e dayanıklı yazmak gerekir. Pratik kalkan: event'e monotonik bir `version`/`sequence` koyup tüketicide eskiyi (daha düşük versiyonu) düşürmek. Sıkı global ordering gerekiyorsa tek partition + tek consumer'a inersiniz — ki bu da throughput'u o tek consumer'a bağlar; çoğu sistem bu bedeli ödemek istemez.\n\n## At-least-once gelince: tüketici idempotent olmalı\n\nRelay tekrar yayımlayabildiği için her event'in en az bir, bazen birden çok kez geleceğini varsaymak gerekir. Çözüm event'i tekil yapmaya çalışmak değil — **tüketiciyi** aynı event'i iki kez işlese de tek kez işlemiş gibi davranacak şekilde kurmak. İşlenen her event `id`'sini bir dedupe tablosuna yazıp işi **aynı transaction içinde** yapmak:\n\n```php\nDB::transaction(function () use ($event) {\n    $inserted = DB::table('processed_messages')->insertOrIgnore([\n        'message_id'   => $event['id'],\n        'processed_at' => now(),\n    ]);\n\n    if ($inserted === 0) {\n        return; // tekrar gelmiş; hiçbir yan etki üretme\n    }\n\n    $this->applyBusinessEffect($event); // asıl iş — yalnızca bir kez\n});\n```\n\n`processed_messages.message_id` üstündeki **unique** kısıt işin kalbi: ikinci kez gelen aynı `id`, `insertOrIgnore` ile sessizce elenir. İş etkisini aynı transaction'a almak şart — yoksa \"işledim ama damgalamadan çöktüm\" boşluğu açılır. Dedupe anahtarının nereden geldiği ve çökmeden sağ çıkan dedupe tasarımı başlı başına bir konu; oraya [ayrı bir notta](/tr/muhendislik/notlar/idempotency-ayni-mesaj-iki-kez/) girdim.\n\nYan etki bir dış sistemse (e-fatura entegratörü, ödeme API'si) dedupe tek başına yetmez — çünkü yan etki transaction'ın dışındadır. Orada anahtarı **dış servise taşımak** gerekir: aynı `id`'yi entegratöre idempotency-key olarak geçmek, ya da göndermeden önce \"bu zaten var mı?\" diye sormak. Bu sınırı, numarayı dış çağrıdan önce mühürleyip recovery'yi bir lookup'a indiren [JIT rezervasyon yazısında](/tr/muhendislik/sistemler/ardisik-numara-uretiminde-race-condition-ve-gap/) ayrıntılı işledim.\n\n## `processed_messages` sonsuza kadar büyüyemez\n\nDedupe tablosu her event için bir satır biriktirir; budanmazsa kendisi bir performans sorununa döner. Anahtar gözlem: bir event `id`'sini sonsuza kadar tutmanız gerekmez — yalnızca **bir tekrarın gelebileceği pencere** kadar tutmanız yeter. Relay retry'ları ve broker redelivery'leri saatler mertebesindedir, günler değil.\n\nPratikte sabit bir **retention penceresi** belirleyip (örneğin 7 gün — en uzun olası redelivery'nin rahatça üstünde) eskisini budarım:\n\n```sql\nDELETE FROM processed_messages\nWHERE processed_at < NOW() - INTERVAL 7 DAY\nLIMIT 10000;   -- tek seferde küçük batch'ler, replica lag'i ve kilidi kontrol altında\n```\n\n`message_id` UUID ise tablo ve indeks büyür; budama bunu sınırlı tutar. Pencereyi seçerken kuralı tersinden kurun: *en uzun gecikmeli redelivery ne kadar sonra gelebilir?* Cevabın güvenli üstünü retention yapın. Pencereyi çok kısa tutarsanız geç gelen bir tekrar dedupe'a takılmaz ve yan etki ikinci kez çalışır.\n\n## Exactly-once neden çoğu zaman bir yanılsama\n\nDağıtık sistemde \"tam bir kez teslim\" cazip ama uçtan uca garanti edilemez. Sebep aynı dual-write'ın broker sınırındaki hali: tüketici işi yaptı ama `ack`'i broker'a ulaştıramadan çöktü mü, broker mesajı yeniden teslim eder — çünkü `ack` görmemiştir. \"İşledim\" ile \"işlediğimi bildirdim\" iki ayrı adımdır ve aralarına çökme girebilir. Bu, iki general probleminin pratikteki yüzü.\n\nBu yüzden hedef exactly-once **delivery** değil, exactly-once **effect**. Bunu da iki ucuz garantiyi birleştirerek alırsınız:\n\n```\nat-least-once delivery  +  idempotent consumer  =  effectively-once\n```\n\nYani teslimi tam bir kez yapmaya çalışmayı bırakıp **tekrarı zararsız** kılarsınız. \"Exactly-once\" pazarlayan sistemlerin altında çoğu zaman tam olarak bu vardır: at-least-once + bir dedupe katmanı. Garantiyi teslimde değil, etkide arayın.\n\n## Event'in içinden geçen kişisel veri: alan seviyesi şifreleme\n\nOutbox oturunca yeni bir yüzey çıkar: event'lerin payload'ında müşteri adı, vergi numarası, adres gibi kişisel veri var ve bu payload artık **kalıcı** bir tabloda (`outbox`) duruyor, üstelik broker'dan geçiyor. Veriyi yerinde bırakırsam, GDPR/KVKK kapsamındaki alanları şifresiz biçimde birden çok yere kopyalamış olurum.\n\nBütün payload'ı şifrelemek istemedim — `topic`, `aggregate_id`, `invoice_id` gibi alanları relay yönlendirmesi ve gözlemlenebilirlik için açık görmem gerekiyordu. İhtiyaç **alan seviyesi** şifreleme: yalnızca hassas alanlar şifreli, gerisi açık. Hassas alanları şemada `x-gdpr-sensitive` ile işaretleyip serileştirmede yalnızca onları şifreledim:\n\n```php\n$schema = [\n    'invoice_id'    => ['x-gdpr-sensitive' => false],\n    'customer_name' => ['x-gdpr-sensitive' => true],\n    'tax_id'        => ['x-gdpr-sensitive' => true],\n    'total'         => ['x-gdpr-sensitive' => false],\n];\n\n$payload = collect($raw)->map(fn ($value, $field) =>\n    ($schema[$field]['x-gdpr-sensitive'] ?? false)\n        ? Crypt::encryptString((string) $value)  // yalnızca hassas alan şifreli\n        : $value\n)->all();\n```\n\nŞemanın hassas alanı beyan etmesi tesadüf değil — bunu kuyruğun kenarında şema doğrulamasıyla birleştirince, \"hangi alan kişisel veri ve doğru tipte mi?\" sorularının ikisi de [kenarda](/tr/muhendislik/notlar/semayi-kuyrugun-kenarinda-dogrulamak/) cevaplanır. Asıl mühendislik kararı şifrelemenin kendisinde değil, çevresindeki üç soruda:\n\n**Anahtar yönetimi ve rotasyon.** Laravel `Crypt`, `APP_KEY` ile AES-256 kullanır. Tek bir anahtara bağlanırsanız rotasyon kabusa döner: eski anahtarla yazılmış outbox satırlarını yeni anahtar açamaz. Çözüm, şifreli değerin yanına **anahtar versiyonu** (key id) yazmak ve birden çok anahtarı aynı anda tanıyan bir keyring tutmak — yeni yazımlar güncel anahtarı kullanır, eski değerler kendi versiyonuyla açılır. Laravel'de `config/app.php`'deki `key` + `previous_keys` tam bunun içindir; decrypt sırasıyla eski anahtarları da dener. Rotasyonu bu yüzden veri taşımadan yapabilirsiniz.\n\n**Şifreli alanda arama.** `Crypt::encryptString` her çağrıda farklı ciphertext üretir (rastgele IV), ki bu güvenlik için doğru ama `WHERE tax_id = ?` ile aramayı imkânsız kılar. Eşitlik araması gerekiyorsa **blind index** eklersiniz: aranabilir alanın deterministik bir HMAC'ini ayrı bir kolonda tutup onu sorgularsınız. Şifreli kolon gizliliği, blind index aranabilirliği taşır; ikisi ayrı kolondur çünkü iki ayrı işi vardır.\n\n**Anahtarı kaybetmek veriyi kaybetmektir.** Alan şifrelemesi `APP_KEY`'i bir **availability** bağımlılığına çevirir: anahtar giderse şifreli alanlar kalıcı çöptür. Anahtar yedeği ve erişim kontrolü, şifrelemenin kendisi kadar tasarımın parçasıdır.\n\n## Ne zaman bu kalıbı kurmazsınız?\n\nOutbox + idempotent tüketim + alan şifrelemesi ucuz değil: bir tablo, bir relay süreci (ya da CDC connector), bir dedupe tablosu ve onun budama işi, bir de anahtar yönetimi getirir. Karşılığında ne aldığınızı bilmiyorsanız ödemeyin. Şu durumlarda kurmuyorum:\n\n- **Event kaybı tolere edilebiliyorsa.** Yayımladığınız şey best-effort bir bildirimse (cache invalidation, \"yeni içerik var\" sinyali) ve bir-iki kayıp önemsizse, doğrudan `publish` yeterli. Outbox'ı kayıp önemli olduğu için kurarsınız.\n- **Yazma ile yayım aynı sistemdeyse.** Hedefiniz de aynı veritabanıysa (ayrı bir broker yoksa), dual-write zaten yok; outbox'a gerek kalmaz.\n- **Hacim tek consumer'a sığıyorsa ve ordering kritikse.** Düşük hacimde tek tüketicili bir kuyruk hem sırayı hem tekilliği basitçe verir; çoklu relay ve partition makinesini kurmak olmayan bir problemi çözmektir.\n\nKalıba ihtiyaç üç koşul **birlikte** doğunca başlar: event kaybı kabul edilemez, üretim/tüketim paralel ve veri ayrı bir sisteme (broker) gidiyor. Üçü birden yoksa daha sade bir çözüm vardır.\n\n---\n\nÜç parça tek bir disipline iner: **veriyi tek bir sisteme yaz, tekrarı tüketicide bastır, hassas alanı yola çıkmadan mühürle.** Outbox kuyruğa olan güveni, idempotent tüketim teslime olan güveni, alan şifrelemesi de payload'a olan güveni gereksiz kılar — her biri bir garantiyi koddan dışarı, tasarımın içine taşır.\n\n## Kaynaklar\n\n- [Pattern: Transactional outbox](https://microservices.io/patterns/data/transactional-outbox.html) — microservices.io\n- [SELECT: the locking clause, FOR UPDATE and SKIP LOCKED](https://www.postgresql.org/docs/current/sql-select.html) — PostgreSQL\n- [InnoDB locking reads: SKIP LOCKED](https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html) — MySQL\n- [Encryption: gracefully rotating encryption keys](https://laravel.com/docs/12.x/encryption) — Laravel\n\n## Sık sorulanlar\n\n### Relay polling ile mi CDC ile mi beslenmeli?\n\nPolling her veritabanında çalışır ve operasyonel maliyeti neredeyse yoktur, ama tarama aralığı kadar gecikme ekler — pratikte 200 ms ile 1 s arası — ve boşa dönen bir sorgu bırakır. CDC bunun yerine WAL ya da binlog'u dinler, gecikme neredeyse kaybolur; bedeli binlog erişimi ve fazladan bir hareketli parçadır. Polling ile başlayın, ölçülmüş bir sebep çıkınca CDC'ye geçin.\n\n### processed_messages satırları ne kadar süre saklanmalı?\n\nYalnızca bir tekrarın hâlâ gelebileceği süre kadar. Relay retry'ları ve broker redelivery'leri günler değil saatler mertebesindedir; mümkün en geç redelivery'nin rahatça üstünde sabit bir saklama penceresi — örneğin yedi gün — yeterlidir ve küçük partiler hâlinde budanır. Pencere kısa kalırsa geç gelen tekrar dedupe'a yakalanmaz ve yan etki ikinci kez çalışır.",
      "date_published": "2026-06-22T00:00:00.000Z",
      "date_modified": "2026-10-05T00:00:00.000Z",
      "tags": [
        "architecture",
        "messaging",
        "idempotency",
        "reliability",
        "security"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/sistemler/cok-kumeli-gitops-sifir-statik-yetki/",
      "url": "https://muhammetsafak.com/tr/muhendislik/sistemler/cok-kumeli-gitops-sifir-statik-yetki/",
      "title": "Çok Kümeli GitOps’ta Sıfır Statik Yetki",
      "summary": "Diskte duran bir kubeconfig, tüm filonuza nişan almış uzun ömürlü bir kimlik bilgisidir — saldırganın tam da istediği şey. Onu, bir saatte sona eren ve hiç diske yazılmayan bir workload kimliğiyle değiştirin. İşte kural, sıra ve karşılığında üstlendiğiniz yeni kontrol düzlemi.",
      "content_html": "<p>Bir GitOps controller’ı — Argo CD, Flux — bir kümede oturur ve birçoğunu reconcile eder. Diğerlerine ulaşmak için her uzak <code>kube-apiserver</code>’a kimlik doğrulaması yapması gerekir. Bunu yapmanın varsayılan yolu bir kimlik bilgisini saklamaktır: kontrol kümesinde bir Kubernetes <code>Secret</code> olarak tutulan bir bearer token, bir client sertifikası, bir kubeconfig. Argo CD’de bunlar <code>argocd.argoproj.io/secret-type: cluster</code> etiketli nesnelerdir.</p>\n<p>Dur ve o <code>Secret</code>’ın ne olduğuna bak. O, <strong>genelde geniş yetkili, tüm filonuza nişan almış, tek bir yerde atıl duran, uzun ömürlü bir kimlik bilgisidir.</strong> Kendi kendine rotate olmaz. Yedeklerinizdedir. Kontrol kümenizde bir saldırganın bulabileceği en değerli tek şeydir, çünkü ona sahip olmak, eriştiği her kümeye sahip olmaktır. <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/neden-boring-architecture/\">Boring architecture</a> disiplininin tamamı, tutmak zorunda olmadığınız yükümlülükleri tutmamakla ilgilidir — ve saklanan filo geneli bir token, sırf kullanım anında taze ve kısa ömürlü bir tane basmak daha fazla iş gibi göründüğü için tuttuğunuz bir yükümlülüktür.</p>\n<p>Bu yazı o fazladan işi, sırasıyla yapmakla ilgili. Hedef <strong>sıfır statik yetki</strong>: controller’ı bir uzak kümeye doğrulayan hiçbir uzun ömürlü kimlik bilgisi asla yazılı kalmaz. Controller’ın sunduğu kimlik talep anında basılır, yaklaşık bir saatte sona erer ve yalnızca bellekte yaşar. Mekanizma, kimlik modeli için SPIFFE ve onu basmak için SPIRE’dır.</p>\n<p>Her şeyden önce: bu, bir elin parmaklarıyla sayabileceğiniz üç küme için bir araç değil. SPIRE, import ettiğiniz bir kütüphane değil, işlettiğiniz bir kontrol düzlemidir — tam da yavaş üstlenmeniz gereken türden bir yetenek; tıpkı <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/\">bölünmeyi hak edene kadar modüler monolitin mikroservisi yendiği gibi</a>. Ağırlığını ancak gerçekten bir filo işlettiğinizde hak eder — çok küme, çok ekip, çalınan bir <code>Secret</code>’ın geçemeyeceği bir denetim gereksinimi. Tek paylaşılan bir küme size yetiyorsa, en ucuz güvenli kimlik bilgisi, basmak için makineyi hiç kurmadığınız olandır. Geri kalanı, filoya gerçekten ulaştığınızı varsayar.</p>\n<h2 id=\"tek-kuralı-baştan-söyleyeyim\">Tek kuralı baştan söyleyeyim</h2>\n<p>Mekanizmadan önce netleştireyim: <strong>bir küme sınırını geçen kimlik bilgisi kısa ömürlü olmalı ve asla saklanmamalı.</strong> Aşağıdaki her şey bu tek cümlenin hizmetinde. Sonunda kimlik doğrulama yolunda hâlâ bir yere diske yazılmış uzun ömürlü bir token kalıyorsa, uğruna geldiğiniz özelliği satın almadan karmaşıklık inşa etmişsinizdir — eksik bir index’ten kaçmak için read replica eklemekle aynı yanlış dönüş.</p>\n<p>“Kısa ömürlü ve asla saklanmaz”ın burada kesin bir anlamı var. Kimlik, varsayılan ömrü <strong>bir saat</strong> olan bir X.509 sertifikası (ya da varsayılan ömrü beş dakika olan bir JWT); workload tarafından yerel bir socket’ten, <strong>sunacak kendine ait bir token’ı olmadan</strong> çekilir ve ömrünün yarısında otomatik rotate edilir. Yarın çalmaya değecek bir sır yok.</p>\n<h2 id=\"spiffe-ve-spire-gerçekte-ne\">SPIFFE ve SPIRE gerçekte ne</h2>\n<p>SPIFFE bir spesifikasyon; SPIRE ise spesifikasyonun tarif ettiği şeyleri basan referans implementasyon. Dört isim tüm tasarımı taşıyor.</p>\n<p>Bir <strong>SPIFFE ID</strong> bir URI’dir: <code>spiffe://&lt;trust-domain&gt;/&lt;path&gt;</code>, örneğin <code>spiffe://prod.example.org/ns/argocd/sa/application-controller</code>. Otorite kısmı <strong>trust domain</strong>’dir — tek bir mantıksal güven kökü, tek bir CA. Path workload’u tanımlar. Bir workload’un elinde tuttuğunu kanıtladığı isim budur.</p>\n<p>Bir <strong>SVID</strong> (SPIFFE Verifiable Identity Document) ise kanıttır. İki biçim:</p>\n<ul>\n<li><strong>X.509-SVID</strong> — SPIFFE ID’si URI SAN’ında (CN’de değil, ki bu sonra önemli olacak) bulunan bir X.509 sertifikası. Onunla mTLS yaparsınız.</li>\n<li><strong>JWT-SVID</strong> — <code>sub</code>’ı SPIFFE ID, <code>aud</code>’unu hedef doğrulayıcıya scope’ladığın imzalı bir JWT. Onu bearer token olarak sunarsınız.</li>\n</ul>\n<p>Asıl mesele ömürler. SPIRE’ın sunucu konfigürasyonunda varsayılanlar <code>default_x509_svid_ttl: 1h</code> ve <code>default_jwt_svid_ttl: 5m</code>’dir; bu SVID’ler, kendi <code>ca_ttl</code>’i varsayılan <code>24h</code> olan bir CA tarafından imzalanır. Bunlar bilerek kısadır: sızdırılmış bir SVID bir saat içinde değersizleşir, ki tam da bu yüzden eskiden token sakladığınız yerde onu kullanmak güvenlidir.</p>\n<p>“Asla saklanmaz”ı mümkün kılan şey, bir SPIRE <strong>agent</strong>’ı tarafından yerel bir Unix domain socket üzerinden sunulan <strong>Workload API</strong>’dir (belgelenmiş varsayılan <code>/tmp/spire-agent/public/api.sock</code>; çoğu kurulum onu <code>/run/spire/sockets/...</code> altında mount eder). Workload o socket’e bağlanır ve SVID’sini ister. <strong>Bunu yapmak için hiçbir kimlik bilgisi sunmaz.</strong> Agent, çağıran process’i kernel üzerinden inceleyerek tanır — PID’sini, oradan da container’ını, namespace’ini, service account’unu — ve bir dizi selector’a karşı <em>workload attestation</em> yapar. Kimlik, <em>hangi sırrı tuttuğuyla</em> değil, <em>process’in doğrulanabilir biçimde ne olduğuyla</em> kurulur. Saklanacak bir şeyin olmamasının tüm nedeni o tersine çevirme.</p>\n<p>Altında SPIRE iki bileşen ve iki attestation’dır:</p>\n<ul>\n<li><strong>spire-server</strong> CA’yı tutar, SVID’leri imzalar ve registration entry’leri saklar. CA’sı varsayılan olarak self-signed, ya da bir <code>UpstreamAuthority</code> plugin’i aracılığıyla kurumsal PKI’nıza zincirlenmiş bir intermediate’tir — bir kez verdiğiniz ve birlikte yaşadığınız bir karar.</li>\n<li><strong>spire-agent</strong> her node’da çalışır. Önce <em>node’u</em> sunucuya kanıtlar — <strong>node attestation</strong> — <code>k8s_psat</code> gibi bir plugin’le; bu, projeli bir service-account token’ını Kubernetes <code>TokenReview</code> API’si üzerinden doğrular. Node’a yerleştirilmiş bir paylaşılan sır yoktur. Sonra her yerel process için, ona bir SVID vermeden önce <strong>workload attestation</strong> yapar.</li>\n</ul>\n<p>Bir <strong>registration entry</strong>, bir SPIFFE ID’yi bir parent’a (node/agent) ve bir selector kümesine (namespace, service account, image) bağlayan sunucu tarafı kayıttır. Politika odur: <em>bu workload, bu şekilde attest edilmiş, bu kimliği alır.</em> Bu entry’lerin yaşam döngüsünü yönetmek gerçek bir iştir ve süregelen operasyonel maliyetin çoğu oradadır.</p>\n<h2 id=\"federasyon-başka-bir-kümeden-gelen-bir-svidye-güvenmek\">Federasyon: başka bir kümeden gelen bir SVID’ye güvenmek</h2>\n<p>Bir trust domain, bir güven köküdür. Çok kümeli bir filo genellikle birden fazla trust domain demektir — küme başına ya da bölge başına bir tane — ve artık A domain’indeki bir workload, B domain’inde basılmış bir SVID’yi doğrulayabilmek zorundadır. İşte bu <strong>federasyon</strong>’dur ve birimi <strong>trust bundle</strong>’dır: bir domain’in SVID’lerini doğrulamanızı sağlayan public CA sertifikaları ve JWT imzalama anahtarları (bir JWKS).</p>\n<p>Her domain bir <strong>bundle endpoint</strong> ayağa kaldırır — güncel bundle’ını sunan bir URL, OIDC’nin <code>jwks_uri</code>’sinin SPIFFE karşılığı — ve her peer güncel kalmak için onu poll eder. İki profil var ve fark, tam da bootstrap sorusudur:</p>\n<ul>\n<li><strong><code>https_web</code></strong> — endpoint, public bir CA’dan bir Web-PKI sertifikasıyla cephelenir. Peer onu sıradan public güven deposuyla doğrular, yani <strong>bant dışında hiçbir başlangıç bundle’ının değiş tokuş edilmesine gerek yoktur.</strong> Güven mevcut Web PKI üzerinden bootstrap olur.</li>\n<li><strong><code>https_spiffe</code></strong> — endpoint kendi X.509-SVID’siyle kimlik doğrular. Onunla ilk kez konuşmak için peer’in o domain’in başlangıç bundle’ını zaten elinde tutması gerekir. Yani <strong>ilk bundle bant dışında gelmek zorundadır</strong>, ondan sonra ileriye dönük olarak en son çekilen bundle kullanılır.</li>\n</ul>\n<p>Bir registration entry üzerinde <code>federatesWith</code>, bir workload’un kimlik doğrulamasına izin verilen yabancı trust domain’leri listeler; agent sonra o yabancı bundle’ları workload’a Workload API üzerinden teslim eder. Bundle’lar <code>spiffe_refresh_hint</code> aralığında (genelde beş dakika civarı) yenilenir ve yeni bir imzalama anahtarını, kullanmaya başlamadan birkaç yenileme döngüsü <em>önce</em> yayınlarsınız ki rotation indiğinde federe peer’ler onu çoktan öğrenmiş olsun.</p>\n<p>“Sıfır statik yetki”nin dürüst şekline dikkat: federasyon, bootstrap güven kararının <strong>yerini değiştirir</strong>, onu silmez. <code>https_spiffe</code> ile bir başlangıç bundle’ını elle gönderirsiniz; <code>https_web</code> ile public CA sistemine yaslanırsınız. Her iki yolda da güven kökü hâlâ <em>bir yerden</em> gelir — ortadan kaldırdığınız şey, uzun ömürlü, geniş kapsamlı, küme başına kimlik bilgisidir; tek seferlik güven çıpası değil. Bu doğru takastır, ama olduğu gibi adlandırın.</p>\n<h2 id=\"apiservera-bir-svidyi-kabul-ettirmek\">apiserver’a bir SVID’yi kabul ettirmek</h2>\n<p>Niyetin Kubernetes API’siyle buluştuğu ve yanılmanın kolay olduğu yer burası. İki mekanizma var ve bugün yalnızca biri temiz.</p>\n<p><strong>JWT-SVID-olarak-OIDC yolu (işleyen).</strong> Kubernetes’e harici bir OIDC issuer’a güvenmesi söylenebilir. SPIRE, <code>/.well-known/openid-configuration</code> ve SPIRE’ın JWT-SVID imzalama anahtarlarıyla beslenen bir JWKS sunan bir <strong>OIDC Discovery Provider</strong> ile gelir. Uzak (spoke) kümenin apiserver’ını o provider’a bir OIDC issuer olarak yöneltirsiniz. Controller, yerel Workload API’sinden taze bir JWT-SVID çeker ve onu bir bearer token olarak sunar; apiserver onu SPIRE’ın JWKS’ine karşı doğrular ve bir subject’e map eder. <code>Secret</code> yok, saklanan token yok — istek başına yeni bir JWT, dakikalar içinde sona eren.</p>\n<p>Bu bir düşünce deneyi değil. Red Hat, SPIFFE/SPIRE desteğini OpenShift 4.20’den itibaren Zero Trust Workload Identity Manager operatörü olarak sevk eder ve tam olarak bu bağlantıyı, OpenShift GitOps’un sevk ettiği bir ürün özelliği olarak değil, bir nasıl-yapılır rehberi olarak belgeler: Argo CD, <code>execProviderConfig</code> aracılığıyla bir client-go <code>ExecCredential</code> plugin’i (<code>apiVersion: client.authentication.k8s.io/v1beta1</code>) kullanır; bu plugin her çağrıda SPIFFE socket’ini (<code>SPIFFE_ENDPOINT_SOCKET</code>) okur, spoke apiserver’ın beklediği audience’la (<code>SPIFFE_JWT_AUDIENCE</code>) bir JWT-SVID ister ve onu teslim eder. Kimlik bilgisi kullanım anında imal edilir ve atılır.</p>\n<p><strong>X.509-SVID-ile-mTLS yolu (tuzak).</strong> İçgüdü düz mTLS yapmaktır: controller X.509-SVID’sini sunar, apiserver SPIFFE bundle’ına client CA olarak güvenir, bitti. Bitmedi. Kubernetes client-cert kimlik doğrulaması <strong>kullanıcı adını sertifikanın Subject CN’inden, grupları Subject O’sundan</strong> türetir — ve SPIFFE ID <strong>URI SAN</strong>’ında yaşar, ki apiserver client-cert kimlik doğrulaması onu okumaz. Bu yüzden ham bir SPIFFE X.509-SVID bir Kubernetes kullanıcısına map olmaz. mTLS kullanmak için apiserver’ın önüne bir proxy — Ghostunnel ya da Envoy — koyarsınız: SPIFFE mTLS’i terminate eder, URI-SAN SPIFFE ID’sini doğrular ve apiserver’ın anladığı bir kimlikle iletir. Ghostunnel kendi rotate olan SVID’leri için Workload API’yi doğrudan tüketir. Bu işler, ama her apiserver’ın önünde bir hareketli parçadır ve OIDC yolu onu hiç gerektirmez.</p>\n<p>Olgunluk konusunda dürüst olun. Upstream Argo CD’nin native SPIFFE kimlik doğrulaması yoktur; jenerik <code>ExecCredential</code> plugin örüntüsüyle çalışır ve en olgun ürünleşme OpenShift’inkidir. Flux, SPIRE’ı <em>deploy etmek</em> için GitOps ile yönetilebilir ve OCI registry kimlik doğrulaması bir JWT-SVID tüketebilir, ama “Flux’u bir uzak kümeye kubeconfig yerine SPIFFE ile doğrula” şeklinde birinci sınıf bir özellik belgelenmemiştir — aynı OIDC/exec-credential/proxy tesisatının üzerinden gider. Native uzak küme SPIFFE kimlik doğrulamasını, etkinleştirdiğiniz bir kutu değil, monte ettiğiniz bir örüntü olarak görün.</p>\n<h2 id=\"ne-kırılır-ve-maliyeti-ne\">Ne kırılır ve maliyeti ne</h2>\n<p>Mimari diyagramların atladığı kısım bu. Saklanan bir kimlik bilgisini kaldırdınız; karşılığında bir kimlik servisini kritik yolda sıkı bir bağımlılık hâline getirdiniz ve kısa ömürlü şeyler, uzun ömürlülerin kırılmadığı yerlerde kırılır.</p>\n<ul>\n<li><strong>SVID rotation artık bir erişilebilirlik bağımlılığı.</strong> SVID’ler kısadır — X.509 bir saat, JWT beş dakika — ve ömürlerinin yarısında rotate olur. Agent ya da Workload API ayakta değilse, ya da agent yenilemek için sunucuya ulaşamıyorsa, <strong>SVID sona erer ve kimlik doğrulama durur.</strong> SPIRE sunucusu her basım ve yenileme için kritik yoldadır. Agent’lar kimlik bilgilerini cache’ler ve <em>kısa</em> sunucu kesintilerine tolerans gösterir; agent’ın <code>availability_target</code> ayarı (ayarlanırsa ≥ 24h olmalı) onu erken rotate ettirerek nazik bir kesinti için pay biriktirir. Ama hata modu gerçek ve yeni: kimlik düzlemi ayakta değilse filo kimlik doğrulaması da ayakta değildir.</li>\n<li><strong>Saat kayması (clock skew) artık bir kesinti sınıfı.</strong> Beş dakikalık bir JWT affetmez. Bir controller ile bir spoke apiserver arasında birkaç dakikalık kayma, geçerli token’ları reddeder. Filo boyunca NTP disiplini, hijyen olmaktan çıkıp sıkı bir gereksinim olur.</li>\n<li><strong>Kararan bir federasyon bundle endpoint’i kümeler arası kimlik doğrulamasını kırar — sessizce, sonradan.</strong> Bir peer’in bundle endpoint’i yenileme penceresini aşacak kadar ulaşılamazsa ve o domain anahtarlarını rotate ederse, cache’lenmiş bundle’ınız bayatlar ve <strong>domain’ler arası SVID doğrulaması başarısız olur</strong>, her iki küme tek başına sağlıklı olsa bile. Hata, kesinti anında değil rotation anında ortaya çıkar, ki bu onu teşhis etmesi kötü kılar.</li>\n<li><strong>Kimlik düzlemi artık stateful, HA-kritik bir servis.</strong> HA’daki SPIRE sunucuları tek bir SQL datastore paylaşır; varsayılan SQLite tek-node’dur, yani production demek, <em>tüm filonun kimlik doğrulayabilme yeteneğinin</em> bağlı olduğu yüksek erişilebilir bir MySQL/PostgreSQL demektir. Çok küme için bir topoloji seçersiniz — nested SPIRE (bir root sunucunun downstream sunuculara intermediate basması, bir root kesintisinden sağ çıkması) ya da küme başına trust domain’ler arası federasyon — ve her biri kendi operasyonel yüzeyini ekler.</li>\n<li><strong>Operasyonel ağırlık baştan aşağı yeni bir kontrol düzlemi.</strong> Her kümede sunucu artı agent’lar; registration-entry yaşam döngüsü (workload başına selector’lar, deployment’larla senkron tutulan); upstream-CA kararı; bundle endpoint’ler ve federasyon ilişkileri; OIDC Discovery Provider; ve mTLS yolunda apiserver başına bir proxy. Cevap “bir kubeconfig sakla” olduğunda bunların hiçbiri yoktu.</li>\n</ul>\n<h2 id=\"sıra-neden-önemli\">Sıra neden önemli?</h2>\n<p>Adımlar bir menü değil; bir dizidir ve onu atlamak, ekiplerin maliyeti faydası olmadan nasıl aldığıdır.</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>1. SPIRE’ı kur: sunucu + agent’lar, node attestation        (temel)</span></span>\n<span class=\"line\"><span>2. Controller’a Workload API üzerinden workload SVID’leri bas</span></span>\n<span class=\"line\"><span>3. Spoke apiserver’a SPIRE’a güvendir (önce OIDC yolu)</span></span>\n<span class=\"line\"><span>4. Kümeler boyunca trust domain’leri federe et</span></span>\n<span class=\"line\"><span>5. Saklanan kubeconfig Secret’larını sil                     (kazanç)</span></span></code></pre>\n<ol start=\"5\">\n<li>adım tüm mesele ve yalnızca 1–4 gerçekten işlediğinde güvenli. Yaygın hata, eski <code>Secret</code>’ı “yedek olarak” tutmaktır — bu da uzun ömürlü filo geneli kimlik bilgisinin hâlâ atıl, hâlâ yedeklerinizde, hâlâ saldırganın aldığı şey olması ve artık ayrıca SPIRE çalıştırıyor olmanız demektir. Kontrol düzleminin parasını ödediniz ve yükümlülüğü tuttunuz. Ya saklanan kimlik bilgisi yok olur ya da bunu yapmadınız; yarım kredi yoktur.</li>\n</ol>\n<p>Tersi hata, sıra fayda verecek hâle gelmeden SPIRE’a uzanmaktır — <code>Secret</code>’ı gayet iyi olacak birkaç küme için federe bir kimlik düzlemi ayağa kaldırmak. Bu, güvenlik rozeti takmış <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">ileride-lazım-olur karmaşıklığıdır</a>: henüz sahip olmadığınız bir patlama yarıçapı için kurulmuş makine. Makineyi haklı çıkaran şey filo, denetim gereksinimi ve çalınan bir token’ın patlama yarıçapıdır. O çizginin altında, en ucuz güvenli kimlik bilgisi gerçekten de hiç basmak zorunda olmadığınız sıkıcı olanıdır.</p>\n<p>Bu yaklaşımın gerçekten bittiği yer açık: statik bir küme başına kimlik bilgisinin kabul edilemez bir patlama yarıçapı olduğu kadar büyük bir filo, stateful, HA, kritik yolda bir kimlik servisini çalıştıracak ve saatlerini senkron tutacak operasyonel olgunlukla. Oraya varmış bir ekip o olgunluğun parasını zaten başka yerde öder — ve o noktada sıfır statik yetki bir süsleme değil, patlama yarıçapının baştan beri talep ettiği kimlik bilgisi modelidir. Daha önce değil.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://spiffe.io/docs/latest/spiffe-about/overview/\">SPIFFE</a> ve <a href=\"https://spiffe.io/docs/latest/spire-about/\">SPIRE</a> belgeleri kimlik modelini ve sunucu/agent bileşenlerini tanımlar; <a href=\"https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/\">SPIFFE Federation spec’i</a> ise bu tasarımın dayandığı bundle-endpoint profillerini belirtir.</p>",
      "content_text": "> **Kısaca:** Diskte duran bir kubeconfig, tüm filonuza nişan almış uzun ömürlü bir kimlik bilgisidir — saldırganın tam da istediği şey. Onu, bir saatte sona eren ve hiç diske yazılmayan bir workload kimliğiyle değiştirin. İşte kural, sıra ve karşılığında üstlendiğiniz yeni kontrol düzlemi.\n\nBir GitOps controller’ı — Argo CD, Flux — bir kümede oturur ve birçoğunu reconcile eder. Diğerlerine ulaşmak için her uzak `kube-apiserver`’a kimlik doğrulaması yapması gerekir. Bunu yapmanın varsayılan yolu bir kimlik bilgisini saklamaktır: kontrol kümesinde bir Kubernetes `Secret` olarak tutulan bir bearer token, bir client sertifikası, bir kubeconfig. Argo CD’de bunlar `argocd.argoproj.io/secret-type: cluster` etiketli nesnelerdir.\n\nDur ve o `Secret`’ın ne olduğuna bak. O, **genelde geniş yetkili, tüm filonuza nişan almış, tek bir yerde atıl duran, uzun ömürlü bir kimlik bilgisidir.** Kendi kendine rotate olmaz. Yedeklerinizdedir. Kontrol kümenizde bir saldırganın bulabileceği en değerli tek şeydir, çünkü ona sahip olmak, eriştiği her kümeye sahip olmaktır. [Boring architecture](/tr/muhendislik/denemeler/neden-boring-architecture/) disiplininin tamamı, tutmak zorunda olmadığınız yükümlülükleri tutmamakla ilgilidir — ve saklanan filo geneli bir token, sırf kullanım anında taze ve kısa ömürlü bir tane basmak daha fazla iş gibi göründüğü için tuttuğunuz bir yükümlülüktür.\n\nBu yazı o fazladan işi, sırasıyla yapmakla ilgili. Hedef **sıfır statik yetki**: controller’ı bir uzak kümeye doğrulayan hiçbir uzun ömürlü kimlik bilgisi asla yazılı kalmaz. Controller’ın sunduğu kimlik talep anında basılır, yaklaşık bir saatte sona erer ve yalnızca bellekte yaşar. Mekanizma, kimlik modeli için SPIFFE ve onu basmak için SPIRE’dır.\n\nHer şeyden önce: bu, bir elin parmaklarıyla sayabileceğiniz üç küme için bir araç değil. SPIRE, import ettiğiniz bir kütüphane değil, işlettiğiniz bir kontrol düzlemidir — tam da yavaş üstlenmeniz gereken türden bir yetenek; tıpkı [bölünmeyi hak edene kadar modüler monolitin mikroservisi yendiği gibi](/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/). Ağırlığını ancak gerçekten bir filo işlettiğinizde hak eder — çok küme, çok ekip, çalınan bir `Secret`’ın geçemeyeceği bir denetim gereksinimi. Tek paylaşılan bir küme size yetiyorsa, en ucuz güvenli kimlik bilgisi, basmak için makineyi hiç kurmadığınız olandır. Geri kalanı, filoya gerçekten ulaştığınızı varsayar.\n\n## Tek kuralı baştan söyleyeyim\n\nMekanizmadan önce netleştireyim: **bir küme sınırını geçen kimlik bilgisi kısa ömürlü olmalı ve asla saklanmamalı.** Aşağıdaki her şey bu tek cümlenin hizmetinde. Sonunda kimlik doğrulama yolunda hâlâ bir yere diske yazılmış uzun ömürlü bir token kalıyorsa, uğruna geldiğiniz özelliği satın almadan karmaşıklık inşa etmişsinizdir — eksik bir index’ten kaçmak için read replica eklemekle aynı yanlış dönüş.\n\n\"Kısa ömürlü ve asla saklanmaz\"ın burada kesin bir anlamı var. Kimlik, varsayılan ömrü **bir saat** olan bir X.509 sertifikası (ya da varsayılan ömrü beş dakika olan bir JWT); workload tarafından yerel bir socket’ten, **sunacak kendine ait bir token’ı olmadan** çekilir ve ömrünün yarısında otomatik rotate edilir. Yarın çalmaya değecek bir sır yok.\n\n## SPIFFE ve SPIRE gerçekte ne\n\nSPIFFE bir spesifikasyon; SPIRE ise spesifikasyonun tarif ettiği şeyleri basan referans implementasyon. Dört isim tüm tasarımı taşıyor.\n\nBir **SPIFFE ID** bir URI’dir: `spiffe://<trust-domain>/<path>`, örneğin `spiffe://prod.example.org/ns/argocd/sa/application-controller`. Otorite kısmı **trust domain**’dir — tek bir mantıksal güven kökü, tek bir CA. Path workload’u tanımlar. Bir workload’un elinde tuttuğunu kanıtladığı isim budur.\n\nBir **SVID** (SPIFFE Verifiable Identity Document) ise kanıttır. İki biçim:\n\n- **X.509-SVID** — SPIFFE ID’si URI SAN’ında (CN’de değil, ki bu sonra önemli olacak) bulunan bir X.509 sertifikası. Onunla mTLS yaparsınız.\n- **JWT-SVID** — `sub`’ı SPIFFE ID, `aud`’unu hedef doğrulayıcıya scope’ladığın imzalı bir JWT. Onu bearer token olarak sunarsınız.\n\nAsıl mesele ömürler. SPIRE’ın sunucu konfigürasyonunda varsayılanlar `default_x509_svid_ttl: 1h` ve `default_jwt_svid_ttl: 5m`’dir; bu SVID’ler, kendi `ca_ttl`’i varsayılan `24h` olan bir CA tarafından imzalanır. Bunlar bilerek kısadır: sızdırılmış bir SVID bir saat içinde değersizleşir, ki tam da bu yüzden eskiden token sakladığınız yerde onu kullanmak güvenlidir.\n\n\"Asla saklanmaz\"ı mümkün kılan şey, bir SPIRE **agent**’ı tarafından yerel bir Unix domain socket üzerinden sunulan **Workload API**’dir (belgelenmiş varsayılan `/tmp/spire-agent/public/api.sock`; çoğu kurulum onu `/run/spire/sockets/...` altında mount eder). Workload o socket’e bağlanır ve SVID’sini ister. **Bunu yapmak için hiçbir kimlik bilgisi sunmaz.** Agent, çağıran process’i kernel üzerinden inceleyerek tanır — PID’sini, oradan da container’ını, namespace’ini, service account’unu — ve bir dizi selector’a karşı *workload attestation* yapar. Kimlik, *hangi sırrı tuttuğuyla* değil, *process’in doğrulanabilir biçimde ne olduğuyla* kurulur. Saklanacak bir şeyin olmamasının tüm nedeni o tersine çevirme.\n\nAltında SPIRE iki bileşen ve iki attestation’dır:\n\n- **spire-server** CA’yı tutar, SVID’leri imzalar ve registration entry’leri saklar. CA’sı varsayılan olarak self-signed, ya da bir `UpstreamAuthority` plugin’i aracılığıyla kurumsal PKI’nıza zincirlenmiş bir intermediate’tir — bir kez verdiğiniz ve birlikte yaşadığınız bir karar.\n- **spire-agent** her node’da çalışır. Önce *node’u* sunucuya kanıtlar — **node attestation** — `k8s_psat` gibi bir plugin’le; bu, projeli bir service-account token’ını Kubernetes `TokenReview` API’si üzerinden doğrular. Node’a yerleştirilmiş bir paylaşılan sır yoktur. Sonra her yerel process için, ona bir SVID vermeden önce **workload attestation** yapar.\n\nBir **registration entry**, bir SPIFFE ID’yi bir parent’a (node/agent) ve bir selector kümesine (namespace, service account, image) bağlayan sunucu tarafı kayıttır. Politika odur: *bu workload, bu şekilde attest edilmiş, bu kimliği alır.* Bu entry’lerin yaşam döngüsünü yönetmek gerçek bir iştir ve süregelen operasyonel maliyetin çoğu oradadır.\n\n## Federasyon: başka bir kümeden gelen bir SVID’ye güvenmek\n\nBir trust domain, bir güven köküdür. Çok kümeli bir filo genellikle birden fazla trust domain demektir — küme başına ya da bölge başına bir tane — ve artık A domain’indeki bir workload, B domain’inde basılmış bir SVID’yi doğrulayabilmek zorundadır. İşte bu **federasyon**’dur ve birimi **trust bundle**’dır: bir domain’in SVID’lerini doğrulamanızı sağlayan public CA sertifikaları ve JWT imzalama anahtarları (bir JWKS).\n\nHer domain bir **bundle endpoint** ayağa kaldırır — güncel bundle’ını sunan bir URL, OIDC’nin `jwks_uri`’sinin SPIFFE karşılığı — ve her peer güncel kalmak için onu poll eder. İki profil var ve fark, tam da bootstrap sorusudur:\n\n- **`https_web`** — endpoint, public bir CA’dan bir Web-PKI sertifikasıyla cephelenir. Peer onu sıradan public güven deposuyla doğrular, yani **bant dışında hiçbir başlangıç bundle’ının değiş tokuş edilmesine gerek yoktur.** Güven mevcut Web PKI üzerinden bootstrap olur.\n- **`https_spiffe`** — endpoint kendi X.509-SVID’siyle kimlik doğrular. Onunla ilk kez konuşmak için peer’in o domain’in başlangıç bundle’ını zaten elinde tutması gerekir. Yani **ilk bundle bant dışında gelmek zorundadır**, ondan sonra ileriye dönük olarak en son çekilen bundle kullanılır.\n\nBir registration entry üzerinde `federatesWith`, bir workload’un kimlik doğrulamasına izin verilen yabancı trust domain’leri listeler; agent sonra o yabancı bundle’ları workload’a Workload API üzerinden teslim eder. Bundle’lar `spiffe_refresh_hint` aralığında (genelde beş dakika civarı) yenilenir ve yeni bir imzalama anahtarını, kullanmaya başlamadan birkaç yenileme döngüsü *önce* yayınlarsınız ki rotation indiğinde federe peer’ler onu çoktan öğrenmiş olsun.\n\n\"Sıfır statik yetki\"nin dürüst şekline dikkat: federasyon, bootstrap güven kararının **yerini değiştirir**, onu silmez. `https_spiffe` ile bir başlangıç bundle’ını elle gönderirsiniz; `https_web` ile public CA sistemine yaslanırsınız. Her iki yolda da güven kökü hâlâ *bir yerden* gelir — ortadan kaldırdığınız şey, uzun ömürlü, geniş kapsamlı, küme başına kimlik bilgisidir; tek seferlik güven çıpası değil. Bu doğru takastır, ama olduğu gibi adlandırın.\n\n## apiserver’a bir SVID’yi kabul ettirmek\n\nNiyetin Kubernetes API’siyle buluştuğu ve yanılmanın kolay olduğu yer burası. İki mekanizma var ve bugün yalnızca biri temiz.\n\n**JWT-SVID-olarak-OIDC yolu (işleyen).** Kubernetes’e harici bir OIDC issuer’a güvenmesi söylenebilir. SPIRE, `/.well-known/openid-configuration` ve SPIRE’ın JWT-SVID imzalama anahtarlarıyla beslenen bir JWKS sunan bir **OIDC Discovery Provider** ile gelir. Uzak (spoke) kümenin apiserver’ını o provider’a bir OIDC issuer olarak yöneltirsiniz. Controller, yerel Workload API’sinden taze bir JWT-SVID çeker ve onu bir bearer token olarak sunar; apiserver onu SPIRE’ın JWKS’ine karşı doğrular ve bir subject’e map eder. `Secret` yok, saklanan token yok — istek başına yeni bir JWT, dakikalar içinde sona eren.\n\nBu bir düşünce deneyi değil. Red Hat, SPIFFE/SPIRE desteğini OpenShift 4.20’den itibaren Zero Trust Workload Identity Manager operatörü olarak sevk eder ve tam olarak bu bağlantıyı, OpenShift GitOps’un sevk ettiği bir ürün özelliği olarak değil, bir nasıl-yapılır rehberi olarak belgeler: Argo CD, `execProviderConfig` aracılığıyla bir client-go `ExecCredential` plugin’i (`apiVersion: client.authentication.k8s.io/v1beta1`) kullanır; bu plugin her çağrıda SPIFFE socket’ini (`SPIFFE_ENDPOINT_SOCKET`) okur, spoke apiserver’ın beklediği audience’la (`SPIFFE_JWT_AUDIENCE`) bir JWT-SVID ister ve onu teslim eder. Kimlik bilgisi kullanım anında imal edilir ve atılır.\n\n**X.509-SVID-ile-mTLS yolu (tuzak).** İçgüdü düz mTLS yapmaktır: controller X.509-SVID’sini sunar, apiserver SPIFFE bundle’ına client CA olarak güvenir, bitti. Bitmedi. Kubernetes client-cert kimlik doğrulaması **kullanıcı adını sertifikanın Subject CN’inden, grupları Subject O’sundan** türetir — ve SPIFFE ID **URI SAN**’ında yaşar, ki apiserver client-cert kimlik doğrulaması onu okumaz. Bu yüzden ham bir SPIFFE X.509-SVID bir Kubernetes kullanıcısına map olmaz. mTLS kullanmak için apiserver’ın önüne bir proxy — Ghostunnel ya da Envoy — koyarsınız: SPIFFE mTLS’i terminate eder, URI-SAN SPIFFE ID’sini doğrular ve apiserver’ın anladığı bir kimlikle iletir. Ghostunnel kendi rotate olan SVID’leri için Workload API’yi doğrudan tüketir. Bu işler, ama her apiserver’ın önünde bir hareketli parçadır ve OIDC yolu onu hiç gerektirmez.\n\nOlgunluk konusunda dürüst olun. Upstream Argo CD’nin native SPIFFE kimlik doğrulaması yoktur; jenerik `ExecCredential` plugin örüntüsüyle çalışır ve en olgun ürünleşme OpenShift’inkidir. Flux, SPIRE’ı *deploy etmek* için GitOps ile yönetilebilir ve OCI registry kimlik doğrulaması bir JWT-SVID tüketebilir, ama \"Flux’u bir uzak kümeye kubeconfig yerine SPIFFE ile doğrula\" şeklinde birinci sınıf bir özellik belgelenmemiştir — aynı OIDC/exec-credential/proxy tesisatının üzerinden gider. Native uzak küme SPIFFE kimlik doğrulamasını, etkinleştirdiğiniz bir kutu değil, monte ettiğiniz bir örüntü olarak görün.\n\n## Ne kırılır ve maliyeti ne\n\nMimari diyagramların atladığı kısım bu. Saklanan bir kimlik bilgisini kaldırdınız; karşılığında bir kimlik servisini kritik yolda sıkı bir bağımlılık hâline getirdiniz ve kısa ömürlü şeyler, uzun ömürlülerin kırılmadığı yerlerde kırılır.\n\n- **SVID rotation artık bir erişilebilirlik bağımlılığı.** SVID’ler kısadır — X.509 bir saat, JWT beş dakika — ve ömürlerinin yarısında rotate olur. Agent ya da Workload API ayakta değilse, ya da agent yenilemek için sunucuya ulaşamıyorsa, **SVID sona erer ve kimlik doğrulama durur.** SPIRE sunucusu her basım ve yenileme için kritik yoldadır. Agent’lar kimlik bilgilerini cache’ler ve *kısa* sunucu kesintilerine tolerans gösterir; agent’ın `availability_target` ayarı (ayarlanırsa ≥ 24h olmalı) onu erken rotate ettirerek nazik bir kesinti için pay biriktirir. Ama hata modu gerçek ve yeni: kimlik düzlemi ayakta değilse filo kimlik doğrulaması da ayakta değildir.\n- **Saat kayması (clock skew) artık bir kesinti sınıfı.** Beş dakikalık bir JWT affetmez. Bir controller ile bir spoke apiserver arasında birkaç dakikalık kayma, geçerli token’ları reddeder. Filo boyunca NTP disiplini, hijyen olmaktan çıkıp sıkı bir gereksinim olur.\n- **Kararan bir federasyon bundle endpoint’i kümeler arası kimlik doğrulamasını kırar — sessizce, sonradan.** Bir peer’in bundle endpoint’i yenileme penceresini aşacak kadar ulaşılamazsa ve o domain anahtarlarını rotate ederse, cache’lenmiş bundle’ınız bayatlar ve **domain’ler arası SVID doğrulaması başarısız olur**, her iki küme tek başına sağlıklı olsa bile. Hata, kesinti anında değil rotation anında ortaya çıkar, ki bu onu teşhis etmesi kötü kılar.\n- **Kimlik düzlemi artık stateful, HA-kritik bir servis.** HA’daki SPIRE sunucuları tek bir SQL datastore paylaşır; varsayılan SQLite tek-node’dur, yani production demek, *tüm filonun kimlik doğrulayabilme yeteneğinin* bağlı olduğu yüksek erişilebilir bir MySQL/PostgreSQL demektir. Çok küme için bir topoloji seçersiniz — nested SPIRE (bir root sunucunun downstream sunuculara intermediate basması, bir root kesintisinden sağ çıkması) ya da küme başına trust domain’ler arası federasyon — ve her biri kendi operasyonel yüzeyini ekler.\n- **Operasyonel ağırlık baştan aşağı yeni bir kontrol düzlemi.** Her kümede sunucu artı agent’lar; registration-entry yaşam döngüsü (workload başına selector’lar, deployment’larla senkron tutulan); upstream-CA kararı; bundle endpoint’ler ve federasyon ilişkileri; OIDC Discovery Provider; ve mTLS yolunda apiserver başına bir proxy. Cevap \"bir kubeconfig sakla\" olduğunda bunların hiçbiri yoktu.\n\n## Sıra neden önemli?\n\nAdımlar bir menü değil; bir dizidir ve onu atlamak, ekiplerin maliyeti faydası olmadan nasıl aldığıdır.\n\n```\n1. SPIRE’ı kur: sunucu + agent’lar, node attestation        (temel)\n2. Controller’a Workload API üzerinden workload SVID’leri bas\n3. Spoke apiserver’a SPIRE’a güvendir (önce OIDC yolu)\n4. Kümeler boyunca trust domain’leri federe et\n5. Saklanan kubeconfig Secret’larını sil                     (kazanç)\n```\n\n5. adım tüm mesele ve yalnızca 1–4 gerçekten işlediğinde güvenli. Yaygın hata, eski `Secret`’ı \"yedek olarak\" tutmaktır — bu da uzun ömürlü filo geneli kimlik bilgisinin hâlâ atıl, hâlâ yedeklerinizde, hâlâ saldırganın aldığı şey olması ve artık ayrıca SPIRE çalıştırıyor olmanız demektir. Kontrol düzleminin parasını ödediniz ve yükümlülüğü tuttunuz. Ya saklanan kimlik bilgisi yok olur ya da bunu yapmadınız; yarım kredi yoktur.\n\nTersi hata, sıra fayda verecek hâle gelmeden SPIRE’a uzanmaktır — `Secret`’ı gayet iyi olacak birkaç küme için federe bir kimlik düzlemi ayağa kaldırmak. Bu, güvenlik rozeti takmış [ileride-lazım-olur karmaşıklığıdır](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/): henüz sahip olmadığınız bir patlama yarıçapı için kurulmuş makine. Makineyi haklı çıkaran şey filo, denetim gereksinimi ve çalınan bir token’ın patlama yarıçapıdır. O çizginin altında, en ucuz güvenli kimlik bilgisi gerçekten de hiç basmak zorunda olmadığınız sıkıcı olanıdır.\n\nBu yaklaşımın gerçekten bittiği yer açık: statik bir küme başına kimlik bilgisinin kabul edilemez bir patlama yarıçapı olduğu kadar büyük bir filo, stateful, HA, kritik yolda bir kimlik servisini çalıştıracak ve saatlerini senkron tutacak operasyonel olgunlukla. Oraya varmış bir ekip o olgunluğun parasını zaten başka yerde öder — ve o noktada sıfır statik yetki bir süsleme değil, patlama yarıçapının baştan beri talep ettiği kimlik bilgisi modelidir. Daha önce değil.\n\n---\n\n*İlgili:* [SPIFFE](https://spiffe.io/docs/latest/spiffe-about/overview/) ve [SPIRE](https://spiffe.io/docs/latest/spire-about/) belgeleri kimlik modelini ve sunucu/agent bileşenlerini tanımlar; [SPIFFE Federation spec’i](https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/) ise bu tasarımın dayandığı bundle-endpoint profillerini belirtir.\n\n## Adımlar\n\n**Çok kümeli GitOps’ta statik yetkiyi kaldırmak** — Diskte duran uzun ömürlü kubeconfig’leri, bir saatte sona eren workload kimlikleriyle değiştirmek.\n\n1. **SPIRE’ı kur** — Sunucu ve agent’ları ayağa kaldır, node attestation’ı kur. Temel bu.\n2. **Controller’a SVID bas** — GitOps controller’a Workload API üzerinden workload SVID’leri ver.\n3. **Spoke’u güvendir** — Spoke apiserver’a SPIRE’a güvenmeyi öğret — önce OIDC yolu.\n4. **Trust domain’leri federe et** — Kümeler boyunca trust domain’leri federe et.\n5. **Saklanan Secret’ları sil** — Kubeconfig Secret’larını sil. Kazanç bu adımda ve yalnızca 1–4 gerçekten işlediğinde güvenli; eskisini \"yedek olarak\" tutmak yükümlülüğü sürdürür.\n\n## Kaynaklar\n\n- [SPIRE server configuration: default_x509_svid_ttl 1h, default_jwt_svid_ttl 5m, ca_ttl 24h](https://spiffe.io/docs/latest/deploying/spire_server/) — SPIFFE\n- [SPIFFE Federation: the https_web and https_spiffe bundle endpoint profiles](https://spiffe.io/docs/latest/spiffe-specs/spiffe_federation/) — SPIFFE\n- [Kubernetes authentication: X509 client certificates take the username from the Subject CN and groups from the Organization fields](https://kubernetes.io/docs/reference/access-authn-authz/authentication/) — Kubernetes\n- [Argo CD declarative setup: remote clusters are stored as Secrets labelled argocd.argoproj.io/secret-type: cluster](https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/) — Argo CD\n\n## Sık sorulanlar\n\n### apiserver bir SPIFFE X.509-SVID'sine neden doğrudan güvenemiyor?\n\nKubernetes client-cert kimlik doğrulaması kullanıcı adını sertifikanın Subject CN'inden, grupları Organization alanlarından türetir. SPIFFE ID ise URI SAN'da yaşar ve o yol URI SAN'ı okumaz; dolayısıyla ham bir X.509-SVID hiçbir Kubernetes kullanıcısına eşlenmez. Ya JWT-SVID-olarak-OIDC yolunu seçersiniz ya da öne kimliği çeviren bir proxy koyarsınız.\n\n### Bu kimlik bilgileri tam olarak ne kadar kısa ömürlü?\n\nSPIRE sunucu varsayılanları X.509-SVID için bir saat, JWT-SVID için beş dakika; imzalayan CA'nın kendi TTL varsayılanı 24 saat ve rotation ömrün yarısında oluyor. Sızdırılmış bir SVID'yi bir saat içinde değersiz kılacak kadar kısa — ve kümeler arası saat kaymasını bir kesinti sınıfına dönüştürecek kadar da kısa.\n\n### Sıfır statik yetki, bootstrap güvenini tamamen ortadan kaldırır mı?\n\nHayır, yerini değiştirir. https_spiffe bundle endpoint profilinde bir başlangıç bundle'ını hâlâ bant dışında iletirsiniz; https_web'de public CA sistemine yaslanırsınız. Ortadan kalkan şey uzun ömürlü, geniş kapsamlı, küme başına kimlik bilgisidir — tek seferlik güven çıpası değil.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "kubernetes",
        "gitops",
        "spiffe",
        "security",
        "architecture"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/sistemler/ebpf-ile-gpu-finops/",
      "url": "https://muhammetsafak.com/tr/muhendislik/sistemler/ebpf-ile-gpu-finops/",
      "title": "eBPF ile GPU FinOps",
      "summary": "Bütün bir GPU için faturalandırılıyorsunuz ve onun %3’ünü kullanıyorsunuz — ve monitoring’iniz size o %3’ün kimin olduğunu söyleyemiyor. eBPF, syscall sınırını izleyerek GPU işini bir pod’a atfeder; DCGM ise silikonun ne kadar iyi koştuğunu ölçer. Farklı sorulara cevap verirler ve gerçek bir chargeback her ikisine de ihtiyaç duyar.",
      "content_html": "<p>Bir GPU node’u, bir raf dolusu CPU makinesinden saatte daha fazlaya mal olur ve finansın onun hakkında sorduğu soru, her şey hakkında sorduğu soruyla aynıdır: <strong>bunu kim kullandı ve ne kadar?</strong> CPU, bellek ve disk için bunu cevaplayabilirsiniz — cgroup’lar bunu process başına hesaplar, cAdvisor pod başına raporlar, Prometheus ekip başına toplar. GPU içinse çoğu kümede dürüst cevap, kimsenin bilmediğidir. Filodaki en pahalı kaynağın parasını ödüyorsunuz ve onun tek bir kuruşunu bile atfedemiyorsunuz.</p>\n<p>Bu, doldurmayı unuttuğunuz bir araç boşluğu değil. Yapısaldır: GPU, <strong>yığınınızın geri kalanının dayandığı muhasebe düzleminin dışındadır.</strong> Bu yazı, bunun nedeni ve atfetmeyi geri kazanmak için eBPF kullanımı ile ilgili — ve eBPF’in ne görebildiği ve ne göremediğine dair sert bir çizgiyle birlikte, çünkü buradaki en pahalı hata, bir aracın fiziksel olarak cevaplayamadığı bir soruyu cevapladığına inanmaktır.</p>\n<h2 id=\"gpu-mevcut-metriklerinize-neden-görünmez\">GPU mevcut metriklerinize neden görünmez?</h2>\n<p>Üç ayrı körlük üst üste yığılır.</p>\n<p><strong>cgroup’lar GPU’yu hesaplamaz.</strong> Bir cgroup, CPU, bellek, block I/O ve PID’leri hesaplayan ve sınırlayan bir kernel yapısıdır. GPU bunların hiçbiri değil. Compute ve framebuffer (VRAM), NVIDIA kernel driver’ı ve userspace CUDA runtime’ı tarafından yönetilir ve cgroup hesaplamasına opaktır. Bir <code>device</code> cgroup controller’ı var, ama o yalnızca device node’larına (<code>/dev/nvidia0</code> ve dostları) <em>erişimi geçitler</em> — izin ver ya da reddet. Tek bir saniye GPU zamanını ya da bir megabayt VRAM’i hesaplamaz. Yani size pod başına CPU ve bellek veren tüm cAdvisor → Prometheus boru hattının, kuruluş gereği, <strong>GPU hakkında söyleyecek hiçbir şeyi yoktur.</strong></p>\n<p><strong><code>nvidia.com/gpu</code> bir tahsis sayısıdır, bir kullanım sinyali değil.</strong> Kubernetes GPU’ları, onları kubelet’e extended resource <code>nvidia.com/gpu</code> olarak kaydeden NVIDIA device plugin’i üzerinden öğrenir. Pod’lar bunu bir integer olarak ister — bütün GPU’lar. Scheduler istenen <em>sayıyı</em> mevcut <em>sayıyla</em> eşler. Tahsis katmanının bildiği tek şey budur. <strong>Bir GPU istemek, bir GPU kullanmak değildir.</strong> Bir pod bütün bir cihazı tutabilir — ve bütün cihazın faturasını ödeyebilir — onu %3’te sürerken. Üzerinden chargeback yapabileceğiniz tahsis sayısı, tam da tüketim hakkında size hiçbir şey söylemeyen sayıdır.</p>\n<p><strong><code>nvidia-smi</code>’nin kullanım sayısı bile size yalan söyler.</strong> <code>nvidia-smi</code>’ye uzanırsınız ve <code>utilization.gpu</code> alanı kurtuluş gibi görünür. Sandığınız şey değil. NVML onu şöyle tanımlar: <em>örnekleme periyodu boyunca bir ya da daha fazla kernel’in çalıştığı zamanın yüzdesi.</em> <strong>Zamansal varlığı — GPU hiç meşgul muydu — ölçer, ne kadarının meşgul olduğunu değil.</strong> Onlarca SM’den birini işgal eden tek thread’lik bir kernel %100’e yakın raporlayabilir. Microsoft, A100’ler üzerinde 8B parametreli bir modeli servis ederken bellek-bağımlı decode fazında %10’un altında compute kullanımı raporlamıştır — naif bir “kullanım” okuması o GPU’ları dolu sayacakken. <code>utilization.gpu</code> üzerinden chargeback yaparsanız, silikon neredeyse boştayken “meşgul” diyen bir sayı üzerinden faturalandırıyorsunuzdur.</p>\n<p>Yani: tahsis katmanı kullanımı-değil-sayıyı bilir, cgroup’lar hiçbir şey bilmez ve tek kolay yüzde yanlış şeyi ölçer. Boşluk budur.</p>\n<h2 id=\"i̇ki-soru-ve-neden-farklı-araçlar-gerektirdikleri\">İki soru ve neden farklı araçlar gerektirdikleri</h2>\n<p>Herhangi bir araçtan önce iki soruyu ayırın, çünkü onları karıştırmak temel hatadır — <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/\">bir aracın çözdüğü problemi eldeki problemle karıştırmakla</a> aynı şekil.</p>\n<ul>\n<li><strong>Atfetme — GPU işini <em>kim</em> yaptı ve <em>ne kadar</em>?</strong> Bu FinOps sorusudur. Pod başına, ekip başına GPU aktivitesi hesabı gerektirir: hangi workload kernel başlattı, VRAM tahsis etti, veri taşıdı ve ne kadar süreyle. Chargeback bunun üzerinden koşar.</li>\n<li><strong>Verimlilik — silikon <em>ne kadar iyi</em> kullanıldı?</strong> SM’ler gerçekten doluydu mu, tensor core’lar aktif miydi, darboğaz bellek bant genişliği miydi? Bu performans sorusudur. Bir ekibin harcamasının <em>haklı</em> olup olmadığını söyler, ama <em>kimin</em> harcaması olduğunu söyleyemez.</li>\n</ul>\n<p>Bunlar iki farklı ölçüm düzlemine eşlenir ve aşağıdaki her şeyi örgütleyen kural şu: <strong>eBPF atfetmeyi cevaplar; GPU’nun kendi sayaçları verimliliği cevaplar. Hiçbiri diğerinin yerine geçmez.</strong></p>\n<h2 id=\"ebpfin-görebildiği-kontrol-düzlemi\">eBPF’in görebildiği: kontrol düzlemi</h2>\n<p>eBPF kernel’de çalışır ve kprobe’lara, uprobe’lara, tracepoint’lere ve syscall’lara takılır. Okunacak bir GPU-kullanımı tracepoint’i yok — ama GPU <em>işinin talep edildiği</em> iki yüzey var ve ikisi de kernel’den görünür:</p>\n<ul>\n<li><strong>Driver ioctl sınırı.</strong> Her parça GPU işi — kernel gönderimi, bellek tahsisi, senkronizasyon — sonunda <code>/dev/nvidiactl</code>’a ve <code>/dev/nvidia0…N</code>’e bir <code>ioctl()</code> olur. Driver’ın giriş noktalarındaki (<code>nvidia_unlocked_ioctl</code>, <code>nvidia_open</code>) bir kprobe o trafiği görür. (Kapalı driver tarihsel olarak yalnızca tek bir tracepoint, donanım hata olayları için <code>nvidia:nvidia_dev_xid</code>, sunar; geri kalan her şey kprobe’lanır.)</li>\n<li><strong>CUDA kütüphane sınırı.</strong> <code>libcuda.so</code> / <code>libcudart.so</code> üzerindeki bir uprobe API’nin kendisini izler: <code>cuLaunchKernel</code>, <code>cuMemAlloc</code>, <code>cuMemcpyHtoD</code>, <code>cuStreamSynchronize</code> ve dostları. Bir giriş uprobe’unu bir dönüş uretprobe’uyla eşlemek her çağrının süresini ölçer.</li>\n</ul>\n<p>Bunun <em>atfetme</em> olmasının nedeni, eBPF’in her hook noktasında çağıran PID/TGID’yi ve cgroup’u native olarak okumasıdır — <code>bpf_get_current_pid_tgid</code> ve cgroup id tam orada. Bu size <strong>PID → cgroup → pod → namespace → ekip</strong> zincirini verir, uygulama değişikliği olmadan ve NVIDIA’nın işbirliği olmadan. Pod başına şunları alırsınız: kernel-başlatma sayıları, başlatma boyutları, VRAM tahsis boyutları, memcpy hacmi ve yönü, çağrı zamanlaması. Bu, tamamen sınırın kernel tarafından türetilen, gerçek ve savunulabilir bir chargeback sinyalidir.</p>\n<p>Buradaki ekosistem genç ama gerçek. Primitifler bugün işliyor — CUDA kütüphanelerini uprobe’layan ve nvidia ioctl yolunu kprobe’layan çalışan yazılar ve eğitimler var ve <code>bpftime</code> eBPF mantığını GPU olaylarına bağlamayı keşfediyor. Tetragon (Cilium), Kubernetes pod kimliğiyle jenerik <code>process_uprobe</code>, <code>process_kprobe</code> ve ioctl izleme sevk eder, yani CUDA sembollerine ya da ioctl yoluna <em>yöneltilebilir</em> — ama bunun sevk edilen bir “GPU FinOps” özelliği değil, yazdığınız bir <code>TracingPolicy</code> olduğunu anlayın. Henüz baskın, anahtar-teslim bir eBPF chargeback ürünü yok. Size “tak-çalıştır eBPF GPU FinOps” satan biri, ekosistemin şu an ulaştığından öteye satıyordur.</p>\n<h2 id=\"ebpfin-göremediği-silikon\">eBPF’in göremediği: silikon</h2>\n<p>Dürüst limit bu ve bir sürümde düzeltilecek bir olgunluk problemi değil — verinin nerede yaşadığının fiziği.</p>\n<p>eBPF <strong>kontrol düzlemini</strong> görür: API çağrıları, ioctl’lar, tahsis boyutları, başlatma sayıları ve izleyebildiğiniz yerde gönderim-tamamlanma zamanlaması. GPU’nun <em>içini</em> <strong>görmez</strong>. SM occupancy’sini, tensor-core kullanımını ya da erişilen bellek bant genişliğini okuyamaz, çünkü bunlar <em>cihazda</em> yaşayan ve yalnızca NVML / DCGM / CUPTI üzerinden açığa çıkan donanım performans sayaçlarıdır. eBPF’in onlara giden bir yolu yoktur. Bir pod’un on bin kernel başlattığını ve 40 GB VRAM tahsis ettiğini söyleyebilir; o kernel’lerin SM’leri doyurup doyurmadığını ya da onları %90 boş bırakıp bırakmadığını söyleyemez.</p>\n<p>İkinci, daha incelikli bir limit var. eBPF’in takılabildiği ioctl sınırında bile, <em>payload’lar</em> büyük ölçüde opaktır. Driver’ın komut yapıları (<code>NV_ESC_*</code> Resource Manager API’si) karmaşık ve fiilen proprietary. Bir ioctl’ın <em>olduğunu</em> görebilirsiniz — komut numarasını, çağıran PID’yi, zamanlamayı — ama keyfi bir RM payload’unun semantik içeriğini decode etmek pratik dışı ve kırılgandır. İşin gerçeğini ve atfetmesini alırsınız, anlamının bedava bir okumasını değil.</p>\n<p>Cazip bir kapıyı kapatan bir not: NVIDIA’nın açık kernel modülleri (<code>open-gpu-kernel-modules</code>, Turing ve sonrası) <strong>kernel arayüz katmanını</strong> açar — modül init’ini, ioctl giriş noktalarını, <code>NV_ESC_*</code> komut yüzeyini. Bu, <em>neyi</em> hook’layacağınızı anlamanıza gerçekten yardımcı olur. Ama GPU’nun beyni kapalı kalır: Turing+’ta yönetimin çoğu GPU üzerindeki GSP firmware’inde koşar ve o firmware — modüllerin ihtiyaç duyduğu user-mode driver bileşenleriyle birlikte — hâlâ kapalı olarak dağıtılır. Kernel modüllerini açmak, eBPF’e donanım performans sayaçlarını <strong>açmaz</strong>. Cihaz-içi limit değişmemiştir. Açık modüllerin eBPF’e SM occupancy okuttuğunu iddia eden biri, mevcut kanıtlara göre yanılıyordur.</p>\n<h2 id=\"hâlâ-ihtiyaç-duyduğunuz-userspace-yolu-dcgm\">Hâlâ ihtiyaç duyduğunuz userspace yolu: DCGM</h2>\n<p>eBPF silikonu göremediği için, cevabın verimlilik yarısı userspace’ten gelir ve standart araç, Prometheus için <strong>dcgm-exporter</strong> ile NVIDIA <strong>DCGM</strong> (Data Center GPU Manager)’dır. DCGM, GPU’nun donanım sayaçlarını driver üzerinden okur ve eBPF’in erişemediği alanları açığa çıkarır:</p>\n<ul>\n<li><code>DCGM_FI_PROF_SM_ACTIVE</code> — bir multiprocessor’da en az bir warp’ın aktif olduğu zamanın oranı, hepsinin ortalaması.</li>\n<li><code>DCGM_FI_PROF_SM_OCCUPANCY</code> — resident warp’ların desteklenen maksimuma oranı: <em>gerçek</em> occupancy.</li>\n<li><code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE</code> — tensor pipe’ın aktif olduğu döngülerin oranı.</li>\n<li><code>DCGM_FI_PROF_DRAM_ACTIVE</code> — bir bellek-bant-genişliği vekili.</li>\n<li><code>DCGM_FI_DEV_FB_USED</code> — gerçekten kullanımda olan framebuffer (VRAM).</li>\n</ul>\n<p>Bunlar size bir ekibin pahalı tahsisinin ekmeğini kazanıp kazanmadığını söyleyen sayılardır. DCGM onları pod’lara, her GPU UUID’yi onu tutan pod’a eşleyen kubelet <strong>Pod Resources API</strong>’si (<code>/var/lib/kubelet/pod-resources</code> adresindeki bir gRPC servisi) üzerinden atfeder.</p>\n<p>Ama DCGM’in atfetmesinin, tam da eBPF’in yerini hak ettiği yerde sert bir kenarı var: <strong>DCGM cihaz-seviyesi metrikler üretir, bu yüzden tek bir fiziksel GPU’yu paylaşan tüketicileri ayırt edemez.</strong> Time-slicing ya da MPS (Multi-Process Service) altında, bir GPU’yu paylaşan tüm pod’lar <em>aynı, kopyalanmış</em> cihaz-seviyesi değerleri alır — <code>DCGM_FI_DEV_FB_USED</code> dahil. NVIDIA’nın kendi exporter’ı, time-slicing etkinken metrikleri container’larla ilişkilendirmediğini belgeler; <code>--kubernetes-virtual-gpus=true</code> ile her paylaşan pod tüm fiziksel GPU’nun durumunu aynalar. MIG altında atfetme, keyfi pod’lara değil GPU-instance seviyesine kayar. Yani tam da paylaşılan-GPU durumunda — ki bu durum, bütün-GPU tahsisi para israf ettiği <em>için</em> var olur — DCGM size kimin ne tükettiğini söyleyemez. eBPF’in PID-seviyesi izlemesi söyleyebilir. İki araç, her birinin en zayıf olduğu dikişte birbirini tamamlar.</p>\n<p>(En derin verimlilik profilleme için <strong>CUPTI</strong>, CUDA Profiling Tools Interface var; DCGM’in açığa çıkarmadığı sayaçları okuyabilir — ama Nsight Compute gibi metrik-replay profilleme kernel’leri birden çok kez yeniden çalıştırır ve ağır overhead taşır. Bu bir profilleme-oturumu aracıdır, her zaman açık tenant başına telemetri değil. DCGM düşük-overhead’li, örnekleme-bazlı sürekli yoldur; CUPTI ağır, derin olandır.)</p>\n<h2 id=\"kernel-vs-userspace-takası-açıkça\">Kernel-vs-userspace takası, açıkça</h2>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-scroll\" tabindex=\"0\" role=\"region\" aria-label=\"Tablo 1\"><table><thead><tr><th>Düzlem</th><th>Araç</th><th>Görür</th><th>Göremez</th></tr></thead><tbody><tr><td>Kernel (kontrol)</td><td>eBPF</td><td>PID→pod atfetme, başlatma sayıları, tahsis boyutları, memcpy hacmi, çağrı zamanlaması — time-slicing altında pod başına <em>dahil</em></td><td>cihaz-içi SM/tensor occupancy, bellek bant genişliği, ioctl payload içleri</td></tr><tr><td>Userspace (cihaz)</td><td>DCGM</td><td>gerçek SM occupancy, tensor aktivitesi, DRAM aktivitesi, kullanılan VRAM — donanım sayaçlarından</td><td>paylaşılan GPU altında (time-slicing/MPS) pod başına atfetme: tüm paylaşanlar aynı değerleri alır</td></tr></tbody></table></div>\n<p>eBPF düşük-overhead’lidir, uygulama değişikliği gerektirmez, vendor işbirliği gerektirmez ve PID/cgroup üzerinden native atfeder — <em>kim ve ne kadar</em> için doğru araç odur. DCGM silikonu okur — <em>ne kadar iyi</em> için doğru araç odur. Maliyetler de dürüst: uprobe’lar gerçek overhead taşır ve CUDA sembolleri <strong>versiyonludur</strong> (<code>cuMemAlloc@CUDA_11.0</code> vs <code>@CUDA_12.0</code>), yani probe’lar dinamik sembol çözümü gerektirir ve driver yükseltmeleri boyunca kırılır; CUDA API çağrıları saniyede on binden fazla atabilir, yani handler’lar ucuz kalmalı yoksa ölçtükleri workload’u yavaşlatırlar. DCGM kendi örnekleme overhead’i olan userspace polling’dir ve yalnızca belirli sayaç gruplarının birlikte okunabildiği bir donanım kısıtı taşır.</p>\n<h2 id=\"i̇kisine-de-neden-hangi-sırayla-ihtiyacınız-var\">İkisine de neden, hangi sırayla ihtiyacınız var</h2>\n<p>Yanlış dönüş, bir aracı seçip ona diğer aracın sorusunu sormaktır — ekipleri DCGM’in cihaz-seviyesi sayıları üzerinden faturalandırıp bir GPU’yu paylaşan herkesi sessizce fazla yüklemek, ya da eBPF’in başlatma sayılarına verimlilik vekili olarak güvenip meşgul görünen bir workload’un iyi kullanıldığı sonucuna varmak. Gerçekten işleyen sıra:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"plaintext\"><code><span class=\"line\"><span>1. eBPF: GPU işini pod/ekibe atfet           (kim, ne kadar — fatura)</span></span>\n<span class=\"line\"><span>2. DCGM: cihaz-içi verimliliği ölç           (ne kadar iyi — haklı mıydı)</span></span>\n<span class=\"line\"><span>3. Onları pod kimliğinde birleştir            (ekibin harcaması VE verimliliği)</span></span></code></pre>\n<p>Atfetme önce, çünkü finansın gerçekten sorduğu soru o ve mevcut yığınınızın hiç cevaplayamadığı o. Verimlilik sonra, çünkü faturayı bir karara dönüştürür — pahalı bir tahsisi %4 occupancy’de tutan bir ekip, artık <em>görebileceğiniz</em> bir <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/\">ileride-lazım-olur kapasite vergisi</a> ödüyordur; oysa önceden GPU, kimsenin açamadığı düz bir kalemdi. Pod kimliğinde birleşince, nihayet her diğer kaynak için sahip olduğunuz şeye sahip olursunuz: atfedilmiş harcama, üzerinde harekete geçecek verimlilik bağlamıyla. İki düzlem gerektirmesinin nedeni, görünmez olmasının nedeniyle aynıdır — silikon muhasebe düzleminde hiç yaşamadı ve tek bir probe bile sınırı kat etmiyor.</p>\n<hr/>\n<p><em>İlgili:</em> <a href=\"https://docs.nvidia.com/datacenter/dcgm/latest/installation/install-dcgm-exporter.html\">DCGM exporter belgeleri</a> pod-atfetme yolunu, <a href=\"https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html\">DCGM özellik genel bakışı</a> ise profilleme alanlarını tanımlar; bunun yaslandığı eBPF primitifleri <a href=\"https://ebpf.io/what-is-ebpf/\">ebpf.io</a> ve <a href=\"https://tetragon.io/\">Tetragon</a>’da belgelenmiştir.</p>",
      "content_text": "> **Kısaca:** Bütün bir GPU için faturalandırılıyorsunuz ve onun %3’ünü kullanıyorsunuz — ve monitoring’iniz size o %3’ün kimin olduğunu söyleyemiyor. eBPF, syscall sınırını izleyerek GPU işini bir pod’a atfeder; DCGM ise silikonun ne kadar iyi koştuğunu ölçer. Farklı sorulara cevap verirler ve gerçek bir chargeback her ikisine de ihtiyaç duyar.\n\nBir GPU node’u, bir raf dolusu CPU makinesinden saatte daha fazlaya mal olur ve finansın onun hakkında sorduğu soru, her şey hakkında sorduğu soruyla aynıdır: **bunu kim kullandı ve ne kadar?** CPU, bellek ve disk için bunu cevaplayabilirsiniz — cgroup’lar bunu process başına hesaplar, cAdvisor pod başına raporlar, Prometheus ekip başına toplar. GPU içinse çoğu kümede dürüst cevap, kimsenin bilmediğidir. Filodaki en pahalı kaynağın parasını ödüyorsunuz ve onun tek bir kuruşunu bile atfedemiyorsunuz.\n\nBu, doldurmayı unuttuğunuz bir araç boşluğu değil. Yapısaldır: GPU, **yığınınızın geri kalanının dayandığı muhasebe düzleminin dışındadır.** Bu yazı, bunun nedeni ve atfetmeyi geri kazanmak için eBPF kullanımı ile ilgili — ve eBPF’in ne görebildiği ve ne göremediğine dair sert bir çizgiyle birlikte, çünkü buradaki en pahalı hata, bir aracın fiziksel olarak cevaplayamadığı bir soruyu cevapladığına inanmaktır.\n\n## GPU mevcut metriklerinize neden görünmez?\n\nÜç ayrı körlük üst üste yığılır.\n\n**cgroup’lar GPU’yu hesaplamaz.** Bir cgroup, CPU, bellek, block I/O ve PID’leri hesaplayan ve sınırlayan bir kernel yapısıdır. GPU bunların hiçbiri değil. Compute ve framebuffer (VRAM), NVIDIA kernel driver’ı ve userspace CUDA runtime’ı tarafından yönetilir ve cgroup hesaplamasına opaktır. Bir `device` cgroup controller’ı var, ama o yalnızca device node’larına (`/dev/nvidia0` ve dostları) *erişimi geçitler* — izin ver ya da reddet. Tek bir saniye GPU zamanını ya da bir megabayt VRAM’i hesaplamaz. Yani size pod başına CPU ve bellek veren tüm cAdvisor → Prometheus boru hattının, kuruluş gereği, **GPU hakkında söyleyecek hiçbir şeyi yoktur.**\n\n**`nvidia.com/gpu` bir tahsis sayısıdır, bir kullanım sinyali değil.** Kubernetes GPU’ları, onları kubelet’e extended resource `nvidia.com/gpu` olarak kaydeden NVIDIA device plugin’i üzerinden öğrenir. Pod’lar bunu bir integer olarak ister — bütün GPU’lar. Scheduler istenen *sayıyı* mevcut *sayıyla* eşler. Tahsis katmanının bildiği tek şey budur. **Bir GPU istemek, bir GPU kullanmak değildir.** Bir pod bütün bir cihazı tutabilir — ve bütün cihazın faturasını ödeyebilir — onu %3’te sürerken. Üzerinden chargeback yapabileceğiniz tahsis sayısı, tam da tüketim hakkında size hiçbir şey söylemeyen sayıdır.\n\n**`nvidia-smi`’nin kullanım sayısı bile size yalan söyler.** `nvidia-smi`’ye uzanırsınız ve `utilization.gpu` alanı kurtuluş gibi görünür. Sandığınız şey değil. NVML onu şöyle tanımlar: *örnekleme periyodu boyunca bir ya da daha fazla kernel’in çalıştığı zamanın yüzdesi.* **Zamansal varlığı — GPU hiç meşgul muydu — ölçer, ne kadarının meşgul olduğunu değil.** Onlarca SM’den birini işgal eden tek thread’lik bir kernel %100’e yakın raporlayabilir. Microsoft, A100’ler üzerinde 8B parametreli bir modeli servis ederken bellek-bağımlı decode fazında %10’un altında compute kullanımı raporlamıştır — naif bir “kullanım” okuması o GPU’ları dolu sayacakken. `utilization.gpu` üzerinden chargeback yaparsanız, silikon neredeyse boştayken “meşgul” diyen bir sayı üzerinden faturalandırıyorsunuzdur.\n\nYani: tahsis katmanı kullanımı-değil-sayıyı bilir, cgroup’lar hiçbir şey bilmez ve tek kolay yüzde yanlış şeyi ölçer. Boşluk budur.\n\n## İki soru ve neden farklı araçlar gerektirdikleri\n\nHerhangi bir araçtan önce iki soruyu ayırın, çünkü onları karıştırmak temel hatadır — [bir aracın çözdüğü problemi eldeki problemle karıştırmakla](/tr/muhendislik/sistemler/veri-yogunluklu-sistemler-kirilma-noktalari/) aynı şekil.\n\n- **Atfetme — GPU işini *kim* yaptı ve *ne kadar*?** Bu FinOps sorusudur. Pod başına, ekip başına GPU aktivitesi hesabı gerektirir: hangi workload kernel başlattı, VRAM tahsis etti, veri taşıdı ve ne kadar süreyle. Chargeback bunun üzerinden koşar.\n- **Verimlilik — silikon *ne kadar iyi* kullanıldı?** SM’ler gerçekten doluydu mu, tensor core’lar aktif miydi, darboğaz bellek bant genişliği miydi? Bu performans sorusudur. Bir ekibin harcamasının *haklı* olup olmadığını söyler, ama *kimin* harcaması olduğunu söyleyemez.\n\nBunlar iki farklı ölçüm düzlemine eşlenir ve aşağıdaki her şeyi örgütleyen kural şu: **eBPF atfetmeyi cevaplar; GPU’nun kendi sayaçları verimliliği cevaplar. Hiçbiri diğerinin yerine geçmez.**\n\n## eBPF’in görebildiği: kontrol düzlemi\n\neBPF kernel’de çalışır ve kprobe’lara, uprobe’lara, tracepoint’lere ve syscall’lara takılır. Okunacak bir GPU-kullanımı tracepoint’i yok — ama GPU *işinin talep edildiği* iki yüzey var ve ikisi de kernel’den görünür:\n\n- **Driver ioctl sınırı.** Her parça GPU işi — kernel gönderimi, bellek tahsisi, senkronizasyon — sonunda `/dev/nvidiactl`’a ve `/dev/nvidia0…N`’e bir `ioctl()` olur. Driver’ın giriş noktalarındaki (`nvidia_unlocked_ioctl`, `nvidia_open`) bir kprobe o trafiği görür. (Kapalı driver tarihsel olarak yalnızca tek bir tracepoint, donanım hata olayları için `nvidia:nvidia_dev_xid`, sunar; geri kalan her şey kprobe’lanır.)\n- **CUDA kütüphane sınırı.** `libcuda.so` / `libcudart.so` üzerindeki bir uprobe API’nin kendisini izler: `cuLaunchKernel`, `cuMemAlloc`, `cuMemcpyHtoD`, `cuStreamSynchronize` ve dostları. Bir giriş uprobe’unu bir dönüş uretprobe’uyla eşlemek her çağrının süresini ölçer.\n\nBunun *atfetme* olmasının nedeni, eBPF’in her hook noktasında çağıran PID/TGID’yi ve cgroup’u native olarak okumasıdır — `bpf_get_current_pid_tgid` ve cgroup id tam orada. Bu size **PID → cgroup → pod → namespace → ekip** zincirini verir, uygulama değişikliği olmadan ve NVIDIA’nın işbirliği olmadan. Pod başına şunları alırsınız: kernel-başlatma sayıları, başlatma boyutları, VRAM tahsis boyutları, memcpy hacmi ve yönü, çağrı zamanlaması. Bu, tamamen sınırın kernel tarafından türetilen, gerçek ve savunulabilir bir chargeback sinyalidir.\n\nBuradaki ekosistem genç ama gerçek. Primitifler bugün işliyor — CUDA kütüphanelerini uprobe’layan ve nvidia ioctl yolunu kprobe’layan çalışan yazılar ve eğitimler var ve `bpftime` eBPF mantığını GPU olaylarına bağlamayı keşfediyor. Tetragon (Cilium), Kubernetes pod kimliğiyle jenerik `process_uprobe`, `process_kprobe` ve ioctl izleme sevk eder, yani CUDA sembollerine ya da ioctl yoluna *yöneltilebilir* — ama bunun sevk edilen bir “GPU FinOps” özelliği değil, yazdığınız bir `TracingPolicy` olduğunu anlayın. Henüz baskın, anahtar-teslim bir eBPF chargeback ürünü yok. Size “tak-çalıştır eBPF GPU FinOps” satan biri, ekosistemin şu an ulaştığından öteye satıyordur.\n\n## eBPF’in göremediği: silikon\n\nDürüst limit bu ve bir sürümde düzeltilecek bir olgunluk problemi değil — verinin nerede yaşadığının fiziği.\n\neBPF **kontrol düzlemini** görür: API çağrıları, ioctl’lar, tahsis boyutları, başlatma sayıları ve izleyebildiğiniz yerde gönderim-tamamlanma zamanlaması. GPU’nun *içini* **görmez**. SM occupancy’sini, tensor-core kullanımını ya da erişilen bellek bant genişliğini okuyamaz, çünkü bunlar *cihazda* yaşayan ve yalnızca NVML / DCGM / CUPTI üzerinden açığa çıkan donanım performans sayaçlarıdır. eBPF’in onlara giden bir yolu yoktur. Bir pod’un on bin kernel başlattığını ve 40 GB VRAM tahsis ettiğini söyleyebilir; o kernel’lerin SM’leri doyurup doyurmadığını ya da onları %90 boş bırakıp bırakmadığını söyleyemez.\n\nİkinci, daha incelikli bir limit var. eBPF’in takılabildiği ioctl sınırında bile, *payload’lar* büyük ölçüde opaktır. Driver’ın komut yapıları (`NV_ESC_*` Resource Manager API’si) karmaşık ve fiilen proprietary. Bir ioctl’ın *olduğunu* görebilirsiniz — komut numarasını, çağıran PID’yi, zamanlamayı — ama keyfi bir RM payload’unun semantik içeriğini decode etmek pratik dışı ve kırılgandır. İşin gerçeğini ve atfetmesini alırsınız, anlamının bedava bir okumasını değil.\n\nCazip bir kapıyı kapatan bir not: NVIDIA’nın açık kernel modülleri (`open-gpu-kernel-modules`, Turing ve sonrası) **kernel arayüz katmanını** açar — modül init’ini, ioctl giriş noktalarını, `NV_ESC_*` komut yüzeyini. Bu, *neyi* hook’layacağınızı anlamanıza gerçekten yardımcı olur. Ama GPU’nun beyni kapalı kalır: Turing+’ta yönetimin çoğu GPU üzerindeki GSP firmware’inde koşar ve o firmware — modüllerin ihtiyaç duyduğu user-mode driver bileşenleriyle birlikte — hâlâ kapalı olarak dağıtılır. Kernel modüllerini açmak, eBPF’e donanım performans sayaçlarını **açmaz**. Cihaz-içi limit değişmemiştir. Açık modüllerin eBPF’e SM occupancy okuttuğunu iddia eden biri, mevcut kanıtlara göre yanılıyordur.\n\n## Hâlâ ihtiyaç duyduğunuz userspace yolu: DCGM\n\neBPF silikonu göremediği için, cevabın verimlilik yarısı userspace’ten gelir ve standart araç, Prometheus için **dcgm-exporter** ile NVIDIA **DCGM** (Data Center GPU Manager)’dır. DCGM, GPU’nun donanım sayaçlarını driver üzerinden okur ve eBPF’in erişemediği alanları açığa çıkarır:\n\n- `DCGM_FI_PROF_SM_ACTIVE` — bir multiprocessor’da en az bir warp’ın aktif olduğu zamanın oranı, hepsinin ortalaması.\n- `DCGM_FI_PROF_SM_OCCUPANCY` — resident warp’ların desteklenen maksimuma oranı: *gerçek* occupancy.\n- `DCGM_FI_PROF_PIPE_TENSOR_ACTIVE` — tensor pipe’ın aktif olduğu döngülerin oranı.\n- `DCGM_FI_PROF_DRAM_ACTIVE` — bir bellek-bant-genişliği vekili.\n- `DCGM_FI_DEV_FB_USED` — gerçekten kullanımda olan framebuffer (VRAM).\n\nBunlar size bir ekibin pahalı tahsisinin ekmeğini kazanıp kazanmadığını söyleyen sayılardır. DCGM onları pod’lara, her GPU UUID’yi onu tutan pod’a eşleyen kubelet **Pod Resources API**’si (`/var/lib/kubelet/pod-resources` adresindeki bir gRPC servisi) üzerinden atfeder.\n\nAma DCGM’in atfetmesinin, tam da eBPF’in yerini hak ettiği yerde sert bir kenarı var: **DCGM cihaz-seviyesi metrikler üretir, bu yüzden tek bir fiziksel GPU’yu paylaşan tüketicileri ayırt edemez.** Time-slicing ya da MPS (Multi-Process Service) altında, bir GPU’yu paylaşan tüm pod’lar *aynı, kopyalanmış* cihaz-seviyesi değerleri alır — `DCGM_FI_DEV_FB_USED` dahil. NVIDIA’nın kendi exporter’ı, time-slicing etkinken metrikleri container’larla ilişkilendirmediğini belgeler; `--kubernetes-virtual-gpus=true` ile her paylaşan pod tüm fiziksel GPU’nun durumunu aynalar. MIG altında atfetme, keyfi pod’lara değil GPU-instance seviyesine kayar. Yani tam da paylaşılan-GPU durumunda — ki bu durum, bütün-GPU tahsisi para israf ettiği *için* var olur — DCGM size kimin ne tükettiğini söyleyemez. eBPF’in PID-seviyesi izlemesi söyleyebilir. İki araç, her birinin en zayıf olduğu dikişte birbirini tamamlar.\n\n(En derin verimlilik profilleme için **CUPTI**, CUDA Profiling Tools Interface var; DCGM’in açığa çıkarmadığı sayaçları okuyabilir — ama Nsight Compute gibi metrik-replay profilleme kernel’leri birden çok kez yeniden çalıştırır ve ağır overhead taşır. Bu bir profilleme-oturumu aracıdır, her zaman açık tenant başına telemetri değil. DCGM düşük-overhead’li, örnekleme-bazlı sürekli yoldur; CUPTI ağır, derin olandır.)\n\n## Kernel-vs-userspace takası, açıkça\n\n| Düzlem | Araç | Görür | Göremez |\n| --- | --- | --- | --- |\n| Kernel (kontrol) | eBPF | PID→pod atfetme, başlatma sayıları, tahsis boyutları, memcpy hacmi, çağrı zamanlaması — time-slicing altında pod başına *dahil* | cihaz-içi SM/tensor occupancy, bellek bant genişliği, ioctl payload içleri |\n| Userspace (cihaz) | DCGM | gerçek SM occupancy, tensor aktivitesi, DRAM aktivitesi, kullanılan VRAM — donanım sayaçlarından | paylaşılan GPU altında (time-slicing/MPS) pod başına atfetme: tüm paylaşanlar aynı değerleri alır |\n\neBPF düşük-overhead’lidir, uygulama değişikliği gerektirmez, vendor işbirliği gerektirmez ve PID/cgroup üzerinden native atfeder — *kim ve ne kadar* için doğru araç odur. DCGM silikonu okur — *ne kadar iyi* için doğru araç odur. Maliyetler de dürüst: uprobe’lar gerçek overhead taşır ve CUDA sembolleri **versiyonludur** (`cuMemAlloc@CUDA_11.0` vs `@CUDA_12.0`), yani probe’lar dinamik sembol çözümü gerektirir ve driver yükseltmeleri boyunca kırılır; CUDA API çağrıları saniyede on binden fazla atabilir, yani handler’lar ucuz kalmalı yoksa ölçtükleri workload’u yavaşlatırlar. DCGM kendi örnekleme overhead’i olan userspace polling’dir ve yalnızca belirli sayaç gruplarının birlikte okunabildiği bir donanım kısıtı taşır.\n\n## İkisine de neden, hangi sırayla ihtiyacınız var\n\nYanlış dönüş, bir aracı seçip ona diğer aracın sorusunu sormaktır — ekipleri DCGM’in cihaz-seviyesi sayıları üzerinden faturalandırıp bir GPU’yu paylaşan herkesi sessizce fazla yüklemek, ya da eBPF’in başlatma sayılarına verimlilik vekili olarak güvenip meşgul görünen bir workload’un iyi kullanıldığı sonucuna varmak. Gerçekten işleyen sıra:\n\n```\n1. eBPF: GPU işini pod/ekibe atfet           (kim, ne kadar — fatura)\n2. DCGM: cihaz-içi verimliliği ölç           (ne kadar iyi — haklı mıydı)\n3. Onları pod kimliğinde birleştir            (ekibin harcaması VE verimliliği)\n```\n\nAtfetme önce, çünkü finansın gerçekten sorduğu soru o ve mevcut yığınınızın hiç cevaplayamadığı o. Verimlilik sonra, çünkü faturayı bir karara dönüştürür — pahalı bir tahsisi %4 occupancy’de tutan bir ekip, artık *görebileceğiniz* bir [ileride-lazım-olur kapasite vergisi](/tr/muhendislik/denemeler/ileride-lazim-olur-kodunun-faturasi/) ödüyordur; oysa önceden GPU, kimsenin açamadığı düz bir kalemdi. Pod kimliğinde birleşince, nihayet her diğer kaynak için sahip olduğunuz şeye sahip olursunuz: atfedilmiş harcama, üzerinde harekete geçecek verimlilik bağlamıyla. İki düzlem gerektirmesinin nedeni, görünmez olmasının nedeniyle aynıdır — silikon muhasebe düzleminde hiç yaşamadı ve tek bir probe bile sınırı kat etmiyor.\n\n---\n\n*İlgili:* [DCGM exporter belgeleri](https://docs.nvidia.com/datacenter/dcgm/latest/installation/install-dcgm-exporter.html) pod-atfetme yolunu, [DCGM özellik genel bakışı](https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html) ise profilleme alanlarını tanımlar; bunun yaslandığı eBPF primitifleri [ebpf.io](https://ebpf.io/what-is-ebpf/) ve [Tetragon](https://tetragon.io/)’da belgelenmiştir.\n\n## Adımlar\n\n**GPU chargeback’i kurmak** — GPU harcamasını ekiplere atfetmek ve o harcamanın haklı olup olmadığını ölçmek.\n\n1. **eBPF ile atıf** — GPU işini syscall sınırından izleyip pod’a ve ekibe atfet. Bu “kim, ne kadar” sorusunu — yani faturayı — cevaplar.\n2. **DCGM ile verimlilik** — Cihaz-içi verimliliği userspace’ten ölç: SM kullanımı, bellek, güç. Bu “ne kadar iyi koştu” sorusunu cevaplar.\n3. **Pod kimliğinde birleştir** — İki ölçümü pod kimliği üzerinden birleştir; ekibin harcaması ve verimliliği aynı yerde durur.\n\n## Kaynaklar\n\n- [NVML: nvmlUtilization_t.gpu is the percent of time one or more kernels was executing](https://docs.nvidia.com/deploy/nvml-api/structnvmlUtilization__t.html) — NVIDIA\n- [DCGM feature overview: SM active, SM occupancy, tensor and DRAM activity, and the counter grouping limit](https://docs.nvidia.com/datacenter/dcgm/latest/user-guide/feature-overview.html) — NVIDIA\n- [GPU Operator time-slicing: DCGM-Exporter does not associate metrics to containers when time-slicing is enabled](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html) — NVIDIA\n- [Kubernetes device plugins: extended resources are integer-only and devices cannot be shared between containers](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/) — Kubernetes\n\n## Sık sorulanlar\n\n### cgroup'lar ve cAdvisor GPU kullanımını neden atfedemiyor?\n\nBir cgroup CPU, bellek, block I/O ve PID hesaplar. GPU compute ve framebuffer ise NVIDIA kernel driver'ı ile userspace CUDA runtime'ı tarafından yönetilir ve cgroup'a opaktır. device cgroup controller'ı yalnızca device node'larına erişimi geçitler; tek bir saniye GPU zamanı hesaplamaz. Dolayısıyla cAdvisor'dan Prometheus'a giden boru hattının raporlayacak hiçbir şeyi yoktur.\n\n### nvidia-smi'nin utilization.gpu değeri chargeback için kullanılabilir mi?\n\nHayır. NVML onu, örnekleme periyodu boyunca bir ya da daha fazla kernel'in çalıştığı zamanın yüzdesi olarak tanımlar. Bu zamansal varlıktır, cihazın ne kadarının meşgul olduğu değil — onlarca multiprocessor'dan birini işgal eden tek thread'lik bir kernel %100'e yakın raporlayabilir. Bunun üzerinden faturalandırmak, neredeyse boş bir silikonun parasını kesmektir.\n\n### DCGM gerçek donanım sayaçlarını okuyorsa eBPF'e neden hâlâ ihtiyaç var?\n\nDCGM cihaz-seviyesi metrik üretir, bu yüzden tek bir fiziksel GPU'yu paylaşan tüketicileri ayırt edemez. Time-slicing ya da MPS altında paylaşan her pod aynı kopyalanmış değerleri alır ve NVIDIA, time-slicing etkinken exporter'ının metrikleri container'larla ilişkilendirmediğini belgeler. eBPF ise PID başına atfeder; yani tam da DCGM'in kapatamadığı durumu kapatır.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "ebpf",
        "gpu",
        "finops",
        "observability",
        "kubernetes"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    },
    {
      "id": "https://muhammetsafak.com/tr/muhendislik/notlar/event-driven-mimari-ne-zaman/",
      "url": "https://muhammetsafak.com/tr/muhendislik/notlar/event-driven-mimari-ne-zaman/",
      "title": "Event-Driven Mimari: Ne Çözer, Neyi Gizler?",
      "summary": "Event'ler, üreticinin tüketicilerini tanımamasını sağlayarak gevşek bağ kazandırır; aynı hamle akışı da görünmez kılar, çünkü bir event kontratını derleyici bir method çağrısı gibi doğrulamaz. İki bedel ya peşin ödenir ya hiç: versiyonlanmış ve şemayla doğrulanan payload, ve her log'a ve her sonraki event'e taşınan bir correlation id. Tek listener'lı bir event, kılık değiştirmiş bir method çağrısıdır.",
      "content_html": "<p>“Sipariş oluşturulduğunda ne oluyor?” sorusuna bir ekipte kimsenin net cevap verememesini izledim. Kod tabanında <code>OrderCreated</code> event’ini sekiz ayrı listener dinliyordu; hangisinin hangi sırada, hangi koşulda çalıştığı tek bir yerden okunamıyordu. Akış kodda değil, kafalarda — hem de eksik — yaşıyordu.</p>\n<p>Event-driven mimari bağı gevşetir. Bu yazı, gevşeyen başka bir şey daha olduğunu hatırlatmak için: nedensellik.</p>\n<h2 id=\"event-ne-çözer\">Event ne çözer?</h2>\n<p>Bir event yayınlayan kod, onu kimin dinlediğini bilmez. <code>OrderCreated</code> yayınlanır; faturalama, bildirim ve analitik onu ayrı ayrı dinler. Sipariş kodu bu üçünden habersizdir.</p>\n<p>Kazanç gerçek: yeni bir tüketici eklemek, üreticiye dokunmadan olur. Bağımsız geliştirme, bağımsız dağıtım, fan-out. Birbirini tanımayan parçalar.</p>\n<h2 id=\"event-neyi-gizler\">Event neyi gizler?</h2>\n<p>Aynı madalyonun öbür yüzü: bağ gevşedikçe <strong>akış görünmez olur</strong>.</p>\n<p>Doğrudan bir method çağrısında akış kodun kendisidir — tanımına gidersiniz, devamını okursunuz. Event’te öyle değil. <code>OrderCreated</code> yayınlandığında ne olduğunu görmek için tüm listener’ları elle bulmanız gerekir. Compiler size yardım etmez: bir method çağrısının kontratını derleyici doğrular, bir event’in kontratını kimse doğrulamaz.</p>\n<p><a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/\">Modüler monolit yazısındaki</a> cümle burada da geçerli — bir network kontratı gibi, bir event kontratı da derleyicinin kör noktasındadır.</p>\n<h2 id=\"ödenmeden-seçilmemeli-iki-ön-koşul\">Ödenmeden seçilmemeli: iki ön koşul</h2>\n<p>Event-driven mimariye geçmeden önce iki bedel peşin ödenir.</p>\n<p><strong>Schema disiplini.</strong> Event bir kontrattır. <code>OrderCreated</code>’in payload’ı değiştiğinde, onu dinleyen sekiz yerin haberi olmaz — ta ki production’da kırılana kadar. Event’ler versiyonlanmalı, alanlar geriye uyumlu eklenmeli, <a href=\"https://muhammetsafak.com/tr/muhendislik/notlar/semayi-kuyrugun-kenarinda-dogrulamak/\">payload bir şema ile doğrulanmalı</a>. Bu disiplin yoksa, gevşek bağ “sessiz kırılma” demektir.</p>\n<p><strong>İzlenebilirlik.</strong> Bir isteğin sistemden geçişini görebilmek için <a href=\"https://muhammetsafak.com/tr/muhendislik/sistemler/polyglot-kuyrukta-dagitik-izleme/\">her event bir correlation ID taşımalı</a>:</p>\n<pre class=\"astro-code astro-code-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word\" tabindex=\"0\" data-language=\"php\"><code><span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">event</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">new</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OrderCreated</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    orderId</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: $order</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">id,</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    correlationId</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">request</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-&gt;</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">header</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">&#39;X-Correlation-Id&#39;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">string</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">Str</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">::</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">uuid</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span></code></pre>\n<p>Bu ID log’lara ve sonraki event’lere taşınmazsa, çok-listener’lı bir akışta “bu iş neden çalıştı?” sorusunun cevabı yoktur. Tracing, event-driven’ın isteğe bağlı bir eklentisi değil, ön koşuludur.</p>\n<h2 id=\"laravelde-in-process-event--event-driven-mimari\">Laravel’de: in-process event ≠ event-driven mimari</h2>\n<p>Laravel’in <code>event()</code>/listener mekanizması, senkron çalıştığında aslında düzenlenmiş bir method çağrısıdır — aynı process, aynı transaction, aynı stack trace. Bu güvenli ve izlenebilir; bunu rahatça kullanın.</p>\n<p>Event-driven mimarinin getirdiği zorluklar, event bir <strong>sınırı</strong> geçtiğinde başlar: bir queue’ya, bir message broker’a, başka bir process’e. Listener’a <code>ShouldQueue</code> ekleyip event’i RabbitMQ’ya gönderdiğiniz an, schema ve tracing borcu devreye girer. İkisini birbirine karıştırmayın: in-process listener kullanmak sizi “event-driven mimariye” geçirmez.</p>\n<h2 id=\"ne-zaman-event-drivena-geçilir\">Ne zaman event-driven’a geçilir?</h2>\n<p>Event’i gerçekten bir sınırın ötesine taşımak şu durumlarda gerekçeli:</p>\n<ul>\n<li><strong>Gerçek bir fan-out var.</strong> Bir olayı, birbirini tanımaması gereken çok sayıda bağımsız tüketici dinliyor.</li>\n<li><strong>Tüketici asenkron olmalı.</strong> İş, üreticinin yanıtını bekletmemeli; “kabul edildi” yeterli.</li>\n<li><strong>Tüketiciler bağımsız ölçeklenmeli ya da dağıtılmalı.</strong> Bu da bizi <a href=\"https://muhammetsafak.com/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/\">mikroservis kararına</a> götürür — aynı ölçülmüş-sinyal eşiği.</li>\n</ul>\n<p>Bunlar yoksa, doğrudan bir service çağrısı hem daha okunur hem daha güvenlidir. Tek bir listener’ı olan bir event, sadece kılık değiştirmiş bir method çağrısıdır — üstelik izi sürülmesi daha zor olanı.</p>\n<hr/>\n<p>Event-driven mimari bağı gevşetir; bunu yaparken nedenselliği de gevşetir. Birincisini istiyorsanız, ikincisinin faturasını — schema ve tracing — peşin ödeyin.</p>\n<p>Gevşek bağ bedava değildir; görünmez akışla ödenir.</p>",
      "content_text": "> **Kısaca:** Event'ler, üreticinin tüketicilerini tanımamasını sağlayarak gevşek bağ kazandırır; aynı hamle akışı da görünmez kılar, çünkü bir event kontratını derleyici bir method çağrısı gibi doğrulamaz. İki bedel ya peşin ödenir ya hiç: versiyonlanmış ve şemayla doğrulanan payload, ve her log'a ve her sonraki event'e taşınan bir correlation id. Tek listener'lı bir event, kılık değiştirmiş bir method çağrısıdır.\n\n\"Sipariş oluşturulduğunda ne oluyor?\" sorusuna bir ekipte kimsenin net cevap verememesini izledim. Kod tabanında `OrderCreated` event'ini sekiz ayrı listener dinliyordu; hangisinin hangi sırada, hangi koşulda çalıştığı tek bir yerden okunamıyordu. Akış kodda değil, kafalarda — hem de eksik — yaşıyordu.\n\nEvent-driven mimari bağı gevşetir. Bu yazı, gevşeyen başka bir şey daha olduğunu hatırlatmak için: nedensellik.\n\n## Event ne çözer?\n\nBir event yayınlayan kod, onu kimin dinlediğini bilmez. `OrderCreated` yayınlanır; faturalama, bildirim ve analitik onu ayrı ayrı dinler. Sipariş kodu bu üçünden habersizdir.\n\nKazanç gerçek: yeni bir tüketici eklemek, üreticiye dokunmadan olur. Bağımsız geliştirme, bağımsız dağıtım, fan-out. Birbirini tanımayan parçalar.\n\n## Event neyi gizler?\n\nAynı madalyonun öbür yüzü: bağ gevşedikçe **akış görünmez olur**.\n\nDoğrudan bir method çağrısında akış kodun kendisidir — tanımına gidersiniz, devamını okursunuz. Event'te öyle değil. `OrderCreated` yayınlandığında ne olduğunu görmek için tüm listener'ları elle bulmanız gerekir. Compiler size yardım etmez: bir method çağrısının kontratını derleyici doğrular, bir event'in kontratını kimse doğrulamaz.\n\n[Modüler monolit yazısındaki](/tr/muhendislik/denemeler/projelere-neden-moduler-monolit-ile-basliyorum/) cümle burada da geçerli — bir network kontratı gibi, bir event kontratı da derleyicinin kör noktasındadır.\n\n## Ödenmeden seçilmemeli: iki ön koşul\n\nEvent-driven mimariye geçmeden önce iki bedel peşin ödenir.\n\n**Schema disiplini.** Event bir kontrattır. `OrderCreated`'in payload'ı değiştiğinde, onu dinleyen sekiz yerin haberi olmaz — ta ki production'da kırılana kadar. Event'ler versiyonlanmalı, alanlar geriye uyumlu eklenmeli, [payload bir şema ile doğrulanmalı](/tr/muhendislik/notlar/semayi-kuyrugun-kenarinda-dogrulamak/). Bu disiplin yoksa, gevşek bağ \"sessiz kırılma\" demektir.\n\n**İzlenebilirlik.** Bir isteğin sistemden geçişini görebilmek için [her event bir correlation ID taşımalı](/tr/muhendislik/sistemler/polyglot-kuyrukta-dagitik-izleme/):\n\n```php\nevent(new OrderCreated(\n    orderId: $order->id,\n    correlationId: request()->header('X-Correlation-Id') ?? (string) Str::uuid(),\n));\n```\n\nBu ID log'lara ve sonraki event'lere taşınmazsa, çok-listener'lı bir akışta \"bu iş neden çalıştı?\" sorusunun cevabı yoktur. Tracing, event-driven'ın isteğe bağlı bir eklentisi değil, ön koşuludur.\n\n## Laravel'de: in-process event ≠ event-driven mimari\n\nLaravel'in `event()`/listener mekanizması, senkron çalıştığında aslında düzenlenmiş bir method çağrısıdır — aynı process, aynı transaction, aynı stack trace. Bu güvenli ve izlenebilir; bunu rahatça kullanın.\n\nEvent-driven mimarinin getirdiği zorluklar, event bir **sınırı** geçtiğinde başlar: bir queue'ya, bir message broker'a, başka bir process'e. Listener'a `ShouldQueue` ekleyip event'i RabbitMQ'ya gönderdiğiniz an, schema ve tracing borcu devreye girer. İkisini birbirine karıştırmayın: in-process listener kullanmak sizi \"event-driven mimariye\" geçirmez.\n\n## Ne zaman event-driven'a geçilir?\n\nEvent'i gerçekten bir sınırın ötesine taşımak şu durumlarda gerekçeli:\n\n- **Gerçek bir fan-out var.** Bir olayı, birbirini tanımaması gereken çok sayıda bağımsız tüketici dinliyor.\n- **Tüketici asenkron olmalı.** İş, üreticinin yanıtını bekletmemeli; \"kabul edildi\" yeterli.\n- **Tüketiciler bağımsız ölçeklenmeli ya da dağıtılmalı.** Bu da bizi [mikroservis kararına](/tr/muhendislik/denemeler/mikroservise-ne-zaman-gecerim/) götürür — aynı ölçülmüş-sinyal eşiği.\n\nBunlar yoksa, doğrudan bir service çağrısı hem daha okunur hem daha güvenlidir. Tek bir listener'ı olan bir event, sadece kılık değiştirmiş bir method çağrısıdır — üstelik izi sürülmesi daha zor olanı.\n\n---\n\nEvent-driven mimari bağı gevşetir; bunu yaparken nedenselliği de gevşetir. Birincisini istiyorsanız, ikincisinin faturasını — schema ve tracing — peşin ödeyin.\n\nGevşek bağ bedava değildir; görünmez akışla ödenir.\n\n## Kaynaklar\n\n- [Enterprise Integration Patterns: Publish-Subscribe Channel](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) — Hohpe and Woolf\n- [Enterprise Integration Patterns: Correlation Identifier](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CorrelationIdentifier.html) — Hohpe and Woolf\n- [Events: queued event listeners and ShouldQueue](https://laravel.com/docs/12.x/events) — Laravel\n\n## Sık sorulanlar\n\n### Laravel in-process event kullanmak event-driven mimari sayılır mı?\n\nSayılmaz. Senkron bir listener düzenlenmiş bir method çağrısıdır — aynı process, aynı transaction, aynı stack trace — ve güvenli, izlenebilirdir. Zorluklar event bir sınırı geçtiğinde başlar: bir queue'ya, bir broker'a ya da başka bir process'e. Schema ve tracing borcu orada devreye girer.\n\n### Event-driven mimariye geçmeden önce hangi bedel ödenir?\n\nİki şey, peşin. Schema disiplini: event bir kontrattır; versiyonlanmalı, geriye uyumlu genişletilmeli ve bir şemayla doğrulanmalıdır, yoksa gevşek bağ yalnızca sessiz kırılma demektir. Bir de izlenebilirlik: her event bir correlation id'yi log'lara ve tetiklediği event'lere taşımazsa, bu iş neden çalıştı sorusunun cevabı olmaz.",
      "date_published": "2026-06-20T00:00:00.000Z",
      "date_modified": "2026-09-06T00:00:00.000Z",
      "tags": [
        "architecture",
        "event-driven",
        "messaging"
      ],
      "authors": [
        {
          "name": "Muhammet Şafak",
          "url": "https://muhammetsafak.com"
        }
      ]
    }
  ]
}