Kimlik Modülüyle Deterministik Zamanlama

Kimlik Modülüyle Deterministik Zamanlama

Kimlik değerinin modüler dönüşümüyle günlük scheduler yükünü 24 saate deterministik biçimde dağıtan yöntemi açıklar. Dağılım dengesi, çakışma, yeniden üretilebilirlik ve zaman dilimi davranışı ele alınır.

Periyodik çalışan bir görev, her yürütmede bütün kayıtları taramak yerine her kaydı günün belirli bir saatine deterministik olarak atayabilir:

bucket(id) = id mod 24

Görev yalnızca mevcut saate karşılık gelen kayıtları işler:

process(id, hour) ⇔ id mod 24 = hour

Bu yöntem, yaklaşık 28 bin kaydın tamamını her scheduler tetiklemesinde değerlendirmek yerine günlük iş yükünü 24 zaman dilimine böler. Merkezi kuyruk, dağıtık kilit, kalıcı cursor veya dinamik zamanlayıcı gerektirmez. Bir kaydın hangi saatte işleneceği yalnızca değişmeyen kimliği ve saat bilgisi üzerinden yeniden hesaplanabilir.

Yaklaşımın sadeliği yanıltıcı olabilir. Mod işlemi yalnızca sayıları sınıflara ayırır; yükün gerçekten dengeli olması, kimliklerin dağılımına ve her kaydın işlem maliyetine bağlıdır. Ayrıca scheduler'ın saat içinde kaç kez çalıştığı, hata sonrasında yeniden denemenin nasıl gerçekleştiği ve bir kaydın aynı gün içinde birden fazla kez işlenmesinin güvenli olup olmadığı açıkça tanımlanmalıdır.

Doğru kurulduğunda bu model, küçük ve orta ölçekli periyodik işlerde son derece düşük operasyonel maliyetle kararlı yük dağılımı sağlar.

Mod işlemiyle zaman bölümü oluşturmak

Kayıt kimliklerinin pozitif tamsayı olduğu varsayılsın. Gün 24 saatlik bölümlere ayrılır:

H = {0, 1, 2, ..., 23}

Her kayıt için saat bölümü:

hᵢ = idᵢ mod 24

olarak belirlenir. "hᵢ", her zaman "0" ile "23" arasındadır.

Saat "15" olduğunda yalnızca:

id mod 24 = 15

koşulunu sağlayan kayıtlar seçilir. Aynı kayıt ertesi gün yine saat 15 bölümüne düşer. Atama değişmez; başka bir durum tablosunda saklanmasına gerek yoktur.

C# tarafında temel koşul şu kadar sadedir:

if (userId % 24 == DateTime.Now.Hour)
{
    Process(userId);
}

SQL tarafında ön filtreleme yapılacaksa eşdeğer koşul:

WHERE USERID % 24 = @Hour

şeklindedir.

Bu yapı bir tür deterministik zamansal parçalamadır. Geleneksel sharding yaklaşımında kayıtlar farklı veritabanı veya düğümlere dağıtılır. Burada aynı ilke fiziksel düğüm yerine zaman dilimine uygulanır:

mekânsal parçalama: kayıt → sunucu zamansal parçalama: kayıt → saat

Amaç paralel throughput artırmak değil, periyodik iş yükünü zaman ekseninde yaymaktır.

Toplam kayıt sayısı "N" ve bölüm sayısı "B = 24" ise ideal bölüm büyüklüğü:

μ = N / B

olur.

"N = 28.742" için:

μ = 28.742 / 24 ≈ 1.197,58

Dolayısıyla her saat yaklaşık 1.198 kaydın işlenmesi beklenir.

Gerçek dağılımın tam olarak eşit olması gerekmez. Kayıt sayısı 24'e tam bölünmeyebilir ve kimlik kümesinde silinmiş, atlanmış veya belirli örüntülerle üretilmiş değerler bulunabilir. Önemli olan farkların operasyonel kapasiteyi bozacak kadar büyümemesidir.

Gerçek dağılımın değerlendirilmesi

28.742 kayıt için elde edilen saatlik bölüm sayıları şöyledir:

0 → 1230 1 → 1219 2 → 1230 3 → 1261 4 → 1186 5 → 1190 6 → 1180 7 → 1165 8 → 1148 9 → 1196 10 → 1205 11 → 1177 12 → 1179 13 → 1229 14 → 1207 15 → 1198 16 → 1241 17 → 1148 18 → 1182 19 → 1204 20 → 1177 21 → 1213 22 → 1140 23 → 1237

En büyük bölüm:

max = 1261

en küçük bölüm:

min = 1140

ve mutlak fark:

Δ = 1261 - 1140 = 121

kayıttır.

İdeal ortalamaya göre en yüksek bölümün sapması:

(1261 - 1197,58) / 1197,58 ≈ %5,29

en düşük bölümün sapması ise:

(1140 - 1197,58) / 1197,58 ≈ -%4,81

düzeyindedir.

Bu dağılım, kayıt başına işlem maliyetlerinin birbirine yakın olduğu varsayımında dengelidir. En yoğun saat ile en hafif saat arasındaki fark yaklaşık yüzde 10,6'dır:

1261 / 1140 ≈ 1,106

Başka bir ifadeyle en yoğun bölüm, en hafif bölümden yaklaşık yüzde 10,6 daha fazla kayıt taşır. Çoğu periyodik veri işleme görevi için bu fark, dinamik kuyruk yönetimi kurmanın operasyonel maliyetinden çok daha düşüktür.

Ancak yalnızca kayıt sayılarına bakmak yeterli değildir. Gerçek yük:

Lₕ = Σ cost(i)
     i mod 24 = h

şeklindedir.

Burada "cost(i)", ilgili kaydın işlem süresini, sorgu sayısını, ağ trafiğini veya ürettiği veri miktarını temsil eder. Her kullanıcı benzer maliyete sahipse:

cost(i) ≈ c

olur ve bölüm yükü yaklaşık olarak kayıt sayısıyla orantılıdır:

Lₕ ≈ c × nₕ

Bazı kayıtların maliyeti diğerlerinden yüzlerce kat yüksekse dengeli kayıt sayısı, dengeli iş yükü anlamına gelmez. Örneğin büyük bir birim tek başına binlerce alt kayıt işlerken küçük bir birim birkaç sorguyla tamamlanıyorsa yalnızca kimlik modülü yetersiz kalabilir.

Bu nedenle ilk doğrulama kayıt sayısıyla, ikinci doğrulama gerçek çalışma süresiyle yapılmalıdır:

bucketCount[h]
bucketDuration[h]
bucketRows[h]
bucketBytes[h]

Kritik ölçüt, bölüm başına ortalama yerine p95 ve maksimum yürütme süresidir. Bir saat bölümü scheduler periyodunu aşacak kadar uzun sürüyorsa sayısal dağılım dengeli görünse bile zamanlama güvenli değildir.

Neden kimlik üzerinden dağıtım çoğu zaman dengelidir?

Ardışık üretilen kimliklerde modulo dağılımı doğal olarak dengelenir. Kimlikler:

1, 2, 3, ..., N

biçiminde kesintisiz ilerliyorsa her 24 ardışık kayıt, 24 bölümün her birine tam olarak bir kayıt verir.

Bu durumda bölüm sayıları arasındaki fark en fazla bir olur:

|max(nₐ - nᵦ)| ≤ 1

Gerçek veritabanlarında silinen kayıtlar, kullanılmayan kimlikler, farklı kaynaklardan taşınan veriler veya başlangıç değerleri nedeniyle bu mükemmel denge bozulabilir. Yine de silinmeler belirli modulo sınıflarını sistematik olarak hedeflemiyorsa dağılım genellikle dengeli kalır.

Örneğin yalnızca çift kimliklerin aktif olduğu bir veri kümesinde:

id mod 24

yalnızca çift bölümleri doldurur. Tek saatler boş kalır. Benzer biçimde kimlikler 24'ün katlarıyla üretiliyorsa bütün kayıtlar sıfırıncı bölüme düşer.

Dolayısıyla modulo yönteminin güvenilirliği şu varsayıma bağlıdır:

kimliklerin düşük bitleri veya kalan sınıfları, iş yüküyle sistematik korelasyon taşımamalıdır

Kimlik üretim düzeni bu koşulu karşılamıyorsa doğrudan kimlik yerine sabit bir hash kullanılabilir:

bucket(id) = floorMod(hash(id), 24)

Hash, ardışık veya örüntülü kimlikleri daha homojen kalan sınıflarına dağıtabilir. Ancak ek karmaşıklık getirir ve kullanılan hash fonksiyonunun bütün sürümlerde değişmeden korunması gerekir. Hash değişirse kayıtların çalışma saatleri de topluca değişir.

Mevcut dağılım zaten yüzde 5 civarında sapma gösteriyorsa hash kullanmak zorunlu değildir. Ölçülmüş ve yeterli bulunan basit modulo dağılımı, teorik olarak daha düzgün fakat daha karmaşık bir fonksiyondan genellikle daha değerlidir.

Saat içinde iki kez çalıştırmanın etkisi

Scheduler'ın her saat başında ve buçukta çalıştığı bir yapı düşünelim. Başlangıç gecikmesi nedeniyle gerçek yürütmeler yaklaşık olarak:

15:01 15:31

zamanlarında gerçekleşebilir. Her iki çalıştırmada da:

DateTime.Now.Hour = 15

olduğu için aynı bölüm seçilir.

Bu davranış, aynı saat bölümü için doğal bir ikinci deneme fırsatı oluşturur. İlk çalıştırmada geçici veritabanı, ağ veya kaynak hatası yaşayan kayıtlar ikinci çalıştırmada yeniden ele alınabilir.

Ancak bu avantaj yalnızca işlem idempotent ise güvenlidir. Aynı kaydın aynı gün içinde iki kez işlenmesi:

  • aynı verinin iki kez eklenmesine,
  • aynı bildirimin iki kez gönderilmesine,
  • aynı dosyanın iki farklı içerikle üzerine yazılmasına,
  • sayaçların iki kez artırılmasına

neden olabiliyorsa yarım saatlik ikinci tetikleme hata üretir.

Güvenli işlem modeli şu özelliklerden en az birine sahip olmalıdır:

aynı hedef anahtara idempotent upsert mevcut çıktı varsa atlama işlem tarihi + kayıt kimliği için unique constraint deterministik dosya yolu başarı durumunun kalıcı kaydı

Örneğin her kullanıcı için günlük çıktı üretiliyorsa hedef:

/yyyy/MM/dd/{userId}.json

biçiminde deterministik olabilir. İlk çalıştırmada dosya atomik olarak yayımlanmışsa ikinci çalıştırma dosyanın varlığını görerek atlar. İlk çalıştırma yarıda kesilmişse hedef dosya oluşmadığından yeniden üretim yapılır.

Veritabanı yazımında:

(user_id, process_date)

üzerinde benzersiz anahtar tanımlanması, ikinci denemenin mükerrer kayıt oluşturmasını önleyebilir. Ancak "INSERT" hatasını yakalayıp yok saymak yerine işlemin semantiğine uygun "MERGE", kontrollü "UPDATE" veya açık varlık kontrolü kullanılmalıdır.

İki tetikleme şu şekilde yorumlanmalıdır:

ilk çalışma → birincil deneme ikinci çalışma → aynı bölüm için telafi denemesi

İkinci çalışmanın zorunlu olarak bütün başarılı kayıtları tekrar işlemesi gerekmez. Kalıcı başarı bilgisi bulunan kayıtlar atlanabilir; yalnızca eksik veya başarısız olanlar yeniden denenebilir.

Saat sınırındaki yarış koşulu

Saat bilgisinin işlem boyunca tekrar tekrar okunması, saat geçişinde farklı kayıtların farklı bölümlere göre değerlendirilmesine yol açabilir:

foreach (final User user in users)
{
    if (user.Id % 24 == DateTime.Now.Hour)
    {
        Process(user);
    }
}

Görev "14:59:59" anında başlar ve döngü sırasında saat "15:00" olursa listenin ilk kısmı 14, kalan kısmı 15 bölümüne göre değerlendirilebilir. Bu davranış aynı yürütmenin deterministik bölüm sözleşmesini bozar.

Saat başlangıçta tek kez sabitlenmelidir:

final int hour = DateTime.Now.Hour;

foreach (final User user in users)
{
    if (user.Id % 24 == hour)
    {
        Process(user);
    }
}

C# dilinde yerel değişken için "final" bulunmadığından gerçek kodda "int hour" kullanılır; temel ilke değerin döngü boyunca yeniden okunmamasıdır.

Daha güvenli yaklaşım scheduler'ın planladığı çalışma zamanını kullanmaktır. Görev 15'inci saat bölümü için planlandıysa gerçek başlangıç gecikerek "16:00" sonrasına taşsa bile hangi bölümün hedeflendiği açıkça bilinmelidir.

Yalnızca duvar saati okunursa uzun gecikmelerde yanlış bölüm işlenebilir:

planlanan bölüm: 15 gerçek başlangıç: 16:02 DateTime.Now.Hour: 16

Bu risk kısa ve düzenli çalışan yerel scheduler görevlerinde düşük olabilir. Fakat sistem yoğunluk nedeniyle bir saatten fazla gecikebiliyorsa çalışma bölümü tetikleyici tarafından parametre olarak iletilmelidir:

ProcessBucket(15)

Böylece iş mantığı işletim sistemi saatinden ayrılır.

Saat dilimi de açıkça tanımlanmalıdır. Yerel saat kullanan sistemlerde yaz saati değişiklikleri bazı bölgelerde bir saatin iki kez yaşanmasına veya hiç yaşanmamasına neden olabilir. Türkiye'de sabit UTC+03:00 kullanılması bu özel dalgalanmayı ortadan kaldırır; yine de uygulama düzeyinde açık bir "TimeZoneInfo" veya zaman politikası tanımlamak daha güvenlidir.

Günlük garanti ve kaçırılan saatler

Modulo zamanlama, görev gün boyunca düzenli çalıştığı sürece her kaydı bir kez hedefler. Ancak uygulama belirli bir saat boyunca kapalı kalırsa o bölümdeki kayıtlar o gün hiç işlenmeyebilir:

uygulama kapalı: 03:00-04:00 kaçırılan bölüm: id mod 24 = 3

Ertesi gün saat 03:00'te kayıtlar yeniden seçilir; fakat işlem günlük zorunluluk taşıyorsa bir günlük gecikme oluşur.

Bu nedenle modulo ataması tek başına kesin teslim garantisi değildir. Yalnızca planlı yürütme zamanını belirler. Kaçırılan işlerin tamamlanması için kalıcı durum gerekir:

due(i, date) =
bucket(i) = hour
AND completed(i, date) = false

Daha dayanıklı modelde scheduler mevcut saat bölümünü işlerken önceki eksikleri de sınırlı biçimde tarayabilir:

önce mevcut bölüm sonra son kapanmış güne ait eksikler

Alternatif olarak kayıt için son başarılı çalışma zamanı tutulabilir:

lastSuccess(userId)

ve seçim koşulu şu şekilde genişletilebilir:

id mod 24 = currentHour OR lastSuccess < requiredDate

Bu yaklaşım mevcut saatin güncel yükünü korurken kaçırılan kayıtların zaman içinde geri kazanılmasını sağlar.

Ancak bütün gecikmiş kayıtların tek yürütmede yüklenmesi, modulo ile sağlanan dengeyi bozabilir. Backlog için üst sınır uygulanmalıdır:

currentBucketLimit backfillLimit totalExecutionDeadline

Örneğin görev önce mevcut bölümdeki yaklaşık 1.200 kaydı, ardından en fazla 100 gecikmiş kaydı işleyebilir. Böylece geçmiş borç kontrollü biçimde azaltılırken scheduler periyodu aşılmaz.

Bölüm sayısının 24 olmak zorunda olmaması

Saatlik zamanlama için 24 bölüm doğal görünür, fakat bölüm sayısı doğrudan scheduler periyodundan türetilebilir.

Görev her yarım saatte farklı bir bölüm işleyecekse:

B = 48

ve bölüm numarası:

slot = hour × 2 + half

olarak tanımlanabilir.

Burada:

half = 0, dakika < 30 half = 1, dakika ≥ 30

olur. Kayıt seçimi:

id mod 48 = slot

şeklinde yapılır.

Bu model her yarım saatlik çalışmada kayıtların yaklaşık kırk sekizde birini işler. Toplam günlük iş yükü değişmez, fakat tek yürütmedeki yoğunluk yarıya iner.

Buna karşılık aynı saat bölümünü iki kez çalıştırmak hata telafisi sağlar. İki yaklaşımın amacı farklıdır:

24 bölüm + saatte iki çalışma: aynı iş için ikinci deneme

48 bölüm + yarım saatte bir çalışma: iş yükünü daha küçük parçalara ayırma

Hangisinin doğru olduğu sistem gereksinimine bağlıdır. Geçici hataların ikinci denemede düzelme olasılığı yüksekse 24 bölüm avantajlıdır. Tek yürütme süresi scheduler aralığına yaklaşıyorsa 48 bölüm daha güvenli olabilir.

Bölüm sayısı değiştirilirse bütün kayıtların zaman ataması değişir:

id mod 24 ≠ id mod 48

Bu nedenle bölüm sayısı sıradan bir konfigürasyon değeri değil, zamanlama şemasının parçasıdır. Değişiklik sırasında eski ve yeni düzenin aynı gün içinde mükerrer veya eksik işlem üretmemesi için geçiş politikası gerekir.

Dinamik kuyruk yerine deterministik parçalama

Merkezi iş kuyruğu daha esnek yük dengelemesi sağlar. Boş worker yeni işi alabilir, uzun süren kayıtlar doğal olarak farklı zamanlara yayılır ve öncelik yönetimi yapılabilir. Buna karşılık kuyruk şu ek bileşenleri gerektirir:

  • iş kaydı,
  • teslim durumu,
  • görünürlük süresi,
  • yeniden deneme sayacı,
  • dead-letter yönetimi,
  • eşzamanlı tüketici koordinasyonu,
  • bakım ve gözlemleme altyapısı.

Tek process ve sınırlı kayıt hacmi bulunan bir görev için bu maliyet gereksiz olabilir.

Modulo parçalama, iş dağıtım durumunu saklamaz:

assignment = pureFunction(id, partitionCount)

Bu fonksiyonun sonucu yeniden hesaplanabilir. Merkezi koordinasyon gerektirmez ve aynı veri kümesinde her çalıştırmada aynı sonucu üretir.

Karşılığında dinamik dengeleme yeteneği sınırlıdır. Uzun süren kayıtlar kendiliğinden başka bölüme taşınmaz. Öncelikli iş, ek bir kural olmadan öne alınamaz. Bölüm kaçırıldığında teslim garantisi ayrıca kurulmalıdır.

Bu nedenle modulo tabanlı zamanlama özellikle şu koşullarda uygundur:

  • Kayıt kimlikleri kararlıysa,
  • İş yükü kayıtlar arasında yaklaşık benzerse,
  • Toplam hacim tek process tarafından günlük olarak işlenebiliyorsa,
  • Karmaşık kuyruk altyapısı istenmiyorsa,
  • Aynı kaydın yeniden işlenmesi güvenliyse,
  • Saatlik bölüm kaçırmaları telafi edilebiliyorsa.

Kayıt maliyetleri aşırı değişken, teslim süreleri katı veya yatay worker ölçeklemesi zorunluysa kalıcı kuyruk daha uygun olabilir.

Modulo işleminin değeri, en gelişmiş yük dengeleme algoritması olmasından değil, problem yeterince düzenliyken daha karmaşık altyapıya ihtiyaç bırakmamasından gelir.

Basit formülün arkasındaki sistem sözleşmesi

Kimlik modülüne göre saat seçmek hesaplama açısından sabit maliyetlidir:

O(1)

Bütün kayıtlar uygulama tarafında taranıyorsa toplam seçim maliyeti:

O(N)

olur. Filtre veritabanına aktarılır ve uygun erişim planı sağlanırsa uygulamaya yalnızca hedef bölüm taşınır. Bununla birlikte "USERID % 24" ifadesi normal indeksin doğrudan kullanımını sınırlayabilir. Büyük tablolarda sanal sütun veya function-based index değerlendirilebilir:

BUCKET AS (MOD(USERID, 24))

ve bu sütun üzerinde indeks oluşturulabilir. Yaklaşık 28 bin kayıt düzeyinde ise tam tarama maliyeti çoğu sistemde düşük olabilir; şema karmaşıklığı eklemeden önce gerçek yürütme planı ölçülmelidir.

Asıl doğruluk formülden değil, çevresindeki sözleşmelerden gelir:

kimlik kararlı mı? bölüm sayısı sabit mi? saat tek kez mi okunuyor? işlem idempotent mi? ikinci tetikleme ne anlama geliyor? kaçırılan bölüm nasıl tamamlanıyor? kayıt sayısı ile gerçek maliyet korelasyonlu mu?

Bu sorular cevaplanmadan yalnızca "% 24" koşulu kullanmak, yükü dağıtsa bile güvenilir bir scheduler tasarımı oluşturmaz.

Buna karşılık koşullar açıkça tanımlandığında yöntem güçlü bir özellik kazanır: sistemin hangi kaydı ne zaman işleyeceği dış bir durumdan değil, saf ve doğrulanabilir bir fonksiyondan türetilir.

zaman bölümü = kimlik mod bölüm sayısı

Bu deterministik ilişki; yeniden başlatmayı, test üretmeyi, yük tahminini ve hata analizini kolaylaştırır. Karmaşık sistemlerde her sorunun dinamik bir koordinasyon katmanıyla çözülmesi gerekmez. Bazen en güvenilir dağıtım mekanizması, değişmeyen bir kimliği sabit sayı ile bölmek ve kalan değerin ifade ettiği sözleşmeyi eksiksiz uygulamaktır.

Bu sayfanın QR kodu