prácticos - Transmisión de archivos de gran tamaño en un servlet de Java
jsp ejemplos prácticos (8)
Estoy construyendo un servidor de Java que necesita escalar. Uno de los servlets servirá imágenes almacenadas en Amazon S3.
Recientemente bajo carga, me quedé sin memoria en mi máquina virtual y fue después de que agregué el código para servir las imágenes, así que estoy bastante seguro de que la transmisión de respuestas de servlets más grandes está causando mis problemas.
Mi pregunta es: ¿hay alguna mejor práctica sobre cómo codificar un servlet de Java para transmitir una respuesta grande (> 200k) a un navegador cuando se lee desde una base de datos u otro almacenamiento en la nube?
Consideré escribir el archivo en una unidad de disco local y luego generar otro hilo para manejar la transmisión, de manera que el hilo del servlet de tomcat pueda reutilizarse. Parece que sería demasiado pesado.
Cualquier pensamiento sería apreciado. Gracias.
¿Por qué no simplemente los apunta a la url S3? Tomar un artefacto de S3 y luego transmitirlo a través de tu propio servidor para mí derrota el propósito de usar S3, que es descargar el ancho de banda y el procesamiento de servir las imágenes a Amazon.
Además de lo que sugirió John, debe enjuagar varias veces el flujo de salida. Dependiendo de su contenedor web, es posible que almacene en caché partes o incluso todos sus resultados y los vacíe a la vez (por ejemplo, para calcular el encabezado Content-Length). Eso consumiría bastante memoria.
Cuando sea posible, no debe almacenar todo el contenido de un archivo para que se sirva en la memoria. En cambio, 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ía "señalarlos a la url S3".
En cuanto a la excepción OOM, ¿está seguro de que tiene que ver con el servicio de los 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, "256MB / 200KB" = 1310. Para 2GB de memoria "extra" (en estos días una cantidad muy razonable) más de 10,000 clientes simultáneos podrían ser compatibles. Aun así, 1300 clientes simultáneos es un número bastante grande. ¿Es este el tipo de carga que experimentaste? De lo contrario, es posible que deba buscar en otro lado la causa de la excepción OOM.
Editar - Con respecto a:
En este caso de uso, las imágenes pueden contener datos confidenciales ...
Cuando leí la documentación de S3 hace unas semanas, me di cuenta de que puede generar claves que expiran en el tiempo que se pueden adjuntar a las URL de S3. Por lo tanto, no tendrías que abrir los archivos en S3 al público. Mi comprensión de la técnica es:
- La página HTML inicial tiene enlaces de descarga a su aplicación web
- El usuario hace clic en un enlace de descarga
- Su aplicación web genera una URL S3 que incluye una clave que caduca, digamos, 5 minutos.
- Envíe un redireccionamiento HTTP al cliente con la URL del paso 3.
- El usuario descarga el archivo de S3. Esto funciona incluso si la descarga demora más de 5 minutos: una vez que comienza una descarga, puede continuar hasta completarse.
Estoy totalmente de acuerdo tanto con Toby como con John Vasileff: S3 es ideal para descargar objetos multimedia grandes si puedes tolerar los problemas asociados. (Una instancia de la propia aplicación hace eso para 10-1000MB FLV y MP4). Por ejemplo: Sin solicitudes parciales (encabezado de rango de bytes). Uno tiene que manejar ese ''manual'', tiempo de inactividad ocasional, etc.
Si eso 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 marcas de microbancos. Otra opción podría ser un FileChannel compartido. (FileChannels es seguro 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á sus posibilidades de éxito trabajando con métricas duras.
- Coloque -XX: + HeapDumpOnOutOfMemoryError en sus parámetros de inicio de JVM, por si acaso
- utiliza jmap en la JVM en ejecución ( jmap -histo <pid> ) bajo carga
- Analiza las métricas (jmap -histo out put, o haz que jhat mire tu volcado de heap). Es muy posible que tu falta de memoria provenga de algún lugar inesperado.
Por supuesto, hay otras herramientas, pero jmap y jhat vienen con Java 5+ ''fuera de la caja''
Consideré escribir el archivo en una unidad de disco local y luego generar otro hilo para manejar la transmisión, de manera que el hilo del servlet de tomcat pueda reutilizarse. Parece que sería demasiado pesado.
Ah, no creo que no puedas hacer eso. E incluso si pudieras, suena dudoso. El hilo de tomcat que está administrando la conexión necesita tener el control. Si está pasando hambre en el hilo, aumente la cantidad de hilos disponibles en ./conf/server.xml. De nuevo, las métricas son la forma de detectar esto, no adivinen.
Pregunta: ¿También se está ejecutando en EC2? ¿Cuáles son los parámetros de inicio de JVM de tu tomcat?
He visto un montón de código como la respuesta de john-vasilef (actualmente aceptada), un ciclo apretado en el que se leen fragmentos de una secuencia y los escriben en la otra secuencia.
El argumento que haré es contra la innecesaria duplicación 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á utilizando ya está dependiendo de él, es una sola línea conocida y probada.
En el siguiente código, estoy transmitiendo un objeto de 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 corriente adecuado parece bastante sólido.
Si puede estructurar sus archivos para que los archivos estáticos estén separados y en su propio contenedor, es probable que el rendimiento más rápido hoy en día se pueda lograr utilizando el CDN de Amazon S3, CloudFront .
Tienes que verificar dos cosas:
- ¿Estás cerrando la transmisión? Muy importante
- Tal vez estás dando conexiones de transmisión "gratis". La transmisión no es grande, pero muchas secuencias al mismo tiempo pueden robar toda tu memoria. Cree un grupo para que no pueda tener un cierto número de secuencias ejecutándose al mismo tiempo
Toby tiene razón, deberías estar apuntando directamente a S3, si puedes. Si no puede, la pregunta es un poco vaga para dar una respuesta precisa: ¿Qué tan grande es su montón de Java? ¿Cuántas secuencias están abiertas al mismo tiempo cuando se queda sin memoria?
¿Qué tan grande es su lectura / escritura (8K es bueno)?
Está leyendo 8K de la secuencia y luego escribe 8k en la salida, ¿verdad? ¿No estás intentando leer toda la imagen desde S3, almacenarla en la memoria y luego enviar todo de una vez?
Si usa búferes de 8K, podría tener 1000 corrientes simultáneas en ~ 8 Megas de espacio en el montón, por lo que definitivamente está haciendo algo mal ...
Por cierto, no elegí 8K de la nada, es el tamaño predeterminado para buffers de socket, envíe más datos, digamos 1Meg, y estará bloqueando en la pila tcp / ip que contiene una gran cantidad de memoria.