Jak modelować i partycjonować dane w usłudze Azure Cosmos DB przy użyciu przykładu wziętego z życia
DOTYCZY: NoSQL
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 masz wbudowane nawyki i intuicje dotyczące projektowania modelu 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 tłumaczy 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.
Pobierz lub wyświetl kod źródłowy wygenerowany przez społeczność , który ilustruje koncepcje z tego artykułu.
Ważne
Współautor społeczności przyczynił się do tego przykładu kodu, a zespół usługi Azure Cosmos DB nie obsługuje jego konserwacji.
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.
Porada
Wyróżniliśmy niektóre wyrazy kursywą; te słowa identyfikują rodzaj "rzeczy", które nasz model będzie musiał manipulować.
Dodanie kolejnych wymagań do naszej specyfikacji:
- Na stronie głównej jest wyświetlane źródło ostatnio utworzonych wpisów.
- Możemy pobrać wszystkie wpisy dla użytkownika, wszystkie komentarze do wpisu i wszystkie polubień dla wpisu,
- Wpisy są zwracane z nazwą użytkownika swoich autorów oraz liczbą komentarzy i polubień, które mają,
- Komentarze i polubienia są również zwracane przy użyciu nazwy 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
Na początek udostępniamy strukturę naszej początkowej specyfikacji, identyfikując wzorce dostępu rozwiązania. Podczas projektowania modelu danych dla usługi Azure Cosmos DB ważne jest, aby zrozumieć, które żądania naszego modelu muszą obsłużyć, aby upewnić się, że model efektywnie obsługuje te żądania.
Aby ułatwić wykonywanie ogólnego procesu, kategoryzujemy te różne żądania jako polecenia lub zapytania, pożyczając trochę słownictwa z CQRS. W usługach CQRS polecenia są żądaniami zapisu (czyli intencjami aktualizacji systemu), a zapytania są żądaniami tylko do odczytu.
Oto lista żądań uwidacznianych przez naszą platformę:
- [C1] Tworzenie/edytowanie użytkownika
- [Q1] Pobieranie użytkownika
- [C2] Tworzenie/edytowanie wpisu
- [Q2] Pobieranie wpisu
- [Q3] Wyświetlanie listy wpisów użytkownika w krótkim formularzu
- [C3] Tworzenie komentarza
- [Q4] Wyświetlanie listy komentarzy w wpisie
- [C4] Podobnie jak w poście
- [Q5] Wyświetlanie listy polubień wpisu
- [Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)
Na tym etapie nie zastanawialiśmy się nad informacjami 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 względem magazynu relacyjnego. Najpierw zaczynamy od tego kroku, ponieważ musimy ustalić, jak te jednostki tłumaczą się pod względem tabel, kolumn, kluczy obcych itp. Jest to znacznie mniej istotne w przypadku bazy danych dokumentów, która nie wymusza żadnego schematu podczas zapisu.
Głównym powodem, dla którego ważne jest zidentyfikowanie naszych wzorców dostępu od początku, jest to, że ta lista żądań będzie naszym zestawem testów. Za każdym razem, gdy iterujemy po modelu danych, przechodzimy przez każde z żądań i sprawdzamy jego wydajność i skalowalność. Obliczamy jednostki żądań używane w każdym modelu i optymalizujemy je. Wszystkie te modele korzystają z domyślnych zasad indeksowania i można je zastąpić przez indeksowanie określonych właściwości, co może dodatkowo poprawić użycie jednostek RU i opóźnienie.
Wersja 1: pierwsza wersja
Zaczynamy od dwóch kontenerów: users
i posts
.
Kontener Użytkownicy
Ten kontener przechowuje tylko elementy użytkownika:
{
"id": "<user-id>",
"username": "<username>"
}
Dzielimy ten kontener na partycje za pomocą id
elementu , co oznacza, że każda partycja logiczna w tym kontenerze zawiera tylko jeden element.
Publikowanie kontenera
Ten kontener hostuje jednostki, takie jak wpisy, komentarze i polubień:
{
"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>"
}
Partycjonujemy ten kontener za pomocą postId
elementu , co oznacza, że każda partycja logiczna w tym kontenerze zawiera jeden wpis, wszystkie komentarze dla tego wpisu i wszystkie polubień 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ływanie się do powiązanych danych zamiast ich osadzania (zapoznaj się z tą sekcją , aby uzyskać szczegółowe informacje na temat tych pojęć), ponieważ:
- nie ma górnego limitu liczby wpisów, które użytkownik może utworzyć,
- posty mogą być dowolnie długie,
- nie ma górnego limitu liczby komentarzy i polubień, które mogą zawierać posty,
- Chcemy mieć możliwość dodania komentarza lub polubionego wpisu bez konieczności aktualizowania samego wpisu.
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 postów na użytkownika oraz maksymalnie 25 komentarzy i 100 polubień na wpis.
[C1] Tworzenie/edytowanie użytkownika
To żądanie jest proste do zaimplementowania, ponieważ po prostu tworzymy lub aktualizujemy element w kontenerze users
. Żądania są ładnie rozłożone na wszystkie partycje dzięki kluczowi id
partycji.
Opóźnienie | Opłata za jednostki RU | Wydajność |
---|---|---|
7 Pani |
5.71 RU |
✅ |
[Q1] Pobieranie użytkownika
Pobieranie użytkownika odbywa się przez odczytanie odpowiedniego elementu z kontenera users
.
Opóźnienie | Opłata za jednostki RU | Wydajność |
---|---|---|
2 Pani |
1 RU |
✅ |
[C2] Tworzenie/edytowanie wpisu
Podobnie jak w przypadku [C1], musimy tylko zapisywać dane w kontenerze posts
.
Opóźnienie | Opłata za jednostki RU | Wydajność |
---|---|---|
9 Pani |
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ą wystawienia 3 dodatkowych zapytań SQL.
Każda z większej liczby zapytań filtruje klucz partycji odpowiedniego kontenera, co jest dokładnie tym, co chcemy zmaksymalizować wydajność i skalowalność. Jednak w końcu musimy wykonać cztery operacje, aby zwrócić pojedynczy wpis, więc poprawimy to w następnej iteracji.
Opóźnienie | Opłata za jednostki RU | Wydajność |
---|---|---|
9 Pani |
19.54 RU |
⚠ |
[Q3] Wyświetlanie listy wpisów użytkownika w krótkim formularzu
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ń.
Ta implementacja przedstawia wiele wad:
- zapytania agregujące liczby komentarzy i polubień muszą zostać wydane dla każdego wpisu zwróconego przez pierwsze zapytanie.
- zapytanie główne nie filtruje klucza
posts
partycji kontenera, co prowadzi do wymyślenia i skanowania partycji w kontenerze.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
130 Pani |
619.41 RU |
⚠ |
[C3] Tworzenie komentarza
Komentarz jest tworzony przez zapisanie odpowiedniego elementu w kontenerze posts
.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
7 Pani |
8.57 RU |
✅ |
[Q4] Wyświetlanie listy komentarzy w poście
Zaczynamy od zapytania, które pobiera wszystkie komentarze do tego wpisu i po raz kolejny, musimy również agregować nazwy użytkowników oddzielnie dla każdego komentarza.
Chociaż główne zapytanie filtruje klucz partycji kontenera, agregowanie nazw użytkowników oddzielnie karze ogólną wydajność. Ulepszymy to później.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
23 Pani |
27.72 RU |
⚠ |
[C4] Lubię wpis
Podobnie jak [C3], tworzymy odpowiedni element w kontenerze posts
.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
6 Pani |
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żytkownika.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
59 Pani |
58.92 RU |
⚠ |
[Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)
Pobieramy najnowsze wpisy, wysyłając posts
zapytanie do kontenera posortowanego według daty utworzenia malejącego, a następnie agregujemy nazwy użytkowników i liczby komentarzy i polubień dla każdego z wpisów.
Po raz kolejny nasze początkowe zapytanie nie filtruje klucza posts
partycji kontenera, co wyzwala kosztowny wentylator. Jest to jeszcze gorsze, ponieważ celujemy większy zestaw wyników i sortujemy wyniki za pomocą ORDER BY
klauzuli , co sprawia, że jest droższa pod względem jednostek żądania.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
306 Pani |
2063.54 RU |
⚠ |
Zastanawianie 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ą wystawienia wielu zapytań w celu zebrania wszystkich danych, które należy zwrócić,
- niektóre zapytania nie filtrują klucza partycji kontenerów, których celem są, co prowadzi do wentylatora, co utrudnia skalowalność.
Rozwiążmy każdy z tych problemów, zaczynając od pierwszego.
Wersja 2: Wprowadzenie do denormalizacji w celu zoptymalizowania zapytań odczytu
Powodem, dla którego musimy wydać więcej żądań w niektórych przypadkach, jest to, że wyniki początkowego żądania nie zawierają wszystkich danych, które musimy zwrócić. Denormalizacja danych rozwiązuje ten rodzaj problemu w naszym zestawie danych podczas pracy z magazynem danych nierelacyjnych, takim jak Usługa Azure Cosmos DB.
W naszym przykładzie zmodyfikujemy elementy publikowania, 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ż komentarz i lubimy elementy, 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>"
}
Denormalizacja komentarza i polubień
Chcemy osiągnąć to, że za każdym razem, gdy dodamy komentarz lub podobne, zwiększamy również commentCount
wartość lub likeCount
w odpowiednim poście. W miarę postId
partycjonowania kontenera posts
nowy element (komentarz lub polubienie) i 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 kontenerze wywołujemy następującą procedurę składowaną w posts
tym kontenerze:
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 liczbę
commentCount
- zastępuje wpis
- dodaje nowy komentarz
Ponieważ procedury składowane są wykonywane jako transakcje niepodzielne, wartość commentCount
i rzeczywista liczba komentarzy zawsze pozostaje zsynchronizowana.
Oczywiście wywołujemy podobną procedurę składowaną podczas dodawania nowych polubień, aby zwiększać likeCount
wartość .
Denormalizacja 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. Gdy musimy zdenormalizować dane między partycjami i kontenerami, możemy użyć źródła danych zmian kontenera źródłowego.
W naszym przykładzie używamy zestawienia zmian kontenera users
, aby reagować za każdym razem, gdy użytkownicy aktualizują swoje nazwy użytkownika. W takim przypadku propagujemy zmianę, wywołując inną procedurę składowaną w kontenerze posts
:
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
userId
do elementu (które mogą być wpisami, komentarzami lub polubień) - dla każdego z tych elementów
- zastępuje element
userUsername
- zastępuje element
- zastępuje element
Ważne
Ta operacja jest kosztowna, ponieważ wymaga wykonania tej procedury składowanej 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 będzie działać bardzo rzadko.
Jakie są zyski z wydajności w wersji 2?
Porozmawiajmy o niektórych wzrostach wydajności w wersji 2.
[Q2] Pobieranie wpisu
Teraz, gdy nasza denormalizacja jest w miejscu, musimy pobrać tylko jeden element, aby obsłużyć to żądanie.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
2 Pani |
1 RU |
✅ |
[Q4] Wyświetlanie listy komentarzy w poście
Tutaj ponownie możemy oszczędzić dodatkowe żądania, które pobrały nazwy użytkowników i skończyć na jednym zapytaniu, które filtruje klucz partycji.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
4 Pani |
7.72 RU |
✅ |
[Q5] Wyświetlanie listy polubień wpisu
Dokładnie taka sama sytuacja podczas wyświetlania listy polubień.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
4 Pani |
8.92 RU |
✅ |
Wersja 3: Upewniając się, że wszystkie żądania są skalowalne
Nadal istnieją dwa żądania, które nie zostały w pełni zoptymalizowane podczas przeglądania 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] Wyświetlanie listy wpisów użytkownika w krótkim formularzu
To żądanie korzysta już z ulepszeń wprowadzonych w wersji 2, co pozwala zaoszczędzić więcej zapytań.
Jednak pozostałe zapytanie nadal nie filtruje klucza partycji kontenera posts
.
Sposób myślenia o tej sytuacji jest prosty:
- To żądanie musi filtrować,
userId
ponieważ chcemy pobrać wszystkie wpisy dla określonego użytkownika. - Nie działa dobrze, ponieważ jest wykonywany względem
posts
kontenera, który nie mauserId
partycjonowania. - Stwierdzając oczywiste, rozwiązalibyśmy nasz problem z wydajnością, wykonując to żądanie względem kontenera podzielonego na partycje za pomocą
userId
polecenia . - Okazuje się, że mamy już taki kontener:
users
kontener!
Dlatego wprowadzamy drugi poziom denormalizacji przez duplikowanie całych wpisów do kontenera users
. Dzięki temu skutecznie uzyskujemy kopię naszych postów, tylko partycjonowanych wzdłuż innego wymiaru, dzięki czemu są one bardziej wydajne w celu pobrania przez nich 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
userId
również pole w elemencie użytkownika, które jest nadmiarowe zid
polem, ale jest wymagane, ponieważusers
kontener jest teraz podzielony nauserId
partycje (a nieid
tak jak wcześniej)
Aby osiągnąć to denormalizacja, po raz kolejny użyjemy kanału informacyjnego zmian. Tym razem reagujemy na zestawienie zmian kontenera w celu wysłania nowego lub zaktualizowanego posts
wpisu do kontenera users
. A ponieważ wyświetlanie wpisów nie wymaga zwrócenia pełnej zawartości, możemy je obcinać w procesie.
Teraz możemy kierować zapytanie do kontenera users
, filtrując klucz partycji kontenera.
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
4 Pani |
6.46 RU |
✅ |
[Q6] Wyświetlanie listy x najnowszych wpisów utworzonych w formie krótkiej (kanał informacyjny)
Musimy radzić sobie z podobną sytuacją: nawet po zachowaniu większej ilości zapytań pozostawionych przez denormalizację wprowadzoną w wersji 2 pozostałe zapytanie nie filtruje klucza partycji kontenera:
Zgodnie z tym samym podejściem maksymalizowanie wydajności i skalowalności tego żądania wymaga osiągnięcia 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ą platformy rejestrowania, wystarczy uzyskać 100 najnowszych postó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
partycjonuje ten kontener, który jest zawsze post
w naszych elementach. Dzięki temu wszystkie elementy w tym kontenerze będą zasiadać w tej samej partycji.
Aby osiągnąć denormalizację, musimy tylko podłączyć potok zestawienia zmian, który wcześniej wprowadziliśmy, aby wysłać wpisy do tego nowego kontenera. Należy pamiętać o tym, że musimy upewnić się, ż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 do kontenera:
Oto treść wyzwalacza końcowego, który 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:
Opóźnienie | Opłata za jednostkę RU | Wydajność |
---|---|---|
9 Pani |
16.97 RU |
✅ |
Podsumowanie
Przyjrzyjmy się ogólnym ulepszeniom wydajności i skalowalności, które wprowadziliśmy w różnych wersjach naszego projektu.
Wersja 1 | Wersja 2 | Wersja 3 | |
---|---|---|---|
[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 |
Zoptymalizowano scenariusz z dużą liczbą operacji odczytu
Być może zauważyliśmy, że skupiliśmy nasze wysiłki na rzecz poprawy wydajności żądań odczytu (zapytań) kosztem żądań zapisu (poleceń). W wielu przypadkach operacje zapisu wyzwalają kolejną denormalizację za pośrednictwem źródeł zmian, co sprawia, że są bardziej kosztowne obliczeniowo i dłuższe w celu zmaterializowania.
Uzasadniamy to skupienie się na wydajności odczytu przez fakt, że platforma rejestrowania (podobnie jak większość aplikacji społecznościowych) jest ciężka do odczytu. Obciążenie z dużym obciążeniem odczytu wskazuje, że ilość żądań odczytu, które musi obsłużyć, jest zwykle większa niż liczba żądań zapisu o wielkości większej niż liczba żądań zapisu. Warto więc sprawić, że żądania zapisu będą droższe do wykonania w celu umożliwienia, aby żądania odczytu mogły być tańsze i wydajniejsze.
Jeśli przyjrzymy się najbardziej ekstremalnej optymalizacji, [Q6] przeszedł od 2000+ jednostek RU do zaledwie 17 jednostek RU; Osiągnęliśmy to poprzez denormalizację postów kosztem około 10 jednostek RU na element. Ponieważ służylibyśmy o wiele więcej żądań kanału informacyjnego niż tworzenie lub aktualizacje postów, koszt tej denormalizacji jest nieznaczny, biorąc pod uwagę ogólne oszczędności.
Denormalizacja można stosować 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 dniu 1. Zapytania filtrujące klucze partycji działają lepiej na dużą skalę, ale zapytania obejmujące wiele partycji mogą być dopuszczalne, jeśli są one wywoływane rzadko lub w przypadku ograniczonego zestawu danych. Jeśli po prostu tworzysz prototyp lub uruchamiasz produkt z małą i kontrolowaną bazą użytkowników, prawdopodobnie możesz oszczędzić te ulepszenia później. Ważne jest , aby monitorować wydajność modelu, aby zdecydować, czy i kiedy nadszedł czas, aby je wprowadzić.
Kanał informacyjny zmian używany do dystrybuowania aktualizacji do innych kontenerów przechowuje wszystkie te aktualizacje w sposób trwały. 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.
Następne kroki
Po wprowadzeniu do praktycznego modelowania danych i partycjonowania warto zapoznać się z następującymi artykułami, aby zapoznać się z omówioną koncepcją: