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.
Ü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:
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:
- 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.
- 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.
- 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_versionburada 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. - 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_keyskullanı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
longolmuş bir alana gelen ilk kesirli değer dokümanları DLQ’ya gönderir. Sayısal tipleri, sıralama ve facet alanları içinkeyword’ü, serbest metin içintext’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_afterdeğerlerini sızdırmak. Sıralama değerleri iç veridir; token’ı, sayfalama yazısınınLastEvaluatedKeyiç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#
- DynamoDB zero-ETL integration with Amazon OpenSearch Service (yeni sekmede açılır) - İki aşamalı tasarım: S3’e point-in-time export, ardından DynamoDB Streams replikasyonu; ön koşullar PITR ve Streams
- Using an OpenSearch Ingestion pipeline with Amazon DynamoDB (yeni sekmede açılır) - Pipeline YAML’ı, tip çıkarımı davranışı, OCU başına 150 shard kuralı ve dokümante edilmiş sınırlar
- Best practices for working with DynamoDB zero-ETL integration and OpenSearch Service (yeni sekmede açılır) - Doküman ID’si olarak primary key, include_keys ile mapping şablonları, bir dakikalık gecikme alarmı ve OCU boyutlandırma formülleri
- Quotas in Amazon DynamoDB (yeni sekmede açılır) - Varsayılan 20 GSI kotası, shard başına iki okuyucu rehberliği ve vektör index kotaları
- NoSQL design for DynamoDB (yeni sekmede açılır) - AWS’nin çerçevesi: veri yalnızca sınırlı sayıda yolla verimli sorgulanır ve tasarım bilinen sorulardan başlar
- What is Amazon DynamoDB? (yeni sekmede açılır) - Servisin JOIN desteği olmayan key-value ve doküman veritabanı olarak tanımı
- Paginate results - OpenSearch Documentation (yeni sekmede açılır) - from ve size üzerindeki 10.000 sonuç tavanı, search_after’ın durumsuzluğu ve derin sayfalamada tercih edilen yöntem olarak PIT
- Point in time search in Amazon OpenSearch Service (yeni sekmede açılır) - keep_alive semantiği ve PIT durumunun node yeniden başlatmaları ile blue/green dağıtımlarda kaybı
- Search API - OpenSearch Documentation (yeni sekmede açılır) - track_total_hits, from, size, sort ve search_after için istek gövdesi referansı
- Set track_total_hits=false by default for _search from 3.x (yeni sekmede açılır) - Davranışı değiştirmeden yayınlanmış 3.x hattını hedefleyen, bugünkü varsayılanı yine de en iyi anlatan hareketsiz teklif: 10.000’e kadar sayım, ötesinde relation gte
- Amazon OpenSearch Service pricing (yeni sekmede açılır) - OpenSearch Ingestion ve Serverless OCU-saat liste fiyatları, klasik koleksiyonların 2 OCU minimumu ile dev-test yarı tabanı ve maliyet tabanı aritmetiği
- Index settings - OpenSearch Documentation (yeni sekmede açılır) - Offset sayfalamayı sınırlayan 10.000 varsayılanlı index.max_result_window ayarı
- Scaling pipelines in Amazon OpenSearch Ingestion (yeni sekmede açılır) - Yüzde 99,9 pipeline erişilebilirliği için en az iki Ingestion OCU önerisi
- Automatic pause and resume for Aurora Serverless v2 (yeni sekmede açılır) - Aurora Serverless v2’nin boşta sıfır ACU’ya duraklaması ve saniyelerle ölçülen devam etme süresi
- Scale to zero for Amazon OpenSearch Serverless (yeni sekmede açılır) - NextGen koleksiyon gruplarının 10 dakika hareketsizlikte 0 OCU’ya inmesi ve 10-30 saniyelik uyanma gecikmesi
- Using vector indexes in DynamoDB (yeni sekmede açılır) - Yaklaşık en yakın komşu index’lerinin ne eklediği ve neyin yerine geçmediği
- PostgreSQL: Text Search Limitations (yeni sekmede açılır) - İlişkisel alternatifi boyutlandırmak için tsvector ve lexeme tavanları
İlgili yazılar
DynamoDB'de OFFSET, keyfi ORDER BY ve ucuz COUNT yok. İmzalı next/prev cursor sunun, sıralamayı sort key ile modelleyin, toplam sayıyı listeden çıkarın.
dynamodb · aws · data-storage-orm +2
Aurora Serverless v2'nin altında ne çalışıyor: paylaşılan depolama katmanı, ACU temelli işlem, Caspian altyapısı, sıfıra ölçekleme ve karışık modlu cluster'lar.
aws · data-storage-orm · architecture +1
Çok takımlı AWS organizasyonları için platform varsayılanı: tek event, birçok consumer, her biri kendi hesabında SQS ve DLQ'suyla; fan-out bus katmanında.
aws · eventbridge · event-driven +4
Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.
distributed-systems · microservices · event-driven +5
Amazon SNS ve SQS ile güvenli cross-account event dağıtımı: IAM policy'leri, KMS şifreleme, AWS CDK kurulumu ve production'da karşılaşılan yaygın sorunlar.
aws · sns · sqs +6