SQSのApproximateNumberOfMessagesVisibleの値が増えないことがある

概要

SQSのメッセージ数を監視する際にApproximateNumberOfMessagesVisibleというメトリクスを使用していたのですが、SQSにメッセージをプッシュしたにも拘らず値が増えないケースがあったため調査してみました。


現象

シチュエーション

「SQSにメッセージが1つ以上入ったらアクションを起こしたい」というシチュエーションでした。


S3へのソースコードのデプロイを検知して、自動でCloudFrontのキャッシュをクリアする」ということがしたかったのですが、ソースコードファイルは複数あり、ファイルごとにS3イベントでLambdaを起動してキャッシュクリアをしていては非常に無駄です。


なので、S3への複数ファイルアップロードをまとめて1つのトリガーにしたいという思いがあり、SQSを使ってみることにしました。

※デプロイをトリガーするための特定のファイル名を決めて単一のトリガーにしたり、LambdaとDynamoDBを使ってデプロイ履歴を管理するロジックを組めば簡単なのですが、さらに簡単にできないかを模索していました。


方法

「S3イベントでSQSを連携し、SQSのメッセージ数のメトリクスをトリガーにしてCloudWatch AlarmでSNS+Lambdaを起動し、CloudFrontのキャッシュをクリア(Invalidation API)する」方法になります。


前提

前提条件が2つありました。

  • SQSのメッセージをLambdaなどでポーリングする処理は特に必要ない
    • SQSにはメッセージをプッシュするだけで取得はしない
  • SQSの1メッセージに対して、SQSに入ったときの1回だけ(=送信メッセージ数)をカウントしたい
    • 1回のプッシュに何回もトリガーされてしまうと複数回キャッシュクリアが走ってしまうため
    • SQSキューにメッセージが残り続けても特にポーリングして処理しないので無駄、かつメトリクスによっては一定期間ごとにカウントされてしまうため、保持期限を設けてメッセージを削除するようにする


設定・条件

上記のソリューションの際に行った設定・条件などを記載します。

  • s3:ObjectCreated:*s3:ObjectRemoved:*のS3イベントを対象にSQSへ通知
  • SQSのメッセージ保持期間を「60秒」にして、メッセージが一定期間後に削除されるように
  • SQSのメッセージ数をカウントするためのメトリクスは、以下2種類でそれぞれ挙動を試してみた
    • ApproximateNumberOfMessagesVisible(表示可能なメッセージ数の近似値)
    • NumberOfMessagesSent(SQSに送信された数)


ApproximateNumberOfMessagesVisibleとNumberOfMessagesSentの違い

ざっくりいうと、ApproximateNumberOfMessagesVisibleがキューにあるメッセージ数(近似値)で、NumberOfMessagesSentが送信されたメッセージ数です。


ただし公式ドキュメントにて、NumberOfMessagesSentの場合は以下のように書いてあります。

処理の試行が失敗した結果としてデッドレターキューにメッセージが送信された場合、メトリクスによってキャプチャされません

docs.aws.amazon.com


これはSQSを、他のSQSのDLQ(デッドレターキュー)として設定したときは、SQSからのDLQとして移動されたメッセージに対してはNumberOfMessagesSentがインクリメントされないとのことです。

以下のような感じです。

  • S3やSNSからSQSにメッセージを投げるときは正常にカウントされる
    • SNSのDLQとしてSQSを指定する場合もカウントされる
  • SQSからSQSに(処理失敗時のDLQとして)送られる場合は、カウントされない


つまり、DLQとして使用するSQSのメッセージ数監視には、NumberOfMessagesSentは使えないということになります。


しかし今回は「S3->SQS」であり、NumberOfMessagesSentでも値がカウントされるので、いったんこちらのお話は考えないことにします。


※クラスメソッド様のDevelopers IOでこちらが詳しく書かれている記事があり大変参考になりました。

dev.classmethod.jp


結果

タイトル通りなのですが、ApproximateNumberOfMessagesVisibleだと値がインクリメントされない場合がありました

また、NumberOfMessagesSentでは正常に値が増えたことが確認できました。


原因推測

ちょっと謎だったのですが原因を考えてみることにしました。

そこで思いついたのが、MessageRetentionPeriod(メッセージ保持期限)により60秒でメッセージが削除されるようにしましたが、「AWS内部でのApproximateNumberOfMessagesVisibleがカウントされる評価タイミングの間にメッセージが消えてしまい、値としてカウントされないのでは?」と考えました。


困る

今回のように、SQSのメッセージを取らない(ポーリングしない)場合、キューに入ったことを通知だけできればすぐメッセージを消しても問題ない、むしろ早く消したいので、MessageRetentionPeriodを最小値の60にしたくなりますよね。。。


また保持期間を伸ばすと、キューに入ったときだけ・1回だけ通知したいのに、メッセージがキューに残り続けることで毎分ApproximateNumberOfMessagesVisibleにカウントされてしまうため、キャッシュクリアが複数回発火されてしまう可能性もあります。

そのためにCloudWatch Alarmの評価期間を伸ばすのも考えられますが、よりリアルタイムではなくなるし、評価期間の境目では結局2回カウントされてしまうし、あまりやりたくはありません。


検証

この挙動の調査をするがため、複数パターンで検証をしてみました。


リソース構成

今回検証したい部分だけに焦点を絞るため、CloudFrontやLambdaは省いて「S3 + SQS + CloudWatch Alarm」というリソースで検証をしました。

これらはCloudFormationで構築します。


検証内容

おおまかな内容

S3にファイルを追加・削除し、それをイベントとしてSQSに取り込み、NumberOfMessagesSentとApproximateNumberOfMessagesVisibleでそれぞれ発火する2種類のCloudWatch Alarmを用意し、どちらのアラームも発火されたかを確認・比較してみます。


詳しい内容

ApproximateNumberOfMessagesVisibleがカウントされる評価タイミングの間にメッセージが消えてしまうのが問題だとしたら、メッセージの保持期間を伸ばせば変わるだろうかと思い、メッセージ保持期間を少しずつ変えて実験してみました。

具体的には、SQSのメッセージ保持期間(MessageRetentionPeriod)を 60, 120, 180, 240 というように、4種類のSQSで実験してみました。


そしてこの4種類のSQSと連携する別々のS3に対して、スクリプトで一定期間の間にファイル追加・削除を5回ループ繰り返し、各メトリクスの値が増えたかどうかを確認します。

ここでは、前回のループのメッセージが次のループの処理の時に残っていないように、保持期間が最大値の240をベースにその前と後に1分ずつ幅を持たせた「360秒」を、ファイル追加・削除の間に置くことにしました。


こんな感じのシェルになります。つまり、10回メトリクスの値がインクリメントされていれば一つもロスしなかったということになります。

for i in $(seq 1 5); do
    aws s3 cp ${filename} s3://${bucketname}/
    sleep 360 # 最大値の240の前と後に1分ずつ幅を持たせる(ラグ用)

    aws s3 rm s3://${bucketname}/${filename}
    sleep 360 # 同じ値でsleep
done


CloudFormationのテンプレート(yml)ファイル、デプロイ用シェル、検証スクリプト用シェルは以下折り畳みで記載してあります。

GitHubにもあります。

github.com

template.yml

AWSTemplateFormatVersion: 2010-09-09
Description: ---
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Stack Name"
        Parameters:
          - StackName
    ParameterGroups:
      - Label:
          default: "MessageRetentionPeriod"
        Parameters:
          - MessageRetentionPeriod

Parameters:
  StackName:
    Type: String
  MessageRetentionPeriod:
    Type: Number

Resources: 
  EventSourceQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub "${StackName}-Queue"
      MessageRetentionPeriod: !Ref MessageRetentionPeriod #sec

  EventSourceQueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues:
        - !Ref EventSourceQueue
      PolicyDocument:
        Statement:
          - Action:
              - "SQS:*"
            Effect: "Allow"
            Resource: !GetAtt EventSourceQueue.Arn
            Principal:
              Service: "s3.amazonaws.com"

  Bucket:
    Type: AWS::S3::Bucket
    DependsOn:
      - EventSourceQueuePolicy
    Properties:
      BucketName: !Sub "${StackName}-bucket"
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      NotificationConfiguration:
        QueueConfigurations:
          - Event: s3:ObjectCreated:*
            Queue: !GetAtt EventSourceQueue.Arn
          - Event: s3:ObjectRemoved:*
            Queue: !GetAtt EventSourceQueue.Arn

  QueueSizeAlarmByNumberOfMessagesSent:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${StackName}-QueueSizeAlarmByNumberOfMessagesSent"
      Namespace: AWS/SQS
      Dimensions:
        - Name: QueueName
          Value: !GetAtt EventSourceQueue.QueueName
      MetricName: NumberOfMessagesSent
      Period: 60
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      DatapointsToAlarm: 1
      Statistic: "Sum"
      Threshold: 1
      TreatMissingData: notBreaching

  QueueSizeAlarmByApproximateNumberOfMessagesVisible:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${StackName}-QueueSizeAlarmByApproximateNumberOfMessagesVisible"
      Namespace: AWS/SQS
      Dimensions:
        - Name: QueueName
          Value: !GetAtt EventSourceQueue.QueueName
      MetricName: ApproximateNumberOfMessagesVisible
      Period: 60
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      DatapointsToAlarm: 1
      Statistic: "Sum"
      Threshold: 1
      TreatMissingData: notBreaching

deploy.sh

cd $(dirname $0)

MessageRetentionPeriod="60"
# MessageRetentionPeriod="120"
# MessageRetentionPeriod="180"
# MessageRetentionPeriod="240"

CFN_TEMPLATE="./template.yml"
STACK_NAME="sqs-queue-size-per-${MessageRetentionPeriod}-period"

aws cloudformation deploy \
    --stack-name ${STACK_NAME} \
    --template-file ${CFN_TEMPLATE} \
    --no-fail-on-empty-changeset \
    --parameter-overrides \
    StackName=${STACK_NAME} \
    MessageRetentionPeriod=${MessageRetentionPeriod}

script.sh

MessageRetentionPeriod="60"
# MessageRetentionPeriod="120"
# MessageRetentionPeriod="180"
# MessageRetentionPeriod="240"

STACK_NAME="sqs-queue-size-per-${MessageRetentionPeriod}-period"

filename="sample.txt"
bucketname="${STACK_NAME}-bucket"

touch ${filename} && echo "aaa" > ${filename}

for i in $(seq 1 5); do
    aws s3 cp ${filename} s3://${bucketname}/
    sleep 360 # 最大値の240の前と後に1分ずつ幅を持たせる(ラグ用)

    aws s3 rm s3://${bucketname}/${filename}
    sleep 360 # 同じ値でsleep
done


結果

まとめ

NumberOfMessagesSentでは全パターンで値がインクリメントされ(ロスなし)、ApproximateNumberOfMessagesVisibleでは一部のパターンで値がインクリメントされませんでした


補足

上記では10回の値のインクリメントがあるかどうかを指標にしましたが、CloudFormationスタックデプロイ時、つまりSQSやS3を作成時に、s3:TestEventというイベントが走ってSQSに2つメッセージが入ったため、その分も検証・実験の指標に加えることにしました。


比較結果

表にすると以下のようになります。

※インクリメント数(TestEvent時)は2回カウントできていればロスなし、インクリメント数(メインテスト時)は10回カウントできていればロスなしです。

保持期間 メトリクス インクリメント数(TestEvent時) インクリメント数(メインテスト時)
60 ApproximateNumberOfMessagesVisible 0 / 2 7 / 10
60 NumberOfMessagesSent 2 / 2 10 / 10
120 ApproximateNumberOfMessagesVisible 0 / 2 10 / 10
120 NumberOfMessagesSent 2 / 2 10 / 10
180 ApproximateNumberOfMessagesVisible 2 / 2 10 / 10
180 NumberOfMessagesSent 2 / 2 10 / 10
240 ApproximateNumberOfMessagesVisible 0 / 2 10 / 10
240 NumberOfMessagesSent 2 / 2 10 / 10


つまり、以下のような結果が出ました。

  • NumberOfMessagesSent
    • TestEvent : 全てロスなし
    • メインテスト : 全てロスなし
  • ApproximateNumberOfMessagesVisible
    • TestEvent : 保持期間180のときだけロスなし
    • メインテスト : 保持期間60のときだけロスあり


以下結果をグラフとして載せました。

60-ApproximateNumberOfMessagesVisible

60-NumberOfMessagesSent

120-ApproximateNumberOfMessagesVisible

120-NumberOfMessagesSent

180-ApproximateNumberOfMessagesVisible

180-NumberOfMessagesSent

240-ApproximateNumberOfMessagesVisible

240-NumberOfMessagesSent


原因

もともとの「ApproximateNumberOfMessagesVisibleがカウントされる評価タイミングの間にメッセージが消えてしまう」という推測ですが、60秒以外では全て値がインクリメントされているので正しそうにも見えましたが、TestEventでは120, 240でもロスが起きていたので、ちょっとわかりませんでした・・・


一つ言えるのは、ApproximateNumberOfMessagesVisibleでは値がインクリメントされないことがあるということだけはわかりました。

Approximate=近似というように、正確性は欠けるということなのですかね。

一度に何十・何百ものメッセージがSQSにプッシュされる場合は誤差になるので問題ないとは思うのですが、少ない数のプッシュでも検知したい場合は問題になりそうです。


結論

今回このような検証をして、実際にSQSのメッセージ数を監視する際、以下のことがわかりました。


ApproximateNumberOfMessagesVisibleが向いているケース

  • SQSの失敗時のDLQとして使う場合
    • そもそもNumberOfMessagesSentがインクリメントされないため
  • 一度にたくさんのメッセージがプッシュされ、多少の数値の誤差は気にならない場合
  • 1メッセージが複数タイミングでカウントされても良い場合(=メッセージを処理しない限りメッセージ数としてカウントしておきたい場合)
    • 保持期間を60秒より大きくしておくとロスの可能性が低くなるが、1メッセージが残り続けて何度もメトリクスでカウントされることがあるため
    • メッセージ数監視としては、これが普通の要件だと思います
  • メッセージ投入を検知するのが少し遅くなってもいい場合
    • 保持期間を伸ばした上で、複数回アラームが発火しないようにアラームの評価期間も伸ばすとき


NumberOfMessagesSentが向いているケース

  • メッセージの保持期間をなるべく短く(60秒に)したい場合
  • 1メッセージにつき1回だけ値をカウントしたい場合
    • = 送信数(名前の通りですね)
  • メッセージ投入を一早く検知したいとき
    • 上記で貼り付けたグラフをよ〜く見ればわかるのですが、NumberOfMessagesSentの方がApproximateNumberOfMessagesVisibleよりタイミングが早くインクリメントされています


今回のようなS3イベントでSQSと連携する、かつ送信されたメッセージ数をカウントしたいような(デプロイ検知のような)場合は、NumberOfMessagesSentが向いていることがわかりました。


逆に、普通にSQSのメッセージ数を監視したい、処理されていない数を監視したいような「一般的な場合」は、ApproximateNumberOfMessagesVisibleが良いかと思われます。


最後に

ちょっと特殊な状況で、かつ原因も究明できませんでしたが、ApproximateNumberOfMessagesVisibleが向かない場合・NumberOfMessagesSentがいい場合もあるということがわかってよかったです。

(単に、キューのメッセージ数なのか、キューに送信されたメッセージ数なのかという、メトリクス名の通りのユースケースに合わせるということですね)