Erlang vs Go vs Scala

0

Natrafiłem ostatnio na mały research dotyczący w.w. języków w kwestii skalowalności i szerokorozumianego performance (latency i throughput)

Link: http://www.dcs.gla.ac.uk/~trinder/papers/sac-18.pdf

TL;DR (Conclusion):

We have investigated programming language characteristics that
support the engineering of multicore web servers. Crucially these
languages must be able to support massive concurrency on multicore
machines with low communication and synchronisation overheads.
We have analysed 12 languages considering computation,
coordination, compilation, and popularity, and selected three representative
high-level languages for detailed analysis: Erlang, Go,
and Scala/Akka (Section 2). We have designed three server benchmarks
that analyse key performance characteristics of the languages,
i.e. inter-process communication latency, process creation time, the
maximum number of supported processes, and throughput (Section
3).
A summary of the recommendations based on this small set of
benchmarks is as follows. For a server where minimising message
latency is crucial, Go and Erlang are the best choice (Figure 3).
Interestingly Akka significantly reduces communication latency in
Scala (Figures 3(a) and 3(b)). Scala/Akka are capable of maintaining
the largest number of dormant processes (∼11M processes in
Figure 4(a)), while Erlang performs the best when processes are
short lived and the goal is to ensure minimal spawn time, e.g. Erlang
takes 58s to spawn 9M processes (Section 4.2). In server applications
where up to 100,000 processes are frequently spawned, Erlang and
Go minimise process creation time and scale smoothly (Figure 4(b)).
Experiments with communicating pairs of processes show that Go
provides the highest throughput independent of the number of cores
and the number of process pairs (Figure 5).
Comparing the performance of complete case study servers implemented
in each language would significantly reinforce these
results, and one possibility is an Instant Messaging (IM) benchmark
[4]. It would also be interesting to study the performance
overheads of providing fault tolerance and of recovering from faults,
another key server capability. Finally we could compare the performance
of server languages on distributed memory architectures, e.g.
a cluster of multicores.

0

Erlang takes 58s to spawn 9M processes (Section 4.2). In server applications where up to 100,000 processes are frequently spawned, Erlang and Go minimise process creation time and scale smoothly

Co to jest proces w Go i która apka w Go odpala często setki tysięcy procesów? W ogóle kto tak robi?

0
Wibowit napisał(a):

Erlang takes 58s to spawn 9M processes (Section 4.2). In server applications where up to 100,000 processes are frequently spawned, Erlang and Go minimise process creation time and scale smoothly

Co to jest proces w Go i która apka w Go odpala często setki tysięcy procesów? W ogóle kto tak robi?

Słuszne spostrzeżenie, z tego co widzę (źródła wrzucili tutaj: https://github.com/bbstk/server-languages-benchmarks/) pod pojęciem "procesu" w Go oni rozumują goroutines - to nie są procesy, a wątki w go. Tych wątków można rzeczywiscie spawnować duże ilości ze względu na to, że to nie są wątki 1:1 w OS. Czyli generalnie, pod pojęciem "processes", w tym dokumencie (wnioskuje po kodzie w Go i Erlangu, Scali nie znam) rozumują wątki.

1

@TurkucPodjadek: w Erlangu to się nazywa procesami, ale nie są to procesy systemowe a Erlangowe właśnie i @Wibowit w Erlangu się je spawnuje na potęgę, przykładowo w Cowboyu (chyba najpopularniejszym serwerze HTTP Erlangowym) każde połączenie przychodzące to jeden niezależny proces, dzięki czemu niezłapany wyjątek nie wyłoży Ci połowy aplikacji a zwyczajnie w świecie ubije jeden proces i po sprawie (bo tak się to najzwyczajniej w Erlangu robi).

Ogólnie kiedyś znalazłem info (teraz nie mogę go ponownie znaleźć), że Akka była inspirowana Erlangiem i autor zwyczajnie chciał przenieść Erlangowe abstrakcje do świata JVMa. Z tego co wiem (nie używałem Akki) podobno wyszło nie najgorzej.

Co do skalowalności to automatyczna klasteryzacja w Erlangu (która jest idiotycznie prosta, bo w większości sprowadza się do net_kernel:connect_node('name@machine') i działa) IMHO wymiata większość tego co jest dostępne. Może sam język nie jest demonem wydajności (JIT bazujący na LLVM jest w trakcie rozwoju), ale dzięki wielu pomysłom (np. GC per proces [wątek jeśli wolicie tą nazwę]) nie traci aż tak bardzo w starciu z obecnymi platformami. Dodatkowo ostatnio pojawia się sporo języków (Elixir, LFE, Joxa), które używają tej samej platformy i oferują "przystępniejszą" składnię niż oryginalna prologowa.

0

każde połączenie przychodzące to jeden niezależny proces, dzięki czemu niezłapany wyjątek nie wyłoży Ci połowy aplikacji a zwyczajnie w świecie ubije jeden proces i po sprawie (bo tak się to najzwyczajniej w Erlangu robi).

W każdym typowym JVMowym frameworku wyjątek przy obsłudze żądania nie wywala aplikacji, więc ubijanie pseudo-procesu w tej kwestii nic nie zmienia.

Słuszne spostrzeżenie, z tego co widzę (źródła wrzucili tutaj: https://github.com/bbstk/server-languages-benchmarks/) pod pojęciem "procesu" w Go oni rozumują goroutines - to nie są procesy, a wątki w go.

Goroutines to nie są wątki - goroutines to bardziej async/ await z dodatkową komunikacją.

Kotlin ma coroutines: https://kotlinlang.org/docs/reference/coroutines.html - prawdopodobnie da się na nich uzyskać coś podobnego do Golangowych goroutines. Jak na razie są eksperymentalne, ale i tak obiecujące.

Wątek natywny potrzebuje zawsze stosu, więc odpalenie milionów natywnych wątków rozsadzi cały RAM i apka leży razem z całym systemem (w sensie OS może i nie leży, ale wpada w panikę i ubija procesy losowo). Wszelakie pseudo procesy czy wątki, które można uruchamiać w ilościach rzędu setki tysięcy naraz są wariantami zielonych wątków: https://en.wikipedia.org/wiki/Green_threads

Ogólnie kiedyś znalazłem info (teraz nie mogę go ponownie znaleźć), że Akka była inspirowana Erlangiem i autor zwyczajnie chciał przenieść Erlangowe abstrakcje do świata JVMa. Z tego co wiem (nie używałem Akki) podobno wyszło nie najgorzej.

Erlang jest przykładem modelu aktorowego, który sprawdził się w praktyce, więc Akka się na nim wzorowała. Jednak od czasów powstania Akka idzie własną drogą i przenoszenie żywcem rozwiązań z Erlanga do Akki niekoniecznie jest optymalne. W świecie Akki jest teraz moda na Reactive Streams na których oparta jest Akka-HTTP. Aktory nie są przeznaczone do samego asynchronicznego przetwarzania wiadomości, a raczej do zarządzania stanem i hierarchią nadzoru. Aktory nie mają backpressure (które jest obecne w Reactive Streams), więc mają tendencję do wypieprzania całej JVMki z powodu braku pamięci do przetwarzania wiadomości przy zbyt dużym obciążeniu. Backpressure z Akka Streams pozwala natomiast na spowolnienie obsługiwania żądań do poziomu, który jest mozliwy w danej chwili - aplikacja przetwarza żądania z pełną prędkością, ale nie zaczyna przetwarzać nowych jeśli aktualnie nie ma wolnych zasobów. Przy przeciążeniu nie ma sensu bezrefleksyjnie zaczynać przetwarzania kolejnego żądania (każde kolejne żądanie przetwarzane w tym samym czasie co inne zwiększa zapotrzebowanie na RAM), a zamiast tego lepiej skupić się na przetworzeniu do końca już rozpoczętych żądań.

0

@Wibowit: ale to jest od dawna stosowana praktyka, czy to poprzez supervisor z modelem simple_one_for_one (lub Elixirowy wrapper na to w postaci DynamicSupervisor od wersji 1.6) czy poprzez biblioteki jak poolboy, które używają wcześniej wymienionego mechanizmu. IIRC Cowboy również używa jakiegoś mechanizmu do back pressure (czy to poolboy czy coś innego nie pamiętam). Poza tym jakiś czas temu pojawiła się biblioteka GenStage dla Elixira, która oferuje podobne rozwiązanie. Wygląda również na to, że samo wysyłanie wiadomości jest obarczone mechanizmem backpressure (a przynajmniej było), więc to nie jest tak, że Erlangowy system "jest durny". Jednak te 9 lat różnicy między Erlangiem (1986) a Javą (1995) dają o sobie znać w takich momentach.

0

Wygląda również na to, że samo wysyłanie wiadomości jest obarczone mechanizmem backpressure (a przynajmniej było), więc to nie jest tak, że Erlangowy system "jest durny".

Właśnie z tej podlinkowanej wiadomości wynika, że jest trochę durny. Po pierwsze działa probabilitstycznie:

sending messages to load queues is very expensive and will lead the scheduler to context switch to another process more often

'more often' nie brzmi jak jednoznaczna gwarancja. Po drugie w przypadku nowego aktora per żądanie działa odwrotnie niż powinien:

Sending messages to processes with zero messages in the queue is free in terms of reductions

Nowy aktor będzie miał pustą kolejkę wiadomości, więc priorytetem będzie zaczynanie obsługi nowych żądań zamiast kończenie już zaczętych. To nie jest dobra strategia przy przeciążeniu systemu. No chyba, że czegoś nie zrozumiałem :)

Reactive streams działają zupełnie inaczej niż aktory jeśli spojrzeć na to z której strony inicjowane jest przetwarzanie wiadomości. W mechanizmie aktorów przetwarzanie nowych wiadomości odbywa się na skutek wrzucenia ich do początkowego aktora w łańcuchu przetwarzania (proste i logiczne). W mechanizmie reactive streams przetwarzanie nowych wiadomości odbywa się na skutek zasygnalizowania przez końcówkę strumienia gotowości otrzymania nowych wiadomości. Dokładnie to jest to rozszerzone na cały strumień - poprzedni element w strumieniu jest wyłączony dopóki następny element nie zasygnalizuje mu, że jest gotowy odebrać nowe elementy. Do tego dochodzą bufory pomiędzy elementami strumienia, by zminimalizować narzut na komunikację - bufory pozwalają na ukrycie opóźnień związanych z sygnalizacją gotowości na odebranie kolejnych porcji danych.

Backpressure w Akka Streams działa bardzo podobnie do flow-control w TCP/IP: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#/media/File:Tcp.svg
Można obejrzeć w akcji integrację Akka Streams z flow-control w TCP/IP tutaj:
Trzeba wziąć jeszcze pod uwagę, że Akka Streams są nieblokujące, więc to czekanie na zwolnienie bądź napełnienie się bufora TCP/ IP tak naprawdę nie blokuje żadnego wątku.
Akka Streams mają integrację nie tylko z TCP i HTTP(S), ale także z plikami ( https://doc.akka.io/docs/akka/2.5.5/scala/stream/stream-io.html#streaming-file-io ) i bazami danych ( http://slick.lightbend.com/ )

0
Wibowit napisał(a):

każde połączenie przychodzące to jeden niezależny proces, dzięki czemu niezłapany wyjątek nie wyłoży Ci połowy aplikacji a zwyczajnie w świecie ubije jeden proces i po sprawie (bo tak się to najzwyczajniej w Erlangu robi).

W każdym typowym JVMowym frameworku wyjątek przy obsłudze żądania nie wywala aplikacji, więc ubijanie pseudo-procesu w tej kwestii nic nie zmienia.

Słuszne spostrzeżenie, z tego co widzę (źródła wrzucili tutaj: https://github.com/bbstk/server-languages-benchmarks/) pod pojęciem "procesu" w Go oni rozumują goroutines - to nie są procesy, a wątki w go.

Goroutines to nie są wątki - goroutines to bardziej async/ await z dodatkową komunikacją.

Goroutines to takie wątki, ale w Go, ale nie są one oczywiście tożsame z wątkami w systemie operacyjnym (zdaje się o tym wspomniałem)
Tutaj jest wyjaśnione jak to działa pod spodem

0

Goroutines to takie wątki, ale w Go, ale nie są one oczywiście tożsame z wątkami w systemie operacyjnym

No i rozmywa się nam pojęcie wątku :] Z samego artykułu który przytoczyłeś wynika iż scheduler w Go opiera się o cooperative multitasking, a nie o preemptive multitasking. Go nie wstrzyma działającej gorutyny o ile ta gorutyna nie wejdzie w jakąś metodę, która współpracuje ze schedulerem Go. Tymczasem wątki mogą być wywłaszczone w dowolnym momencie (preemption). Formą cooperative multitasking jest np: https://en.wikipedia.org/wiki/Fiber_(computer_science)

Lepiej skupić się na konkretnych problemach i ich rozwiązaniach, a nie na nazywaniu abstrakcji, które ciężko skategoryzować.

0
Wibowit napisał(a):

Wygląda również na to, że samo wysyłanie wiadomości jest obarczone mechanizmem backpressure (a przynajmniej było), więc to nie jest tak, że Erlangowy system "jest durny".

Właśnie z tej podlinkowanej wiadomości wynika, że jest trochę durny. Po pierwsze działa probabilitstycznie:

sending messages to load queues is very expensive and will lead the scheduler to context switch to another process more often

'more often' nie brzmi jak jednoznaczna gwarancja. Po drugie w przypadku nowego aktora per żądanie działa odwrotnie niż powinien:

Sending messages to processes with zero messages in the queue is free in terms of reductions

Nowy aktor będzie miał pustą kolejkę wiadomości, więc priorytetem będzie zaczynanie obsługi nowych żądań zamiast kończenie już zaczętych. To nie jest dobra strategia przy przeciążeniu systemu. No chyba, że czegoś nie zrozumiałem :)

Każdy proces ma przypisane standardowo tyle samo redukcji (czasu procesora), więc źle to zrozumiałeś. To wysyłający zadania będzie miał zmniejszaną ilość redukcji, nie odbierający, więc dalej zadania będą przetwarzane tak jak było to zaplanowane. Domyślnie IIRC jest to 2000 redukcji zanim zostanie przełączony kontekst. Z racji, że w Erlangu każde zadanie z reguły to osobny proces, to w ten sposób "głodzisz" wysyłającego, a nie odbiorców.

W mechanizmie reactive streams przetwarzanie nowych wiadomości odbywa się na skutek zasygnalizowania przez końcówkę strumienia gotowości otrzymania nowych wiadomości. Dokładnie to jest to rozszerzone na cały strumień - poprzedni element w strumieniu jest wyłączony dopóki następny element nie zasygnalizuje mu, że jest gotowy odebrać nowe elementy.

Dokładnie tak działa gen_stage, gdzie to odbiorca ogranicza ile wiadomości chce otrzymać.

Trzeba wziąć jeszcze pod uwagę, że Akka Streams są nieblokujące, więc to czekanie na zwolnienie bądź napełnienie się bufora TCP/ IP tak naprawdę nie blokuje żadnego wątku.

Tak samo wysyłanie wiadomości w Erlangu. Wysłanie wiadomości nie blokuje nikogo, co najwyżej może spowodować zmianę kontekstu zaraz po wysłaniu wiadomości.

Akka Streams mają integrację nie tylko z TCP i HTTP(S), ale także z plikami ( https://doc.akka.io/docs/akka/2.5.5/scala/stream/stream-io.html#streaming-file-io ) i bazami danych ( http://slick.lightbend.com/ )

W Eliksirze masz Flow, które oferuje wsparcie dla każdego Enumerable (na ten przykład Stream), więc z automatu masz wsparcie dla plików czy DB. TCP i HTTP są z reguły obsługiwane trochę inaczej, ale z racji, że Erlang jest od początku zaprojektowany jako sRTC to ma naprawdę wiele mechanizmów, które wspierają te założenia, gdzie w Javie to programista musi bardzo często dbać o zachowanie założeń.

0
Wibowit napisał(a):

Goroutines to takie wątki, ale w Go, ale nie są one oczywiście tożsame z wątkami w systemie operacyjnym

No i rozmywa się nam pojęcie wątku :]
Z samego artykułu który przytoczyłeś wynika iż scheduler w Go opiera się o cooperative multitasking, a nie o preemptive multitasking. [/quote]

I teraz pytanie: jakie to w tym kontekście będzie miało znaczenie?

Go nie wstrzyma działającej gorutyny o ile ta gorutyna nie wejdzie w jakąś metodę, która współpracuje ze schedulerem Go.

Mogłbyś tutaj wyjaśnić, co masz dokładnie na myśli?

Tymczasem wątki mogą być wywłaszczone w dowolnym momencie (preemption). Formą cooperative multitasking jest np: https://en.wikipedia.org/wiki/Fiber_(computer_science),

Które wątki, w jakim systemie, przy jakim cpu schedulerze? Jak określamy się dokładnie to dokładnie.

Lepiej skupić się na konkretnych problemach i ich rozwiązaniach, a nie na nazywaniu abstrakcji, które ciężko skategoryzować.

Właśnie. ;-)

PS Ja wątek rozumuje prosto - zdolność do przeprowadzania różnych obliczeń dokładnie w tym samym momencie przez ten sam program, w takim sposób, że

  • jeden wątek programy jest niezależny od drugiego (np. jeden nie czeka aż drugi coś skończy)
  • ze strony OSa nie jest konieczny żaden cs do obsłużenia kolejnego wątku, jeśli logical core jest akurat "wolny"
0

Każdy proces ma przypisane standardowo tyle samo redukcji (czasu procesora), więc źle to zrozumiałeś. To wysyłający zadania będzie miał zmniejszaną ilość redukcji, nie odbierający, więc dalej zadania będą przetwarzane tak jak było to zaplanowane. Domyślnie IIRC jest to 2000 redukcji zanim zostanie przełączony kontekst. Z racji, że w Erlangu każde zadanie z reguły to osobny proces, to w ten sposób "głodzisz" wysyłającego, a nie odbiorców.

Jeśli to tak działa to przy tworzeniu nowego aktora dla każdego żądania tenże aktor będzie miał zapas redukcji, więc od razu zostanie odpalony i będzie wczytywał dane od klienta. Dopiero potem przytka się na przepychaniu tych danych dalej. Ale to za późno, bo już zapchał RAM danymi od klienta. Tak czy nie?

PS Ja wątek rozumuje prosto - zdolność do przeprowadzania różnych obliczeń dokładnie w tym samym momencie przez ten sam program, w takim sposób, że
jeden wątek programy jest niezależny od drugiego (np. jeden nie czeka aż drugi coś skończy)
ze strony OSa nie jest konieczny żaden cs do obsłużenia kolejnego wątku, jeśli logical core jest akurat "wolny"

Goroutines vel coroutines opierają się właśnie na nieustannym wstrzymywaniu, nieustannym cyklu suspend + resume. Jeśli jedna gorutyna chce dostać dane od innej to przy próbie odczytu z kanału jest suspend i potem scheduler Go decyduje co dalej zrobić. Dopóki kanał jest pusty to resume się oczywiście nie zrobi.

Na stronie z dokumentacją dla Go jest coś takiego - https://golang.org/doc/faq#goroutines

Why goroutines instead of threads?
Goroutines are part of making concurrency easy to use. The idea, which has been around for a while, is to multiplex independently executing functions—coroutines—onto a set of threads. When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked. The programmer sees none of this, which is the point. The result, which we call goroutines, can be very cheap: they have little overhead beyond the memory for the stack, which is just a few kilobytes.

Jak widać jest wyraźne rozróżnienie na wątki i gorutyny.

1
TurkucPodjadek napisał(a):
Wibowit napisał(a):

Go nie wstrzyma działającej gorutyny o ile ta gorutyna nie wejdzie w jakąś metodę, która współpracuje ze schedulerem Go.

Mogłbyś tutaj wyjaśnić, co masz dokładnie na myśli?

Taki prosty przykład https://stackoverflow.com/questions/25073815/golang-goroutine-infinite-loop .

0
Wibowit napisał(a):

Każdy proces ma przypisane standardowo tyle samo redukcji (czasu procesora), więc źle to zrozumiałeś. To wysyłający zadania będzie miał zmniejszaną ilość redukcji, nie odbierający, więc dalej zadania będą przetwarzane tak jak było to zaplanowane. Domyślnie IIRC jest to 2000 redukcji zanim zostanie przełączony kontekst. Z racji, że w Erlangu każde zadanie z reguły to osobny proces, to w ten sposób "głodzisz" wysyłającego, a nie odbiorców.

Jeśli to tak działa to przy tworzeniu nowego aktora dla każdego żądania tenże aktor będzie miał zapas redukcji, więc od razu zostanie odpalony i będzie wczytywał dane od klienta. Dopiero potem przytka się na przepychaniu tych danych dalej. Ale to za późno, bo już zapchał RAM danymi od klienta. Tak czy nie?

Nie do końca, bo jak odpalasz spawn/1 to taki proces dopiero ląduje w kolejce schedulera, nie oznacza, że jest od razu odpalany, więc jeśli rodzic po odpaleniu procesu ma jeszcze jakieś redukcje to jeszcze będzie się wykonywał. Kolejną rzeczą, jest że w Erlangu również jest event loop, który działa w osobnym wątku i cała komunikacja między światem zewnętrznym a procesami Erlanga również odbywa się na zasadzie wymiany wiadomości, więc proces będzie czekał aż otrzyma wiadomość z event loopa z danymi. Więc "pod spodem" działa to podobnie do goroutines, ale daje Ci wyższy poziom abstrakcji i ukrywa implementacyjne szczegóły. Kolejną rzeczą jest fakt, że procesy z reguły żyją krótko (generational hypothesis), więc GC może często zwyczajnie wyrzucić całość pamięci jaka była przydzielona danemu procesowi bez przejmowania się (https://www.erlang-solutions.com/blog/erlang-garbage-collector.html). Więc to nie Agent bezpośrednio czyta dane, a VMka i potem przekazuje te dane do agenta jak coś się pojawi.

0

Hmm, dziwnie to opisałeś. Podam przykład:

  • robię sobie pseudo-chmurę plikową
  • sto tysięcy ludków chce mi wysłać pliki naraz
  • moja chmura przycina się na zapisie danych
  • Erlang dla każdego żądania tworzy nowego aktora i to mu idzie sprawnie
  • aktor do zapisu danych jest przyblokowany
  • nowe aktory obsługujące żądania jednak zaczynają od wczytywania plików od klientów, co zapycha RAM
  • jak już aktor od żądania wczyta trochę danych do RAMu to chce to zapisać z użyciem aktora do zapisu ale w tym momencie się przycina (bo aktor do zapisu jest przyblokowany)
  • dużo czasu procesora jest wolne więc Erlang tworzy nowe aktory do obsługi żądań, które zapychają jeszcze więcej RAMu i zaczyna się panika
  • co robić?

Zaznaczam, że nie wiem jak Erlangowe mechanizmy działają, więc być może przykład nie ma pokrycia w rzeczywistości. Jednak z tych opisów co tutaj podałeś wynika, że taki scenariusz jest możliwy.

0

@Wibowit: ogólnie całe IO przechodzi przez epoll (w nowych wersjach), więc cały system działa w drugą stronę, czyli jak chcesz pisać do pliku to wysyłasz wiadomość do procesu i otrzymujesz karę jeśli jest zajęty. Więc powoli to wszystko idzie wstecz. Więc system da radę (bo epoll będzie stopował procesy).

Alternatywnie jak masz naprawdę ograniczone zasoby, np. ograniczoną ilość połączeń z DB to można użyć poolboya (tego np. używa Cowboy do ograniczenia ilości połączeń przychodzących i Ecto do posiadania puli ciągłych połączeń z DB) lub jeśli jest to przetwarzanie danych to można użyć Elixirowej biblioteki GenStage, która trochę przypomina reaktywne programowanie, gdzie to konsument określa ile jest w stanie na raz zrobić (oraz wrapper na to Flow, który oferuje przyjaźniejszy interfejs do części zadań).

0

Hmm, no i okazuje się, że Erlang automagicznie nie zabezpiecza nas przed wysadzeniem VMki przy przeciążeniu serwera tylko musimy sobie podokładać jakieś biblioteki i używać ich zgodnie z przeznaczeniem. Tak to jest na każdej innej platformie, różnica jest tylko w składni i co za tym idzie - wygodzie.

0

Oczywiście, że nie zabezpiecza, ale czyni napisanie kodu, który mógłby zabić VMkę zdecydowanie trudniejszym. Tak to będzie gdy masz platformę od początku zaprojektowaną na to by była soft real-time. Ale zabezpieczyć w 100% się nigdy nie da.

0

Wybieram najnowszy Swift z 2014 roku.

1 użytkowników online, w tym zalogowanych: 0, gości: 1