[Amazon SQS] FIFOキューを使用したメール送信(VUIのバックエンドのレスポンスを高速化する)

1 はじめに

CX事業本部の平内(SIN)です。

Amazon Connectや、Alexaなど(※1)、VUIなシステムで利用されるLambdaでは、UXへの配慮のためか、タイムアウトが厳密に定義されており、どちらも、レスポンスが8秒以上かかると、エラーとして処理されてします。

このような制限の中で、活用されるのが、Amazon SQS(以下、SQS)です。SQSで、処理を階層(非同期)化して簡単にボトルネックを解消できます。また、フルマネージドなサービスであるSQSを挟むことで、耐障害性の向上も同時に図れます。

なお、昨年末より東京リージョンも利用可能になっている、FIFOキューでは、順番や、重複の制御も必要ないので、要件にもよりますが、軽易にキューを扱えると言えます。

今回は、VUIなバックエンドでメールを送ることを想定しFIFOキューで処理する要領を纏めます。

記事の内容は、単純なSQS(FIFO)の実装要領です。自分用の覚書であることをお許しください。

※1 Amazon Lexは、VUIとは限らない性質も有り、タイムアウト値(セッションタイムアウト)は、ボット作成時に作成者が分単位で自由に設定できます。

2 考慮すべき制限(要件)等

念のため、SQS(FIFO)を使用する場合に、考慮すべき制限(要件)等を列挙しておきます。
参考:Amazon Simple Queue Service とは

  • 処理速度は、バッチ処理でない場合、300件/秒 (1秒あたりの送信、受信、又は削除のオペレーション数)
  • グループIDが同じ場合、重複するメッセージは、自動的に削除される(削除されたくない場合は、メッセージごとに違うグループIDを付与する)
  • グループIDが同じ場合、順序が保たれる(順次処理したくない場合は、メッセージごとに違うグループIDを付与する)
  • メッセージの重複排除には、メッセージ重複排除IDを指定する
  • キュー作成時にコンテンツに基づく重複排除にチェックすることで、自動的にコンテンツハッシュ値(SHA256)が生成され、上記の、メッセージ重複削除IDは必要なくなる

  • シーケンス番号は、SQSによって各メッセージに割り当てられる連続番号
  • メッセージの削除は、可視性タイムアウト内に行う必要がある
  • メッセージサイズは、256KB以内

3 メール送信のLambda

サンプルに使用するのは、Twilioでショートメールを送るコードです。 メール送信の本体は、sendmail()です。Performance クラスは、単純に計測するためのものです。

import * as AWS from 'aws-sdk';
import { performance } from 'perf_hooks';

exports.handler = async (event: any) => {

    const phoneNumber = process.env.PHONE_NUMBER;
    const message = 'テストメッセージ';

    const performance = new Performance("メール送信"); // 計測開始
    const response = await sendmail(phoneNumber, message); // メール送信
    performance.measurement(); // 計測終了

    console.log(response);
}

async function sendmail(phoneNumber: string, message: string) {
    const account_sid = process.env.TWILIO_ACCOUNT_SID;
    const auth_token = process.env.TWILIO_AUTH_TOKEN;
    const from = process.env.TWILIO_NUMBER;

    const client = require('twilio')(
        account_sid,
        auth_token
    );
    return new Promise((resolve,reject) => {
        client.messages
        .create({
            body: message,
            from: from,
            to: phoneNumber
        })
        .then((result: any) => {
            resolve(result);
        })
        .catch( (err: any) => {
            reject(err);
        });
    })
}

export class Performance {
    startTime = 0;
    title: string ='';

    constructor(title:string){
        this.startTime = performance.now();
        this.title = title;
    }

    measurement(){
        const span = performance.now() - this.startTime;
        console.log(`[performance] ${this.title} ${(span/1000).toFixed(2)}sec`);
    }
}

sendmail()の前後で計測して、2.01secとなってました。(※ 処理時間は、条件によって大きく変化します)

[performance] メール送信 2.01sec

4 SQS

特別な設定無しで、FIFOのキュー(sample.fifo)を作成しました。

4 プロデューサー

sendmail()をコンシューマー側に移動し、プロデューサーになるようにsendmail()の内容をSQSへの送信に書き換えます。

async function sendmail(phoneNumber: string, message: string) {

  const body = {
    phoneNumber: phoneNumber,
    message: message
  }

  const account  = process.env.ACCOUNT;
  const region = 'ap-northeast-1';
  const queueName = 'sample.fifo';
  const url = `https://sqs.${region}.amazonaws.com/${account}/${queueName}`;
  const deduplicationId = Math.random().toString(32).substring(2); // 重複制御
  const groupId = 'sqs-fifo-sample'; // 同じグループとする

  const sqs = new AWS.SQS();
  const params: AWS.SQS.Types.SendMessageRequest = {
    MessageBody: JSON.stringify(body),
    MessageGroupId: groupId,
    MessageDeduplicationId: deduplicationId,
    QueueUrl: url,
  };
  return await sqs.sendMessage(params).promise();
}

Twilioへ送信をSQSへの送信に切り替えたことで,一応処理時間は短縮されています。

[performance] メール送信 0.37sec

5 コンシューマー

コンシューマー側は、以下のようになります。SQSから受信したキューの内容でメールを送信します。キューは、処理した時点で、削除しています。 可視性タイムアウトは、明示的に指定していないので、デフォルトの30秒となっています。

import * as AWS from 'aws-sdk';

exports.handler= async (event: any) => {

  const sqs = new AWS.SQS();
  const account  = process.env.ACCOUNT;
  const region = 'ap-northeast-1';
  const queueName = 'sample.fifo';
  const url = `https://sqs.${region}.amazonaws.com/${account}/${queueName}`;

  var params: AWS.SQS.ReceiveMessageRequest = {
    MaxNumberOfMessages: 10,
    QueueUrl: url,
    WaitTimeSeconds: 0
   };

   await sqs.receiveMessage(params, (err, data) => {
    if (err) {
      console.log("Receive Error", err);
    } else if (data.Messages) {
      for(var i=0; i<data.Messages.length; i++) {
        const message = data.Messages[i];
        const body:{phoneNumber: string, message: string} = JSON.parse(message.Body!)
        // メール送信
        await sendmail(body.phoneNumber, body.message)
        var deleteParams = {
          QueueUrl: url,
          ReceiptHandle: data.Messages[i].ReceiptHandle!
        };
        sqs.deleteMessage(deleteParams, function(err, data) {
          if (err) {
            console.log("Delete Error", err);
          } else {
            console.log("Message Deleted", data);
          }
        });
      }
    }
  });
}

6 トリガー

標準キューを使用する場合、キューへのメッセージの到着をトリガーとしてコンシューマーを起動できたのですが、FIFOでは、それが出来ません。このため、コンシューマー起動する方法は、別途設定する必要があります。

方法としては、例えば、CloudWatch Eventsで定期的に実行したり、CloudWatch Logsのフィルターパターンを設定して、プロデューサーのログ出力からキックする方法があると思います。

(1) スケジュール

(2) フィルタ−によるトリガー

7 最後に

今回は、SQS(FIFO)の利用について纏めてみました。

最初に書いたとおり、VUIのバックエンドでは、比較的早いレスポンスが求められます。SQSの利用は、処理時間を高速化する手法として、非常に有効だと思います。また、メッセージの重複制御などが必要ないFIFOは、要件によっては非常に有用かも知れません。