La lección de la alta memoria fuera del montón causada por una compresión


No te pierdas de pasar

Haga clic en la palabra azul para seguirnos

1. Introducción al proyecto

lz_rec_push_kafka_consume
Este proyecto interactúa con el algoritmo a través de kafka y genera previamente el cuerpo del mensaje a través de la plataforma de recomendación push (lz_rec_push_platform).

2. Antecedentes del problema

Se encuentra que el contenedor k8s del proyecto se reiniciará. El tiempo de reinicio es solo la expansión push, y el volumen de datos push por hora se expande aproximadamente 5 veces.
Cuando ocurre un problema, la configuración del contenedor: CPU: 4, memoria: 3G dentro del montón, 1G fuera del montón.

3. Proceso de resolución de problemas: mirar-oler-preguntar-cortar

Esperanza: verifique el sistema de monitoreo y observe la situación de los recursos de la instancia del contenedor cuando ocurre el reinicio



Nota: Mecanismo de reinicio del contenedor: cuando la supervisión de k8s encuentra que el uso de memoria de la "instancia" excede la aplicación, el contenedor se reiniciará. Esta acción usa kill -9 directamente en lugar de reiniciar la máquina virtual a través de la instrucción jvm, así que no piense en descargar el montón aquí.




Al principio se sospechó que era la memoria, pero si la memoria es insuficiente, debería ser oom. Así que primero elimine el problema de la memoria insuficiente en el montón. Expanda la memoria de la instancia a: 6G, 5G en el montón, 1G fuera del montón. Descubrió que el fenómeno de reinicio no ha mejorado en absoluto.

Olor: compruebe el estado del proyecto: subprocesos, uso de memoria en pila, uso de memoria fuera de pila.

  1. A través de jstack y jstat, verifique el estado del hilo y el estado de la recolección de basura del proyecto, no hay un aumento repentino del hilo, no hay fullGC y youngGC frecuente.

  2. A través del comando top, se encuentra que el tamaño del montón mostrado por el comando res es mucho mayor que el del comando jstat (se olvidó de mantener la escena). En este momento, se sospecha que es causado por una fuga de memoria fuera del montón. Para determinar que la fuga está fuera del montón en lugar de dentro del montón, analice el archivo de registro de GC.

  • Analice el registro de GC con la ayuda de easygc: no hay una situación de GC completa (las cuatro GC completas de la figura se activan manualmente para probar: jmap -histo: live), y cada youngGC normalmente puede recuperar el objeto.

  • Modifique el script de inicio, establezca el parámetro -Xmx y el parámetro -Xms en 4G, y aumente el parámetro del montón de volcado (-XX: + HeapDumpOutOfMemoryError -XX: HeapDumpPath = / data / logs /), si oom ocurre en el montón, podemos pensar Análisis de los archivos del montón.
    Pero las cosas salieron contraproducentes: cuando el contenedor se reinició muchas veces, no había oom en el montón del proyecto, es decir, no había un vertedero. En este momento, es más seguro que debería ser una pérdida de memoria fuera del montón.

  • Configure el parámetro fuera del montón: -XX: MaxDirectMemorySize se usa para limitar el uso de la memoria fuera del montón, pero el uso de memoria de la instancia aún se amplía a 11G. Amigos en Internet dicen que este parámetro se puede usar para limitar el uso de la memoria fuera del montón, ¿es porque no lo estoy usando bien? Originalmente quería usar este parámetro para activar el error de escasez de memoria fuera del montón, a fin de verificar la dirección de la pérdida de memoria fuera del montón.
    Dado que esta dirección no es viable, expanda el exterior del montón para ver si la fuga fuera del montón se puede recuperar o si es una fuga permanente.

  • Las fugas de memoria fuera del montón generalmente son causadas por referencias de objetos en el montón (la más común es causada por NIO, pero esta vez NIO significa no tener la culpa), y las referencias en el montón no se pueden reciclar (supongo). A través de la cuarta figura, después de que el youngGC en condiciones naturales o después de que el fullGC se active manualmente, la recolección de basura puede probar el montón y volver al nivel normal. Aquí se juzga que la memoria filtrada es valorada por la referencia recuperable.
    Entonces viene el problema: esta parte de la referencia se ha acumulado mucho antes de la recolección de basura, lo que resulta en un espacio de memoria insuficiente fuera del montón, lo que provoca la eliminación del contenedor k8s. Supongo que verifica esta idea a continuación.

    • Deje que el jefe de operación y mantenimiento ajuste la instancia de k8s a 12G, porque el uso de memoria del contenedor es casi estable en aproximadamente 11g cada vez que se reinicia. (Bueno, de hecho, el jefe de operación y mantenimiento vio que el contenedor se estaba reiniciando y solicitó activamente una expansión para ayudar en la investigación, como una)

    • Limite la memoria en el montón a 7G y use 6G en el montón, dejando tanto espacio como sea posible fuera del montón.

  • Después del ajuste de la memoria de la instancia, las tres instancias del proyecto continuaron ejecutándose durante dos días sin reiniciarse y la memoria se puede recuperar normalmente después de cada "datos pregenerados". Por lo tanto, se determina que la memoria fuera del montón filtrada es reciclable, no se filtra permanentemente, y la recuperación puede completarse después de que se recuperen las referencias en el montón.

  • La imagen de arriba es el diagrama de monitoreo de recursos de la instancia de k8s, que solo puede reflejar la situación de recursos del contenedor, no la situación del montón del proyecto en el contenedor. Esta imagen solo puede demostrar que la memoria fuera del montón se puede recuperar normalmente, no con fugas permanentes. Ahora que el reinicio ya no es, el problema está solucionado, ¿y puedes irte? Ingenuo, un nodo de 12G, desperdicio innecesario, el jefe de operación y mantenimiento matará personas para sacrificarlas al cielo.
    Se puede observar a través del comando jstat, y el registro de GC se puede concluir que el uso de la memoria del montón se puede estabilizar básicamente dentro de 4G, y no hay necesidad de desperdiciar 12G de espacio.

  • Pregunta: El problema actual que debe resolverse es averiguar la causa de la pérdida de memoria fuera del montón.

    1. Encuentre artículos sobre la solución de problemas de memoria del montón a través de Google: hablemos sobre cómo descubrir el ERROR de las pérdidas de memoria fuera del montón de JVM. Un proceso de solución de problemas de las pérdidas de memoria fuera del montón

    2. Tomando prestadas las observaciones de Arthas, cuando la zona del Edén se expanda al 85% +, se realizará una ronda de youngGC. Así que mire el monitoreo y descargue el montón cuando el uso de Eden alcance el 80% (jmap -dump: format = b, file = heap.hprof).

    Cortar: Analice el archivo de montón a través de herramientas de análisis: JProfiler (se usará más adelante), MemoryAnalyzer

    1. Utilice la herramienta Memory Analyzer (MAT) para abrir el archivo de montón. El proceso de uso específico puede ser Baidu, no se detalla aquí.

    • Primero abre el archivo de pila


      • Después de ingresar, vi tres errores obvios en los resultados del análisis: el problema uno y el problema dos son causados ​​por la introducción de arthas, así que omítalo directamente.


      • Al ver si la tercera pregunta brilla, sabíamos qué hacía java.lang.ref.Finalizer cuando estábamos aprendiendo Java cuando éramos jóvenes. Si está interesado, puede buscarlo en Google o echar un vistazo: JVM finaliza el principio de implementación y esto Asesinato


      • java.lang.ref.Finalizer básicamente determina que hay un problema en la fase de reciclaje y entra en la búsqueda de objetos a reciclar. En este momento, no estamos enredados en cuántos objetos no se han reciclado y por qué no se reciclan. Es si estos objetos no recuperados se dirigen a la memoria del montón.


        • Haga clic en la instancia para ver la clase. Aquí puede ver que hay más de 3500 objetos no recuperados que apuntan a java.util.zip.ZipFile $ ZipFileInflaterInputStream. Google descubrió rápidamente que todavía hay muchos socios pequeños que enfrentan el mismo problema, como: flujo comprimido de Java GZIPStream La pérdida de memoria resultante.


        • Al ver ZipFileInflaterInputStream, recordé inmediatamente dónde se usa la compresión: los mensajes push se almacenan en redis después de la generación previa, y los mensajes se comprimen y almacenan después de la generación por lotes. Se usa la compresión zip. El ejemplo de código es el siguiente:
          Desafortunadamente, la herramienta de compresión usada en el proyecto Para la compresión zip que viene con jdk, los niños interesados ​​pueden aprender sobre la compresión zip basada en Deflater e Inflater. (El método de uso específico se refiere directamente a los comentarios de muestra sobre estas dos clases, que deberían ser el método de uso más autorizado). Lo siguiente es mi uso en el proyecto:

      
              byte[] input = log.getBytes();
      
              try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(input.length)) {
                  final Deflater compressor = new Deflater();
                  compressor.setInput(input);
                  compressor.finish();
      
                  byte[] buffer = new byte[1024];
                  int offset = 0;
                  for (int length = compressor.deflate(buffer, offset, buffer.length); length > 0; length = compressor.deflate(buffer, offset, buffer.length)) {
                      outputStream.write(buffer, 0, length);
                      outputStream.flush();
                  }
                  //compressor.end();
                  return Base64Utils.encodeToString(outputStream.toByteArray());
              }
          }
      
          public static String zipDecompress(final String str) throws Exception {
      
              byte[] input = Base64Utils.decodeFromString(str);
      
              try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(input.length)) {
      
                  final Inflater decompressor = new Inflater();
                  decompressor.setInput(input);
      
                  byte[] buffer = new byte[1024];
                  for (int length = decompressor.inflate(buffer); length > 0 || !decompressor.finished(); length = decompressor.inflate(buffer)) {
                      byteArrayOutputStream.write(buffer, 0, length);
                  }
                  //decompressor.end();
                  return new String(byteArrayOutputStream.toByteArray());
              }
          }
      
      
      1. Lo extraño es que la precompresión de compresión y descompresión está escrita en el formato de try with resource, y es razonable cerrar el stream. Algunos amigos en línea recomiendan usar snapy en lugar de zip, pero todavía no quiero saber por qué no hay recuperación de recursos inmediatamente después de que aparece la pila de métodos.

      2. Haga clic para ingresar el método de desinflado de Deflater o el método de inflado de Inflater, y puede encontrar que ambos llaman al método "nativo". Consulte el código fuente para obtener el código detallado. Ambas clases de herramientas tienen un método end (), que se anota de la siguiente manera:

      /**
           * Closes the compressor and discards any unprocessed input.
           * This method should be called when the compressor is no longer
           * being used, but will also be called automatically by the
           * finalize() method. Once this method is called, the behavior
           * of the Deflater object is undefined.
           */
      
      1. Entonces, en el código anterior, simplemente suelte la llamada al método end () en las dos líneas comentadas (las dos líneas se agregan después del problema de bloqueo). El método end () puede liberar la memoria utilizada fuera del montón después de la llamada, en lugar de esperar a que llegue la recolección de basura jvm, y luego recuperar indirectamente el búfer fuera del montón cuando se recicla la referencia. Continuando mirando el código fuente, no es difícil encontrar que Deflater e Inflater han reescrito el método finalize, y la implementación de este método es llamar al método end, que verifica nuestra conjetura anterior. Es bien sabido que se llamará al método finalize cuando el objeto se recicle y solo se llamará una vez. Por lo tanto, el espacio fuera del montón al que se hace referencia no se puede reclamar antes de que se reclame el objeto.

       /**
           * Closes the compressor and discards any unprocessed input.
           * This method should be called when the compressor is no longer
           * being used, but will also be called automatically by the
           * finalize() method. Once this method is called, the behavior
           * of the Deflater object is undefined.
           */
          public void end() {
              synchronized (zsRef) {
                  long addr = zsRef.address();
                  zsRef.clear();
                  if (addr != 0) {
                      end(addr);
                      buf = null;
                  }
              }
          }
      
          /**
           * Closes the compressor when garbage is collected.
           */
          protected void finalize() {
              end();
          }
      
      1. Mirando el espacio de almacenamiento de redis, bueno, incluso el período pico de datos no es mucho, creo que demasiado.

      Pensando: El reinicio del proyecto solo apareció después de que se expandieron los datos de Kafka, entonces, ¿por qué no apareció este problema antes de la expansión? De hecho, el problema siempre ha existido, pero cuando la cantidad de datos es pequeña, la memoria fuera del montón se puede liberar normalmente después de que la referencia se recolecta como basura. Sin embargo, después de que se expande el volumen, el tráfico instantáneo aumenta, lo que genera una gran cantidad de referencias al uso de memoria fuera del montón. Antes de la siguiente recolección de elementos no utilizados, la cola ReferenceQueue ha acumulado una gran cantidad de referencias, lo que aumenta la memoria del montón en el contenedor.

      Medicina: elimina las acciones de compresión y descompresión

      Después de eliminar las acciones de compresión y descompresión, libere la versión para su observación. El monitoreo de recursos de la instancia k8s del proyecto está en un rango razonable.




      Hasta ahora, se ha resuelto el problema de la memoria fuera del montón.

      Cinco, pensar y revisar

      Problema: cuando utilice recursos, mantenga el hábito de liberarlos a tiempo después de su uso. Este problema se debe al uso incorrecto de la compresión y debe considerarse un error de bajo nivel.

      Dado que era la primera vez que se solucionaban fugas de memoria fuera del montón, no existía una experiencia enriquecedora para bloquear los puntos problemáticos para solucionarlos rápidamente y no se hicieron desvíos. El artículo es un poco detallado, pero el objetivo principal es registrar el proceso de resolución de problemas. La primera vez que publiqué un blog, mis pensamientos al escribir estaban un poco desordenados, por favor, perdóname. Si hay alguna redacción incorrecta, espero señalarlo. Si tiene alguna buena sugerencia, espero darle algunos consejos.


      Maravillosas recomendaciones anteriores

      Resumen de las preguntas de la entrevista entre bastidores de Tencent, Ali y Didi (incluidas las respuestas)

      Entrevista: ¡Las preguntas de entrevista de varios subprocesos más completas de la historia!

      La última versión de Alibaba impulsa las preguntas de la entrevista de back-end de Java

      ¿JVM es difícil de aprender? Eso es porque no leíste este artículo en serio

      -FIN-

      Siga la cuenta pública de WeChat del autor: "JAVA Rotten Pigskin"

      Obtenga más información sobre el conocimiento de la arquitectura back-end de Java y el último libro de entrevistas

      Todo lo que pides es bonito, me lo tomo en serio.

    Supongo que te gusta

    Origin blog.csdn.net/yunzhaji3762/article/details/108878553
    Recomendado
    Clasificación