Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Sugeruję przeczytanie transkrypcji raportu Aleksandra Valyalkina z końca 2019 r. „Optymalizacje Go w VictoriaMetrics”

VictoriaMetrics — szybki i skalowalny system DBMS służący do przechowywania i przetwarzania danych w postaci szeregów czasowych (zapis tworzy czas i zbiór wartości odpowiadający temu czasowi, uzyskiwany np. poprzez okresowe odpytywanie stanu czujników lub zbieranie metryka).

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Oto link do filmu przedstawiającego ten raport – https://youtu.be/MZ5P21j_HLE

Slajdy

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Opowiedz nam o sobie. Jestem Aleksander Walalkin. Tutaj moje konto na GitHubie. Pasjonuję się Go i optymalizacją wydajności. Napisałem wiele przydatnych i mniej przydatnych bibliotek. Zaczynają od któregokolwiek fast, lub z quick prefiks.

Obecnie pracuję nad VictoriaMetrics. Co to jest i co ja tam robię? O tym opowiem w tej prezentacji.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Zarys raportu jest następujący:

  • Na początek opowiem czym jest VictoriaMetrics.
  • Następnie powiem ci, jakie są szeregi czasowe.
  • Następnie opowiem Ci, jak działa baza danych szeregów czasowych.
  • Następnie opowiem Ci o architekturze bazy danych: z czego się składa.
  • A potem przejdźmy do optymalizacji, jakie ma VictoriaMetrics. Jest to optymalizacja dla odwróconego indeksu i optymalizacja dla implementacji zestawu bitów w Go.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Czy ktoś z publiczności wie, czym jest VictoriaMetrics? No cóż, wiele osób już wie. To dobra wiadomość. Dla tych, którzy nie wiedzą, jest to baza danych szeregów czasowych. Opiera się na architekturze ClickHouse, na niektórych szczegółach implementacji ClickHouse. Przykładowo na takich jak: MergeTree, obliczenia równoległe na wszystkich dostępnych rdzeniach procesorów i optymalizacja wydajności poprzez pracę na blokach danych, które są umieszczane w pamięci podręcznej procesora.

VictoriaMetrics zapewnia lepszą kompresję danych niż inne bazy danych szeregów czasowych.

Skaluje się w pionie - czyli można dodać więcej procesorów, więcej RAM-u na jednym komputerze. VictoriaMetrics z powodzeniem wykorzysta dostępne zasoby i poprawi produktywność liniową.

VictoriaMetrics skaluje się także w poziomie – czyli możesz dodać dodatkowe węzły do ​​klastra VictoriaMetrics, a jego wydajność będzie rosła niemal liniowo.

Jak się domyślacie, VictoriaMetrics to szybka baza danych, bo innych nie umiem pisać. Jest to napisane w Go, więc będę o tym mówił podczas tego spotkania.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Kto wie, co to jest szereg czasowy? Zna też wiele osób. Szereg czasowy to szereg par (timestamp, значение), gdzie te pary są posortowane według czasu. Wartość jest liczbą zmiennoprzecinkową – float64.

Każda seria czasowa jest jednoznacznie identyfikowana za pomocą klucza. Z czego składa się ten klucz? Składa się z niepustego zestawu par klucz-wartość.

Oto przykład szeregu czasowego. Kluczem tej serii jest lista par: __name__="cpu_usage" to nazwa metryki, instance="my-server" - jest to komputer, na którym zbierana jest ta metryka, datacenter="us-east" - to jest centrum danych, w którym znajduje się ten komputer.

W rezultacie otrzymaliśmy nazwę szeregu czasowego składającą się z trzech par klucz-wartość. Klucz ten odpowiada liście par (timestamp, value). t1, t3, t3, ..., tN - są to znaczniki czasu, 10, 20, 12, ..., 15 — odpowiednie wartości. Jest to użycie procesora w danym momencie dla danego wiersza.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Gdzie można zastosować szeregi czasowe? Czy ktokolwiek ma jakiś pomysł?

  • W DevOps możesz mierzyć procesor, pamięć RAM, sieć, liczbę obrotów na sekundę, liczbę błędów itp.
  • IoT - możemy mierzyć temperaturę, ciśnienie, współrzędne geograficzne i coś jeszcze.
  • Również finanse – możemy monitorować ceny wszelkiego rodzaju akcji i walut.
  • Ponadto szeregi czasowe można wykorzystać w monitorowaniu procesów produkcyjnych w fabrykach. Mamy użytkowników, którzy używają VictoriaMetrics do monitorowania turbin wiatrowych dla robotów.
  • Szeregi czasowe są również przydatne do zbierania informacji z czujników różnych urządzeń. Na przykład dla silnika; do pomiaru ciśnienia w oponach; do pomiaru prędkości, odległości; do pomiaru zużycia benzyny itp.
  • Szeregi czasowe można również wykorzystać do monitorowania statków powietrznych. Każdy samolot posiada czarną skrzynkę, która zbiera szeregi czasowe dla różnych parametrów stanu statku powietrznego. Szeregi czasowe są również wykorzystywane w przemyśle lotniczym.
  • Opieka zdrowotna to ciśnienie krwi, tętno itp.

Może być więcej zastosowań, o których zapomniałem, ale mam nadzieję, że rozumiesz, że szeregi czasowe są aktywnie wykorzystywane we współczesnym świecie. A wielkość ich wykorzystania rośnie z roku na rok.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Dlaczego potrzebujesz bazy danych szeregów czasowych? Dlaczego nie można używać zwykłej relacyjnej bazy danych do przechowywania szeregów czasowych?

Ponieważ szeregi czasowe zawierają zwykle dużą ilość informacji, które trudno przechowywać i przetwarzać w konwencjonalnych bazach danych. Dlatego pojawiły się specjalistyczne bazy danych dla szeregów czasowych. Bazy te skutecznie przechowują punkty (timestamp, value) z podanym kluczem. Zapewniają interfejs API do odczytywania przechowywanych danych według klucza, pojedynczej pary klucz-wartość, wielu par klucz-wartość lub wyrażenia regularnego. Na przykład, jeśli chcesz znaleźć obciążenie procesora wszystkich swoich usług w centrum danych w Ameryce, musisz użyć tego pseudozapytania.

Zazwyczaj bazy danych szeregów czasowych udostępniają wyspecjalizowane języki zapytań, ponieważ SQL szeregów czasowych nie jest zbyt dobrze dostosowany. Chociaż istnieją bazy danych obsługujące SQL, nie jest to zbyt odpowiednie. Języki zapytań, takie jak PromQL, NapływQL, Topnik, Q. Mam nadzieję, że ktoś słyszał przynajmniej jeden z tych języków. Wiele osób zapewne słyszało o PromQL. To jest język zapytań Prometheus.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Tak wygląda nowoczesna architektura bazy danych szeregów czasowych na przykładzie VictoriaMetrics.

Składa się z dwóch części. Jest to pamięć odwróconego indeksu i pamięć wartości szeregów czasowych. Te repozytoria są oddzielone.

Kiedy do bazy danych pojawi się nowy rekord, najpierw uzyskujemy dostęp do odwróconego indeksu, aby znaleźć identyfikator szeregu czasowego dla danego zbioru label=value dla danej metryki. Znajdujemy ten identyfikator i zapisujemy wartość w magazynie danych.

Kiedy przychodzi żądanie pobrania danych z TSDB, w pierwszej kolejności przechodzimy do odwróconego indeksu. Zdobądźmy wszystko timeseries_ids rekordy pasujące do tego zestawu label=value. I wtedy dostajemy wszystkie potrzebne dane z hurtowni danych, indeksowane wg timeseries_ids.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Przyjrzyjmy się przykładowi, jak baza danych szeregów czasowych przetwarza przychodzące zapytanie wybierające.

  • Przede wszystkim dostaje wszystko timeseries_ids z odwróconego indeksu zawierającego dane pary label=valuelub spełniają dane wyrażenie regularne.
  • Następnie pobiera wszystkie punkty danych z magazynu danych w zadanym przedziale czasu dla znalezionych timeseries_ids.
  • Następnie baza danych wykonuje pewne obliczenia na tych punktach danych, zgodnie z żądaniem użytkownika. A potem zwraca odpowiedź.

W tej prezentacji opowiem Wam o pierwszej części. To jest wyszukiwanie timeseries_ids przez odwrócony indeks. O drugiej i trzeciej części możesz obejrzeć później Źródła VictoriaMetricslub poczekaj, aż przygotuję inne raporty :)

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Przejdźmy do odwróconego indeksu. Wielu może pomyśleć, że to proste. Kto wie, czym jest indeks odwrócony i jak działa? Och, już nie tak wielu ludzi. Spróbujmy zrozumieć, co to jest.

To właściwie proste. To po prostu słownik, który odwzorowuje klucz na wartość. Co to jest klucz? Ta para label=valueGdzie label и value - to są linie. A wartości są zbiorem timeseries_ids, który obejmuje daną parę label=value.

Odwrócony indeks pozwala szybko znaleźć wszystko timeseries_ids, które dały label=value.

Pozwala także na szybkie odnalezienie timeseries_ids szeregi czasowe dla kilku par label=valuelub dla par label=regexp. Jak to się stało? Znajdując punkt przecięcia zbioru timeseries_ids dla każdej pary label=value.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Przyjrzyjmy się różnym implementacjom odwróconego indeksu. Zacznijmy od najprostszej naiwnej implementacji. Ona wygląda tak.

Funkcja getMetricIDs pobiera listę ciągów. Każda linia zawiera label=value. Ta funkcja zwraca listę metricIDs.

Jak to działa? Tutaj mamy zmienną globalną o nazwie invertedIndex. To jest zwykły słownik (map), który zamapuje ciąg znaków, aby wyciąć int. Linia zawiera label=value.

Implementacja funkcji: get metricIDs dla pierwszego label=value, potem zajmiemy się wszystkim innym label=value, rozumiemy metricIDs dla nich. I wywołaj funkcję intersectInts, co zostanie omówione poniżej. Ta funkcja zwraca część przecięcia tych list.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Jak widać, implementacja odwróconego indeksu nie jest bardzo skomplikowana. Ale to jest naiwne wdrożenie. Jakie ma wady? Główną wadą naiwnej implementacji jest to, że taki odwrócony indeks jest przechowywany w pamięci RAM. Po ponownym uruchomieniu aplikacji tracimy ten indeks. Nie ma możliwości zapisania tego indeksu na dysku. Jest mało prawdopodobne, aby taki odwrócony indeks był odpowiedni dla bazy danych.

Druga wada jest również związana z pamięcią. Odwrócony indeks musi zmieścić się w pamięci RAM. Jeśli przekroczy wielkość pamięci RAM, wówczas oczywiście otrzymamy błąd braku pamięci. I program nie będzie działać.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Problem ten można rozwiązać, korzystając z gotowych rozwiązań, takich jak Poziom DBLub RocksDB.

Krótko mówiąc, potrzebujemy bazy danych, która pozwoli nam szybko wykonać trzy operacje.

  • Pierwszą operacją jest nagrywanie ключ-значение do tej bazy danych. Robi to bardzo szybko, gdzie ключ-значение są dowolnymi ciągami znaków.
  • Druga operacja to szybkie wyszukanie wartości za pomocą danego klucza.
  • A trzecia operacja to szybkie wyszukanie wszystkich wartości po podanym przedrostku.

LevelDB i RocksDB – te bazy danych zostały opracowane przez Google i Facebook. Najpierw pojawił się LevelDB. Potem chłopaki z Facebooka wzięli LevelDB i zaczęli go udoskonalać, stworzyli RocksDB. Teraz prawie wszystkie wewnętrzne bazy danych działają na RocksDB wewnątrz Facebooka, łącznie z tymi, które zostały przeniesione do RocksDB i MySQL. Nazwali go MojeRocks.

Odwrócony indeks można zaimplementować za pomocą LevelDB. Jak to zrobić? Zapisujemy jako klucz label=value. Wartość jest identyfikatorem szeregu czasowego, w którym występuje dana para label=value.

Jeśli mamy wiele szeregów czasowych z daną parą label=value, wówczas w tej bazie danych będzie wiele wierszy z tym samym kluczem i różnymi timeseries_ids. Aby uzyskać listę wszystkich timeseries_ids, które zaczynają się od tego label=prefix, wykonujemy skanowanie zakresu, dla którego ta baza danych jest zoptymalizowana. Oznacza to, że wybieramy wszystkie linie zaczynające się od label=prefix i zdobądź niezbędne timeseries_ids.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Oto przykładowa implementacja tego, jak by to wyglądało w Go. Mamy odwrócony indeks. To jest LevelDB.

Funkcja jest taka sama jak w przypadku implementacji naiwnej. Powtarza naiwną implementację niemal linia po linii. Jedynym punktem jest to, że zamiast się zwrócić map uzyskujemy dostęp do odwróconego indeksu. Wszystkie wartości otrzymujemy dla pierwszego label=value. Następnie przeglądamy wszystkie pozostałe pary label=value i uzyskaj dla nich odpowiednie zestawy identyfikatorów metrycznych. Następnie znajdujemy skrzyżowanie.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Wszystko wydaje się być w porządku, jednak rozwiązanie to ma wady. VictoriaMetrics początkowo wdrożyła odwrócony indeks oparty na LevelDB. Ale w końcu musiałem z tego zrezygnować.

Dlaczego? Ponieważ LevelDB jest wolniejszy niż naiwna implementacja. W naiwnej implementacji, mając dany klucz, od razu pobieramy cały wycinek metricIDs. Jest to bardzo szybka operacja – cały plasterek jest gotowy do użycia.

W LevelDB za każdym razem, gdy wywoływana jest funkcja GetValues musisz przejść przez wszystkie linie zaczynające się od label=value. I uzyskaj wartość dla każdej linii timeseries_ids. Z takich timeseries_ids zbierz ich kawałek timeseries_ids. Oczywiście jest to znacznie wolniejsze niż zwykłe uzyskiwanie dostępu do zwykłej mapy za pomocą klucza.

Drugą wadą jest to, że LevelDB jest napisany w C. Wywoływanie funkcji C z Go nie jest zbyt szybkie. Zajmuje to setki nanosekund. Nie jest to zbyt szybkie, bo w porównaniu do zwykłego wywołania funkcji napisanego w go, które zajmuje 1-5 nanosekund, różnica w wydajności jest kilkudziesięciu razy większa. Dla VictoriaMetrics była to fatalna wada :)

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Napisałem więc własną implementację odwróconego indeksu. I zadzwonił do niej zestaw scalony.

Mergeset opiera się na strukturze danych MergeTree. Ta struktura danych jest zapożyczona z ClickHouse. Oczywiście zestaw mergeset powinien być zoptymalizowany pod kątem szybkiego wyszukiwania timeseries_ids według podanego klucza. Mergeset jest napisany w całości w Go. Możesz zobaczyć Źródła VictoriaMetrics w GitHub. Implementacja mergeset znajduje się w folderze /lib/mergeset. Możesz spróbować dowiedzieć się, co się tam dzieje.

Interfejs API mergeset jest bardzo podobny do LevelDB i RocksDB. Oznacza to, że pozwala szybko zapisywać tam nowe rekordy i szybko wybierać rekordy po zadanym przedrostku.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

O wadach zestawu scalonego porozmawiamy później. Porozmawiajmy teraz o tym, jakie problemy pojawiły się w produkcji VictoriaMetrics podczas wdrażania odwróconego indeksu.

Dlaczego powstały?

Pierwszym powodem jest wysoki współczynnik rezygnacji. W tłumaczeniu na język rosyjski jest to częsta zmiana szeregów czasowych. W tym momencie kończy się szereg czasowy i zaczyna nowy lub rozpoczyna się wiele nowych szeregów czasowych. A to się często zdarza.

Drugim powodem jest duża liczba szeregów czasowych. Na początku, gdy monitoring zyskiwał na popularności, liczba szeregów czasowych była niewielka. Na przykład dla każdego komputera należy monitorować obciążenie procesora, pamięci, sieci i dysku. 4 serie czasowe na komputer. Załóżmy, że masz 100 komputerów i 400 szeregów czasowych. To bardzo mało.

Z biegiem czasu ludzie zorientowali się, że można mierzyć bardziej szczegółowe informacje. Na przykład zmierz obciążenie nie całego procesora, ale osobno każdego rdzenia procesora. Jeśli masz 40 rdzeni procesora, masz 40 razy więcej szeregów czasowych do pomiaru obciążenia procesora.

Ale to nie wszystko. Każdy rdzeń procesora może mieć kilka stanów, np. bezczynność, gdy jest bezczynny. A także pracuj w przestrzeni użytkownika, pracuj w przestrzeni jądra i innych stanach. Każdy taki stan można także mierzyć jako oddzielny szereg czasowy. To dodatkowo zwiększa liczbę rzędów 7-8 razy.

Z jednej metryki otrzymaliśmy 40 x 8 = 320 metryk dla tylko jednego komputera. Pomnóż przez 100, zamiast 32 otrzymamy 000 400.

Potem pojawił się Kubernetes. A było jeszcze gorzej, ponieważ Kubernetes może hostować wiele różnych usług. Każda usługa w Kubernetesie składa się z wielu podów. A to wszystko trzeba monitorować. Ponadto stale wdrażamy nowe wersje Twoich usług. Dla każdej nowej wersji należy utworzyć nowy szereg czasowy. W rezultacie liczba szeregów czasowych rośnie wykładniczo i stajemy przed problemem dużej liczby szeregów czasowych, co nazywa się dużą kardynalnością. VictoriaMetrics radzi sobie z tym skutecznie w porównaniu do innych baz danych szeregów czasowych.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Przyjrzyjmy się bliżej wysokiemu współczynnikowi rezygnacji. Co powoduje wysoki wskaźnik rezygnacji w produkcji? Ponieważ niektóre znaczenia etykiet i tagów stale się zmieniają.

Weźmy na przykład Kubernetes, który ma tę koncepcję deployment, czyli kiedy zostanie wdrożona nowa wersja Twojej aplikacji. Z jakiegoś powodu programiści Kubernetes zdecydowali się dodać do etykiety identyfikator wdrożenia.

Do czego to doprowadziło? Co więcej, przy każdym nowym wdrożeniu wszystkie stare szeregi czasowe zostają przerwane i zamiast nich rozpoczynają się nowe szeregi czasowe z nową wartością etykiety deployment_id. Takich wierszy mogą być setki tysięcy, a nawet miliony.

Ważną rzeczą w tym wszystkim jest to, że całkowita liczba szeregów czasowych rośnie, ale liczba szeregów czasowych, które są aktualnie aktywne i odbierają dane, pozostaje stała. Stan ten nazywany jest wysokim współczynnikiem rezygnacji.

Głównym problemem wysokiego współczynnika rezygnacji jest zapewnienie stałej szybkości wyszukiwania dla wszystkich szeregów czasowych dla danego zestawu etykiet w określonym przedziale czasu. Zwykle jest to przedział czasu dla ostatniej godziny lub ostatniego dnia.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Jak rozwiązać ten problem? Oto pierwsza opcja. Ma to na celu podzielenie odwróconego indeksu na niezależne części w czasie. Oznacza to, że po pewnym czasie kończymy pracę z bieżącym odwróconym indeksem. I utwórz nowy odwrócony indeks. Mija kolejny przedział czasu, tworzymy kolejny i kolejny.

Próbkując z tych odwróconych indeksów, znajdujemy zbiór odwróconych indeksów mieszczących się w danym przedziale. I odpowiednio stamtąd wybieramy identyfikator szeregu czasowego.

Oszczędza to zasoby, ponieważ nie musimy patrzeć na części, które nie mieszczą się w danym przedziale. Oznacza to, że zazwyczaj jeśli wybieramy dane z ostatniej godziny, to dla poprzednich przedziałów czasowych pomijamy żądania.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Istnieje inna możliwość rozwiązania tego problemu. Ma to na celu przechowywanie dla każdego dnia osobnej listy identyfikatorów szeregów czasowych, które wystąpiły w tym dniu.

Przewagą tego rozwiązania nad poprzednim rozwiązaniem jest to, że nie powielamy informacji o szeregach czasowych, które nie zanikają w czasie. Są stale obecne i nie zmieniają się.

Wadą jest to, że takie rozwiązanie jest trudniejsze do wdrożenia i trudniejsze do debugowania. I VictoriaMetrics wybrała to rozwiązanie. Tak to się działo historycznie. To rozwiązanie również wypada dobrze w porównaniu do poprzedniego. Ponieważ tego rozwiązania nie wdrożono ze względu na konieczność duplikowania danych w każdej partycji dla szeregów czasowych, które nie ulegają zmianie, czyli nie zanikają w czasie. VictoriaMetrics zostało zoptymalizowane przede wszystkim pod kątem zużycia miejsca na dysku, a poprzednia implementacja spowodowała pogorszenie zużycia miejsca na dysku. Jednak ta implementacja lepiej nadaje się do minimalizacji zużycia miejsca na dysku, dlatego została wybrana.

Musiałem z nią walczyć. Problem polegał na tym, że w tej implementacji nadal trzeba wybrać znacznie większą liczbę timeseries_ids dla danych niż wtedy, gdy odwrócony indeks jest podzielony w czasie.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Jak rozwiązaliśmy ten problem? Rozwiązaliśmy to w oryginalny sposób - przechowując w każdym odwróconym wpisie indeksu kilka identyfikatorów szeregów czasowych zamiast jednego identyfikatora. Oznacza to, że mamy klucz label=value, co występuje w każdym szeregu czasowym. A teraz oszczędzamy kilka timeseries_ids w jednym wpisie.

Oto przykład. Poprzednio mieliśmy N wpisów, ale teraz mamy jeden wpis, którego prefiks jest taki sam jak wszystkie pozostałe. W przypadku poprzedniego wpisu wartość zawiera wszystkie identyfikatory szeregów czasowych.

Umożliwiło to zwiększenie szybkości skanowania takiego odwróconego indeksu nawet 10-krotnie. Pozwoliło nam to zmniejszyć zużycie pamięci podręcznej, ponieważ teraz przechowujemy ciąg znaków label=value tylko raz w pamięci podręcznej razem N razy. Ta linia może być duża, jeśli w tagach i etykietach przechowujesz długie linie, które Kubernetes lubi tam wpychać.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Inną opcją przyspieszenia wyszukiwania w odwróconym indeksie jest sharding. Tworzenie kilku odwróconych indeksów zamiast jednego i dzielenie danych między nimi według klucza. To jest zestaw key=value para. Oznacza to, że otrzymujemy kilka niezależnych odwróconych indeksów, które możemy wykonywać równolegle na kilku procesorach. Poprzednie wdrożenia pozwalały jedynie na pracę w trybie jednoprocesorowym, czyli skanowanie danych tylko na jednym rdzeniu. Rozwiązanie to umożliwia skanowanie danych na kilku rdzeniach jednocześnie, tak jak lubi to robić ClickHouse. To właśnie planujemy wdrożyć.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Wróćmy teraz do naszych owiec - do funkcji przecięcia timeseries_ids. Zastanówmy się, jakie mogą być wdrożenia. Ta funkcja pozwala znaleźć timeseries_ids dla danego zestawu label=value.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Pierwsza opcja to naiwna implementacja. Dwie zagnieżdżone pętle. Tutaj otrzymujemy wejście funkcji intersectInts dwa plasterki - a и b. Na wyjściu powinno nam zwrócić przecięcie tych plasterków.

Naiwna implementacja wygląda tak. Iterujemy po wszystkich wartościach z plasterka a, wewnątrz tej pętli przechodzimy przez wszystkie wartości plasterka b. I porównujemy je. Jeśli pasują, oznacza to, że znaleźliśmy skrzyżowanie. I zapisz to result.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Jakie są wady? Główną wadą jest złożoność kwadratowa. Na przykład, jeśli twoje wymiary to plasterek a и b milion na raz, wówczas ta funkcja nigdy nie zwróci Ci odpowiedzi. Ponieważ będzie musiał wykonać bilion iteracji, co jest dużo nawet dla nowoczesnych komputerów.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Druga implementacja opiera się na mapie. Tworzymy mapę. Wszystkie wartości z plasterka umieszczamy na tej mapie a. Następnie przechodzimy przez plaster w osobnej pętli b. I sprawdzamy, czy ta wartość pochodzi z plasterka b na mapie. Jeśli istnieje, dodaj go do wyniku.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Jakie są korzyści? Zaletą jest to, że istnieje tylko złożoność liniowa. Oznacza to, że funkcja będzie wykonywana znacznie szybciej w przypadku większych plasterków. W przypadku wycinka o rozmiarze miliona funkcja ta zostanie wykonana w ciągu 2 milionów iteracji, w przeciwieństwie do bilionów iteracji poprzedniej funkcji.

Wadą jest to, że ta funkcja wymaga więcej pamięci do utworzenia tej mapy.

Drugą wadą jest duży narzut związany z mieszaniem. Ta wada nie jest zbyt oczywista. Dla nas też nie było to zbyt oczywiste, więc początkowo w VictoriaMetrics realizacja skrzyżowań odbywała się za pomocą mapy. Ale potem profilowanie pokazało, że czas głównego procesora spędzany jest na zapisywaniu na mapie i sprawdzaniu obecności wartości na tej mapie.

Dlaczego w takich miejscach marnuje się czas procesora? Ponieważ Go wykonuje operację mieszania na tych liniach. Oznacza to, że oblicza skrót klucza, aby następnie uzyskać do niego dostęp pod danym indeksem w HashMap. Operacja obliczania skrótu jest wykonywana w ciągu kilkudziesięciu nanosekund. W przypadku VictoriaMetrics jest to powolne.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Zdecydowałem się zaimplementować bitset zoptymalizowany specjalnie dla tego przypadku. Tak wygląda teraz przecięcie dwóch plasterków. Tutaj tworzymy bitset. Dodajemy do niego elementy z pierwszego plasterka. Następnie sprawdzamy obecność tych elementów w drugim przekroju. I dodaj je do wyniku. Oznacza to, że prawie nie różni się od poprzedniego przykładu. Tyle, że zastąpiliśmy dostęp do mapy funkcjami niestandardowymi add и has.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Na pierwszy rzut oka wydaje się, że powinno to działać wolniej, jeśli wcześniej użyto tam standardowej mapy, a następnie wywołano jakieś inne funkcje, jednak profilowanie pokazuje, że to coś działa 10 razy szybciej niż standardowa mapa w przypadku VictoriaMetrics.

Ponadto zużywa znacznie mniej pamięci w porównaniu do implementacji mapy. Ponieważ przechowujemy tutaj bity zamiast wartości ośmiobajtowych.

Wadą tej implementacji jest to, że nie jest ona ani taka oczywista, ani trywialna.

Kolejną wadą, której wielu może nie zauważyć, jest to, że w niektórych przypadkach ta implementacja może nie działać dobrze. Oznacza to, że jest zoptymalizowany dla konkretnego przypadku, dla tego przypadku przecięcia identyfikatorów szeregów czasowych VictoriaMetrics. Nie oznacza to, że nadaje się do wszystkich przypadków. Jeśli zostanie użyty nieprawidłowo, nie uzyskamy wzrostu wydajności, ale błąd braku pamięci i spowolnienie wydajności.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Rozważmy wdrożenie tej struktury. Jeśli chcesz zajrzeć, znajduje się on w źródłach VictoriaMetrics, w folderze lib/uint64set. Jest zoptymalizowany specjalnie dla przypadku VictoriaMetrics, gdzie timeseries_id to wartość 64-bitowa, gdzie pierwsze 32 bity są w zasadzie stałe, a zmieniają się tylko ostatnie 32 bity.

Ta struktura danych nie jest przechowywana na dysku, działa jedynie w pamięci.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Oto jego API. To nie jest bardzo skomplikowane. Interfejs API jest dostosowany specjalnie do konkretnego przykładu użycia VictoriaMetrics. Oznacza to, że nie ma tu żadnych niepotrzebnych funkcji. Oto funkcje, które są jawnie używane przez VictoriaMetrics.

Istnieją funkcje add, który dodaje nowe wartości. Jest funkcja has, który sprawdza nowe wartości. I jest funkcja del, który usuwa wartości. Istnieje funkcja pomocnicza len, która zwraca rozmiar zestawu. Funkcjonować clone dużo klonuje. I funkcja appendto konwertuje ten zestaw na plasterek timeseries_ids.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Tak wygląda implementacja tej struktury danych. zestaw składa się z dwóch elementów:

  • ItemsCount to pole pomocnicze umożliwiające szybkie zwrócenie liczby elementów w zestawie. Można by obejść się bez tego pola pomocniczego, ale należało je tutaj dodać, ponieważ VictoriaMetrics często pyta w swoich algorytmach o długość zestawu bitów.

  • Drugie pole to buckets. To jest wycinek konstrukcji bucket32. Każda struktura przechowuje hi pole. Są to górne 32 bity. I dwa plasterki - b16his и buckets z bucket16 Struktury.

Tutaj przechowywanych jest 16 górnych bitów drugiej części 64-bitowej struktury. Tutaj zestawy bitów są przechowywane dla niższych 16 bitów każdego bajtu.

Bucket64 składa się z tablicy uint64. Długość jest obliczana na podstawie tych stałych. W jednym bucket16 maksymalnie można zapisać 2^16=65536 fragment. Jeśli podzielisz to przez 8, otrzymasz 8 kilobajtów. Jeśli ponownie podzielisz przez 8, otrzymasz 1000 uint64 oznaczający. To jest Bucket16 – to jest nasza 8-kilobajtowa struktura.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Przyjrzyjmy się, jak zaimplementowana jest jedna z metod tej struktury do dodawania nowej wartości.

Wszystko zaczyna się od uint64 znaczenia. Obliczamy górne 32 bity, obliczamy dolne 32 bity. Przejdźmy przez wszystko buckets. Porównujemy 32 górne bity w każdym segmencie z dodaną wartością. A jeśli pasują, wywołujemy funkcję add w strukturze b32 buckets. I dodaj tam dolne 32 bity. A gdyby wrócił true, to znaczy, że dodaliśmy tam taką wartość, a takiej wartości nie mieliśmy. Jeśli powróci false, to takie znaczenie już istniało. Następnie zwiększamy liczbę elementów w konstrukcji.

Jeśli nie znaleźliśmy tego, którego potrzebujesz bucket z wymaganą wartością hi, następnie wywołujemy funkcję addAlloc, z którego powstanie nowy bucket, dodając go do struktury segmentu.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

To jest implementacja funkcji b32.add. Jest podobnie jak w przypadku poprzedniej realizacji. Obliczamy najbardziej znaczące 16 bitów, najmniej znaczące 16 bitów.

Następnie przechodzimy przez wszystkie górne 16 bitów. Znajdujemy dopasowania. Jeśli istnieje dopasowanie, wywołujemy metodę add, którą omówimy na następnej stronie bucket16.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

A oto najniższy poziom, który należy maksymalnie zoptymalizować. Obliczamy dla uint64 wartość id w bicie plasterka, a także bitmask. Jest to maska ​​dla danej wartości 64-bitowej, za pomocą której można sprawdzić obecność tego bitu lub go ustawić. Sprawdzamy, czy ten bit jest ustawiony, ustawiamy go i zwracamy obecność. To nasza implementacja, która pozwoliła nam przyspieszyć działanie przecinających się identyfikatorów szeregów czasowych 10-krotnie w porównaniu do konwencjonalnych map.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Oprócz tej optymalizacji VictoriaMetrics oferuje wiele innych optymalizacji. Większość tych optymalizacji została dodana nie bez powodu, ale po sprofilowaniu kodu na produkcji.

To jest główna zasada optymalizacji - nie dodawaj optymalizacji zakładając, że tutaj będzie wąskie gardło, bo może się okazać, że tam wąskiego gardła nie będzie. Optymalizacja zwykle pogarsza jakość kodu. Dlatego optymalizację warto wykonywać dopiero po profilowaniu, a najlepiej na produkcji, aby były to realne dane. Jeśli ktoś jest zainteresowany, może zajrzeć do kodu źródłowego VictoriaMetrics i zapoznać się z innymi dostępnymi optymalizacjami.

Przejdź do optymalizacji w VictoriaMetrics. Aleksander Walalkin

Mam pytanie odnośnie bitsetu. Bardzo podobny do implementacji wektora bool w C++, zoptymalizowany zestaw bitów. Czy stamtąd wziąłeś wdrożenie?

Nie, nie stamtąd. Implementując ten bitset kierowałem się znajomością struktury szeregów czasowych tych identyfikatorów, które są wykorzystywane w VictoriaMetrics. A ich struktura jest taka, że ​​górne 32 bity są w zasadzie stałe. Dolne 32 bity mogą ulec zmianie. Im niższy bit, tym częściej może się zmieniać. Dlatego ta implementacja jest specjalnie zoptymalizowana dla tej struktury danych. O ile mi wiadomo, implementacja C++ jest zoptymalizowana pod kątem ogólnego przypadku. Jeśli optymalizujesz dla przypadku ogólnego, oznacza to, że nie będzie ona najbardziej optymalna dla konkretnego przypadku.

Radzę także obejrzeć relację Aleksieja Milovida. Około miesiąc temu opowiadał o optymalizacji w ClickHouse pod konkretne specjalizacje. Mówi tylko, że w ogólnym przypadku implementacja C++ lub inna implementacja jest dostosowana do przeciętnego działania w szpitalu. Może działać gorzej niż implementacja oparta na wiedzy, taka jak nasza, w której wiemy, że 32 górne bity są w większości stałe.

Mam drugie pytanie. Jaka jest zasadnicza różnica w stosunku do InfluxDB?

Istnieje wiele zasadniczych różnic. Pod względem wydajności i zużycia pamięci InfluxDB w testach wykazuje 10 razy większe zużycie pamięci dla szeregów czasowych o dużej kardynalności, gdy jest ich dużo, na przykład miliony. Na przykład VictoriaMetrics zużywa 1 GB na milion aktywnych wierszy, podczas gdy InfluxDB zużywa 10 GB. A to duża różnica.

Druga zasadnicza różnica polega na tym, że InfluxDB ma dziwne języki zapytań – Flux i InfluxQL. Nie są one zbyt wygodne do pracy z szeregami czasowymi w porównaniu do PromQL, który jest obsługiwany przez VictoriaMetrics. PromQL to język zapytań firmy Prometheus.

Jeszcze jedna różnica polega na tym, że InfluxDB ma nieco dziwny model danych, w którym w każdej linii można przechowywać kilka pól z innym zestawem tagów. Linie te są dalej podzielone na różne tabele. Te dodatkowe komplikacje komplikują późniejszą pracę z tą bazą danych. Trudno to wspierać i rozumieć.

W VictoriaMetrics wszystko jest znacznie prostsze. Tam każda seria czasowa jest kluczem-wartością. Wartość jest zbiorem punktów - (timestamp, value), a kluczem jest zestaw label=value. Nie ma rozdziału pomiędzy polami i pomiarami. Umożliwia wybranie dowolnych danych, a następnie łączenie, dodawanie, odejmowanie, mnożenie i dzielenie, w przeciwieństwie do InfluxDB, gdzie, o ile wiem, obliczenia między różnymi wierszami nadal nie są realizowane. Nawet jeśli zostaną zaimplementowane, jest to trudne, trzeba napisać dużo kodu.

Mam pytanie wyjaśniające. Czy dobrze zrozumiałem, że był jakiś problem, o którym mówiłeś, że ten odwrócony indeks nie mieści się w pamięci, więc jest tam partycjonowanie?

Najpierw pokazałem naiwną implementację odwróconego indeksu na standardowej mapie Go. Ta implementacja nie jest odpowiednia dla baz danych, ponieważ ten odwrócony indeks nie jest zapisywany na dysku, a baza danych musi zostać zapisana na dysku, aby dane pozostały dostępne po ponownym uruchomieniu. W tej implementacji po ponownym uruchomieniu aplikacji odwrócony indeks zniknie. Stracisz dostęp do wszystkich danych, ponieważ nie będziesz mógł ich znaleźć.

Cześć! Dziękujemy za raport! Nazywam się Paweł. Jestem z Wildberries. Mam do Ciebie kilka pytań. Pytanie pierwsze. Czy myślisz, że gdybyś przy budowie architektury swojej aplikacji wybrał inną zasadę i podzielił dane w czasie, to być może byłbyś w stanie przecinać dane podczas wyszukiwania, opierając się jedynie na tym, że jedna partycja zawiera dane dla drugiej przez pewien okres czasu, czyli w jednym przedziale czasu i nie musiałbyś się martwić, że Twoje kawałki będą inaczej rozmieszczone? Pytanie nr 2 - skoro implementujesz podobny algorytm z bitsetem i całą resztą, to może próbowałeś użyć instrukcji procesora? Może próbowałeś takich optymalizacji?

Na drugie odpowiem od razu. Jeszcze nie dotarliśmy do tego punktu. Ale jeśli zajdzie taka potrzeba, dotrzemy tam. A pierwsze pytanie: jakie było pytanie?

Omówiłeś dwa scenariusze. I powiedzieli, że wybrali ten drugi, z bardziej złożoną implementacją. I nie woleli pierwszego, w którym dane są podzielone według czasu.

Tak. W pierwszym przypadku całkowita objętość indeksu byłaby większa, ponieważ w każdej partycji musielibyśmy przechowywać zduplikowane dane dla tych szeregów czasowych, które przebiegają przez wszystkie te partycje. A jeśli współczynnik rezygnacji Twoich szeregów czasowych jest niewielki, czyli stale wykorzystywane są te same szeregi, to w pierwszym przypadku stracilibyśmy znacznie więcej na ilości zajmowanego miejsca na dysku w porównaniu do drugiego przypadku.

I tak - tak, podział czasu jest dobrą opcją. Prometeusz go używa. Ale Prometeusz ma jeszcze jedną wadę. Podczas łączenia tych fragmentów danych musi przechowywać w pamięci metainformacje dla wszystkich etykiet i szeregów czasowych. Dlatego też, jeśli scalane fragmenty danych są duże, zużycie pamięci podczas łączenia znacznie wzrasta, w przeciwieństwie do VictoriaMetrics. Podczas łączenia VictoriaMetrics w ogóle nie zużywa pamięci; zużywane jest tylko kilka kilobajtów, niezależnie od rozmiaru scalonych fragmentów danych.

Algorytm, którego używasz, wykorzystuje pamięć. Oznacza znaczniki szeregów czasowych, które zawierają wartości. W ten sposób sprawdzasz obecność pary w jednej tablicy danych i w drugiej. I rozumiesz, czy nastąpiło przecięcie, czy nie. Zazwyczaj bazy danych implementują kursory i iteratory, które przechowują ich bieżącą zawartość i przeglądają posortowane dane ze względu na prostą złożoność tych operacji.

Dlaczego nie używamy kursorów do przeglądania danych?

Tak.

Posortowane wiersze przechowujemy w LevelDB lub mergeset. Możemy przesunąć kursor i znaleźć przecięcie. Dlaczego z tego nie korzystamy? Ponieważ jest powolny. Ponieważ kursory oznaczają, że musisz wywołać funkcję dla każdej linii. Wywołanie funkcji trwa 5 nanosekund. A jeśli mamy 100 000 000 linii, to okazuje się, że na samo wywołanie funkcji spędzamy pół sekundy.

Jest coś takiego, tak. I moje ostatnie pytanie. Pytanie może wydawać się trochę dziwne. Dlaczego nie można w momencie nadejścia danych odczytać wszystkich niezbędnych agregatów i zapisać ich w wymaganej formie? Po co oszczędzać ogromne wolumeny w niektórych systemach, takich jak VictoriaMetrics, ClickHouse itp., a następnie spędzać na nich dużo czasu?

Podam przykład, żeby było jaśniej. Powiedzmy, jak działa mały prędkościomierz zabawkowy? Rejestruje przebyty dystans, cały czas dodając go do jednej wartości, a drugi raz. I dzieli. I osiąga średnią prędkość. Możesz zrobić mniej więcej to samo. Dodaj wszystkie niezbędne fakty na bieżąco.

OK, rozumiem pytanie. Twój przykład ma swoje miejsce. Jeśli wiesz, jakich agregatów potrzebujesz, jest to najlepsza implementacja. Problem polega jednak na tym, że ludzie zapisują te metryki, niektóre dane w ClickHouse i nie wiedzą jeszcze, w jaki sposób będą je agregować i filtrować w przyszłości, więc muszą zapisywać wszystkie surowe dane. Ale jeśli wiesz, że musisz obliczyć coś średnio, to dlaczego tego nie obliczyć, zamiast przechowywać tam kilka surowych wartości? Ale dzieje się tak tylko wtedy, gdy wiesz dokładnie, czego potrzebujesz.

Nawiasem mówiąc, bazy danych do przechowywania szeregów czasowych obsługują zliczanie agregatów. Na przykład Prometeusz wspiera zasady nagrywania. Oznacza to, że można to zrobić, jeśli wiesz, jakich jednostek będziesz potrzebować. VictoriaMetrics jeszcze tego nie ma, ale zwykle poprzedza go Prometheus, w którym można to zrobić w regułach przekodowania.

Na przykład w mojej poprzedniej pracy musiałem policzyć liczbę zdarzeń w przesuwanym oknie w ciągu ostatniej godziny. Problem w tym, że musiałem zrobić w Go niestandardową implementację, czyli usługę zliczania tego czegoś. Usługa ta ostatecznie nie była trywialna, bo trudna do wyliczenia. Implementacja może być prosta, jeśli konieczne jest zliczanie niektórych agregatów w ustalonych odstępach czasu. Jeśli chcesz liczyć zdarzenia w przesuwanym oknie, to nie jest to tak proste, jak się wydaje. Myślę, że nie zostało to jeszcze zaimplementowane w ClickHouse ani w bazach danych serii czasowych, ponieważ jest trudne do wdrożenia.

I jeszcze jedno pytanie. Właśnie rozmawialiśmy o uśrednianiu i przypomniałem sobie, że kiedyś istniało coś takiego jak grafit z zapleczem węglowym. I wiedział, jak przerzedzić stare dane, to znaczy pozostawić jeden punkt na minutę, jeden punkt na godzinę itp. W zasadzie jest to całkiem wygodne, jeśli potrzebujemy surowych danych, relatywnie rzecz biorąc, przez miesiąc, a wszystko inne może być rozcieńczony. Jednak Prometheus i VictoriaMetrics nie obsługują tej funkcji. Czy planuje się go wspierać? Jeśli nie, to dlaczego nie?

Dziękuję za pytanie. Nasi użytkownicy zadają to pytanie okresowo. Pytają, kiedy dodamy obsługę downsamplingu. Jest tu kilka problemów. Po pierwsze, każdy użytkownik rozumie downsampling coś innego: ktoś chce otrzymać dowolny punkt w danym przedziale, ktoś chce wartości maksymalnej, minimalnej, średniej. Jeśli wiele systemów zapisuje dane w Twojej bazie danych, nie można ich wszystkich połączyć w jedną całość. Może się zdarzyć, że każdy system wymaga innego rozcieńczenia. A to jest trudne do wdrożenia.

Po drugie, VictoriaMetrics, podobnie jak ClickHouse, jest zoptymalizowana do pracy na dużych ilościach nieprzetworzonych danych, więc może przerzucić miliard wierszy w mniej niż sekundę, jeśli w systemie jest wiele rdzeni. Skanowanie punktów szeregów czasowych w VictoriaMetrics – 50 000 000 punktów na sekundę na rdzeń. Wydajność ta skaluje się do istniejących rdzeni. Oznacza to, że jeśli masz na przykład 20 rdzeni, będziesz skanować miliard punktów na sekundę. Ta właściwość VictoriaMetrics i ClickHouse zmniejsza potrzebę downsamlingu.

Kolejną cechą jest to, że VictoriaMetrics skutecznie kompresuje te dane. Kompresja średnio w produkcji wynosi od 0,4 do 0,8 bajta na punkt. Każdy punkt to znacznik czasu + wartość. I jest kompresowany średnio do mniej niż jednego bajtu.

Siergiej. Mam pytanie. Jaki jest minimalny kwant czasu nagrywania?

Jedna milisekunda. Niedawno odbyliśmy rozmowę z innymi twórcami baz danych szeregów czasowych. Ich minimalny przedział czasu to jedna sekunda. Na przykład w graficie jest to również jedna sekunda. W OpenTSDB jest to również jedna sekunda. InfluxDB ma precyzję nanosekundową. W VictoriaMetrics jest to jedna milisekunda, ponieważ w Prometheusie jest to jedna milisekunda. Usługa VictoriaMetrics została pierwotnie opracowana jako zdalna pamięć masowa dla Prometheusa. Ale teraz może zapisywać dane z innych systemów.

Osoba, z którą rozmawiałem, twierdzi, że mają dokładność co do sekundy - im to wystarczy, ponieważ zależy to od rodzaju danych, które są przechowywane w bazie danych szeregów czasowych. Jeśli są to dane DevOps lub dane z infrastruktury, gdzie zbierasz je w odstępach 30 sekund na minutę, to wystarczy druga dokładność, nie potrzebujesz niczego mniej. A jeśli zbierasz te dane z systemów transakcyjnych o wysokiej częstotliwości, potrzebujesz dokładności w nanosekundach.

Dokładność milisekundowa w VictoriaMetrics jest również odpowiednia w przypadku DevOps i może być odpowiednia w większości przypadków, o których wspomniałem na początku raportu. Jedyną rzeczą, do której może nie być odpowiedni, są systemy handlu o wysokiej częstotliwości.

Dziękuję! I kolejne pytanie. Jaka jest kompatybilność w PromQL?

Pełna kompatybilność wsteczna. VictoriaMetrics w pełni obsługuje PromQL. Dodatkowo dodaje dodatkową zaawansowaną funkcjonalność w PromQL, która jest tzw MetrykiQL. Na YouTube krąży dyskusja na temat tej rozszerzonej funkcjonalności. Wiosną przemawiałem na Spotkaniu Monitorującym w St. Petersburgu.

Kanał telegramu VictoriaMetrics.

W ankiecie mogą brać udział tylko zarejestrowani użytkownicy. Zaloguj się, Proszę.

Co powstrzymuje Cię przed przejściem na VictoriaMetrics jako długoterminowe miejsce przechowywania danych dla Prometheusa? (Napisz w komentarzu, dodam to do ankiety))

  • 71,4%Nie używam Prometheusa 5

  • 28,6%Nie wiedziałem o VictoriaMetrics2

Głosowało 7 użytkowników. 12 użytkowników wstrzymało się od głosu.

Źródło: www.habr.com

Dodaj komentarz