Article

📬 SQSのロングポーリングをJavaで使う:空レスポンスと無駄なAPI呼び出しを減らす

aws, sqs, java, backend

Amazon SQSのコンシューマーでは、メッセージがない状態でReceiveMessageを短い間隔で繰り返すと、空レスポンスが増えます。

この無駄を減らす仕組みが**ロングポーリング(Long Polling)**です。

ロングポーリングは、メッセージがない場合に即座に空レスポンスを返すのではなく、一定時間だけSQS側で待機してから結果を返します。

AWS公式ドキュメントでは、WaitTimeSecondsを0より大きくするとロングポーリングが有効になります。最大待機時間は20秒です。

ショートポーリングとの違い

WaitTimeSeconds = 0の場合はショートポーリングです。

ショートポーリング
Consumer -> ReceiveMessage -> SQS -> すぐ応答

ロングポーリングでは、メッセージが無ければ指定時間まで待ちます。

ロングポーリング
Consumer -> ReceiveMessage(waitTimeSeconds = 20) -> SQS
                                         ├─ メッセージ到着: すぐ返す
                                         └─ 到着しない: 最大20秒待つ

「20秒を指定したら必ず20秒待つ」わけではありません。待機中にメッセージが到着すれば、その時点でレスポンスが返ります。

Java SDK 2.xで実装する

SqsClientでは、ReceiveMessageRequestにwaitTimeSecondsを指定します。

import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;

public class SqsConsumer {
    private final SqsClient sqsClient;
    private final String queueUrl;

    public SqsConsumer(SqsClient sqsClient, String queueUrl) {
        this.sqsClient = sqsClient;
        this.queueUrl = queueUrl;
    }

    public void pollOnce() {
        ReceiveMessageResponse response = sqsClient.receiveMessage(
                ReceiveMessageRequest.builder()
                        .queueUrl(queueUrl)
                        .waitTimeSeconds(20)
                        .maxNumberOfMessages(10)
                        .build()
        );

        for (Message message : response.messages()) {
            process(message);
        }
    }

    private void process(Message message) {
        System.out.println(message.body());
    }
}

waitTimeSeconds(20)は、メッセージがない場合に最大20秒待つ指定です。

maxNumberOfMessages(10)は、1回のReceiveMessageで最大10件まで返してよいという指定です。SQS APIの上限も10件です。

ロングポーリングで何が改善するのか

AWS公式ドキュメントでは、ロングポーリングには主に次の効果があります。

  • メッセージが無いときの空レスポンスを減らす
  • 不要なReceiveMessage呼び出しを減らす
  • false empty responseを減らす

false empty responseとは、キューにメッセージがあるのに、そのReceiveMessageでは空レスポンスになる状態です。

ショートポーリングはSQS内部の一部サーバーをサンプリングして確認します。ロングポーリングではより広く確認するため、この状態を減らせます。

継続的なワーカーではループする

実際のコンシューマーでは、1回受信して終わりではなく継続的にポーリングします。

while (!Thread.currentThread().isInterrupted()) {
    ReceiveMessageResponse response = sqsClient.receiveMessage(
            ReceiveMessageRequest.builder()
                    .queueUrl(queueUrl)
                    .waitTimeSeconds(20)
                    .maxNumberOfMessages(10)
                    .build()
    );

    for (Message message : response.messages()) {
        process(message);
    }
}

ロングポーリング自体が待機するため、空レスポンス時に短いsleepを入れて高頻度再試行するような設計は基本的に不要です。

キュー単位でも設定できる

ロングポーリングはリクエスト単位だけでなく、キュー属性ReceiveMessageWaitTimeSecondsでも設定できます。

aws sqs set-queue-attributes \
  --queue-url "$QUEUE_URL" \
  --attributes ReceiveMessageWaitTimeSeconds=20

このコマンドの意味は次の通りです。

  • aws: AWS CLIを実行する
  • sqs: Amazon SQSの操作を選ぶ
  • set-queue-attributes: 既存キューの属性を変更する
  • --queue-url: 対象キューのURLを指定する
  • ReceiveMessageWaitTimeSeconds=20: デフォルトの受信待機時間を20秒にする

毎回同じ待機時間を使うならキュー側に設定し、用途ごとに変えたい場合はリクエスト側のwaitTimeSecondsを使うと整理しやすくなります。

HTTPタイムアウトを短くしすぎない

ロングポーリングでよくあるハマりどころがHTTPクライアント側のタイムアウトです。

例えばSQS側に20秒待つよう指定していても、HTTP側が10秒でタイムアウトする設定なら、SQSが正常に待機している途中でクライアント側が失敗します。

AWS公式ドキュメントでも、HTTPレスポンスタイムアウトはWaitTimeSecondsより長く設定するよう案内されています。

WaitTimeSeconds = 20秒
HTTP response/read timeout > 20秒

AWS SDK for Java 2.xでは、API call timeoutやHTTPクライアントのtimeoutを明示的に設定できます。ロングポーリングの待機時間だけを変更して、HTTP側の設定を確認し忘れないことが重要です。

Visibility Timeoutとは別物

ロングポーリングとVisibility Timeoutは役割が違います。

設定 役割
Long Polling メッセージを受信するまでどれだけ待つか
Visibility Timeout 受信後、同じメッセージを他のワーカーからどれだけ隠すか
ReceiveMessage開始
  ↓ Long Polling
メッセージ受信
  ↓ Visibility Timeout
処理完了・DeleteMessage

ロングポーリングを20秒にしても、処理可能時間が20秒になるわけではありません。処理時間に合わせて設計するのはVisibility Timeout側です。

複数キューを1本の処理で順番に待たない

複数キューを扱う場合にも注意が必要です。

Queue Aを最大20秒待つ
↓
Queue Bを最大20秒待つ
↓
Queue Cを最大20秒待つ

このように1つの処理で順番に待つと、Queue Aが空の間にQueue Bへ届いたメッセージの処理開始が遅れる可能性があります。

AWS公式のベストプラクティスでは、複数キューをロングポーリングする場合、キューごとに別スレッドを使う方法が案内されています。

Javaでは専用ワーカー、Executor、非同期クライアントなどを使い、各キューを独立して待てる構成にすると扱いやすくなります。

20秒固定が常に正解ではない

AWSは多くの場合20秒のロングポーリングを推奨していますが、アプリケーションの停止要求へ素早く反応したい場合や、ネットワーク・プロキシの制約がある場合は短い値を選ぶこともあります。

一方で待機時間を極端に短くすると、空レスポンスとAPI呼び出しが増え、ロングポーリングのメリットが小さくなります。

まとめ

  • WaitTimeSeconds > 0でロングポーリングになる
  • 最大待機時間は20秒
  • メッセージ到着時は待機時間の途中でも返る
  • 空レスポンスと不要なAPI呼び出しを減らせる
  • HTTPタイムアウトはWaitTimeSecondsより長くする
  • Long PollingとVisibility Timeoutは別の設定
  • 複数キューは独立して待てる構成にする

SQSワーカーが空のReceiveMessageを繰り返しているなら、まずロングポーリングの設定を確認すると改善しやすいです。

参考