2010-08-18 15 views
8

Planeo utilizar el trabajo retrasado para ejecutar algunos análisis de fondo. En mi prueba inicial vi una gran cantidad de uso de memoria, así que básicamente creé una tarea muy simple que se ejecuta cada 2 minutos solo para observar cuánta memoria está siendo utilizada.utiliza alto uso de memoria en rieles

La tarea es muy simple y la analytics_eligbile? método siempre devuelve falso, dado que los datos están ahora, por lo que básicamente no se está llamando al código de golpe pesado. Tengo alrededor de 200 publicaciones en mis datos de muestra en desarrollo. Publicar has_one analytics_facet.

Independientemente de la lógica/negocio interno aquí, lo único que hace esta tarea es llamar a analytics_eligible? método 200 veces cada 2 minutos. En cuestión de 4 horas, mi uso de memoria física es de 110 MB y la memoria virtual es de 200 MB. ¡Solo por hacer algo así de simple! ¡Ni siquiera puedo empezar a imaginarme cuánta memoria comerá si está haciendo análisis reales en 10,000 Publicaciones con información real de producción! De acuerdo, puede que no se ejecute cada 2 minutos, más como cada 30, aún así no creo que vaya a volar.

Esto está ejecutando ruby ​​1.9.7, rails 2.3.5 en Ubuntu 10.x 64 bit. Mi computadora portátil tiene una memoria de 4GB y una CPU de doble núcleo.

¿Los rieles son realmente tan malos o estoy haciendo algo mal?

Delayed::Worker.logger.info('RAM USAGE Job Start: ' + `pmap #{Process.pid} | tail -1`[10,40].strip) 
Post.not_expired.each do |p| 
    if p.analytics_eligible? 
     #this method is never called 
     Post.find_for_analytics_update(p.id).update_analytics 
    end 
end 
Delayed::Worker.logger.info('RAM USAGE Job End: ' + `pmap #{Process.pid} | tail -1`[10,40].strip) 

Delayed::Job.enqueue PeriodicAnalyticsJob.new(), 0, 2.minutes.from_now 

modelo de Post

def analytics_eligible? 
     vf = self.analytics_facet 
     if self.total_ratings > 0 && vf.nil? 
      return true 
     elsif !vf.nil? && vf.last_update_tv > 0 
      ratio = self.total_ratings/vf.last_update_tv 
      if (ratio - 1) >= Constants::FACET_UPDATE_ELIGIBILITY_DELTA 
       return true 
      end 
     end 
     return false 
    end 

Respuesta

18

ActiveRecord es bastante hambriento de memoria: tenga mucho cuidado al hacer selects, y tenga en cuenta que Ruby devuelve automáticamente la última instrucción en un bloque como valor de retorno, lo que significa que está devolviendo una serie de registros que obtienen guardado como resultado en alguna parte y por lo tanto no son elegibles para GC.

Además, cuando llamas "Post.not_expired.each", estás cargando tus publicaciones no_expuestas en la RAM. Una mejor solución es find_in_batches, que específicamente solo carga X registros en RAM a la vez.

fijándolo podría ser algo tan simple como:

def do_analytics 
    Post.not_expired.find_in_batches(:batch_size => 100) do |batch| 
    batch.each do |post| 
     if post.analytics_eligible? 
     #this method is never called 
     Post.find_for_analytics_update(post.id).update_analytics 
     end 
    end 
    end 
    GC.start 
end 

do_analytics 

Algunas cosas están sucediendo aquí. En primer lugar, todo tiene un alcance en una función para evitar que las colisiones variables se aferren a las referencias de los iteradores de bloques. A continuación, find_in_batches recupera los objetos batch_size de la base de datos a la vez, y siempre que no genere referencias a ellos, sea elegible para la recolección de elementos no utilizados después de cada iteración, lo que reducirá el uso de la memoria total. Finalmente, llamamos al GC.start al final del método; esto obliga al GC a iniciar un barrido (que no le gustaría hacer en una aplicación en tiempo real, pero dado que se trata de un trabajo en segundo plano, está bien si se necesitan 300 ms adicionales para ejecutarse). También tiene un beneficio muy claro si devuelve nil, lo que significa que el resultado del método es nil, lo que significa que no podemos aferrarnos accidentalmente a las instancias de AR devueltas desde el buscador.

Usar algo como esto debería garantizar que no termines con objetos AR filtrados, y debería mejorar enormemente tanto el rendimiento como el uso de la memoria. Deberá asegurarse de no estar filtrando en otra parte de su aplicación (las variables de clase, globales y referencias de clase son los peores infractores), pero sospecho que esto resolverá su problema.

Dicho todo esto, este es un problema de cron (trabajo recurrente periódico), en lugar de un problema de DJ, en mi opinión. Puede tener un analizador analítico de un solo uso que ejecuta su análisis cada X minutos con script/runner, invocado por cron, que limpia muy bien cualquier posible pérdida de memoria o uso indebido por ejecución (ya que todo el proceso termina al final)

+0

Lo único que agregaría a esta excelente respuesta es una nota que indica que cualquier proceso de Rails consumirá mucha memoria, sus 110mb no son poco comunes. Esto no es indicativo de una pérdida de memoria en su código o de cuánto procesamiento ha realizado. Si procesas 1000 registros o 10M, usarás la misma cantidad de memoria si has hecho las cosas correctamente (de la forma en que Chris lo explicó). – wuputah

0

Es un hecho que Ruby consume memoria (y fugas). No sé si puedes hacer mucho al respecto, pero al menos te recomiendo que eches un vistazo al Ruby Enterprise Edition.

REE es un puerto de fuente abierta que promete "33% menos de memoria" entre todas las otras cosas buenas. He usado REE con Passenger en producción durante casi dos años y estoy muy contento.

+0

Bueno, tengo ciertas cosas sobre RoR hasta ahora, pero si es tan malo, realmente me decepciona. Estoy intentando REE ahora, gracias! – badnaam

+0

La promesa de REE de "33% menos de uso de memoria" se debe al proceso de bifurcación después de que se haya cargado la infraestructura de Rails. En un solo proceso, no tendrá un efecto significativo. –

1

Si tiene problemas de memoria, una solución es utilizar otra tecnología de procesamiento en segundo plano, como resque. Es el procesamiento de BG utilizado por github.

Gracias a la arquitectura padre/hijo de Resque, trabajos que utilizan la secreción excesiva de memoria que la memoria al finalización. Sin crecimiento no deseado

¿Cómo?

En algunas plataformas, cuando un trabajador se reserva Resque un trabajo de TI inmediatamente un proceso hijo. El hijo procesa el trabajo y luego sale. Cuando el hijo ha salido exitosamente, el trabajador se reserva otro trabajo y repite el proceso.

Puede encontrar más detalles técnicos en README.

+0

Gracias. ¿En qué plataformas funciona esta arquitectura padre/hijo? – badnaam

+0

Sé que funciona en Linux y OS X. ¿Posiblemente no funciona en Windows? – wuputah

6

Cargar datos en lotes y usar el recolector de basura agresivamente como Chris Heald ha sugerido le dará algunas ganancias realmente grandes, pero otra área que la gente a menudo pasa por alto es en qué marcos están cargando.

Cargando una pila predeterminada de Rails le dará ActionController, ActionMailer, ActiveRecord y ActiveResource todos juntos. Si está compilando una aplicación web, es posible que no esté utilizando todo esto, pero probablemente esté utilizando la mayoría.

Cuando usted está construyendo un trabajo en segundo plano, puede evitar cosas de carga que no necesite mediante la creación de un entorno personalizado para que:

# config/environments/production_bg.rb 

config.frameworks -= [ :action_controller, :active_resource, :action_mailer ] 

# (Also include config directives from production.rb that apply) 

Cada uno de estos marcos será sólo sentarse a esperar para una correo electrónico que nunca se enviará, o un controlador que nunca será llamado. Simplemente no tiene sentido cargarlos. Ajuste su archivo database.yml, configure su trabajo de fondo para que se ejecute en el entorno production_bg, y tendrá una pizarra mucho más limpia para empezar.

Otra cosa que puede hacer es usar ActiveRecord directamente sin cargar los raíles. Esto podría ser todo lo que necesita para esta operación en particular. También he encontrado que usar un ORM liviano como Sequel hace que tu trabajo en segundo plano sea muy liviano si haces principalmente llamadas SQL para reorganizar registros o borrar datos viejos. Si necesita acceder a sus modelos y sus métodos, necesitará usar ActiveRecord. Sin embargo, a veces vale la pena implementar nuevamente la lógica simple en SQL puro por razones de rendimiento y eficiencia.

Al medir el uso de la memoria, el único número que debe preocuparse es la memoria "real".La cantidad virtual contiene bibliotecas compartidas y el costo de éstas se distribuye entre cada proceso que las usa, aunque se cuente por completo para cada una.

Al final, si ejecutar algo importante requiere 100 MB de memoria, pero puede obtener hasta 10 MB con tres semanas de trabajo, no veo por qué te molestaría. 90 MB de memoria cuesta como máximo alrededor de $ 60 por año en un proveedor administrado que generalmente es mucho menos costoso que su tiempo.

Ruby on Rails adopta la filosofía de estar más preocupado por su productividad y su tiempo que por el uso de la memoria. Si quieres recortarlo, ponlo a dieta, puedes hacerlo pero tomará un poco de esfuerzo.

+0

¡Buenos puntos! ¡Muchas gracias! – badnaam

Cuestiones relacionadas