Optymalizacja Apache Spark

Marcin Orliński
Marcin Orliński
May 21, 2025
10 min read
Loading the Elevenlabs Text to Speech AudioNative Player...

Optymalizacja Apache Spark

Apache Spark to potężne narzędzie do przetwarzania danych, które pozwala na rzędy wielkości poprawę czasów wykonania w porównaniu z algorytmami MapReduce Hadoop lub przetwarzaniem pojedynczego węzła. Czasami jednak, czy to ze względu na stare nawyki, które programiści przenoszą z systemów przetwarzania proceduralnego, czy po prostu nie wiedząc lepszego, Spark może zmagać się nawet z umiarkowaną ilością danych i prostymi połączeniami. Ale zanim zaczniesz dodawać więcej węzłów do klastra obliczeniowego lub wyposażać te, które już tam są w dodatkową pamięć RAM (coś, co nie każdy programista może zrobić), oto 5 aspektów interfejsu API Spark, które warto rozważyć, aby upewnić się, że wykorzystujesz framework w sposób, w jaki ma być używany:

 

Dane w spoczynku

Zarządzanie strategią przechowywania danych wejściowych/wyjściowych/tymczasowych jest kluczem nie tylko do optymalizacji własnej aplikacji, ale także każdej innej aplikacji, która wykorzystuje te same dane do ich przetwarzania. Strategia ta polega m.in. na wyborze odpowiedniego systemu pamięci masowej, formatów plików i partycjonowaniu danych.

Jako programista prawdopodobnie nie masz zbyt dużego wpływu na rodzaj systemu pamięci masowej, który musisz obsługiwać za pomocą swojej aplikacji. Ważne jest jednak, aby pamiętać, jak działa dana pamięć masowa w konkretnym scenariuszu, tj. czy jest to szybkość przepustowości, koszt modyfikacji danych itp.

 

Formaty plików obsługiwane przez Apache Spark

Ponieważ Spark obsługuje szeroką gamę formatów danych, łatwo jest zaangażować się w coś, co jest nieoptymalne tylko dlatego, że jesteśmy w pewnym stopniu zaznajomieni z wybranym typem i lekceważymy potencjalne oszczędności wydajności innych, często dostosowane do komputerów rozproszonych.

Zawsze powinieneś preferować binarne, kolumnowe formaty danych (np. Parkiet) zamiast alternatywnych (np. CSV). Nie tylko są one szybsze do załadowania, ale także umożliwi Ci przejście od obsługi przypadków krawędzi (postaci ucieczki, nowe linie itp.) na inne, bardziej produktywne zadania.

 

Typy plików z możliwością dzielenia

Łatwo przeoczyć, że niektórych formatów plików nie można łatwo podzielić na kawałki, co uniemożliwia korzystanie z rozproszonego charakteru frameworka Spark. Pliki takie jak ZIP lub JSON muszą być odczytane w całości, zanim będą mogły zostać zinterpretowane.

Upewnij się, że pliki, które ładujesz do Spark, można podzielić w tym sensie.

 

Partycjonowanie tabeli

Wiedza o tym, w jaki sposób Twoje tabele będą najczęściej odczytywane przez użytkowników końcowych (i samodzielnie), może pozwolić na inteligentne podzielenie surowych danych tabeli na partycję, podzieloną przez klucz, na który zapytasz. Partycjonowanie, w niektórych scenariuszach, pozwala ograniczyć całkowitą ilość danych załadowanych do klastra Spark, co może mieć dramatyczny wpływ na szybkość wykonywania kodu.

 

Przekrzywienie danych w Apache Spark

„Skośność” to termin statystyczny używany do opisania asymetrii danych w danym rozkładzie. W Big Data nazywamy dane „wypaczonymi”, gdy nie są równomiernie rozłożone, a zatem trudno je równomiernie równomiernie zrównoleglić. Ma to szczególnie wpływ na operacje Dołącz w Twoim kodzie.

Przykładem może być tabela, w której jeden konkretny klucz jest przypisany do nieproporcjonalnej liczby wierszy. Podczas wykonywania połączenia na tym klawiszu, Spark wyśle wszystkie rekordy z tym samym kluczem połączenia do tej samej partycji. W ten sposób nasz „popularny” klucz będzie zmuszony do przetworzenia w ramach jednego zadania, co często sugeruje z tego powodu poważne koszty przesuwania.

Możesz monitorować te sytuacje, zwracając uwagę na interfejs użytkownika Spark i to, co dzieje się pod maską wykonania. Jeśli w swojej pracy zidentyfikujesz zadanie, które trwa znacznie dłużej niż inne, z dużymi wartościami dla rozmiaru odczytu/rekordów Shuffle, możesz bezpiecznie założyć, że jeden węzeł wykonuje większość ciężkich zadań podczas przetwarzania. Jest to najprawdopodobniej spowodowane Skew danych. Jeśli chcesz wiedzieć, w jaki sposób może to pomóc Twojej firmie, odwiedź naszą Doradztwo w zakresie inżynierii danych strona.

Sposoby złagodzenia tego:

 

  • Używanie połączeń rozgłoszeniowych zamiast Sortuj połączenia scalania

Jeśli łączysz dwie tabele, z których jedna jest znacznie mniejsza od drugiej (wystarczająco mała, aby zmieściła się w pamięci wykonawcy), możesz chcieć „zasugerować” Spark, aby używał Broadcast Join zamiast Sort Merge Join. Może to spowodować dramatyczną poprawę czasów łączenia wypaczonych danych. Więcej na temat „podpowiedzi” dołączenia: https://spark.apache.org/docs/latest/sql-ref-syntax-qry-select-hints.html#join-hints

Należy pamiętać, że Spark planuje połączenia Broadcast automatycznie, pod warunkiem, że jedna ze stron połączenia jest mniejsza niż próg łączenia transmisji (właściwość SPARK.sql.AutoBroadcastJoinThreshold).

 

  • Solenie kluczy

Ideą solenia kluczy jest wprowadzenie dodatkowej, sztucznej zmienności do twojej kluczowej wartości. Można to osiągnąć poprzez wprowadzenie równomiernie przypisanych „wiader” do głównej tabeli danych, rozbicie stołu po prawej stronie z tym samym zakresem wiader (kopiowanie wartości innych niż numer wiadra) i wykonanie połączenia nad kluczem AND łyżką. Pozwoli to na podzielenie pojedynczej największej wartości klucza na liczbę partycji równą liczbie wiader, co z kolei rozłoży dane bardziej równomiernie w węzłach.

 

Buforowanie danych w Spark

Główną zaletą Sparka nad podejściem Map-Reduce firmy Hadoop (ideologicznym poprzednikiem Sparka) jest możliwość wykorzystania pamięci RAM do obliczeń.

Jednak Spark wykorzystuje również Lazy Evaluation do swojego planu wykonania, co oznacza odkładanie uzyskania jakichkolwiek wyników pośrednich, aż będzie to absolutnie konieczne. Oznacza to również, że te pośrednie wyniki nie zostaną zapisane w pamięci, ponieważ wszystko, o co dba Spark, jest ostatnim krokiem planu. Działa to dobrze, jeśli masz prosty pociąg przekształceń, w którym każdy krok przyjmuje jako wejście wyjścia poprzedniego.

Jednakże, gdy obiekt jest odczytany więcej niż raz, podejście to prowadzi do ponownego obliczania go za każdym razem po odczytaniu. Aby tego uniknąć, można określić, które ramki danych powinny być buforowane w pamięci w danym momencie za pomocą metody wbudowanej pamięci podręcznej ().

Buforowanie jest samo w sobie leniwą operacją, więc będzie wykonywane dopiero po bezpośrednim dostępie do danych.

 

Reparticjonowanie za pomocą Apache Spark

Ważne jest, aby pamiętać, jak techniki manipulacji wpływają na datę i jak jest ona przechowywana w węzłach.

Na przykład podczas filtrowania bardzo dużego zestawu danych i kończąc na stosunkowo znacznie mniejszym, spełniającym wyrażenie filtru, niektóre partycje mogą być puste w niektórych węzłach roboczych. Prowadzi to do nierównomiernego rozłożenia danych w sieci, co zmniejsza poziom równoległości, jaki można osiągnąć podczas przetwarzania nowych, przefiltrowanych danych.

Repartition () i coalesce () to wbudowane metody, które zostały zaprojektowane w celu wymuszenia losowania, prowadząc do równomiernego rozmieszczenia danych między węzłami roboczymi.

Repartition () odtwarza partycje od podstaw i wykonuje pełne tasowanie. Coalesce () łączy partycje razem, co powoduje mniejszy ruch danych w porównaniu z repartition () (części danych dla scalonych partycji są już na miejscu), jednak może prowadzić do nierównych partycji, co z kolei prowadzi do przekrzywienia danych.

 

UDF i zmienne transmisji

Funkcje zdefiniowane przez użytkownika (UDF) należy unikać z punktu widzenia wydajności — wymuszają one reprezentację danych jako obiektów w JVM. Jest to koszt, który płacisz za możliwość reprezentowania swoich transformacji w języku programowania wysokiego poziomu. Strukturalny interfejs API Spark jest wolny od tego ograniczenia i jest ogólnie zalecanym sposobem wykonywania manipulacji danymi.

Jeśli jednak chcesz użyć UDF w swoim kodzie, rozważ użycie zmiennych transmisji. Jeśli masz dane, które będą używane w wielu połączeniach UDF, transmisja będzie oznaczać, że we wszystkich węzłach znajduje się jedna kopia danych tylko do odczytu i nie musi być ponownie wysyłana przy każdym wywołaniu UDF. Nadawanie może być przydatne również poza przypadkiem użycia UDF, tj. podczas korzystania z tabel wyszukiwania.

Odwiedź nasz blog, aby uzyskać bardziej szczegółowe artykuły dotyczące inżynierii danych:

Share this post
Data Engineering
Marcin Orliński
MORE POSTS BY THIS AUTHOR
Marcin Orliński

Curious how we can support your business?

TALK TO US