Takip et

Elixir’de Dağıtık Sistemler Oluşturma: Bölüm 5 — Supervisor’ı Sıfırdan İnşa Etmek

Dağıtık sistemler geliştirirken, beklenmedik hataların ve sistem kesintilerinin önüne geçmek kritik bir öneme sahiptir.

Elixir’de Dağıtık Sistemler Oluşturma: Bölüm 5 — Supervisor’ı Sıfırdan İnşa Etmek

Dağıtık sistemler geliştirirken, beklenmedik hataların ve sistem kesintilerinin önüne geçmek kritik bir öneme sahiptir. Elixir, bu tür zorluklarla başa çıkmak için Erlang’ın sağlam OTP (Open Telecom Platform) yapısını miras almıştır ve bu yapının temel taşlarından biri de Supervisor’lardır. Peki, uygulamanızdaki bir sürecin aniden çökmesi durumunda ne olur? Ya da bir ödeme işleminin tam ortasında harici bir servise bağlantı kesilirse sisteminiz nasıl tepki verir? Bu makalede, Elixir’in Supervisor mekanizmasını sıfırdan inşa ederek, hata toleranslı ve kendiliğinden iyileşen dağıtık sistemlerin nasıl kurulacağını adım adım inceleyeceğiz.

Elixir ve Dağıtık Sistemlerde Güvenilirlik Neden Önemli?

Günümüzün sürekli çalışan ve yüksek erişilebilirlik beklentisi olan uygulamalarında, sistemlerin hatalara karşı dayanıklı olması bir lüks değil, bir zorunluluktur. Bir e-ticaret sitesinde ödeme işlemini gerçekleştiren bir mikroservisin anlık bir hata nedeniyle çökmesi, sadece kullanıcı deneyimini olumsuz etkilemekle kalmaz, aynı zamanda ciddi finansal kayıplara da yol açabilir. Geleneksel programlama dillerinde bu tür durumlar genellikle karmaşık hata yakalama blokları ve manuel yeniden başlatma mekanizmalarıyla yönetilmeye çalışılırken, Elixir ve Erlang dünyasında bu sorunlara çok daha zarif ve etkili bir çözüm sunulur: OTP.

OTP, özellikle telekomünikasyon sektöründeki yüksek erişilebilirlik gereksinimleri için tasarlanmış bir dizi kütüphane ve tasarım ilkesidir. “Let it crash” (Bırak çöksün) felsefesi, OTP’nin temelini oluşturur. Bu felsefe, hataları karmaşık bir şekilde düzeltmeye çalışmak yerine, hatanın olduğu süreci güvenli bir şekilde sonlandırıp, temiz bir başlangıçla yeniden ayağa kaldırmanın daha güvenli ve pratik olduğunu savunur. İşte bu noktada Supervisor’lar devreye girer. Supervisor, belirli bir sorumluluğu olan çocuk süreçleri (child processes) izleyen ve bu süreçler çöktüğünde önceden tanımlanmış stratejilere göre onları yeniden başlatan özel bir GenServer’dır. Bu sayede, uygulamanızın genel durumu bozulmadan, hatalı süreçler izole edilir ve sistem kendiliğinden iyileşir. Bu mekanizma, özellikle ağ bağlantısı kesintileri, veritabanı hataları veya üçüncü taraf API’lerinden kaynaklanan geçici sorunlar gibi dağıtık sistemlerde sıkça karşılaşılan durumlar için hayati önem taşır. Elixir’in hafif süreçleri ve mesajlaşma tabanlı eşzamanlılık modeliyle birleşen Supervisor’lar, karmaşık dağıtık sistemleri basit, anlaşılır ve bakımı kolay bir şekilde inşa etmenizi sağlar.

Supervisor Nedir ve Nasıl Çalışır?

Elixir’deki Supervisor (Denetleyici), Erlang/OTP’nin kalbinde yer alan ve uygulamalarınızın hata toleranslı olmasını sağlayan temel bir bileşendir. Özünde, Supervisor bir veya daha fazla “çocuk süreci” (child processes) yöneten ve izleyen özel bir GenServer’dır. Bu çocuk süreçler, uygulamanızın belirli işlevlerini yerine getiren diğer GenServer’lar, Task’lar veya hatta başka Supervisor’lar olabilir. Supervisor’ın ana görevi, çocuk süreçlerinden herhangi biri beklenmedik bir şekilde sonlandığında (çöktüğünde), önceden tanımlanmış bir stratejiye göre onu otomatik olarak yeniden başlatmaktır. Bu sayede, uygulamanızın genel stabilitesi korunur ve tek bir sürecin hatası tüm sistemi çökertmez.

Bir Supervisor’ı tanımlarken, yöneteceği çocuk süreçleri ve her çocuk için bir “çocuk belirtimi” (child specification) sağlamanız gerekir. Çocuk belirtimi, sürecin modülünü, başlangıç argümanlarını ve en önemlisi, nasıl başlatılacağını ve yeniden başlatılacağını belirten meta verileri içerir. Supervisor’lar, çocuk süreçlerini izlerken farklı yeniden başlatma stratejileri kullanabilirler:

  • :one_for_one (Bire Bir): En yaygın stratejidir. Yalnızca çöken çocuk süreci yeniden başlatır. Diğer çocuk süreçler etkilenmez. Bu, bağımsız çalışan süreçler için idealdir.
  • :one_for_all (Hepsi İçin Bir): Bir çocuk süreci çöktüğünde, Supervisor tüm çocuk süreçleri (kendisi dahil) sonlandırır ve ardından hepsini baştan başlatır. Bu strateji, süreçlerin birbirine sıkıca bağlı olduğu ve birinin çökmesinin diğerlerini de geçersiz kıldığı durumlar için uygundur. Örneğin, bir veritabanı bağlantısı yöneticisi çökerse, ona bağlı tüm iş süreçlerinin de yeniden başlatılması gerekebilir.
  • :rest_for_one (Kalanlar İçin Bir): Bir çocuk süreci çöktüğünde, Supervisor sadece çöken süreçten sonra başlatılan tüm çocuk süreçleri sonlandırır ve yeniden başlatır. Çöken sürecin kendisi de dahil olmak üzere bu süreçler yeniden başlatılır. Bu strateji, süreçlerin belirli bir başlatma sırasına sahip olduğu ve sonraki süreçlerin önceki süreçlere bağımlı olduğu durumlar için kullanışlıdır.
  • :simple_one_for_one (Basit Bire Bir): Dinamik olarak çocuk süreci eklemek için kullanılır. Bu Supervisor, başlangıçta hiçbir çocuk süreci başlatmaz. Çocuk süreçler, Supervisor’a bir mesaj gönderilerek çalışma zamanında (runtime) başlatılır. Bu, örneğin her yeni kullanıcı bağlantısı için ayrı bir süreç başlatmanız gereken durumlarda çok faydalıdır.

Supervisor, bu stratejileri uygulayarak, uygulamanızın hatalara karşı otomatik olarak tepki vermesini ve kendini onarmasını sağlar. Bu, geliştiricilerin hata yönetimiyle ilgili karmaşık mantık yazmak yerine, iş mantığına odaklanmasına olanak tanır. Bir Supervisor’ın yaşam döngüsü, bir OTP uygulamasının başlangıcından itibaren başlar ve uygulamanın kapanmasına kadar devam eder, böylece tüm çocuk süreçlerinin sürekli olarak denetlenmesini sağlar.

Sıfırdan Bir Supervisor Oluşturma Adımları

Şimdi, Elixir’de kendi Supervisor’ımızı nasıl oluşturacağımızı adım adım inceleyelim. Bu örnekte, basit bir sayaç GenServer’ı yönetecek bir Supervisor tasarlayacağız. Sayaç, her çağrıldığında değerini artıran ve bir hata simüle edebilen bir süreç olacak.

Adım 1: Çocuk Süreci (GenServer) Tanımlama

Öncelikle, Supervisor’ımızın yöneteği basit bir GenServer oluşturalım. Bu GenServer, bir sayacı tutacak ve bir hata simüle etme yeteneğine sahip olacak.


defmodule MyCounter do
  use GenServer

  # GenServer Callbacks

  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  @impl true
  def init(initial_value) do
    IO.puts("MyCounter started with initial value: #{initial_value}")
    {:ok, initial_value}
  end

  @impl true
  def handle_call(:get_value, _from, state) do
    {:reply, state, state}
  end

  @impl true
  def handle_call(:increment, _from, state) do
    new_state = state + 1
    IO.puts("MyCounter incremented to: #{new_state}")
    {:reply, new_state, new_state}
  end

  @impl true
  def handle_call(:crash, _from, state) do
    IO.puts("MyCounter is crashing now!")
    # Hata simülasyonu: Süreci aniden sonlandır
    exit(:boom)
    {:reply, :crashed, state} # Bu satıra asla ulaşılamaz
  end

  @impl true
  def terminate(reason, state) do
    IO.puts("MyCounter terminating. Reason: #{inspect(reason)}, State: #{state}")
    :ok
  end

  # Client API

  def get_value do
    GenServer.call(__MODULE__, :get_value)
  end

  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  def crash do
    GenServer.call(__MODULE__, :crash)
  end
end
  

Bu MyCounter modülü, start_link/1 ile başlatılabilir, get_value/0 ile mevcut değerini sorgulayabilir, increment/0 ile değerini artırabilir ve crash/0 ile kendini kasten çökertir. init/1 ve terminate/2 callback’leri, sürecin yaşam döngüsünü izlememize yardımcı olacak çıktı mesajları içerir.

Adım 2: Özel Supervisor Modülü Uygulama

Şimdi, MyCounter GenServer’ımızı denetleyecek bir Supervisor oluşturalım. Bu Supervisor, Supervisor davranışını kullanacak ve init/1 geri çağırmasında çocuk belirtimlerini tanımlayacaktır.


defmodule MyApp.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    children = [
      # MyCounter'ı bir çocuk süreç olarak tanımla
      # :id, çocuk sürecin benzersiz tanımlayıcısıdır.
      # :start, {Modül, Fonksiyon, Argümanlar} şeklinde başlangıç fonksiyonunu belirtir.
      # :restart, sürecin nasıl yeniden başlatılacağını belirler (:permanent, :transient, :temporary).
      # :shutdown, sürecin nasıl sonlandırılacağını belirler (:brutal_kill, :infinity, veya bir zaman aşımı).
      # :type, sürecin türünü belirler (:worker veya :supervisor).
      %{
        id: MyCounter,
        start: {MyCounter, :start_link, [0]}, # 0 başlangıç değeriyle başlat
        restart: :permanent, # Sürekli yeniden başlat
        shutdown: 5000,      # 5 saniye içinde sonlandır
        type: :worker
      }
    ]

    # Supervisor'ın yeniden başlatma stratejisini belirle
    # Bu örnekte, sadece çöken süreci yeniden başlatacağız.
    opts = [strategy: :one_for_one, name: __MODULE__]

    Supervisor.init(children, opts)
  end
end
  

Bu MyApp.Supervisor modülü, MyCounter‘ı :permanent (kalıcı) bir çocuk olarak tanımlar. Bu, MyCounter her çöktüğünde Supervisor tarafından yeniden başlatılacağı anlamına gelir. :shutdown seçeneği, sürecin nazikçe sonlandırılması için 5000 milisaniye (5 saniye) süre tanır. :one_for_one stratejisi, sadece çöken MyCounter örneğinin yeniden başlatılmasını sağlar.

Adım 3: Supervisor’ı Uygulama Ağacına Ekleme

Bir Elixir uygulamasında, tüm Supervisor’lar ve çocuk süreçler genellikle bir uygulama modülü aracılığıyla yönetilen bir “uygulama ağacı” (application tree) içinde düzenlenir. Bu, sistemin başlatılması ve kapatılması için merkezi bir kontrol noktası sağlar.


defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Uygulama Supervisor'ımızı buraya ekliyoruz
      MyApp.Supervisor
    ]

    # Uygulama ağacını başlat
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
  

Bu MyApp.Application modülü, uygulamanızın ana giriş noktasıdır. start/2 fonksiyonu, MyApp.Supervisor‘ı uygulama ağacının bir parçası olarak başlatır. Bu sayede, uygulamanız başladığında Supervisor da otomatik olarak devreye girer ve MyCounter‘ı denetlemeye başlar.

Adım 4: Çalıştırma ve Davranışı Gözlemleme

Şimdi bu yapıyı çalıştıralım ve davranışını gözlemleyelim. Bir Elixir projesi oluşturduğunuzu varsayarak (mix new my_app --sup), yukarıdaki kodları ilgili dosyalara (lib/my_counter.ex, lib/my_app/supervisor.ex, lib/my_app/application.ex) yerleştirdikten sonra, iex -S mix ile interaktif bir oturum başlatabilirsiniz.

Uygulama başladığında, MyCounter‘ın başlatıldığına dair bir çıktı görmelisiniz:


iex(1)> MyCounter started with initial value: 0
  

Şimdi sayacı artırın ve değerini kontrol edin:


iex(2)> MyCounter.increment()
MyCounter incremented to: 1
1
iex(3)> MyCounter.get_value()
1
  

Ve şimdi MyCounter‘ı kasten çökertelim:


iex(4)> MyCounter.crash()
MyCounter is crashing now!
MyCounter terminating. Reason: :boom, State: 1
MyCounter started with initial value: 0
  

Gördüğünüz gibi, MyCounter çöktüğünde (MyCounter terminating. Reason: :boom, State: 1), Supervisor devreye girer ve onu yeniden başlatır (MyCounter started with initial value: 0). Yeniden başlatılan sayaç, başlangıç değeri olan 0 ile tekrar başlar. Bu, Supervisor’ın hata toleransı sağlama ve sistemin kendiliğinden iyileşme yeteneğinin bir göstergesidir.

Yeniden Başlatma Stratejileri ve Senaryoları

Supervisor’lar, çocuk süreçlerini yönetirken farklı yeniden başlatma stratejileri sunar. Bu stratejileri doğru anlamak ve uygun senaryolarda kullanmak, uygulamanızın güvenilirliğini ve performansını doğrudan etkiler.

:one_for_one (Bire Bir) Stratejisi

Bu, en yaygın ve varsayılan stratejidir. Bir çocuk süreci çöktüğünde, Supervisor sadece o çöken süreci yeniden başlatır. Diğer çocuk süreçler etkilenmez ve çalışmaya devam eder. Bu strateji, süreçlerin birbirinden bağımsız çalıştığı veya birinin çökmesinin diğerlerini doğrudan etkilemediği durumlar için idealdir.

Senaryo: Bir web sunucusunda her gelen HTTP isteği için ayrı bir GenServer (veya Task) başlattığınızı düşünün. Bir isteği işleyen süreç bir hata nedeniyle çökerse, sadece o istekle ilgili sürecin yeniden başlatılması gerekir. Diğer istekleri işleyen süreçlerin etkilenmemesi, genel sistemin kesintisiz çalışmasını sağlar.


# MyApp.Supervisor içindeki children listesi
children = [
  %{
    id: RequestProcessor,
    start: {RequestProcessor, :start_link, []},
    restart: :permanent,
    type: :worker
  },
  %{
    id: DatabaseClient,
    start: {DatabaseClient, :start_link, []},
    restart: :permanent,
    type: :worker
  }
]
opts = [strategy: :one_for_one]
  

Yukarıdaki örnekte, RequestProcessor çökerse, sadece o yeniden başlatılır. DatabaseClient bundan etkilenmez.

:one_for_all (Hepsi İçin Bir) Stratejisi

Bu strateji, bir çocuk süreci çöktüğünde, Supervisor’ın tüm çocuk süreçleri sonlandırıp ardından hepsini baştan başlatmasını sağlar. Bu, süreçlerin birbirine sıkıca bağlı olduğu ve birinin durumunun diğerlerinin durumu için kritik olduğu durumlarda kullanılır.

Senaryo: Bir ödeme ağ geçidi entegrasyonu düşünebiliriz. Bu ağ geçidi, bir HTTP istemcisi, bir kimlik doğrulama belirteci (token) yöneticisi ve bir işlem günlüğü kaydedici gibi birden fazla bileşenden oluşabilir. Eğer kimlik doğrulama belirteci yöneticisi çökerse, HTTP istemcisinin veya işlem günlüğü kaydedicinin güncel olmayan veya geçersiz bir belirteçle çalışması anlamsız hale gelir. Bu durumda, tüm bu bileşenlerin birlikte yeniden başlatılması, sistemin tutarlı bir duruma geri dönmesini sağlar.


# MyApp.PaymentGateway.Supervisor içindeki children listesi
children = [
  %{id: HttpClient, start: {HttpClient, :start_link, []}},
  %{id: TokenManager, start: {TokenManager, :start_link, []}},
  %{id: TransactionLogger, start: {TransactionLogger, :start_link, []}}
]
opts = [strategy: :one_for_all]
  

Bu yapıda, TokenManager çökerse, HttpClient ve TransactionLogger da sonlandırılıp yeniden başlatılır.

:rest_for_one (Kalanlar İçin Bir) Stratejisi

Bir çocuk süreci çöktüğünde, Supervisor o süreçten sonra başlatılan tüm çocuk süreçleri sonlandırır ve yeniden başlatır. Çöken sürecin kendisi de dahil olmak üzere bu süreçler yeniden başlatılır. Bu strateji, süreçlerin belirli bir başlatma sırasına sahip olduğu ve sonraki süreçlerin önceki süreçlere bağımlı olduğu durumlar için idealdir.

Senaryo: Bir veri işleme hattı hayal edin. İlk süreç veriyi alır, ikinci süreç veriyi temizler, üçüncü süreç veriyi analiz eder. Eğer veri temizleme süreci (ikinci süreç) çökerse, ondan sonraki analiz sürecinin çalışmaya devam etmesi anlamsızdır çünkü temizlenmemiş veri üzerinde çalışacaktır. Bu durumda, veri temizleme sürecini ve ondan sonraki tüm süreçleri yeniden başlatmak mantıklıdır. Veri alım süreci (birinci süreç) ise etkilenmeden çalışmaya devam edebilir.


# MyApp.DataPipeline.Supervisor içindeki children listesi
children = [
  %{id: DataIngestor, start: {DataIngestor, :start_link, []}},
  %{id: DataCleaner, start: {DataCleaner, :start_link, []}},
  %{id: DataAnalyzer, start: {DataAnalyzer, :start_link, []}}
]
opts = [strategy: :rest_for_one]
  

Burada, DataCleaner çökerse, DataCleaner ve DataAnalyzer yeniden başlatılır. DataIngestor ise çalışmaya devam eder.

:simple_one_for_one (Basit Bire Bir) Stratejisi

Bu strateji, çalışma zamanında (runtime) dinamik olarak çocuk süreçleri başlatmak için kullanılır. Supervisor başlangıçta hiçbir çocuk süreci başlatmaz. Çocuk süreçler, Supervisor’a bir mesaj gönderilerek (genellikle Supervisor.start_child/2 veya Supervisor.start_child/3 ile) başlatılır.

Senaryo: Bir sohbet uygulaması veya oyun sunucusu gibi bir sistemde, her yeni kullanıcı bağlantısı için ayrı bir süreç başlatmanız gerekebilir. Bu süreçlerin sayısı önceden belli değildir ve dinamik olarak değişir. :simple_one_for_one Supervisor, her yeni bağlantı geldiğinde bir kullanıcı oturumu sürecini başlatmak için mükemmel bir seçimdir.


# MyApp.UserSession.Supervisor içindeki init fonksiyonu
def init(:ok) do
  children = [
    # Çocuk belirtimi, dinamik olarak başlatılacak süreç için bir şablon görevi görür.
    # Argümanlar, start_child çağrısında sağlanır.
    %{
      id: UserSession,
      start: {UserSession, :start_link, []}, # Argümanlar boş bırakılır
      type: :worker,
      restart: :temporary # Oturumlar genellikle geçicidir
    }
  ]
  opts = [strategy: :simple_one_for_one, name: __MODULE__]
  Supervisor.init(children, opts)
end

# UserSession.Supervisor'dan dinamik olarak çocuk başlatma
# Supervisor.start_child(MyApp.UserSession.Supervisor, [user_id, socket_pid])
  

:simple_one_for_one stratejisi, özellikle sunucu tarafında bağlantı yöneticileri veya işleme kuyrukları gibi dinamik kaynakları yönetmek için vazgeçilmezdir. Her stratejinin kendine özgü avantajları ve kullanım alanları vardır. Doğru stratejiyi seçmek, sisteminizin hata toleransı ve kaynak yönetimi açısından ne kadar verimli olacağını belirler.

Gerçek Dünya Uygulamaları: Bir Ödeme Ağ Geçidi Yönetimi

Elixir’deki Supervisor’ların gerçek dünyadaki gücünü anlamak için, bir ödeme ağ geçidi sistemini ele alalım. Bu sistem, farklı bileşenlerin bir araya gelerek çalıştığı ve her birinin hataya dayanıklı olması gereken karmaşık bir yapıdır. Bir ödeme işleminin başarılı bir şekilde tamamlanması, birçok alt sürecin hatasız çalışmasına bağlıdır.

Vaka Analizi: Hata Toleranslı Ödeme İşlemleri

Bir ödeme ağ geçidi, genellikle aşağıdaki ana bileşenleri içerir:

  1. Veritabanı Bağlantısı Yöneticisi (DatabaseConnection): Ödeme bilgilerini, işlem kayıtlarını ve kullanıcı verilerini saklar.
  2. Harici Ödeme Sağlayıcı İstemcisi (PaymentProviderClient): PayPal, Stripe gibi üçüncü taraf ödeme sağlayıcılarıyla iletişim kurar.
  3. İşlem İşleyici (TransactionProcessor): Gelen ödeme taleplerini alır, doğrular ve ödeme sağlayıcıya yönlendirir.
  4. Bildirim Gönderici (NotificationSender): Ödeme başarılı olduğunda veya başarısız olduğunda kullanıcılara e-posta veya SMS bildirimleri gönderir.

Bu bileşenlerin her biri, bir Elixir GenServer’ı olarak uygulanabilir. Şimdi bu bileşenleri bir Supervisor ağacında nasıl yöneteceğimizi tasarlayalım.

Ödeme Ağ Geçidi Supervisor Yapısı

Ana uygulama Supervisor’ımız, bu bileşenleri yönetecek bir alt Supervisor’ı başlatabilir. Bu alt Supervisor’ın kendi içinde farklı yeniden başlatma stratejileri kullanması gerekebilir.


# lib/payment_gateway/supervisor.ex
defmodule PaymentGateway.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    children = [
      # Veritabanı bağlantısı kritik ve diğer tüm süreçler ona bağımlı olabilir.
      # Çökerse, tüm sistemi yeniden başlatmak mantıklı olabilir.
      %{
        id: PaymentGateway.DatabaseConnection,
        start: {PaymentGateway.DatabaseConnection, :start_link, []},
        restart: :permanent,
        shutdown: :infinity, # Veritabanı bağlantısı kritik, sonsuz bekle
        type: :worker
      },
      # Ödeme sağlayıcı istemcisi, veritabanına ve işlemciye bağımlı olabilir.
      # Hata verirse, sadece kendini yeniden başlatması yeterli olabilir.
      %{
        id: PaymentGateway.PaymentProviderClient,
        start: {PaymentGateway.PaymentProviderClient, :start_link, []},
        restart: :permanent,
        shutdown: 15_000, # 15 saniye içinde sonlandır
        type: :worker
      },
      # İşlem işleyici, hem veritabanına hem de ödeme sağlayıcıya bağımlıdır.
      # Önceki süreçler yeniden başlatılırsa, bu da yeniden başlatılmalı.
      %{
        id: PaymentGateway.TransactionProcessor,
        start: {PaymentGateway.TransactionProcessor, :start_link, []},
        restart: :permanent,
        shutdown: 10_000, # 10 saniye içinde sonlandır
        type: :worker
      },
      # Bildirim gönderici, diğerlerinden daha az kritik olabilir ve bağımsız çalışabilir.
      %{
        id: PaymentGateway.NotificationSender,
        start: {PaymentGateway.NotificationSender, :start_link, []},
        restart: :permanent,
        shutdown: 5_000, # 5 saniye içinde sonlandır
        type: :worker
      }
    ]

    # Strateji seçimi: Eğer veritabanı bağlantısı çökerse, tüm ödeme ağ geçidi bileşenlerinin
    # tutarlı bir duruma dönmesi için :one_for_all stratejisi düşünülebilir.
    # Ancak, eğer her bileşen kendi başına hata verebiliyor ve bağımsız olarak iyileşebiliyorsa,
    # :one_for_one daha uygun olabilir. Bu örnekte, genel bir tutarlılık için :one_for_all seçelim.
    # Alternatif olarak, DatabaseConnection için ayrı bir Supervisor ve diğerleri için :one_for_one
    # kullanan bir üst Supervisor da yapılabilir.
    opts = [strategy: :one_for_all, name: __MODULE__]
    Supervisor.init(children, opts)
  end
end
  

Bu PaymentGateway.Supervisor, :one_for_all stratejisi ile yapılandırılmıştır. Bunun anlamı şudur: Eğer PaymentGateway.DatabaseConnection gibi kritik bir bileşen çökerse, tüm ödeme ağ geçidi bileşenleri (istemci, işlemci, bildirim gönderici) de sonlandırılır ve ardından hepsi birlikte yeniden başlatılır. Bu, sistemin tutarlı bir başlangıç durumuna dönmesini sağlar. Eğer sadece PaymentGateway.PaymentProviderClient geçici bir ağ hatası nedeniyle çökerse, yine de tüm diğer bileşenler yeniden başlatılacaktır. Bu, bazı durumlarda gereksiz olabilir ancak sistemin genel tutarlılığını garanti altına alır.

Alternatif bir yaklaşım olarak, PaymentGateway.DatabaseConnection‘ı ayrı bir Supervisor altında :one_for_one stratejisiyle yönetip, diğer bileşenleri de başka bir Supervisor altında :one_for_one ile yönetebiliriz. Bu, daha modüler bir yapı sağlar ve hataların kapsamını daraltır. Ancak, veritabanı bağlantısının kesilmesi durumunda diğer tüm süreçlerin de geçersiz hale geleceği bir senaryoda, :one_for_all daha basit ve güvenli bir çözüm sunar.

Bu vaka analizi, Elixir Supervisor’larının karmaşık dağıtık sistemlerde hata toleransını nasıl sağladığını göstermektedir. Her bileşen, kendi sorumluluğuna odaklanırken, Supervisor onların yaşam döngüsünü denetleyerek sistemin genel kararlılığını garanti altına alır. Bu sayede, geliştiriciler beklenmedik hatalarla başa çıkmak için karmaşık manuel mekanizmalar yazmak zorunda kalmaz, bunun yerine iş mantığına odaklanabilirler.

İleri Düzey Supervisor Kullanımı ve İpuçları

Supervisor’lar, temel yeniden başlatma stratejilerinin ötesinde, daha karmaşık senaryoları yönetmek için de güçlü yetenekler sunar. Deneyimli Elixir geliştiricileri için bazı ileri düzey kullanım ipuçları ve püf noktaları şunlardır:

Dinamik Denetim (Dynamic Supervision)

Daha önce bahsettiğimiz :simple_one_for_one stratejisi, dinamik olarak çocuk süreçleri başlatmak için harikadır. Ancak bazen, belirli bir adla başlatılan süreçleri denetlemeniz veya mevcut bir Supervisor’a çalışma zamanında yeni çocuk belirtimleri eklemeniz gerekebilir. Supervisor.start_child/2 veya Supervisor.start_child/3 fonksiyonları, bu tür dinamik başlatmalar için kullanılır.

Örnek: Bir web soketi sunucusunda, her yeni kullanıcı bağlantısı için ayrı bir oturum süreci başlatmanız gerekebilir. Bu oturum süreçleri, kullanıcı bağlantısı kesildiğinde otomatik olarak sonlandırılmalıdır.


# lib/websocket_manager/supervisor.ex
defmodule WebsocketManager.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    children = [
      # UserSession için bir şablon tanımlıyoruz.
      # start fonksiyonundaki argümanlar boş bırakılır, çünkü bunlar start_child çağrısında sağlanacaktır.
      %{
        id: UserSession,
        start: {UserSession, :start_link, []},
        type: :worker,
        restart: :temporary # Kullanıcı ayrıldığında oturum süreci sonlanmalı
      }
    ]

    opts = [strategy: :simple_one_for_one, name: __MODULE__]
    Supervisor.init(children, opts)
  end

  # Client API: Yeni bir kullanıcı oturumu başlatmak için
  def start_user_session(user_id, socket_pid) do
    # UserSession'ın start_link/2 fonksiyonu için argümanları sağlıyoruz
    Supervisor.start_child(__MODULE__, [user_id, socket_pid])
  end
end
  

Bu sayede, WebsocketManager.Supervisor.start_user_session(user_id, socket_pid) çağrısı ile her yeni kullanıcı için dinamik olarak bir UserSession süreci başlatılabilir. :temporary yeniden başlatma stratejisi, süreç normal bir şekilde sonlandığında (kullanıcı bağlantısı kesildiğinde) Supervisor’ın onu yeniden başlatmamasını sağlar, ancak bir hata nedeniyle çökerse yeniden başlatır.

Özel Yeniden Başlatma Mantığı ve Hata Eşiği

Supervisor’lar, süreçleri yeniden başlatırken bir hata eşiği (max_restarts ve max_seconds) belirlemenize olanak tanır. Bu, bir sürecin belirli bir süre içinde çok sık çökmesi durumunda Supervisor’ın pes etmesini ve tüm Supervisor ağacını (veya üst Supervisor’ı) çökertmesini sağlar. Bu, sonsuz bir yeniden başlatma döngüsüne girmeyi önler ve ciddi bir hatanın üst katmanlara bildirilmesini sağlar.


# Supervisor.init opts kısmında
opts = [
  strategy: :one_for_one,
  name: __MODULE__,
  max_restarts: 3, # 5 saniye içinde 3'ten fazla yeniden başlatma olursa
  max_seconds: 5   # Supervisor da çöker.
]
  

Bu yapılandırma, belirli bir çocuk sürecinin sürekli olarak hata vermesi durumunda, Supervisor’ın kendisinin de başarısız olmasını sağlayarak daha büyük bir sorunun sinyalini verir. Bu, bir sürecin sürekli çökmesine neden olan temel bir yazılım hatası olduğunda önemlidir.

Supervisor Ağaçlarının Tasarımı

Karmaşık uygulamalarda, tek bir Supervisor tüm çocuk süreçleri yönetmek için yeterli olmayabilir. Genellikle, uygulamanızın farklı mantıksal alanları için ayrı Supervisor’lar oluşturursunuz ve bu Supervisor’ları daha üst düzey bir “uygulama Supervisor’ı” altında gruplandırırsınız. Bu, Supervisor’ları bir ağaç yapısında düzenleyerek sorumlulukları ayırmanıza ve hataların etkisini izole etmenize olanak tanır.

Örnek:


# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Ödeme Ağ Geçidi Supervisor'ı
      PaymentGateway.Supervisor,
      # Kullanıcı Yönetimi Supervisor'ı
      UserManager.Supervisor,
      # Bildirimler Supervisor'ı
      Notification.Supervisor
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
  

Bu hiyerarşik yapı, uygulamanın farklı domain’lerinin bağımsız olarak yönetilmesini sağlar. Örneğin, UserManager.Supervisor altındaki bir süreç çökerse, bu durum PaymentGateway.Supervisor‘ı veya onun çocuk süreçlerini etkilemez. Bu, modülerliği artırır ve bakım kolaylığı sağlar.

Bu ileri düzey teknikler, Elixir’de daha sağlam, ölçeklenebilir ve yönetilebilir dağıtık sistemler inşa etmenize yardımcı olur. Supervisor’lar, Elixir’in “Let it crash” felsefesinin pratik uygulamasını temsil eder ve sistemlerinizi beklenmedik durumlara karşı dayanıklı hale getirmenin anahtarıdır.

Sonuç ve Sıkça Sorulan Sorular

Bu makalede, Elixir’in dağıtık sistemlerde hata toleransı sağlamanın temel taşı olan Supervisor mekanizmasını sıfırdan inşa etmeyi öğrendik. OTP’nin “Let it crash” felsefesini benimseyerek, sistemlerimizi beklenmedik hatalara karşı daha dirençli hale getirebilir ve kendiliğinden iyileşen uygulamalar oluşturabiliriz. Çocuk süreçleri tanımlamaktan, Supervisor modülünü uygulamaya ve farklı yeniden başlatma stratejilerini anlamaya kadar, Elixir’in bu güçlü yeteneğini nasıl kullanacağımızı adım adım gördük. Gerçek dünya senaryolarıyla pekiştirilen bu bilgiler, Elixir ile sağlam ve güvenilir sistemler geliştirmeniz için size sağlam bir temel sunmaktadır.

Supervisor’lar, karmaşık dağıtık sistemlerin yönetimini basitleştirir, geliştiricilerin hata yönetimi yerine iş mantığına odaklanmasını sağlar ve uygulamanın genel kararlılığını artırır. Dinamik denetim, hata eşikleri ve hiyerarşik Supervisor ağaçları gibi ileri düzey teknikler sayesinde, Elixir ile inşa ettiğiniz sistemler sadece çalışmakla kalmaz, aynı zamanda zorlu koşullar altında bile güvenilirliğini korur.

Sıkça Sorulan Sorular

1. Supervisor’lar sadece GenServer’ları mı denetleyebilir?

Hayır, Supervisor’lar sadece GenServer’ları değil, aynı zamanda diğer Supervisor’ları, Task’ları ve hatta temel Elixir süreçlerini de denetleyebilir. Çocuk belirtiminde :start anahtarını kullanarak sürecin nasıl başlatılacağını belirtmeniz yeterlidir.

2. Bir çocuk süreci neden :temporary olarak başlatılır?

:temporary (geçici) yeniden başlatma stratejisi, bir sürecin normal bir şekilde sonlandığında (yani, hata vermeden) Supervisor tarafından yeniden başlatılmamasını sağlar. Ancak, süreç beklenmedik bir hata nedeniyle çökerse, Supervisor onu yeniden başlatır. Bu, genellikle dinamik olarak başlatılan ve belirli bir görevi tamamladıktan sonra sonlanması beklenen süreçler (örneğin, bir kullanıcının web soketi oturumu) için kullanılır.

3. :shutdown seçeneği ne işe yarar?

:shutdown seçeneği, bir Supervisor bir çocuk süreci sonlandırmaya çalıştığında ona ne kadar süre tanınacağını belirtir. Değer bir tamsayı ise (milisaniye cinsinden), süreç bu süre içinde nazikçe sonlanmazsa :brutal_kill ile zorla öldürülür. :infinity değeri, sürecin sonsuza kadar nazikçe sonlanmasını bekleyeceği anlamına gelir. :brutal_kill değeri ise sürecin hemen zorla sonlandırılacağını belirtir. Bu, sistemin kapanışında veya yeniden başlatmalarda kaynakların düzgün bir şekilde serbest bırakılması için önemlidir.

4. Supervisor stratejisini ne zaman değiştirmeliyim?

Supervisor stratejisi, çocuk süreçlerinizin birbirine olan bağımlılık derecesine göre seçilmelidir. Eğer süreçler tamamen bağımsızsa ve birinin çökmesi diğerlerini etkilemiyorsa :one_for_one idealdir. Eğer bir sürecin çökmesi, diğer tüm süreçlerin de geçersiz hale gelmesine neden oluyorsa :one_for_all uygundur. Süreçlerin belirli bir sıraya göre başlatıldığı ve sonraki süreçlerin önceki süreçlere bağımlı olduğu durumlarda ise :rest_for_one tercih edilebilir. Dinamik olarak çocuk süreci başlatmanız gerektiğinde :simple_one_for_one kullanmalısınız.

#Elixir #OTP #Supervisor #DağıtıkSistemler #HataToleransı #YazılımMimarisi #GenServer #SüreçYönetimi

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.