Giriş: kimsenin tıklamadığı işler
Her pazartesi sabah 08:00’de e-posta kutuna aynı bülten düşer. Dizi platformu aboneliğin her ay aynı gün gece yarısı yenilenir. Geçen hafta istediğin şifre sıfırlama linki bir saat sonra geçersiz olmuş, bir süre sonra da kaydı veritabanından sessizce silinmiştir. Bunların hiçbiri için kimse bir butona basmadı. Bir yerde bir kod kendi kendine uyandı, işini yaptı ve tekrar uykuya daldı.
O kodun adı job. Ciddi her uygulamada birkaç tane bulunur, çoğunda onlarca. Buna rağmen konu genelde parça parça öğrenilir: bir forumdan kopyalanmış cron satırı, önceki ekipten devralınmış bir Hangfire kurulumu, yıllar önce birinin oluşturduğu ve kimsenin dokunmaya cesaret edemediği bir SQL Server Agent job’ı.
Bu yazıda job kavramını en baştan anlatıyorum. Job’ın ne olduğunu, aynı fikrin neden bu kadar farklı isimle anıldığını, job’lara neden ihtiyaç duyulduğunu, nerede çalıştıklarını, kurarken karşına çıkacak terimleri ve işler ters gittiğinde bile düzgün davranan bir job’ın nasıl yazıldığını adım adım ele alıyorum. Kod örnekleri C# ile yazıldı ama buradaki fikirlerin tamamı her dil ve platform için geçerli.
Job nedir?
Job, bir kullanıcının sonucunu beklemediği, arka planda kendi başına çalışan iş birimidir.
Yazdığımız kodun büyük kısmı biri istediği için çalışır. Kullanıcı bir butona tıklar, tarayıcı sunucuya istek gönderir, sunucu işi yapar ve cevabı geri döner. Kullanıcı bu süre boyunca bekler. İş 30 saniye sürüyorsa ekranda 30 saniye boyunca yükleniyor simgesi döner.
Job bu düzeni tersine çevirir. Çalışmasının sebebi saatin gelmesi ya da bir kuyruğa yeni bir mesaj düşmesidir. O sırada bir ekranın başında bekleyen kimse yoktur. İş bir saniye de sürebilir bir saat de, sonucundan faydalanacak kişi o an çevrimiçi bile olmayabilir.
Akılda tutmak için kullanışlı bir model şu: job, bir fonksiyon ile bir tetikleyicinin birleşimidir. Fonksiyon her gün yazdığın sıradan koddur. Tetikleyici (trigger) ise o kodun ne zaman çalışacağına karar verir. Job’larla ilgili asıl karmaşıklık fonksiyonun kendisinde değil, tetikleyicide ve işler ters gittiğinde ne olacağında yatar.
// İstekle çalışan kod: kullanıcı bu adrese istek attığında çalışır
app.MapGet("/report", () => BuildReport());
// Job (temsili kod): her gece 03:00'te çalışır, kullanıcı yoktur
scheduler.Schedule(() => BuildReport(), "0 3 * * *");
İki satırda da BuildReport fonksiyonu birebir aynı. Değişen tek şey tetikleyici.
Job’lar tetikleyicilerine göre iki büyük aileye ayrılır:
- Zamana bağlı job’lar bir takvime göre çalışır. “Her gece 03:00’te”, “her 15 dakikada bir”, “her ayın ilk günü” gibi. Gece hazırlanan raporlar ve aylık abonelik yenilemeleri bu aileye girer.
- Olaya bağlı job’lar bir şey olduğunda çalışır. Bir kullanıcı kayıt olur, bir kuyruğa mesaj bırakılır, o kuyruğu dinleyen bir worker mesajı alıp hoş geldin e-postasını gönderir. İş yine arka planda yapılır ama tetikleyen şey saat değil bir olaydır.
Bu iki aileyi aklında tut. Bir sonraki bölümdeki terimlerin çoğu aslında ya bu ailelerden birinin adı ya da onları çalıştıran yapının adı.
Aynı aile, farklı isimler
“Job” kelimesi kimin ağzından çıktığına göre biraz farklı şeyler çağrıştırır. Bir veritabanı yöneticisi job deyince SQL Server Agent’ı düşünür. Linux tarafında çalışan biri cron’u düşünür. Uygulama geliştirici ise bir kuyruk ve onu işleyen worker’ı. Hepsi aynı fikirden bahsediyor, sadece yazılımın farklı katmanlarından bakıyorlar. Bu bölümde her ismi tek tek ele alıyorum.
Scheduled job (zamanlanmış görev)
En genel ve en tarafsız ifade. Belirli bir zamanda ya da aralıkla çalışan iş anlamına gelir, araç, dil ya da işletim sistemi hakkında bir şey söylemez. Windows’ta bu adı birebir taşıyan bir araç bile var: Görev Zamanlayıcı (Task Scheduler). Herhangi bir programı belirlenen takvime göre çalıştırabilir.
Örnek: “Her ayın 1’inde geçen ayın faturalarını arşivleyen zamanlanmış bir görevimiz var.”
Cron job
Cron, 1970’lerden beri Unix sistemlerin parçası olan bir zamanlayıcı. Bir şeyin ne zaman çalışacağını tarif eden bir satır yazılır, cron da o işi zamanı gelince çalıştırır. Cron job, bu satırlardan her birine verilen isimdir.
Terim o kadar yaygınlaştı ki artık Linux ile hiçbir ilgisi olmayan platformlarda bile zamana bağlı her iş için “cron job” deniyor. Biri “şuna bir cron kuralım” dediğinde genelde sadece “bunu belirli aralıklarla çalışır hale getirelim” demek istiyordur. Cron’un ortaya koyduğu zamanlama formatı, yani cron ifadesi, bugün neredeyse tüm modern araçlarda kullanılıyor. Nasıl okunduğunu tetikleyiciler bölümünde anlatıyorum.
Örnek: 0 3 * * * /usr/bin/backup.sh satırı her gece 03:00’te bir yedekleme betiği çalıştırır.
Background job (arka plan işi)
Uygulama geliştiricilerin kullandığı terim. Önceki bölümdeki iki aileyi de kapsar: zamanlanmış işleri ve “sonra ama kısa süre içinde” yapılmak üzere kuyruğa atılan işleri. Belirleyici özelliği, işin kullanıcı isteğinin içinden çıkarılıp başka bir yapıya devredilmesidir.
Her ekosistemde kendini bu terimle tanımlayan kütüphaneler var: .NET için Hangfire, Ruby için Sidekiq, Python için Celery, Node.js için BullMQ.
Örnek: Kullanıcı kayıt olduktan sonra API hemen cevap döner ve hoş geldin e-postasını gönderecek bir background job’ı kuyruğa ekler.
Veritabanı job’ı
Pek çok veritabanının kendi içinde bir zamanlayıcısı bulunur. SQL Server’da bu SQL Server Agent’tır, Management Studio’da doğrudan “Jobs” adında bir klasör görürsün. Oracle’da DBMS_SCHEDULER, PostgreSQL’de de yaygın olarak kullanılan pg_cron eklentisi var.
Özellikle kurumsal projelerde “job” kelimesinin akla doğrudan veritabanını getirmesinin sebebi bu. Gece yedekleri, index bakımları ve sistemler arası veri aktarımları yıllardır doğrudan veritabanı içinde tanımlanıyor. Bu job’lar genelde araya hiç uygulama kodu girmeden doğrudan SQL çalıştırır, çoğu zaman da bir stored procedure. Veri işi için hızlıdırlar ama mantığın kod deposunun dışında yaşaması anlamına da gelirler.
Örnek: Her pazar 02:00’de parçalanmış index’leri yeniden oluşturan bir SQL Server Agent job’ı.
Worker ve background service
Bu ikisi job değil. Bunlar job’ları çalıştıran yapılar ve job ile karıştırılmaları kafa karışıklığının en yaygın sebeplerinden biri.
Worker, iş bekleyen ve gelen işi yürüten, sürekli açık duran bir süreçtir. Kuyruk tabanlı bir sistemde worker’lar kuyruktaki mesajları sırayla alıp işler. .NET’teki BackgroundService, uygulama açık olduğu sürece çalışan bir sınıftır, Worker Service şablonu da tamamen bu sınıfların etrafında kurulmuş bir proje tipidir. Windows Service ya da Linux’taki systemd servisi ise işletim sisteminin bir worker’ı arka planda canlı tutma yöntemidir: makine açılınca başlatır, çökerse yeniden ayağa kaldırır.
Kısacası job yapılacak iştir, worker o işi yapan çalışandır, zamanlayıcı ya da kuyruk da çalışana sıradaki işi söyleyen yöneticidir.
Tek bakışta
| Terim | Neyi ifade eder | Tipik ortam | Örnek araçlar |
|---|---|---|---|
| Scheduled job (zamanlanmış görev) | Belirli bir zamanda çalışan iş | Her yerde | Windows Görev Zamanlayıcı, Quartz.NET |
| Cron job | Cron ifadesiyle tanımlanan zamana bağlı iş | Linux, artık genel kullanımda her yerde | cron, Kubernetes CronJob |
| Background job | İstekten çıkarılan, zamanlanmış ya da kuyruklu iş | Uygulama kodu | Hangfire, Sidekiq, Celery, BullMQ |
| Veritabanı job’ı | Veritabanı motorunun tanımlayıp çalıştırdığı iş | SQL Server, Oracle, PostgreSQL | SQL Server Agent, DBMS_SCHEDULER, pg_cron |
| Worker / background service | Job’ları çalıştıran süreç | Uygulama ya da işletim sistemi | .NET BackgroundService, Windows Service, systemd |
Job’lara neden ihtiyaç duyulur?
Kod bir isteğin içinde sorunsuz çalışıyorsa job’a gerek yoktur. Job’lar, bazı işlerin isteğin içine sığmaması yüzünden var. Bunun beş yaygın sebebi bulunuyor.
1. İş, kullanıcıyı bekletemeyecek kadar uzun. 200 sayfalık bir PDF üretmek, yüklenen yüz fotoğrafı yeniden boyutlandırmak ya da bir yıllık satış verisini işlemek dakikalar sürebilir. Web sunucularının ve tarayıcıların zaman aşımı sınırları var, kullanıcıların sabrı ise daha da kısa. İş bir job’a taşındığında istek milisaniyeler içinde “Dosyan hazırlanıyor, hazır olunca e-posta ile haber vereceğiz” gibi bir mesajla döner.
2. İşin belirli bir saatte yapılması gerekiyor. Abonelik gece yarısı yenilenir. Randevudan 24 saat önce hatırlatma gider. Rapor sabah 08:00’de yöneticinin önünde olmalıdır. O anlarda hiçbir kullanıcı işlem yapmıyor, dolayısıyla işi başlatacak başka bir şey gerekiyor.
3. İş düzenli olarak tekrar ediyor. Süresi dolmuş oturumları temizlemek, döviz kurlarını saatte bir çekmek, veritabanını her gece yedeklemek. Bunları elle yapmak mümkün değil, rastgele kullanıcı isteklerinin içine gömmek de işin ne zaman çalışacağını belirsiz hale getirir.
4. İşin hata vermesi kullanıcının işlemini bozmamalı. Hoş geldin e-postasını kayıt isteğinin içinde gönderen bir akış düşün. Mail sunucusu o an çalışmıyorsa kayıt da mı başarısız olmalı? Olmamalı. E-postayı bir background job’a almak bu ikisini birbirinden ayırır. Hesap hemen oluşturulur, e-posta ise gidene kadar yeniden denenir.
5. Ağır iş sakin saatlere kaydırılmalı. Bazı işler veritabanını ya da ağı ciddi şekilde yorar. Bunları trafiğin en düşük olduğu gece 03:00’te çalıştırmak, gün içinde gerçek kullanıcılar için sistemi hızlı tutar. Önceden hesaplama (precomputing) fikri de buradan doğar: ağır bir sonuç gece bir kez hesaplanır, hazır bir tabloya yazılır ve gün içinde kullanıcılar bu hazır sonucu anında okur.
Kısa bir karar kuralı: Bir iş yavaşsa, belirli bir zamana bağlıysa, tekrar ediyorsa, ayrı başarısız olabilmesi gerekiyorsa ya da maliyetliyse job olmaya adaydır.
Gerçek hayattan senaryolar
Teori örneklerle daha kolay oturur. Aşağıda büyük ihtimalle hepsinin bir ucundan dokunduğun yedi job var. Her birinde üç şeye bakmak yeterli: onu ne tetikliyor, ne yapıyor ve ne sıklıkla çalışıyor.
Kayıt sonrası hoş geldin e-postası
- Tetikleyici: bir olay. Yeni bir kullanıcı oluşturulur.
- Ne yapar: API kullanıcıyı kaydeder, kuyruğa “hoş geldin e-postası gönder” mesajı bırakır ve cevabı döner. Bir worker mesajı alır ve e-posta sağlayıcısıyla konuşur.
- Sıklık: her kayıtta bir kez, birkaç saniye içinde.
Olaya bağlı background job’ın klasik örneği. E-posta sağlayıcısı yavaşsa ya da o an çalışmıyorsa kullanıcı bunu hiç fark etmez.
Gece raporları ve özet tabloları
- Tetikleyici: zaman, her gece 03:00.
- Ne yapar: günün siparişlerini okur, mağaza ve ürün bazında toplamları hesaplar, sonuçları bir özet tablosuna yazar. Gün içinde dashboard’lar milyonlarca sipariş satırını taramak yerine bu tablodan okur.
- Sıklık: günde bir kez.
Önceki bölümdeki önceden hesaplama fikrinin ta kendisi. Ağır hesap gece bir kez yapılır, hızlı sonuçtan herkes faydalanır.
Süresi dolmuş verilerin temizlenmesi
- Tetikleyici: zaman, her 15 dakikada ya da saatte bir.
- Ne yapar: süresi dolmuş oturumları, bir saatten eski şifre sıfırlama token’larını ve kimsenin sahiplenmediği geçici yükleme dosyalarını siler.
- Sıklık: sık ve küçük.
Gösterişsiz ama vazgeçilmez. Bu job olmadan tablolar ve diskler sonsuza kadar büyür.
Dış bir API’den veri senkronizasyonu
- Tetikleyici: zaman, örneğin saatte bir.
- Ne yapar: döviz kurlarını, bir tedarikçiden stok bilgisini ya da bir pazaryerinden yeni siparişleri çeker ve yerel veritabanını günceller.
- Sıklık: verinin ne kadar güncel olması gerektiğine bağlı.
Bu job hataya hazırlıklı olmak zorunda. Karşı taraf yavaş olabilir, istek sınırına (rate limit) takılabilir ya da geçici olarak kapalı olabilir. Yeniden deneme mantığı en çok burada önem kazanır.
Abonelik yenileme ve ödeme alma
- Tetikleyici: zaman, genelde günde bir.
- Ne yapar: bugün yenilenecek abonelikleri bulur, kayıtlı ödeme yönteminden tahsilat yapar, aboneliği uzatır ve makbuz gönderir.
- Sıklık: günlük.
İki kez çalışmasının felaket olduğu job budur, çünkü müşteriden iki kez para çekilir. İlerleyen bölümde anlatacağım idempotency kavramının neden isteğe bağlı olmadığını gösteren en iyi örnek.
Hatırlatma ve bildirim gönderme
- Tetikleyici: zaman, çoğu zaman birkaç dakikada bir.
- Ne yapar: 24 saat sonra başlayacak randevuları ya da 2 saattir bekleyen sepetleri bulur ve henüz bildirim gönderilmemiş her biri için bildirim yollar.
- Sıklık: sık.
Buradaki kritik ifade “henüz bildirim gönderilmemiş” kısmı. Job daha önce ne yaptığını hatırlamak zorunda.
Veritabanı yedeği
- Tetikleyici: zaman, her gece.
- Ne yapar: yedek alır, ayrı bir depolama alanına kopyalar ve 30 günden eski yedekleri siler.
- Sıklık: günlük, bazen arada daha küçük saatlik yedeklerle birlikte.
Bu iş çoğu zaman uygulama kodu yerine bir veritabanı job’ı ya da işletim sistemi seviyesinde bir cron job olarak kurulur.
Job’lar nasıl tetiklenir?
Her job’ı başlatacak bir şey gerekir. Üç tür tetikleyici var ve gerçek sistemlerin çoğu üçünü birden kullanır.
Zamana bağlı tetikleyiciler
Job saate göre çalışır. Bunun birbirine benzeyen ama farklı davranan iki türü var.
Sabit aralık, “her N dakikada bir çalış” demektir. Her 15 dakikada bir çalışan temizlik job’ı buna örnek. Günün hangi saati olduğuyla ilgilenmez, sadece son çalışmadan bu yana ne kadar zaman geçtiğine bakar.
Günün belirli bir saati (ya da takvim) ise “03:00’te çalış” veya “her ayın 1’inde 09:00’da çalış” demektir. Gece raporu buna örnek. Burada tam an önemlidir, saat dilimi de öyle. İstanbul’daki 03:00 ile UTC’ye ayarlı bir sunucudaki 03:00 arasında üç saat fark var.
Olaya bağlı tetikleyiciler
Job bir şey olduğu için çalışır. Genelde uygulama bir kuyruğa mesaj yazar, o kuyruğu dinleyen bir worker mesajı alır. Kayıt sonrası e-posta buna örnek. Diğer yaygın kaynaklar arasında bir depolama alanına yeni dosya düşmesi, bir ödeme sağlayıcısından gelen webhook ya da bir tabloya yeni satır eklenmesi sayılabilir.
Elle tetikleme
Bazen job’ı bir insan bilerek başlatır. Bir yönetici panelde “Arama index’ini yeniden oluştur” butonuna basar ya da bir geliştirici dün gece başarısız olan aktarımı yeniden çalıştırır. İyi kurulmuş bir job, takvimiyle başladığı kadar kolay elle de başlatılabilir. Bir şeyler ters gittiğinde bu son derece işe yarar.
Cron ifadesi nasıl okunur?
Zamanlayıcıların çoğu zamana bağlı tetikleyicileri bir cron ifadesiyle tanımlar. Cron ifadesi, aralarında boşluk bulunan beş alandan oluşur.
┌───────────── dakika (0 ile 59 arası) │ ┌─────────── saat (0 ile 23 arası) │ │ ┌───────── ayın günü (1 ile 31 arası) │ │ │ ┌─────── ay (1 ile 12 arası) │ │ │ │ ┌───── haftanın günü (0 ile 6 arası, pazar = 0) │ │ │ │ │ 0 3 * * *
Yıldız “her” anlamına gelir. Yani 0 3 * * * şöyle okunur: 0. dakikada, 3. saatte, ayın her gününde, her ayda, haftanın her gününde. Kısacası her gece 03:00’te.
| İfade | Anlamı |
|---|---|
*/15 * * * * | Her 15 dakikada bir |
0 * * * * | Her saat başı |
0 3 * * * | Her gün 03:00’te |
30 8 * * 1 | Her pazartesi 08:30’da |
0 9 1 * * | Her ayın 1’inde 09:00’da |
0 0 * * 1-5 | Pazartesiden cumaya her gece yarısı |
Bazı araçlar saniye için altıncı, yıl için yedinci bir alan ekler. Bunun en bilinen örneği Quartz.NET. Kullandığın aracın hangi formatı beklediğine mutlaka bakmak gerekir. crontab.guru gibi siteler herhangi bir ifadeyi anlaşılır bir cümleye çevirir, yayına almadan önce ifadeyi orada kontrol etmek iyi bir alışkanlıktır.
Job’lar nerede çalışır?
Job sadece koddur. Asıl soru, o kodu kimin uyandıracağı ve kimin canlı tutacağı. Job’lar için beş yaygın yer var, her biri farklı ödünleşimlerle gelir.
İşletim sistemi
İşletim sistemi programları kendi başına belirli bir takvime göre çalıştırabilir. Linux’ta bunun adı cron, Windows’ta Görev Zamanlayıcı. Küçük bir program ya da betik yazılır, işletim sistemi de onu zamanı gelince çalıştırır. İki çalışma arasında bellekte hiçbir şey kalmaz.
İşletim sistemi bir programı arka planda kalıcı olarak da çalıştırabilir: Windows Service ya da Linux’ta systemd servisi. Bunlar makine açılınca başlar, belirli bir kullanıcı hesabıyla çalışır ve çökerlerse yeniden başlatılır. Kendi iç zamanlayıcılarıyla birden fazla job yürüten bir worker için doğal bir yuvadır.
Veritabanı
Daha önce gördüğümüz gibi veritabanlarının kendi zamanlayıcıları var: SQL Server Agent, Oracle’ın DBMS_SCHEDULER‘ı, PostgreSQL’in pg_cron‘u. Job tamamen veri üzerine kuruluysa, örneğin tablolar arasında satır taşımak ya da bakım yapmaksa, ideal seçenektir çünkü veri veritabanından hiç çıkmaz. Dezavantajı, mantığın kod deposu yerine veritabanında yaşaması ve değiştirmek için çoğu zaman bir DBA’ya ihtiyaç duyulması.
Uygulama içindeki kütüphaneler
.NET için Hangfire ve Quartz.NET, Ruby için Sidekiq, Python için Celery ya da Node.js için BullMQ gibi kütüphaneler job’ları kendi uygulamanın içinde ya da ona eşlik eden bir süreçte çalıştırır. Job’ları bir veritabanında ya da Redis’te saklar, başarısız olanları yeniden dener ve genelde neyin çalıştığını, neyin başarısız olduğunu ve nedenini gösteren bir arayüzle gelir. Job mantığı normal kodda kalır, normal testlerden ve kod incelemesinden geçer.
Container’lar
Kubernetes üzerinde çalışıyorsan bir CronJob kaynağı, cron takvimine göre bir container başlatır, işini yaptırır ve kapatır. İşletim sistemi cron’unun container dünyasındaki karşılığı. Üstüne her çalışma birbirinden izole olur ve log’lar merkezi olarak toplanır.
Bulut
Bulut sağlayıcıları yönetilen zamanlayıcılar sunar: timer trigger ile Azure Functions, Lambda ile birlikte AWS EventBridge Scheduler, Google Cloud Scheduler. Bir fonksiyon yazılır, ona bir takvim ya da kuyruk bağlanır, sunucuyu, ölçeklemeyi ve yeniden başlatmayı sağlayıcı halleder. Çalışma başına ödeme yapılır, karşılığında ortam üzerindeki kontrolün bir kısmından vazgeçilir.
Seçeneklerin karşılaştırması
| Yer | En uygun olduğu iş | Güçlü yanı | Dikkat edilmesi gereken |
|---|---|---|---|
| İşletim sistemi zamanlayıcısı (cron, Görev Zamanlayıcı) | Basit betikler ve küçük programlar | Her yerde hazır, kurulum gerektirmez | Görünürlük az, log ve uyarılar sana kalmış |
| İşletim sistemi servisi (Windows Service, systemd) | Birden fazla job yürüten uzun ömürlü worker’lar | Tam kontrol, açılışta başlar | Zamanlama, yeniden deneme ve izlemeyi kendin kurarsın |
| Veritabanı zamanlayıcısı | Saf veri işleri, bakım, yedekleme | Hızlı, veri veritabanından çıkmaz | Mantık kod deposunun dışında, veritabanı yetkisi gerekir |
| Uygulama kütüphanesi (Hangfire, Quartz.NET) | Mevcut bir uygulamadaki background job’lar | Yeniden deneme, arayüz, mantık kodda | Depolama gerektirir, bir bağımlılık daha ekler |
| Container (Kubernetes CronJob) | Zaten Kubernetes kullanan ekipler | İzole çalışmalar, merkezi log | Cluster yönetiminin karmaşıklığı |
| Bulut (Functions, Lambda) | Olaya bağlı ve kullandıkça öde işler | Yönetilecek sunucu yok, kendiliğinden ölçeklenir | Çalışma süresi sınırları, sağlayıcıya bağımlılık |
Tek bir doğru cevap yok. Pratikte sık görülen kurulum, uygulama job’ları için bir kütüphane ve yedekleme ile bakım için veritabanı zamanlayıcısının birlikte kullanılması.
Job dünyasının terimleri
İlk job kurulurken dokümantasyonda, ayar dosyalarında ve arayüzlerde birkaç terimle sürekli karşılaşılır. Her birinin hangi sorunu çözdüğü görüldüğünde hiçbiri karmaşık değil.
Scheduler (zamanlayıcı). Her job’ın ne zaman çalışması gerektiğini bilen ve o an geldiğinde onu başlatan bileşen. Cron bir zamanlayıcıdır, Hangfire’ın tekrarlayan işleri yöneten kısmı da öyle. Üzerine görev listesi iliştirilmiş bir çalar saat gibi düşünülebilir.
Trigger (tetikleyici). Belirli bir job’ın ne zaman çalışacağını söyleyen kural: bir cron ifadesi, bir aralık, bir olay ya da elle yapılan bir tıklama. Bir job’ın birden fazla tetikleyicisi olabilir.
Worker. Job’ı fiilen çalıştıran süreç ya da thread. Bir sistemde tek worker da olabilir elli worker da. Worker sayısı arttıkça aynı anda çalışabilecek job sayısı da artar.
Queue (kuyruk). Job’lar için bir bekleme sırası. Uygulama kuyruğa mesaj bırakır, worker’lar mesajları sırayla alır. Worker’lar meşgulse mesajlar sadece bekler. Bir anda 10.000 kişi kayıt olursa kuyruk bu yükü emer, worker’lar da kendi hızlarında yetişir. RabbitMQ, Azure Service Bus, Amazon SQS ve Redis yaygın kuyruk teknolojileri.
Interval (aralık). İki çalışma arasındaki süre. “Her 15 dakikada bir” demek 15 dakikalık bir aralık demek. Küçük ama önemli bir soru, aralığın önceki çalışmanın başından mı yoksa bitişinden mi sayıldığı. 20 dakika süren bir job her 15 dakikada bir çalışacak şekilde ayarlanmışsa, bu iki seçim çok farklı sonuçlar doğurur.
Timeout (zaman aşımı). Bir job’ın ya da içindeki bir adımın durdurulmadan önce sürebileceği en uzun süre. Timeout yoksa ölü bir ağ bağlantısını bekleyen bir job sonsuza kadar asılı kalabilir ve arkasından gelen tüm çalışmaları tıkayabilir.
Retry ve backoff (yeniden deneme ve bekleme). Retry, başarısız olan bir job’ı yeniden çalıştırmak demek. Backoff ise her denemeden önce biraz daha uzun beklemek: önce 10 saniye, sonra 30, sonra 2 dakika. Böylece zorlanan bir servise, çökmüşken üstüne yüklenmek yerine toparlanması için zaman tanınır.
Concurrency (eş zamanlılık, üst üste binme). Aynı job’ın iki çalışmasının aynı anda gerçekleşmesi. Genelde bir çalışma aralıktan uzun sürdüğünde ya da uygulama birden fazla sunucuda çalıştığında ortaya çıkar. Bazı job’lar bunu tolere eder, pek çoğu etmez. Müşteriden iki kez ödeme alacak olan abonelik yenileme job’ı gibi.
Idempotency. Bir job’ı iki kez çalıştırmak bir kez çalıştırmakla aynı sonucu veriyorsa o job idempotent’tir. “Siparişin durumunu Kargolandı yap” idempotent bir iştir. “Kullanıcının bakiyesine 10 puan ekle” değildir. Idempotency bir job’ın sahip olabileceği en değerli özellik, çünkü yeniden denemeler, yeniden başlatmalar ve üst üste binmeler zararsız hale gelir.
Transaction ve atomiklik. Transaction, birden fazla veritabanı işlemini tek bir paket haline getirir: ya hepsi başarılı olur ya hiçbiri. Bu ya hep ya hiç özelliğine atomiklik (atomicity) denir. Bir job yarısında hata verdiğinde geride yarım kalmış iş bırakmasını engelleyen şey budur.
Dead letter queue. Çok fazla kez başarısız olan mesajların gönderildiği ayrı bir kuyruk. Sistem bu mesajları sonsuza kadar denemek ya da sessizce silmek yerine bir kenara park eder, böylece bir insan neyin ters gittiğine bakabilir.
Distributed lock (dağıtık kilit). Birden fazla sunucunun görebildiği bir kilit. Belirli bir job’ı aynı anda sadece bir sunucunun çalıştırdığından emin olmak için kullanılır. Bir veritabanında, Redis’te ya da job kütüphanesinin kendisinde tutulabilir.
Adım adım ilk job’ı kurmak
Aynı fikri dört kez, her adımda gerçek kullanıma biraz daha yaklaşarak kuruyoruz. Örnekler C# ve .NET ile yazıldı ama yapı her teknolojide aynı.
1. Adım: timer ile en sade job
Mümkün olan en küçük job, bir fonksiyon ve bir timer’dan ibaret. Yeni bir console uygulaması açılıp şu kod yapıştırılır:
using System;
using System.Threading;
class Program
{
static Timer _timer;
static void Main()
{
_timer = new Timer(_ =>
{
try
{
Console.WriteLine($"Job çalıştı: {DateTime.Now:HH:mm:ss}");
}
catch (Exception ex)
{
Console.WriteLine($"Job hata verdi: {ex.Message}");
}
finally
{
// Bir sonraki çalışma ancak bu çalışma bittikten sonra kurulur
_timer.Change(TimeSpan.FromSeconds(5), Timeout.InfiniteTimeSpan);
}
}, null, Timeout.Infinite, Timeout.Infinite);
_timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); // ilk çalışma hemen
Console.WriteLine("Durdurmak için Enter'a bas.");
Console.ReadLine();
}
}
Buradaki üç detayı anlamak önemli, çünkü hepsi her job sisteminde karşına çıkacak.
Timer tek seferlik kuruluyor. Bir kez tetikleniyor ve bir sonraki çalışma, iş bittikten sonra finally bloğunda kuruluyor. İş 5 saniyeden uzun sürerse bir sonraki çalışma da o kadar geç başlıyor. İki çalışma asla üst üste binmiyor. try bloğunun içine Thread.Sleep(8000); eklendiğinde çalışmalar arasındaki sürenin 13 saniyeye çıktığı görülür.
İş try/catch ile sarılı. Bu olmasaydı tek bir exception timer’ı öldürür ve job bir daha hiç çalışmazdı. throw new Exception("patladı"); eklendiğinde job’ın hatayı yazdığı ve 5 saniye sonra yeniden çalıştığı görülür.
Yeniden kurma işlemi finally içinde, yani çalışma başarılı da olsa başarısız da olsa gerçekleşiyor.
2. Adım: gerçek bir background service
Console.ReadLine() ile bekleyen bir console uygulaması öğrenmek için yeterli ama gerçek bir uygulamada, uygulamayla birlikte başlayan, düzgün kapanan ve doğru dürüst log yazan bir yapı gerekir. .NET’te bunun karşılığı BackgroundService. dotnet new worker komutuyla hazır bir proje şablonu oluşturulur ve şu sınıf yazılır:
public class SessionCleanupWorker : BackgroundService
{
private readonly ILogger<SessionCleanupWorker> _logger;
public SessionCleanupWorker(ILogger<SessionCleanupWorker> logger) => _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(15));
do
{
try
{
await DeleteExpiredSessionsAsync(stoppingToken);
_logger.LogInformation("Oturum temizliği tamamlandı");
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Oturum temizliği başarısız oldu");
}
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
private Task DeleteExpiredSessionsAsync(CancellationToken ct)
{
// DELETE FROM Sessions WHERE ExpiresAt < şu an
return Task.CompletedTask;
}
}
Ardından Program.cs içinde kaydedilir:
var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService<SessionCleanupWorker>(); builder.Build().Run();
İlk adımdaki kurallar burada da geçerli, sadece hazır olarak geliyorlar. PeriodicTimer bir sonraki tetiklemeyi beklemeden önce işin bitmesini bekler, dolayısıyla çalışmalar üst üste binmez. CancellationToken ise uygulama kapanırken koda haber verir. Böylece uzun süren bir job yarıda öldürülmek yerine düzgün şekilde durabilir.
Bunu Windows Service olarak çalıştırmak için Microsoft.Extensions.Hosting.WindowsServices paketi eklenir ve builder.Services.AddWindowsService() çağrılır. Linux’ta Microsoft.Extensions.Hosting.Systemd paketi ve AddSystemd() aynı işi systemd için yapar. Job kodunda hiçbir değişiklik gerekmez.
3. Adım: günün belirli bir saatinde çalıştırmak
Aralık kolay. “Her gece 03:00” için küçük bir hesap daha gerekiyor: bir sonraki 03:00’e ne kadar kaldı?
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeUntilNext(hour: 3), stoppingToken);
try
{
await BuildNightlyReportAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Gece raporu başarısız oldu");
}
}
}
private static TimeSpan TimeUntilNext(int hour)
{
var now = DateTime.Now;
var next = now.Date.AddHours(hour); // bugün o saat
if (next <= now) next = next.AddDays(1); // geçtiyse yarın
return next - now;
}
Servis 14:00’te başlarsa job 13 saat bekler, 02:30’da başlarsa 30 dakika. Dikkat edilirse bekleme işten önce geliyor. İş önce gelseydi, gün içinde uygulama her yeniden başlatıldığında ağır gece job’ı öğlen saatinde çalışırdı. Gerçek projelerde en sık rastlanan job hatalarından biri bu.
Gerçek bir uygulamada saat dilimleri ve yaz saati geçişleri de düşünülmeli. Saatlerin ileri ya da geri alındığı gece 03:00 iki kez yaşanabilir ya da hiç yaşanmayabilir. Sunucuları UTC’de çalıştırıp dönüşümü bilinçli olarak yapmak bu sürprizlerin çoğunu önler.
4. Adım: aynı job’lar bir kütüphane ve cron ile
Arka planda ne olduğu anlaşıldıktan sonra bir kütüphane ciddi miktarda kod tasarrufu sağlar. Hangfire ile gece raporu ve hoş geldin e-postası birer satıra iner:
// Tekrarlayan, zamana bağlı job
RecurringJob.AddOrUpdate<ReportService>(
"nightly-report", s => s.BuildNightlyReport(), "0 3 * * *");
// Tek seferlik, olaya bağlı job (örneğin kayıttan hemen sonra)
BackgroundJob.Enqueue<EmailService>(s => s.SendWelcomeEmail(userId));
Hangfire bunları bir veritabanında saklar, başarısız job’ları otomatik olarak yeniden dener ve her çalışmayı gösteren bir arayüz sunar.
Job bağımsız bir programsa Linux’ta düz cron yeterlidir:
# crontab -e 0 3 * * * /usr/bin/dotnet /opt/reports/NightlyReport.dll >> /var/log/nightly-report.log 2>&1
Birbirinden çok farklı dört araç, birebir aynı fikir: bir fonksiyon ve bir tetikleyici.
Başını ağrıtmayan job tasarımı
Kendi bilgisayarında çalışan bir job yazmak işin kolay kısmı. Ağ kesintilerine, deploy’lara ve yeniden başlatmalara rağmen yıllarca çalışmaya devam eden bir job ise birkaç bilinçli tasarım kararı ister. Aşağıdaki kuralların her biri, bir yerde birinin gece 03:00’te yaşadığı bir sorundan doğmuştur.
İki kez çalışması güvenli olsun
Her job’ın er ya da geç iki kez çalışacağı varsayılmalı: bir timeout sonrası yeniden deneme, ortasında bir yeniden başlatma, aynı anda tetiklenen iki sunucu. Tasarım da iki kez çalışmanın hiçbir şeyi değiştirmeyeceği şekilde yapılmalı.
Veri job’ları için en basit kalıp sil ve yeniden yaz. “Bugünün toplamlarını özet tablosuna ekle” demek ikinci çalışmada rakamları ikiye katlar. Bunun yerine job “bugüne ait ne varsa sil, sonra bugünün toplamlarını yaz” der. Bir kez de çalışsa beş kez de, sonuç aynıdır.
Kart çekmek ya da e-posta göndermek gibi dış dünyada iz bırakan job’larda yapılan işin kaydı tutulmalı ve önce o kayda bakılmalı. “812 numaralı aboneliğin ekim ödemesini al” diyen bir job, bir şey yapmadan önce o ödemenin zaten alınıp alınmadığını kontrol etmeli. Pek çok ödeme sağlayıcısı tam da bu sebeple idempotency key kabul eder.
Ya hep ya hiç çalışsın
Bir job yarısında hata verirse, her şeyi başlamadan önceki haliyle bırakmalı. Transaction’lar bunun için var. Yukarıdaki sil ve yeniden yaz kalıbı, bir transaction içinde şöyle görünür:
BEGIN TRANSACTION; DELETE FROM dbo.DailySalesSummary WHERE SalesDate = @Day; INSERT INTO dbo.DailySalesSummary (SalesDate, StoreId, Total) SELECT @Day, StoreId, SUM(Amount) FROM dbo.Orders WHERE OrderDate >= @Day AND OrderDate < DATEADD(day, 1, @Day) GROUP BY StoreId; COMMIT;
INSERT başarısız olursa DELETE de geri alınır. Özet tablosunu okuyan biri ya dünün eksiksiz verisini ya bugünün eksiksiz verisini görür, asla boş ya da yarı dolu bir tablo görmez.
Çalışmalar üst üste binmesin
Her 10 dakikada bir çalışan bir job’ın bir çalışması 25 dakika sürerse, aynı veri üzerinde çalışan üç kopya ortaya çıkabilir. Bunu kaynağında önlemek gerekir: önceki örneklerde olduğu gibi bir sonraki çalışmayı ancak mevcut çalışma bitince kurmak ya da kütüphanenin hazır korumasını kullanmak. Örneğin Hangfire’da [DisableConcurrentExecution] attribute’u bu işi görür.
Birden fazla sunucuyu hesaba kat
Uygulama yük dengeleme için iki sunucuda çalışmaya başladığı anda, süreç içindeki her timer da iki kez çalışır, her sunucuda bir kez. Gece raporu iki kez oluşur, abonelik job’ı iki kez ödeme alır. Ya job’lar tek bir ayrılmış sunucuda çalıştırılmalı, ya ortak depolama üzerinden koordinasyon yapan bir kütüphane kullanılmalı ya da iş yapılmadan önce bir distributed lock alınmalı. SQL Server’daki sp_getapplock ve Redis üzerinde tutulan bir kilit, sonuncusu için yaygın yöntemler.
Yeniden dene ama nazikçe
Geçici hatalar normaldir: anlık bir ağ kopması, veritabanının yedeğe geçmesi, istek sınırına takılmak. Bunlar, denemeler arasındaki süre giderek uzatılarak ve deneme sayısına bir sınır konarak yeniden denenmeli:
for (int attempt = 1; attempt <= 3; attempt++)
{
try
{
await SyncExchangeRatesAsync(ct);
break; // başarılı, denemeyi bırak
}
catch (HttpRequestException) when (attempt < 3)
{
var wait = TimeSpan.FromSeconds(10 * Math.Pow(3, attempt - 1)); // 10 sn, 30 sn
await Task.Delay(wait, ct);
}
}
Sadece gerçekten geçebilecek hatalar yeniden denenmeli. Bir timeout yeniden denemeye değer. “Müşteri bulunamadı” hatası ise her seferinde aynı şekilde başarısız olur. Son denemeden sonra hata yukarı taşınmalı ki biri haberdar olsun. Kuyruk tabanlı sistemlerde dead letter queue tam bu noktada devreye girer. .NET’te Polly gibi kütüphaneler bu politikaları tek satıra indirir.
Timeout koy, iptale saygı göster
Dış dünyaya yapılan her çağrının bir timeout’u olmalı ki asılı kalan tek bir bağlantı job’ı sonsuza kadar dondurmasın. CancellationToken en alt katmana kadar taşınmalı. Böylece uygulama bir deploy için kapandığında job, bir yazma işleminin ortasında öldürülmek yerine düzgün şekilde durur.
Her an yeniden başlatılabileceğini unutma
Uygulamalar deploy’lar, çökmeler ve sunucu güncellemeleri yüzünden yeniden başlar. Job gün ortasında bir yeniden başlatmanın hemen ardından çalışırsa ne olacağı sorulmalı. Cevap “ağır gece job’ı öğlen çalışır” ise bir koruma eklenmeli: saat aralığını kontrol etmek ya da çalışma kayıtlarına bakıp bugünkü çalışmanın zaten başarılı olup olmadığını görmek.
Parça parça çalış
Bir milyon satırı tek sorguda ya da tek transaction’da işlemek tabloları dakikalarca kilitleyebilir ve belleği şişirebilir. İş bölünmeli: her seferinde bir ay, bin satır ya da tek bir müşteri. Parça parça çalışmak ilerlemeyi görünür kılar ve başarısız bir çalışmanın kaldığı yerden devam etmesini sağlar. Büyük miktarda veri yazarken satırları bir ORM üzerinden tek tek eklemek yerine SqlBulkCopy ya da INSERT INTO ... SELECT gibi toplu işlemler tercih edilmeli.
Job’ların çalıştığından emin olmak
Job’lar kimse bakmıyorken çalışır. Bütün mesele de bu, tehlike de buradan geliyor. Bozulan bir API birkaç dakika içinde fark edilir çünkü kullanıcılar şikayet eder. Bozulan bir gece job’ı ise biri geçen ayın raporunun neden boş olduğunu sorana kadar haftalarca sessizce başarısız olabilir.
Çalışma geçmişi tut
Yapılabilecek en basit ve en faydalı şey her çalışmayı kayıt altına almak. Küçük bir tablo yeterli:
CREATE TABLE dbo.JobRunLog (
Id int IDENTITY PRIMARY KEY,
JobName nvarchar(100) NOT NULL,
StartedAt datetime2 NOT NULL,
FinishedAt datetime2 NULL,
Status nvarchar(20) NOT NULL, -- Running, Succeeded, Failed
RowsAffected int NULL,
ErrorMessage nvarchar(max) NULL
);
Job başlarken bir satır yazılır, bitince güncellenir. Artık “rapor dün gece çalıştı mı?”, “genelde ne kadar sürüyor?” ve “giderek yavaşlıyor mu?” gibi sorular cevaplanabilir. Aynı tablo, önceki bölümdeki yeniden başlatma sürprizlerine karşı koruma olarak da kullanılabilir: ağır işe başlamadan önce bugün için başarılı bir çalışma olup olmadığına bakılır. Hangfire ve Quartz.NET gibi kütüphaneler bu geçmişi senin yerine tutar ve bir arayüzde gösterir.
Hata olduğunda haber ver
Her hata bir insana ulaşmalı: bir e-posta, bir Slack ya da Teams mesajı, hata takip aracında bir kayıt. Sadece kimsenin okumadığı bir log dosyasına yazan bir catch bloğu, hiç catch bloğu olmamasıyla aynı şey.
Sessizlik olduğunda da haber ver
Bu kolayca gözden kaçar. Çöken bir job gürültü çıkarır. Servis durdurulduğu, sunucu değiştirildiği ya da biri takvimi yorum satırına aldığı için hiç başlamayan bir job ise tamamen sessizdir. Hiçbir şey başarısız olmaz, dolayısıyla hiçbir uyarı da gelmez.
Çözümü heartbeat, diğer adıyla dead man’s switch. Her başarılı çalışmanın sonunda job bir izleme servisine sinyal gönderir. Sinyal zamanında gelmezse servis uyarı verir. Healthchecks.io ve Cronitor gibi araçlar tam olarak bunun için yapılmış, çoğu izleme platformu da benzer bir özellik sunuyor.
Verinin tazeliğini kullanıcıya göster
Kullanıcılar bir job’ın ürettiği veriyi görüyorsa, verinin ne kadar güncel olduğu da gösterilmeli. Bir raporun altındaki küçük bir “Son güncelleme: bugün 03:12” satırı doğru beklenti oluşturur ve sessiz bir hatayı görünür hale getirir. O satır bir gün dünün tarihini gösterirse biri mutlaka fark eder.
Sık yapılan hatalar
Job sorunlarının çoğu aynı kısa listeden çıkar. Bunlardan kaçınmak, canlıdaki pek çok sistemin önüne geçmek için yeterli.
- Arka plan işini bir web isteğinin içinden başlatmak. Bir controller’dan
Task.Run(...)ile işi ateşleyip cevap dönmek background job gibi görünür ama değildir. Uygulama yeniden başlarsa ya da sunucu süreci geri dönüştürürse iş iz bırakmadan kaybolur. Gerçek bir kuyruk, hosted service ya da job kütüphanesi kullanılmalı. - Exception’ları yutmak. Boş bir
catchjob’ı hayatta tutar ama bütün sorunları gizler. Hata log’lanmalı ve bir insana ulaştığından emin olunmalı. - Tek bir exception’ın zamanlayıcıyı öldürmesine izin vermek. Tam tersi hata: hiç
catchyoktur, ilk hata timer’ı durdurur ve job bir daha çalışmaz. - Her şeyi tek dev sorguda yapmak. Tabloları kilitler, zaman aşımına düşer ve kaldığı yerden devam edemez. İş parçalara bölünmeli.
- Saat dilimlerini unutmak. Sunucu UTC’de, iş İstanbul saatiyle yürüyor ve “gece yarısı” job’ı yerel saatle 03:00’te çalışıyor. Her job’ın hangi saate göre çalıştığına karar verilmeli ve bu bir yere yazılmalı.
- Ağır işi uygulama açılışında çalıştırmak. Uygulama her başladığında hemen çalışan bir job, her deploy’dan sonra gün ortasında çalışır.
- Tek sunucu olduğunu varsaymak. Uygulama iki sunucuya çıktığı anda süreç içindeki her job iki kez çalışır.
- Job’ı idempotent yapmamak. Yeniden denemeler ve yeniden başlatmalar mükerrer e-postalara, çift ödemelere ve şişmiş toplamlara dönüşür.
- Çalışma geçmişi tutmamak. Biri “çalıştı mı?” diye sorduğunda verilebilecek tek cevap tahmin olur.
Yayına almadan önce kontrol listesi
Yeni bir job canlıya çıkmadan önce şu yedi soru cevaplanmalı. Her birinin net bir cevabı varsa job sağlam kurulmuş demektir.
- Onu ne tetikliyor ve tam olarak ne zaman? Aralık mı günün belirli bir saati mi, hangi saat dilimi, yaz saati geçişlerinde ne oluyor?
- Bir çalışma beklenenden uzun sürerse ne olur? İki çalışma üst üste binebilir mi, binerse bu güvenli mi?
- İki kez çalışırsa ne olur? Job idempotent mi, yoksa e-postaları, ödemeleri ya da veriyi çoğaltır mı?
- Yarısında hata verirse ne olur? İş bir transaction içinde mi, yoksa geride yarım kalmış veri bırakabilir mi?
- Hata verdiğinde kim haberdar olur? Sadece bir log satırı değil, bir insana ulaşan bir uyarı var mı?
- Yeniden başlatmadan sonra ya da ikinci bir sunucuda ne olur? Yanlış saatte mi çalışır, paralel olarak iki kez mi?
- Çalıştığını nereden bilirim? Bir çalışma geçmişi, bir heartbeat ya da bir yerde “son güncelleme” bilgisi var mı?
Kapanış
Job basit bir fikir: bir fonksiyon ve bir tetikleyici. Cron job, zamanlanmış görev, background job ve veritabanı job’ı aynı fikrin yazılımın farklı katmanlarında yaşayan versiyonları. Bir job’ın nerede çalışacağına karar vermek, çoğu zaman işin ve ekibin zaten nerede olduğuna bakmaktan ibaret.
Zor olan bir job’ı çalıştırmak değil, onu yıllarca güvenle çalışır halde tutmak. Idempotent olması, transaction ile korunması, çalışmalarının üst üste binmemesi, hataları makul şekilde yeniden denemesi ve yaptığı her şeyin kaydını tutması bunun temeli. Bunlar yapıldığında job’lar, sistemin kimsenin düşünmesine gerek kalmayan sessiz ve güvenilir parçası olur.
Tüm projelerim ve üzerinde çalıştığım konuları incelemek istersen https://hub.barisgunduz.com/ adresini ziyaret edebilirsin.