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
Nie mogę tego odtworzyć z jednorożcem - wszystko działa zgodnie z oczekiwaniami. – Anthony
@Anthony - Edytowałem swój wpis z konfiguracją jednorożca. Czy widzisz coś nie tak z tym, co mam na liście? –
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. –