2008-09-11 9 views
36

Estoy creando un servidor de Java que necesita escalar. Uno de los servlets servirá imágenes almacenadas en Amazon S3.Transmisión de archivos de gran tamaño en un servlet de Java

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 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 de disco local y luego generar otro hilo para manejar la transmisión de manera que el hilo del servlet de tomcat se pueda reutilizar. Parece que sería demasiado pesado.

Cualquier pensamiento sería apreciado. Gracias.

Respuesta

47

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 la 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, el uso de las imágenes pueden contener datos sensibles ...

Cuando leo a través de la documentación de S3 hace unas semanas, me di cuenta de que pueda generar claves con vencimiento de tiempo que se pueden adjuntar a las URL 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 siguiente:

  1. página HTML inicial tiene enlaces de descarga a su webapp
  2. usuario hace clic en un enlace de descarga
  3. Su aplicación web genera una URL de S3 que incluye una clave que expira en, digamos , 5 minutos.
  4. Envía una redirección HTTP al cliente con la URL del paso 3.
  5. 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.
+0

Hmm, dado que no se establece una longitud de contenido, el contenedor de servlets debe almacenar en búfer porque necesita establecer el encabezado de longitud del contenido antes de que pueda transmitir cualquier dato. Entonces, ¿no estás seguro de la cantidad de memoria que guardas? –

+1

Peter, si no puede dirigir a los usuarios directamente a una URL de servicio en la nube y desea establecer el encabezado de longitud de contenido, y aún no conoce el tamaño, y no puede consultar el tamaño del servicio en la nube, entonces supongo La mejor opción es transmitir primero a un archivo temporal en el servidor. Por supuesto, guardar una copia en el servidor antes de enviar el primer byte al cliente puede hacer que el usuario piense que la solicitud falló en función de la duración de la transferencia cloud -> server. –

+0

@PeterKriens el encabezado content-length no es obligatorio. también, puede usar transferencias fragmentadas donde solo necesita especificar la longitud de un fragmento. – bluesmoon

17

¿Por qué simplemente no 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.

0

Usted tiene que comprobar dos cosas:

  • ¿Está el cierre de la corriente? Muy importante
  • Quizás esté 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
1

toby tiene razón, debe apuntar directamente a S3, si puede. 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?
¿Cuán 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 utiliza 8K tampones, usted podría tener 1.000 transmisiones simultáneas de entrar ~ 8Megs de espacio de almacenamiento dinámico, por lo que son sin duda haciendo algo mal ....

Por cierto, yo 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 en la pila tcp/ip que contiene una gran cantidad de memoria.

0

Además de lo que sugirió John, debe enjuagar varias veces la secuencia 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.

2

Estoy totalmente de acuerdo tanto con Toby como con John Vasileff: S3 es ideal para descargar objetos multimedia grandes si puede tolerar los problemas asociados. (Una instancia de la propia aplicación hace eso para 10-1000MB FLV y MP4.) P. ej .: Sin solicitudes parciales (encabezado de rango de bytes), sin embargo. Uno tiene que manejar ese 'manual', 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 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.

  1. Lugar -XX: + HeapDumpOnOutOfMemoryError en ustedes parámetros de inicio de JVM, por si acaso
  2. tomar el uso jmap en la JVM en ejecución (jmap -histo <pid>) bajo carga
  3. analyize 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 por ahí, pero jmap & jhat vienen con Java 5+ 'fuera de la caja'

He pensado en escribir el archivo en una unidad temporal local y luego generando 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?

0

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 sea posible utilizando el CDN de Amazon S3, CloudFront.

10

He visto un montón de código como la respuesta de john-vasilef (actualmente aceptada), un ciclo apretado leyendo fragmentos de una secuencia y escribiéndolos en la otra secuencia.

El argumento que haré es contra la duplicación de código innecesario, a favor de utilizar 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 flujo adecuado parece bastante sólido.

+0

Estoy de acuerdo con el uso de lo que está disponible, pero su código tiene un problema: si 'response.getOutputStream()' genera una excepción, su objeto 'InputStream in' no se cerrará. También la característica 'try-with-resources' de Java 7 debería ser el patrón ahora, y el futuro CommonsIO tendrá [this] (http://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/ io/IOUtils.html # closeQuietly (java.io.Closeable ...)). Bonito sombrero :) – BonanzaOne

+0

@Evandro Buena captura-- ¿Qué tal esto? (Además, gracias :) –

+0

Agradable, el código es más seguro ahora = D. – BonanzaOne

Cuestiones relacionadas