İkinci sunucumuzu eklediğimiz andan itibaren dağıtılmış sistemler, Amazon’da bir yaşam tarzı haline geldi. 1999’da Amazon’da çalışmaya ilk başladığımda o kadar az sunucumuz vardı ki bazılarına “fishy” veya “online-01” gibi akılda kalıcı isimler verebiliyorduk. Ancak, 1999’da bile dağıtılmış bilişim hiç kolay değildi. O zamanlar da şimdiki gibi dağıtılmış sistemler gecikme, ölçeklendirme, API ağ iletişimini anlamak, verileri marshalling ve unmarshalling yapmak, ve Paxos gibi algoritmaların karmaşıklığı gibi zorluklar içeriyordu. Sistemler hızlı bir şekilde daha fazla büyüyüp dağıtıldıkça teorik uç durumlar düzenli olaylara dönüştü.

Güvenilir uzak mesafe telefon bağlantıları veya Amazon Web Services (AWS) hizmetleri gibi dağıtılmış yardımcı bilişim hizmetlerini geliştirmek zordur. Dağıtılmış bilişim aynı zamanda birbiriyle ilişkili iki problem nedeniyle diğer bilişim biçimlerinden daha tuhaf ve daha az sezgiseldir. Bağımsız hatalar ve non-determinizm dağıtılmış sistemlerde en etkili sorunlara yol açmaktadır. Çoğu mühendisin aşina olduğu tipik bilişim hatalarına ek olarak, dağıtılmış sistemlerdeki hatalar birçok şekilde meydana gelebilir. Daha da kötüsü, bir şeyin başarısız olup olmadığını her zaman bilmek imkansızdır.

Amazon Derleyici Kitaplığı içerisinde, AWS’nin dağıtılmış sistemlerden kaynaklanan karmaşık geliştirme ve operasyon sorunlarını nasıl ele aldığını açıklıyoruz. Diğer makalelerde bu tekniklerin ayrıntılarına inmeden önce dağıtılmış bilişimin bu kadar tuhaf olmasına neden olan kavramları gözden geçirmek gerekir. İlk önce dağıtılmış sistemlerin türlerine bakalım.

Dağıtılmış sistemlerin türleri

Dağıtılmış sistemler aslında uygulama zorluğu açısından değişiklik gösterir. Spektrumun bir ucunda çevrim dışı dağıtılmış sistemler vardır. Bunlar toplu işleme sistemleri, büyük veri analizi kümeleri, film sahnesi içerik oluşturma grupları, protein katlanması kümeleri vb. içerir. Uygulanması çok zor olmakla birlikte, dağıtılmış sistemler, dağıtılmış bilişimlerin neredeyse tüm avantajlarından yararlanırlar (ölçeklenebilirlik ve hata toleransı) ve neredeyse hiçbir olumsuz tarafından etkilenmezler (karmaşık hata modları ve nondeterminizm).
 
Spektrumun ortasında yumuşak gerçek zamanlı dağıtılmış sistemler vardır. Bunlar sürekli sonuç üretmesi veya güncellemesi gereken, ancak bunu yapması için oldukça geniş bir zaman aralığına sahip olan kritik sistemlerdir. Bu sistemlere örnek olarak bazı arama dizini derleyicileri, kötü durumdaki sunucuları arayan sistemler, Amazon Elastic Compute Cloud (Amazon EC2) rolleri vb. verilebilir. Bir arama dizin oluşturucusu, uygunsuz müşteri etkisi olmadan (uygulamaya bağlı olarak) 10 dakikadan birkaç saate kadar çevrim dışı olabilir. Amazon EC2 rolleri (öncelikli olarak) her EC2 bulut sunucusuna güncellenmiş kimlik bilgilerini sunmalıdır. Ancak eski kimlik bilgilerinin kullanım süresi bir süre bitmediğinden dolayı bunun için saatlerce vakit vardır.
 
Spektrumun diğer, ama en zor, ucunda zor gerçek zamanlı dağıtılmış sistemler vardır. Bunlar genellikle istek/yanıt hizmetleri olarak adlandırılır. Amazon’da bir dağıtılmış sistem oluşturmayı düşünüyorsak ilk aklımıza gelen tür zor gerçek zamanlı sistemdir. Maalesef, zor gerçek zamanlı dağıtılmış sistemler doğru yapılması en zor olan sistemdir. İsteklerin geliş zamanının belirsiz olmasına ek olarak hızlı bir şekilde yanıt verilmesi gerektiği için bu sistemler zordur (örneğin, müşterinin aktif olarak yanıt beklemesi). Örnek olarak ön uç web sunucuları, sipariş hattı, kredi kartı işlemleri, her AWS API, telefon hizmeti vb. verilebilir. Bu makalenin başlıca odak noktası zor gerçek zamanlı dağıtılmış sistemlerdir.

Zor gerçek zamanlı sistemler tuhaftır

Süpermen çizgi romanlarının birinde, Süpermen her şeyin geriye doğru olduğu bir gezegende (Bizarro World) yaşayan Bizarroadında ikinci bir şahsiyetle karşılaşır. Bizarro biraz Süpermen’ebenzemektedir ama aslında kötü biridir. Zor gerçek zamanlı dağıtılmış sistemler de böyledir. Birbirine benzeyen normal bilişim gibi görünüyorlar. Ancak aslında birbirlerinden farklılar. Daha doğrusu zor gerçek zamanlı sistemler birazcık kötü taraftadır.

Zor gerçek zamanlı dağıtılmış sistemleri geliştirme bir nedenden dolayı tuhaftır: istek/yanıt ağ iletişimi. TCP/IP, DNS, soketler veya diğer bu tür protokollerin asıl detaylarından bahsetmiyoruz. Bu konuları anlamak zor olabilir. Ancak, bu konular bilişimdeki diğer sorunlara benzemektedirler.

Zor gerçek zamanlı dağıtılmış sistemleri zorlaştıran şey ağın bir hata etki alanından diğerine mesaj yollanmasını sağlamasıdır. Bir mesaj göndermek zararsız görünebilir. Aslında, mesaj göndermek ile her şey normalden daha karmaşık hale gelmeye başlar.

Basit bir örnek olarak, aşağıdaki Pac-Man uygulamasından bir kod parçacığına bakın. Tek bir makinede çalıştırılması amaçlandığın için hiçbir ağdan bir mesaj göndermez.

board.move(pacman, user.joystickDirection())
ghosts = board.findAll(":ghost")
for (ghost in ghosts)
  if board.overlaps(pacman, ghost)
    user.slayBy(":ghost")
    board.remove(pacman)
    return

Şimdi bu kodun, pano nesnesinin durumunun başka bir sunucuda sağlandığı ağa bağlı bir sürümünü geliştirdiğimizi varsayalım. Pano nesnesine yapılan findAll() gibi her çağrı, iki sunucu arasında mesaj gönderme ve alma ile sonuçlanır.

İki sunucu arasında her istek/yanıt mesajı gönderildiğinde, aynı dizinden en az sekiz adım her zaman gerçekleşmek zorundadır. Ağa bağlı Pac-Man kodunu anlamak için istek/yanıt mesajlaşmasının temel özelliklerini gözden geçirelim. 

Ağ üzerinden istek/yanıt mesajlaşması

Bir istek/yanıt gönderip alma eylemi her zaman aynı adımları içerir. Aşağıdaki diyagramda gösterildiği üzere; istemci makine CLIENT ağ NETWORK üzerinden sunucu makinesine SERVER’a bir istek MESSAGE gönderir. Sunucu makinesi ise bir mesaj REPLY ile yine ağ NETWORK üzerinden yanıt verir.

Her şeyin yolunda gittiği olumlu senaryoda aşağıdaki adımlar gerçekleşir:

1. POST REQUEST: CLIENT, NETWORK’e istek MESSAGE koyar.
2. DELIVER REQUEST: NETWORK SERVER’a MESSAGE iletir.
3. VALIDATE REQUEST: SERVER MESSAGE’ı doğrular.
4. UPDATE SERVER STATE: SERVER MESSAGE’a göre gerekirse durumunu günceller.
5. POST REPLY: SERVER yanıtı REPLY NETWORK’e koyar.
6. DELIVER REPLY: NETWORK CLIENT’a YANIT iletir.
7. VALIDATE REPLY: CLIENT REPLY’ı doğrular.
8. UPDATE CLIENT STATE: CLIENT REPLY’a göre gerekirse durumunu günceller.

Bu adımlar değersiz bir istek/yanıt döngüsü için çok fazla! Yine de, bu adımlar ağ üzerinden istek/yanıt iletişiminintanımıdır.Birini bile atlamak söz konusu değildir. Örneğin 1. adımı atlamak imkansızdır. İstemci, MESSAGE’ı bir şekilde ağa NETWORK’e koymak zorundadır. Bu, fiziksel olarak elektrik sinyallerinin CLIENT ve SERVER arasındaki ağı oluşturan bir dizi yönlendirici aracılığıyla kablolar üzerinden yolculuk etmesine yol açan ağ bağdaştırıcısı aracılığıyla paket gönderilmesi anlamına gelir. Bu durum 2. adımdan farklıdır çünkü 2. adım bağımsız nedenlerden dolayı başarısız olabilir. Örneğin, aniden kesilen ve gelen paketleri kabul edemeyen SERVER. Aynı mantık kalan diğer adımlara da uygulanabilir.

Bu yüzden, ağ üzerinden tek bir istek/yanıt bir parçayı (yöntemi arayarak) sekiz parçaya ayırır. Daha da kötüsü, yukarıda belirtildiği gibi CLIENT, SERVER, ve NETWORK birbirinden bağımsız olarak başarısız olabilir. Mühendislerin kodları yukarıda belirtilen herhangi bir adımın başarısız olmasıyla başa çıkmalıdır. Bu, tipik mühendislikte nadiren geçerli olur. Nedenini öğrenmek için kodun tekli makine sürümünün aşağıdaki ifadesini inceleyelim.

board.find("pacman")

Teknik olarak, board.find’ın uygulaması hatasız olsa bile bu kodun çalıştığı sırada tuhaf şekillerde bozulabilir. Örneğin, CPU çalışma zamanında aniden fazla ısınabilir. Aynı zamanda makinenin güç kaynağı da aniden bozulabilir. Çekirdek panikleyebilir. Hafıza dolabilir ve board.find’ın oluşturmaya çalıştığı bazı nesneler oluşmayabilir. Veya, makinenin üzerinde çalıştığı disk dolabilir ve board.find bazı istatistik dosyalarını güncellemeyebilir ve bunun sonucunda hata vermemesi gerekirken hata verebilir. Sunucuya biraz da RAM’e bir gama ışını vurabilir. Ancak çoğu zaman mühendisler bu konular için endişelenmezler. Örneğin, birim testleri hiçbir zaman “CPU bozulursa ne olur” senaryosunu içermez ve çok nadiren yetersiz bellek senaryosunu içerirler.

Tipik mühendislikte bu tür hatalar tek bir makinede meydana gelir. Yani, tek hata etki alanı. Örneğin, CPU aniden yandığı için board.find yöntemi başarısız olursa bütün makinenin bozulduğunu düşünmek doğru olur. Kavramsal olarak bile bu hatayla başa çıkmak mümkün değil. Daha önce belirtilen diğer hata türleri hakkında da benzer çıkarımlar yapılabilir. Bu durumların bazıları için testler yazmaya çalışabilirsiniz ancak tipik mühendisliğin bu durumda pek bir anlamı yoktur. Bu hatalar gerçekleşirse kalan diğer her şeyin de bozulacağını düşünmek doğru olur. Teknik olarak, hepsinin aynı kaderi paylaştığınısöyleyebiliriz. Aynı kaderi paylaşmak, mühendislerin başa çıkması gereken farklı hata modlarını son derece azaltır.

Zor gerçek zamanlı dağıtılmış sistemlerde hata modlarıyla başa çıkmak

Zor gerçek zamanlı dağıtılmış sistemlerde çalışan mühendisler ağ hatasının tüm açılarını test etmelidir çünkü sunucular ve ağ aynı kaderi paylaşmaz. Tek makine vakasından faklı olarak, ağ bozulursa istemci makine çalışmaya devam edecektir. Uzak makine bozulursa istemci makine çalışmaya devam edecektir ve bu böyle devam edecektir.

Daha önce bahsedilen istek/yanıt adımlarının hata olasılıklarını etraflıca test etmek için mühendisler her adımın başarısız olabileceğini varsaymak zorundadır. Ve kodun (hem istemci hem de sunucu üzerinde) her zaman bu hatalar ışığında doğru çalıştığından emin olmalıdırlar.
Hadi işlerin yolunda gitmediği bir istek/yanıt eylemine bakalım:

1. POST REQUEST hatası: Bu durumda, ya NETWORK mesajı iletemedi (örneğin, ara yönlendirici tam da yanlış zamanda bozuldu) ya da SERVER iletiyi açık bir şekilde reddetti.
2. DELIVER REQUEST hatası: NETWORK başarılı bir şekilde MESSAGE’ı SERVER’a iletir, ancak SERVER MESSAGE’ı alır almaz bozulur.
3. VALIDATE REQUEST hatası: SERVER MESSAGE’ın geçersiz olduğuna karar verir. Nedeni neredeyse her şey olabilir. Örneğin, ya istemcinin ya da sunucunun üzerinde bozuk paketler, uyumsuz yazılımı sürümü veya hatalar.
4. UPDATE SERVER STATE hatası: SERVER durumunu güncellemeye çalışır ama başarısız olur.
5. POST REPLY hatası: Başarılı bir şekilde veya hatalı bir şekilde yanıt vermeye çalışmasına bakılmaksızın, SERVER yanıtı gönderemeyebilir. Örneğin, ağ kartı tam da yanlış zamanda yanabilir.
6. DELIVER REPLY hatası: NETWORK önceki bir adımda çalışıyor olsa da daha önce açıklandığı gibi CLIENT’a REPLY iletmesi mümkün olmayabilir.
7. VALIDATE REPLY hatası: CLIENT REPLY’ın geçersiz olduğuna karar verir.
8. UPDATE CLIENT STATE hatası: CLIENT ileti REPLY alabilir ancak (uyumsuzluktan dolayı) kendi durumunu güncelleyemeyebilir, mesajı anlamayabilir veya başka bir nedenden dolayı başarısız olabilir.

Dağıtılmış bilişimi zorlaştıran şey bu hata modlarıdır. Ben bunları mahşerin sekiz hata modu olarak adlandırıyorum. Bu hata modlarının ışığında, Pac-Man kodundaki bu ifadeyi yeniden gözden geçirelim.

board.find("pacman")

Bu ifade aşağıdaki istemci taraflı eylemleri içerecek şekilde genişler:

1. Ağ üzerinden Pano makinesine {action: "find", name: "pacman", userId: "8765309"} gibi bir mesaj gönderin.
2. Ağ, uygun değilse veya Pano makinesine bağlantı açık bir şekilde reddediliyorsa hata verir. Bu durum biraz özel çünkü müşteri belirleyici bir şekilde sunucu makinenin isteği almış olmasının mümkün olmayacağını bilir.
3. Yanıt için bekleyin.
4. Eğer hiç yanıt alınamazsa zaman aşımına uğrar. Bu adımda zaman aşımı, isteğin sonucunun UNKNOWN olduğu anlamına gelir. Gerçekleşmiş veya gerçekleşmemiş olabilir. İstemci UNKNOWN ile doğru bir şekilde başa çıkmalıdır.
5. Eğer yanıt alınmışsa başarı yanıtı, hata yanıtı veya anlaşılmaz/bozuk bir yanıt olup olmadığına karar verin.
6. Hata değilse, yanıta unmarshalling uygulanır ve kodun anlayabileceği bir nesneye dönüştürülür.
7. Eğer hata veya anlaşılmaz bir yanıt ise bir istisna yapın.
8. İstisnayı işleyen birim, isteğin yeniden mi deneneceğineya da vazgeçip oyunu mu durduracağına karar vermelidir.

İfade aynı zamanda aşağıdaki sunucu taraflı eylemleri de başlatır:

1. İsteği alın (bu hiçbir zaman gerçekleşmeyebilir).
2. İsteği doğrulayın.
3. Kullanıcının hâlâ yaşayıp yaşamadığını kontrol edin. (Sunucu, uzun zamandır kullanıcıdan mesaj almadığı için ondan vazgeçmiş olabilir.)
4. Sunucunun, kullanıcının hala (muhtemelen) orada olduğunu bilmesi için kullanıcı için hayatta tutma tablosunu güncelleyin.
5. Kullanıcının konumunu kontrol edin.
6. {xPos: 23, yPos: 92, clock: 23481984134} gibi bir mesaj içeren yanıt gönderin.
7. Her türlü sunucu mantığı istemcinin sonraki etkilerini doğru bir şekilde işlemelidir. Örnek olarak, mesajı alamamak, mesajı almak ama anlamamak, mesajı almak ve kilitlenmek veya başarılı bir şekilde işlemek verilebilir.

Kısacası zor gerçek zamanlı dağıtılmış sistemler kodunda, normal kodlu bir ifade on beş ekstra adıma dönüşür. Bu genişleme, istemci ve sunucu arasındaki her iletişim döngüsünün başarısız olabileceği sekiz farklı noktadan kaynaklanır. board.find(“pacman”) gibi ağ üzerinde bir döngüyü temsil eden tüm ifadeler aşağıdaki gibi sonuçlanır.

(error, reply) = network.send(remote, actionData)
switch error
  case POST_FAILED:
    // handle case where you know server didn't get it
  case RETRYABLE:
    // handle case where server got it but reported transient failure
  case FATAL:
    // handle case where server got it and definitely doesn't like it
  case UNKNOWN: // i.e., time out
    // handle case where the *only* thing you know is that the server received
    // the message; it may have been trying to report SUCCESS, FATAL, or RETRYABLE
  case SUCCESS:
    if validate(reply)
      // do something with reply object
    else
      // handle case where reply is corrupt/incompatible

Bu karmaşıklık kaçınılmazdır. Eğer kod tüm vakaları doğru işlemezse hizmet eninde sonunda tuhaf şekillerde başarısız olacaktır. Pac-Man örneği gibi bir istemci/sunucu sisteminin karşılaşabileceği tüm hata modları için testler yazmayı hayal edin!

Zor gerçek zamanlı dağıtılmış sistemleri test etme

Pac-Man kod parçacığının tek makine sürümünü test etmek nispeten basittir. Farklı bazı Pano nesneleri oluşturun, bunları farklı durumlara koyun, farklı durumlarda olan Kullanıcı nesneleri oluşturun ve böyle devam edin. Mühendisler uç koşullar hakkında çok iyi düşünürler ve belki üretici test etme işlemi veya fuzzer kullanırlar.

Pac-Man kodunda, pano nesnesinin kullanıldığı dört yer vardır. Dağıtılmış Pac-Man kodunda, daha önce gösterildiği gibi beş farklı olası sonucun olduğu (POST_FAILED, RETRYABLE, FATAL, UNKNOWN veya SUCCESS) dört nokta vardır. Bunlar, testlerin durum alanını olağanüstü bir şekilde çoğaltır. Örneğin, zor gerçek zamanlı dağıtılmış sistemlerin mühendisleri birçok permütasyonla uğraşmak zorundadır. Diyelim ki board.find() çağrısı POST_FAILED ile başarısız oldu. Bu durumda RETRYABLE ile başarısız olduğunda ne olacağını test etmeniz gerekir, daha sonra FATAL ile başarısız olduğunda ne olacağını test etmeniz gerekir, ve bu böyle devam etmelisiniz.

Fakat bu test bile yetersizdir. Tipik kodda, mühendisler board.find() çalışırsa ve sonrasında panoya yapılan bir sonraki çağrının da, board.move()’un da çalışacağını varsayabilir. Zor gerçek zamanlı dağıtılmış sistemler mühendisliğinde böyle bir garanti yoktur. Sunucu makine bağımsız olarak herhangi bir zamanda çalışmayabilir. Bunun sonucu olarak, mühendisler panoya yapılan her çağrı için beş durum için de test yazmak zorundadır. Diyelim ki bir mühendis Pac-Man’in tekli makine sürümünde test etmek için 10 senaryo geliştirdi. Fakat bu dağıtılmış sistemler sürümünde bu senaryoların her birini 20 kez test etmek zorundalar. Bu da, test matrisinin 10’dan 200’e arttığı anlamına geliyor!

Ama bekleyin, bir şey daha var. Mühendis aynı zamanda sunucu koduna da sahip olabilir. Herhangi bir istemci, ağ ve sunucu taraflı hata kombinasyonu gerçekleşirse gerçekleşsin istemci ve sunucunun hatalı bir durumda kalmaması için hepsini test etmek zorundalar. Sunucu kodu aşağıdaki gibi görünebilir.

handleFind(channel, message)
  if !validate(message)
    channel.send(INVALID_MESSAGE)
    return
  if !userThrottle.ok(message.user())
    channel.send(RETRYABLE_ERROR)
    return
  location = database.lookup(message.user())
  if location.error()
    channel.send(USER_NOT_FOUND)
    return
  else
    channel.send(SUCCESS, location)

handleMove(...)
  ...

handleFindAll(...)
  ...

handleRemove(...)
  ...

Test edilmesi gereken dört adet sunucu taraflı işlev vardır. Her işlevin tek bir makinede beşer tane testi olduğunu varsayalım. Bu 20 adet test olduğunu gösterir. İstemciler aynı sunucuya birden fazla mesaj gönderdiğinden dolayı sunucunun güçlü kalmasını sağlamak için testler farklı isteklerin dizilimlerini simüle etmelidir. İsteklere örnek olarak bul, taşı, sil ve tümünüBul verilebilir.

Diyelim ki bir yapının, her senaryoda ortalama üç çağrı olmak üzere 10 farklı senaryosu var. Bu 30 test daha var demektir. Ancak bir senaryo aynı zamanda hata vakalarını da test etmelidir. Bu testlerin her biri için, istemci dört hata türünden herhangi birini aldığında (POST_FAILED, RETRYABLE, FATAL ve UNKNOWN) ve sonra geçersiz bir istek ile sunucuyu tekrar çağırdığında ne olduğunu simüle etmeniz gerekir. Örneğin, bir istemci başarılı bir şekilde bul’u çağırabilir ama daha sonra taşı dediğinde bazen UNKNOWN yanıtını alabilir. Sonra tekrar herhangi bir nedenden dolayı bul’u çağırabilir. Sunucu bu durumu doğru işliyor mu? Muhtemelen, ancak bunu test etmeden bilemezsiniz. Bu yüzden istemci taraflı kodda olduğu gibi, sunucu tarafında da test matrisi karmaşıklaşır.

Yabancı bilinmeyenler ile başa çıkma

Özellikle birden fazla istekler üzerine bir dağıtılmış sistemin karşılaşabileceği tüm hata permütasyonlarını düşünmek akıllara durgunluk verir. Dağıtılmış sistemlere yaklaşmanın bir yolunun hiçbir şeye güvenmemek olduğunu öğrendik. Ağ iletişimine neden olmadığı sürece, hiçbir kod satırı yapması gereken şeyi yapmayabilir.

Belki de başa çıkması en zor şey, daha önceki bölümde belirtilen UNKNOWN hata türüdür. İstemci her zaman isteğin başarılı olup olmadığını bilemez. Belki de Pac-Man’ı taşıdı (veya bir bankacılık hizmetinde kullanıcının banka hesabından para çekti), veya taşımadı. Mühendisler bu durumlarla nasıl başa çıkmalıdır? Bu durum çok zor çünkü mühendisler birer insan ve insanlar gerçek belirsizlikle boğuşmaya eğilimliler. İnsanlar aşağıdaki gibi kodları gözden geçirmeye alışkınlar.

bool isEven(number)
  switch number % 2
    case 0
      return true
    case 1
      return false

İnsanlar bu kodu anlayabiliyorlar çünkü bu kod ne yapıyor gibigörünüyorsa onu yapıyor. İnsanlar bu kodun, işin bir kısmını bir hizmete dağıtan dağıtılmış sürümüyle ilgili zorluk yaşarlar.

bool distributedIsEven(number)
  switch mathServer.mod(number, 2)
    case 0
      return true
    case 1
      return false
    case UNKNOWN
      return WHAT_THE_FARG?

UNKNOWN’u doğru bir şekilde işlemeyi çözmek bir insan için neredeyse imkansızdır. UNKNOWN aslında ne anlama gelir? Kod yeniden denemeli midir? Eğer öyleyse kaç kez denemelidir? Yeniden denemeler arasında ne kadar beklemelidir? Kodun yan etkileri olduğunda durum daha da kötüleşir. Aşağıdaki örnekte gösterildiği gibi, tek bir makinede çalışan bir bütçelendirme uygulamasının içerisinde, bir hesaptan para çekmek kolaydır.

class Teller
  bool doWithdraw(account, amount)
    switch account.withdraw(amount)
      case SUCCESS
        return true
      case INSUFFICIENT_FUNDS
        return false

Ancak bu uygulamanın dağıtılmış sürümü UNKNOWN nedeniyle tuhaftır.

class DistributedTeller
  bool doWithdraw(account, amount)
    switch this.accountService.withdraw(account, amount)
      case SUCCESS
        return true
      case INSUFFICIENT_FUNDS
        return false
      case UNKNOWN
        return WHAT_THE_FARG?

UNKNOWN hata türünün nasıl işleneceğini çözmeye çalışmak, dağıtılmış mühendislikte işlerin her zaman göründüğü gibi olmadığının bir kanıtıdır.

Düzinelerce zor gerçek zamanlı dağıtılmış sistem

Mahşerin sekiz hata modu, bir dağıtılmış sistem içerisindeki soyutlamanın herhangi bir düzeyinde gerçekleşebilir. Bir önceki örnek bir tek istemci makine, bir ağ ve bir tek sunuculu makineyle sınırlandırılmıştı. Bu basitleştirilmiş senaryoda bile hata durumu matrisi karmaşıklığa boğuldu. Gerçek dağıtılmış sistemler tek istemci makine örneğinden daha karmaşık hata durum matrislerine sahiptir. Gerçek dağıtılmış sistemler birden fazla soyutlama düzeyinde izlenebilen birden fazla makine içerir:

1. Tek makineler
2. Makine grupları
3. Makine gruplarının grupları
4. ve benzerleri (muhtemelen)

Örneğin, AWS üzerine kurulmuş bir hizmet belli bir Erişilebilirlik Alanının içerisinde olan kaynakları işlemeye yönelik makineleri gruplandırabilir. Diğer iki Erişilebilirlik Alanını işleyen iki adet daha makine grubu olabilir. Daha sonra bu gruplar, bir AWS Bölgesi grubu olarak gruplandırılabilir. Bu Bölge grubu (mantıklı olarak) diğer Bölge gruplarıyla iletişim kurabilir. Maalesef, bu daha yüksek, daha mantıklı derecede bile, aynı sorunlar geçerlidir.

Bir hizmetin birkaç sunucuyu GROUP1 isimli bir tek mantıksal gruba topladığını varsayalım. GROUP1 grubu bazen GROUP2 isimli bir başka grup sunucusuna mesajlar gönderebilir. Bu bir tekrarlamalı dağıtılmış mühendislik örneğidir. Daha önce bahsedilen tüm ağ iletişimi hata modları burada geçerli olabilir. Diyelim ki GROUP1 GROUP2’ye bir istek göndermek istiyor. Aşağıdaki diyagramda gösterildiği gibi, iki makineli istek/yanıt etkileşimi, daha önce bahsedilen tek makine ile aynıdır.

Öyle ya da böyle GROUP1 içerisindeki bazı makineler AĞ üzerinden GROUP2’ye yönelik (mantıklı olarak) bir mesaj göndermek zorundadır. GROUP2 içerisindeki bazı makineler isteği işlemek zorundadır ve bu böyle devam eder. GROUP1 ve GROUP2’nin makine gruplarından oluşması temel bilgileri değiştirmez. GROUP1, GROUP2 ve NETWORK birbirinden bağımsız olarak hala başarısız olabilir.

Ancak bu sadece grup düzeyinden bakış açısı. Her grup içerisinde aynı zamanda makineden makineye düzeyinde etkileşim vardır. Örneğin, GROUP2 aşağıdaki diyagramda gösterildiği gibi yapılandırılmış olabilir.

Öncelikle yük dengeleyici aracılığıyla GROUP2’ye yönelik bir mesaj grup içerisindeki bir makineye (muhtemelen S20) gönderilir. Sistemin tasarımcıları S20’nin UPDATE STATE aşamasında başarısız olabileceğini bilir. Bunun sonucu olarak, S20’nin mesajı en azından bir başka makineye, eşlerinin herhangi birine veya başka bir gruptaki bir makineye göndermesi gerekebilir. S20 bunu aslında nasıl yapar? Aşağıdaki diyagramda gösterildiği gibi diyelim ki S25’e bir istek/yanıt mesajı göndererek.

Bu yüzden S20 ağ iletişimini tekrarlamalı olarak gerçekleştirir. Bununla birlikte, yine bağımsız olarak sekiz hata oluşabilir. Dağıtılmış mühendislik bir kez yerine iki kez gerçekleşir. GROUP1’in GROUP2’ye mesajı, mantıksal çerçevede, her sekiz şekilde de başarısız olabilir. Bu mesaj başka bir mesaj ile sonuçlanır. Bunun kendisi de bağımsız olarak daha önce bahsedilen sekiz şekilde de başarısız olabilir. Bu senaryoyu test etmek en azından aşağıdakileri içerir:

• GROUP1’den GROUP2’ye grup düzeyinde mesajlaşmanın başarısız olabileceği sekiz yol için bir test.
• S20’den S25’e sunucu düzeyinde mesajlaşmanın başarısız olabileceği sekiz yol için bir test.

Bu istek/yanıt mesajlaşma örneği dağıtılmış sistemlerin, onlarla 20 yıllık bir deneyimden sonra bile özellikle can sıkıcı bir sorun olarak kalmasının nedenini gösteriyor. Uç vakaların sonsuzluğunu göz önünde bulundurursak test yapmanın çok zor olduğunu anlıyoruz ama bu sistemlerde de özellikle önemlidir. Sistemler dağıtıldıktan sonra hataların ortaya çıkması uzun zaman alabilir. Ve hataların sisteme ve bitişik sistemlere tahmin edilemeyecek kadar kapsamlı bir etkisi vardır.

Dağıtılmış hatalar çoğu zaman gizlidir

Eğer eninde sonunda bir hata gerçekleşecekse genel tecrübe geç olmasından çok erken olmasından yanadır. Örneğin, düzeltmesi altı ay sürecek bir hizmetteki ölçeklendirme hatasını, en azından söz konusu hizmetin böylesine bir ölçeklendirmeye ulaşmasına gerek kalmadan en az altı ay öncesinden bulmak daha iyidir. Benzer şekilde, üretime başlamadan önce hataları bulmak daha iyidir. Eğer hatalar üretime başlarsa onları, çok fazla müşteriyi etkilemeden veya başka yan etkilere sebep olmadan hızlı bir şekilde bulmak daha iyi olacaktır.

Mahşerin sekiz hata modunun tüm permütasyonlarını işleyememekten kaynaklandığı anlamına gelen dağıtılmış hatalar çoğunlukla vahimdir. Telekomünikasyon sistemlerinden çekirdek internet sistemlerine kadar, büyük dağıtılmış sistemlerde örnekler zaman içerisinde çoğalır. Bu yaygın ve pahalı kesintiler aylar öncesinden üretim için dağıtılan hatalardan kaynaklanabilir. Sonrasında, aslında bu hataların gerçekleşmesine (ve tüm sisteme yayılmasına) neden olan senaryo kombinasyonlarını tetiklemek biraz zaman alır.

Dağıtılmış hatalar epidemik olarak yayılırlar

Dağıtılmış hataların temeli olan bir başka sorunu size anlatayım.

1. Dağıtılmış hatalar zorunlu olarak ağ kullanımını içerirler.
2. Bu yüzden dağıtılmış hataların diğer makinelere (veya makine gruplarına) yayılması daha olasıdır çünkü tabiatı gereği bu hatalar halihazırda, makineleri birbirine bağlayan tek şeyi içerirler.

Amazon da bu dağıtılmış hataları deneyimledi. Eski ama ilişkili bir örnek olarak, www.amazon.com’da site genelinde yaşanan bir hata verebilir. Hata, tek bir sunucunun diski dolduğu için uzaktan katalog hizmetinde hata vermesinden kaynaklanmıştı.

Bu hata koşulunun kötü idare edilmesinden dolayı uzaktan katalog sunucusu, aldığı tüm isteklere boş yanıtlar göndermeye başlamıştı. Aynı zamanda çok hızlı bir şekilde yanıt veriyordu. Çünkü hiçbir şeyi göndermek bir şeyi göndermekten çok daha hızlıdır (en azından bu durumda öyleydi). Bu sırada, web sitesi ve uzaktan katalog hizmeti arasındaki yük dengeleyici, yanıtların sıfır uzunlukta olduğunu fark etmedi. Fakat diğer tüm uzak katalog sunucularından bariz bir şekilde daha hızlı olduğunufark etti. Bu yüzden www.amazon.com’dan diski dolu olan uzaktan katalog sunucusuna çok büyük bir miktarda trafik gönderdi. Etkili bir şekilde, bir uzaktan sunucu hiçbir ürün bilgisi gösteremediği için bütün web sitesi çöktü.

Bozuk sunucuyu hemen bulduk ve web sitesini geri yüklemek için hizmetten kaldırdık. Daha sonra bu durumun tekrarlanmasını önlemek için her zamanki temel nedenleri belirleme ve sorunları tanımlama sürecimizi takip ettik. Bu dersleri, diğer sistemlerin aynı sorunu yaşamasını önlemek için Amazon’da paylaştık. Bu hata modu hakkındaki özel dersleri öğrenmenin yanı sıra, bu olay, dağıtılmış sistemlerde hata modlarının nasıl hızlı ve öngörülemez bir şekilde yayıldığını görmek için güzel bir örnek oldu.

Dağıtılmış sistemlerdeki sorunların özeti

Kısacası dağıtılmış sistemler mühendisliği zordur çünkü:

• Mühendisler hata koşullarını birleştiremezler. Bunun yerine hataların birçok permütasyonunu düşünmeliler. Çoğu hata, diğer hata koşullarından bağımsız olarak (ve bu yüzden) diğer herhangi bir hata durumuyla (birlikte) gerçekleşebilir.
• Herhangi bir ağ işleminin sonucu UNKNOWN olabilir; bu durumda istek başarılı, başarısız veya alınmış ama işlenmemiş olabilir.
• Dağıtılmış hatalar sadece düşük düzey fiziksel makinelerde değil dağıtılmış sistemlerin her mantıksal düzeyinde gerçekleşebilir.
• Dağıtılmış hatalar yinelemeden dolayı sistemin yüksek düzeylerinde daha da kötüleşir.
• Dağıtılmış hatalar, sisteme dağıtıldıktan çok sonra ortaya çıkarlar.
• Dağıtılmış hatalar tüm bir sistemde yayılabilirler.
• Yukarıda sayılan problemlerin birçoğu değiştirilemez olan ağ iletişimi fiziğinin kanunlarından kaynaklanır.

Dağıtılmış bilişimin zor ve tuhaf olması, bu problemlerle başa çıkmanın yolları olmadığı anlamına gelmez. Amazon Derleyici Kitaplığı boyunca AWS’nin dağıtılmış sistemleri nasıl yönettiğini ayrıntılarıyla inceliyoruz. Umuyoruz ki, siz kendi müşterileriniz için oluştururken bizim öğrendiklerimizden birazını da olsa değerli bulursunuz.


Yazar hakkında

Jacob Gabrielson, Amazon Web Services'ta Kıdemli Baş Mühendistir. Başta dahili mikro hizmet platformları olmak üzere 17 yıldır Amazon'da çalışmaktadır. Son 8 yıldır; yazılım dağıtım sistemleri, denetim düzlemi hizmetleri, Spot market, Lightsail ve son zamanlarda container'lar dahil olmak üzere EC2 ve ECS üzerinde çalışmaktadır. Jacob'ın tutkuları; sistem programlama, programlama dilleri ve dağıtılmış bilişim üzerinedir. En sevmediği şey, özellikle hata durumları altında çift modlu sistem davranışıdır. Seattle'daki Washington Üniversitesi'nden Bilgisayar Bilimi lisans derecesine sahiptir.

Zaman aşımları, yeniden denemeler ve sapmalı geri çekilme Dağıtılmış sistemlerde geri dönüşten kaçınma