Udostępnij przez


Jak modelować i partycjonować dane przy użyciu rzeczywistego przykładu

W tym artykule przedstawiono kilka pojęć związanych z usługą Azure Cosmos DB, takich jak modelowanie danych, partycjonowanie i aprowizowana przepływność , aby zademonstrować sposób rozwiązywania rzeczywistych problemów z projektowaniem danych.

Jeśli zwykle pracujesz z relacyjnymi bazami danych, prawdopodobnie wyrobiłeś sobie nawyki dotyczące projektowania modeli danych. Ze względu na określone ograniczenia, ale także unikatowe mocne strony usługi Azure Cosmos DB, większość z tych najlepszych rozwiązań nie przekłada się dobrze i może przeciągnąć Cię do nieoptymalnych rozwiązań. Celem tego artykułu jest przeprowadzenie pełnego procesu modelowania rzeczywistego przypadku użycia w usłudze Azure Cosmos DB, od modelowania elementów po kolokację jednostek i partycjonowanie kontenerów.

Przykład ilustrujący pojęcia przedstawione w tym artykule, pobierz lub wyświetl kod źródłowy wygenerowany przez społeczność.

Ważne

Członek społeczności przekazał ten przykład kodu. Zespół usługi Azure Cosmos DB nie wspiera jego utrzymania.

Scenariusz

W tym ćwiczeniu rozważymy domenę platformy blogowania, w której użytkownicy mogą tworzyć wpisy. Użytkownicy mogą również lubić i dodawać komentarze do tych wpisów.

Wskazówka

Niektóre wyrazy są wyróżnione kursywą , aby zidentyfikować rodzaj "rzeczy", którymi manipuluje nasz model.

Dodawanie kolejnych wymagań do naszej specyfikacji:

  • Na pierwszej stronie wyświetla się kanał wpisów ostatnio utworzonych.
  • Możemy pobrać wszystkie wpisy dla jednego użytkownika, wszystkie komentarze do jednego wpisu i wszystkie polubienia do jednego wpisu.
  • Wpisy są zwracane z nazwą użytkownika autorów oraz liczbą komentarzy i polubień, które posiadają.
  • Komentarze i polubienia są również zwracane z nazwą użytkownika użytkowników, którzy je utworzyli.
  • Gdy są wyświetlane jako listy, wpisy muszą przedstawiać tylko obcięte podsumowanie ich zawartości.

Identyfikowanie głównych wzorców dostępu

Aby rozpocząć, nadajemy strukturę naszej wstępnej specyfikacji, identyfikując wzorce dostępu naszego rozwiązania. Podczas projektowania modelu danych dla usługi Azure Cosmos DB ważne jest, aby zrozumieć, które żądania ma obsługiwać nasz model, aby upewnić się, że model obsługuje te żądania wydajnie.

Aby ułatwić zrozumienie ogólnego procesu, dzielimy te różne żądania na polecenia i zapytania, pożyczając słownictwo z podziału odpowiedzialności poleceń i zapytań (CQRS). W usłudze CQRS polecenia to żądania zapisu (czyli intencje aktualizacji systemu), a zapytania są żądaniami tylko do odczytu.

Oto lista żądań uwidacznianych przez naszą platformę:

  • [C1] Tworzenie lub edytowanie użytkownika
  • [Q1] Pobierz użytkownika
  • [C2] Tworzenie lub edytowanie wpisu
  • [Q2] Pobieranie wpisu
  • [Q3] Lista wpisów użytkownika w skróconej formie
  • [C3] Tworzenie komentarza
  • [Q4] Wyświetl listę komentarzy do wpisu
  • [C4] Polubić post
  • [Q5] Wyświetlanie listy polubień wpisu
  • [Q6] Wyświetl x najnowszych wpisów utworzonych w skróconej formie (kanał informacyjny)

Na tym etapie nie myśleliśmy o tym, co zawiera każda jednostka (użytkownik, wpis itp.). Ten krok jest zwykle jednym z pierwszych, które należy rozwiązać podczas projektowania w odniesieniu do magazynu relacyjnego. Najpierw zaczynamy od tego kroku, ponieważ musimy ustalić, jak te jednostki tłumaczą się pod względem tabel, kolumn, kluczy obcych itd. Jest to znacznie mniej istotne w przypadku bazy danych dokumentów, która nie wymusza żadnego schematu podczas zapisu.

Ważne jest, aby zidentyfikować nasze wzorce dostępu od początku, ponieważ ta lista żądań będzie naszym zestawem testów. Za każdym razem, gdy iterujemy nasz model danych, przechodzimy przez każde z żądań i sprawdzamy jego wydajność i skalowalność. Obliczamy jednostki żądań (RU) używane w każdym modelu i optymalizujemy je. Wszystkie te modele korzystają z domyślnych zasad indeksowania i można je zastąpić poprzez indeksowanie konkretnych właściwości, co może dodatkowo zoptymalizować zużycie jednostek Rzeczywistości Jednostek oraz zmniejszyć opóźnienia.

Wersja 1: pierwsza wersja

Zaczynamy od dwóch kontenerów: users i posts.

Kontener użytkowników

Ten kontener przechowuje tylko elementy użytkownika:

{
    "id": "<user-id>",
    "username": "<username>"
}

Dzielimy ten kontener na partycje za pomocą idmetody , co oznacza, że każda partycja logiczna w tym kontenerze zawiera tylko jeden element.

Kontener postów

Ten kontener zawiera elementy, takie jak wpisy, komentarze i polubienia.

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "title": "<post-title>",
    "content": "<post-content>",
    "creationDate": "<post-creation-date>"
}

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "creationDate": "<like-creation-date>"
}

Dzielimy ten kontener na partycje za pomocą metody postId, co oznacza, że każda partycja logiczna w tym kontenerze zawiera jeden wpis, wszystkie komentarze dla tego wpisu i wszystkie polubienia dla tego wpisu.

Wprowadziliśmy type właściwość w elementach przechowywanych w tym kontenerze, aby odróżnić trzy typy jednostek hostujących ten kontener.

Ponadto wybraliśmy odwołanie do powiązanych danych zamiast osadzać je, ponieważ:

  • Nie ma górnego limitu liczby wpisów, które użytkownik może utworzyć.
  • Wpisy mogą być dowolnie długie.
  • Nie ma górnego limitu liczby komentarzy i polubień, które mogą mieć post.
  • Chcemy mieć możliwość dodania komentarza lub polubienie wpisu bez konieczności aktualizowania samego wpisu.

Aby dowiedzieć się więcej na temat tych pojęć, zobacz Modelowanie danych w usłudze Azure Cosmos DB.

Jak dobrze działa nasz model?

Nadszedł czas, aby ocenić wydajność i skalowalność naszej pierwszej wersji. Dla każdego z zidentyfikowanych wcześniej żądań mierzymy jego opóźnienie i liczbę jednostek żądań, z których korzysta. Ten pomiar jest wykonywany względem fikcyjnego zestawu danych zawierającego 100 000 użytkowników z 5 do 50 wpisów na użytkownika, a do 25 komentarzy i 100 polubień na post.

[C1] Tworzenie lub edytowanie użytkownika

To żądanie jest proste do zaimplementowania, ponieważ po prostu utworzymy lub zaktualizujemy element w kontenerze users . Żądania są równomiernie rozłożone na wszystkie partycje dzięki kluczowi partycji id.

Diagram przedstawiający zapisywanie pojedynczego elementu w kontenerze użytkowników.

Latency Jednostki żądania Wydajność
7 ms 5.71 RU

[Q1] Pobieranie użytkownika

Pobieranie użytkownika odbywa się przez odczytanie odpowiedniego elementu z kontenera users .

Diagram pobierania pojedynczego elementu z kontenera users.

Latency Jednostki żądania Wydajność
2 ms 1 RU

[C2] Tworzenie lub edytowanie wpisu

Podobnie jak [C1], po prostu musimy zapisać do kontenera posts.

Diagram przedstawiający zapisywanie pojedynczego elementu wpisu w kontenerze postów.

Latency Jednostki żądania Wydajność
9 ms 8.76 RU

[Q2] Pobieranie wpisu

Zaczynamy od pobrania odpowiedniego dokumentu z kontenera posts . Ale to nie wystarczy, zgodnie z naszą specyfikacją, musimy również zagregować nazwę użytkownika autora wpisu, liczbę komentarzy i liczbę polubień dla wpisu. Wymienione agregacje wymagają wydania trzech kolejnych zapytań SQL.

Diagram pobierania wpisu i agregowania dodatkowych danych.

Każde zapytanie filtruje po kluczu partycji odpowiedniego kontenera, co jest dokładnie tym, czego chcemy, aby zmaksymalizować wydajność i skalowalność. Ale w końcu musimy wykonać cztery operacje, aby zwrócić pojedynczy wpis, więc poprawimy to w następnej iteracji.

Latency Jednostki żądania Wydajność
9 ms 19.54 RU

[Q3] Lista wpisów użytkownika w skróconej formie

Najpierw musimy pobrać żądane wpisy z zapytaniem SQL, które pobiera wpisy odpowiadające temu konkretnemu użytkownikowi. Musimy jednak również wydać więcej zapytań, aby zagregować nazwę użytkownika autora oraz liczbę komentarzy i polubień.

Diagram pobierania wszystkich wpisów dla użytkownika i agregowania dodatkowych danych.

Ta implementacja przedstawia wiele wad:

  • Zapytania, które agregują liczbę komentarzy i polubień, muszą być wystawiane dla każdego wpisu zwróconego przez pierwsze zapytanie.
  • Główne zapytanie nie filtruje klucza partycji kontenera posts, co prowadzi do rozproszenia zapytania (ang. fan-out) i przeskanowania partycji w kontenerze.
Latency Jednostki żądania Wydajność
130 ms 619.41 RU

[C3] Tworzenie komentarza

Komentarz jest tworzony przez zapisanie odpowiedniego elementu w kontenerze posts .

Diagram przedstawiający pisanie pojedynczego elementu komentarza do kontenera postów.

Latency Jednostki żądania Wydajność
7 ms 8.57 RU

[Q4] Lista komentarzy do wpisu

Zaczynamy od zapytania, które pobiera wszystkie komentarze dla tego wpisu i po raz kolejny, musimy również agregować nazwy użytkowników oddzielnie dla każdego komentarza.

Diagram pobierania wszystkich komentarzy dla wpisu i agregowania dodatkowych danych.

Mimo że główne zapytanie filtruje według klucza partycji kontenera, osobne agregowanie nazw użytkowników szkodzi ogólnej wydajności. Poprawimy to później.

Latency Jednostki żądania Wydajność
23 ms 27.72 RU

[C4] Lubię to stanowisko

Podobnie jak [C3], tworzymy odpowiedni element w kontenerze posts .

Diagram przedstawiający pisanie pojedynczego elementu (na przykład) do kontenera postów.

Latency Jednostki żądania Wydajność
6 ms 7.05 RU

[Q5] Wyświetlanie listy polubień wpisu

Podobnie jak [Q4], wysyłamy zapytania dotyczące polubień tego wpisu, a następnie agregujemy ich nazwy użytkowników.

Diagram pobierania wszystkich polubień dla wpisu i agregowania dodatkowych danych.

Latency Jednostki żądania Wydajność
59 ms 58.92 RU

[Q6] Wyświetl listę x najnowszych wpisów utworzonych w krótkim formularzu (kanał informacyjny)

Pobieramy najnowsze wpisy, wykonując posts zapytanie dotyczące kontenera posortowanego według malejącej daty utworzenia, a następnie agregujemy nazwy użytkowników i liczby komentarzy i polubień dla każdego z wpisów.

Diagram pobierania najnowszych wpisów i agregowania dodatkowych danych.

Po raz kolejny nasze początkowe zapytanie nie uwzględnia klucza partycji kontenera posts, co wywołuje kosztowny fan-out. Sytuacja jest jeszcze gorsza, ponieważ celujemy w większy zestaw wyników i sortujemy wyniki z użyciem klauzuli ORDER BY, co czyni je droższymi pod względem jednostek zapytań.

Latency Jednostki żądania Wydajność
306 ms 2063.54 RU

Zastanowić się nad wydajnością wersji 1

Patrząc na problemy z wydajnością, które napotkaliśmy w poprzedniej sekcji, możemy zidentyfikować dwie główne klasy problemów:

  • Niektóre żądania wymagają wydania wielu zapytań w celu zebrania wszystkich danych, które należy zwrócić.
  • Niektóre zapytania nie filtrują po kluczu partycji kontenerów, na które są kierowane, co prowadzi do rozproszenia, które utrudnia naszą skalowalność.

Rozwiążmy każdy z tych problemów, zaczynając od pierwszego.

Wersja 2: Wprowadzenie denormalizacji w celu zoptymalizowania zapytań odczytu

Powodem, dla którego musimy w niektórych przypadkach wysłać więcej żądań, jest to, że wyniki początkowego żądania nie zawierają wszystkich danych, które musimy zwrócić. Denormalizowanie danych rozwiązuje ten rodzaj problemu w naszym zestawie danych podczas pracy z magazynem danych nierelacyjnych, takim jak Azure Cosmos DB.

W naszym przykładzie modyfikujemy elementy wpisu, aby dodać nazwę użytkownika autora wpisu, liczbę komentarzy i liczbę polubień:

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

Modyfikujemy również elementy, takie jak komentarze i polubienia, aby dodać nazwę użytkownika, który je utworzył.

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "userUsername": "<comment-author-username>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "userUsername": "<liker-username>",
    "creationDate": "<like-creation-date>"
}

Denormalizowanie liczby komentarzy i polubień

Chcemy, aby za każdym razem, gdy dodajemy komentarz lub polubienie, zwiększał się również licznik commentCount lub likeCount w odpowiednim poście. W miarę jak postId dokonuje partycjonowania naszego kontenera posts, nowy element (komentarz lub polubienie) oraz odpowiadający mu wpis znajdują się w tej samej partycji logicznej. W związku z tym możemy użyć procedury składowanej do wykonania tej operacji.

Podczas tworzenia komentarza ([C3]), zamiast dodawać nowy element w posts kontenerze, wywołujemy następującą procedurę składowaną dla tego kontenera:

function createComment(postId, comment) {
  var collection = getContext().getCollection();

  collection.readDocument(
    `${collection.getAltLink()}/docs/${postId}`,
    function (err, post) {
      if (err) throw err;

      post.commentCount++;
      collection.replaceDocument(
        post._self,
        post,
        function (err) {
          if (err) throw err;

          comment.postId = postId;
          collection.createDocument(
            collection.getSelfLink(),
            comment
          );
        }
      );
    })
}

Ta procedura składowana przyjmuje identyfikator wpisu i treść nowego komentarza jako parametry, a następnie:

  • pobiera wpis.
  • zwiększa wartość commentCount.
  • zastępuje wpis.
  • dodaje nowy komentarz.

Ponieważ procedury składowane są wykonywane jako niepodzielne transakcje, wartość commentCount oraz rzeczywista liczba komentarzy zawsze pozostają zsynchronizowane.

Oczywiście wywołujemy podobną procedurę składowaną przy dodawaniu nowych polubień, aby zwiększać licznik likeCount.

Denormalizowanie nazw użytkowników

Nazwy użytkowników wymagają innego podejścia, ponieważ użytkownicy nie tylko znajdują się w różnych partycjach, ale w innym kontenerze. Kiedy musimy zdenormalizować dane w poprzek partycji i kontenerów, możemy skorzystać z kanału zmian kontenera źródłowego.

W naszym przykładzie używamy strumienia zmian kontenera users, aby reagować za każdym razem, gdy użytkownicy aktualizują swoje nazwy użytkowników. W takim przypadku propagujemy zmianę, wywołując inną procedurę składowaną w kontenerze posts :

Diagram przedstawiający denormalizowanie nazw użytkowników w kontenerze postów.

function updateUsernames(userId, username) {
  var collection = getContext().getCollection();
  
  collection.queryDocuments(
    collection.getSelfLink(),
    `SELECT * FROM p WHERE p.userId = '${userId}'`,
    function (err, results) {
      if (err) throw err;

      for (var i in results) {
        var doc = results[i];
        doc.userUsername = username;

        collection.upsertDocument(
          collection.getSelfLink(),
          doc);
      }
    });
}

Ta procedura składowana przyjmuje identyfikator użytkownika i nową nazwę użytkownika jako parametry, a następnie:

  • pobiera wszystkie elementy pasujące do userId (które mogą być postami, komentarzami lub polubienia).
  • dla każdego z tych elementów:
    • zastępuje element userUsername.
    • zastępuje element.

Ważne

Operacja ta jest kosztowna, ponieważ wymaga wykonania tej procedury przechowywanej na każdej partycji kontenera posts. Zakładamy, że większość użytkowników wybiera odpowiednią nazwę użytkownika podczas rejestracji i nigdy jej nie zmieni, więc ta aktualizacja jest uruchamiana bardzo rzadko.

Jakie są wzrosty wydajności wersji 2?

Porozmawiajmy o niektórych wzrostach wydajności wersji 2.

[Q2] Pobieranie wpisu

Teraz, gdy nasza denormalizacja jest już gotowa, wystarczy pobrać tylko jeden element, aby zrealizować to żądanie.

Diagram pobierania pojedynczego elementu z kontenera postów zdenormalizowanych.

Latency Jednostki żądania Wydajność
2 ms 1 RU

[Q4] Lista komentarzy do wpisu

Tutaj ponownie możemy oszczędzić dodatkowe żądania, które pobierały nazwy użytkowników i zredukować do pojedynczego zapytania filtrującego według klucza partycji.

Diagram pobierania wszystkich komentarzy dla zdenormalizowanego wpisu.

Latency Jednostki żądania Wydajność
4 ms 7.72 RU

[Q5] Wyświetlanie listy polubień wpisu

Dokładnie taka sama sytuacja w przypadku wyświetlania listy polubień.

Diagram pobierania wszystkich polubień dla zdenormalizowanego wpisu.

Latency Jednostki żądania Wydajność
4 ms 8.92 RU

Wersja 3: Upewnij się, że wszystkie żądania są skalowalne

Pozostają jeszcze dwa żądania, które nie zostały w pełni zoptymalizowane w kontekście naszych ogólnych ulepszeń wydajności. Te żądania to [Q3] i [Q6].. Są to żądania obejmujące zapytania, które nie filtrują klucza partycji kontenerów, których dotyczą.

[Q3] Lista wpisów użytkownika w skróconej formie

To żądanie już korzysta z ulepszeń wprowadzonych w wersji 2, co pozwala zaoszczędzić więcej zapytań.

Diagram przedstawiający zapytanie umożliwiające wyświetlenie listy zdenormalizowanych wpisów użytkownika w krótkiej formie.

Jednak pozostałe zapytanie nadal nie filtruje klucza partycji kontenera posts.

Sposób myślenia o tej sytuacji jest prosty:

  • To żądanie musi zastosować filtr na userId, gdyż chcemy pobrać wszystkie wpisy dla określonego użytkownika.
  • Nie działa dobrze, ponieważ jest wykonywany względem posts kontenera, który nie ma userId partycjonowania.
  • Stwierdzając rzecz oczywistą, rozwiązalibyśmy problem z wydajnością, wykonując to żądanie względem kontenera podzielonego na partycje z użyciem userId.
  • Okazuje się, że mamy już taki kontener: users kontener!

Wprowadzamy więc drugi poziom denormalizacji przez duplikowanie całych wpisów do kontenera users . Dzięki temu efektywnie uzyskujemy kopię naszych wpisów, tylko partycjonowaną z innego punktu widzenia, dzięki czemu są bardziej wydajne w pobieraniu przez userId.

Kontener users zawiera teraz dwa rodzaje elementów:

{
    "id": "<user-id>",
    "type": "user",
    "userId": "<user-id>",
    "username": "<username>"
}

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

W tym przykładzie:

  • Wprowadziliśmy type pole w elemencie użytkownika, aby odróżnić użytkowników od wpisów.
  • Dodaliśmy pole userId w elemencie użytkownika, które jest zdublowane z polem id, ale jest wymagane, ponieważ kontener users jest teraz podzielony na partycje z userId (a nie id poprzednio).

Aby osiągnąć tę denormalizację, po raz kolejny użyjemy strumienia zmian. Tym razem reagujemy na zestawienie zmian kontenera w celu wysłania nowego lub zaktualizowanego posts wpisu do kontenera users . Ponieważ listowanie wpisów nie wymaga zwrócenia ich pełnej zawartości, możemy je skracać w procesie.

Diagram denormalizowania wpisów w kontenerze użytkowników.

Teraz możemy kierować zapytanie do users kontenera, filtrując według klucza partycji kontenera.

Diagram pobierania wszystkich wpisów dla zdenormalizowanego użytkownika.

Latency Jednostki żądania Wydajność
4 ms 6.46 RU

[Q6] Wyświetl listę x najnowszych wpisów utworzonych w krótkim formularzu (kanał informacyjny)

Musimy tutaj radzić sobie z podobną sytuacją: nawet po redukcji liczby niepotrzebnych zapytań pozostawionych przez denormalizację wprowadzoną w wersji 2, pozostałe zapytania nie filtrują według klucza partycji kontenera.

Diagram przedstawiający zapytanie zawierające listę x najnowszych wpisów utworzonych w krótkim formularzu.

Aby maksymalizować wydajność i skalowalność tego żądania zgodnie z tym samym podejściem, należy zadbać, by dotyczyło ono tylko jednej partycji. Tylko trafienie jednej partycji jest możliwe, ponieważ musimy zwrócić tylko ograniczoną liczbę elementów. Aby wypełnić stronę główną naszej platformy blogowania, wystarczy pobrać 100 najnowszych wpisów bez konieczności stronicowania całego zestawu danych.

Dlatego aby zoptymalizować to ostatnie żądanie, wprowadzamy trzeci kontener do naszego projektu, całkowicie dedykowany do obsługi tego żądania. Zdenormalizujemy nasze wpisy w tym nowym feed kontenerze:

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

Pole type dzieli ten kontener, który zawsze jest post w naszych elementach. Dzięki temu wszystkie elementy w tym kontenerze znajdą się w tej samej partycji.

Aby osiągnąć denormalizację, musimy po prostu podłączyć potok danych zmiany, który wcześniej wprowadziliśmy w celu rozsyłania wpisów do tego nowego kontenera. Należy pamiętać o tym, że musimy mieć pewność, że przechowujemy tylko 100 najnowszych postów; w przeciwnym razie zawartość kontenera może wzrosnąć poza maksymalny rozmiar partycji. To ograniczenie można zaimplementować, wywołując wyzwalacz po każdym dodaniu dokumentu w kontenerze:

Diagram przedstawiający denormalizowanie wpisów w kontenerze kanału informacyjnego.

Oto treść wyzwalacza końcowego, która obcina kolekcję:

function truncateFeed() {
  const maxDocs = 100;
  var context = getContext();
  var collection = context.getCollection();

  collection.queryDocuments(
    collection.getSelfLink(),
    "SELECT VALUE COUNT(1) FROM f",
    function (err, results) {
      if (err) throw err;

      processCountResults(results);
    });

  function processCountResults(results) {
    // + 1 because the query didn't count the newly inserted doc
    if ((results[0] + 1) > maxDocs) {
      var docsToRemove = results[0] + 1 - maxDocs;
      collection.queryDocuments(
        collection.getSelfLink(),
        `SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
        function (err, results) {
          if (err) throw err;

          processDocsToRemove(results, 0);
        });
    }
  }

  function processDocsToRemove(results, index) {
    var doc = results[index];
    if (doc) {
      collection.deleteDocument(
        doc._self,
        function (err) {
          if (err) throw err;

          processDocsToRemove(results, index + 1);
        });
    }
  }
}

Ostatnim krokiem jest przekierowanie zapytania do nowego feed kontenera:

Diagram pobierania najnowszych wpisów.

Latency Jednostki żądania Wydajność
9 ms 16.97 RU

Podsumowanie

Przyjrzyjmy się ogólnym ulepszeniom wydajności i skalowalności, które wprowadziliśmy w różnych wersjach naszego projektu.

V1 V2 V3
[C1] 7 ms/ 5.71 RU 7 ms/ 5.71 RU 7 ms/ 5.71 RU
[Q1] 2 ms/ 1 RU 2 ms/ 1 RU 2 ms/ 1 RU
[C2] 9 ms/ 8.76 RU 9 ms/ 8.76 RU 9 ms/ 8.76 RU
[Q2] 9 ms/ 19.54 RU 2 ms/ 1 RU 2 ms/ 1 RU
[Q3] 130 ms/ 619.41 RU 28 ms/ 201.54 RU 4 ms/ 6.46 RU
[C3] 7 ms/ 8.57 RU 7 ms/ 15.27 RU 7 ms/ 15.27 RU
[Q4] 23 ms/ 27.72 RU 4 ms/ 7.72 RU 4 ms/ 7.72 RU
[C4] 6 ms/ 7.05 RU 7 ms/ 14.67 RU 7 ms/ 14.67 RU
[Q5] 59 ms/ 58.92 RU 4 ms/ 8.92 RU 4 ms/ 8.92 RU
[Q6] 306 ms/ 2063.54 RU 83 ms/ 532.33 RU 9 ms/ 16.97 RU

Zoptymalizowaliśmy scenariusz z dużą liczbą operacji odczytu

Możesz zauważyć, że skupiliśmy nasze wysiłki na poprawę wydajności żądań odczytu (zapytań) kosztem żądań zapisu (poleceń). W wielu przypadkach operacje zapisu wyzwalają teraz kolejną denormalizację poprzez strumienie zmian, co sprawia, że są bardziej kosztowne obliczeniowo i dłuższe do zmaterializowania się.

Uzasadniamy to skupienie się na wydajności odczytu ze względu na fakt, że platforma blogowa, podobnie jak większość aplikacji społecznościowych, charakteryzuje się dużym natężeniem odczytów. Obciążenie z dużym obciążeniem odczytu wskazuje, że liczba żądań odczytu, które musi obsłużyć, jest zwykle o różnej wielkości większa niż liczba żądań zapisu. Dlatego warto, aby żądania zapisu były bardziej kosztochłonne, co pozwoli na to, aby żądania odczytu były tańsze i bardziej wydajne.

Jeśli przyjrzymy się najbardziej ekstremalnej optymalizacji, [Q6] zmniejszyła się z ponad 2000 jednostek RU do zaledwie 17 jednostek RU. Osiągnęliśmy to, denormalizując posty, co kosztuje około 10 jednostek RU na każdy element. Ponieważ obsługiwalibyśmy znacznie więcej żądań kanału informacyjnego niż tworzyli lub aktualizowali posty, koszt tej denormalizacji jest znikomy, biorąc pod uwagę ogólne oszczędności.

Denormalizacja może być stosowana przyrostowo

Ulepszenia skalowalności, które omówiliśmy w tym artykule, obejmują denormalizację i duplikowanie danych w zestawie danych. Należy zauważyć, że te optymalizacje nie muszą być wprowadzane w ciągu pierwszego dnia. Zapytania filtrujące klucze partycji działają lepiej na dużą skalę, ale zapytania obejmujące wiele partycji mogą być akceptowalne, jeśli są one wywoływane rzadko lub w przypadku ograniczonego zestawu danych. Jeśli dopiero tworzysz prototyp lub uruchamiasz produkt z małą i kontrolowaną bazą użytkowników, prawdopodobnie możesz oszczędzić te ulepszenia na później. Ważne jest monitorowanie wydajności modelu , dzięki czemu możesz zdecydować, czy i kiedy należy je wprowadzić.

Kanał informacyjny zmian używany do ciągłego dystrybuowania aktualizacji do innych kontenerów przechowuje wszystkie te aktualizacje. Ta trwałość umożliwia zażądanie wszystkich aktualizacji od czasu utworzenia kontenera i zdenormalizowanych widoków bootstrap jako jednorazowej operacji nadrabiania zaległości, nawet jeśli system ma już wiele danych.

Dalsze kroki

Po wprowadzeniu do praktycznego modelowania i partycjonowania danych warto zapoznać się z następującymi artykułami, aby zapoznać się z pojęciami: