2011-11-30 14 views
9

Tengo un servicio Java de 32 bits con problemas de escalabilidad: con un alto número de usuarios nos quedamos sin memoria debido al conteo excesivo de hilos. A largo plazo, planeo cambiar a 64 bits y reducir la proporción de subprocesos por usuario. En el corto plazo, me gustaría reducir el tamaño de la pila (-Xss, -XX: ThreadStackSize) para obtener más espacio libre. Pero esto es arriesgado porque si lo hago demasiado pequeño, obtendré StackOverflowErrors.¿Cómo puedo medir la profundidad de la pila de hilos?

¿Cómo puedo medir el tamaño promedio y máximo de la pila de mi aplicación para guiar mi decisión a un valor óptimo de XS? Estoy interesado en dos enfoques posibles:

  1. Medición de una JVM en funcionamiento durante las pruebas de integración. ¿Qué herramientas de generación de perfiles informarán la profundidad máxima de la pila?
  2. Análisis estático de la aplicación que busca jerarquías de llamadas profundas. La reflexión en inyección de dependencia hace improbable que esto funcione.

actualización: Yo sé de la manera correcta a largo plazo para solucionar este problema. Por favor concéntrese en la pregunta que hice: ¿cómo mido medida profundidad de la pila?

Actualización 2: Tengo una respuesta agradable en una pregunta relacionada específicamente sobre JProfiler: Can JProfiler measure stack depth? (he publicado la pregunta por separado según las recomendaciones de apoyo a la comunidad de JProfiler)

+1

Debería considerar cambiar al modelo asíncrono. No tiene sentido tener más hilos que núcleos de CPU en el sistema. –

+2

@VladLazarenko - de acuerdo. Como dije, a largo plazo planeo reducir la proporción de subprocesos por usuario, pero necesito una solución rápida antes. –

+0

¿Cuántos hilos estás creando y cómo los estás administrando? ¿Y por qué necesita un hilo por usuario, no puede reutilizar los hilos y ofrecer hilo por solicitud? –

Respuesta

6

Puede hacerse una idea de la profundidad de la pila con algo parecido a un aspecto que se puede tejer a su código (cargar tejedor de tiempo para permitir el asesoramiento de todo el código cargado excepto el cargador de clase del sistema). El aspecto funcionaría alrededor de todo el código ejecutado y sería capaz de notarlo cuando llame a un método y cuando regrese. Puede usar esto para capturar la mayor parte del uso de la pila (se perderá todo lo cargado desde el cargador de clases del sistema, por ejemplo, java. *). Aunque no es perfecto, evita tener que cambiar tu código para juntar StackTraceElement [] en puntos de muestra y también te lleva al código que no has escrito jdk.

Por ejemplo (aspectj):

public aspect CallStackAdvice { 

    pointcut allMethods() : execution(* *(..)) && !within(CallStackLog); 

    Object around(): allMethods(){ 
     String called = thisJoinPoint.getSignature().toLongString(); 
     CallStackLog.calling (called); 
     try { 
      return proceed(); 
     } finally { 
      CallStackLog.exiting (called); 
     } 
    } 

} 

public class CallStackLog { 

    private CallStackLog() {} 

    private static ThreadLocal<ArrayDeque<String>> curStack = 
     new ThreadLocal<ArrayDeque<String>>() { 
     @Override 
     protected ArrayDeque<String> initialValue() { 
      return new ArrayDeque<String>(); 
     } 
    }; 

    private static ThreadLocal<Boolean> ascending = 
     new ThreadLocal<Boolean>() { 
     @Override 
     protected Boolean initialValue() { 
      return true; 
     } 
    }; 

    private static ConcurrentHashMap<Integer, ArrayDeque<String>> stacks = 
     new ConcurrentHashMap<Integer, ArrayDeque<String>>(); 

    public static void calling (String signature) { 
     ascending.set (true); 
     curStack.get().push (signature.intern()); 
    } 

    public static void exiting (String signature) { 
     ArrayDeque<String> cur = curStack.get(); 
     if (ascending.get()) { 
      ArrayDeque<String> clon = cur.clone(); 
      stacks.put (hash (clon), clon); 
     } 
     cur.pop(); 
     ascending.set (false); 
    } 

    public static Integer hash (ArrayDeque<String> a) { 
     //simplistic and wrong but ok for example 
     int h = 0; 
     for (String s : a) { 
      h += (31 * s.hashCode()); 
     } 
     return h; 
    } 

    public static void dumpStacks(){ 
     //implement something to print or retrieve or use stacks 
    } 
} 

y una pila de muestra podría ser como:

net.sourceforge.jtds.jdbc.TdsCore net.sourceforge.jtds.jdbc.JtdsStatement.getTds() 
public boolean net.sourceforge.jtds.jdbc.JtdsResultSet.next() 
public void net.sourceforge.jtds.jdbc.JtdsResultSet.close() 
public java.sql.Connection net.sourceforge.jtds.jdbc.Driver.connect(java.lang.String, java.util.Properties) 
public void phil.RandomStackGen.MyRunnable.run() 

muy lento y tiene sus propios problemas de memoria, pero puede ser viable para obtener la información de la pila necesitas.

Puede utilizar max_stack y max_locals para cada método en los rastreos de pila para calcular el tamaño de un marco (consulte class file format) para el método. Basado en vm spec, creo que esto debería ser (max_stack + max_locals) * 4bytes para el tamaño de fotograma máximo para un método (long/double ocupa dos entradas en la pila de operando/local vars y se contabiliza en max_stack y max_locals).

Puede javap fácilmente las clases de interés y ver los valores de marco si no tiene mucho en las pilas de llamadas. Y algo como asm le ofrece algunas herramientas fáciles de usar para hacer esto en una escala mayor.

Una vez que haya calculado esto, necesita estimar los marcos de pila adicionales para las clases de JDK que pueden ser llamados por usted en sus puntos de acumulación máxima y agregarlos a sus tamaños de pila. No será perfecto, pero debería darte un punto de partida decente para la afinación de Xss sin hackear el JVM/JDK.

Otra nota: no sé qué hace JIT/OSR para encuadrar los tamaños o los requisitos de pila, así que tenga en cuenta que puede tener diferentes impactos de -Xss en una JVM fría vs. cálida.

EDIT tenían algunas horas de inactividad y lanzaron juntos otro enfoque. Este es un agente de Java que instrumentará métodos para realizar un seguimiento de un tamaño máximo de marco de pila y profundidad de pila. Esto podrá instrumentar la mayoría de las clases de jdk junto con su otro código y bibliotecas, brindándole mejores resultados que el tejedor de aspectos. Necesitas asm v4 para que esto funcione. Era más por el gusto de hacerlo, así que archiva esto bajo la jactanciosa java por diversión, sin ganancias.

primer lugar, hacer algo para rastrear el tamaño y la profundidad del marco de pila:

package phil.agent; 

public class MaxStackLog { 

    private static ThreadLocal<Integer> curStackSize = 
     new ThreadLocal<Integer>() { 
     @Override 
     protected Integer initialValue() { 
      return 0; 
     } 
    }; 

    private static ThreadLocal<Integer> curStackDepth = 
     new ThreadLocal<Integer>() { 
     @Override 
     protected Integer initialValue() { 
      return 0; 
     } 
    }; 

    private static ThreadLocal<Boolean> ascending = 
     new ThreadLocal<Boolean>() { 
     @Override 
     protected Boolean initialValue() { 
      return true; 
     } 
    }; 

    private static ConcurrentHashMap<Long, Integer> maxSizes = 
     new ConcurrentHashMap<Long, Integer>(); 
    private static ConcurrentHashMap<Long, Integer> maxDepth = 
     new ConcurrentHashMap<Long, Integer>(); 

    private MaxStackLog() { } 

    public static void enter (int frameSize) { 
     ascending.set (true); 
     curStackSize.set (curStackSize.get() + frameSize); 
     curStackDepth.set (curStackDepth.get() + 1); 
    } 

    public static void exit (int frameSize) { 
     int cur = curStackSize.get(); 
     int curDepth = curStackDepth.get(); 
     if (ascending.get()) { 
      long id = Thread.currentThread().getId(); 
      Integer max = maxSizes.get (id); 
      if (max == null || cur > max) { 
       maxSizes.put (id, cur); 
      } 
      max = maxDepth.get (id); 
      if (max == null || curDepth > max) { 
       maxDepth.put (id, curDepth); 
      } 
     } 
     ascending.set (false); 
     curStackSize.set (cur - frameSize); 
     curStackDepth.set (curDepth - 1); 
    } 

    public static void dumpMax() { 
     int max = 0; 
     for (int i : maxSizes.values()) { 
      max = Math.max (i, max); 
     } 
     System.out.println ("Max stack frame size accummulated: " + max); 
     max = 0; 
     for (int i : maxDepth.values()) { 
      max = Math.max (i, max); 
     } 
     System.out.println ("Max stack depth: " + max); 
    } 
} 

A continuación, hacer que el agente de java:

package phil.agent; 

public class Agent { 

    public static void premain (String agentArguments, Instrumentation ins) { 
     try { 
      ins.appendToBootstrapClassLoaderSearch ( 
       new JarFile ( 
        new File ("path/to/Agent.jar"))); 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } 
     ins.addTransformer (new Transformer(), true); 
     Class<?>[] classes = ins.getAllLoadedClasses(); 
     int len = classes.length; 
     for (int i = 0; i < len; i++) { 
      Class<?> clazz = classes[i]; 
      String name = clazz != null ? clazz.getCanonicalName() : null; 
      try { 
       if (name != null && !clazz.isArray() && !clazz.isPrimitive() 
         && !clazz.isInterface() 
         && !name.equals ("java.lang.Long") 
         && !name.equals ("java.lang.Boolean") 
         && !name.equals ("java.lang.Integer") 
         && !name.equals ("java.lang.Double") 
         && !name.equals ("java.lang.Float") 
         && !name.equals ("java.lang.Number") 
         && !name.equals ("java.lang.Class") 
         && !name.equals ("java.lang.Byte") 
         && !name.equals ("java.lang.Void") 
         && !name.equals ("java.lang.Short") 
         && !name.equals ("java.lang.System") 
         && !name.equals ("java.lang.Runtime") 
         && !name.equals ("java.lang.Compiler") 
         && !name.equals ("java.lang.StackTraceElement") 
         && !name.startsWith ("java.lang.ThreadLocal") 
         && !name.startsWith ("sun.") 
         && !name.startsWith ("java.security.") 
         && !name.startsWith ("java.lang.ref.") 
         && !name.startsWith ("java.lang.ClassLoader") 
         && !name.startsWith ("java.util.concurrent.atomic") 
         && !name.startsWith ("java.util.concurrent.ConcurrentHashMap") 
         && !name.startsWith ("java.util.concurrent.locks.") 
         && !name.startsWith ("phil.agent.")) { 
        ins.retransformClasses (clazz); 
       } 
      } catch (Throwable e) { 
       System.err.println ("Cant modify: " + name); 
      } 
     } 

     Runtime.getRuntime().addShutdownHook (new Thread() { 
      @Override 
      public void run() { 
       MaxStackLog.dumpMax(); 
      } 
     }); 
    } 
} 

La clase agente tiene el gancho premain para la instrumentación. En ese gancho, agrega un transformador de clase que los instrumentos en el seguimiento del tamaño del marco de pila. También agrega el agente al cargador de clases de arranque para que también pueda procesar clases jdk. Para hacer eso, necesitamos retransformar todo lo que ya se haya cargado, como String.class.Pero tenemos que excluir una variedad de cosas que el agente o el registro de la pila utilizan, lo que conduce a ciclos infinitos u otros problemas (algunos de los cuales se encontraron por prueba y error). Finalmente, el agente agrega un gancho de cierre para volcar los resultados a stdout.

public class Transformer implements ClassFileTransformer { 

    @Override 
    public byte[] transform (ClassLoader loader, 
     String className, Class<?> classBeingRedefined, 
      ProtectionDomain protectionDomain, byte[] classfileBuffer) 
      throws IllegalClassFormatException { 

     if (className.startsWith ("phil/agent")) { 
      return classfileBuffer; 
     } 

     byte[] result = classfileBuffer; 
     ClassReader reader = new ClassReader (classfileBuffer); 
     MaxStackClassVisitor maxCv = new MaxStackClassVisitor (null); 
     reader.accept (maxCv, ClassReader.SKIP_DEBUG); 

     ClassWriter writer = new ClassWriter (ClassWriter.COMPUTE_FRAMES); 
     ClassVisitor visitor = 
      new CallStackClassVisitor (writer, maxCv.frameMap, className); 
     reader.accept (visitor, ClassReader.SKIP_DEBUG); 
     result = writer.toByteArray(); 
     return result; 
    } 
} 

El transformador acciona dos transformaciones separadas - uno para averiguar el tamaño de marco de pila max para cada método y uno para instrumentar el método para la grabación. Podría ser factible en una sola pasada, pero no quería usar la API de árbol de ASM ni pasar más tiempo averiguándolo.

public class MaxStackClassVisitor extends ClassVisitor { 

    Map<String, Integer> frameMap = new HashMap<String, Integer>(); 

    public MaxStackClassVisitor (ClassVisitor v) { 
     super (Opcodes.ASM4, v); 
    } 

    @Override 
    public MethodVisitor visitMethod (int access, String name, 
     String desc, String signature, 
      String[] exceptions) { 
     return new MaxStackMethodVisitor ( 
      super.visitMethod (access, name, desc, signature, exceptions), 
      this, (access + name + desc + signature)); 
    } 
} 

public class MaxStackMethodVisitor extends MethodVisitor { 

    final MaxStackClassVisitor cv; 
    final String name; 

    public MaxStackMethodVisitor (MethodVisitor mv, 
     MaxStackClassVisitor cv, String name) { 
     super (Opcodes.ASM4, mv); 
     this.cv = cv; 
     this.name = name; 
    } 

    @Override 
    public void visitMaxs (int maxStack, int maxLocals) { 
     cv.frameMap.put (name, (maxStack + maxLocals) * 4); 
     super.visitMaxs (maxStack, maxLocals); 
    } 
} 

Las clases MaxStack * visitantes manejan averiguar el tamaño del marco de pila máximo.

public class CallStackClassVisitor extends ClassVisitor { 

    final Map<String, Integer> frameSizes; 
    final String className; 

    public CallStackClassVisitor (ClassVisitor v, 
     Map<String, Integer> frameSizes, String className) { 
     super (Opcodes.ASM4, v); 
     this.frameSizes = frameSizes; 
     this.className = className; 
    } 

    @Override 
    public MethodVisitor visitMethod (int access, String name, 
     String desc, String signature, String[] exceptions) { 
     MethodVisitor m = super.visitMethod (access, name, desc, 
          signature, exceptions); 
     return new CallStackMethodVisitor (m, 
       frameSizes.get (access + name + desc + signature)); 
    } 
} 

public class CallStackMethodVisitor extends MethodVisitor { 

    final int size; 

    public CallStackMethodVisitor (MethodVisitor mv, int size) { 
     super (Opcodes.ASM4, mv); 
     this.size = size; 
    } 

    @Override 
    public void visitCode() { 
     visitIntInsn (Opcodes.SIPUSH, size); 
     visitMethodInsn (Opcodes.INVOKESTATIC, "phil/agent/MaxStackLog", 
       "enter", "(I)V"); 
     super.visitCode(); 
    } 

    @Override 
    public void visitInsn (int inst) { 
     switch (inst) { 
      case Opcodes.ARETURN: 
      case Opcodes.DRETURN: 
      case Opcodes.FRETURN: 
      case Opcodes.IRETURN: 
      case Opcodes.LRETURN: 
      case Opcodes.RETURN: 
      case Opcodes.ATHROW: 
       visitIntInsn (Opcodes.SIPUSH, size); 
       visitMethodInsn (Opcodes.INVOKESTATIC, 
         "phil/agent/MaxStackLog", "exit", "(I)V"); 
       break; 
      default: 
       break; 
     } 

     super.visitInsn (inst); 
    } 
} 

Las clases de visitante de CallStack * manejan métodos de instrumentación con código para llamar al registro de la pila de marco.

Y entonces usted necesita un MANIFEST.MF para el Agent.jar:

Manifest-Version: 1.0 
Premain-Class: phil.agent.Agent 
Boot-Class-Path: asm-all-4.0.jar 
Can-Retransform-Classes: true 

Por último, añadir lo siguiente a la línea de comandos java para el programa que desea instrumento:

-javaagent:path/to/Agent.jar 

También necesitará tener asm-all-4.0.jar en el mismo directorio que Agent.jar (o cambiar Boot-Class-Path en el manifiesto para hacer referencia a la ubicación).

Un ejemplo del resultado podría ser:

Max stack frame size accummulated: 44140 
Max stack depth: 1004 

Todo esto es un poco crudo, pero funciona para mí para ponerse en marcha.

Nota: el tamaño del marco de pila no es un tamaño de pila total (todavía no se sabe cómo obtenerlo). En la práctica, hay una variedad de gastos generales para la pila de subprocesos. Descubrí que, por lo general, necesitaba entre 2 y 3 veces el tamaño de fotograma máximo de la pila como valor -Xss. Ah, y asegúrese de hacer la afinación -Xss sin cargar el agente, ya que se suma a los requisitos de tamaño de la pila.

+0

La única debilidad que puedo ver en su enfoque es que no se manejará cuando se lanza una excepción desde un método más abajo en la pila. Esto requeriría un bloque try/finally. – mchr

5

que reduciría el ajuste -Xss en un entorno de prueba hasta que veas un problema. Luego agrega un poco de espacio para la cabeza.

Al reducir el tamaño de su pila, su aplicación tendrá más espacio para las pilas de hilos.

Simplemente cambiando a un sistema operativo de 64 bits podría darle a su aplicación más memoria ya que la mayoría de los sistemas operativos de 32 bits solo permiten 1.5 GB para cada aplicación, sin embargo, una aplicación de 32 bits en un sistema operativo de 64 -3.5 GB según el sistema operativo.

+0

Sí, ya estamos probando este enfoque, pero ahora la prueba es simplemente binaria: ¿obtenemos StackOverflowError o no? Me gustaría una comprensión más detallada del uso real de la pila de la aplicación. Buen punto sobre el tamaño del montón, lo había olvidado. Sí, 64 bits está en los planes a largo plazo, pero la aplicación tiene dependencias nativas restantes de 32 bits que necesitan trabajo. –

+2

+1 Aunque este es un enfoque de prueba y error, hará el trabajo. Es una pena 'jvisualvm' no ofrece información como esta. –

+1

Aunque tenga código nativo de 32 bits, un sistema operativo de 64 bits le proporcionará más memoria incluso con una JVM de 32 bits. –

3

No hay herramientas fácilmente utilizables en Java VM para consultar la profundidad de la pila en bytes. Pero puedes llegar allí. Aquí hay algunos indicadores:

  • Las excepciones contienen matrices de marcos de pila que le proporcionan los métodos que se invocaron.

  • Para cada método, puede encontrar the Code attribute en el archivo .class. Este atributo contiene el tamaño del marco por método en el campo max_stack.

Así que lo que necesita es una herramienta que compila una HashMap que contiene el nombre del método + nombre de archivo + número de línea como claves y el valor max_stack como valores. Cree un Throwable, busque los cuadros de pila desde él con getStackTrace() y luego itere sobre el StackTraceElement s.

Nota:

Cada entrada en la pila de operandos puede contener un valor de cualquier tipo de máquina virtual de Java, incluyendo un valor de tipo long o escriba doble.

Así que cada entrada de la pila es probablemente de 64 bits, por lo que necesita para multiplicarse max_stack con 8 para obtener bytes.

Cuestiones relacionadas