İçeriğe Atla
Mustafa Erbay
Yaşam · 9 dk okuma · görüntülenme Read in English

Dağıtık Sistemlerde İşlem Tekrar Denemeleri: Gözlemlerim

Dağıtık sistemlerde işlem tekrar denemeleri neden kaçınılmazdır? Yirmi yıllık tecrübemden öğrendiğim pratik yaklaşımlar ve hayat dersleri.

100%

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.

Paylaş:

Bu yazı faydalı oldu mu?

Yükleniyor...

Bu yazı nasıldı?

Sıkça Sorulanlar

Bu makale ile ilgili okurların sorduğu yaygın sorular.

İşlem tekrar denemelerine yeni başlıyorsam, ilk olarak hangi basit ama etkili stratejiyi uygulamalıyım?
Ben başlarken, sabit aralıklarla sabit sayıda yeniden deneme yapmak en kolay başlangıç noktasıydı. Örneğin, bir API çağrısını en fazla 3 kez, 1 saniye ara ile denerdim. Bu, anlık ağ kesintileri ya da geçici servis yavaşlıkları gibi çoğu geçici hatayı kapardı. Ancak zamanla, bu sabit stratejinin bazen gereğinden fazla yük oluşturduğunu gördüm. Yine de, özellikle yeni bir sistemde veya düşük kritiklikte işlemlerde, bu yaklaşım hem hızlı uygulanabilir hem de anlamlı bir iyileştirme sağlar. Deneyim kazandıkça, daha akıllı stratejilere geçtim.
Üstel geriye dönüş (exponential backoff) mi, sabit aralıklı deneme mi daha iyi? Hangi durumda hangisini tercih ediyorsun?
Benim tecrübemde, üstel geriye dönüş neredeyse her zaman sabit aralıktan daha etkili. Özellikle yoğunluk yaratabilecek sistemlerde, sabit aralıklı denemeler sistemleri daha da zorlayabilir. Örneğin, bir veritabanı geçici olarak çöktüğünde, her 1 saniyede bir denemek yerine, 1, 2, 4 saniye şeklinde artırmak, sistemin toparlanma şansı verir. Üstel geriye dönüşü, özellikle yüksek trafiğe sahip finansal entegrasyonlarda kullanıyorum. Ancak çok kritik olmayan durumlarda, sabit aralıklı denemeyle başlamak pratik olabilir.
Bir işlemi kaç kez tekrar denemeliyim? Aşırı deneme yaparsam ne olur?
Ben genellikle 3 ila 5 deneme arasında bir sınır koyuyorum. 3 deneme, çoğu geçici hatayı yeterince kapsar; 5’in üstüne çıkmak, sistemi gereksiz yorabilir. Özellikle idempotent olmayan işlemlerde çok sayıda deneme, veri tutarsızlığına yol açabilir. Örneğin, bir ödeme işlemi tekrarlanırsa, çift ücretlendirme olabilir. Bu yüzden, sadece idempotent işlemlerde deneme sayısı artırılmalı. Deneyimimce, sınırsız veya çok yüksek sayıda deneme, 'daha güvenli' değil, aslında daha tehlikelidir.
Tüm işlemlerde yeniden deneme mekanizması olmalı mı, yoksa seçmeli mi uygulanmalı?
Ben başlangıçta her yere deneme koydum, ama zamanla anladım ki seçmeli uygulamak çok daha akıllıca. Özellikle idempotent olmayan veya zincirleme etki yaratan işlemlerde blind retry tehlikeli. Örneğin, bir tedarikçiden stok rezervasyonu yapan servise otomatik retry koymak, yanlışlıkla fazla rezervasyona neden olabilir. Bunun yerine, sadece geçici hatalar (timeout, 5xx) için, ve idempotent garantisi olan işlemlerde deneme ekliyorum. Bu, hem güvenliği artırır hem de sistemi gereksiz yormaz.
ME

Mustafa Erbay

Sistem Mimarisi · Network Uzmanı · Altyapı, Güvenlik ve Yazılım

2006'dan bu yana sistem mimarisi, network, sunucu altyapıları, büyük yapıların kurulumu, yazılım ve sistem güvenliği ekseninde çalışıyorum. Bu blogda sahada karşılığı olan teknik deneyimlerimi paylaşıyorum.

Kişisel Notlar

Bu notlar sadece sizde saklanır. Tarayıcınızda yerel olarak tutulur.

Hazır 0 karakter

Yorumlar

Sunucu Taraflı AI Moderasyon

Yorumlar sunucuda yapay zeka ile denetlenir ve kalıcı olarak saklanır.

?
0/2000

Sunucu taraflı AI denetim

✉️ Ücretsiz · Spam yok · İstediğin an çık

Yeni yazılardan haberdar olun

Yeni içerikler ve teknik notlar e-postanıza gelsin.

  • 📌
    Haftanın en iyisi Sadece okumaya değer tek yazı
  • 🔧
    Alet çantası Bu hafta kullandığım araçlar
  • 🧠
    Perde arkası Blog'a girmeyen notlar

Spam yapmıyoruz. İstediğiniz zaman ayrılabilirsiniz. · Sadece Umami (self-hosted, Google yok) ile takip.

Okuma İstatistikleriniz

0

Yazı Okundu

0dk

Okuma Süresi

0

Gün Serisi

-

Favori Kategori

İlgili Yazılar