Demandes concurrentes avec IRM Ruby

j'ai assemblé un exemple simple en essayant de prouver des requêtes concurrentes dans des Rails en utilisant un exemple de base. Notez que J'utilise L'IRM Ruby2 et Rails 4.2.

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

je passe ensuite à 4 onglets différents dans Chrome sur mon mac (i7 / 4 Core) et voir s'ils sont lancés en série ou en parallèle (réellement concurrents ce qui est proche mais pas la même chose). c'est à dire, http://localhost:3000/api_call

Je ne peux pas utiliser Puma, mince, ou Licorne. Les demandes de chacun dans la série. Premier onglet après 10 Secondes, deuxième onglet après 20 (puisqu'il a fallu attendre le premier pour terminer), troisième onglet Après cela....

D'après ce que j'ai lu, je crois que ce qui suit est vrai (s'il vous plaît me corriger) et étaient mes résultats:

  • la Licorne est multi process et mon exemple doit avoir travaillé (d'après la définition du nombre de travailleurs dans une licorne.RB config file), mais il ne l'a pas fait. Je vois 4 les travailleurs commencent mais tout fonctionne en série. J'utilise le Gem unicorn-rails, avec des rails de départ avec unicorn-c config / unicorn.rb, et dans ma licorne.rb j'ai:

-- licorne.rb

worker_processes 4
preload_app true
timeout 30
listen 3000
after_fork do |server, worker|
  ActiveRecord::Base.establish_connection
end
  • Thin et Puma sont multithreaded (bien que Puma au moins a un clustered " mode où vous pouvez commencer les travailleurs avec un paramètre A-w) et ne devrait pas fonctionner de toute façon (en mode multithreaded) avec IRM Ruby2.0 0 parce que "il y a un verrou D'interpréteur Global (Gil) qui assure qu'un seul thread peut être exécuté à la fois".

,

  • est-ce que j'ai un exemple valide (ou est-ce que j'utilise mal le sommeil)?
  • est-ce que mes affirmations ci-dessus au sujet du multiprocessus et du multithread (en ce qui concerne les Rails IRM 2) sont correctes?
  • une idée de pourquoi Je ne peux pas le faire fonctionner avec Unicorn (ou n'importe quel serveur pour cette matière)?

il y a une très question similaire à la mienne mais je ne peux pas faire fonctionner comme répondu et il ne répond pas à toutes mes questions sur les demandes concurrentes en utilisant L'IRM Ruby.

projet Github: https://github.com/afrankel/limitedBandwidth (note: le projet est en regardant de plus que cette question de la multi-processus/d'enfilage sur le serveur)

26
demandé sur Community 2015-04-30 00:55:17

2 réponses

je vous invite à lire la série de Jesse Storimer Nobody understands the Gil Ça pourrait vous aider à mieux comprendre certains internes de L'IRM.

j'ai aussi trouvé Pragmatique de la Simultanéité avec Ruby , qui se lit intéressante. On y trouve des exemples de tests effectués simultanément.

EDIT: En outre, je peux recommander l'article Supprimer config.des threads! Peut - être pas pertinent pour les Rails 4, mais il explique les options de configuration, dont une que vous pouvez utiliser pour permettre la concurrence.


discutons de la réponse à votre question.

vous pouvez avoir plusieurs fils (en utilisant L'IRM), même avec Puma. La GIL garantit qu'un seul thread est actif à la fois, c'est la contrainte que les développeurs dub comme restrictif (en raison de l'absence de réel parallèle exécution.) Gardez à l'esprit que GIL ne garantit pas la sécurité des fils. Cela ne signifie pas que les autres fils ne fonctionnent pas, ils attendent leur tour. Ils peuvent s'entrelacer (les articles peuvent aider à mieux comprendre).

permettez-moi de clarifier certains termes: processus ouvrier, fil. Un processus fonctionne dans un espace mémoire séparé et peut servir plusieurs fils. Les fils du même processus fonctionnent dans un espace mémoire partagé, qui est celui de leur processus. Avec des fils, nous voulons dire Rubis les threads dans ce contexte, pas les threads CPU.

en ce qui concerne la configuration de votre question et le GitHub repo que vous avez partagé, je pense qu'une configuration appropriée (J'ai utilisé Puma) est de configurer 4 ouvriers et 1 à 40 threads. L'idée est qu'un ouvrier sert un onglet. Chaque onglet envoie jusqu'à 10 demandes.

alors commençons:

je travaille sur Ubuntu sur une machine virtuelle. J'ai donc d'abord activé les 4 cœurs de ma machine virtuelle paramètre (et certains autres paramètres dont j'ai pensé qu'il pourrait aider). Je peux vérifier ça sur ma machine. Je suis donc allé avec que.

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

j'ai utilisé votre projet GitHub et l'ai légèrement modifié. J'ai créé un fichier de configuration Puma nommé puma.rb (mis dans le répertoire config ) avec le contenu suivant:

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

par défaut Puma est démarré avec 1 worker et 1 thread. Vous pouvez utiliser des variables d'environnement à modifier ces paramètres. Je l'ai fait:

export MAX_THREADS=40
export WEB_CONCURRENCY=4

pour démarrer Puma avec cette configuration j'ai tapé

bundle exec puma -C config/puma.rb

dans les Rails répertoire app.

j'ai ouvert le navigateur avec quatre onglets pour appeler L'URL de l'application.

la première demande a commencé vers 15:45:05 et la dernière vers 15h49:44. C'est un temps écoulé de 4 minutes et 39 secondes. Vous pouvez aussi voir les id de la requête dans non triés ordre dans le fichier journal. (Voir ci-dessous)

chaque appel API dans le projet GitHub dort pendant 15 secondes. Nous avons 4 onglets, chacun avec 10 appels API. Cela fait un temps écoulé maximum de 600 secondes, soit 10 minutes (en mode strictement série).

le résultat idéal en théorie serait tout en parallèle et un temps écoulé pas loin de 15 secondes, mais je ne m'y attendais pas du tout. Je n'étais pas sûr à quoi s'attendre comme un résultat exactement, mais j'étais encore positivement surpris (considérant que j'ai couru sur une machine virtuelle et L'IRM est freinée par la GIL et d'autres facteurs). Le temps écoulé de cet essai était inférieur à la moitié du temps maximum écoulé (en mode strictement série), nous avons coupé le résultat en moins de la moitié.

EDIT je lis plus loin sur le Rack::Lock qui enveloppe un mutex autour de chaque requête (troisième article ci-dessus). J'ai trouvé l'option config.allow_concurrency = true pour gagner du temps. Un petite mise en garde était d'augmenter le pool de connexion (si la requête n'a pas de requête la base de données a dû être réglée en conséquence); le nombre de threads maximum est un bon défaut. 40 dans ce cas.

j'ai testé l'application avec jRuby et le temps réel écoulé était de 2mins, avec allow_concurrency=true.

j'ai testé l'application avec IRM et le temps réel écoulé a été de 1min47s, avec allow_concurrency=true. Ce fut une grande surprise pour moi. Ce ça m'a vraiment surpris, car je m'attendais à ce que L'IRM soit plus lente que JRuby. Il ne l'était pas. Cela me fait remettre en question la vaste discussion sur les différences de vitesse entre L'IRM et JRuby.

regarder les réponses sur les différents onglets est "plus aléatoire" maintenant. Il arrive que l'onglet 3 ou 4 soit rempli avant l'onglet 1, ce que j'ai d'abord demandé.

je pense que parce que vous n'avez pas les conditions de course le test semble être OK. Cependant, je ne suis pas sûr de les conséquences de l'application large si vous définissez config.allow_concurrency=true dans une application du monde réel.

n'hésitez pas à le consulter et à me faire part de vos commentaires. J'ai toujours le clone sur ma machine. Laissez-moi savoir si vous êtes intéressé.

pour répondre à vos questions dans l'ordre:

  • je pense que votre exemple est valide par résultat. Pour simultanéité cependant, il est préférable de tester avec des ressources partagées (comme par exemple dans le deuxième article).
  • En ce qui concerne vos déclarations, comme mentionné au début de cette réponse, L'IRM est multi-threaded, mais limité par GIL à un actif thread à la fois. Cela soulève la question: avec L'IRM n'est-il pas mieux tester avec plus de processus et moins de threads? Je ne sais pas vraiment, un première hypothèse serait plutôt pas ou pas beaucoup de différence. Peut-être quelqu'un peut jeter de la lumière sur cette.
  • votre exemple est très bien je pense. Juste besoin d'un peu légère modification.

Annexe

fichier Journal d'application Rails:

**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
23
répondu Ely 2015-05-14 02:41:50

pour @Elyasin et @Arthur Frankel, j'ai créé ce repo pour tester le Puma en cours D'IRM et JRuby. Dans ce petit projet, je n'ai pas fait sleep pour imiter une requête de longue date. Comme j'ai constaté que dans L'IRM, le GIL semble traiter cela différemment que le traitement régulier, plus similaire à une demande d'E/S externe.

j'ai mis le calcul de la séquence de fibonacci dans le contrôleur. Sur ma machine, le fib(39) en a pris 6.les x secondes dans JRuby, et 11 secondes en IRM, ce qui est suffisant pour montrer les différences.

j'ai ouvert 2 fenêtres de navigateur. Au lieu d'ouvrir des onglets dans le même navigateur, Je l'ai fait pour empêcher certaines restrictions de la requête concurrente qu'un navigateur envoie au même domaine. Je suis maintenant sûr des détails, mais 2 navigateurs différents est suffisant pour empêcher que cela se produise.

j'ai testé thin + IRM, et Puma + IRM, puis Puma + JRuby. Les résultats sont:

  1. thin + IRM: pas surpris, quand j'ai rapidement rechargé les 2 navigateurs, le premier terminé après 11 secondes. Puis la deuxième requête a commencé, et a pris encore 11 secondes pour finir.

  2. parlons D'abord de Puma + JRuby. Comme j'ai rapidement rechargé les 2 navigateurs, ils ont semblé commencer presque à la même seconde, et fini à la même seconde, aussi. Il a fallu environ 6,9 secondes pour les terminer. Puma est un multi-thread server et JRuby prennent en charge le multi-threading.

  3. enfin Puma + IRM. Il a fallu 22 secondes pour terminer pour les deux navigateurs après que j'ai rapidement rechargé les 2 navigateurs. Ils ont commencé presque à la même seconde, fini presque à la même seconde. Mais il a fallu deux fois plus de temps pour que les deux finissent. C'est exactement ce que GIL fait: passer d'un fil à l'autre pour la concurrence, mais la serrure elle-même empêche le parallélisme de se produire.

A propos de mon installation:

Les serveurs
  • ont tous été lancés en mode de production de Rails. En mode production, config.cache_classes est fixé à true , ce qui implique config.allow_concurrency = true
  • Puma a été lancé avec 8 threads min et 8 threads max.
3
répondu Xenofex 2015-12-26 10:45:24