Частое исключение NoHttpResponseException с AmazonS3.getObject(запрос).getObjectContent()

У меня есть вспомогательная процедура, которая пытается загружать потоки с S3. Очень часто (около 1% запросов) я получаю сообщение журнала о NoHttpResponseException, которое через некоторое время вызывает SocketTimeoutException при чтении из S3ObjectInputStream.

Я делаю что-то не так, или это просто мой маршрутизатор/интернет? Или это можно ожидать от S3? Я не замечаю проблем в другом месте.

  public void
fastRead(final String key, Path path) throws StorageException 
    {
        final int pieceSize = 1<<20;
        final int threadCount = 8;

        try (FileChannel channel = (FileChannel) Files.newByteChannel( path, WRITE, CREATE, TRUNCATE_EXISTING ))
        {
            final long size = s3.getObjectMetadata(bucket, key).getContentLength();
            final long pieceCount = (size - 1) / pieceSize + 1;

            ThreadPool pool = new ThreadPool (threadCount);
            final AtomicInteger progress = new AtomicInteger();

            for(int i = 0; i < size; i += pieceSize)
            {
                final int start = i;
                final long end = Math.min(i + pieceSize, size);

                pool.submit(() ->
                {
                    boolean retry;
                    do
                    {
                        retry = false;
                        try
                        {
                            GetObjectRequest request = new GetObjectRequest(bucket, key);
                            request.setRange(start, end - 1);
                            S3Object piece = s3.getObject(request);
                            ByteBuffer buffer = ByteBuffer.allocate ((int)(end - start));
                            try(InputStream stream = piece.getObjectContent())
                            {
                                IOUtils.readFully( stream, buffer.array() );
                            }
                            channel.write( buffer, start );
                            double percent = (double) progress.incrementAndGet() / pieceCount * 100.0;
                            System.err.printf("%.1f%%\n", percent);
                        }
                        catch(java.net.SocketTimeoutException | java.net.SocketException e)
                        {
                            System.err.println("Read timed out. Retrying...");
                            retry = true;
                        }
                    }
                    while (retry);

                });
            }

            pool.<IOException>await();
        }
        catch(AmazonClientException | IOException | InterruptedException e)
        {
            throw new StorageException (e);
        }
    }

2014-05-28 08:49:58 INFO com.amazonaws.http.AmazonHttpClient executeHelper Unable to execute HTTP request: The target server failed to respond
org.apache.http.NoHttpResponseException: The target server failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:95)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:62)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:254)
at org.apache.http.impl.AbstractHttpClientConnection.receiveResponseHeader(AbstractHttpClientConnection.java:289)
at org.apache.http.impl.conn.DefaultClientConnection.receiveResponseHeader(DefaultClientConnection.java:252)
at org.apache.http.impl.conn.ManagedClientConnectionImpl.receiveResponseHeader(ManagedClientConnectionImpl.java:191)
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:300)
at com.amazonaws.http.protocol.SdkHttpRequestExecutor.doReceiveResponse(SdkHttpRequestExecutor.java:66)
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:127)
at org.apache.http.impl.client.DefaultRequestDirector.tryExecute(DefaultRequestDirector.java:713)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:518)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:906)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:805)
at com.amazonaws.http.AmazonHttpClient.executeHelper(AmazonHttpClient.java:385)
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:233)
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:3569)
at com.amazonaws.services.s3.AmazonS3Client.getObject(AmazonS3Client.java:1130)
at com.syncwords.files.S3Storage.lambda$fastRead$0(S3Storage.java:123)
at com.syncwords.files.S3Storage$$Lambda$3/1397088232.run(Unknown Source)
at net.almson.util.ThreadPool.lambda$submit$8(ThreadPool.java:61)
at net.almson.util.ThreadPool$$Lambda$4/1980698753.call(Unknown Source)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:744)

Ответ 1

ОБНОВЛЕНИЕ. В ответ на проблемы, которые я создал на GitHub, были обновлены SDK AWS. Я не уверен, как изменилась ситуация. Вторая часть этого ответа (критика getObject) скорее всего (надеюсь)?) Неправильно.


S3 предназначен для отказа, и он часто не работает.

К счастью, AWS SDK для Java имеет встроенные средства для повторных запросов. К сожалению, они не охватывают случай SocketExceptions при загрузке объектов S3 (они делают работают при загрузке и выполнении других операций). Таким образом, необходим код, аналогичный этому в вопросе (см. Ниже).

Когда механизм работает по своему желанию, вы все равно увидите сообщения в своем журнале. Вы можете скрыть их путем фильтрации событий INFO журнала событий с com.amazonaws.http.AmazonHttpClient. (AWS SDK использует журнал регистрации Apache.)

В зависимости от вашего сетевого подключения и работоспособности серверов Amazon механизм повтора может завершиться неудачей. Как указано lvlv, способ настройки соответствующих параметров осуществляется через ClientConfiguration. Параметр, который я предлагаю изменить, - это количество повторений, которое по умолчанию 3. Другие вещи, которые вы можете попробовать, - это увеличение или уменьшение времени ожидания подключения и сокета (по умолчанию 50 секунд, что не слишком долго, вероятно, слишком долго, учитывая тот факт, что вы часто будете таймаутом независимо от того, что) и используя TCP KeepAlive (по умолчанию выкл).

ClientConfiguration cc = new ClientConfiguration()
    .withMaxErrorRetry (10)
    .withConnectionTimeout (10_000)
    .withSocketTimeout (10_000)
    .withTcpKeepAlive (true);
AmazonS3 s3Client = new AmazonS3Client (credentials, cc);

Механизм повтора может быть даже превышен, установив a RetryPolicy (опять же, в ClientConfiguration). Его наиболее интересным элементом является RetryCondition, который по умолчанию:

проверяет различные условия в следующем порядке:

  • Повторить попытки исключения из AmazonClientException, вызванные IOException;
  • Повторить на исключениях AmazonServiceException, которые являются либо 500 внутренними серверами ошибки, 503 ошибки обслуживания недоступны, ошибки управления сервисом или ошибки перекоса часов.

См. SDKDefaultRetryCondition javadoc и источник

 

Половинные попытки повторной попытки скрыты в SDK

Что встроенный механизм (который используется во всем AWS SDK) не обрабатывает, это чтение данных объекта S3.

AmazonS3Client использует свой собственный механизм повтора, если вы вызываете AmazonS3.getObject (GetObjectRequest getObjectRequest, File destinationFile). Механизм находится внутри ServiceUtils.retryableDownloadS3ObjectToFile (source), который использует неоптимальное поведение проводного повторного использования (он будет только повторять один раз и никогда на SocketException!). Весь код в ServiceUtils кажется плохо спроектированным (issue).

Я использую код, похожий на:

  public void
read(String key, Path path) throws StorageException
    {
        GetObjectRequest request = new GetObjectRequest (bucket, key);

        for (int retries = 5; retries > 0; retries--) 
        try (S3Object s3Object = s3.getObject (request))
        {
            if (s3Object == null)
                return; // occurs if we set GetObjectRequest constraints that aren't satisfied

            try (OutputStream outputStream = Files.newOutputStream (path, WRITE, CREATE, TRUNCATE_EXISTING))
            {
                byte[] buffer = new byte [16_384];
                int bytesRead;
                while ((bytesRead = s3Object.getObjectContent().read (buffer)) > -1) {
                    outputStream.write (buffer, 0, bytesRead);
                }
            }
            catch (SocketException | SocketTimeoutException e)
            {
                // We retry exceptions that happen during the actual download
                // Errors that happen earlier are retried by AmazonHttpClient
                try { Thread.sleep (1000); } catch (InterruptedException i) { throw new StorageException (i); }
                log.log (Level.INFO, "Retrying...", e);
                continue;
            }
            catch (IOException e)
            {
                // There must have been a filesystem problem
                // We call `abort` to save bandwidth
                s3Object.getObjectContent().abort();
                throw new StorageException (e);
            }

            return; // Success
        }
        catch (AmazonClientException | IOException e)
        {
            // Either we couldn't connect to S3
            // or AmazonHttpClient ran out of retries
            // or s3Object.close() threw an exception
            throw new StorageException (e);
        }

        throw new StorageException ("Ran out of retries.");
    }

Ответ 2

Ранее у меня были подобные проблемы. Я нашел каждый раз после того, как вы закончили один S3Object, вам нужно закрыть(), чтобы освободить некоторый ресурс обратно в пул, в соответствии с официальным примером от AWS S3:

AmazonS3 s3Client = new AmazonS3Client(new ProfileCredentialsProvider());        
S3Object object = s3Client.getObject(
              new GetObjectRequest(bucketName, key));
InputStream objectData = object.getObjectContent();
// Process the objectData stream.
objectData.close();

Спасибо за добавление ссылки. BTW, я думаю, увеличение максимального соединения, повторение и тайм-аут ClientConfiguration (по умолчанию максимальное соединение равно 50) также может помочь решить проблему, вот так:

AmazonS3 s3Client = new AmazonS3Cient(aws_credential, 
                       new ClientConfiguration().withMaxConnections(100)
                                      .withConnectionTimeout(120 * 1000)
                                      .withMaxErrorRetry(15))