26

Połączyłem prosty przykład próbujący udowodnić współbieżne żądania w Railsach przy użyciu podstawowego przykładu. Zauważ, że używam MRI Ruby2 i Rails 4.2.Równoczesne żądania z MRI Ruby

def api_call 
    sleep(10) 
    render :json => "done" 
    end 

I wtedy pójść do 4 różnych kart w Chrome na moim Mac (I7/4 rdzenia) i zobaczyć, czy się uruchomić szeregowo lub równolegle (naprawdę jednoczesnego który jest blisko, ale nie to samo). tzn., http://localhost:3000/api_call

Nie mogę uruchomić tego za pomocą Puma, Thin lub Unicorn. Żądania przychodzą kolejno w serii. Pierwsza zakładka po 10 sekundach, sekunda po 20 (ponieważ musiała poczekać na pierwszą, aby zakończyć), trzecia po tej ...

Z tego, co przeczytałem, uważam poniższe za prawdziwe (proszę poprawić mnie) i były moje wyniki:

  • Jednorożec jest wieloprocesowy i mój przykład powinien zadziałać (po zdefiniowaniu liczby pracowników w pliku konfiguracyjnym unicorn.rb), ale tak się nie stało. Widzę 4 pracowników zaczynających, ale wszystko działa w seriach. Używam gem jednorożec szyn, począwszy od szyny z Jednorożca -c config/unicorn.rb, aw moim unicorn.rb mam:

- unicorn.rb

worker_processes 4 
preload_app true 
timeout 30 
listen 3000 
after_fork do |server, worker| 
    ActiveRecord::Base.establish_connection 
end 
  • Cienki i Puma są wielowątkowe (chociaż Puma ma co najmniej tryb "clustered", w którym można uruchamiać pracowników z parametrem -w) i nie powinien działać tak czy inaczej (w trybie wielowątkowym) z MRI Ruby2.0, ponieważ "istnieje globalna blokada interpretera (GIL), który zapewnia, że ​​tylko jeden wątek może być uruchomiony na raz ".

Więc

  • Czy muszę mieć ważny przykład (lub korzysta spać tylko źle)?
  • Czy powyższe stwierdzenia dotyczące wieloprocesowości i wielowątkowości (w odniesieniu do MRI Rails 2) są prawidłowe?
  • Jakieś pomysły na to, dlaczego nie mogę pracować z Unicornem (lub z jakimkolwiek serwerem)?

Jest bardzo similar question to mine, ale nie mogę uzyskać działa jako odpowiedź i nie odpowiada na wszystkie moje pytania dotyczące jednoczesnych żądań przy użyciu MRI Ruby.

Github projekt: https://github.com/afrankel/limitedBandwidth (uwaga: projekt patrzy na więcej niż to pytanie multi-process/wątków na serwerze)

+0

Nie mogę tego odtworzyć z jednorożcem - wszystko działa zgodnie z oczekiwaniami. – Anthony

+0

@Anthony - Edytowałem swój wpis z konfiguracją jednorożca. Czy widzisz coś nie tak z tym, co mam na liście? –

+0

Aktualizacja: Właściwie to udało mi się sprawić, że działa. Musiałem zwiększyć sen do 60 sekund (mój limit czasu do 180 w konfiguracji jednorożca), a następnie wynik karty 1 powrócił w ciągu 1 minuty, 2,3 i 4 wrócił w 1,3 minuty. Być może jest więc pewne opóźnienie w znalezieniu nowego pracownika. Byłbym zainteresowany, gdyby ktoś mógł wyjaśnić mój wynik, a także potwierdzić moje pytania powyżej. –

Odpowiedz

22

Zapraszam do zapoznania się z szeregu Jesse Storimer na Nobody understands the GIL To może pomóc zrozumieć lepiej niektóre wewnętrzne MRI.

Znalazłem również Pragmatic Concurrency with Ruby, który jest interesujący. Ma kilka przykładów równoczesnego testowania.

EDIT: Dodatkowo mogę polecić artykuł Removing config.threadsafe! nie mogą być istotne dla Rails 4, ale to wyjaśnia opcje konfiguracyjne, z których jeden można użyć, aby umożliwić współbieżności.


Porozmawiajmy o odpowiedzi na twoje pytanie.

Możesz mieć kilka wątków (używając MRI), nawet z Puma. GIL zapewnia, że ​​tylko jeden wątek jest aktywny w tym samym czasie, to jest ograniczenie, które deweloperzy nazywają restrykcyjnym (z powodu braku rzeczywistego wykonywania równoległego). Pamiętaj, że GIL nie gwarantuje bezpieczeństwa nici. Nie oznacza to, że inne wątki nie działają, czekają na swoją kolej. Mogą się przeplatać (artykuły mogą pomóc lepiej zrozumieć).

Pozwól, że wyjaśnię kilka terminów: proces roboczy, wątek. Proces jest uruchamiany w osobnym obszarze pamięci i może obsługiwać wiele wątków. Wątki tego samego procesu są uruchamiane w obszarze pamięci współużytkowanej, który jest procesem tego procesu. W przypadku wątków w tym kontekście mamy na myśli wątki Ruby, a nie wątki procesora.

Jeśli chodzi o konfigurację twojego pytania i repozytorium GitHub, które podzieliłeś, myślę, że odpowiednią konfiguracją (użyłem Puma) jest skonfigurowanie 4 pracowników i od 1 do 40 wątków. Chodzi o to, że jeden pracownik obsługuje jedną kartę. Każda karta wysyła do 10 żądań.

Więc zaczynajmy:

pracuję na Ubuntu na maszynie wirtualnej. Najpierw włączono 4 rdzenie w ustawieniach mojej maszyny wirtualnej (i niektóre inne ustawienia, które uważałem za pomocne). Mogę to zweryfikować na moim komputerze. Więc poszedłem z tym.

Linux command --> lscpu 
Architecture:   x86_64 
CPU op-mode(s):  32-bit, 64-bit 
Byte Order:   Little Endian 
CPU(s):    4 
On-line CPU(s) list: 0-3 
Thread(s) per core: 1 
Core(s) per socket: 4 
Socket(s):    1 
NUMA node(s):   1 
Vendor ID:    GenuineIntel 
CPU family:   6 
Model:     69 
Stepping:    1 
CPU MHz:    2306.141 
BogoMIPS:    4612.28 
L1d cache:    32K 
L1d cache:    32K 
L2d cache:    6144K 
NUMA node0 CPU(s):  0-3 

Użyłem twojego wspólnego projektu GitHub i nieco go zmodyfikowałem. Stworzyłem plik konfiguracyjny o nazwie Puma puma.rb (umieścić go w katalogu config) o następującej treści:

workers Integer(ENV['WEB_CONCURRENCY'] || 1) 
threads_count = Integer(ENV['MAX_THREADS'] || 1) 
threads 1, threads_count 

preload_app! 

rackup  DefaultRackup 
port  ENV['PORT']  || 3000 
environment ENV['RACK_ENV'] || 'development' 

on_worker_boot do 
    # Worker specific setup for Rails 4.1+ 
    # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot 
    #ActiveRecord::Base.establish_connection 
end 

Domyślnie Puma rozpoczyna się 1 pracownika i 1 wątku. Zmiennych środowiskowych można używać do modyfikowania tych parametrów. Zrobiłem tak:

export MAX_THREADS=40 
export WEB_CONCURRENCY=4 

Aby rozpocząć Puma z tej konfiguracji Wpisałem

bundle exec puma -C config/puma.rb 

w Rails app katalogu.

Otworzyłem przeglądarkę z czterema zakładkami, aby wywołać adres URL aplikacji.

Pierwsza prośba rozpoczęła się około 15:45:05, a ostatnia prośba miała miejsce około 15:49:44. To jest czas, który upłynął od 4 minut do 39 sekund. Możesz również zobaczyć identyfikator żądania w porządku nie sortowanym w pliku dziennika. (Zobacz poniżej)

Każde wywołanie API w projekcie GitHub jest w trybie uśpienia przez 15 sekund. Mamy cztery 4 zakładki, każda z 10 zaproszeniami API. To sprawia, że ​​maksymalny czas trwania wynosi 600 sekund, czyli 10 minut (w trybie ciągłym).

Idealny wynik teoretyczny będzie równoczesny i czas, który upłynął nie dalej niż 15 sekund, ale nie spodziewałem się tego w ogóle. Nie byłem pewien, czego się spodziewać w wyniku dokładnie, ale byłem nadal pozytywnie zaskoczony (biorąc pod uwagę, że uruchomiłem maszynę wirtualną i MRI jest ograniczany przez GIL i kilka innych czynników). Czas, jaki upłynął w tym teście, był mniejszy niż połowa maksymalnego czasu (w trybie szeregowym), dlatego wynik został zmniejszony do mniej niż połowy.

EDIT czytam dalej o Rack :: Blokada który owija się wokół każdego żądania mutex (trzecia artykułu powyżej). Zauważyłem, że opcja config.allow_concurrency = true oszczędza czas. Małe zastrzeżenie: polegało na zwiększeniu puli połączeń (chociaż żądanie nie zawierało zapytania, konieczne było odpowiednie ustawienie bazy danych); liczba maksymalnych wątków wynosi . Dobra wartość domyślna. 40 w tym przypadku.

Testowałem aplikację z jRuby, a rzeczywisty czas minął 2 minuty, z allow_concurrency = true.

Testowałem aplikację z MRI, a rzeczywisty czas minął 1min47s, z allow_concurrency = true. To była dla mnie wielka niespodzianka. To naprawdę mnie zaskoczyło, ponieważ spodziewałem się, że MRI będzie wolniejsze niż JRuby. Nie było. To sprawia, że ​​kwestionuję szeroko rozpowszechnioną dyskusję na temat różnic prędkości pomiędzy MRI i JRuby.

Oglądanie odpowiedzi na różnych kartach jest teraz "bardziej losowe". Zdarza się, że zakładka 3 lub 4 kończy się przed pierwszą kartą, o którą prosiłem wcześniej.

Myślę, że ponieważ nie masz warunków wyścigu, test wydaje się być OK. Nie jestem jednak pewny, czy aplikacja będzie miała szerokie konsekwencje, jeśli ustawisz config.allow_concurrency = true w aplikacji działającej w świecie rzeczywistym.

Zapraszam do zapoznania się z nimi i poinformowania mnie o ewentualnych opiniach. Nadal mam klona na moim komputerze. Daj mi znać jeśli jesteś zainteresowany.

Aby odpowiedzieć na pytania w kolejności:

  • myślę, że przykład jest ważny przez wynik. Jednak w przypadku współbieżności lepiej jest testować przy użyciu współdzielonych zasobów (jak na przykład w drugim artykule).
  • W odniesieniu do twoich wypowiedzi, jak wspomniano na początku tej odpowiedzi , MRI jest wielowątkowe, ale ograniczone przez GIL do jednego aktywnego wątku naraz. Rodzi to pytanie: czy MRI nie jest lepszym , aby przetestować więcej procesów i mniej wątków? Nie wiem tak naprawdę, pierwsze przypuszczenie byłoby raczej nie lub nie ma znaczenia. Może ktoś może rzucić na to światło.
  • Twój przykład jest w porządku, myślę. Potrzebowałem tylko drobnych modyfikacji .

Dodatek

plików dziennika Rails app:

**config.allow_concurrency = false (by default)** 
-> Ideally 1 worker per core, each worker servers up to 10 threads. 

[3045] Puma starting in cluster mode... 
[3045] * Version 2.11.2 (ruby 2.1.5-p273), codename: Intrepid Squirrel 
[3045] * Min threads: 1, max threads: 40 
[3045] * Environment: development 
[3045] * Process workers: 4 
[3045] * Preloading application 
[3045] * Listening on tcp://0.0.0.0:3000 
[3045] Use Ctrl-C to stop 
[3045] - Worker 0 (pid: 3075) booted, phase: 0 
[3045] - Worker 1 (pid: 3080) booted, phase: 0 
[3045] - Worker 2 (pid: 3087) booted, phase: 0 
[3045] - Worker 3 (pid: 3098) booted, phase: 0 
Started GET "/assets/angular-ui-router/release/angular-ui-router.js?body=1" for 127.0.0.1 at 2015-05-11 15:45:05 +0800 
... 
... 
... 
Processing by ApplicationController#api_call as JSON 
    Parameters: {"t"=>"15?id=9"} 
Completed 200 OK in 15002ms (Views: 0.2ms | ActiveRecord: 0.0ms) 
[3075] 127.0.0.1 - - [11/May/2015:15:49:44 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 60.0230 

**config.allow_concurrency = true** 
-> Ideally 1 worker per core, each worker servers up to 10 threads. 

[22802] Puma starting in cluster mode... 
[22802] * Version 2.11.2 (ruby 2.2.0-p0), codename: Intrepid Squirrel 
[22802] * Min threads: 1, max threads: 40 
[22802] * Environment: development 
[22802] * Process workers: 4 
[22802] * Preloading application 
[22802] * Listening on tcp://0.0.0.0:3000 
[22802] Use Ctrl-C to stop 
[22802] - Worker 0 (pid: 22832) booted, phase: 0 
[22802] - Worker 1 (pid: 22835) booted, phase: 0 
[22802] - Worker 3 (pid: 22852) booted, phase: 0 
[22802] - Worker 2 (pid: 22843) booted, phase: 0 
Started GET "/" for 127.0.0.1 at 2015-05-13 17:58:20 +0800 
Processing by ApplicationController#index as HTML 
    Rendered application/index.html.erb within layouts/application (3.6ms) 
Completed 200 OK in 216ms (Views: 200.0ms | ActiveRecord: 0.0ms) 
[22832] 127.0.0.1 - - [13/May/2015:17:58:20 +0800] "GET/HTTP/1.1" 200 - 0.8190 
... 
... 
... 
Completed 200 OK in 15003ms (Views: 0.1ms | ActiveRecord: 0.0ms) 
[22852] 127.0.0.1 - - [13/May/2015:18:00:07 +0800] "GET /api_call.json?t=15?id=10 HTTP/1.1" 304 - 15.0103 

**config.allow_concurrency = true (by default)** 
-> Ideally each thread serves a request. 

Puma starting in single mode... 
* Version 2.11.2 (jruby 2.2.2), codename: Intrepid Squirrel 
* Min threads: 1, max threads: 40 
* Environment: development 
NOTE: ActiveRecord 4.2 is not (yet) fully supported by AR-JDBC, please help us finish 4.2 support - check http://bit.ly/jruby-42 for starters 
* Listening on tcp://0.0.0.0:3000 
Use Ctrl-C to stop 
Started GET "/" for 127.0.0.1 at 2015-05-13 18:23:04 +0800 
Processing by ApplicationController#index as HTML 
    Rendered application/index.html.erb within layouts/application (35.0ms) 
... 
... 
... 
Completed 200 OK in 15020ms (Views: 0.7ms | ActiveRecord: 0.0ms) 
127.0.0.1 - - [13/May/2015:18:25:19 +0800] "GET /api_call.json?t=15?id=9 HTTP/1.1" 304 - 15.0640 
+0

Dziękujemy! Pozwól mi to przeżuć i szybko odpowiedzieć. –

+0

Na marginesie, który wykracza poza zakres pytania: Przekazałem twój przykład do uruchomienia w środowisku jRuby. Test trwał 2 minuty (i 1 sekundę). – Elyasin

+0

@ArthurFrankel Edytowałem odpowiedź z kilkoma nowymi wglądami podczas ustawiania allow_concurrency = true z JRuby i MRI. – Elyasin

3

zarówno @Elyasin i @Arthur Frankel, stworzyłem ten repo do testowania Puma działa w MRI i JRuby. W tym małym projekcie nie wykonałem sleep, aby emulować długotrwałe żądanie.Jak odkryłem, że w MRI, GIL wydaje się traktować to inaczej niż zwykłe przetwarzanie, bardziej podobnie jak zewnętrzne żądanie we/wy.

Umieściłem obliczenia sekwencji fibonacci w kontrolerze. Na mojej maszynie, fib(39) zajął 6.x sekundy w JRuby i 11 sekund w MRI, co wystarczy, by pokazać różnice.

Otworzyłem 2 okna przeglądarki. Zamiast otwierać karty w tej samej przeglądarce, zrobiłem to, aby zapobiec pewnym ograniczeniom jednoczesnego żądania wysyłania przeglądarki do tej samej domeny. Jestem pewien, że szczegóły, ale dwie różne przeglądarki są wystarczające, aby temu zapobiec.

Testowałem cienki + MRI i Puma + MRI, a następnie Puma + JRuby. Wyniki:

  1. cienki + MRI: nie zdziwiony, gdy szybko załadowałem 2 przeglądarki, pierwsza zakończyła się po 11 sekundach. Następnie rozpoczęła się druga prośba, trwało jeszcze 11 sekund.

  2. Porozmawiajmy najpierw o Puma + JRuby. Ponieważ szybko wczytałem ponownie dwie przeglądarki, wydawało się, że zaczynają się prawie w tej samej sekundzie, i skończyły w tej samej sekundzie. Oba zajęły około 6,9 sekundy. Puma jest serwerem wielowątkowym, a JRuby obsługuje wielowątkowość.

  3. Wreszcie Puma + MRI. Ukończenie obu przeglądarek zajęło 22 sekundy po szybkim ponownym załadowaniu dwóch przeglądarek. Zaczęli prawie w tej samej sekundzie, skończyli prawie w tej samej sekundzie. Ale ukończenie obu zajęło dwa razy więcej czasu. Dokładnie to robi GIL: przełączanie między wątkami dla współbieżności, ale samo blokowanie zapobiega powstawaniu równoległości.

O mojej konfiguracji:

  • Serwery były uruchomionego w trybie produkcyjnym Rails. W trybie produkcyjnym config.cache_classes ustawiono na true, co implikuje config.allow_concurrency = true
  • Puma została uruchomiona z 8 wątkami min i 8 wątkami max.
Powiązane problemy