Giriş: Yan Projelerde Dağıtık Kilit İhtiyacı
Kendi küçük projelerimde veya yan projelerimde çalışırken, çoğu zaman tek bir sunucuda veya hatta tek bir işlemde kalıyorum. Ama bazen işler biraz daha karmaşıklaşıyor. Örneğin, aynı anda birden fazla yerden erişilen bir yapılandırma dosyasını güncellemek istediğimde veya birden fazla worker’ın aynı anda bir görevi atomik olarak işlemesini sağlamak istediğimde, dağıtık kilit mekanizmalarına ihtiyaç duyabiliyorum. Bu tür senaryolar, özellikle projenin büyüklüğü arttıkça veya kullanıcı sayısı çoğaldıkça kaçınılmaz hale geliyor.
Normalde Redis’in distributed lock kütüphaneleri veya ZooKeeper gibi daha karmaşık çözümler akla gelir. Ancak, benim gibi pratik çözümler arayanlar için bu tür ağır sistemlere başvurmak her zaman mantıklı değil. Özellikle kaynakların kısıtlı olduğu veya projenin henüz o kadar karmaşıklaşmadığı durumlarda, daha basit ve hafif yaklaşımlar hem geliştirmeyi kolaylaştırır hem de altyapı maliyetini düşürür. Bu yazıda, yan projelerimde kullandığım, nispeten daha basit ama iş gören dört dağıtık kilit yaklaşımını ve neden bu yolları tercih ettiğimi anlatacağım.
1. PostgreSQL’in Advisory Locks ile Güvenliği Sağlamak
PostgreSQL’in sunduğu pg_advisory_lock ve pg_try_advisory_lock fonksiyonları, dağıtık kilitler için oldukça şaşırtıcı derecede etkili bir çözüm sunuyor. Bu fonksiyonlar, veritabanı düzeyinde oturum bazlı kilitler oluşturur. Kilitler oturum sonlandığında otomatik olarak kalkar, bu da kilitli kalma riskini azaltır. Özellikle zaten PostgreSQL kullanan projeler için ek bir bağımlılık yaratmadan bu özelliği kullanmak büyük avantaj sağlıyor.
Bu yaklaşımı bir üretim ERP’sini geliştirirken kullanmıştım. Stok yönetimi modülünde, aynı ürünün birden fazla operatör tarafından aynı anda güncellenmesini engellemek gerekiyordu. Her operatör ekranına bir pg_try_advisory_lock çağrısı ekledik. Eğer kilit başarıyla alınırsa, işlem devam ediyordu. Başka bir operatör aynı anda kilidi almaya çalışırsa, pg_try_advisory_lock hemen döner ve operatöre “Bu ürün şu anda başka bir kullanıcı tarafından işleniyor” mesajı gösterilirdi. Bu, veri tutarsızlığını önlemede çok etkili oldu.
-- Kilit almak için (otomatik kapanır)
SELECT pg_advisory_lock(123456); -- 123456 benzersiz bir kilit ID'si
-- Kilit almaya çalış (başarısız olursa hemen döner)
SELECT pg_try_advisory_lock(123456);
-- Kilit bırakmak için (genellikle gerekmez çünkü oturum kapanınca kalkar)
SELECT pg_advisory_unlock(123456);
Bu yöntemin en büyük avantajı, PostgreSQL’in zaten var olan güvenilirliğini kullanmasıdır. Ayrıca, kilit ID’lerini kolayca oluşturabiliriz; örneğin, tablo ID’si ve ilgili kayıt ID’sinin bir kombinasyonunu kullanmak gibi. Bu, hem kilitleri yönetmeyi kolaylaştırır hem de doğru kaynağın kilitlendiğinden emin olmamızı sağlar. Ancak, PostgreSQL’in olmadığı bir ortamda bu çözüm işe yaramaz.
Bu yöntemi kullanırken dikkat edilmesi gereken nokta, kilit ID’lerinin benzersiz ve anlamlı olmasıdır. Eğer ID’ler rastgele seçilirse, farklı kaynaklar için yanlışlıkla aynı kilidi almaya çalışabilirsiniz. Genellikle, table_id * 1000000 + record_id gibi bir formül ile benzersiz ID’ler oluşturulabilir. Bu, tablonun ve kaydın benzersizliğini koruyarak kilitlerin de benzersiz olmasını sağlar.
2. Dosya Kilitleme Mekanizmaları: Basit ama Etkili Bir Alternatif
Linux tabanlı sistemlerde, dosya kilitleme mekanizmaları, dağıtık kilitler için oldukça temel ve etkili bir yöntemdir. flock komutu veya programatik olarak fcntl sistem çağrısı aracılığıyla bir dosya üzerinde kilit oluşturabilirsiniz. Bu kilitler, dosya açık olduğu sürece geçerlidir ve dosya kapatıldığında otomatik olarak kalkar. Bu, özellikle yapılandırma dosyaları gibi belirli kaynakları korumak için harikadır.
Tek bir makinede çalışan, birden fazla worker’ın bir kuyruktan görev çektiği senaryolarda bu yöntem işe yarar: her worker işe başlamadan önce sabit bir “lock.file” üzerinde flock ile kilit almayı dener. Kilit başarıyla alınırsa worker kuyruktan görevi alıp işlemeye devam eder; alınamazsa görevin başka bir worker tarafından alındığını anlayıp başka bir işe yönelir. Böylece aynı görev iki kez işlenmez.
# Bir script'te flock kullanımı
exec 9>/path/to/your/lock.file
if flock -n 9; then
echo "Kilit alındı, işlem devam ediyor..."
# Asıl işinizi burada yapın
sleep 5
echo "İşlem tamamlandı."
# Kilit otomatik olarak kalkacak (çünkü exec ile açılan dosya kapanacak)
else
echo "Kilit alınamadı, başka bir işlem devam ediyor."
fi
Bu yöntemin en büyük avantajı, herhangi bir ek servis veya veritabanına ihtiyaç duymamasıdır. Sadece dosya sistemi yeterlidir. Bu, minimalist projeler için harika bir seçenektir. Ancak, dosya kilitlerinin yönetimi biraz daha dikkat gerektirir. Eğer bir worker beklenmedik bir şekilde çökerse ve dosyayı açık bırakırsa, kilit sonsuza kadar kalabilir. Bu tür durumları önlemek için flock komutunun -w (timeout) seçeneği veya fcntl ile kilitleri programatik olarak yönetmek faydalı olabilir.
Ayrıca, bu yöntemin ölçeklenebilirliği sınırlıdır. Eğer projeniz birden fazla farklı sunucuya dağılacaksa, bu basit dosya kilitleme yöntemi yeterli olmayacaktır. Ancak, tek bir sunucudaki birden fazla işlem arasında senkronizasyon sağlamak için hala geçerli ve basit bir çözümdür.
3. Redis SETNX (SET if Not Exists) İle Basit Kilit Mekanizması
Redis, in-memory veri yapısı deposu olarak, dağıtık sistemlerde kilit mekanizmaları oluşturmak için popüler bir araçtır. SETNX komutu, bir key’in var olup olmadığını kontrol eder ve eğer yoksa, key’i belirlediğiniz değere ayarlar. Bu komutun atomik doğası, onu basit dağıtık kilitler için ideal kılar. Bir kilit almak için, benzersiz bir key adı kullanır ve SETNX ile bir değer (genellikle bir işlem ID’si veya zaman damgası) ayarlamaya çalışırsınız. Eğer komut 1 döndürürse, kilidi başarıyla almışsınız demektir.
Bir mobil uygulamanın backend’ini geliştirirken, kullanıcıların aynı anda bir kampanya başlığı altında benzersiz bir hediye kazanmasını engellemek için bu yöntemi kullanmıştım. Her kampanya için benzersiz bir Redis key’i oluşturulmuştu (örneğin, campaign:123:lock). Bir kullanıcı hediye talep ettiğinde, backend önce SETNX campaign:123:lock user_id_timestamp komutunu çalıştırıyordu. Eğer SETNX başarılı olursa, kullanıcıya hediye veriliyor ve kilit serbest bırakılıyordu. Başarısız olursa, başka bir kullanıcının hediye aldığını anlayıp kullanıcıya “Tüm hediyeler tükendi” mesajı gösteriliyordu.
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
lock_key = "my_resource_lock"
lock_value = "process_id_12345" # Benzersiz bir değer olmalı
timeout_seconds = 30 # Kilidin otomatik kalkması için süre
# Kilit almaya çalış
if r.set(lock_key, lock_value, nx=True, ex=timeout_seconds):
print("Kilit alındı, işlem devam ediyor...")
try:
# Asıl işinizi burada yapın
pass
finally:
# Kilidi bırak
# Dikkat: Sadece kendi kilidimizi bırakmalıyız
if r.get(lock_key) == lock_value.encode():
r.delete(lock_key)
print("Kilit bırakıldı.")
else:
print("Kilit alınamadı, başka bir işlem devam ediyor.")
Bu yaklaşımın en büyük avantajı, Redis’in hızlı olması ve SETNX komutunun atomikliği sayesinde güvenilir bir kilit mekanizması sunmasıdır. EX (expire) parametresi ile kilidin belirli bir süre sonra otomatik olarak kalkmasını sağlayarak, kilitli kalma sorununu da büyük ölçüde çözebilirsiniz. Ancak, EX parametresi Redis 2.6.12 ve sonraki sürümlerde mevcuttur, bu yüzden kullandığınız Redis sürümünü kontrol etmeniz önemlidir.
Bu yöntemde dikkat edilmesi gereken önemli bir nokta, kilidi bırakırken (delete işlemi) sadece kendi aldığınız kilidi bıraktığınızdan emin olmaktır. Aksi takdirde, siz kilidi bırakmadan başka bir işlem kilidi almışsa, sizinkiyle onun kilidini yanlışlıkla silmiş olursunuz. Bu nedenle, GET ile kilidin değerini kontrol edip, sadece kendi değerinizse DELETE yapmak en doğru yaklaşımdır.
4. Basit Bir Veritabanı Tablosu ile Kilit Yönetimi
En temel çözümlerden biri, veritabanınızda özel bir kilit tablosu oluşturmaktır. Bu tabloda, kilit adı, kilit sahibi (örneğin, işlem ID’si veya sunucu adı) ve bir zaman damgası gibi alanlar bulunabilir. Kilit almak için, benzersiz bir kilit adı ile bu tabloya bir kayıt eklemeye çalışırsınız. Eğer ekleme başarılı olursa, kilidi almışsınız demektir. Kilitleri serbest bırakmak için ise ilgili kaydı tablodan silersiniz.
Çok basit bir mesaj kuyruğu senaryosunda bu yöntem yeterli olur: her worker, kritik bir işlemi yapmadan önce locks adında bir tabloda message_queue_lock adında bir kayıt olup olmadığını kontrol eder. Kayıt yoksa yeni bir kayıt ekler ve işleme devam eder; kayıt varsa başka bir worker’ın o işi üstlendiğini anlar.
-- Kilit tablosu örneği
CREATE TABLE locks (
lock_name VARCHAR(255) PRIMARY KEY,
owner_id VARCHAR(255) NOT NULL,
acquired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Kilit almak için (INSERT IGNORE veya ON CONFLICT ile)
-- PostgreSQL örneği
INSERT INTO locks (lock_name, owner_id)
VALUES ('message_queue_lock', 'worker_id_abc')
ON CONFLICT (lock_name) DO NOTHING;
-- Eğer INSERT IGNORE (MySQL) veya ON CONFLICT DO NOTHING (PostgreSQL)
-- 0 satır etkilenirse kilit alınamamıştır. 1 satır etkilenirse kilit alınmıştır.
-- Kilit bırakmak için
DELETE FROM locks WHERE lock_name = 'message_queue_lock' AND owner_id = 'worker_id_abc';
Bu yaklaşımın en büyük avantajı, mevcut veritabanı altyapınızı kullanmasıdır. Ekstra bir servis kurmanıza veya yönetmenize gerek kalmaz. Ancak, kilitlerin otomatik olarak serbest bırakılması konusunda dikkatli olmanız gerekir. Eğer bir worker çökerse ve kilidi silmezse, kilit tablosunda kalıcı bir kayıt bırakabilir. Bu durumu önlemek için, kilit tablosuna bir “son kullanma tarihi” (expiration timestamp) alanı ekleyip, belirli aralıklarla çalışan bir temizlik işlemi (garbage collection) ile eski kilitleri temizleyebilirsiniz.
Bu yöntemin bir diğer dezavantajı ise, eğer kilit mekanizması karmaşıklaşırsa (örneğin, kilit süresi uzatmak, kilit sahipliğini devretmek gibi), veritabanı tablosu yönetimi karmaşık hale gelebilir. Bu nedenle, bu yöntem genellikle çok basit senaryolar ve ek servis kurmanın mantıklı olmadığı durumlar için uygundur. Özellikle tek bir sunucuda çalışan birkaç işlem için pratik bir çözüm olabilir.
Sonuç: Pragmatik Çözümlerin Gücü
Yan projelerimde dağıtık kilit mekanizmalarına ihtiyaç duyduğumda, her zaman en karmaşık veya en popüler çözümü tercih etmiyorum. Yukarıda bahsettiğim PostgreSQL advisory locks, dosya kilitleme, Redis SETNX ve basit veritabanı tablosu gibi yaklaşımlar, projenin gereksinimlerine ve mevcut altyapıya bağlı olarak oldukça etkili olabilir. Önemli olan, trade-off’ları anlamak ve projenizin ihtiyacına en uygun çözümü seçmektir.
Bu basit yöntemler, özellikle kaynakların kısıtlı olduğu veya projenin henüz erken aşamalarında olduğu durumlarda, size hem geliştirme kolaylığı hem de altyapısal verimlilik sağlar. Karmaşık çözümlere geçmeden önce, bu daha hafif yaklaşımların işinizi görüp görmediğini değerlendirmek her zaman faydalıdır. Unutmayın, en iyi çözüm her zaman en karmaşık olanı değildir; bazen en basit olanı, en pragmatik olandır.
Gelecek yazılarda, bu yaklaşımların daha detaylı kullanım senaryolarını ve performans karşılaştırmalarını inceleyebiliriz.