Jak skuteczny może być Meteor, udostępniając ogromną kolekcję wielu klientom?

100

Wyobraź sobie następujący przypadek:

  • 1000 klientów jest podłączonych do strony Meteor wyświetlającej zawartość kolekcji „Somestuff”.

  • „Somestuff” to kolekcja zawierająca 1000 przedmiotów.

  • Ktoś wstawia nowy element do kolekcji „Somestuff”

Co się stanie:

  • Wszystkie listy Meteor.Collectionklientów zostaną zaktualizowane, tj. Wstawienie zostanie przesłane do wszystkich (co oznacza jedną wiadomość wstawienia wysłaną do 1000 klientów)

Jaki jest koszt procesora w odniesieniu do serwera w celu określenia, który klient wymaga aktualizacji?

Czy to prawda, że ​​do klientów zostanie przekazana tylko wprowadzona wartość, a nie cała lista?

Jak to działa w prawdziwym życiu? Czy są dostępne jakieś testy porównawcze lub eksperymenty na taką skalę?

Flavien Volken
źródło

Odpowiedzi:

119

Krótka odpowiedź jest taka, że ​​tylko nowe dane są przesyłane przewodem. Oto jak to działa.

Istnieją trzy ważne części serwera Meteor, które zarządzają subskrypcjami: funkcja publikowania , która definiuje logikę danych, które dostarcza subskrypcja; kierowca Mongo , który ogląda bazy danych dla zmian; oraz pole scalania , które łączy wszystkie aktywne subskrypcje klienta i wysyła je przez sieć do klienta.

Funkcje publikowania

Za każdym razem, gdy klient Meteor subskrybuje kolekcję, serwer uruchamia funkcję publikowania . Zadaniem funkcji publikowania jest ustalenie zestawu dokumentów, które powinien posiadać klient, i wysłanie każdej właściwości dokumentu do pola korespondencji seryjnej. Działa raz dla każdego nowego klienta subskrybującego. W funkcji publikowania można umieścić dowolny kod JavaScript, na przykład dowolnie złożoną kontrolę dostępu za pomocą this.userId. Publikowania funkcja wysyła dane w polu seryjnej poprzez wywołanie this.added, this.changeda this.removed. Zobacz pełną dokumentację publikowania, aby uzyskać więcej informacji.

Większość publikuje funkcje nie trzeba syf wokół z niskim poziomie added, changeda removedAPI, choć. Jeśli publikuje zwraca kursor Mongo, serwer Meteor automatycznie łączy wyjście sterownika (Mongo insert, updatei removedwywołania zwrotne) do wejścia w pole scalania ( this.added, this.changedi this.removed). To całkiem fajne, że możesz wykonać wszystkie sprawdzenia uprawnień z góry w funkcji publikowania, a następnie bezpośrednio podłączyć sterownik bazy danych do pola scalającego bez żadnego kodu użytkownika na drodze. A kiedy autopublikowanie jest włączone, nawet ten fragment jest ukryty: serwer automatycznie tworzy zapytanie dla wszystkich dokumentów w każdej kolekcji i umieszcza je w polu scalania.

Z drugiej strony nie jesteś ograniczony do publikowania zapytań do bazy danych. Na przykład można napisać funkcję publikowania, która odczytuje pozycję GPS z urządzenia wewnątrz Meteor.setIntervallub sonduje starsze REST API z innej usługi internetowej. W tych przypadkach, można emitować zmian w polu seryjnej poprzez wywołanie niskim poziomie added, changeda removedDDP API.

Kierowca Mongo

Zadaniem kierowcy Mongo jest obserwowanie bazy danych Mongo pod kątem zmian w zapytaniach na żywo. Zapytania te działają w sposób ciągły i powrócić aktualizacje jako zmiana wyników przez wywołanie added, removedoraz changedwywołania zwrotne.

Mongo nie jest bazą danych czasu rzeczywistego. Więc kierowca sonduje. Przechowuje w pamięci kopię ostatniego wyniku zapytania dla każdego aktywnego zapytania na żywo. Na każdym cyklu odpytywania, porównuje nowy wynik z poprzedniego zapisanego wyniku obliczania minimalnego zestawu added, removedi changed wydarzeń, które opisują różnicę. Jeśli wielu dzwoniących rejestruje wywołania zwrotne dla tego samego zapytania na żywo, sterownik ogląda tylko jedną kopię zapytania, wywołując każde zarejestrowane wywołanie zwrotne z tym samym wynikiem.

Za każdym razem, gdy serwer aktualizuje kolekcję, sterownik ponownie oblicza każde zapytanie na żywo w tej kolekcji (przyszłe wersje Meteor udostępnią skalujący interfejs API w celu ograniczenia liczby zapytań na żywo, które są przeliczane podczas aktualizacji). łapać aktualizacje baz danych poza pasmem, które omijały serwer Meteor.

Pole scalania

Zadaniem pole scalania jest zestawić wyniki ( added, changedi removed połączenia) wszystkich aktywnych funkcji danego klienta publikują w jeden strumień danych. Dla każdego podłączonego klienta istnieje jedno pole scalania. Zawiera pełną kopię pamięci podręcznej minimongo klienta.

W Twoim przykładzie z pojedynczą subskrypcją pole scalania jest zasadniczo przejściem. Ale bardziej złożona aplikacja może mieć wiele subskrypcji, które mogą się nakładać. Jeśli dwie subskrypcje ustawiają ten sam atrybut w tym samym dokumencie, pole korespondencji seryjnej decyduje, która wartość ma priorytet i wysyła ją tylko do klienta. Nie ujawniliśmy jeszcze interfejsu API do ustawiania priorytetów subskrypcji. Na razie priorytet jest określany przez kolejność subskrybowania przez klienta zestawów danych. Pierwsza subskrypcja, którą tworzy klient, ma najwyższy priorytet, druga subskrypcja jest następna i tak dalej.

Ponieważ pole korespondencji seryjnej zawiera stan klienta, może wysłać minimalną ilość danych, aby każdy klient był aktualny, niezależnie od tego, jaka funkcja publikowania go dostarcza.

Co się dzieje po aktualizacji

Więc teraz przygotowaliśmy grunt pod Twój scenariusz.

Mamy 1000 połączonych klientów. Każdy jest subskrybowany na tę samą aktywną kwerendę Mongo ( Somestuff.find({})). Ponieważ zapytanie jest takie samo dla każdego klienta, sterownik wykonuje tylko jedno zapytanie na żywo. Istnieje 1000 aktywnych pól korespondencji seryjnej. I każdy klient publikują funkcja zarejestrował added, changedi removedna tej kwerendy na żywo, który karmi się jednym z pól korespondencji seryjnej. Nic więcej nie jest połączone ze skrzynkami scalającymi.

Najpierw kierowca Mongo. Kiedy jeden z klientów wstawia nowy dokument do Somestuff, wyzwala to ponowne obliczenie. Sterownik Mongo ponownie uruchamia zapytanie o wszystkie dokumenty Somestuff, porównuje wynik z poprzednim wynikiem w pamięci, znajduje jeden nowy dokument i wywołuje każdy z 1000 zarejestrowanych insertwywołań zwrotnych.

Następnie funkcje publikowania. Niewiele się tu dzieje: każde z 1000 insertwywołań zwrotnych wypycha dane do pola scalającego przez wywołanie added.

Wreszcie, każde pole scalania porównuje te nowe atrybuty ze swoją znajdującą się w pamięci kopią pamięci podręcznej klienta. W każdym przypadku stwierdza, że ​​wartości nie są jeszcze na kliencie i nie przesłaniają istniejącej wartości. Tak więc pole scalające emituje DATAkomunikat DDP na połączeniu SockJS do swojego klienta i aktualizuje swoją kopię w pamięci po stronie serwera.

Całkowity koszt procesora to koszt porównania jednego zapytania Mongo plus koszt 1000 pól scalających sprawdzających stan klientów i konstruowania nowego ładunku wiadomości DDP. Jedyne dane przesyłane przez łącze to pojedynczy obiekt JSON wysyłany do każdego z 1000 klientów, odpowiadający nowemu dokumentowi w bazie danych, oraz jeden komunikat RPC do serwera od klienta, który dokonał oryginalnej wstawki.

Optymalizacje

Oto, co zdecydowanie zaplanowaliśmy.

  • Bardziej wydajny sterownik Mongo. Mamy zoptymalizowany sterownik w 0.5.1 do uruchomienia tylko jednego obserwatora za wyraźną zapytania.

  • Nie każda zmiana bazy danych powinna powodować ponowne obliczenie zapytania. Możemy wprowadzić pewne automatyczne ulepszenia, ale najlepszym podejściem jest API, które pozwala deweloperowi określić, które zapytania należy ponownie uruchomić. Na przykład dla programisty jest oczywiste, że wstawienie wiadomości do jednego pokoju rozmów nie powinno unieważniać zapytania na żywo o wiadomości w drugim pokoju.

  • Sterownik Mongo, funkcja publikowania i pole scalające nie muszą działać w tym samym procesie ani nawet na tym samym komputerze. Niektóre aplikacje wykonują złożone zapytania na żywo i potrzebują więcej procesora do oglądania bazy danych. Inni mają tylko kilka odrębnych zapytań (wyobraź sobie silnik bloga), ale prawdopodobnie wielu połączonych klientów - potrzebują oni więcej procesora do pól scalających. Oddzielenie tych elementów pozwoli nam na niezależne skalowanie każdego elementu.

  • Wiele baz danych obsługuje wyzwalacze, które są uruchamiane po zaktualizowaniu wiersza i udostępniają stare i nowe wiersze. Dzięki tej funkcji sterownik bazy danych mógłby zarejestrować wyzwalacz zamiast odpytywania o zmiany.

debergalis
źródło
Czy jest jakiś przykład, jak używać Meteor.publish do publikowania danych innych niż kursor? Na przykład wyniki ze starszego interfejsu API reszty wspomnianego w odpowiedzi?
Tony
@Tony: To jest w dokumentacji. Sprawdź przykład liczenia sal.
Mitar
Warto zauważyć, że w wersjach 0.7, 0.7.1, 0.7.2 Meteor przełączony OpLog przestrzegać sterownik dla większości zapytań (są wyjątki skip, $neara $wherezawierające zapytań), które jest o wiele bardziej wydajny w obciążeniu procesora, przepustowości sieci i umożliwia skalowanie na aplikację serwery.
imslavko
A co, gdy nie każdy użytkownik widzi te same dane. 1. subskrybowali różne tematy .2. pełnią różne role, więc w ramach tego samego głównego tematu jest kilka wiadomości, które nie powinny do nich dotrzeć.
tgkprog
@debergalis w sprawie unieważnienia pamięci podręcznej, być może znajdziesz pomysły z mojego artykułu vanisoft.pl/~lopuszanski/public/cache_invalidation.pdf warte
zachodu
29

Z mojego doświadczenia wynika, że ​​korzystanie z wielu klientów podczas udostępniania ogromnej kolekcji w Meteor jest zasadniczo niewykonalne, począwszy od wersji 0.7.0.1. Spróbuję wyjaśnić dlaczego.

Jak opisano w powyższym poście, a także na https://github.com/meteor/meteor/issues/1821 , serwer meteor musi przechowywać kopię opublikowanych danych dla każdego klienta w polu scalania . To właśnie umożliwia magię Meteor, ale także powoduje, że duże współdzielone bazy danych są wielokrotnie przechowywane w pamięci procesu węzła. Nawet podczas korzystania z możliwej optymalizacji dla kolekcji statycznych, takich jak ( Czy istnieje sposób, aby stwierdzić, że kolekcja meteor jest statyczna (nigdy się nie zmieni)? ), Napotkaliśmy ogromny problem z wykorzystaniem procesora i pamięci w procesie Node.

W naszym przypadku publikowaliśmy zbiór 15 tys. Dokumentów dla każdego klienta, który był całkowicie statyczny. Problem polega na tym, że skopiowanie tych dokumentów do skrzynki scalającej klienta (w pamięci) po połączeniu w zasadzie spowodowało, że proces Node osiągnął 100% CPU na prawie sekundę, co spowodowało duże dodatkowe użycie pamięci. Jest to z natury nieskalowalne, ponieważ każdy łączący się klient rzuci serwer na kolana (a jednoczesne połączenia będą się wzajemnie blokować), a zużycie pamięci wzrośnie liniowo w liczbie klientów. W naszym przypadku każdy klient powodował dodatkowe ~ 60 MB zużycia pamięci, mimo że przesyłane surowe dane wynosiły tylko około 5 MB.

W naszym przypadku, ponieważ kolekcja była statyczna, rozwiązaliśmy ten problem, wysyłając wszystkie dokumenty jako .jsonplik, który został spakowany przez nginx i wczytywany do anonimowej kolekcji, co skutkowało transferem tylko ~ 1MB danych bez dodatkowego procesora lub pamięć w procesie węzła i znacznie szybszy czas ładowania. Wszystkie operacje na tej kolekcji zostały wykonane przy użyciu _ids ze znacznie mniejszych publikacji na serwerze, co pozwala zachować większość zalet Meteora. Umożliwiło to skalowanie aplikacji do wielu innych klientów. Ponadto, ponieważ nasza aplikacja jest w większości tylko do odczytu, dodatkowo poprawiliśmy skalowalność, uruchamiając wiele instancji Meteor za nginx z równoważeniem obciążenia (choć z pojedynczym Mongo), ponieważ każda instancja Node jest jednowątkowa.

Jednak kwestia udostępniania dużych, zapisywalnych kolekcji wielu klientom jest problemem inżynieryjnym, który musi zostać rozwiązany przez firmę Meteor. Prawdopodobnie istnieje lepszy sposób niż przechowywanie kopii wszystkiego dla każdego klienta, ale wymaga to poważnego przemyślenia jako problemu z systemami rozproszonymi. Obecne problemy związane z ogromnym zużyciem procesora i pamięci po prostu się nie skalują.

Andrew Mao
źródło
@Harry oplog nie ma znaczenia w tej sytuacji; dane były statyczne.
Andrew Mao,
Dlaczego nie robi różnic w kopiach minimongo po stronie serwera? Może to wszystko zmieniło się w 1.0? To znaczy zazwyczaj są takie same ja nadziei, nawet funkcje, które nazywa to powrót byłby podobny (jeśli Obserwuję, że aby coś, co jest tam zapisane, jak również i potencjalnie różne.)
MistereeDevlord
@MistereeDevlord Rozróżnianie zmian i pamięci podręczne danych klienta są teraz oddzielne. Nawet jeśli wszyscy mają te same dane i potrzebna jest tylko jedna różnica, pamięć podręczna na klienta różni się, ponieważ serwer nie może traktować ich identycznie. Z pewnością można to zrobić mądrzej w porównaniu z istniejącą implementacją.
Andrew Mao,
@AndrewMao Jak upewnić się, że pliki spakowane gzip są zabezpieczone podczas wysyłania ich do klienta, tj. Tylko zalogowany klient ma do nich dostęp?
FullStack
4

Eksperyment, którego możesz użyć, aby odpowiedzieć na to pytanie:

  1. Zainstaluj meteor testowy: meteor create --example todos
  2. Uruchom go pod inspektorem Webkit (WKI).
  3. Sprawdź zawartość komunikatów XHR poruszających się po kablu.
  4. Zwróć uwagę, że cała kolekcja nie jest przesuwana w poprzek przewodu.

Wskazówki dotyczące korzystania z WKI można znaleźć w tym artykule . Jest trochę nieaktualny, ale w większości nadal aktualny, szczególnie w przypadku tego pytania.

javajosh
źródło
2
Wyjaśnienie mechanizmu odpytywania: eventedmind.com/posts/meteor-liveresultsset
cmather