İçeriğe atla

Kimlik Okumaları DynamoDB'de, Arama OpenSearch'te: Zero-ETL Okuma Modeli

Kimlik okumaları DynamoDB'de kalsın; keyfi sıralama, filtre, facet ve tam metin arama zero-ETL OpenSearch okuma modeline gitsin. Bazen tek PostgreSQL ikisini de yener.

Ayhan Sipahi Ayhan Sipahi

Ürün büyüdükçe DynamoDB liste endpoint’lerinde hep aynı belirtiler birikir: her sıralama seçeneği için bir GSI, okuma sonrası filtreleri telafi eden bir doldurma döngüsü, facet sayılarının yerine geçen sayaç öğeleri. Bu belirtilerin her biri, key-range erişimi için kurulmuş bir depoya zorla sokulmuş arama şeklinde bir okumadır. Benimsemeye değer varsayılan, bir taşıma değil bir yönlendirme kuralı: kimlik okumaları (şu siparişi getir, şu müşterinin siparişlerini tarihe göre listele) DynamoDB’de kalır; keyfi sıralama, anlık filtre kombinasyonları, facet’ler, kesin toplamlar ve tam metin arama ise DynamoDB’nin Amazon OpenSearch Service ile zero-ETL entegrasyonunun (yeni sekmede açılır) beslediği bir okuma modeline gider. Baştan söylenmesi gereken önemli bir istisna var: kayıt sistemini (system of record) hâlâ özgürce seçebilen bir ekip için tek bir PostgreSQL, iki depo işletmekten çoğu zaman daha iyidir. Bu pipeline’ı henüz çalıştırmadım; aşağıdaki her şey dokümante edilmiş davranıştan türüyor; pipeline kurulduğu gün nelerin ölçüleceğinin planı da hazır.

Kimlik Okumaları ve Arama Okumaları#

Kimlik okuması adresini kendi üzerinde taşır: bir sipariş ID’si, bir müşterinin partition key’i artı tam da bu soru için tasarlanmış bir sort key üzerindeki tarih aralığı. DynamoDB bunları her ölçekte ucuz ve öngörülebilir biçimde yanıtlar ve bu bir tesadüf değildir. AWS servisi, JOIN desteği olmayan bir key-value ve doküman veritabanı olarak tanımlar (yeni sekmede açılır); NoSQL tasarım kılavuzu (yeni sekmede açılır) da sonucunu açıkça söyler: veri yalnızca sınırlı sayıda yolla verimli sorgulanabilir ve şema tasarımına, tablonun yanıtlayacağı soruları bilmeden başlanmamalıdır. DynamoDB’nin iyi olduğu her şey bu çerçeveden çıkar: nokta okumalar, tek partition key altındaki öğe koleksiyonları, önceden modellenmiş sort key üzerindeki aralık taramaları.

Arama okuması ise bir sonucu adlandırmak yerine tarif eder: istek anında seçilmiş üç filtreye uyan, kullanıcının tıkladığı sütuna göre sıralanmış, kenar çubuğunda facet sayıları olan ve 7. sayfaya atlanabilen siparişler. Hiçbir key tasarımı buna hizmet edemez, çünkü anahtarlar seçilirken bu soru ortada yoktu. Zorlamak her kod tabanında aynı belirtileri üretir. Dördüncü ve beşinci GSI belirir; varsayılan kota (yeni sekmede açılır) tablo başına 20 GSI olsa da asıl kısıt çok daha erken gelir: her index, indexlediği ya da yansıttığı attribute’lara dokunan her mutasyona kalıcı olarak bir yazma ekler ve sorgu esnekliği bu harcamadan çok daha yavaş büyür. Doldurma döngüleri, okuma sonrası filtrelerin bedelini tam okuma maliyetiyle öder. Her facet değeri için bir sayaç öğesi türer. Organizasyonel işaret en son gelir: her yeni rapor talebi bir veri modelleme toplantısına dönüşür, yani okuma gereksinimleri sayılabilir olmaktan çıkmıştır.

Kullanışlı test de bu sayılabilirlik. Bir yıl önce listeleyebildiğiniz okuma desenlerinden bugün hâlâ kaçını listeleyebildiğinizi sayın. Küme sabitse DynamoDB tek başına yeterlidir ve single-table tasarım rehberindeki modelleme teknikleri işi görür. Her sprint büyüyen bir küme ise artık bir arama iş yüküdür ve hiçbir key modellemesi istek anında kurulan sorulara yetişemez.

Kimlik tarafının nasıl sayfalanacağı (imzalı cursor’lar, tek sıralama olarak sort key, toplam sayıdan vazgeçmek) sayfalama yazısında anlatılıyor. O yazının karar ağacı, DynamoDB’nin tamamen dışını gösteren tek bir dalla bitiyordu: partition’lar arası keyfi sıralama. Aşağısı o dalın sonuna kadar tartışılmış hâli; istisna dalları da dahil:

Evet

Hayır, her sprint büyüyor

Hayır, seçim hâlâ açık

Evet, yazma yolu ona bağlı

Tam metin, facet, keyfi sıralama, kesin toplam

Analitik özetler

Yalnızca anlamsal benzerlik

Aynı sıcak sorgu, sadece pahalı

Hayır

Evet

Erişim desenleri sayılabilir ve sabit mi?

DynamoDB'de modelle: sort key, sparse veya overloaded GSI

DynamoDB zaten kayıt sistemi mi?

İlişkisel kayıt sistemi: PostgreSQL veya Aurora

Okuma hangi şekilde?

Varsayılan: OpenSearch'te zero-ETL okuma modeli

S3 export artı Athena, veya Redshift'e zero-ETL

DynamoDB vektör index'i

DAX veya uygulama önbelleği

Boş stream okuyucusu ve nöbet kapasitesi var mı?

Yeniden düşün: ilişkisel okuma modeli veya daha az özellik

Mapping şablonu, DLQ ve gecikme alarmlarıyla yayınla

API Katmanında Okuma Yönlendirmesi#

Hangi okuma deposu kazanırsa kazansın hayatta kalan parça, yönlendirmenin kendisidir: tek bir dağıtıcı, iki backend, tek bir açık kural. Bunu kod olarak yazmak kararı test edilebilir kılar ve endpoint başına doğaçlamaya dağılmasını önler.

type OrderPage = {
  items: unknown[];
  nextToken: string | null;
  // Deponun adı değil, tazelik sözleşmesi: istemci bayatlığı buradan
  // gösterir ve backend değişse de sözleşme kırılmaz.
  consistency: 'strong' | 'eventual';
  stalePossible: boolean;
};

// Ürünün desteklediği her liste okuması, adıyla bildirilmiş. Key'lere ya da
// bir GSI'a modellenmiş profil, filtrelese ve sıralasa bile DynamoDB'de
// kalır; bir profili arama okuması yapan şey filtre parametresinin varlığı
// değil, key modelinin o soruyla hiç tanışmamış olmasıdır.
const orderViews = {
  customerNewest: { backend: 'dynamodb' },      // sort key, ScanIndexForward
  customerOpenByDate: { backend: 'dynamodb' },  // açık siparişlerde sparse GSI
  customerByPrice: { backend: 'dynamodb' },     // tam bu sıralama için modellenmiş GSI
  catalogSearch: { backend: 'opensearch' },     // serbest metin, facet, anlık filtreler
} as const;

type ViewName = keyof typeof orderViews;

// İki implementasyon başka yerde yaşıyor; buradaki mesele kararın kendisi.
declare function queryOrders(view: ViewName, params: unknown): Promise<OrderPage>;
declare function searchOrders(params: unknown): Promise<OrderPage>;

export function listOrders(view: ViewName, params: unknown): Promise<OrderPage> {
  const { backend } = orderViews[view];
  return backend === 'dynamodb' ? queryOrders(view, params) : searchOrders(params);
}

Registry, sayılabilirlik testinin kod hâlidir. View eklemek bilinçli bir edimdir ve diff, yeni sorunun bir key modeli mi kazandığını yoksa arama profiline mi düştüğünü gösterir; kimsenin kaydetmediği bir istek şekli sessizce bir yere yönlendirilmez, eksik bir ürün kararıdır. Tetikleyici listesinde bilerek olmayan bir şey var: tek başına numaralı bir page parametresi yalnızca from = (page - 1) * size demektir, toplam gerektirmez; arama yolunu sayfa numarasının kendisi değil, “42 sayfanın 7.‘si”ni göstermek zorunlu kılar.

Sözleşmede iki sonucun açıkça yer alması gerekir. Birincisi, yanıt depo adı değil bir tazelik sözleşmesi taşır. DynamoDB cevabı kendiliğinden taze değildir; okumalar varsayılan olarak eventually consistent’tır ve GSI okumaları her zaman öyledir. OpenSearch cevabı ise replikasyonun üstüne index refresh aralığının da arkasındadır. consistency ile stalePossible istemciye neyi gösterebileceğini söyler ve bir view ileride backend değiştirse de çalışmaya devam eder. İkincisi, arama yolunun da opak bir sayfalama token’ına ihtiyacı vardır: search_after sıralama değerleri, ham bir LastEvaluatedKey ile tamamen aynı biçimde iç veri sızdırır; bu yüzden sayfalama yazısının DynamoDB cursor’larına uyguladığı muamelenin aynısını görürler: bütünlük için imzalama ve kapsam bağlama, değerlerin kendisi gizli kalacaksa şifreleme ya da sunucu tarafı handle; çünkü imza tek başına onları okunur bırakır.

Zero-ETL Entegrasyonunun Gerçekte Kurduğu Yapı#

Entegrasyon iki aşamada çalışır. Önce tablonun point-in-time export’unu S3’e alır ve bu anlık görüntüyü index’e toplu yükler; sonra bir OpenSearch Ingestion pipeline’ı üzerinden DynamoDB Streams’i izleyerek devam eden değişiklikleri replike eder. Ön koşullar aşamalardan çıkar: export için point-in-time recovery, değişiklik takibi için Streams açık olmalıdır. İki aşama da üretim trafiğiyle tablo kapasitesi için yarışmaz; çünkü export tablodan değil PITR yedeğinden okur, değişiklik akışı da Streams’ten gelir.

Pipeline yapılandırması (yeni sekmede açılır) Data Prepper formatında bir YAML:

version: "2"
cdc-pipeline:
  source:
    dynamodb:
      acknowledgments: true
      tables:
        - table_arn: "arn:aws:dynamodb:eu-central-1:111122223333:table/app"
          export:
            s3_bucket: "my-export-bucket"
            s3_prefix: "export/"
          stream:
            start_position: "LATEST"
      aws:
        sts_role_arn: "arn:aws:iam::111122223333:role/pipeline-role"
        region: "eu-central-1"
  sink:
    - opensearch:
        hosts: ["https://search-mydomain.eu-central-1.es.amazonaws.com"]
        aws:
          sts_role_arn: "arn:aws:iam::111122223333:role/pipeline-role"
          region: "eu-central-1"
        index: "orders"
        document_id: "${getMetadata(\"primary_key\")}"
        action: "${getMetadata(\"opensearch_action\")}"
        document_version: "${getMetadata(\"document_version\")}"
        document_version_type: "external"
        include_keys: ["orderId", "customerId", "status", "productName", "discount", "total", "price", "rating", "revision"]
        template_type: "index_template"
        template_content: |
          { "template": { "mappings": { "properties": {
            "customerId": { "type": "keyword" }, "orderId": { "type": "keyword" },
            "status": { "type": "keyword" }, "price": { "type": "float" },
            "total": { "type": "float" }, "rating": { "type": "float" },
            "discount": { "type": "float" }, "revision": { "type": "long" } } } } }
        dlq:
          s3:
            bucket: "my-dlq-bucket"
            key_path_prefix: "dlq/orders"
            region: "eu-central-1"
            sts_role_arn: "arn:aws:iam::111122223333:role/pipeline-role"

Bütün düzenin doğruluğunu dört ayar taşıyor ve best-practice sayfası (yeni sekmede açılır) hepsine arka çıkıyor; mapping şablonu katı bir kural, geri kalanlar güçlü birer varsayılan. document_id tablonun primary key’ini kullanır; böylece bir öğenin güncellemesi kopya yaratmak yerine aynı dokümanın üzerine düşer. action, opensearch_action metadata’sını kullanır; böylece tablodaki bir silme dokümanı da siler; bu olmadan, silinen öğeler index’te bayat arama sonuçları olarak kalır. document_version ile document_version_type: external sıralama deliğini kapatır: onsuz, sırası bozuk gelen bir stream teslimatı yeni verinin üstüne eskisini yazabilir. Sink ayrıca bir mapping şablonunu include_keys ile birlikte bildirir; böylece index yalnızca arama, sıralama ve facet için kullandığınız attribute’ları tutar. Geri kalan her şey DynamoDB’den ID ile çekilir; primary key’i doküman ID’si yapmanın ucuzlattığı şey de tam olarak budur. Şablondaki bir yazım da dağıtımdan önce doğrulayıcıdan geçmeyi hak ediyor: DynamoDB kılavuzu template_type: index_template yazarken Data Prepper sink referansı index-template listeliyor; iki sayfadan birine güvenmek yerine yapılandırmayı pipeline doğrulayıcısından geçirin.

Hydration adımının kendi küçük sözleşmesi var. BatchGetItem öğeleri belirli bir sırada döndürmez ve işlenmemiş anahtarlar bırakabilir; bir isabet, DynamoDB’nin o arada sildiği ya da filtrenin dışına çıkardığı bir öğeye işaret edebilir ve dünün fiyatıyla sıralanmış bir doküman bugünün fiyatıyla hydrate olur. API katmanı arama sıralamasına göre yeniden dizer, canlı öğesi artık uymayan isabetleri düşürür, gerektiğinde sayfayı yeniden doldurur ve bu düşüşleri sayar; çünkü yükselen düşüş oranı, geriye düşen replikanın kendisidir. Sıralamayı süren alanlar için de bir taraf seçer: ya index’teki değeri gösterir ya da hydrate edilen değere göre yeniden doğrulayıp diziler; bir sayıyla sıralayıp başka bir sayıyı göstermek, yanlış sıralanmış bir sayfa olarak okunur.

Dokümantasyondaki bir tutarsızlığı canlıda keşfetmek yerine baştan bilmek daha iyi. Bu satırlar yazılırken DynamoDB geliştirici kılavuzu Streams’in yeni ve eski görüntülerle açılmasını isterken, OpenSearch ingestion sayfası NEW_IMAGE’ı yeterli sayıyor ve NEW_AND_OLD_IMAGES’ı da destekleniyor diye listeliyor. NEW_AND_OLD_IMAGES açmak, daha büyük stream kayıtları pahasına iki yorumu da karşılar.

Mimarinin biçimini özelliklerden çok dokümante edilmiş sınırlar belirliyor. Pipeline ve tablo aynı hesapta ve aynı Region’da olmalı; her pipeline tam olarak tek bir kaynak tabloyu destekliyor (geçerli metin ingestion sayfasının sınırlar bölümü; DynamoDB kılavuzunun girişi hâlâ “bir veya daha fazla tablo” vadediyor, aşağıdaki stream görüntüsü çelişkisiyle aynı türden bir doküman uyuşmazlığı). DynamoDB Streams olayları 24 saat tutar; ilk export’u 24 saatten uzun süren çok büyük bir tablo aradaki değişiklikleri kaybeder ve bir günden uzun duran bir pipeline tam bir yeniden export demektir. Kota sayfası (yeni sekmede açılır) ayrıca her stream shard’ı için en fazla iki eşzamanlı okuyucu süreç öngörülmesini söylüyor: aggregate’ler veya bir outbox için stream’i zaten izleyen bir Lambda varsa, pipeline ikinci yuvayı alır; üçüncü bir okuyucu katı bir engel değil, throttling riskidir. İkiyi geçince akışı başka bir doğrudan tüketici eklemek yerine Kinesis Data Streams for DynamoDB ya da bir EventBridge pipe ile dallandırın. Global tablolar için aynı sayfa tek eşzamanlı okuyucu öneriyor; o tek okuyucu da pipeline olurdu. Single-table tasarımlara bir not daha: tek bir overloaded tablo yine tek pipeline demektir; varlık tiplerini farklı index’lere ayırma işi, aynı stream üzerine birden çok pipeline kurmak yerine o pipeline’ın yapılandırması içindeki yönlendirmeyle yapılır.

Mapping Tuzağı#

İkinci haftada gelmesi en muhtemel arıza, ingestion kılavuzunda (yeni sekmede açılır) dokümante edilmiş durumda. Açık bir mapping yoksa OpenSearch alan tiplerini gördüğü ilk dokümandan çıkarır: tam sayı long olur, kesirli sayı float. Şemasız bir tablo tipli bir index ile buluşur ve kaybeden şemasız taraf olur. Replike edilen ilk öğe "discount": 10 taşıyor, sonraki bir öğe "discount": 9.5 getiriyorsa, sonraki doküman ingest edilemez ve (tabii yapılandırdıysanız) dead-letter kuyruğuna düşer. Çözüm şablonu baştan bildirmek:

{
  "template": {
    "mappings": {
      "properties": {
        "discount": { "type": "float" },
        "status": { "type": "keyword" },
        "productName": {
          "type": "text",
          "fields": { "raw": { "type": "keyword" } }
        }
      }
    }
  }
}

Şablonun öbür yarısı keyword ile text kararı. text, kullanıcının arama kutusuna yazdığı şey için analiz edilir; keyword ise sıralanan ve facet’lenen alanlar için analizsiz kalır. Bunu yanlış kurmak beklenen yönde bozulmaz: bir text alanında doğrudan sıralama düpedüz reddedilir, çünkü text alanlarında fielddata varsayılan olarak kapalıdır; fielddata’yı açıp zorlamak ise görünen ada göre değil analiz edilmiş token’lara göre sıralar. Yukarıdaki çok alanlı desen (eşleşme için analizli, sıralama için raw) ikisine birden hizmet etmenin standart yoludur. Var olan bir alandaki mapping değişikliği genelde reindex gerektirir (yeni bir alan yerinde eklenebilir); yani okuma modelinin de bir şeması vardır. İlişkisel bir migration’dan farkı, kurtarma prosedürünün index’i silip tablodan yeniden kurmak olmasıdır.

Arama Tarafında Sayfalama#

Sayfalama yazısının ana savı, DynamoDB’nin size bir cursor verdiği ve başka hiçbir şey vermediğiydi. OpenSearch daha fazlasını verir ve her eklemenin kendi bedeli vardır.

Numaralı sayfalar from ve size ile geri gelir; DynamoDB’nin yapamadığı offset sayfalaması budur. Tavan, index.max_result_window index ayarıdır (yeni sekmede açılır) ve varsayılanı 10.000 sonuçtur: sayfalama dokümantasyonu (yeni sekmede açılır) offset sayfalamayı orada keser; çünkü derin offset’ler her shard’ın atlanan sonuçları üretip çöpe atmasına yol açar. Pencereyi büyütmek mümkündür ve derinliği shard başına belleğe karşı takas eder; yani 42 sayfanın 7.‘si varsayılan pencerede çalışır, 700. sayfa çalışmaz.

Pencerenin ötesinde search_after, son sonucun sıralama değerlerinden devam eder ve derinlik sınırı yoktur. Tasarımı gereği durumsuzdur; bu, dokümanlar eklenip silindikçe sıralamanın sayfalayan istemcinin altında kayabileceği anlamına gelir ve tamamen deterministik bir sıralama gerektirir, bu yüzden her sıralama listesinin sonunda benzersiz bir eşitlik bozucu alan durmalıdır. /orders/_search adresine giden filtreli, sıralı bir sayfa şöyle görünür:

{
  "size": 20,
  "track_total_hits": 10000,
  "query": {
    "bool": {
      "filter": [
        { "term": { "customerId": "CUST-2048" } },
        { "term": { "status": "shipped" } },
        { "range": { "total": { "gte": 100 } } }
      ]
    }
  },
  "sort": [
    { "price": "asc" },
    { "orderId": "asc" }
  ],
  "search_after": [129.99, "ORDER-7431"]
}

customerId term’i tenant sınırıdır ve isteğe bağlı değildir. Index müşteriler arasında ortaktır; bu yüzden arama katmanı bu filtreyi her sorguya sunucu tarafında ekler. OpenSearch’e bu filtre olmadan ulaşan bir istek, başka müşterilerin siparişlerini döndürür. Filtre bir uygulama kontrolüdür, sınırın tamamı değil: domain’e, export bucket’ına ya da DLQ’ya doğrudan ulaşan her şey onu atlar; o yüzeylerin kendi erişim hikâyesi olmalı ve sınırın deponun içinde yaşaması gerekiyorsa yönetilen domain’ler doküman düzeyi güvenlik sunar.

Sabit bir anlık görüntü üzerinde derin sayfalama içinse OpenSearch dokümantasyonu Point in Time artı search_after önerir: PIT, seçtiğiniz bir keep_alive penceresi boyunca aranan veri kümesini dondurur ve işiniz bitince silinmelidir. Yönetilen bir domain’de PIT dokümantasyonu (yeni sekmede açılır) operasyonel olarak önemli bir uyarı ekler: PIT durumunun dayanıklılığı yoktur; node yeniden başlatmaları, node değişimleri, blue/green dağıtımlar ve süreç yeniden başlatmaları hepsini siler. Oturum ortasında buharlaşabilen donmuş bir anlık görüntü, süresi dolmuş bir HMAC cursor’dan farklı bir arıza modudur; ama istemci tarafındaki cevap aynıdır: baştan başlama yolu.

DynamoDB’nin büyük ve değişken bir sonuç kümesi üzerinde tek çağrıyla ucuza üretemediği kesin toplamlar track_total_hits’ten gelir. Pratikteki varsayılan davranış, 10.000 eşleşmeye kadar doğru saymak ve daha büyüğünü "relation": "gte" ile alt sınır olarak bildirmektir. Search API referansı (yeni sekmede açılır) parametreyi bu varsayılanı belirtmeden dokümante eder; mevcut davranışın en net tarifi, 3.x hattını hedefleyen ama o hat davranışı değiştirmeden yayınlanmış, hareketsiz bir değişiklik teklifinde (yeni sekmede açılır) duruyor ve davranışın kendisi Elasticsearch 7’den miras. Kendi cluster sürümünüze karşı doğrulayın. track_total_hits: true her istekte her eşleşmeyi sayar ve orantılı maliyet çıkarır: kesin sayı bedelini ödediğinizde vardır ve “10.000+” ile yaşayabilecek bir arayüz bunu öyle göstermelidir. Her durumda bu sayı, tabloyu geriden izleyen index görünümü için kesindir; bir arama rakamıdır, kota ya da fatura rakamı değil.

Hiçbir bedelle DynamoDB karşılığı olmayan tek ekleme alaka düzeyi: bir metin sorgusu için _score’a göre sıralama yerleşiktir ve bir filtre ekranını aramaya dönüştüren şey budur. Genel takas açıkça söylenecek kadar simetrik: DynamoDB’nin sayfalaması ucuz, öngörülebilir ve toplamsızdır; OpenSearch’ünki toplamları, sayfa numaralarını ve alaka düzeyini, sonuç kümesi derinliğiyle büyüyen bir maliyetle sunar. Facet sayılarının kendi dipnotu var: terms aggregation her shard’dan aday bucket toplar; yüksek cardinality’li alanlarda sayılar varsayılan olarak yaklaşıktır, dokümante edilmiş bir hata payı ve belleği doğrulukla takas eden bir shard_size ayarı vardır.

Replikasyon Gecikmesi ve Kendi Yazdığını Okuma#

İki deponun arasındaki boşluk replikasyon gecikmesidir ve en görünür kurbanı kendi yazdığını okumaktır: istemci bir sipariş oluşturur, 201 alır, liste görünümüne gider ve liste, siparişi henüz görmemiş bir replika tarafından servis edilir. Dokümantasyon gecikmeyi operasyonel bir metrik olarak ele alır ve ona bir üst sınır bağlamaz. Entegrasyon kılavuzu devam eden değişikliklerin gerçek zamana yakın replike edildiğini söyler; best-practice sayfası, normal işleyişte sessiz kalacağı gerekçesiyle replikasyon gecikmesi bir dakikayı aşınca alarm kurmayı önerir; 24 saatten sonra olaylar stream’den düşer ve replika yeniden kurulum ister. Tasarım bütçesi budur: normal işleyişte gerçek zamana yakınlık, bir insana haber gitmeden önce bir dakika, kurtarmanın otomatik olmaktan çıkmasına kadar bir gün.

Maliyet sırasına göre hafifletmeler:

  1. Yazanın bir sonraki okumasını DynamoDB’ye geri yönlendirin. Mutasyondan sonra detay görünümü öğeyi anahtarıyla, gerekiyorsa strongly consistent çeker. Ucuzdur ve en yaygın şikayeti kapatır.
  2. Yeni yazılan öğeyi ilk arama sayfasına ekleyin. API katmanı oluşturduğu öğeyi elinde tutar ve kısa bir pencere boyunca o kullanıcının ilk sayfasının başına iliştirir. Yeni altyapı yok; replika yetişince biraz uğraştıran bir tekilleştirme var. Bu ancak, öğenin aktif filtresine gerçekten uyduğu, en yeniden eskiye sıralı bir listede dürüst kalır; fiyat, puan ya da alaka sıralamasında öğe ya doğru sıralama konumuna girer ya da sayfada hiç yer almaz. Dolu bir sayfanın başına ekleme sınırı da kaydırır: ilk sayfa boyu kadar öğeyi döndürün ve sonraki token’ı orijinal yanıttan değil, gerçekten gönderilen son arama isabetinden üretin; yoksa taşan son isabet, okuyucu devam ettiğinde atlanır.
  3. Yazmanın yüzeye çıkmasını bekleyin. Öğeye uygulamanın kontrol ettiği bir revizyon attribute’u damgalayın, onu include_keys üzerinden index’e taşıyın (pipeline, listenin dışındaki her şeyi ayıklar) ve onaylamadan önce tam o revizyonu yoklayın; stream’in atadığı document_version burada işe yaramaz, çünkü yazan taraf o değeri hiç öğrenmez ve eşzamanlı bir yazma beklenen yazma gibi görünebilir. Doğru kurulmuş bir yoklama bile yalnızca onayı geciktirir; yoklama başladığında DynamoDB yazması çoktan kalıcıdır, bu yüzden zaman aşımı yolunun da kendi cevabı olmalıdır. Bu, yazma yoluna gecikme ekleyerek daha güçlü bir garanti satın alır; yalnızca ürünün gerçekten talep ettiği yerde kullanılmalıdır.
  4. Arayüzde doğruyu gösterin. İlgili satırda bir eşitleniyor göstergesi en az maliyetlidir ve çoğu zaman doğru cevaptır; çünkü satır gerçekten eşitlenmektedir.

İşe yaramayan tek şey, bu boşluğu tasarlamadan bırakıp destek biletlerinin keşfetmesine izin vermektir. Bayatlık penceresi, sayfalama token formatının yanında API sözleşmesine yazılmalıdır.

İkinci Deponun Bedeli#

Para tabanı liste fiyatlarından tek başına görünüyor. Bu pipeline’ı çalıştırmadığım için aşağıdaki hesap yayınlanmış fiyatlandırma (yeni sekmede açılır) üzerinde aritmetiktir ve bir tahmin olarak okunmalıdır. OpenSearch Ingestion OCU-saat başına ölçer; us-east-1’de liste fiyatı bu satırlar yazılırken USD 0,24 ve pipeline, sayacı trafik olsun olmasın işleyen, sürekli açık bir kapasitedir: gün boyu çalışan bir OCU, 730 saat çarpı USD 0,24, kabaca ayda USD 175 eder; hedef depo ve depolama hariç. AWS ayrıca yüzde 99,9 pipeline erişilebilirliği için en az iki Ingestion OCU öneriyor (yeni sekmede açılır); bu da daha başka hiçbir şey sayılmadan o kalemi ikiye katlar. Hedef tarafta yol ikiye ayrılır. Yönetilen bir domain, seçtiğiniz instance boyutunda kesintisiz çalışır. Klasik bir OpenSearch Serverless koleksiyonu aynı fiyattan en az 2 OCU (yeni sekmede açılır) faturalar, kabaca ayda USD 350; dev-test seçeneği bunun yarısıdır ve taban hesap başına uygulanır, koleksiyonlar arasında paylaşılır, koleksiyon başına yeniden ödenmez. NextGen koleksiyon grupları cevabı tümden değiştirir: minimumları 0 OCU’dur ve 10 dakika hareketsizlikten sonra sıfıra inerler (yeni sekmede açılır); bedeli, uyanma sonrası ilk istekte 10-30 saniyelik gecikmedir. Hangi tabanın size uygulanacağı, birçok ekibin henüz değerlendirmediği bir koleksiyon tipine bağlı; bu sayıların ortalamasını almak yerine fiyat sayfasını kendi Region’ınıza karşı kontrol edin. Pipeline’lar boştayken durdurulabilir ve durdurulmuş pipeline OCU saati işletmez; staging ortamları için bu önemlidir. Yine de tasarrufun bir kenarı var: stream’in 24 saatlik tutma süresini aşacak kadar duran pipeline tam bir yeniden export’a döner; geceleri durdurmayı alışkanlık yapmadan önce DynamoDB kaynağının durdur-başlat davranışını kontrol edin.

Güvenlik yüzeyi, fiyat listesinde görünmeyen bir biçimde büyür. Pipeline rolüne tabloda dynamodb:ExportTableToPointInTime artı stream okuma izinleri, export bucket’ında hem yazma hem geri okuma erişimi (pipeline anlık görüntüyü oraya yazar, sonra oradan yükler), DLQ bucket’ında yazma izni (bucket’lar şifreliyse ilgili KMS izinleriyle birlikte) ve hedef domain’e imzalı HTTP erişimi gerekir. Daha da önemlisi, index artık OpenSearch’ün kendi erişim kontrol modelinin yönettiği bir üretim verisi kopyası tutar; DynamoDB’nin IAM tabanlı modeli orada geçmez. IAM politikalarının tabloya sağladığı alan düzeyi yetkilendirme arama katmanında yeniden kurulmalı ya da hassas alanlar index’e hiç girmemelidir; include_keys burada aynı zamanda bir güvenlik kontrolü olarak da iş görür; ama yalnızca index için: export bucket’ı ile DLQ aynı verinin kopyalarını taşır ve kendi saklama ile erişim kurallarını ister.

Operasyonel yük ise tekrarlayan kısımdır. Bir mapping şablonu, değişeceği gün için bir reindex prosedürü gerektirir. Bir DLQ, yeniden oynatma script’i gerektirir ve bu script en iyi, test edecek zaman varken, ilk backfill’den önce yazılır. Gecikme, pipeline’ın EndToEndLatency ve PipelineLatency metriklerinde alarm ister; başlangıç eşiği yukarıdaki bir dakikadır. Kapasitenin dokümante formülleri vardır: best-practice sayfası provisioned tablolar için pipeline’ı (tablo WCU / 1000) - 1 ile (tablo WCU / 1000) + 1 OCU arasında, tabanı bir olacak şekilde boyutlandırır; kabaca bir OCU saniyede 1 MB taşır ve her OCU paralelde 150’ye kadar stream shard’ı işler. Bu işlerin hiçbiri egzotik değil; hepsi birlikte, tamamen ikinci depoya bağlı ikinci bir nöbet yüzeyi oluşturur.

Tek PostgreSQL’in Kazandığı Yer#

Yukarıdaki mimariye karşı en güçlü argüman tek bir ilişkisel veritabanıdır ve eksiksiz bir cevap hak eder. Tek bir PostgreSQL veya Aurora instance’ı nokta okumaları, index’lenmiş herhangi bir sütunda keyfi ORDER BY’ı, keyset veya offset sayfalamayı, kesin COUNT(*)’ı, anlık filtre kombinasyonlarını ve tsvector artı GIN index ile hatırı sayılır bir tam metin aramayı tek depoda, tek sorgu diliyle, tek yedekleme hikâyesiyle ve tek tutarlılık modeliyle yapar. OpenSearch replikasını gerekçelendiren her özellik orada bir biçimde vardır; replikasyon gecikmesi olmadan, mapping şablonu olmadan, ikinci fatura olmadan.

İlişkisel tavanlar gerçektir ama sanıldığından geç gelir. Dokümante edilmiş metin arama sınırları (yeni sekmede açılır) arasında doküman başına 1 MB altı tsvector ve 2 KB altı lexeme vardır; ürün ve back-office araması için cömert sınırlardır. OFFSET sayfalaması derinlikle doğrusal bozulur ve çözümü, bir arama motorundan çok önce keyset sayfalamadır. Kesin COUNT(*) vardır ama bedava değildir: büyük, seçici olmayan bir sonuç kümesinde o da bir taramadır ve diğer her şeyle aynı indexleme ve ölçüm disiplinini ister. OpenSearch’e karşı gerçek eksikler alaka düzeyi ayarı, yazım hatası toleransı ve büyük ölçekte facet ergonomisidir; bir ekip bunlara bedel ödemeden önce onlara ihtiyacı olduğunu doğrulamalıdır.

Yani çatal, nerede durduğunuzla ilgili. Kayıt sistemi hâlâ seçilmemişse ya da erişim desenleri hiçbir zaman sayılabilir olmadıysa: ilişkisel veritabanını seçin, iki depo sorusu kendiliğinden dağılır. DynamoDB zaten kayıt sistemiyse ve yazma yolu her yazma hızında tek haneli milisaniyelik anahtar erişimine, on-demand modda sıfıra inen throughput faturasına ve bağlantı havuzu derdinin yokluğuna dayanıyorsa: o zaman zero-ETL replikası bu güçleri koruyan bir ektir; Postgres’e taşınmak ise bir okuma sorununu çözmek için yazma yolunu başka bir platforma taşımak olurdu. Takas ters yönde de işler; ama eskisi kadar keskin değil: provisioned, hep açık bir Aurora instance’ının faturası hiçbir zaman sıfıra inmez, Aurora Serverless v2 boştayken uyanma gecikmesi pahasına sıfır ACU’ya duraklayabilir (yeni sekmede açılır), sakin bir on-demand DynamoDB tablosu ise hiçbir ayar gerektirmeden throughput faturasını keser; depolama her ikisinde de işlemeye devam eder. İki depolu mimariyi, DynamoDB yazma yolunda hakkını verdiğinde seçin. Baştan yanlış olan bir depo seçimini kurtarmak için ona uzanmak, yanlış ilk sistemin üstüne bir ikincisini eklemek olur.

Key-Value Çerçevesinin İddia Etmediği Şeyler#

DynamoDB’ye önceden modellenmiş aralık okumaları olan bir key-value deposu demek haklı bir itirazı davet eder: platform görünür biçimde bundan fazlasıdır. Single-table tasarım öğe koleksiyonlarını, sparse index’leri ve overloaded GSI’ları zengin erişim desenlerine dönüştürür ve bunu tam da desenler önceden adlandırıldığı için yapar; yönlendirme kuralında ona karşı tek bir söz yoktur. DynamoDB artık yaklaşık en yakın komşu sorguları için yerleşik vektör index’leri ve bir SearchVectors API’si de taşıyor; bu, veritabanının içinde yeni bir sorgu şeklidir. Vektör arama dokümantasyonu (yeni sekmede açılır) ve kotaları sınırı da çizer: ANN araması, istek başına 100 sonuçluk TopK tavanıyla anlamsal benzerlik döndürür; yanıtları sayfalama olmadan 16 MB ile sınırlıdır, satır içi filtreleri yalnızca eşitlik kabul eder ve keyfi sıralama, boolean facet’ler, kesin toplamlar veya sözcüksel eşleşme eklemez. Derin sayfalanan, filtreli, sıralı bir liste ekranı onun şeklinin dışındadır.

Key-value çerçevesi, istek anında kurulan okumalar hakkında bir iddiadır; yalnızca onlar hakkında. Tasarım anında adlandırabildiğiniz sorular, ne kadar gelişkin olursa olsun key modeline aittir. İstek anında kurulan sorular ait değildir ve bu çizgi yok sayıldığında biriken şey, girişteki belirtilerdir.

Bu Tasarımın İlk Kırıldığı Yerler#

  • Yazma faturası ikiye katlanana kadar sıralama başına bir GSI. Bağımsız sıralama attribute’larını sayın; iki üçü geçince, bir sonraki index’i göndermeden önce replikanın fiyatını çıkarın.
  • Öğenin tamamını index’e aynalamak. Mapping şablonu artı include_keys kullanın ve tam öğeleri DynamoDB’den ID ile çekin. Index’lenen her şey, arama katmanının erişim modeline de açılmış olur.
  • Tipleri dinamik mapping’e bırakmak. Çıkarımla long olmuş bir alana gelen ilk kesirli değer dokümanları DLQ’ya gönderir. Sayısal tipleri, sıralama ve facet alanları için keyword’ü, serbest metin için text’i açıkça bildirin.
  • İlk backfill’den önce DLQ olmaması. Kuyruğu pipeline ile birlikte yapılandırın, yeniden oynatma script’ini aynı gün yazın.
  • Stream okuyucu envanterini atlamak. Shard başına iki okuyucu süreç tasarım sınırıdır; mevcut bir Lambda tüketicisi artı pipeline bunu doldurur.
  • Replikayı doğruluk kaynağı sanmak. Index’e asla doğrudan yazmayın. Index’lenen her attribute tablodan yeniden kurulabilir olmalıdır; çünkü kurtarma prosedürü yeniden kurulumdur.
  • Sayfalama token’larında search_after değerlerini sızdırmak. Sıralama değerleri iç veridir; token’ı, sayfalama yazısının LastEvaluatedKey için yaptığı gibi imzalayın ve kapsama bağlayın; değerler okunmaz kalacaksa şifreleyin.
  • Çare olarak elde tutulan facet sayaçları. Facet değeri başına bir sayaç öğesi, ani yük altında sıcak anahtar problemini yeniden yaratır; rate-limit stratejileri yazısı bunun replikadan önce nasıl kırıldığını gösteriyor.

Pipeline Çalışınca İzlenecek Metrikler#

Bunların hiçbiri henüz çalışmadığı için elde olan, sonuçlar değil bir ölçüm planıdır. Önce replikasyon gecikmesi: pipeline’ın EndToEndLatency ve PipelineLatency ortalamaları, AWS önerisine göre 60 saniyede alarm kurulmalı ve gecikmenin önce kendini gösterdiği yazma patlamaları sırasında izlenmeli. DLQ doküman sayıları sıfırda durmalı; sürekli sıfırdan farklı bir oran çoğunlukla önce mapping şablonunu işaret eder, ama izinler, aşırı büyük dokümanlar ve sink retleri de aynı kuyruğa düşer. Sink’in versiyon çakışması hataları ise ayrı bir sinyaldir: mapping’i değil yazma yolunu, index’e yazan ikinci bir süreci ya da sırası bozulmuş document_version değerlerini gösterir. Depo başına trafik payı yönlendirme kuralının tutup tutmadığını söyler: DynamoDB yolu neredeyse hiçbir şeye cevap vermiyorsa ya kimlik okumaları yanlış yönlendiriliyordur ya da ürün gerçekten arama ağırlıklıdır; karar vermeden önce hangisi olduğunu bulmak gerekir. Derin sayfa oranı, from değeri 1.000’i aşan istekler, 10.000 penceresinin ne zaman bir destek biletine dönüşeceğini önceden haber verir. Index boyutunun tablo boyutuna oranı include_keys’in işini yapıp yapmadığını gösterir. Ve ana tablodaki GSI sayısı arama gerekçesiyle artık büyümemeli; çünkü replika bunları emmek için var. Gerçekten kimlik-şekilli yeni bir desen için GSI eklemekse meşru kalır.

Ayrım şu koşullarda geçerli: DynamoDB zaten kayıt sistemidir, yazma yolu deponun güçlü yanlarına dayanır ve arama şeklindeki gereksinimler gelmeye devam eder. Kayıt sistemi hâlâ açıksa veya erişim desenleri hiçbir zaman sayılabilir olmadıysa tek PostgreSQL’e yönelin; görünürdeki tek arama gereksinimi bir back-office ekranıysa replikayı tümden erteleyin. İlk somut adım gösterişsiz: pipeline’ı oluşturmadan önce mapping şablonunu ve DLQ yeniden oynatma script’ini yazın, kullanıcıya dönük trafik index’e bağlanmadan önce ilk export’u staging tablosunda koşturun.

Kaynaklar#

İlgili yazılar