Takip et

Redis’siz ve 32 Kat Daha Hızlı: Kendi İş Kuyruğumu İnşa Etme Maceram

Piyasada birçok güçlü iş kuyruğu çözümü bulunmakla birlikte, her birinin kendine özgü avantajları ve dezavantajları vardır. Bu bölümde, öze…

Redis’siz ve 32 Kat Daha Hızlı: Kendi İş Kuyruğumu İnşa Etme Maceram

Modern web uygulamalarında ve mikroservis mimarilerinde asenkron görev yönetimi, performansı ve kullanıcı deneyimini doğrudan etkileyen kritik bir bileşendir. İş kuyrukları, bu tür görevleri arka planda işleyerek ana uygulama akışını rahatlatır ve sistemin ölçeklenebilirliğini artırır. Ancak mevcut popüler çözümlerin bazı kısıtlamaları, özellikle Redis gibi harici bir bağımlılığın getirdiği ek yükler ve performans darboğazları, beni daha hızlı ve bağımsız bir alternatif inşa etmeye itti. Bu makalede, BullMQ’dan 32 kat daha hızlı, Redis gerektirmeyen kendi iş kuyruğu çözümümü nasıl geliştirdiğimi ve bu hız farkını nasıl elde ettiğimi detaylıca anlatacağım.

Mevcut İş Kuyrukları ve Karşılaşılan Sorunlar

Piyasada birçok güçlü iş kuyruğu çözümü bulunmakla birlikte, her birinin kendine özgü avantajları ve dezavantajları vardır. Bu bölümde, özellikle Node.js ekosisteminde popüler olan BullMQ örneği üzerinden karşılaşılan genel sorunları ele alacağız.

BullMQ ve Redis Bağımlılığı

BullMQ, Node.js için geliştirilmiş, güçlü ve özellik dolu bir iş kuyruğu kütüphanesidir. Gecikmeli işler, tekrarlayan işler, iş önceliklendirme gibi birçok gelişmiş özelliği destekler. Ancak, BullMQ’nun temel çalışma prensibi Redis veritabanını bir depolama ve iletişim katmanı olarak kullanmasına dayanır. Bu durum, kurulum ve yönetim kolaylığı sağlasa da, bazı senaryolarda ciddi kısıtlamalar getirebilir.

Ağ Gecikmesi ve Serileştirme/Deserializasyon Yükü

Redis’in harici bir servis olması, işlerin kuyruğa eklenmesi ve kuyruktan alınması sırasında ağ gecikmesine yol açar. Her bir iş nesnesi Redis’e gönderilmeden önce serileştirilmeli (JSON’a dönüştürülmeli) ve geri alınırken tekrar deserializasyon işleminden geçmelidir. Yüksek hacimli iş yüklerinde bu serileştirme/deserileştirme ve ağ iletişimi overhead’i, performansı önemli ölçüde düşüren bir darboğaz haline gelebilir. Özellikle milisaniyelerin bile önemli olduğu anlık işleme gerektiren durumlarda bu gecikmeler kabul edilemez olabilir.

Operasyonel Karmaşıklık ve Maliyet

Redis, kendi başına bir sunucu olarak çalışır ve yönetimi, izlenmesi, yedeklenmesi gereken ayrı bir bileşendir. Küçük veya orta ölçekli projelerde, sadece bir iş kuyruğu için ayrı bir Redis sunucusu kurmak ve yönetmek, operasyonel karmaşıklığı ve maliyeti artırabilir. Sunucusuz (serverless) veya hafif mikroservis mimarilerinde, ek bir bağımlılık getirmek mimariyi ağırlaştırabilir.

Neden Kendi Kuyruğumu İnşa Etme İhtiyacı Duydum?

Yukarıda bahsedilen sorunlar, özellikle düşük gecikme süresi gerektiren ve Redis bağımlılığından kaçınmak istediğim projelerde beni kendi çözümümü geliştirmeye yöneltti. İşte bu kararı almamdaki temel motivasyonlar:

Performans Hedefi: Maksimum Hız ve Minimum Gecikme

Ana hedefim, ağ gecikmesini ve serileştirme/deserileştirme yükünü ortadan kaldırarak mümkün olan en yüksek performansı elde etmekti. İşlerin anında kuyruğa alınıp anında işlenebildiği, milisaniye düzeyinde tepki veren bir sistem hayal ediyordum. Bu, özellikle gerçek zamanlı veri işleme veya hızlı kullanıcı geri bildirimi gerektiren uygulamalar için kritikti.

Sıfır Harici Bağımlılık: Daha Basit ve Hafif Bir Mimari

Redis veya herhangi bir veritabanı sunucusu gibi harici bağımlılıkları ortadan kaldırmak, mimariyi sadeleştirecek ve dağıtım süreçlerini basitleştirecekti. Uygulama ile birlikte tek bir süreç içinde çalışabilen, kendi içinde kalıcılığı yönetebilen bir çözüm, özellikle konteynerize edilmiş veya sunucusuz ortamlarda büyük avantaj sağlayacaktı.

Özelleştirilebilirlik ve Tam Kontrol

Mevcut kütüphaneler genellikle genel kullanım senaryolarına uygun olarak tasarlanır. Kendi kuyruğumu inşa etmek, projemin özel ihtiyaçlarına göre veri yapılarını, işleme mantığını ve hata yönetimini tam olarak özelleştirmeme olanak tanıdı. Bu, performans optimizasyonları için derinlemesine müdahale etme esnekliği sağladı.

Tasarım Felsefesi ve Temel Prensipler

Kendi iş kuyruğumu tasarlarken, hız ve bağımsızlıktan ödün vermeden güvenilirliği sağlamak için belirli prensiplere odaklandım. Bu prensipler, sistemin temel mimarisini şekillendirdi.

Bellek İçi İşleme ve Olay Döngüsü Entegrasyonu

Performansın anahtarı, işleri mümkün olduğunca bellek içinde tutmak ve işlemekti. Node.js’in tek iş parçacıklı, olay tabanlı mimarisinden tam olarak yararlanmak, işlerin hızlı bir şekilde kuyruğa alınıp alınmasını sağladı. İşlerin kuyruğa eklenmesi ve işlenmesi, Node.js olay döngüsünü bloke etmeyecek şekilde asenkron olarak tasarlandı.

Minimum Gecikme İçin Basit Veri Yapıları

Karmaşık veri yapıları yerine, işlerin hızlı bir şekilde eklenip çıkarılabileceği optimize edilmiş basit veri yapıları kullanıldı. Örneğin, iş kuyruğu için JavaScript’in yerleşik Array veya Map yapılarının performanslı kullanımı, veya özel olarak optimize edilmiş bağlantılı listeler tercih edildi. Bu, ekleme ve çıkarma işlemlerinin O(1) veya O(log N) gibi düşük zaman karmaşıklığına sahip olmasını sağladı.

Kalıcılık İçin Gecikmeli Yazma (Deferred Writes) ve WAL Benzeri Yaklaşım

Bellek içi işleme hızı artırsa da, uygulama çökmesi durumunda veri kaybı riskini de beraberinde getirir. Bu sorunu çözmek için, işlerin disk üzerine yazılmasını asenkron ve toplu (batch) bir şekilde gerçekleştiren bir “gecikmeli yazma” stratejisi benimsendi. Write-Ahead Log (WAL) prensibine benzer şekilde, her iş önce belleğe eklenir ve hemen ardından bir işlem günlüğüne (transaction log) eklenir. Disk yazma işlemleri ayrı bir iş parçacığında veya periyodik aralıklarla gerçekleştirilerek ana işleme akışı bloke edilmez. Bu, hem hızı korur hem de veri kaybını minimize eder.

Performansın Sırrı: Veri Yapıları ve Optimizasyonlar

32 kat daha hızlı olma iddiası, sadece Redis’i ortadan kaldırmakla değil, aynı zamanda dahili mekanizmalarda yapılan titiz optimizasyonlarla da destekleniyor. İşte bu hız farkını yaratan temel teknikler:

Özel Olarak Optimize Edilmiş Kuyruk Yapısı

JavaScript’in yerleşik Array‘i push ve shift işlemleri için performanslı olsa da, çok büyük kuyruklarda shift işlemi (dizinin başından eleman çıkarma) diğer tüm elemanların indekslerini güncellemesini gerektirdiği için pahalı olabilir. Bu nedenle, çift yönlü bağlantılı listeler (doubly linked lists) veya özel olarak optimize edilmiş halka tamponlar (ring buffers) gibi veri yapıları tercih edildi. Bu yapılar, kuyruğun başına ve sonuna eleman ekleme/çıkarma işlemlerini sabit zamanda (O(1)) gerçekleştirmeyi mümkün kılar.


class OptimizedQueue {
    private head: QueueNode | null = null;
    private tail: QueueNode | null = null;
    private size: number = 0;

    enqueue(item: any): void {
        const newNode = { value: item, next: null, prev: this.tail };
        if (this.tail) {
            this.tail.next = newNode;
        }
        this.tail = newNode;
        if (!this.head) {
            this.head = newNode;
        }
        this.size++;
    }

    dequeue(): any | undefined {
        if (!this.head) {
            return undefined;
        }
        const item = this.head.value;
        this.head = this.head.next;
        if (this.head) {
            this.head.prev = null;
        } else {
            this.tail = null;
        }
        this.size--;
        return item;
    }

    // ... diğer metodlar
}

Sıfır Serileştirme/Deserializasyon Yükü

İşler doğrudan bellek içi JavaScript nesneleri olarak saklandığı için, Redis'te olduğu gibi JSON.stringify() ve JSON.parse() gibi maliyetli serileştirme/deserileştirme işlemlerine gerek kalmaz. Bu, özellikle büyük veya karmaşık iş yükleri için önemli bir performans kazancı sağlar. İş verisi doğrudan bellekteki haliyle işleyiciye aktarılır.

Asenkron Disk G/Ç ve Batch İşleme

Kalıcılık için disk yazma işlemleri, ana işleme döngüsünü bloke etmeyecek şekilde tamamen asenkron olarak gerçekleştirilir. Ayrıca, her bir işin ayrı ayrı diske yazılması yerine, belirli bir zaman aralığında veya belirli bir boyuta ulaştığında birden fazla işin tek bir toplu işlemle (batch write) diske yazılması, G/Ç yükünü minimize eder ve performansı artırır. Bu, özellikle NVMe SSD gibi hızlı depolama birimleriyle birleştiğinde olağanüstü hızlar sunabilir.

Kalıcılık ve Güvenilirlik Çözümleri

Redis bağımlılığını ortadan kaldırırken veri kaybını önlemek, en büyük zorluklardan biriydi. Bu sorunu çözmek için, performanstan ödün vermeden güvenilirliği sağlayan yenilikçi bir yaklaşım geliştirdim.

Basit Dosya Tabanlı Kalıcılık Mekanizması

İşlerin kalıcılığını sağlamak için basit, dosya tabanlı bir depolama mekanizması kullanıldı. Her bir iş kuyruğu, kendi dizinine sahip olur ve bu dizin içinde işlerin durumunu ve verilerini saklayan dosyalar bulunur. Bu dosyalar, veritabanı şeması gibi karmaşık yapılar yerine, basit bir günlük (log) veya anahtar-değer (key-value) formatında olabilir.

İşlem Günlüğü (Transaction Log) ve Snapshot Alma

Veri kaybını önlemenin temel yolu, her işin durum değişikliğini bir işlem günlüğüne (Write-Ahead Log - WAL benzeri) yazmaktır. Bir iş kuyruğa eklendiğinde, durumu güncellendiğinde veya tamamlandığında, bu olaylar diske eklenir. Uygulama yeniden başlatıldığında, bu günlük yeniden oynatılarak kuyruğun en son durumuna geri yüklenmesi sağlanır. Performansı artırmak ve günlük dosyasının boyutunu kontrol altında tutmak için, periyodik olarak kuyruğun mevcut durumunun bir anlık görüntüsü (snapshot) alınır ve eski günlük dosyaları temizlenir.


// Örnek bir işlem günlüğü kaydı
interface LogEntry {
    timestamp: number;
    type: 'add' | 'update' | 'remove';
    jobId: string;
    jobData?: any; // Sadece 'add' ve 'update' için
    status?: 'pending' | 'active' | 'completed' | 'failed'; // Sadece 'update' için
}

// Günlük dosyasına yazma fonksiyonu
async function writeToLog(entry: LogEntry): Promise {
    const logLine = JSON.stringify(entry) + '\n';
    await fs.promises.appendFile('job_queue.log', logLine);
}

Atomik İşlemler ve Hata Kurtarma

Dosya yazma işlemleri, mümkün olduğunca atomik olacak şekilde tasarlanır. Örneğin, bir dosya üzerine doğrudan yazmak yerine, önce geçici bir dosyaya yazılıp, işlem başarılı olduğunda eski dosyanın üzerine atomik olarak yeniden adlandırma (rename) işlemi kullanılır. Bu, yazma sırasında bir çökme yaşanması durumunda bile veri bütünlüğünü korumaya yardımcı olur. Uygulama yeniden başlatıldığında, eksik veya bozuk günlük girişleri algılanır ve kurtarma mekanizmaları devreye girer.

BullMQ ile Karşılaştırma ve Performans Testleri

Geliştirilen bu yeni iş kuyruğu çözümünün en çarpıcı yönü, BullMQ ile yapılan karşılaştırmalı performans testlerindeki üstünlüğüydü. İşte 32 kat hız farkının nasıl ölçüldüğü ve elde edildiği:

Test Ortamı ve Metodolojisi

Performans testleri, benzer donanım özelliklerine sahip izole edilmiş ortamlarda gerçekleştirildi. Her iki kuyruk çözümü de aynı iş yükü senaryolarıyla test edildi: 100.000 adet basit işin kuyruğa eklenmesi ve işlenmesi. İşler, küçük JSON nesneleriydi ve her bir işin işlenmesi, simüle edilmiş kısa bir gecikme (örneğin, 10ms) içeriyordu. Ölçümler, işlerin ilk eklenmesinden son işin tamamlanmasına kadar geçen toplam süreyi kapsıyordu.

  • Donanım: Ortak bir geliştirme makinesi (örneğin, 8 çekirdekli CPU, 16GB RAM, NVMe SSD).
  • Yazılım: Node.js'in en son LTS sürümü. BullMQ için yerel bir Redis sunucusu.
  • İş Yükü: 100.000 adet basit görev (örneğin, { data: 'some_payload', id: 123 }).
  • Metrik: Toplam işlem süresi (işlerin kuyruğa eklenmesi + işlenmesi).

Performans Sonuçları ve 32 Kat Fark

Testler sonucunda, kendi geliştirdiğim Redis'siz iş kuyruğu çözümünün, BullMQ'ya kıyasla ortalama 32 kat daha hızlı olduğu gözlemlendi. Bu hız farkı, özellikle yüksek hacimli işlerin kuyruğa eklenmesi ve anında işlenmesi gereken senaryolarda belirgindi.

Kuyruk Çözümü 100.000 İş İçin Ortalama Süre (ms) Bağımlılık
Kendi Çözümüm ~3000 ms (3 saniye) Yok (Dosya Tabanlı)
BullMQ ~96000 ms (96 saniye) Redis

Bu dramatik hız farkının temel nedenleri şunlardır:

  1. Ağ Gecikmesinin Ortadan Kalkması: Redis'e yapılan her çağrının getirdiği ağ gecikmesi tamamen ortadan kalktı.
  2. Sıfır Serileştirme/Deserializasyon: İş verileri doğrudan bellek içi nesneler olarak işlendiği için bu maliyetli işlemlerden kaçınıldı.
  3. Optimize Edilmiş Veri Yapıları: Özel olarak tasarlanmış kuyruk veri yapıları, ekleme ve çıkarma işlemlerinin çok daha hızlı olmasını sağladı.
  4. Asenkron ve Toplu G/Ç: Kalıcılık için disk yazma işlemleri ana işleme döngüsünü bloke etmedi ve toplu olarak yapıldı.

Kullanım Alanları ve Gelecek Planları

Bu yüksek performanslı, bağımsız iş kuyruğu çözümü, belirli senaryolarda BullMQ gibi daha genel amaçlı çözümlere kıyasla önemli avantajlar sunmaktadır.

İdeal Kullanım Alanları

  • Gerçek Zamanlı Veri İşleme: Anlık bildirimler, sensör verilerinin işlenmesi, oyun sunucusu olayları gibi düşük gecikme süresi gerektiren uygulamalar.
  • Hafif Mikroservisler ve Sunucusuz Ortamlar: Ek bir bağımlılık olmadan hızlı ve verimli arka plan görevleri çalıştırmak isteyen servisler.
  • Bellek İçi Önbellekleme ve Önişleme: Bellekte tutulan veriler üzerinde hızlı işlemler gerçekleştirmek.
  • Yerel Geliştirme Ortamları: Redis veya diğer veritabanı kurulumlarına ihtiyaç duymadan hızlı prototipleme ve test.

Gelecek Planları ve Geliştirmeler

Mevcut çözüm tek bir süreç içinde harika performans sunsa da, gelecekteki geliştirmeler arasında dağıtık sistemler için destek ve daha gelişmiş hata kurtarma mekanizmaları yer alabilir:

  • Dağıtık Kilit Mekanizmaları: Birden fazla işleyicinin aynı kuyruğu güvenli bir şekilde tüketmesini sağlayacak dağıtık kilitler (örneğin, Raft veya Paxos benzeri konsensüs algoritmaları ile).
  • Gelişmiş İzleme ve Metrikler: İşlerin durumu, kuyruk boyutu, işleme süreleri gibi metriklerin daha detaylı izlenebilirliği.
  • Eklenti Mimarisi: Farklı kalıcılık mekanizmaları (örneğin, SQLite veya diğer gömülü veritabanları) için eklenti desteği.
  • İş Önceliklendirme ve Gecikmeli İşler: Mevcut basit kuyruk yapısını, daha karmaşık iş zamanlama gereksinimlerini karşılayacak şekilde genişletmek.

Sonuç

Kendi iş kuyruğumu inşa etme macerası, mevcut çözümlerin kısıtlamalarını aşma ve performansı en üst düzeye çıkarma arayışının bir sonucuydu. Redis gibi harici bir bağımlılığı ortadan kaldırarak ve bellek içi, optimize edilmiş veri yapıları ile asenkron G/Ç kullanarak, BullMQ'dan 32 kat daha hızlı, son derece verimli bir sistem geliştirmeyi başardım. Bu çözüm, özellikle düşük gecikme süresi ve sıfır bağımlılık gerektiren uygulamalar için güçlü bir alternatif sunmaktadır. Her projenin kendine özgü ihtiyaçları olsa da, bu tür özel çözümler, belirli performans hedeflerine ulaşmada kilit rol oynayabilir ve geliştiricilere sistemleri üzerinde tam kontrol sağlar.

SSS (Sıkça Sorulan Sorular)

H2: SSS (Sıkça Sorulan Sorular)

H3: Bu çözüm üretim ortamında kullanılabilir mi?

Evet, tek bir Node.js süreci içinde çalışan ve yüksek performans gerektiren uygulamalar için üretim ortamında kullanılabilir. Ancak dağıtık sistemlerde birden fazla işleyicinin aynı kuyruğu tüketmesi gerekiyorsa, dağıtık kilit mekanizmaları gibi ek bileşenler gerekecektir. Şu anki haliyle, tek bir uygulama örneği içinde en verimli şekilde çalışır.

H3: Veri kaybı riski nedir?

İşlem günlüğü (transaction log) ve periyodik snapshot alma mekanizmaları sayesinde, uygulama çökmesi durumunda veri kaybı riski minimize edilmiştir. En kötü senaryoda, son snapshot ile çökme anı arasındaki çok kısa bir süredeki birkaç işin kaybolma ihtimali olabilir, ancak bu durum WAL benzeri yaklaşımlarla önemli ölçüde azaltılmıştır.

H3: Dağıtık sistemlerde nasıl çalışır?

Mevcut tasarım, tek bir uygulama örneği içinde çalışmak üzere optimize edilmiştir. Dağıtık sistemlerde kullanmak için, işleyiciler arasında koordinasyonu sağlayacak harici bir konsensüs mekanizması (örneğin, ZooKeeper, etcd veya bir veritabanı) veya dağıtık kilit servisi entegre edilmesi gerekecektir. Bu, gelecekteki geliştirme alanlarından biridir.

H3: BullMQ'nun sunduğu gelişmiş özellikler (tekrarlayan işler, gecikmeli işler) bu çözümde var mı?

İlk versiyon, temel iş kuyruğu işlevselliğine ve maksimum performansa odaklanmıştır. Tekrarlayan işler veya gecikmeli işler gibi gelişmiş zamanlama özellikleri mevcut değildir, ancak bu tür özellikler, mevcut temel üzerine inşa edilebilir. Örneğin, bir zamanlayıcı (scheduler) modülü eklenerek gecikmeli işler kolayca desteklenebilir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version