
Baza danych szeregów czasowych (TSDB, time series database) w Prometheus 2 to doskonały przykład rozwiązania inżynieryjnego, które oferuje znaczące ulepszenia w porównaniu do przechowalni v2 w Prometheus 1 pod względem prędkości gromadzenia danych i wykonywania zapytań oraz efektywności wykorzystania zasobów. Wdrożyliśmy Prometheus 2 w Percona Monitoring and Management (PMM), a ja miałem okazję zbadać wydajność Prometheus 2 TSDB. W tym artykule omówię wyniki tych obserwacji.
Średnie obciążenie robocze Prometheus
Dla tych, którzy są przyzwyczajeni do pracy z bazami danych o podstawowym przeznaczeniu, zwykłe obciążenie robocze Prometheus jest dość ciekawe. Prędkość gromadzenia danych dąży do stabilnej wartości: zazwyczaj usługi, które monitorujesz, wysyłają w przybliżeniu taką samą ilość metryk, a infrastruktura zmienia się stosunkowo wolno.
Zapytania informacyjne mogą pochodzić z różnych źródeł. Niektóre z nich, takie jak alerty, również dążą do stabilnej i przewidywalnej wartości. Inne, takie jak zapytania użytkowników, mogą powodować skoki, choć nie jest to charakterystyczne dla większości obciążenia.
Test obciążeniowy
W trakcie testowania skupiłem się na zdolności gromadzenia danych. Uruchomiłem Prometheus 2.3.2, skompilowany przy użyciu Go 1.10.1 (jako część PMM 1.14) na usłudze Linode, korzystając z tego skryptu: . Aby maksymalnie realistycznie generować obciążenie, za pomocą tego uruchomiłem kilka węzłów MySQL z rzeczywistym obciążeniem (Test Sysbench TPC-C), z których każdy emulował 10 węzłów Linux/MySQL.
Wszystkie poniższe testy przeprowadzono na serwerze Linode z ośmioma wirtualnymi rdzeniami i 32 GB pamięci, na którym uruchomiono 20 symulacji obciążeniowych monitorujących dwieście instancji MySQL. Lub, w terminach Prometheus, 800 celów (targets), 440 zbiorów (scrapes) na sekundę, 380 tysięcy rekordów (samples) na sekundę i 1,7 miliona aktywnych szeregów czasowych.
Projekt
Zwykłe podejście tradycyjnych baz danych, w tym to, które wykorzystywał Prometheus 1.x, polega na . Jeśli jest go zbyt mało, aby wytrzymać obciążenie, napotkasz duże opóźnienia, a niektóre zapytania nie zostaną wykonane. Użycie pamięci w Prometheus 2 konfiguruje się za pomocą klucza storage.tsdb.min-block-duration, który określa, jak długo zapisy będą przechowywane w pamięci przed zapisaniem na dysku (domyślnie to 2 godziny). Ilość potrzebnej pamięci będzie zależała od liczby szeregów czasowych, etykiet (labels) oraz intensywności zbierania danych (scrapes), w sumie z czystym napływem danych. W kwestii przestrzeni dyskowej, Prometheus dąży do użycia 3 bajtów na zapis (sample). Z drugiej strony, wymagania dotyczące pamięci są znacznie wyższe.
Mimo że istnieje możliwość skonfigurowania rozmiaru bloku, nie zaleca się ręcznego ustawiania tego parametru, więc stajesz przed koniecznością zapewnienia Prometheusowi tyle pamięci, ile potrzebuje dla twojego obciążenia.
Jeśli pamięci będzie niewystarczająco, aby obsłużyć napływ metryk, Prometheus ulegnie awarii z powodu braku pamięci lub zostanie zamknięty przez OOM killer.
Dodanie swapu, aby opóźnić moment awarii, gdy Prometheus zabraknie pamięci, nie pomaga szczególnie, ponieważ wykorzystanie tej funkcji powoduje wybuchowe zużycie pamięci. Myślę, że chodzi o Go, jego garbage collector i sposób, w jaki działa z swapem.
Innym interesującym podejściem jest skonfigurowanie zapisu bloku head na dysk o określonej porze, zamiast liczyć od momentu uruchomienia procesu.

Jak można zobaczyć na wykresie, zapisy na dysk odbywają się co dwie godziny. Jeśli zmienisz parametr min-block-duration na godzinę, te zapisy będą odbywać się co godzinę, zaczynając po pół godzinie.
Jeśli chcesz używać tego i innych wykresów w swojej instalacji Prometheus, możesz wykorzystać ten . Został on zaprojektowany dla PMM, ale przy niewielkich zmianach pasuje do każdej instalacji Prometheus.
Mamy aktywny blok, zwany head block, który jest przechowywany w pamięci; bloki z starszymi danymi są dostępne poprzez mmap(). To eliminuje potrzebę konfigurowania osobnego cache, ale również oznacza, że musisz pozostawić wystarczająco dużo miejsca dla cache systemu operacyjnego, jeśli chcesz wykonywać zapytania do danych starszych niż te, które pomieści head block.
To również oznacza, że zużycie pamięci wirtualnej przez Prometheus będzie wyglądać dość wysoko, na co nie warto się zbytnio martwić.

Kolejnym interesującym aspektem projektu jest wykorzystanie WAL (write ahead log). Jak wynika z dokumentacji dotyczącej magazynu, Prometheus używa WAL, aby uniknąć utraty danych podczas awarii. Niestety konkretne mechanizmy zapewnienia trwałości danych są słabo udokumentowane. Wersja Prometheus 2.3.2 zapisuje WAL na dysku co 10 sekund, a ten parametr nie jest konfigurowany przez użytkownika.
Kompakcje (Compactions)
Prometheus TSDB jest zaprojektowany na wzór LSM (Log Structured Merge - dziennikowo-strukturalne drzewo z łączeniem): blok główny jest okresowo zapisywany na dysku, a mechanizm kompaktacji łączy kilka bloków razem, aby uniknąć skanowania zbyt dużej liczby bloków podczas zapytań. Tutaj widać liczbę bloków, które obserwowałem w systemie testowym po dobie obciążenia.

Jeśli chcesz dowiedzieć się więcej o magazynie, możesz zbadać plik meta.json, w którym są informacje o dostępnych blokach i o tym, jak się pojawiły.
{
"ulid": "01CPZDPD1D9R019JS87TPV5MPE",
"minTime": 1536472800000,
"maxTime": 1536494400000,
"stats": {
"numSamples": 8292128378,
"numSeries": 1673622,
"numChunks": 69528220
},
"compaction": {
"level": 2,
"sources": [
"01CPYRY9MS465Y5ETM3SXFBV7X",
"01CPYZT0WRJ1JB1P0DP80VY5KJ",
"01CPZ6NR4Q3PDP3E57HEH760XS"
],
"parents": [
{
"ulid": "01CPYRY9MS465Y5ETM3SXFBV7X",
"minTime": 1536472800000,
"maxTime": 1536480000000
},
{
"ulid": "01CPYZT0WRJ1JB1P0DP80VY5KJ",
"minTime": 1536480000000,
"maxTime": 1536487200000
},
{
"ulid": "01CPZ6NR4Q3PDP3E57HEH760XS",
"minTime": 1536487200000,
"maxTime": 1536494400000
}
]
},
"version": 1
}Kompakcje w Prometheus są związane z czasem zapisu bloku głównego na dysk. W tym momencie może odbywać się kilka takich operacji.

Wygląda na to, że kompresja nie jest w żaden sposób ograniczona i może powodować duże skoki I/O dysku podczas działania.

Skoki obciążenia CPU

Oczywiście, ma to dość negatywny wpływ na prędkość działania systemu, a także stanowi poważne wyzwanie dla magazynów LSM: jak przeprowadzać kompresję, aby utrzymać wysoką prędkość zapytań, a jednocześnie nie powodować zbyt dużego obciążenia?
Zarządzanie pamięcią w procesie kompresji również wygląda dość interesująco.

Możemy zobaczyć, jak po kompresji większość pamięci zmienia stan z Cached na Free: oznacza to, że potencjalnie cenna informacja została z niej usunięta. Ciekawe, czy używa się tutaj fadvice() czy jakiejś innej techniki minimalizacji, czy jest to spowodowane tym, że pamięć podręczna została opróżniona z bloków zniszczonych podczas kompresji?
Odzyskiwanie po awarii
Odzyskiwanie po awariach zajmuje czas, i to jest uzasadnione. Dla przychodzącego strumienia z milionem zapisów na sekundę musiałem czekać około 25 minut, podczas gdy trwało odzyskiwanie na dysku SSD.
level=info ts=2018-09-13T13:38:14.09650965Z caller=main.go:222 msg="Rozpoczynanie Prometheus" version="(version=2.3.2, branch=v2.3.2, revision=71af5e29e815795e9dd14742ee7725682fa14b7b)"
level=info ts=2018-09-13T13:38:14.096599879Z caller=main.go:223 build_context="(go=go1.10.1, user=Jenkins, date=20180725-08:58:13OURCE)"
level=info ts=2018-09-13T13:38:14.096624109Z caller=main.go:224 host_details="(Linux 4.15.0-32-generic #35-Ubuntu SMP Fri Aug 10 17:58:07 UTC 2018 x86_64 1bee9e9b78cf (none))"
level=info ts=2018-09-13T13:38:14.096641396Z caller=main.go:225 fd_limits="(soft=1048576, hard=1048576)"
level=info ts=2018-09-13T13:38:14.097715256Z caller=web.go:415 component=web msg="Rozpocznij nasłuchiwanie na połączenia" address=:9090
level=info ts=2018-09-13T13:38:14.097400393Z caller=main.go:533 msg="Rozpoczynanie TSDB ..."
level=info ts=2018-09-13T13:38:14.098718401Z caller=repair.go:39 component=tsdb msg="znaleziono zdrowy blok" mint=1536530400000 maxt=1536537600000 ulid=01CQ0FW3ME8Q5W2AN5F9CB7R0R
level=info ts=2018-09-13T13:38:14.100315658Z caller=web.go:467 component=web msg="prefiks routera" prefix=\/prometheus
level=info ts=2018-09-13T13:38:14.101793727Z caller=repair.go:39 component=tsdb msg="znaleziono zdrowy blok" mint=1536732000000 maxt=1536753600000 ulid=01CQ78486TNX5QZTBF049PQHSM
level=info ts=2018-09-13T13:38:14.102267346Z caller=repair.go:39 component=tsdb msg="znaleziono zdrowy blok" mint=1536537600000 maxt=1536732000000 ulid=01CQ78DE7HSQK0C0F5AZ46YGF0
level=info ts=2018-09-13T13:38:14.102660295Z caller=repair.go:39 component=tsdb msg="znaleziono zdrowy blok" mint=1536775200000 maxt=1536782400000 ulid=01CQ7SAT4RM21Y0PT5GNSS146Q
level=info ts=2018-09-13T13:38:14.103075885Z caller=repair.go:39 component=tsdb msg="znaleziono zdrowy blok" mint=1536753600000 maxt=1536775200000 ulid=01CQ7SV8WJ3C2W5S3RTAHC2GHB
level=error ts=2018-09-13T14:05:18.208469169Z caller=wal.go:275 component=tsdb msg="Wykryto uszkodzenie WAL; stosując trymowanie" err="nieoczekiwany kontrolny CRC32 d0465484, oczekiwano 0" file=\/opt\/prometheus\/data\/ .prom2-data\/wal\/007357 pos=15504363
level=info ts=2018-09-13T14:05:19.471459777Z caller=main.go:543 msg="TSDB uruchomiony"
level=info ts=2018-09-13T14:05:19.471604598Z caller=main.go:603 msg="Ładowanie pliku konfiguracyjnego" filename=\/etc\/prometheus.yml
level=info ts=2018-09-13T14:05:19.499156711Z caller=main.go:629 msg="Zakończono ładowanie pliku konfiguracyjnego" filename=\/etc\/prometheus.yml
level=info ts=2018-09-13T14:05:19.499228186Z caller=main.go:502 msg="Serwer jest gotowy do przyjmowania żądań sieciowych."Głównym problemem procesu przywracania jest wysokie zużycie pamięci. Mimo że w normalnej sytuacji serwer może stabilnie działać z takim samym poziomem pamięci, w przypadku awarii może się nie podnieść z powodu OOM. Jedynym rozwiązaniem, które znalazłem, jest wyłączenie zbierania danych, uruchomienie serwera, pozwolenie mu na odbudowę, a następnie ponowne uruchomienie z włączonym zbieraniem.
Rozgrzewka
Kolejnym zachowaniem, o którym należy pamiętać podczas rozgrzewki, jest stosunek niskiej wydajności do wysokiego zużycia zasobów tuż po uruchomieniu. Podczas niektórych, ale nie wszystkich uruchomień, zauważyłem znaczne obciążenie CPU i pamięci.


Spadki w wykorzystaniu pamięci wskazują, że Prometheus nie jest w stanie skonfigurować wszystkich zbiorów na starcie, co prowadzi do utraty niektórych informacji.
Nie ustaliłem dokładnych przyczyn wysokiego obciążenia procesora i pamięci. Podejrzewam, że jest to związane z tworzeniem nowych szeregów czasowych w bloku head z wysoką częstotliwością.
Skoki obciążenia CPU
Oprócz nieciągłości, które generują dość wysokie obciążenie I/O, zauważyłem poważne skoki obciążenia procesora co dwie minuty. Wpływ ten jest dłuższy przy dużym napływie danych i wydaje się, że są one spowodowane przez śmieciarza Go, przynajmniej niektóre rdzenie są całkowicie obciążone.


Te skoki nie są wcale nieistotne. Wydaje się, że gdy występują, wewnętrzny punkt dostępu oraz metryki Prometheus stają się niedostępne, co powoduje przerwy w danych w tych samych przedziałach czasowych.

Można również zauważyć, że eksporter Prometheus zawiesza się na jedną sekundę.

Możemy zauważyć korelacje ze zbieraniem śmieci (GC).

Podsumowanie
TSDB w Prometheus 2 działa szybko, jest w stanie obsłużyć miliony szeregów czasowych i jednocześnie tysiące zapisów na sekundę, używając do tego dość skromnego sprzętu. Wykorzystanie CPU oraz dysku I/O również imponuje. Mój przykład wykazywał do 200 000 metryk na sekundę na jeden używany rdzeń.
Przy planowaniu rozbudowy należy pamiętać o wystarczających ilościach pamięci, musi to być pamięć rzeczywista. Objętość pamięci, którą obserwowałem, wynosiła około 5 GB na 100 000 zapisów na sekundę napływu danych, co w połączeniu z pamięcią podręczną systemu operacyjnego dawało łącznie około 8 GB zajętej pamięci.
Oczywiście, jeszcze wiele pracy przed nami, aby okiełznać skoki CPU i I/O dysku, co nie jest zaskoczeniem, biorąc pod uwagę, jak młoda jest TSDB Prometheus 2 w porównaniu do InnoDB, TokuDB, RocksDB, WiredTiger, ale wszystkie miały podobne problemy na początku swojego cyklu życia.
Źródło: habr.com
