こまぶろ

技術のこととか仕事のこととか。

Connpass APIをLambdaから扱う〜Lambdaの非同期呼び出しによる分散処理〜

Connpass APIを用いたボットの作成

先日、ConnpassのAPIをNode.jsから実行する方法について記事を書きました。

ky-yk-d.hatenablog.com

上の記事の末尾にも記載した通り、現在はConnpassのイベント新着情報をSlackに通知するボットを作成しています。

github.com

実装の方針としては、

  • 定期実行が必要
  • 1回1回はAPI呼び出し+通知だけ

ということで、AWS LambdaをCloudWatch Eventsを用いてスケジュール起動することを考えました。CloudWatch Eventsを使うのは初めてですが、LambdaでAPIコールを行うのは今までも何度もしてきたので、そこまで苦労はないと考えていました。

docs.aws.amazon.com

直面した問題

しかし、いざ開発を始めてみると、Lambda側で少々工夫が必要であることに気づきました。それは、以下のような問題に直面したからです。

  • 1個あたりの検索結果が増えるとConnpassのAPIでエラーになる
  • 検索結果の総量が増えるとLambdaのタイムアウトにひっかかる

これらについて順に説明します。

問題①:1個あたりの検索結果が増えるとConnpassのAPIでエラーになる

ConnpassのAPIでは、様々な検索条件をクエリ文字列に付加することができます。その中の一つとして、文字列で検索ワードのAND条件とOR条件を指定することができるため、ウォッチ対象のテーマをOR条件に付加していけば、1本のリクエストで複数のテーマのイベント情報を取得することができます。検索ワードごとに別々のリクエストを発行する必要はありません。

たた、あまりに多くの検索ワードをOR条件として付加してしまうと、膨大な数のイベント情報が取得されてしま うからか、適切にレスポンスが取得できずにエラーとなってしまいます。

問題②:検索結果の総量が増えるとLambdaのタイムアウトにひっかかる

ConnpassのAPIの問題以外に、Lambda側の問題もあります。検索結果の件数が総量として増えれば、それだけ処理にかかかる時間は増加します。そうなると、Lambdaのタイムアウト時間に抵触する恐れが出てきます。実際、タイムアウト時間を10秒に設定した場合には、エラーが発生してしまっていました。

考えた解決策

起動部のLambdaから本体のLambdaを複数回&非同期で呼び出す

以上のことから、次のような解決策を考えました。

  • 1回のリクエストで対象とする検索ワードを限定する(ConnpassのAPIでのエラー対策)
  • 1回のLambdaの実行ではタイムアウトしない程度の処理のみを行う(Lambdaのタイムアウト対策)
  • 上のLambda(本体)を呼び出すLambda(起動部)を作成し、そこで検索ワードを処理させる
  • 起動部から本体を非同期で呼び出す(同期呼び出しすると起動部がタイムアウトする)

図にすると以下のような形。

f:id:ky_yk_d:20180805193703p:plain

実装の一部

const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();
// 中略
ArrayOfArrayOfWords.forEach((element,index)=>{  // 「『検索ワードの配列』の配列」を順に処理
      queries.keyword_or = element;
      const params = {
        FunctionName: targetLambdaArn,
        InvocationType: 'Event', // 非同期呼び出し。即座にステータスコード202で返ってくる
        Payload: JSON.stringify(queries)
      };
      lambda.invoke(params).promise().then(
        (res)=>{
          console.log(index,res);
        },
        (error)=>{
          console.log('Error:', index, error);
        });
    });

LambdaのInvocationTypeについて

ポイントは、Lambdaの呼び出しタイプとして'Event'を指定していることです。起動部と本体を切り分けたところで、本体を起動部が同期呼び出ししてしまうと、結局起動部がタイムアウトになってしまいます。

そこで、Lamdabの呼び出しタイプを指定します。パラメータでInvocationType: 'Event'と指定すると、Lambdaは非同期的呼び出しとなります。つまり、呼び出された側(本体)の処理が終わるのを待たずに、呼び出した側(起動部)は次の処理に進みます。

dev.classmethod.jp

このようにすることで、呼び出し側(起動部)は必要な回数だけ本体を呼び出し、本来の目的であるConnpassのAPIとの通信〜通知を本体に委ねて自分自身は処理を終えて終了します。したがって、API通信に時間がかかってもタイムアウトとなる心配はありません。

ベストプラクティスは何なのか?

以上のような構成を採ることで、ConnpassのAPIの制限とLambdaのタイムアウトを回避することができました。しかし、この方法がベストプラクティスなのかどうかは疑問です。というのも、Lambdaを複数回実行するようにしたため課金的に不利になっているからです。そしてあまりエレガントに見えない。

サーバーレスで並列・分散処理というのはよく目にし、Lambda以外にもSNSやらSQSやら何やらと使えそうなものがたくさんありません。しかし、どうにもよくわかっていないというのが正直なところです。調べている過程で、StepFunctionsを使っている例にも接しましたが、いまいちピンときていません。

qiita.com

今回の例に限らず、AWSのサービスには少しずつ親しんできていますが、本格的な業務の構成を実際に触ったことがないので、どういう構成が良いもの(少なくとも、業務利用に耐えるもの)であるのかがわからないでいます。CloudDesignPatternというのもありますが、うーん、という感じ。有識者の方からのコメントをお待ちしています。。