Pergunta

Estou construindo um servidor Java que precisa ser dimensionado.Um dos servlets servirá imagens armazenadas no Amazon S3.

Recentemente, sob carga, fiquei sem memória em minha VM e foi depois de adicionar o código para servir as imagens, então tenho certeza de que o streaming de respostas de servlets maiores está causando meus problemas.

Minha pergunta é :existe alguma prática recomendada sobre como codificar um servlet java para transmitir uma resposta grande (> 200k) de volta para um navegador quando lido de um banco de dados ou outro armazenamento em nuvem?

Considerei gravar o arquivo em uma unidade temporária local e, em seguida, gerar outro thread para lidar com o streaming, para que o thread do servlet do Tomcat possa ser reutilizado.Parece que seria muito pesado.

Qualquer pensamento seria apreciado.Obrigado.

Foi útil?

Solução

Quando possível, você não deve armazenar todo o conteúdo de um arquivo a ser servido na memória.Em vez disso, adquira um InputStream para os dados e copie os dados para o Servlet OutputStream em partes.Por exemplo:

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();

Eu concordo com Toby, você deveria "apontar para o URL do S3".

Quanto à exceção OOM, você tem certeza de que tem a ver com o fornecimento de dados de imagem?Digamos que sua JVM tenha 256 MB de memória "extra" para usar no fornecimento de dados de imagem.Com a ajuda do Google, “256 MB/200 KB” = 1310.Para 2 GB de memória "extra" (atualmente uma quantidade bastante razoável), mais de 10.000 clientes simultâneos poderiam ser suportados.Mesmo assim, 1.300 clientes simultâneos é um número bastante grande.Este é o tipo de carga que você experimentou?Caso contrário, talvez seja necessário procurar em outro lugar a causa da exceção OOM.

Editar - Em relação a:

Neste caso de uso, as imagens podem conter dados confidenciais...

Quando li a documentação do S3 há algumas semanas, percebi que você pode gerar chaves com prazo de validade que podem ser anexadas a URLs do S3.Assim, você não precisaria abrir os arquivos do S3 ao público.Minha compreensão da técnica é:

  1. A página HTML inicial possui links para download para seu webapp
  2. O usuário clica em um link de download
  3. Seu webapp gera um URL S3 que inclui uma chave que expira em, digamos, 5 minutos.
  4. Envie um redirecionamento HTTP para o cliente com o URL da etapa 3.
  5. O usuário baixa o arquivo do S3.Isso funciona mesmo que o download demore mais de 5 minutos - assim que o download for iniciado, ele poderá continuar até a conclusão.

Outras dicas

Por que você simplesmente não aponta para o URL do S3?Pegar um artefato do S3 e depois transmiti-lo através de seu próprio servidor para mim anula o propósito de usar o S3, que é descarregar a largura de banda e o processamento de servir as imagens para a Amazon.

Já vi muitos códigos como a resposta de john-vasilef (atualmente aceita), um loop while apertado lendo pedaços de um fluxo e gravando-os no outro fluxo.

O argumento que eu apresentaria é contra a duplicação desnecessária de código, a favor do uso do IOUtils do Apache.Se você já estiver usando em outro lugar, ou se outra biblioteca ou estrutura que você estiver usando já depender dela, é uma linha única que é conhecida e bem testada.

No código a seguir, estou transmitindo um objeto do Amazon S3 para o cliente em um 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 linhas de um padrão bem definido com fechamento de fluxo adequado parecem bastante sólidas.

Concordo plenamente com Toby e John Vasileff - S3 é ótimo para descarregar grandes objetos de mídia se você puder tolerar os problemas associados.(Uma instância do próprio aplicativo faz isso para FLVs e MP4s de 10 a 1000 MB.) Por exemplo:No entanto, não há solicitações parciais (cabeçalho de intervalo de bytes).É preciso lidar com isso 'manualmente', tempos de inatividade ocasionais, etc.

Se isso não for uma opção, o código de John parece bom.Descobri que um buffer de bytes de 2k FILEBUFFERSIZE é o mais eficiente em marcas de microbench.Outra opção pode ser um FileChannel compartilhado.(FileChannels são thread-safe.)

Dito isso, acrescentaria também que adivinhar o que causou um erro de falta de memória é um erro clássico de otimização.Você aumentaria suas chances de sucesso trabalhando com métricas rígidas.

  1. Coloque -XX:+HeapDumpOnOutOfMemoryError nos parâmetros de inicialização da JVM, apenas para garantir
  2. use jmap na JVM em execução (jmap -histo <pid>) sob carga
  3. Analise as métricas (jmap -histo out put ou faça com que você dê uma olhada no seu heap dump).Pode muito bem ser que a sua falta de memória venha de algum lugar inesperado.

É claro que existem outras ferramentas por aí, mas jmap e jhat vêm com Java 5+ 'pronto para uso'

Considerei gravar o arquivo em uma unidade temporária local e, em seguida, gerar outro thread para lidar com o streaming, para que o thread do servlet do Tomcat possa ser reutilizado.Parece que seria muito pesado.

Ah, não acho que você não possa fazer isso.E mesmo se você pudesse, parece duvidoso.O thread do Tomcat que está gerenciando a conexão precisa estar sob controle.Se você estiver enfrentando falta de threads, aumente o número de threads disponíveis em ./conf/server.xml.Novamente, as métricas são a forma de detectar isso – não apenas adivinhar.

Pergunta:Você também está executando no EC2?Quais são os parâmetros de inicialização da JVM do seu Tomcat?

toby está certo, você deveria apontar diretamente para S3, se puder.Caso contrário, a pergunta é um pouco vaga para dar uma resposta precisa:Qual é o tamanho do seu heap java?Quantos fluxos são abertos simultaneamente quando você fica sem memória?
Qual é o tamanho do seu buffer/leitura e gravação (8K é bom)?
Você está lendo 8K do stream e depois gravando 8k na saída, certo?Você não está tentando ler a imagem inteira do S3, armazená-la em buffer e depois enviar tudo de uma vez?

Se você usar buffers de 8K, poderá ter 1.000 fluxos simultâneos em aproximadamente 8Megs de espaço de heap, então definitivamente está fazendo algo errado....

Aliás, eu não escolhi 8K do nada, é o tamanho padrão para buffers de soquete, envie mais dados, digamos 1Meg, e você estará bloqueando a pilha tcp/ip que contém uma grande quantidade de memória.

Você tem que verificar duas coisas:

  • Você está fechando o stream?Muito importante
  • Talvez você esteja oferecendo conexões de streaming "de graça".O fluxo não é grande, mas muitos fluxos ao mesmo tempo podem roubar toda a sua memória.Crie um pool para que você não possa ter um determinado número de streams em execução ao mesmo tempo

Além do que John sugeriu, você deve liberar repetidamente o fluxo de saída.Dependendo do seu contêiner da web, é possível que ele armazene em cache partes ou até mesmo toda a sua saída e a libere de uma só vez (por exemplo, para calcular o cabeçalho Content-Length).Isso queimaria um pouco de memória.

Se você puder estruturar seus arquivos para que os arquivos estáticos fiquem separados e em seu próprio bucket, o desempenho mais rápido hoje provavelmente poderá ser alcançado usando o CDN do Amazon S3, CloudFront.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top