AWS HTTP error: cURL error 56: TCP connection reset by peer

PHPでS3にバックアップファイルをUploadする時に、以下のエラーがでたので調査して、できればリトライを入れようと思います。

An exception occurred while uploading parts to a multipart upload. The following parts had errors:
- Part 4810: Error executing "UploadPart" on "https://backup.s3.ap-northeast-1.amazonaws.com/cms_20240311.tgz?partNumber=4810&uploadId=utWzapUi_ahxTc.WK1DEDy.qSNcS2P6xdbgQtqdQfi8lEb5oWy8w5AyyAv7Q0K7UiP69PWmGDOZHo3bUJpZr3tmiP06tVjnwGY9BkzAge3WkPmXi1qFF199pFrMwdMHBrVmlWNkKuGI632cRVfxhEw--"; AWS HTTP error: cURL error 56: TCP connection reset by peer (see https://curl.haxx.se/libcurl/c/libcurl-errors.html)  (server): 100 Continue -

ここで疑問に思ったのは、少なくてもAWS SDK for PHPを使っている限りは必要に応じてデフォルトでリトライをしてくれているはずです。
では、上記のようなエラーがでるということはリトライを使い果たしても失敗したのでしょうか? そもそも read: connection reset by peer って正確には何だ?という状態だったので調べました。

read: connection reset by peer とは

サーバ側から(今回だとKinesis Data Streamsのエンドポイントのサーバ)から RST(Reset TCP) パケット(正確言うとRSTフラグが1のパケット)が送られて来た時にハンドリングされたエラーメッセージです。これを送信された場合は、接続要求や通信状態が拒否されたものとみなし、通信をリセットして終了する必要があるとのことです。発生条件はサーバ側の処理能力を超えた場合などに発生しうるそうです。

発生箇所は色々考えられますが、 エラーメッセージに read tcp xxxx とある場合はリクエストを送信して、レスポンスを読み込もうとして(read tcpしようとして)発生したと推測できます。

つまり、今回のログで言うと Post "https://kinesis.ap-northeast-1.amazonaws.com/" のリクエストはサーバ側に届いたものの、レスポンスを受信するタイミングでTPCレイヤーで通信に失敗したと見なせると思います。

PHP側ではRSTパッケージを送られたかどうかは、エラーの文字列に connection reset by peer が含まれているかどうかでも分かります

AWS SDK for PHP側のリトライハンドリングについて

なにやらリトライはしないようになっている。

RetryMiddleware.php
    public function __invoke(RequestInterface $request, array $options): PromiseInterface
    {
        if (!isset($options['retries'])) {
            $options['retries'] = 0;
        }

        $fn = $this->nextHandler;

        return $fn($request, $options)
            ->then(
                $this->onFulfilled($request, $options),
                $this->onRejected($request, $options)
            );
    }

カスタムリトライの実装

AWS SDK for PHP バージョン 3 での Amazon S3 マルチパートアップロードの使用

オブジェクトアップローダー

PutObject または MultipartUploader がタスクに最適かどうかが不明な場合は、ObjectUploader を使用します。ObjectUploader は、ペイロードサイズに基づいてどれが最適かにより、PutObject または MultipartUploader を使用して大きなファイルを Amazon S3 にアップロードします。

require 'vendor/autoload.php';

use Aws\Exception\MultipartUploadException;
use Aws\S3\MultipartUploader;
use Aws\S3\ObjectUploader;
use Aws\S3\S3Client;
サンプルコード
// Create an S3Client.
$s3Client = new S3Client([
    'profile' => 'default',
    'region' => 'us-east-2',
    'version' => '2006-03-01'
]);

$bucket = 'your-bucket';
$key = 'my-file.zip';

// Use a stream instead of a file path.
$source = fopen('/path/to/large/file.zip', 'rb');

$uploader = new ObjectUploader(
    $s3Client,
    $bucket,
    $key,
    $source
);

do {
    try {
        $result = $uploader->upload();
        if ($result["@metadata"]["statusCode"] == '200') {
            print('<p>File successfully uploaded to ' . $result["ObjectURL"] . '.</p>');
        }
        print($result);
        // If the SDK chooses a multipart upload, try again if there is an exception.
        // Unlike PutObject calls, multipart upload calls are not automatically retried.
    } catch (MultipartUploadException $e) {
        rewind($source);
        $uploader = new MultipartUploader($s3Client, $source, [
            'state' => $e->getState(),
        ]);
    }
} while (!isset($result));

fclose($source);

MultipartUploader

マルチパートアップロードは、大容量オブジェクトのアップロードを効率よく行えるように設計されています。マルチパートアップロードでは、オブジェクトを分割して、別々に任意の順序で並行してアップロードできます。

Amazon S3 のユーザーには、100 MB を超えるオブジェクトに対してマルチパートアップロードを使用することをお勧めします。

インポート
require 'vendor/autoload.php';

use Aws\Exception\MultipartUploadException;
use Aws\S3\MultipartUploader;
use Aws\S3\S3Client;
サンプルコード
// Create an S3Client
$s3Client = new S3Client([
    'profile' => 'default',
    'region' => 'us-west-2',
    'version' => '2006-03-01'
]);

//Using stream instead of file path
$source = fopen('/path/to/large/file.zip', 'rb');
$uploader = new MultipartUploader($s3Client, $source, [
    'bucket' => 'your-bucket',
    'key' => 'my-file.zip',
]);

do {
    try {
        $result = $uploader->upload();
    } catch (MultipartUploadException $e) {
        rewind($source);
        $uploader = new MultipartUploader($s3Client, $source, [
            'state' => $e->getState(),
        ]);
    }
} while (!isset($result));
fclose($source);