Laravel uygulamanızı Nightwatch gibi bir barındırılan gözlem platformunda çalıştırıyorsanız, muhtemelen fatura yönetilebilir olması için telemetrinizi örnek alarak azaltmışsınızdır. Ancak tüm telemetrilerinizi saklamak istiyorsanız bu makale size uygun bir çözüm sunmaktadır.
laravel/nightwatch, Laravel’in resmi gözlem SDK’sıdır ve enstrümantasyonu gerçekten iyidir. Ancak barındırma tarafı beni rahatsız etti. Kullanım başına ücretlendirme var, verimlilik sizin ödeme isteklerinize göre sınırlı ve telemetriniz başka birinin deposunda yaşıyor. Birçok ekip bu değişimi kabulleniyor.
Ancak, yüksek trafikli uygulamalar örnek almak istemiyor; düzenlemeleri olan yapılar, yığın izlerinin dışarı çıkmasına izin vermiyor; daha küçük ekipler zaten Postgres’in yazıları içerecek kadar boşluğa sahip. Aynı SDK’nın başka bir yere yönlendirilmesini istiyorlar.
Bu nedenle, Nightwatch’ın ingest bağlamasını yakalayan ve yükleri yerel bir TCP soketine yönlendiren bir ajan yazdım, ardından bunları sağladığım bir Postgres veritabanına boşaltıyorum. Tek bir örnekte, bu işlem yaklaşık 13,400 yük/s oranını sürdürüyor. Bu, herhangi bir örnek alım olmadan 2,000-5,000 req/s yapan bir uygulama için yeterli bir kapasite sağlıyor.
Mimari
Mimari
Üç katman, her biri belirli bir darboğazı çözmek için seçildi.
laravel/nightwatch
│
TCP
│
▼
ReactPHP dinleyici
│
▼
SQLite WAL tampon
│
▼
Postgres (COPY protokolü)
Yükleme yolu ve boşaltma yolu birbirinden ayrılmıştır. Yükleme asla Postgres üzerinde engel oluşturmamalı. Boşaltma, Postgres ile bağlantı kaybolursa veri kaybı yaşanmamalıdır.
Katman 1: Engelleme yapmayan yükleme, ReactPHP ile
Katman 1: Engelleme yapmayan yükleme, ReactPHP ile
TCP dinleyici, tek bir olay döngüsü üzerinde çalışan bir ReactPHP\Socket\TcpServer‘dır. Bir süreç, birçok eşzamanlı bağlantıdan yükleri kabul eder ve bunları tampon içine iter. PHP-FPM işçi süreçleri bu noktada devreye girmiyor. Nightwatch’ın yükleme bağlaması, istek kapanışında, yerel TCP soketine yazarak Laravel Cloud’a gitmek yerine yönlendirilir.
Wire protokolü kasıtlı olarak minimaldir: [length]:[version]:[tokenHash]:[payload]. Gzip, sihirli byte (0x1f 0x8b) ile tespit edilir ve xxh128 token hash’i 7 karaktere kısaltılır. Bunun bu kadar minimal kalma nedeni, ajanın yükü yeniden kodlamamasıdır. Nightwatch JSON gönderir, tampon olduğu gibi depolar ve boşaltma işçisi, yalnızca alanları doğru sütunlara yönlendirmek için ilk süreçtir. Sıcak yolda json_decode/json_encode dolaşımını atlamak, profil değerlendirmesinde yük başına yaklaşık 30-50µs kaydetmeye yardımcı oldu ve bu, bu hızda anlamlı bir bütçedir.
Katman 2: Tampon olarak SQLite WAL
Katman 2: Tampon olarak SQLite WAL
Neden SQLite tamponu olarak? Çünkü sadece dondurma işlemleri ile bellek haritalı dosya hızında çökme güvenli yazmaları sağlayan tek gömülü veritabanıdır ve sıfır ops gideri vardır.
Pragma sırası önemlidir ve beni bir kez bozmuştur:
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000; -- ~64 MB
PRAGMA mmap_size = 268435456; -- 256 MB
busy_timeout ayarı önce journal_mode = WAL şeklinde yapılmalı. Tam tersini yaparsanız, yük altında ilk eşzamanlı yazım işi yarışarak hemen SQLITE_BUSY hatası alır, beklemek yerine. Buna bir öğleden sonramı kaybettim.
synchronous = NORMAL ayarı tampon için uygundur çünkü Postgres dayanıklı depodur. Tamponun sadece bir işlem çökmesine dayanmasını sağlamak yeterlidir.
Satırlara tek bir synced sütunu verilir ve üç durumu vardır: 0 (beklemede), 100+workerId (drain işçisi N tarafından talep edilmiş), 1 (boşaltılmış). Boşaltma işçileri, bir yığın işin kendilerine ait olduğunu atomik bir şekilde işaretler, ardından SELECT işlemi gerçekleştirir. Güncelleme atomik kısımdır; SELECT işçiye satırları teslim eder. Eğer bir işçi yığın işlemi ortasında ölürse, üst düzey SIGCHLD işleyicisi, talep edilen satırları beklemeye bırakır.
Katman 3: Postgres COPY ile boşaltma
Katman 3: Postgres COPY ile boşaltma
Boşaltma işçisi pgsqlCopyFromArray() metodunu kullanır ve 10 yüksek hacimli tablo için (istekler, sorgular, işler, günlükler, önbellek olayları, postalar, bildirimler, çıkış istekleri, planlı görevler, komutlar) kullanılır. COPY, bu yığın boyutunda eşdeğer çoklu satır INSERT’lerden yaklaşık 5-10 kat daha hızlıdır; parse-plan giderleri ortadan kalkar ve wire format daha yoğun hale gelir.
INSERT, eşleşme satırını parmak izi ile güncelleyerek grubun istisna yolu için hayatta kalır ve her kullanıcı için sayaçlar için. COPY, güncellemeleri yapamaz, bu nedenle bunlar daha yavaş bir yolda kalır. Ayrıca bunlar en düşük hacimli tablolardır, bu yüzden sorun oluşturmaz.
Verimlilik için en büyük tek satır değişikliği:
SET synchronous_commit = off;
Bu, 2-5 kat kazanç sağlar. Ajan, durumu zaten SQLite WAL tarafından garanti edildiği için boşaltma bağlantısında synchronous_commit ayarını iptal eder. En kötü senaryo, aynı yığın iki kez COPY işleminden geçer. Bu, bir gözlem ürünü için kabul edilebilir bir durumdur.
Yığın boyutu her COPY çağrısı için 5,000 satırdır. 1k, 5k, 10k, 50k değerlerini test ettim. 5k’dan sonra, Postgres yazma gecikmesi baskın hale gelir ve tampon boşaltmadan daha hızlı bir şekilde dolmaya başlar.
Fork güvenliği tuzağı
Fork güvenliği tuzağı
Bu, tüm bir haftasonumu aldı.
pcntl_fork() yöntemi, ajanın N drain işçisini başlatmasını sağlar. Her çocuğun kendi SQLite ve Postgres bağlantısına ihtiyacı vardır. Naif yaklaşım (her ikisini de ebeveyn süreçte açmak, fork yapmak ve çocukların miras almasına izin vermek) ilk çocuğun çıkmasıyla SQLite WAL’ını bozar.
Çözüm anlaşılmazdır: fork’tan hemen önce ebeveynin SQLite PDO’sunu kapatın ve her ikisini de fork’tan sonra hem ebeveyn hem de her çocukta yeniden oluşturun. PDO, dosya kilitleri ve her bağlantı durumu kurar ki bu, fork(2)‘nin kopyala-yazma anlamsalası tarafından kısmen kopyalanır. Çocuk çıkış yaptığında ve yok edici fonksiyonunu çalıştırdığında, ebeveynin hâlâ sahibi olduğunu düşündüğü durumu yok eder.
Temiz bir hata mesajı yoktur. Saatler sonra hiçbir belirgin tetikleyici olmadan karışıklık SQLITE_CORRUPT hataları alırsınız.
Postgres için de aynı kural geçerlidir fakat başarısızlık durumu daha samimidir: hemen “bozuk boru” hataları alırsınız çünkü her iki süreç de aynı TCP soketinden okumaya çalışır.
Darboğazın gerçekten nerede olduğu
Darboğazın gerçekten nerede olduğu
Tüm bunlardan sonra, yükleme tek bir örnekte yaklaşık 13,400 yük/s’ye ulaşır. Bu, SQLite’ın sınırı değildir (tampon, bunun çok daha hızlı olduğu yükleri absorbe edebilir). Bu, Postgres değildir (4 boşaltıcı işçi ile ve COPY ile yaklaşık 22,000 satır/s sürdürülebilir). Bu, tek bir PHP olay döngüsündeki TCP kabul döngüsüdür.
Çözüm SO_REUSEPORT ve aynı port üzerinde dinleyen birden çok ajan sürecidir. Linux çekirdeği yeni bağlantıları bunlar arasında dağıtır. macOS bunu yapmaz (ilk kabul eden sürece her bağlantıyı verir), bu nedenle bu yalnızca Linuxa özel bir optimizasyondur.
Nightwatch ile birlikte çalıştırmak
Nightwatch ile birlikte çalıştırmak
Barındırılan planı çıkarmanıza gerek yok. NIGHTOWL_PARALLEL_WITH_NIGHTWATCH=true ayarını yapın ve ajanın hizmet sağlayıcısı, Nightwatch’ın Core::ingest bağlamasını bir fan-out adaptörü ile sarmalar. Her yük, hem Laravel Cloud’a hem de yerel TCP soketinize gider, bu yüzden ikisini yan yana çalıştırabilir ve hangi kaynakları kullandığınızı karşılaştırabilirsiniz.
Fan-out, Nightwatch yükü kabul ettikten sonra çalışır, bu yüzden zaten ödediğiniz barındırılan yolu bozamaz.
Açık kaynak olan
Açık kaynak olan
Tüm uygulama MIT lisanslıdır, Packagist’te nightowl/agent olarak bulunmaktadır ve her Laravel 11 veya 12 uygulamasında çalışır:
composer require nightowl/agent
php artisan nightowl:install
php artisan nightowl:agent
Repo: github.com/lemed99/nightowl-agent
Kendi Postgres tablolarınızın üzerine UI inşa etmek istemiyorsanız usenightowl.com adresinde bir barındırılan gösterge paneli mevcut. Ajan bunun olmadan da sorunsuz çalışır.
Kaynak: Orijinal Makale


