Social Pittの予約投稿システム – AWS & Rubyで構築するスケーラブルな投稿スケジューリング

DX事業本部の木村です。

Socialpitt では2022年10月にInstagramのフィード・リール予約投稿機能がリリースされました。

今回はそのシステムのインフラ、サーバーサイドを含むシステム全体について紹介いたします。

予約投稿について

SocialPittの構成
SocialPittの構成

Social Pitt の紹介とサービスを支える技術 で紹介した通り、 Social PittはCoreと各Serviceに分かれています。 予約投稿機能は Social Pittアカウント運用 の機能で、 今回紹介する予約投稿のシステム自体は全てCoreにおける話になります。

Social Pitt アカウント運用では以前からTwitterの投稿の予約投稿には対応していましたが、 10月にインフラをGCPからAWS、サーバーサイドの使用言語をPythonからRubyへ移行しており、 この機会に予約投稿のシステム全体も1から作り直し、Instagramの予約投稿にも対応することにしました。

Social Pitt Coreの概要

Social Pitt Core の構成
Social Pitt Core の構成

Coreは外部サービスからAPIでデータを収集するワーカーと、その収集したデータを返す内部APIからなっています。 ワーカーはSQSと Shoryuken を利用した仕組みを利用しており、 簡単にスケールすることが可能になっています。

SQSのキューには、ECS Scheduled Taskによって定期的にメッセージをエンキューしたり、 サービス側からエンキューしたりすることで非同期にジョブを実行できます。

予約投稿システム

予約投稿のシステムについても、Coreの仕組みに乗せ、予約投稿時間になったらエンキューし、 それを消化するワーカーを動かすようにしてスケールさせるような構成にすることにしましたが、 既存の仕組みをそのまま使用するにあたってはいくつか問題がありました。

Coreの仕組みで予約投稿システムを実現するにあたって問題になるのは以下の2点です。

  • ECS Scheduled Taskでメッセージをエンキューする際の遅延
  • At-least-onceの配信による重複実行

ここからはそれらの問題をどのように解決したかについて紹介していきます。

ECS Scheduled Taskでメッセージをエンキューする際の遅延

上図の通り、 既存のシステムでは定期実行タスクについてECS Scheduled Taskを利用してFargateのタスクを実行しメッセージをエンキューしていました。

Scheduled Taskでメッセージをエンキューする場合には

という問題があります。

非同期にバックグラウンドでデータを収集する用途ではこの仕組みで全く問題はないのですが、 設定した予約投稿時間中にタスクを実行したい予約投稿システムではこれらは問題がありました。

この問題を解決するには

  1. エンキューのためのECSサービスを動かし続ける
  2. ECS on EC2を利用する

などの方法がありますが、今回は1に近い方法かつ新たにサービスを作成せずに行いました。

具体的にはこのようなシステム図になります。

予約投稿システムの構成
予約投稿システムの構成

EventBridgeの API destinations 機能を用いて CoreのAPIのエンキュー用のエンドポイントに毎分リクエストし、 APIのサービスから予約投稿のメッセージをエンキューの処理を行います。 APIサービスは常に稼働しているため、1の仕組みを新たなサービスを追加せずとも実現することができました。

この仕組みの場合には 予約設定時間の0秒ちょうどに動く保証はない ため、 求められる要件によっては1分前にエンキューし、ワーカーでは予約時刻まで待ってから投稿を行うなどの工夫が必要になります。

At-least-onceの配信による重複実行

EventBridgeのメッセージ配信、およびSQSのメッセージ配信は共にat-least-onceを保証しており、 メッセージが重複して送信される可能性があります。

そもそも、Shoryukenを利用したワーカーを書く際には冪等性を意識する必要がありますが、 予約システムではそれに加えて重複実行させないことをどこかで保証できるようにする必要がありました。 今回はexactly-onceをワーカー側で保証するようにしました。

予約投稿のワーカーでは以下の順で処理を行います。

  1. キューからメッセージを受け取り、処理する投稿IDを取得
  2. 投稿IDでDBを検索し、投稿のステータスがscheduledでなければここで処理を終了
  3. DBのロックを取り、scheduledであることを確認してから、job_in_progress にステータスを変更 a. ロックを取れない、ロックを取った後のステータスの確認でscheduledでなければ処理を終了
  4. 予約投稿処理を実行

Railsのコードとしては、以下のようにシンプルに実現できます。

post = Post.scheduled.find(message["post_id"])
post.with_lock do
  raise DuplicateJobError unless post.scheduled?
  post.job_in_progress!
end

このようにDBレイヤーでロックを取りつつステータスを変更することで、 重複配信された場合でもscheduled ⇒ job_in_progressにステータスを変更できる1プロセスのみで予約投稿処理が実行され、 重複実行を避けることができます。

その他に考慮した点

予約投稿機能を作成する上でその他に考慮した点です。

投稿の添付メディアの事前アップロード

予約投稿時間になってから画像や動画のアップロードと投稿の公開を行うと、 サイズが大きい場合にアップロード自体に時間がかかり、予約投稿時刻に間に合わなくなってしまいます。 Instagram, Twitter共にメディアを事前にアップロードし、そのidを用いて投稿を作成することができるため、 添付メディアは予約投稿時刻の10分前に事前にアップロードを行うようにしています。 Instagramの場合は事前アップロードのコンテナの有効期限は24時間のため、 予約投稿時刻を未来に更新する場合にはidを無効化する必要があり、細かいところで注意する必要があります。

予約投稿のワーカーでは、事前アップロードのidがあればそれを用いて投稿を作成し、 (予約時刻直前に予約した場合などで)idがなければアップロードを行なってから投稿を行なっています。

リトライ

Shoryukenを用いている場合にはSQSのVisibility TimeoutとMax Receive CountでSQS側でリトライが行われますが、 こちらが要件にマッチしているかは考慮する必要があります。 現在Social PittではRails側で生じた例外は全てキャッチして、 投稿の失敗をユーザーにメール通知しリトライは行わないようにしています。

おわりに

今回はSocial Pittの予約投稿機能の裏側について紹介いたしました。

予約投稿機能がリリースされたことにより、社内のSNS運用代行における予約投稿率も上昇し業務効率化につながったり、 ユーザーからも予約投稿機能について喜びの声をいただいたりとこの機能を無事リリースできて良かったです。

個人的には既存のシステムに乗せたこの仕組みについては満足しているのですが、 予約投稿機能のリリースの直後に発表された Amazon EventBridge Scheduler を利用することで もっとシンプルに予約投稿機能を実現できるかもしれないと気になっているところです。 (予約投稿時間の変更など考慮すべき点は色々ありそうですが……)