MongoDB'de Read Preference Nedir, Nasıl Ayarlanır?

Selamlar, bu yazıda MongoDB'nin replica set ortamlarında sıkça karşımıza çıkan ama doğru kurulmazsa ilginç sürprizler doğuran bir konuya bakacağız: read preference. Konuyu hem teorik tarafıyla hem de Node.js, Python ve connection string seviyesindeki pratik karşılıklarıyla ele alacağız. Lafı çok uzatmadan başlayalım.

Bir replica set'te yazma işlemleri her zaman primary'ye gider, bu kesin. Ama okuma tarafı öyle değil. Replica set'in birden fazla üyesi var ve siz isterseniz okumaları primary dışındaki node'lara da yönlendirebilirsiniz. İşte read preference dediğimiz şey bu yönlendirmenin politikasını belirliyor.

Read Preference Nedir?

Kısaca, bir okuma isteğinin replica set'in hangi üyesine gideceğine karar veren ayar. Driver tarafında, connection string'de ya da operasyon başına tek tek belirleyebiliyorsunuz. Burada kritik nokta şu: secondary'lerden okuma yaptığınız anda eventual consistency dünyasına geçiyorsunuz. Yani primary'ye az önce yazdığınız bir kayıt, secondary'ye replication ile gelene kadar geçen sürede orada görünmeyebilir.

Bence bu detay genelde gözden kaçıyor. 'Performansı artırmak için secondary'lerden okuyalım' kararı kolay verilir, sonra bir kullanıcı kendi yazdığı veriyi ekranda göremediğinde 'bug var' diye ticket açar.

Beş Mod

MongoDB'nin desteklediği beş read preference modu var:

  • primary (varsayılan): Tüm okumalar primary'ye gider. En güçlü tutarlılık.
  • primaryPreferred: Primary müsaitse oraya, değilse secondary'ye düşer.
  • secondary: Okumalar her zaman bir secondary'ye gider. Eventual consistency.
  • secondaryPreferred: Önce secondary'lere bakar, hiçbiri yoksa primary'ye düşer. Analitik raporlar için klasik tercih.
  • nearest: Primary mi secondary mi olduğuna bakmadan ağ gecikmesi en düşük olan node'a gider.

Hangisini ne zaman seçmeli? Şahsi kanaatim şu: kullanıcının kendi verisiyle çalıştığı operasyonel akışlarda primary kalmak en sağlıklısı. Dashboard, rapor, BI sorgusu gibi 'biraz eski veri olsa da olur' işlerde secondaryPreferred mantıklı. Coğrafi olarak dağıtık bir replica set'iniz varsa nearest gerçekten işe yarar, ama o zaman da tag set'lerle birlikte düşünmek gerekir.

Kodda Nasıl Ayarlarız?

Node.js tarafında driver'ın ReadPreference sınıfı var. Bir collection üzerinde secondaryPreferred ile okuma yapmak istersek:

const { MongoClient, ReadPreference } = require('mongodb');

const client = new MongoClient('mongodb://host1,host2,host3/?replicaSet=rs0');
await client.connect();

const reports = client
  .db('analytics')
  .collection('events')
  .withReadPreference(ReadPreference.SECONDARY_PREFERRED);

const rows = await reports.find({ type: 'pageview' }).toArray();

PyMongo tarafında da benzer bir yapı var. Collection seviyesinde ayar verebilir, ya da operasyon başına override edebilirsiniz:

from pymongo import MongoClient, ReadPreference

client = MongoClient('mongodb://host1,host2,host3/?replicaSet=rs0')
analytics = client.analytics.get_collection(
    'events',
    read_preference=ReadPreference.SECONDARY_PREFERRED,
)

for row in analytics.find({'type': 'pageview'}):
    print(row)

Burada dikkat: client, database, collection ve operasyon - dört seviyede ayar yapabiliyorsunuz ve en spesifik olan kazanıyor. Yani client'ta primary demiş olsanız bile, tek bir aggregate çağrısında secondaryPreferred geçebilirsiniz.

Tag Set'lerle Hedefli Okuma

İşler ilginçleşiyor. Replica set üyelerine tag atayabiliyorsunuz. Mesela region: 'eu-west' ve role: 'analytics' gibi. Sonra read preference'a bu tag'leri vererek 'sadece şu özellikteki node'lardan oku' diyebiliyorsunuz.

const cfg = rs.conf();
cfg.members[1].tags = { region: 'eu-west', role: 'analytics' };
rs.reconfig(cfg);

Driver tarafından kullanımı şöyle:

const pref = new ReadPreference('secondary', [{ region: 'eu-west' }]);
const col = db.collection('events').withReadPreference(pref);

Ben bunu özellikle bir DC'de analytics yükünü ayrı tutmak istediğinizde çok faydalı buluyorum. Operasyonel trafik bir bölgede, ağır aggregation pipeline'lar başka bölgedeki secondary'lerde - replica set tek, yük dağılımı net.

maxStalenessSeconds

Secondary'den okumanın en sinir bozucu tarafı: ya secondary çok geri kalmışsa? maxStalenessSeconds tam bunun için var. Driver'a 'replication lag'i şu saniyenin üzerinde olan secondary'leri okumaya dahil etme' diyebiliyorsunuz.

analytics = client.analytics.get_collection(
    'events',
    read_preference=ReadPreference.SECONDARY_PREFERRED,
    max_staleness_seconds=120,
)

Minimum değer 90 saniye, ondan aşağısına driver izin vermiyor. Mantıklı bir alt sınır da bu zaten; replication monitoring'i için belli bir buffer lazım.

Connection String Seviyesinde

Tüm bunları koddan da yapabilirsiniz, ama uygulama kodu yerine connection string'de tutmak çoğu zaman daha pratik. Özellikle DevOps tarafında env değişkeni üzerinden yönetiyorsanız:

mongodb://host1,host2,host3/?replicaSet=rs0&readPreference=secondaryPreferred&maxStalenessSeconds=120&readPreferenceTags=region:eu-west

Bu şekilde uygulamayı yeniden derlemeden yük politikasını değiştirebiliyorsunuz. Tabii test ortamında önce dener miyim? Tabii ki.

Sık Karşılaşılan Hatalar

  • 'Read your own writes' beklentisi: secondary'den okuyup primary'ye yazıp tekrar okuyunca aynı veriyi göremezsiniz. Kullanıcı odaklı endpoint'lerde secondaryPreferred kullanmak çoğu zaman bug üretir.
  • maxStalenessSeconds'u unutmak: secondary'lerden okuyorsunuz ama lag kontrolü yoksa, çok geri kalmış bir node'dan saatler önceki veriyi okumak işten değil.
  • Tag set'i tek node'a daraltmak: Tag eşleşmesi çok dar olursa ve o node düşerse driver başka aday bulamaz, okuma hata verir. Tag set'leri en az 2-3 üyeye eşleşecek şekilde planlamak şart.
  • nearest'i 'her durumda en hızlı' sanmak: Nearest sadece ping latency'sine bakar, node'un yük durumuna değil. Yoğun bir secondary, uzaktaki boş bir secondary'den daha yavaş olabilir.

Kapanış

Bu yazıda MongoDB read preference'ın beş modunu, tag set'lerle hedefli okumayı, maxStalenessSeconds ile lag kontrolünü ve connection string seviyesinde ayarı gördük. Bana sorarsanız default olan primary'den ayrılmak için iyi bir gerekçeniz olmalı; ama analitik ve coğrafi dağıtık senaryolarda doğru kullanılırsa hem yükü dağıtır hem de gecikmeyi ciddi şekilde düşürür. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.