Dağıtık sistemlerde çalışmaya başladığımdan beri, her şeyin “bir kere” düzgün çalışmasını beklemek yerine, her zaman bir şeylerin ters gideceğini varsaymayı öğrendim. Özellikle bir üretim ERP’sinde, tedarik zinciri entegrasyonlarını veya finansal işlemleri yönetirken, network kesintileri, sunucu yüklenmeleri veya geçici veri tabanı kilitlenmeleri gibi durumlar yüzünden işlemlerin yarım kalması kaçınılmazdır. Bu gibi anlarda, “işlem tekrar denemeleri” (retry mechanisms) sadece bir teknik detay olmaktan çıkıp, sistemin genel dayanıklılığını ve hatta kendi mental sağlığımı doğrudan etkileyen bir felsefe haline geldi.
Hayatın kendisi gibi, dağıtık sistemler de belirsizliklerle dolu. Her bir mikroservis, her bir network hop’u, her bir veri tabanı çağrısı kendi başına birer hata kaynağı olabilir. Bu yüzden, bir işlemi başlattığımızda, onun başarılı olacağını garanti edemeyiz. Bu yazıda, yıllar içinde edindiğim gözlemlerle, bu kaçınılmaz hatalarla nasıl başa çıktığımızı, hangi tekrar deneme yaklaşımlarını kullandığımızı ve bu teknik detayların bana hayat hakkında neler öğrettiğini anlatacağım.
Giriş: Dağıtık Sistemlerde Yeniden Denemenin Kaçınılmazlığı
Birçok kez karşılaştığım bir senaryo var: Bir servis başka bir servise çağrı yapıyor ve o anlık bir network tıkanıklığı yüzünden çağrı başarısız oluyor. Eğer bu çağrı, mesela bir üretim siparişinin durumunu güncellemeyi içeriyorsa, başarısızlığın kabul edilmesi ciddi iş kesintilerine yol açabilir. İlk başta basit bir try-catch bloğu ile hatayı yakalayıp loglamak yeterli gibi görünse de, bu sadece sorunu ertelemek veya daha büyük bir kriz yaratmak demek.
Dış API entegrasyonlarında bu durum defalarca karşıma çıkar. Üçüncü parti bir ödeme sağlayıcısına yapılan çağrının anlık bir gecikme yüzünden başarısız olması, siparişin beklemede kalmasına ve müşteri memnuniyetinin düşmesine neden olabilir. Bu tür durumları ele almak için sistemin doğasında var olan geçici hataları kabul edip, bunlara uygun bir yeniden deneme stratejisi geliştirmemiz gerektiğini anladım. Basitçe, ilk denemede başarısız olan bir işlem için “bekle ve tekrar dene” yaklaşımı, çoğu zaman işleri yoluna koymanın en pratik yoludur.
import requests
import time
def make_api_call_simple(url, data, retries=3):
for i in range(retries):
try:
response = requests.post(url, json=data, timeout=5)
response.raise_for_status() # HTTP 4xx/5xx hatalarını yakala
print(f"Deneme {i+1}: Başarılı.")
return response.json()
except requests.exceptions.RequestException as e:
print(f"Deneme {i+1} başarısız: {e}")
if i < retries - 1:
time.sleep(1) # Basit bir bekleme
raise Exception(f"Tüm denemeler başarısız oldu: {url}")
# Örnek kullanım
try:
result = make_api_call_simple("http://example.com/api/order", {"item": "widget"})
print("API çağrısı başarıyla tamamlandı:", result)
except Exception as e:
print("API çağrısı başarısız:", e)
Bu basit örnek bile, bir işlemi sadece bir kez denemek yerine, hatalara karşı daha esnek olmanın ne kadar önemli olduğunu gösteriyor. Ancak bu yeterli değil; daha sofistike yaklaşımlar da var.
Basit Yeniden Denemelerden Ötesi: Gecikmeli Yaklaşımlar
Basit time.sleep(1) yaklaşımları, özellikle yoğun yük altındaki sistemlerde veya bir kaynağın gerçekten aşırı yüklendiği durumlarda işe yaramaz. Hatta durumu daha da kötüleştirebilir. Tüm istemcilerin aynı anda tekrar denemesi, “thundering herd” olarak bilinen bir soruna yol açarak, zaten zor durumdaki servisi tamamen çökertme riski taşır. Bu yüzden, yeniden deneme stratejilerinde “gecikme” (backoff) ve “jitter” (rastgeleleştirme) kavramları hayati önem taşır.
Yoğun raporlama dönemlerinde veri tabanına yapılan çağrılarda bunu net bir şekilde görürsünüz. Çok sayıda kullanıcı aynı anda karmaşık raporlar çektiğinde, veri tabanı geçici olarak kilitlenebilir veya sorgular yavaşlayabilir. Eğer her başarısız çağrı anında tekrar deneseydi, veri tabanı tamamen kullanılamaz hale gelirdi. Çözüm, üstel gecikme (exponential backoff) uygulamaktır.
Üstel gecikme, her başarısız denemeden sonra bekleme süresini katlayarak artırır (örn: 1 saniye, 2 saniye, 4 saniye, 8 saniye…). Bu, servise kendini toparlaması için zaman tanır. Jitter ise bu gecikme süresine rastgele bir miktar ekleyerek, tüm istemcilerin aynı anda tekrar denemesini engeller. Kendi yan ürünümün backend’inde, dış API’lara yapılan çağrılarda bu kombinasyonu aktif olarak kullanıyorum ve sistemin çok daha sağlam çalıştığını görüyorum.
import requests
import time
import random
def make_api_call_with_backoff(url, data, retries=5, initial_delay=0.5, max_delay=30):
for i in range(retries):
try:
response = requests.post(url, json=data, timeout=10)
response.raise_for_status()
print(f"Deneme {i+1}: Başarılı.")
return response.json()
except requests.exceptions.RequestException as e:
print(f"Deneme {i+1} başarısız: {e}")
if i < retries - 1:
delay = min(initial_delay * (2 ** i), max_delay)
jitter = random.uniform(0, delay * 0.1) # %10'a kadar jitter
total_delay = delay + jitter
print(f" {total_delay:.2f} saniye bekliyorum...")
time.sleep(total_delay)
raise Exception(f"Tüm denemeler başarısız oldu: {url}")
# Örnek kullanım (burada HTTP 500 hatası döndüren bir endpoint varsayalım)
# result = make_api_call_with_backoff("http://example.com/api/failing_endpoint", {"param": "value"})
Bu yaklaşım, bana hayatta da bir ders verdi: Bazı durumlar karşısında ısrarcı olmak iyi olsa da, bazen durup biraz beklemek, durumu sakinleştirmek ve daha sonra akıllıca tekrar denemek daha verimli sonuçlar verir. Sabır, hem sistemlerde hem de insan ilişkilerinde kritik bir değer.
İdempotence’nin Önemi ve Yan Etkileri
Yeniden deneme mekanizmalarını tasarlarken, atladığımız veya yeterince düşünmediğimiz en büyük konulardan biri “idempotence” kavramıdır. İdempotent bir işlem, birden fazla kez çağrılsa bile sistem üzerinde aynı etkiyi yaratır. Yani, bir işlemi 1 kez yapmakla 10 kez yapmak arasında sonuç açısından bir fark yoktur. Örneğin, bir kullanıcının bakiyesine 100 birim eklemek idempotent değildir (her denemede bakiye artar), ancak bir kullanıcının bakiyesini 100 birim olarak ayarlamak veya belirli bir transaction_id ile para transferi kaydı oluşturmak idempotent olabilir.
Özellikle para transferi gibi finansal işlemlerde idempotence’nin ne kadar kritik olduğunu defalarca gördüm. Eğer bir transfer işlemi network hatası yüzünden başarısız olursa ve client tekrar denerse, aynı transaction_id ile yeni bir kayıt oluşturulmaya çalışılır. Veri tabanında bu transaction_id için benzersiz bir kısıtlama (unique constraint) varsa, ikinci deneme otomatik olarak başarısız olur ve bu, çift ödeme gibi felaket senaryolarını engeller.
-- PostgreSQL'de idempotent bir transfer kaydı için tablo yapısı
CREATE TABLE transfers (
id SERIAL PRIMARY KEY,
transaction_id UUID UNIQUE NOT NULL, -- Bu önemli!
from_account_id INT NOT NULL,
to_account_id INT NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Bir transfer kaydının eklenmesi (eğer transaction_id zaten varsa hata verir)
INSERT INTO transfers (transaction_id, from_account_id, to_account_id, amount)
VALUES ('a1b2c3d4-e5f6-7890-1234-567890abcdef', 101, 202, 500.00);
İdempotent olmayan işlemler için tekrar deneme stratejileri çok daha karmaşıktır. Bu tür durumlarda, transaction outbox pattern gibi yaklaşımlar veya event-sourcing kullanarak, işlemin durumunu güvenilir bir şekilde takip etmek ve yalnızca onaylanmamış işlemleri tekrar denemek gerekebilir. Kendi yan ürünümde, kullanıcıların belirli bir eylemi birden fazla kez tetiklemesini engelleyen mekanizmalar tasarlarken, bu prensibi her zaman göz önünde bulunduruyorum. Yanlışlıkla yapılan tekrar denemeler yüzünden veri tutarsızlıkları yaşamak, debugging sürecini kabusa çevirebilir. Bu bana, hayatta da attığımız adımların geri dönüşü olup olmadığını iyi düşünmemiz gerektiğini öğretti. Bazı hatalar, tekrar denense de aynı kötü sonucu verir.
Circuit Breaker ve Rate Limiter ile Sistemi Koruma
Bir sistemi tekrar denemelerle dayanıklı hale getirirken, bir diğer önemli nokta da “ne zaman duracağımızı” bilmektir. Eğer bir servis tamamen çökmüşse veya aşırı yüklenmişse, ona yüzlerce hatta binlerce tekrar deneme isteği göndermek, durumu daha da kötüleştirir. İşte bu noktada “Circuit Breaker” (Devre Kesici) ve “Rate Limiter” (Hız Sınırlayıcı) paternleri devreye girer.
Geçen yıl, bir üretim firmasının ERP’sinde, dışarıdaki bir lojistik firmasının API’sına yapılan sevkiyat bildirimleri sürekli hata vermeye başladığında, bizim sistemdeki kuyruklar şişmişti. Nedeni, lojistik firmasının API’sının geçici olarak hizmet dışı kalmasıydı. Bizim sistemimiz, backoff ile de olsa, sürekli olarak başarısız çağrılar yapmaya devam ediyordu. Bu durum, lojistik firmasının API’si tekrar ayağa kalktığında bile, bizim sistemimizdeki yığılmanın çözülmesini engelliyordu.
Bu senaryoyu çözmek için, dış API çağrılarımızda Circuit Breaker paternini uyguladık. Belirli bir zaman aralığında (örneğin 60 saniye içinde) %50’den fazla çağrı başarısız olursa, devre kesici “açık” duruma geçiyor ve sonraki tüm çağrıları doğrudan reddediyordu. Bu, hem kendi sistemimizdeki kaynakların boşa harcanmasını engelledi hem de uzaktaki servisin daha fazla yüklenmesini önledi. Bir süre (örn. 5 dakika) sonra devre kesici “yarı açık” duruma geçiyor ve birkaç deneme çağrısına izin veriyordu. Bu denemeler başarılı olursa, devre tekrar kapanıyordu.
Rate limiting ise, belirli bir süre içinde bir servise yapılabilecek çağrı sayısını sınırlar. Kendi siteme yaptığım AI entegrasyonlarında, üçüncü parti AI modellerine olan API çağrılarım için rate limiting kullanıyorum. Bu, hem maliyetleri kontrol altında tutmama hem de API sağlayıcılarının kullanım politikalarına uymama yardımcı oluyor. Bu iki patern, bana hayatta da kendi sınırlarımızı bilmemiz, bazen durup dinlenmemiz ve başkalarını da aşırı zorlamamamız gerektiğini hatırlatıyor.
İnsan Faktörü ve Monitoring: Ne Zaman Müdahale Etmeli?
Tekrar deneme mekanizmaları ve koruyucu paternler ne kadar iyi olursa olsun, sistemler hiçbir zaman %100 otonom değildir. İnsan faktörü, yani benim ve ekibimin müdahalesi, bazen kaçınılmaz hale gelir. İşte bu noktada güçlü monitoring ve alerting sistemleri devreye girer. Bir sistemin ne zaman “iyi” çalıştığını, ne zaman “orta” çalıştığını ve ne zaman “felaket” durumunda olduğunu bilmek, doğru zamanda doğru kararı vermek için hayati önem taşır.
Tipik bir örnek, PostgreSQL’de WAL bloat problemi yüzünden disk kullanımının beklenmedik şekilde artmasıdır. journald loglarında anlık FATAL hatalar görünür ama sistemdeki pgbouncer ve uygulama katmanındaki retry mekanizmaları sayesinde, kullanıcılar doğrudan bir kesinti yaşamayabilir. Tam da bu yüzden disk doluluğu kritik eşiğe yaklaştığında otomatik bir alarmın düşmesi hayati önem taşır; eğer sadece retry mekanizmalarına güvenilip monitoring yapılmazsa, sessizce ilerleyen sorun bir noktada sistemi tamamen çökertebilir.
# journalctl çıktısından bir örnek
May 19 03:14:23 server-prod systemd[1]: [email protected]: Main process exited, code=exited, status=1/FAILURE
May 19 03:14:23 server-prod systemd[1]: [email protected]: Failed with result 'exit-code'.
May 19 03:14:24 server-prod systemd[1]: [email protected]: Service hold-off time over, scheduling restart.
May 19 03:14:24 server-prod systemd[1]: [email protected]: State 'auto-restart' is still active.
Bu tür olaylar, bana sadece teknik sistemleri değil, kendi iş yükümü ve mental sağlığımı da izlemem gerektiğini öğretti. Tıpkı bir sistem gibi, insan da belirli bir stres eşiğine kadar işleri halledebilir, ancak belirli bir noktanın ötesinde müdahale veya dinlenme gereklidir. Otomatik yeniden denemeler, ilk darbeyi absorbe ederken, asıl sorunun kök nedenini bulup çözmek için bana zaman kazandırır. Bu, sistemin performans sorunlarını çözme deneyimim yazısında da bahsettiğim gibi, sürekli bir gözlem ve öğrenme döngüsüdür. Monitoring sadece bir araç değil, aynı zamanda bir iletişim aracıdır; sistemin bizimle konuştuğu dildir.
Kapanış: Hayat ve Sistemlerde Esneklik Sanatı
Dağıtık sistemlerde işlem tekrar denemeleri üzerine yaptığım bu gözlemler, bana sadece teknik konularda değil, hayatın birçok alanında da önemli dersler verdi. Her şeyin ilk denemede mükemmel olmayacağını kabul etmek, hataları birer öğrenme fırsatı olarak görmek ve esneklik göstermek, hem sistemleri hem de kendi hayatımızı daha dayanıklı hale getirir.
Bir üretim ERP’sinde karmaşık iş akışlarını tasarlarken, bir network altyapısını kurarken veya kendi yan ürünlerimi geliştirirken, hep bu prensiplerle hareket ettim:
- Beklenmedik Durumlara Hazırlıklı Olmak: Her zaman her şeyin ters gidebileceği ihtimalini göz önünde bulundurmak.
- Sabırlı Olmak: Hemen pes etmek yerine, doğru bir gecikmeyle tekrar denemek.
- Akıllıca Tekrar Denemek: Üstel gecikme ve jitter gibi yaklaşımlarla, sistemi ve kendimi yormadan doğru anı beklemek.
- Sınırları Bilmek: Circuit Breaker ve Rate Limiter ile kendi kapasitemizi ve diğer sistemlerin kapasitesini korumak.
- Gözlemlemek ve Müdahale Etmek: Monitoring ile sorunları erken tespit etmek ve gerektiğinde aktif rol almak.
Bu yaklaşımlar, sadece kod satırlarında veya network konfigürasyonlarında değil, günlük hayatımızdaki zorluklar karşısında da bize yol gösterebilir. Bir projede başarısız olduğumuzda, bir ilişkide anlaşmazlık yaşadığımızda veya yeni bir beceri öğrenirken zorlandığımızda, bu “tekrar deneme” felsefesi bize dirençli olmayı, pes etmemeyi ve her denemede biraz daha iyiye gitmeyi hatırlatır. Sonuçta, hem sistemler hem de insanlar, hatalardan ders çıkararak ve esneklik göstererek gelişir. Bir sonraki karmaşık veri tabanı optimizasyonları yazımda, bu esnekliği veri tabanı katmanında nasıl sağladığımızı anlatacağım.