Orta7 dakikalık okuma

Bir DynamoDB GSI Neden Nihai Tutarlıdır

Bir öğe yazarsın, hemen onun için bir Global Secondary Index sorgularsın ve geri hiçbir şey almazsın — yazma başarılı olmuş ve temel tabloya bir GetItem öğeyi gayet iyi döndürse bile.

Bozuk bir şey yok. GSI'lerin en şaşırtıcı özelliğine çarptın: bir GSI'nin her okuması nihai tutarlıdır (eventually consistent). Bir yazmadan sonra, index'in henüz yetişmediği kısa bir pencere vardır.

DynamoDB GSI'leri nihai tutarlı mıdır?

Evet — bir Global Secondary Index'in her okuması nihai tutarlıdır (eventually consistent) ve bundan vazgeçmenin bir yolu yoktur. Yazman önce temel tabloya işlenir, sonra index'e asenkron olarak yayılır; bu yüzden bir yazmadan hemen sonra yapılan bir sorgu eski (stale) ya da eksik satırlar döndürebilir. DynamoDB bir GSI için ConsistentRead bayrağı sunmaz.

  • Bir GSI, ayrı, asenkron olarak çoğaltılan bir tablodur — yazman önce temel tabloya işlenir, sonra index'e yayılır.
  • Bir GSI için ConsistentRead bayrağı yoktur. Temel tablonun aksine, boşluğu kapatmak için güçlü bir okumayı zorlayamazsın.
  • Kendi yazmalarını GSI'den değil, temel tablodan oku. Bir yazmadan hemen sonra birincil anahtarı zaten elinde tutuyorsun.
  • Benzersizliği bir GSI sorgusuyla değil, koşullu bir yazmayla zorla. Yayılım boşluğu, bir "bu alındı mı?" kontrolünü bir yarışa dönüştürür.

Belirti: "kendini bulamayan" bir kayıt

Bir kullanıcı-hesapları servisi için bir Members tablosu al. Temel tablo dahili bir id ile anahtarlanmıştır, ama kullanıcılar e-posta ile giriş yapar, bu yüzden bir e-posta-arama GSI'si vardır:

Members (temel tablo)
PKSKemaildisplayName
ACC#a1f9cPROFILEada@northwind.testAda L.
EmailIndex (GSI)
GSI1PKGSI1SK
ada@northwind.testACC#a1f9c

Kayıt akışı arka arkaya iki şey yapar: yeni üyeyi PutItem eder, sonra başka kimsenin o adresi almadığını kontrol etmek ve profili yüklemek için Query EmailIndex WHERE GSI1PK = "ada@northwind.test" yapar.

Bu iki çağrıyı birkaç milisaniye arayla çalıştır ve Query sıfır öğe döndürebilir. Bunu bir saniye sonra tekrar yap ve satır oradadır. Yazma başarısız olmadı — index henüz güncellenmemişti.

Bu neden olur: GSI'ler asenkron olarak çoğaltılır

Bir GSI, kendi partition'ları ve kendi anahtar şeması olan ayrı, dahili olarak yönetilen bir tablodur. Temel-tablo yazmanla aynı işlem (transaction) içinde sürdürülmez.

PutItem yaptığında, DynamoDB temel tabloya dayanıklı olarak işler, yazmanı onaylar ve sonra değişikliği her GSI'ye asenkron olarak yayar. AWS GSI dokümantasyonu bunu açıkça belirtir: GSI'ler yalnızca nihai tutarlı okumaları destekler.

Bir temel-tablo yazması ile index güncellemesi arasındaki yayılım gecikmesi genellikle bir saniyenin kesridir — ama yük altında garanti edilmez ve sınırlandırılmaz. Sanki sınırlıymış gibi tasarlamak tuzaktır.

Bu bir hata değil; orijinal Dynamo tasarım ödünleşimidir. 2007 Amazon Dynamo makalesi, güçlü tutarlılık yerine erişilebilirliği ve partition toleransını seçti.

GSI'ler o soyu miras alır. Gevşek bağlantı, index'in temel tablodan bağımsız olarak ölçeklenmesini ve yazılabilir kalmasını sağlayan şeydir.

EmailIndexTemel tabloAppEmailIndexTemel tabloAppasenkron yayılımPutItem (yeni üye)200 OKE-posta ile Query0 öğe (eski)değişikliği çoğaltE-posta ile Query1 öğe (yetişti)

200 OK ile "değişikliği çoğalt" arasındaki boşluk, index okumanın eski (stale) olduğu penceredir. Onu kapatan hiçbir tutarlı-okuma bayrağı yoktur.

Temel tablonun aksine — güçlü tutarlı bir GetItem/Query'yi zorlamak için ConsistentRead = true geçtiğin yer — bir GSI o seçeneği düpedüz reddeder.

Bir LSI güçlü olarak okunabilir, çünkü temel tablonun partition'larını paylaşır; o ayrımın neden var olduğu için bkz. GSI ve LSI.

Daha ince bir tuzak: yalnızca eksik yeni değerler değil, eski eski değerler

Eksik-satır durumu bariz olandır. Daha sessiz hata, eski bir önceki değeri okumaktır.

Diyelim ki Ada e-postasını ada@northwind.test'ten ada.l@northwind.test'e değiştiriyor. Temel tablo atomik olarak güncellenir, ama bir an için GSI hâlâ eski index girişini döndürebilir.

Yeni değere karşı bir arama ıskalar, terk edilmiş değer ise hâlâ çözülür.

Daha kötüsü: GSI'yi sorgular ve okuduğun şeye göre geri yazarsan, artık var olmayan bir değere göre işlem yapabilirsin. Herhangi bir GSI okumasını, gerçeğin gerisinde kalabilen bir anlık görüntü olarak ele al.

Etrafında tasarla — onunla savaşma

Yayılım penceresi gerçektir, bu yüzden düzeltme mimaridir, ayarladığın bir yeniden deneme düğmesi değil. Dört desen, kabaca tercih sırasıyla:

  1. Kendi yazmalarını temel tablodan oku. Bir yazmadan hemen sonra birincil anahtarı (ACC#a1f9c) zaten elinde tutuyorsun, bu yüzden GSI'yi sorgulamak yerine temel tabloda güçlü tutarlı bir GetItem yap.

    GSI, diğer erişim deseni içindir — "elimde bir e-posta var, hesabı bul" — az önce yaptığın yazmayı onaylamak için değil.

  2. Benzersizliği GSI ile değil, bir koruma öğesiyle zorla. Bir e-postanın alınmadığını kanıtlamak için asla bir GSI sorgusuna güvenme — yayılım boşluğu bunu, eşzamanlı iki kaydın ikisinin de kaybedebileceği bir yarış haline getirir.

    Bunun yerine, bir ConditionExpressionattribute_not_exists(PK) olan bir TransactWriteItems içinde, e-postanın kendisi üzerinde anahtarlanmış (PK = "EMAIL#ada@northwind.test") özel bir benzersizlik öğesi yaz.

    Atomik olarak uygulanan güçlü tutarlı temel-tablo koşulları, benzersizliği gerçekten zorlayan şeydir.

    TransactWriteItems:
      - Put member item    (PK = ACC#a1f9c, SK = PROFILE)
      - Put uniqueness item (PK = EMAIL#ada@northwind.test)
          ConditionExpression: attribute_not_exists(PK)

    İkinci bir kayıt aynı adres için yarışırsa, koşulu başarısız olur ve tüm işlem reddedilir — GSI yok, yayılım gecikmesi yok, çift talep yok.

    O attribute_not_exists koşulunu, koda bağlamadan önce DynamoDB Expression Builder ile oluştur ve önizle.

  3. Gecikmeyi UX'te tolere et. GSI okuması gerçekten doğru araç olduğunda (var olan bir kullanıcı için e-posta ile giriş), pencere saniyenin altındadır ve zararsızdır — yerleşik bir hesap çok önce yayılmıştır.

    Güçlü tutarlı temel-tablo yolunu yalnızca yazmadan-sonra-okuma anına ayır.

  4. Yeniden sorgula, varsayma. Bir iş akışı yepyeni bir öğeyi GSI üzerinden gözlem etmek zorundaysa, boş bir sonucu "henüz görünmüyor" olarak ele al, "yok" olarak değil ve kısa bir geri çekilme (backoff) sonrası yeniden sorgula.

    Ama tahmin işini tamamen ortadan kaldıran 1 ve 2 numaralı desenleri tercih et.

Yayılım boşluğunu kendin gör

Sezgi geliştirmenin en hızlı yolu bunu olurken izlemektir. DynoTable'da temel tabloya bir öğe koyar ve hemen ikinci bir sekmede GSI'yi sorgularsın.

Yüklü bir tabloda, index'in temel verinin gerisinde kaldığını ara sıra yakalayacaksın, sonra bir sonraki yenilemede yakınsadığını izleyeceksin.

Gecikmeyi kendi verinle görmek, "kendi yazmalarını temel tablodan oku" kuralını herhangi bir diyagramdan çok daha iyi yerleştirir.

Tuzaklar ve sonraki adımlar

  • Mantığı bir GSI yazma-sonrası-okumasına kilitleme. Benzersizlik kontrolleri, "yazmam yerine oturdu mu" onayları ve oku-değiştir-yaz döngüleri, güçlü tutarlı temel tabloya aittir.
  • Bir GSI'de ConsistentRead'e uzanma — buna izin verilmez ve hata verir.
  • Temel anahtar zaten yanıtladığında bir erişim desenini bir GSI olarak modelleme. Bir okumayı birincil anahtardan sun ve yayılım penceresini tamamen atla.

Doğru anahtar şeklini seçmek single-table tasarımda tüm oyundur; bir Query'nin bir Scan'i ne zaman yendiğini bilmek seni en baştan index'ten uzak tutar (Query ve Scan).

Benzersizlik ConditionExpression'ını DynamoDB Expression Builder'da oluştur ve test et. Sonra temel-tablo yazmalarının bir GSI'ye gerçek zamanlı yayıldığını izlemek ve anahtarlarını nihai tutarlılık penceresi seni asla ısırmayacak şekilde tasarlamak için DynoTable'ı dene.

Güncellendi