Optymalizacja ciągów w ClickHouse. Referat Yandexu

Analityczna baza danych ClickHouse przetwarza wiele różnych ciągów, wykorzystując zasoby. Aby przyspieszyć działanie systemu, ciągle dodawane są nowe optymalizacje. Programista ClickHouse, Nikolaj Koczetow, opowiada o typie danych ciągowych, w tym o nowym typie, LowCardinality, i wyjaśnia, jak można przyspieszyć pracę z ciągami.

Odtwarzaj wideo

— Na początku zrozummy, jak można przechowywać ciągi.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Mamy typy danych ciągowych. String jest dobrze dostosowany jako domyślny, warto go używać prawie zawsze. Ma niewielki Overhead — 9 bajtów na jeden ciąg. Jeśli chcemy, aby rozmiar ciągów był stały i znany z góry, lepiej użyć FixedString. W nim można określić potrzebną liczbę bajtów, jest wygodny dla danych typu adresy IP lub funkcje skrótu.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Oczywiście, czasami coś zwalnia. Powiedzmy, że wykonujesz zapytanie do tabeli. ClickHouse odczytuje dość dużą ilość danych, powiedzmy, z szybkością 100 GB/s, przy jednoczesnym przetwarzaniu niewielkiej liczby ciągów. Mamy dwie tabele, które przechowują prawie identyczne dane. Z drugiej tabeli ClickHouse odczytuje dane z większą prędkością, ale liczba ciągów odczytywanych na sekundę jest trzy razy mniejsza.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Jeżeli spojrzymy na rozmiar skompresowanych danych, okaże się, że jest prawie równy. W rzeczywistości w tabelach zapisane są te same dane — miliard pierwszych liczb — tylko w pierwszej kolumnie zapisane są jako UInt64, a w drugiej — jako String. Z tego powodu drugie zapytanie dłużej odczytuje dane z dysku i je dekompresuje.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Oto inny przykład. Załóżmy, że istnieje z góry znany zbiór ciągów, ograniczony stałą 1000 lub 10 000 i praktycznie nigdy się nie zmienia. Dla tego przypadku odpowiedni jest typ danych Enum, w ClickHouse są dwa — Enum8 i Enum16. Dzięki przechowywaniu w Enum szybko przetwarzamy zapytania.

W ClickHouse istnieją optymalizacje dla GROUP BY, IN, DISTINCT i optymalizacje dla niektórych funkcji, na przykład dla porównania z stałym ciągiem. Oczywiście liczby w ciągu nie są konwertowane, a wręcz przeciwnie, stały ciąg jest przekształcany do wartości Enum. Po tym wszystkim wszystko szybko się porównuje.

Ale są też wady. Nawet jeśli znamy dokładny zbiór ciągów, czasami musi on być uzupełniany. Pojawił się nowy ciąg — musimy wykonać ALTER.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

ALTER dla Enum w ClickHouse jest zoptymalizowane. Nie przepisujemy danych na dysku, ale ALTER mogą zwalniać z powodu tego, że struktury Enum są przechowywane w schemacie samej tabeli. Dlatego musimy poczekać na zapytania do odczytu z tabeli, na przykład.

Pojawia się pytanie, czy można to zrobić lepiej? Pewnie, że tak. Można zachować strukturę Enum nie w schemacie tabeli, ale w ZooKeeper. Mogą jednak wystąpić problemy związane z synchronizacją. Na przykład jedna replika otrzymała dane, druga — nie, i jeśli ma stary Enum, to coś się zepsuje. (W ClickHouse prawie zakończyliśmy współbieżne zapytania ALTER. Gdy je zakończymy całkowicie, nie będziemy musieli czekać na zapytania do odczytu.)

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Aby nie bawić się w ALTER Enum, można używać zewnętrznych słowników ClickHouse. Przypomnę, że to struktura danych w formacie klucz-wartość wewnątrz ClickHouse, za pomocą której można pozyskiwać dane z zewnętrznych źródeł, na przykład z tabel MySQL.

W słowniku ClickHouse przechowujemy wiele różnych ciągów, a w tabeli — ich identyfikatory w postaci liczb. Jeśli potrzebujemy uzyskać ciąg, wywołujemy funkcję dictGet i pracujemy z nią. Po tym nie musimy wykonywać ALTER. Aby coś dodać do Enum, wstawiamy to do tej samej tabeli MySQL.

Ale pojawiają się inne problemy. Po pierwsze, niewygodna składnia. Jeśli chcemy uzyskać ciąg, musimy wywołać dictGet. Po drugie, brak niektórych optymalizacji. Porównanie z stałym ciągiem dla słowników nie jest równie szybkie.

Mogą także wystąpić problemy z aktualizacją. Załóżmy, że zapytaliśmy o ciąg w słowniku podręcznym, ale ten nie został do niego dodany. W takim przypadku musimy poczekać, aż dane załadują się z zewnętrznego źródła.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Ogólnym brakiem obu metod jest to, że przechowujemy wszystkie klucze w jednym miejscu i je synchronizujemy. Dlaczego więc nie trzymać słowników lokalnie? Brak synchronizacji — brak problemów. Można przechowywać słownik lokalnie na kawałku dysku. To znaczy, że zrobiliśmy Insert, zapisaliśmy słownik. Jeśli pracujemy z danymi w pamięci, możemy zapisać słownik albo w bloku danych, albo w kawałku kolumny, albo w jakimś pamięci podręcznej, aby przyspieszyć obliczenia.

Kodowanie słowników ciągów

W ten sposób przeszliśmy do stworzenia nowego typu danych w ClickHouse — LowCardinality. To format przechowywania danych: jak są zapisywane na dysku i jak są odczytywane, jak są reprezentowane w pamięci oraz schema ich przetwarzania.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Na slajdzie są dwie kolumny. Po prawej stronie standardowo przechowywane są wiersze, w typie String. Widać, że są to jakieś modele telefonów komórkowych. Po lewej stronie znajduje się dokładnie taka sama kolumna, tylko w typie LowCardinality. Składa się ona ze słownika z wieloma różnymi wierszami (wiersze z kolumny po prawej) i listy pozycji (numerów wierszy).

Dzięki tym dwóm strukturom można odtworzyć pierwotną kolumnę. Istnieje również odwrotny indeks — tabela haszująca, która pomaga znaleźć pozycję w słowniku na podstawie wiersza. Jest potrzebna do przyspieszenia niektórych zapytań. Na przykład, jeśli chcemy porównać, wyszukać wiersz w naszej kolumnie lub je ze sobą połączyć.

LowCardinality — parametryczny typ danych. Może być albo liczbą, albo czymś, co jest przechowywane jako liczba, albo ciągiem znaków, albo Nullable od nich.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Cechą LowCardinality jest to, że może być przechowywany dla niektórych funkcji. Na slajdzie widać przykład zapytania. W pierwszej linijce stworzyłem kolumnę typu LowCardinality od String, nazwałem ją S. Następnie zapytałem o jej nazwę — ClickHouse powiedział, że to LowCardinality od String. Wszystko się zgadza.

Trzecia linijka jest prawie taka sama, tylko wywołaliśmy funkcję length. W ClickHouse funkcja length zwraca typ danych UInt64. Ale my mamy teraz LowCardinality od UInt64. O co w tym chodzi?

Optymalizacja ciągów w ClickHouse. Referat Yandexu

W słowniku przechowywane były nazwy telefonów komórkowych, zastosowaliśmy funkcję length. Teraz mamy podobny słownik, składający się tylko z liczb — to długości ciągów. Kolumna z pozycjami się nie zmieniła. W efekcie przetworzyliśmy mniej danych, oszczędzając czas zapytania.

Mogą być też inne optymalizacje, na przykład dodanie prostego cache. Przy obliczaniu wartości funkcji można zapamiętać ją i zbudować podobną bez ponownego obliczania.

Może być też dokonana optymalizacja GROUP BY, ponieważ nasza kolumna ze słownikiem już częściowo zgrupowana — można szybciej obliczać wartość funkcji haszujących i mniej więcej znaleźć bucket, do którego umieścić kolejny wiersz. Można także specjalizować niektóre funkcje agregujące, na przykład uniq, ponieważ do niej można przesłać tylko słownik, a pozycje pozostawić nietknięte — dzięki temu wszystko będzie działać szybciej. Pierwsze dwie optymalizacje już dodaliśmy do ClickHouse.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

A co, jeśli stworzymy kolumnę z naszym typem danych i wstawimy do niej wiele różnych złych wierszy? Czy nasza pamięć się nie przepełni? Nie, w tym celu ClickHouse ma dwie specjalne ustawienia. Pierwsza to low_cardinality_max_dictionary_size. To maksymalny rozmiar słownika, który może być zapisany na dysku. Wstawianie odbywa się w następujący sposób: gdy wstawiamy dane, przychodzi do nas strumień wierszy, z których tworzymy dużą wspólną słowniczek. Jeśli słowniczek staje się większy niż wartość ustawienia, zapisujemy obecny słownik na dysku, a pozostałe wiersze gdzieś „obok”, obok indeksów. W rezultacie nigdy nie przeliczymy dużego słownika i nie wystąpią problemy z pamięcią.

Drugie ustawienie nazywa się low_cardinality_use_single_dictionary_for_part. Wyobraź sobie, że w poprzednim schemacie, kiedy wstawialiśmy dane, nasz słownik przepełnił się, a my zapisaliśmy go na dysku. Pojawia się pytanie, a dlaczego teraz nie tworzyć jeszcze jednego takiego samego słownika?

Kiedy on się przepełni, znowu zapiszemy go na dysku i zaczniemy tworzyć trzeci. To ustawienie domyślnie wyłącza taką możliwość.

W rzeczywistości wiele słowników może być przydatnych, jeśli chcemy wstawić jakąś ilość wierszy, ale przypadkowo wstawiliśmy „śmieci”. Powiedzmy, że najpierw wstawiliśmy złe wiersze, a potem dobre. Wtedy słownik podzieli się na wiele małych słowników. Część z nich będzie zawierała „śmieci”, ale ostatnie będą zawierały dobre wiersze. I jeśli przeczytamy, powiedzmy, tylko ostatnią granulę, to wszystko też będzie działało szybko.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Zanim zaczniemy mówić o zaletach LowCardinality, od razu powiem, że raczej nie osiągniemy zmniejszenia danych na dysku (chociaż to może się zdarzyć), ponieważ ClickHouse kompresuje dane. Domyślną opcją jest LZ4. Możliwe jest również kompresowanie za pomocą ZSTD. Ale oba algorytmy już implementują kompresję słownikową, więc nasz zewnętrzny słownik ClickHouse nie pomoże zbytnio.

Aby nie być gołosłownym, wziąłem kilka danych z metryki — String, LowCardinality(String) i Enum — i zapisałem je w różnych typach danych. Powstały trzy kolumny, w których zapisano miliard wierszy. W pierwszej kolumnie, CodePage, jest tylko 62 wartości. Widać, że w przypadku LowCardinality(String) zcompressowaliśmy je lepiej. String jest nieco gorszy, ale to prawdopodobnie z powodu krótkich ciągów, których długości przechowujemy, a zajmują one dużo miejsca i słabo się kompresują.

Jeśli weźmiemy PhoneModel, jest ich 48 tysięcy — już więcej, a różnice między String i LowCardinality(String) są niemal niezauważalne. W przypadku URL również zaoszczędziliśmy tylko 2 GB — myślę, że nie warto na tym polegać.

Ocena wydajności pracy

Optymalizacja ciągów w ClickHouse. Referat Yandexu
Link ze slajdu

Teraz ocenimy wydajność pracy. Aby ją ocenić, użyłem zbioru danych z opisem przejazdów taksówek w Nowym Jorku. On dostępne jest dostępny na GitHubie. Zawiera nieco ponad miliard przejazdów. Zawiera lokalizację, czas rozpoczęcia i zakończenia przejazdu, sposób płatności, liczbę pasażerów oraz rodzaj taksówki — zielona, żółta czy Uber.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Pierwsze zapytanie wykonałem dość prosto — zapytałem, gdzie najczęściej zamawiano taksówki. W tym celu trzeba wziąć lokalizację, z której zamawiano, zrobić na niej GROUP BY i policzyć funkcję count. Oto co generuje ClickHouse.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Aby zmierzyć prędkość przetwarzania zapytania, stworzyłem trzy tabele z danymi, ale użyłem dla naszej lokalizacji startowej trzech różnych typów danych — String, LowCardinality i Enum. LowCardinality i Enum były pięć razy szybsze od String. Enum jest szybszy, ponieważ operuje na liczbach. LowCardinality — ponieważ wdrożono optymalizację GROUP BY.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Jeszcze bardziej skomplikujmy zapytanie — zapytajmy, gdzie znajduje się najpopularniejszy park w Nowym Jorku. Ponownie będziemy to mierzyć według tego, gdzie najczęściej zamawiano taksówki, ale tym razem przefiltrujemy tylko te lokalizacje, które zawierają słowo „park”. Dodamy również funkcję like.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Patrzymy na czas — widzimy, że Enum nagle zaczyna spowalniać. Działa nawet wolniej niż standardowy typ danych String. Dzieje się tak, ponieważ funkcja like wcale nie jest zoptymalizowana dla Enum. Musimy konwertować nasze ciągi z Enum na zwykłe ciągi — wykonujemy więcej pracy. LowCardinality(String) również nie jest zoptymalizowany domyślnie, ale w tym przypadku like działa na słowniku, dlatego zapytanie przyspiesza w porównaniu do String.

Pracując z Enum, napotykamy na większy problem. Jeśli chcemy go zoptymalizować, musimy to zrobić w każdym miejscu w kodzie. Załóżmy, że napisaliśmy nową funkcję — konieczne będzie wymyślenie optymalizacji dla Enum. W przypadku LowCardinality wszystko jest zoptymalizowane domyślnie.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Przyjrzyjmy się ostatniemu zapytaniu, bardziej sztucznemu. Po prostu policzymy funkcję haszującą naszej lokalizacji. Funkcja haszująca to dość wolne zapytanie, trwa długo, więc wszystko spowolni się około trzy razy.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

LowCardinality nadal działa szybciej, choć nie ma tu filtrowania. Dzieje się tak, ponieważ nasze funkcje działają tylko na słowniku. Funkcja obliczania hasza ma jeden argument — może przetwarzać mniej danych i również może zwrócić LowCardinality.

Optymalizacja ciągów w ClickHouse. Referat Yandexu

Nasz globalny plan polega na osiągnięciu prędkości działania nie niższej niż String w każdych okolicznościach i utrzymaniu przyspieszenia. A może kiedyś zastąpimy String LowCardinality, zaktualizujesz ClickHouse, a wszystko będzie działać trochę szybciej.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster