Çoklu Instance Ortamında Cron İşleri Neden Sorun Yaratır?
Cron işleri, arka planda belirli aralıklarla görev çalıştırmak için çok kullanışlıdır. Ancak, yatay ölçeklemeye sahip bir uygulamada, bir cron işinin aynı anda birden fazla instance tarafından çalıştırılması, tekrarlayan işlemlere ve veri tutarsızlıklarına yol açabilir. Örneğin, her bir instance’ın aynı veri kaydını güncellemeye çalışması veya belirli bir görevi tekrarlaması ciddi sorunlara neden olabilir. Bu sorunu çözmek için locking mekanizmaları kullanarak görevlerin yalnızca tek bir instance tarafından çalıştırılmasını sağlamalıyız.
NestJS'de Cron İşleri ile Çalışmak
NestJS, cron işleri için @nestjs/schedule modülünü sunar ve bu modül ile cron görevlerini kolayca yönetebiliriz. Bu modül, cron görevlerini zamanlamak için dekoratörler sağlar. Kurulum için öncelikle modülü ekleyelim:
npm install @nestjs/schedule
Modülü kurduktan sonra, @Cron, @Interval, ve @Timeout gibi dekoratörleri kullanarak görevlerimizi tanımlayabiliriz. Aşağıda, basit bir cron işinin nasıl tanımlandığını görebilirsiniz:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
// Dakikada bir çalışan bir cron görevi tanımlayalım
@Cron(CronExpression.EVERY_MINUTE)
handleCron() {
console.log('Cron job çalışıyor: ', new Date().toISOString());
}
}
Yukarıdaki örnekte, @Cron(CronExpression.EVERY_MINUTE) ifadesi, her dakika çalışacak bir cron işini belirtir. NestJS, @Cron dekoratörü ile farklı zaman aralıkları belirlemenize olanak tanır. Ancak bu iş, uygulamanızın birden fazla instance’ı olduğunda her instance tarafından çalıştırılır. Çoklu instance ortamında her cron işini yalnızca bir kez çalıştırmak için kilitleme (locking) mekanizmalarına ihtiyaç duyulur.
Locking Mekanizmaları: Çoklu Instance Sorunlarını Çözme Yöntemleri
Locking mekanizmaları, belirli bir görevin yalnızca bir instance tarafından çalıştırılmasını sağlar. Böylece tekrarlanan işlemler veya veri tutarsızlıkları önlenmiş olur. İşte en yaygın kullanılan dört locking yöntemi:
Veritabanı ile Pessimistic Lock
Veritabanı tabanlı pessimistic locking, özellikle SQL veritabanlarında tercih edilen bir yöntemdir. Bir görev başlamadan önce veritabanında bir satır kilitlenir ve işlem tamamlandığında bu kilit kaldırılır. Örneğin, PostgreSQL veya MySQL kullanıyorsanız, SELECT FOR UPDATE gibi komutlar ile satır kilitleyebilirsiniz. Bu yöntem oldukça güvenilirdir, ancak her cron çalışmasında veritabanı bağlantısı kurduğu için performans sorunlarına yol açabilir.
Avantajlar:
- Güvenilir ve SQL, NoSQL destekli veritabanlarında kolayca uygulanabilir.
- Veritabanı işlemlerine entegre olduğu için merkezi bir kilitleme sağlar.
Dezavantajlar:
- Ağır yük altında veritabanı bağlantıları açısından maliyetli olabilir.
Redis ile Lock Kaydı
Redis, hızlı ve hafif bir veri yapısı sunar, bu nedenle locking mekanizmalarında oldukça etkilidir. Görev çalıştırılmadan önce Redis üzerinde bir kilit oluşturulup (örneğin bir anahtar değeri atanarak) görev sonunda bu kilit kaldırılır. Redis ile zaman aşımı (timeout) ekleyerek kilidin sonsuza kadar açık kalmamasını da sağlayabilirsiniz.
Avantajlar:
- Redis hızlıdır ve düşük gecikme süresi sağlar.
- Merkezi bir kilit mekanizması sunar ve kolay ölçeklenebilir.
Dezavantajlar:
- Redis bağlantısında sorun oluşursa kilitler açık kalabilir. Tabi TTL eklediğimiz durumda bu dezavantajdan kurtulmuş oluruz.
- Redis gibi bir harici depolama servisine ihtiyaç duyar.
API Call ile İzleme
Bazı projelerde merkezi bir API aracılığıyla görevlerin durumu izlenir. Her cron işi başladığında bu API’ye bir çağrı yapılarak “çalışıyor” durumu atanır ve iş bittiğinde bu durum güncellenir. API yanıtına göre görev yalnızca bir kez çalıştırılır. Bu yöntem, daha karmaşık ve merkezi bir izleme gerektiren projeler için uygundur.
Avantajlar:
- Merkezi bir kontrol sağlar.
- Çoklu sistemler veya mikro servislerde görev izlemeyi kolaylaştırır.
Dezavantajlar:
- Ek geliştirme süreci gerektirir. Örneğin Api Call'ları atacak third party bir app vasıtasıyla veya curl istekleri atan sh kodlarıyla bu cron yönetilebilir.
- API performansı cron işlerinin verimliliğini ufak da olsa etkileyebilir.
Queue (Kuyruk) Kullanımı ile Görev Yönetimi
Queue tabanlı yaklaşımlar, özellikle görevlerin sırayla işlenmesini sağlamada ve işlem tekrarı riskini azaltmada çok etkilidir. NestJS ile BullMQ kullanarak her cron işini bir kuyruğa ekleyebilir ve bu görevlerin yalnızca tek bir instance tarafından işlenmesini sağlayabilirsiniz. Queue yapısı, çok-instance ortamlarında görev yönetiminde en etkili çözüm yöntemlerinden biridir.
Avantajlar: Görevlerin sıralı işlenmesi, yatay ölçekleme uyumu
Dezavantajlar: Ek bağımlılıklar (Redis, BullMQ) ve yapılandırma ihtiyacı
Farklı Instance’ların Aynı İş İçin Queue’ya Ekleme Durumu
Çoklu instance ortamında her instance’ın aynı cron işini queue'ya eklemek istemesi durumunda, görev tekrarlarını önlemek için Unique Job (Benzersiz Görev) Tanımlanabilir. BullMQ, aynı jobId
ile eklenen görevlerin birden fazla eklenmesini engelleyebilir. Bu sayede tüm instance’lar aynı işi queue’ya eklemeye çalışsa bile iş yalnızca bir kez eklenmiş olur.
Kod Örneği - Redis lock
Adım 1: Redis Client Oluşturma
İlk olarak, Redis bağlantısını ayarlamak için bir Redis client oluşturun. ioredis kütüphanesini kullanarak bağlantıyı kurabilirsiniz:
npm install ioredis
// redis.service.ts
// Redis/Cache servis oluşturmanın birçok yöntemi bulunmakta istediğiniz bir yöntemi entegre edebilirsiniz.
import { Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
@Injectable()
export class RedisService {
private client: Redis.Redis;
constructor() {
this.client = new Redis({
host: 'localhost', // Redis sunucusunun host adresi
port: 6379, // Redis sunucusunun port numarası
});
}
getClient(): Redis.Redis {
return this.client;
}
}
Bu servis, diğer dosyalarda RedisService üzerinden Redis client’ını kullanmanıza olanak tanır.
Adım 2: Redis Lock Mekanizması ile Cron İşini Tekil Olarak Çalıştırma
Şimdi Redis client’ı kullanarak kilit işlemlerini gerçekleştiren bir cron iş fonksiyonu yazalım. Bu örnekte, Redis üzerinde bir kilit oluşturuluyor; kilit alınabiliyorsa iş çalıştırılıyor. İş tamamlandığında kilit kaldırılıyor.
// task.service.ts
// Ben loglarımı console.log olarak kullandım ama siz yerleşik Nestjs Logger'ı kullanabilirsiniz.
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { RedisService } from './redis.service';
@Injectable()
export class TaskService {
private readonly lockKey = 'cron-job-lock'; // Kilit anahtarı
private readonly lockTTL = 60; // Kilit süresi (saniye cinsinden)
constructor(private readonly redisService: RedisService) {}
@Cron(CronExpression.EVERY_MINUTE)
async handleCronJob() {
const client = this.redisService.getClient();
try {
// NX (Only set if not exists) ve EX (Expire time) ile kilidi almayı deniyoruz.
const isLocked = await client.set(this.lockKey, 'locked', 'NX', 'EX', this.lockTTL);
// Eğer kilit alınamazsa işlem zaten başka bir instance tarafından yürütülmektedir.
if (!isLocked) {
console.log('Bu cron işi başka bir instance tarafından çalıştırılıyor.');
return;
}
// İşlem başlıyor
console.log('Cron job başlatıldı: ', new Date().toISOString());
// Buraya cron işinin yapılacağı işlemleri ekleyin
await this.executeJob();
console.log('Cron job tamamlandı.');
} catch (error) {
console.error('Cron job çalıştırılırken hata oluştu:', error);
} finally {
// İşlem tamamlandığında veya hata oluştuğunda kilidi kaldır.
// Bu kısım çok kritiktir. Eğer hata oluştuğunda kilit
// kalkmazsa timeout süresi kadar cron çalışamaz hale gelir.
// Bu yüzden try ve finally'yi mutlaka kullanın. Dilerseniz
// catch kullanmayıp filterlarda yönetebilirsiniz. Daha doğru
// bir yaklaşım olur.
await client.del(this.lockKey);
console.log('Kilit kaldırıldı.');
}
}
private async executeJob() {
// Burada cron job işlemi yapılır
return new Promise((resolve) => setTimeout(resolve, 5000)); // Örnek: 5 saniye bekleme
}
}
Bonus PM2
PM2, Node.js uygulamalarını production ortamında yönetmek için yaygın olarak kullanılan bir process manager (işlem yöneticisi) aracıdır. Uygulamayı birden fazla instance ile çalıştırma imkanı sunarak CPU çekirdeklerini verimli kullanmayı sağlar. PM2, cluster mode ile aynı uygulamanın birden fazla örneğini başlatabilir ve load balancing (yük dengeleme) yaparak isteklere cevap verecek instance’ları otomatik olarak seçer.
PM2 ile Multiple Instance Yönetimi
PM2 ile -i parametresini kullanarak birden çok instance başlatabilirsiniz. Örneğin, maksimum CPU çekirdeğini kullanacak şekilde başlatmak için şu komut uygulanabilir:
pm2 start app.js -i max
Bu komut, uygulamanın her bir CPU çekirdeğinde bir instance başlatmasını sağlar. NODE_APP_INSTANCE gibi değişkenlerle her instance’a özel bir ID atanır, böylece farklı işlemler bu ID ile tanımlanabilir.
PM2 ile Multiple Instance Yönetiminin Dezavantajları
PM2’nin birden fazla instance yönetiminde önemli bir dezavantajı, kendi başına bir failover veya tekil işlem kontrolü sağlamamasıdır. Eğer bir işin yalnızca bir instance tarafından yürütülmesi gerekiyorsa, PM2 bu yönetimi doğrudan desteklemez. Dinamik ortamlarda instance’ların ölçeklendirilmesi gerektiğinde, PM2 uygulamanın durumunu sürekli takip etmediği için her instance bağımsız olarak çalışır ve iş sürekliliği garanti edilemez.
PM2, multiple instance yönetiminde başlangıç seviyesinde bir çözüm sunar, ancak özellikle cron işlerinin tek bir instance’ta çalışması gerektiğinde yetersiz kalabilir. Özellikle K8s gibi araçlarda yatay ölçeklendirmeyi de yönetemiyorsanız bu yöntemi kullanmanız imkansız hale gelir. Bu tür görevler için Redis tabanlı lock mekanizmaları veya BullMQ gibi araçlar, daha güvenilir bir yapı sağlayarak tekrar eden işleri tekilleştirmek için daha uygun olacaktır.
Kapanış
Örnekleriyle önemli bir ve çok yaygın bir sorunu ele almaya çalıştım. Aslında nestjs/scheduler kütüphanesinin bunu çok daha iyi ele almasını beklerdim fakat şu tarih itibariyle beklentimi karşılamadı. Bunun gibi daha birçok alternatif olabilir. Yorumlarda siz de bulduğunuz alternatifleri avantaj ve dezavantajlarıyla bahsedebilirseniz çok memnun olurum.
Kolay gelsin 😊
Top comments (0)