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 (Network Time Protocol) 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, distributed locking ve event sıralaması gibi kritik işlemlerde ciddi sorunlara yol açar. Bu yazıda, clock skew'in neden olduğu problemleri, monotonik saatlerin rolünü ve .NET'te doğru zaman yönetimi stratejilerini detaylıca ele alacağız.
1. Fiziksel Saatler Neden Güvenilmez?
Her bilgisayarın donanımında bir gerçek zamanlı saat (RTC - Real-Time Clock) bulunur. Bu saat, bir kuvars osilatörüne dayanır ve şu nedenlerle kayabilir:
-
Sıcaklık Değişimleri: Kuvars osilatörünün frekansı sıcaklıkla değişir.
-
Donanım Yaşı: Osilatör zamanla yaşlanır ve frekansı kayar.
-
NTP Senkronizasyonu: NTP, saati periyodik olarak düzeltir, ancak bu düzeltmeler ani sıçramalara (clock jump) yol açabilir.
-
Sanallaştırma: VM'lerde saat, hipervizör tarafından yönetilir ve ek gecikmeler oluşabilir.
Tipik Clock Skew Değerleri:
-
İyi senkronize edilmiş bir cluster'da: 1-10 milisaniye
-
NTP olmadan: saniyeler, dakikalar hatta saatler
-
Google Spanner gibi sistemlerde: ~7 milisaniye (TrueTime API ile)
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.
text
Node A: Event X (saat: 10:00:00.100) Node B: Event Y (saat: 10:00:00.050) ← Aslında sonra oldu ama saat geride
Sistem, Event Y'yi Event X'ten önce sanır. Ancak gerçekte Event X önce olmuştur.
B. Timeout ve Distributed Locking Sorunu
Dağıtık kilitleme sistemlerinde, kilit süresi (TTL) yerel saate göre hesaplanır. Eğer iki düğümün saati farklıysa:
text
Node A: Kilidi 30 saniye TTL ile alır (yerel saat: 10:00:00)
Node B: Saati 5 saniye ileri (yerel saat: 10:00:05)
30 saniye sonra kilit süresi dolar (Node B'ye göre 10:00:35)
Ancak Node A'ya göre henüz 10:00:30 - kilit hala geçerli!
Bu, çit sorunu (fencing problem) olarak bilinir: Kilit süresi doldu sanılır ama aslında dolmamıştır.
C. Event Sourcing ve Audit Log Sorunu
Event'lerin zaman damgalarına göre sıralandığı sistemlerde (event sourcing, audit log), clock skew yanlış sıralamaya yol açar.
D. Last-Write-Wins (LWW) Çakışma Çözümü
LWW tabanlı çakışma çözümünde, en son yazılan kazanır. Ancak saatler kaymışsa, "en son" yanlış belirlenir ve veri kaybı olur.
3. Monotonik Saatler (Monotonic Clocks)
Monotonik saat, duvar saatinden (wall clock) 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ç?" sorusunu yanıtlamaz; "ne kadar süre geçti?" sorusunu yanıtlar.
-
Sistem Başlangıcına Göre: Genellikle sistem açılışından itibaren geçen süreyi ölçer.
.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 (logical clocks) devreye girer. Lamport saati, Leslie Lamport tarafından 1978'de önerilen, 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.
Örnek:
text
Node A: (1) → (2) → mesaj gönder (3) Node B: → mesaj al (max(0,3)+1 = 4) → (5)
Lamport Saatlerinin Sınırı: A ve B olayları arasında A → B (A, B'den önce) ilişkisi varsa, Lamport(A) < Lamport(B) doğrudur. Ancak tersi her zaman doğru değildir: 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.
Örnek:
text
Node A: [A:1, B:0, C:0] Node B: [A:0, B:1, C:0] Node C: [A:0, B:0, C:1]
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ı
7. .NET'te Zaman Yönetimi En İyi Pratikleri
A. TimeProvider (NET 8+) Kullanın
TimeProvider, .NET 8 ile gelen, 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, // Duvar saati (audit için)
...
};
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. CancellationTokenSource ile Timeout
csharp
// CancellationTokenSource, dahili olarak monotonik saat kullanır using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await DoWorkAsync(cts.Token);
D. Distributed Locking'de Fencing Token
csharp
// Kilit alındığında monotonik artan token alın
var lockHandle = await _distributedLock.AcquireAsync("resource");
var fencingToken = lockHandle.FencingToken; // Monotonik
// Kaynağa yazarken token'ı gönder
await _resource.WriteAsync(data, fencingToken);
// Kaynak, daha düşük token'lı istekleri reddeder
E. 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 İzleyin | Düğümler arası saat farkını metrik olarak izleyin |
| Time API'lerini Kullanın | .NET 8+ TimeProvider, TimeSpan, DateTimeOffset |
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++; // Aynı fiziksel zaman içinde
}
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.