Pregunta

Estoy construyendo un servidor Java que necesita escalar.Uno de los servlets servirá imágenes almacenadas en Amazon S3.

Recientemente, bajo carga, me quedé sin memoria en mi VM y fue después de agregar el código para mostrar las imágenes, así que estoy bastante seguro de que la transmisión de respuestas de servlet más grandes está causando mis problemas.

Mi pregunta es :¿Existe alguna práctica recomendada sobre cómo codificar un servlet Java para transmitir una respuesta grande (>200k) a un navegador cuando se lee desde una base de datos u otro almacenamiento en la nube?

He considerado escribir el archivo en una unidad temporal local y luego generar otro hilo para manejar la transmisión de modo que el hilo del servlet Tomcat pueda reutilizarse.Parece que sería muy pesado.

Cualquier idea sería apreciada.Gracias.

¿Fue útil?

Solución

Cuando sea posible, no debe almacenar todo el contenido de un archivo que se entregará en la memoria.En su lugar, adquiera un InputStream para los datos y copie los datos al Servlet OutputStream en partes.Por ejemplo:

ServletOutputStream out = response.getOutputStream();
InputStream in = [ code to get source input stream ];
String mimeType = [ code to get mimetype of data to be served ];
byte[] bytes = new byte[FILEBUFFERSIZE];
int bytesRead;

response.setContentType(mimeType);

while ((bytesRead = in.read(bytes)) != -1) {
    out.write(bytes, 0, bytesRead);
}

// do the following in a finally block:
in.close();
out.close();

Estoy de acuerdo con Toby, en su lugar deberías "señalarles la URL de S3".

En cuanto a la excepción OOM, ¿está seguro de que tiene que ver con la entrega de datos de imagen?Digamos que su JVM tiene 256 MB de memoria "extra" para usar para servir datos de imágenes.Con la ayuda de Google, "256 MB/200 KB" = 1310.Con 2 GB de memoria "extra" (hoy en día una cantidad muy razonable) se podrían admitir más de 10.000 clientes simultáneos.Aun así, 1300 clientes simultáneos es un número bastante grande.¿Es este el tipo de carga que experimentó?De lo contrario, es posible que deba buscar en otra parte la causa de la excepción OOM.

Editar - Respecto a:

En este caso de uso, las imágenes pueden contener datos confidenciales...

Cuando leí la documentación de S3 hace unas semanas, noté que se pueden generar claves con vencimiento temporal que se pueden adjuntar a las URL de S3.Por lo tanto, no tendría que abrir los archivos de S3 al público.Mi comprensión de la técnica es:

  1. La página HTML inicial tiene enlaces de descarga a su aplicación web
  2. El usuario hace clic en un enlace de descarga.
  3. Su aplicación web genera una URL S3 que incluye una clave que caduca en, digamos, 5 minutos.
  4. Envíe una redirección HTTP al cliente con la URL del paso 3.
  5. El usuario descarga el archivo desde S3.Esto funciona incluso si la descarga tarda más de 5 minutos; una vez que comienza la descarga, puede continuar hasta completarse.

Otros consejos

¿Por qué no les indicarías la URL de S3?Tomar un artefacto de S3 y luego transmitirlo a través de su propio servidor anula el propósito de usar S3, que es descargar el ancho de banda y el procesamiento de servir las imágenes a Amazon.

He visto una gran cantidad de código como la respuesta de john-vasilef (actualmente aceptada), un bucle while apretado que lee fragmentos de una secuencia y los escribe en la otra secuencia.

El argumento que yo presentaría es en contra de la duplicación innecesaria de código, a favor del uso de IOUtils de Apache.Si ya lo está usando en otro lugar, o si otra biblioteca o marco que está usando ya depende de él, es una línea única conocida y bien probada.

En el siguiente código, transmito un objeto desde Amazon S3 al cliente en un servlet.

import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;

InputStream in = null;
OutputStream out = null;

try {
    in = object.getObjectContent();
    out = response.getOutputStream();
    IOUtils.copy(in, out);
} finally {
    IOUtils.closeQuietly(in);
    IOUtils.closeQuietly(out);
}

6 líneas de un patrón bien definido con un cierre de flujo adecuado parecen bastante sólidas.

Estoy totalmente de acuerdo tanto con Toby como con John Vasileff: S3 es excelente para descargar objetos multimedia grandes si puede tolerar los problemas asociados.(Una instancia de su propia aplicación hace eso para archivos FLV y MP4 de 10 a 1000 MB). Por ejemplo:Sin embargo, no hay solicitudes parciales (encabezado de rango de bytes).Hay que manejar eso "manualmente", el tiempo de inactividad ocasional, etc.

Si esa no es una opción, el código de John se ve bien.Descubrí que un búfer de bytes de 2k FILEBUFFERSIZE es el más eficiente en microbench marks.Otra opción podría ser un FileChannel compartido.(Los FileChannels son seguros para subprocesos).

Dicho esto, también agregaría que adivinar qué causó un error de falta de memoria es un error de optimización clásico.Mejoraría sus posibilidades de éxito si trabajara con métricas estrictas.

  1. Coloque -XX:+HeapDumpOnOutOfMemoryError en los parámetros de inicio de su JVM, por si acaso
  2. utilice jmap en la JVM en ejecución (jmap -histo <pid>) bajo carga
  3. Analice las métricas (jmap -histo de salida, o haga que jhat mire su volcado de montón).Es muy posible que tu falta de memoria provenga de algún lugar inesperado.

Por supuesto, existen otras herramientas, pero jmap y jhat vienen con Java 5+ "listos para usar"

He considerado escribir el archivo en una unidad temporal local y luego generar otro hilo para manejar la transmisión de modo que el hilo del servlet Tomcat pueda reutilizarse.Parece que sería muy pesado.

Ah, no creo que no puedas hacer eso.E incluso si pudieras, suena dudoso.El subproceso Tomcat que gestiona la conexión debe tener el control.Si experimenta falta de subprocesos, aumente la cantidad de subprocesos disponibles en ./conf/server.xml.Una vez más, las métricas son la forma de detectar esto: no se limite a adivinar.

Pregunta:¿También estás ejecutando EC2?¿Cuáles son los parámetros de inicio de JVM de su Tomcat?

Toby tiene razón, deberías apuntar directamente a S3, si puedes.Si no puedes, la pregunta es un poco vaga para dar una respuesta precisa:¿Qué tamaño tiene tu montón de Java?¿Cuántas transmisiones hay abiertas simultáneamente cuando te quedas sin memoria?
¿Qué tamaño tiene su búfer de lectura/escritura (8K es bueno)?
Estás leyendo 8K de la transmisión y luego escribiendo 8k en la salida, ¿verdad?¿No estás intentando leer la imagen completa desde S3, almacenarla en la memoria intermedia y luego enviarla toda de una vez?

Si usa buffers de 8K, podría tener 1000 transmisiones simultáneas en ~8Megs de espacio de almacenamiento dinámico, por lo que definitivamente está haciendo algo mal...

Por cierto, no elegí 8K de la nada, es el tamaño predeterminado para los buffers de socket, envía más datos, digamos 1Meg, y estarás bloqueando la pila tcp/ip que contiene una gran cantidad de memoria.

Tienes que comprobar dos cosas:

  • ¿Estás cerrando la transmisión?Muy importante
  • Tal vez estés ofreciendo conexiones de transmisión "gratis".La transmisión no es grande, pero muchas transmisiones al mismo tiempo pueden robar toda la memoria.Cree un grupo para que no pueda tener una cierta cantidad de transmisiones ejecutándose al mismo tiempo

Además de lo que sugirió John, debes vaciar repetidamente el flujo de salida.Dependiendo de su contenedor web, es posible que almacene en caché partes o incluso toda la salida y la vacíe de una vez (por ejemplo, para calcular el encabezado Content-Length).Eso quemaría bastante memoria.

Si puede estructurar sus archivos de modo que los archivos estáticos estén separados y en su propio depósito, es probable que hoy en día se pueda lograr el rendimiento más rápido utilizando la CDN de Amazon S3. Frente a la nube.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top