プログラミング

AWS LambdaでFirebase Cloud Messaging(Push通知)を非同期的に実行する

eyecatch-aws

サービスを開発し運用していくと、様々な問題にぶつかります。サーバーの処理性能もその一つです。

開発・テスト段階では、データ量が少なく問題なかったが、運用し始めてデータ量が増えてくると処理が遅くなったり。有名所で言えば、「N+1」問題などです。「N+1」に関してはまた気が向いた時に記事しようと思います。

今回は、FCMのPush通知のレスポンスが遅くなってしまったことによって様々な問題が発生していた状況であったのを、AWS Lambdaを使用して改善したので、その知識経験をシェアします。

AWS Lambda実装前の問題点

問題となった機能を簡単に説明すると、現在運用しているアプリではFacebookやTwitter・Instagramのような「いいね」機能があります。この「いいね」ボタンをユーザーがタップするたびにAPIを飛ばし、サーバー内で「いいね」・「通知」テーブルのデータ作成・FirebaseへのPush通知のAPIを実行、それらがTransactionで完了したら、ようやくユーザーにサクセスレスポンスを返す、という処理の流れでした。

しかし、この「いいね」機能は普通のSNSと違って、時間が経てば何度でも同じ対象に対して「いいね」が出来る仕様です。なので、処理数が非常に多い、という状況でした。

Firebaseサーバーとの通信には思っていたよりも多くの時間がかかるため、処理自体は軽いのですが、APIサーバーのスレッドをこの「いいね」機能で大部分を専有してしまっていました。

結果として、スレッドがいっぱいになる状態がずっと続き、APIサーバーがその他の処理を返せなくなり、5xx系エラーを頻発する状態が時々発生していました。

ちなみに、サーバー監視は、以下の2点を主に使用しています。

  • New RelicのAPMを用いてエンドポイント毎のスループットを確認
  • Elastic BeanstalkのSNS(Short Notification Service?)通知によりヘルスの悪化を何度も確認

解決方法及び実装概要

この問題の解決方法は、ユーザーが「いいね」を実行した際に、「いいね」用の通知データを作成し、cronのような定期実行で非同期的にpush通知を実行、レスポンス自体はすぐにユーザーに返す、というアーキテクチャで実装することにしました。

ユーザーへのレスポンスをすぐに行うことで、APIサーバーのスレッドを専有してしまうのを防ぎたかったのです。

詳細なポイントをリストアップすると以下のようになります。

  • 「いいね」で作成される通知データの未処理分を保持するDBテーブルを新たに作成する
  • Event Bridge・API Gatewayで、数分毎にAWS Lambdaの関数をキックする
  • AWS Lambdaの関数がキックされたら、未処理Push通知データをFirebaseサーバーに送信する

結果として、

  • 「いいね」実行時のユーザーへのレスポンスが極端に早くなる
  • APIサーバーのスレッドが塞がれることがなくなる
  • 5xx系エラーの頻発を防ぐことが出来る

一言で言えば、「同期的に行っていた通知処理を、非同期に変更する」ことで、サーバーが抱えていた問題を解決しました。

実装後は、サーバーのヘルスが悪化することがほぼなくなり成功だったと言えます。

AWS Lambdaの実装概要

AWS Lambda は、サーバーのプロビジョニングや管理の必要なしにコードを実行できるコンピューティングサービスです。AWS Lambda は必要時にのみコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで自動的にスケーリングします。

AWS Lambda とは – AWS Lambda

AWS Lambdaを実装するには、いくつか方法がありますが、今回は、sam cliを使用して、コード群をZip形式でアップロードする方法で実装しました。

実際に実装を進めていく上で、公式ドキュメントは必須ですね、何度も確認しながら一つ一つ進めていきます。

もう少しAWS Lambdaの内容を細かく見ていきます。

AWS Lambdaのトリガー

AWS Lambdaにアップロードしたコードを実行する = キックする、そのトリガーは何なのかということですが、こちらも柔軟に設定することが可能です。

例えば、エンドポイントを設定することでAPIで実行したり、Lambdaの管理画面上でスロットルで手動で実行することも可能です。

今回は、API Gatewayを設定することで、LambdaをAPIで叩くように設定しました。

AWS Lambdaが実行されたときの処理順序

では、AWS Lambdaがキックされたら、アップロードしたコード群がどのような処理順序で実行されていくのでしょうか。その詳細は、template.yamlに記述することになります。

Globals:
  Function:
    Timeout: 300
    Memory: 128

Resources:
  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: test/
      Handler: sample.sample_handler
      Runtime: ruby2.7など
      Events:
        FCMNotifier:
          Type: Api
          Properties:
            Path: /path
            Method: get
      Environment:
        Variables:
          TOKEN: ABC 
          KEY: ABC

こちらはあくまでサンプルですが、このtemplate.yamlの中身を一つひとつ紐解いていくと、Lambdaをより理解することが出来るようになります。

Pathに設定されているpathがMethodに指定されているHTTP Methodで叩かれると、そのFunction(上記の例ではTestFunction)が実行される。

関数が実行されると、CodeUriに記述されているパスのHandlerに記述されているファイル名+関数名が実行される、という流れ。Handlerの記述方法は、file.functionという書き方。

記述自体はシンプルでわかりやすいですね。

Event Bridgeの役割

情報通信は魔法ではないので、もちろんAWS Lambdaも実態としてはサーバーの実機がどこかに存在します。ただ管理画面上ではそれらは見ることが出来ません。

EC2などであれば、Linuxだったりするので、EC2インスタンス内でcron等を設定することが可能でしたが、lambdaではそれが不可能です。

それを解決しAWS Lambdaで定期実行するためには、Event Bridgeを使用する必要があります。こちらもコードで実装することが出来るかとは思いますが、今回はLambdaの管理画面上で行いました。

Lambdaの「デザイナー」タブにて、「Event Bridge」を追加し、詳細を設定することが出来ます。ここで、毎分毎や毎週月曜日の何時といった感じで、cronのように定期実行処理を追加出来ます。

AWS Lambdaの設定時の注意点

AWS Lambdaは、必要な時に必要なリソースのみを用いて実行してくれるコンピューティングシステムです。そのため、Lambdaの基本設定として、メモリ及びタイムアウトを設定することは必須になります。

処理するデータ容量や処理内容に応じて、柔軟に設定することが可能。どれほどのリソースが必要か、これは運用していく中で少しずつ見極めていくのが良いと思います。設定の変更自体は管理画面上で簡単に行うことができ、変更は(おそらく)すぐに反映されます。この設定は、料金に関わってくるので注意が必要です。

template.yamlに記述することも可能です。

Lambda内の標準出力のログ確認方法

Lambdaにアップロードした処理が正常に動いているのか確認するには、原始的ではありますが確実な標準出力をまずは実装すると良いと思います。(rubyであればputs

この標準出力などのログは、設定すればCloudWatch Logsで確認することが可能なので、こちらも併せて実装しておくとより開発・テスト・運用の全ての面で安心です。

AWS LambdaでPush通知を非同期的に対応する方法のまとめ

サーバーの運用は難しくもやりがいのある領域かと感じます。

デザインやフロントエンドのUI/UXと違って、ユーザーの目にはほとんど感じることはないでしょう。しかし、サーバーのレスポンスタイムが遅かったり、エラーが頻発していたりすると、ユーザービリティを大きくそこなってしまうのも事実です。

縁の下の力持ち的な役割であり、サービスの運営には不可欠です。

「大切なことは、目には見えない」ですね。