threads rails parallel multithreaded ruby-on-rails ruby multithreading ruby-on-rails-4 multiprocessing

ruby-on-rails - parallel - ruby on rails concurrency



Solicitudes concurrentes con MRI Ruby (2)

Los invito a leer la serie de Jesse Storimer. Nadie entiende el GIL. Puede ayudarlos a comprender mejor algunos aspectos internos de MRI.

También encontré simultaneidad pragmática con Ruby , que resulta interesante. Tiene algunos ejemplos de prueba al mismo tiempo.

EDITAR: Además, puedo recomendar el artículo Eliminando config.threadsafe! Puede que no sea relevante para Rails 4, pero explica las opciones de configuración, una de las cuales puede usar para permitir la concurrencia.

Discutamos la respuesta a tu pregunta.

Puedes tener varios hilos (usando MRI), incluso con Puma. GIL garantiza que solo un hilo esté activo a la vez, esa es la restricción que los desarrolladores doblan como restrictiva (debido a que no hay una ejecución paralela real). Tenga en cuenta que GIL no garantiza la seguridad del hilo. Esto no significa que los otros hilos no se estén ejecutando, están esperando su turno. Pueden intercalarse (los artículos pueden ayudar a comprender mejor).

Permítanme aclarar algunos términos: proceso de trabajo, hilo. Un proceso se ejecuta en un espacio de memoria separado y puede servir para varios hilos. Los hilos del mismo proceso se ejecutan en un espacio de memoria compartida, que es el de su proceso. Con los hilos nos referimos a los hilos Ruby en este contexto, no a los hilos de la CPU.

Con respecto a la configuración de su pregunta y al repositorio de GitHub que compartió, creo que una configuración adecuada (utilicé Puma) es configurar 4 trabajadores y 1 a 40 subprocesos. La idea es que un trabajador atienda una sola pestaña. Cada pestaña envía hasta 10 solicitudes.

Entonces empecemos:

Trabajo en Ubuntu en una máquina virtual. Así que primero habilité los 4 núcleos en la configuración de mi máquina virtual (y algunas otras configuraciones de las cuales pensé que podría ser útil). Podría verificar esto en mi máquina. Así que fui con eso.

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

Usé su proyecto GitHub compartido y lo modifiqué un poco. puma.rb un archivo de configuración de Puma llamado puma.rb (lo puse en el directorio config ) con el siguiente contenido:

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

Por defecto, Puma se inicia con 1 trabajador y 1 hilo. Puede usar variables de entorno para modificar esos parámetros. Así lo hice:

export MAX_THREADS=40 export WEB_CONCURRENCY=4

Para iniciar Puma con esta configuración escribí

bundle exec puma -C config/puma.rb

en el directorio de la aplicación Rails.

Abrí el navegador con cuatro pestañas para llamar a la URL de la aplicación.

La primera solicitud comenzó alrededor de las 15:45:05 y la última solicitud fue alrededor de las 15h49: 44. Ese es un tiempo transcurrido de 4 minutos y 39 segundos. También puede ver los id de la solicitud en orden no ordenado en el archivo de registro. (Vea abajo)

Cada llamada API en el proyecto GitHub duerme durante 15 segundos. Tenemos cuatro 4 pestañas, cada una con 10 llamadas API. Eso hace un tiempo máximo transcurrido de 600 segundos, es decir, 10 minutos (en un estricto modo en serie).

El resultado ideal en teoría sería todo en paralelo y un tiempo transcurrido no inferior a 15 segundos, pero no esperaba eso en absoluto. No estaba seguro de qué esperar exactamente como resultado, pero aún así me sorprendió positivamente (teniendo en cuenta que corrí en una máquina virtual y MRI está restringido por el GIL y algunos otros factores). El tiempo transcurrido de esta prueba fue menos de la mitad del tiempo máximo transcurrido (en el estricto modo de serie), cortamos el resultado en menos de la mitad.

EDITAR Leí más acerca de Rack :: Lock que envuelve un mutex alrededor de cada solicitud (tercer artículo arriba). Encontré la opción config.allow_concurrency = true para config.allow_concurrency = true tiempo. Una pequeña advertencia fue aumentar el grupo de conexiones (aunque la solicitud no requiere consulta, la base de datos debe establecerse en consecuencia); la cantidad de hilos máximos es un buen valor predeterminado. 40 en este caso.

Probé la aplicación con jRuby y el tiempo transcurrido real fue de 2 minutos, con allow_concurrency = true.

Probé la aplicación con MRI y el tiempo transcurrido real fue de 1min47s, con allow_concurrency = true. Esto fue una gran sorpresa para mí. Esto realmente me sorprendió, porque esperaba que la MRI fuera más lenta que JRuby. No era. Esto me hace cuestionar la amplia discusión sobre las diferencias de velocidad entre MRI y JRuby.

Ver las respuestas en las diferentes pestañas ahora es "más aleatorio". Sucede que la pestaña 3 o 4 completa antes de la pestaña 1, que solicité primero.

Creo que debido a que no tienes condiciones de carrera, la prueba parece estar bien. Sin embargo, no estoy seguro acerca de las amplias consecuencias de la aplicación si establece config.allow_concurrency = true en una aplicación real.

No dude en consultarlo y dejarme saber cualquier comentario que puedan tener los lectores. Todavía tengo el clon en mi máquina. Déjame saber si estás interesado.

Para responder a sus preguntas en orden:

  • Creo que tu ejemplo es válido por resultado. Para concurrencia, sin embargo, es mejor probar con recursos compartidos (como por ejemplo en el segundo artículo).
  • Con respecto a sus declaraciones, como se mencionó al principio de esta respuesta, MRI tiene varios hilos, pero está restringido por GIL a un hilo activo a la vez. Esto plantea la pregunta: ¿con MRI no es mejor probar con más procesos y menos hilos? Realmente no lo sé, una primera suposición sería bastante o no una gran diferencia. Tal vez alguien pueda arrojar luz sobre esto.
  • Tu ejemplo está bien, creo. Solo necesitaba algunas pequeñas modificaciones.

Apéndice

Aplicación de archivos de registro de rieles:

**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

Puse un ejemplo simple tratando de probar solicitudes concurrentes en Rails usando un ejemplo básico. Tenga en cuenta que estoy usando MRI Ruby2 y Rails 4.2.

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

Luego, voy a 4 pestañas diferentes en Chrome en mi mac (I7 / 4 Core) y veo si se ejecutan en serie o en paralelo (realmente concurrente, que está cerca pero no es lo mismo). es decir, http://localhost:3000/api_call

No puedo hacer que esto funcione usando Puma, Delgado o Unicornio. Las solicitudes vienen en serie. Primera pestaña después de 10 segundos, segunda después de 20 (ya que tuvo que esperar a que la primera se complete), la tercera después de eso ...

Por lo que he leído, creo que lo siguiente es cierto (corrígeme) y fueron mis resultados:

  • Unicorn es multiproceso y mi ejemplo debería haber funcionado (después de definir el número de trabajadores en un archivo de configuración unicorn.rb), pero no fue así. Puedo ver a 4 trabajadores comenzando pero todo funciona en serie. Estoy usando la gema unicorn-rails, comenzando rails con unicorn -c config / unicorn.rb, y en mi unicorn.rb tengo:

- unicorn.rb

worker_processes 4 preload_app true timeout 30 listen 3000 after_fork do |server, worker| ActiveRecord::Base.establish_connection end

  • Thin y Puma tienen múltiples subprocesos (aunque Puma al menos tiene un modo '' clustered '' donde puede iniciar trabajadores con un parámetro -w) y no deberían funcionar de todos modos (en modo multiproceso) con MRI Ruby2.0 porque "hay un bloqueo de intérprete global (GIL) que asegura que solo se puede ejecutar un hilo a la vez ".

Asi que,

  • ¿Tengo un ejemplo válido (o estoy usando el sueño incorrecto)?
  • ¿Son correctas mis afirmaciones anteriores sobre multiprocesamiento y multiproceso (con respecto a MRI Rails 2)?
  • ¿Alguna idea sobre por qué no puedo hacer que funcione con Unicorn (o cualquier servidor para el caso)?

Hay una pregunta muy similar a la mía, pero no puedo hacer que funcione como respondí y no responde a todas mis preguntas sobre solicitudes concurrentes usando MRI Ruby.

Proyecto Github: https://github.com/afrankel/limitedBandwidth (nota: el proyecto está buscando más que esta cuestión de multiproceso / subprocesamiento en el servidor)


Tanto para @Elyasin como para @Arthur Frankel, creé este repo para probar Puma corriendo en MRI y JRuby. En este pequeño proyecto, no sleep para emular una solicitud de larga ejecución. Como descubrí que en MRI, el GIL parece tratarlo de manera diferente que el procesamiento regular, más similarmente a una solicitud de E / S externa.

Pongo el cálculo de la secuencia de fibonacci en el controlador. En mi máquina, el fib(39) tardó 6.x segundos en JRuby, y 11 segundos en MRI, que es suficiente para mostrar las diferencias.

Abrí 2 ventanas del navegador. En lugar de abrir pestañas en el mismo navegador, hice esto para evitar ciertas restricciones de la solicitud concurrente que un navegador envía al mismo dominio. Ahora estoy seguro de los detalles, pero 2 navegadores diferentes son suficientes para evitar que eso suceda.

Probé Thin + MRI, y Puma + MRI, luego Puma + JRuby. Los resultados son:

  1. thin + MRI: no me sorprendió, cuando recargué rápidamente los 2 navegadores, el primero terminó después de 11 segundos. Luego, comenzó la segunda solicitud, tomó otros 11 segundos para terminar.

  2. Primero hablemos de Puma + JRuby. A medida que volví a cargar rápidamente los 2 navegadores, parecían comenzar casi en el mismo segundo, y terminaron en el mismo segundo, también. Ambos tardaron alrededor de 6,9 ​​segundos en terminar. Puma es un servidor multiproceso y JRuby admite multi-threading.

  3. Finalmente Puma + MRI. Me tomó 22 segundos terminar para ambos navegadores después de que recargué rápidamente los 2 navegadores. Comenzaron casi en el mismo segundo, y terminaron casi en el mismo segundo también. Pero les llevó dos veces terminar. Eso es exactamente lo que hace GIL: cambiar entre los hilos por concurrencia, pero el bloqueo en sí mismo impide que ocurra el paralelismo.

Acerca de mi configuración:

  • Los servidores fueron lanzados en el modo de producción Rails. En el modo de producción, config.cache_classes se establece en true , lo que implica config.allow_concurrency = true
  • Puma se inició con 8 hilos mínimo y 8 hilos máximo.