Dağıtık Sistemlerde Clock Skew ve Monotonik Saatler
Dağıtık sistemlerde zaman, göründüğü kadar basit değildir. Her düğümün kendi fiziksel saati vardır ve bu saatler NTP ile senkronize edilse bile mükemmel değildir. İki düğüm arasındaki saat farkına clock skew (saat kayması) denir. Bu kayma; sıralama (ordering), timeout, dağıtık kilitleme ve event sıralaması gibi kritik işlemlerde ciddi sorunlara yol açar.
1. Fiziksel Saatler Neden Güvenilmez?
Her bilgisayarın donanımında bir gerçek zamanlı saat (RTC) bulunur. Bu saat, bir kuvars osilatörüne dayanır ve şu nedenlerle kayabilir:
-
Sıcaklık değişimleri: Osilatörün frekansı sıcaklıkla değişir.
-
Donanım yaşı: Osilatör zamanla yaşlanır.
-
NTP senkronizasyonu: Saati periyodik düzeltir, ancak ani sıçramalara (clock jump) yol açabilir.
-
Sanallaştırma: VM'lerde saat, hipervizör tarafından yönetilir; ek gecikmeler oluşabilir.
Tipik Clock Skew Değerleri:
-
İyi senkronize edilmiş bir cluster'da: 1-10 milisaniye
-
NTP olmadan: saniyeler, dakikalar hatta saatler
2. Clock Skew'in Yol Açtığı Sorunlar
A. Sıralama (Ordering) Sorunu
İki düğümde meydana gelen olayların sırasını belirlemek istediğinizde, saatler kaymışsa yanlış sıralama yaparsınız. Sistem, gerçekte sonra olan bir olayı önce sanabilir.
B. Timeout ve Distributed Locking Sorunu
Dağıtık kilitleme sistemlerinde kilit süresi (TTL) yerel saate göre hesaplanır. İki düğümün saati farklıysa, kilit süresi doldu sanılabilir ama aslında dolmamıştır. Bu, çit sorunu (fencing problem) olarak bilinir.
C. Event Sourcing ve Audit Log Sorunu
Event'lerin zaman damgalarına göre sıralandığı sistemlerde clock skew yanlış sıralamaya yol açar.
D. Last-Write-Wins (LWW) Çakışma Çözümü
Saatler kaymışsa, "en son" yanlış belirlenir ve veri kaybı olur.
3. Monotonik Saatler (Monotonic Clocks)
Monotonik saat, duvar saatinden bağımsız olarak yalnızca ileriye doğru ilerleyen bir saattir. Sistem saati NTP ile geriye alınsa bile monotonik saat etkilenmez.
Özellikler:
-
Her Zaman İleri: Asla geriye gitmez.
-
Süre Ölçümü İçin İdeal: İki olay arasındaki süreyi doğru ölçer.
-
Mutlak Zaman Değil: "Saat kaç?" değil, "ne kadar süre geçti?" sorusunu yanıtlar.
.NET'te Monotonik Saat:
csharp
// Stopwatch - monotonik saat
var stopwatch = Stopwatch.StartNew();
await DoWorkAsync();
stopwatch.Stop();
Console.WriteLine($"İşlem süresi: {stopwatch.ElapsedMilliseconds}ms");
// Environment.TickCount64 - monotonik (64-bit)
var start = Environment.TickCount64;
await DoWorkAsync();
var elapsed = Environment.TickCount64 - start;
// .NET 8+ TimeProvider ile
var timeProvider = TimeProvider.System;
var timestamp = timeProvider.GetTimestamp();
await DoWorkAsync();
var elapsed = timeProvider.GetElapsedTime(timestamp);
Ne Zaman Monotonik Saat Kullanılmalı?
-
Timeout ölçümü
-
Performans ölçümü (benchmark)
-
Retry backoff hesaplaması
-
Circuit breaker süreleri
-
Cache TTL (yerel)
Ne Zaman Duvar Saati Kullanılmalı?
-
Kullanıcıya gösterilecek zaman damgaları
-
Audit log (mutlak zaman gerekli)
-
Veritabanı kayıt zamanı (created_at)
-
Cron job zamanlaması
4. Lamport Saatleri (Mantıksal Saatler)
Fiziksel saatler güvenilmez olduğunda, mantıksal saatler devreye girer. Lamport saati, olayların nedensel sırasını (causal ordering) belirlemek için kullanılan bir sayaçtır.
Kurallar:
-
Her düğümün bir sayacı vardır (başlangıçta 0).
-
Her yerel olayda sayaç 1 artırılır.
-
Mesaj gönderirken, gönderen sayaç değerini mesaja ekler.
-
Mesaj alındığında, alıcı sayacını
max(kendi_sayacı, gelen_sayac) + 1olarak günceller.
Sınırı: Lamport(A) < Lamport(B) ise A'nın B'den önce olduğu kesin değildir (eşzamanlı olabilirler).
5. Vector Clocks (Vektör Saatleri)
Vector clocks, Lamport saatlerinin sınırını aşarak eşzamanlılık (concurrency) tespiti yapar. Her düğüm, tüm düğümlerin sayaçlarını içeren bir vektör tutar.
Karşılaştırma:
-
V1 < V2ise: V1, V2'den önce. -
V1 > V2ise: V1, V2'den sonra. -
Aksi halde: Eşzamanlı (concurrent) — çakışma var!
Vector clocks, DynamoDB ve Riak gibi sistemlerde çakışma tespiti için kullanılır.
6. Hybrid Logical Clocks (HLC)
HLC, fiziksel saatin yakınlığını ve mantıksal saatin nedensel sıralamasını birleştirir. CockroachDB, MongoDB ve YugabyteDB gibi modern dağıtık veritabanları HLC kullanır.
Çalışma Prensibi:
-
HLC, bir
(physical_time, logical_counter)çiftidir. -
Fiziksel saat ilerledikçe HLC de ilerler.
-
Aynı fiziksel zaman içinde birden fazla olay olursa, logical counter artırılır.
-
Bir düğümün saati gerideyse, diğer düğümün HLC'sini alarak senkronize olur.
Avantajları: Fiziksel saate yakın (okunabilir zaman damgaları), nedensel sıralama garantisi, clock skew'e karşı dayanıklılık.
7. .NET'te Zaman Yönetimi En İyi Pratikleri
A. TimeProvider (.NET 8+) Kullanın
TimeProvider, test edilebilir zaman soyutlamasıdır.
csharp
public class OrderService
{
private readonly TimeProvider _timeProvider;
public OrderService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
{
var now = _timeProvider.GetUtcNow(); // Test edilebilir
var timestamp = _timeProvider.GetTimestamp(); // Monotonik
var order = new Order { CreatedAt = now, ... };
await SaveAsync(order);
var elapsed = _timeProvider.GetElapsedTime(timestamp);
_logger.LogInformation("Sipariş oluşturma süresi: {Elapsed}", elapsed);
return order;
}
}
// Test'te FakeTimeProvider kullanımı
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
var service = new OrderService(fakeTime);
B. Timeout için Stopwatch Kullanın
csharp
// ❌ YANLIŞ - Duvar saati kullanımı
var start = DateTime.UtcNow;
await DoWorkAsync();
if (DateTime.UtcNow - start > TimeSpan.FromSeconds(5))
throw new TimeoutException();
// ✅ DOĞRU - Monotonik saat kullanımı
var stopwatch = Stopwatch.StartNew();
await DoWorkAsync();
if (stopwatch.Elapsed > TimeSpan.FromSeconds(5))
throw new TimeoutException();
C. Distributed Locking'de Fencing Token
csharp
var lockHandle = await _distributedLock.AcquireAsync("resource");
var fencingToken = lockHandle.FencingToken; // Monotonik
await _resource.WriteAsync(data, fencingToken);
// Kaynak, daha düşük token'lı istekleri reddeder
D. Event Sourcing'de Zaman Damgaları
csharp
public record OrderCreatedEvent(
Guid OrderId,
DateTimeOffset OccurredAt, // Duvar saati (audit)
long SequenceNumber, // Monotonik sıra numarası
string CorrelationId
);
Event'lerin sıralaması için sequence number kullanın, zaman damgasına güvenmeyin.
8. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Süre ölçümü için monotonik saat kullanın | Stopwatch, Environment.TickCount64, TimeProvider.GetTimestamp() |
| Mutlak zaman için duvar saati kullanın | Audit log, kullanıcıya gösterilecek zaman damgaları |
| Sıralama için mantıksal saat kullanın | Lamport, Vector Clock veya HLC |
| NTP ile saatleri senkronize edin | Tüm düğümlerde NTP aktif olsun |
| Zaman damgalarını UTC saklayın | Yerel saat dilimleri karışıklık yaratır |
| Sequence number kullanın | Event sıralaması için zaman damgasına güvenmeyin |
| Fencing token kullanın | Distributed locking'de clock skew'e karşı koruma |
| TimeProvider ile test edilebilir olun | Zaman bağımlı kodları test edilebilir hale getirin |
| Clock skew'i izleyin | Düğümler arası saat farkını metrik olarak izleyin |
9. Gerçek Dünya Senaryosu: E-Ticaret Sipariş Sıralaması
Senaryo: Bir e-ticaret uygulamasında, sipariş event'leri 3 servis tarafından üretiliyor: Order Service, Payment Service ve Shipment Service. Event'ler bir Kafka topic'ine yazılıyor ve sıralı olarak işlenmesi gerekiyor.
Problem: Servislerin saatleri 50ms kaymış. Event'ler yanlış sırada işleniyor.
Çözüm: HLC (Hybrid Logical Clock) veya sequence number kullanın.
csharp
public class HybridLogicalClock
{
private long _lastPhysicalTime;
private int _logicalCounter;
private readonly object _lock = new();
public (long PhysicalTime, int Counter) Now()
{
lock (_lock)
{
var physicalNow = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (physicalNow > _lastPhysicalTime)
{
_lastPhysicalTime = physicalNow;
_logicalCounter = 0;
}
else
{
_logicalCounter++;
}
return (_lastPhysicalTime, _logicalCounter);
}
}
public void Update((long PhysicalTime, int Counter) received)
{
lock (_lock)
{
var physicalNow = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var maxPhysical = Math.Max(physicalNow, Math.Max(_lastPhysicalTime, received.PhysicalTime));
if (maxPhysical == _lastPhysicalTime && maxPhysical == received.PhysicalTime)
_logicalCounter = Math.Max(_logicalCounter, received.Counter) + 1;
else if (maxPhysical == _lastPhysicalTime)
_logicalCounter++;
else if (maxPhysical == received.PhysicalTime)
_logicalCounter = received.Counter + 1;
else
_logicalCounter = 0;
_lastPhysicalTime = maxPhysical;
}
}
}
Bu sayede, event'ler doğru sırada işlenir; clock skew sorun olmaktan çıkar.
Sonuç
Dağıtık sistemlerde zaman, göründüğü kadar basit değildir. Clock skew, fiziksel saatlerin doğal bir sonucudur ve sıralama, timeout, locking gibi kritik işlemlerde ciddi sorunlara yol açar. Monotonik saatler, süre ölçümü için güvenli tek seçenektir; asla geriye gitmez. Lamport saatleri, Vector Clocks ve Hybrid Logical Clocks, nedensel sıralamayı fiziksel saatlerden bağımsız olarak sağlar. .NET'te Stopwatch, Environment.TickCount64 ve TimeProvider ile monotonik saatleri kullanabilir; TimeProvider ile test edilebilir zaman yönetimi yapabilirsiniz. Unutmayın: Dağıtık sistemlerde "şu an" kavramı, her düğümde farklıdır. Güvenilir sistemler, zamanı bir gerçek olarak değil, bir "tahmin" olarak ele alır ve gerekli önlemleri alır.