ZOZOTOWNのマーケティングメール配信を支える技術

ZOZOTOWNのマーケティングメール配信を支える技術

はじめに

こんにちは、MA部MA基盤ブロックの@turbofish_です。ZOZOTOWNではプッシュ通知やLINE、メール、サイト内お知らせでのキャンペーン配信を行っており、MA部ではそれらの配信を担うマーケティングオートメーション(MA)のシステムを開発しています。本記事ではその中でも、メールの配信を担当する基盤システムをリアーキテクチャし、バッチでの配信とリアルタイムな配信の両立を実現した取り組みをご紹介します。

目次

背景・課題

ZOZOTOWNでのキャンペーン配信

ZOZOTOWNでの配信の特徴はさまざまですが、ここでは配信タイミングを軸として下図のように分類します。

分類 配信ロジック 配信タイミング
バッチ配信 事前に決められた条件に当てはまる会員に、決められた時刻に一斉に送信する。 配信開始から終了まで数十分〜数時間の幅があってもよい
リアルタイムイベント配信 在庫残り1点や値下げなど、会員に関係するイベントが発生した時に配信する。 データの変更を検知したらできるだけ早く配信する必要がある(配信が遅れるとユーザへのメリットが薄れる)。

上記の分類はメールのみならず全てのチャネルにおいて共通しており、マーケティングメールもその性質によって配信処理に対して許容できる時間に差があります。

キャンペーン配信の分類とMA部で開発しているマーケティングプラットフォームZMPのアーキテクチャについて詳しく知りたい方は、2024年3月に齋藤が執筆した記事をご参照ください。

techblog.zozo.com

メール配信基盤の機能要件

メール配信基盤とは、ZOZOTOWNのマーケティングメールの配信を担うシステムです。MA部が開発するマーケティングプラットフォームZMPをはじめ、事前に登録された他システムから配信リクエストを受け取り、リクエストの内容に沿ってメールを配信します。

メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。

メールテンプレートとパラメータによる配信用メール生成

メール配信基盤を使用するシステムは、メール配信基盤に配信リクエストを送る前に、このメールテンプレートと、メールの配信先とメール本文に挿入するパラメータを保存した配信対象者テーブルを準備します。配信対象者テーブルは、CSV形式に変換し、適切な大きさに分割して、配信リストとして外部サービスにアップロードして配信に使用します。

キャンペーン配信時間に期限が存在する場合は、メール配信基盤はリクエストを受信してから期限までに間に合うよう配信処理を行う必要があります。前提として、配信処理にかかる時間は、メール配信数に比例して増加する傾向があります。ZOZOTOWNのメール配信で最も配信量が多い時は、配信リクエストの生成を開始してから全ての送信先に配信されるまでに数時間かかることもあります。そのため、配信数が多いバッチ配信の場合は、それを見越して配信処理にかかる時間を考慮した上で適切な時間に配信されるようスケジューリングしています。

メール配信基盤では、実際にメールを配信する処理は外部サービスをAPI経由で利用しており、その外部配信サービスのAPIの同時実行数には上限があります。よってメール配信基盤も、その上限数以上の並列処理はできません。

旧メール配信基盤のシステムアーキテクチャと課題

この記事では、リアーキテクチャ前の基盤を旧メール配信基盤、リアーキテクチャ後を新メール配信基盤と呼びます。新旧の記載がないメール配信基盤という表記は、リアーキテクチャの前後に関わらない文脈であることを意味します。MA部ではほとんどのシステムをGCP上に構築しており、メール配信基盤も同様にGCPを使用しています。

旧メール基盤のアーキテクチャ

旧メール配信基盤は、DigdagというOSSのワークフローエンジンを使用して実装されており、ワークフロー起動時に配信待ちのリクエストを取得してバッチで処理していました。DigdagはリトライやSLAに加え、プラグインでエラー発生時のSlack通知などの機能をサポートしており、配信処理のロジックの実装のみに集中できる点が大きな魅力でした。旧メール配信基盤のアーキテクチャは下図の通りです。

旧メール配信基盤アーキテクチャ

旧メール配信基盤は、下記の3つのワークフローで構成されていました。

ワークフロー 役割 実行タイミング
起動 - 配信対象のリクエストの取得
- 配信の有効期限チェック
- 配信前処理ワークフローの起動
3分おきに定期実行
配信前処理 - 重複配信の制御
- メールアドレスの取得
- 配信リストを外部サービスにアップロード
起動ワークフローから起動される
配信処理 - 外部の配信サービスへのリクエスト
- 配信実績の書き込み
5分おきに定期実行

旧メール配信基盤システムの処理やアーキテクチャの詳細について知りたい方は、2024年3月に松岡が執筆した記事をご参照ください。

techblog.zozo.com

旧メール配信基盤の課題

Digdagは1つのジョブが持つ全てのリクエストの配信処理が終わるまで次の処理を開始できません。これにより、配信スループットを決める最大の要因である外部サービスのAPIへのリクエスト量を分散させることができず、リソース効率があまり良くない状態でした。また、大規模なバッチ配信の直後にリアルタイムイベント配信が発生した場合、バッチ配信を捌き切るまで、リアルタイムイベント配信に長い処理待ちが発生してしまう状態でした。

例として、簡単のために外部サービスへの同時リクエスト数上限を2と仮定し、バッチ配信が4リクエスト、リアルタイムイベント配信が1リクエストほぼ同時に作成された場合の処理について考えてみます。旧メール配信基盤では、処理は下図のようになります。バッチ配信のリクエスト中に外部サービスへのリクエストが1並列になっている時間もあるにも関わらず、バッチ配信が全て完了するまでリアルタイムイベント配信が行われません。

旧メール配信基盤での処理

以上の理由から、旧メール配信基盤はリアルタイムイベント配信に対応できず、バッチ配信のリクエストのみを処理していました。リアルタイムイベント配信は、MA部の中でも最も古いシステムの1つであるRTMという別のシステムが担っていました。RTMの詳細について知りたい方は、2020年8月に田島が執筆した記事をご参照ください。

techblog.zozo.com

メールを配信するシステムが社内に複数存在することの最も重要な弊害として、複数システムから外部の配信サービスへのリクエストの総量が制御できない状態でした。複数のシステムが同じタイミングで配信すると、外部メール配信サービスのサーバーに過剰な負荷がかかり、全ての配信が遅くなってしまうリスクがありました。

新メール配信基盤の機能要件とアーキテクチャ

新メール配信基盤の機能要件

旧メール配信基盤の処理効率の改善に着手できていなかったなか、リアルタイムイベント配信を担っていたRTMをリプレイスするにあたり、RTMからのメール配信においてもメール配信基盤を使用することになりました。よって、新規の要件として下記の2つが求められました。

  • 外部の配信サービスのリソースを最大限活用できるアーキテクチャ
  • リアルタイムイベント配信を優先的に配信できる仕組み

以前より課題だった外部配信サービスの活用効率を改善し、配信全体の処理を最適化する必要がありました。加えて、配信対象が少なく優先度の高いリアルタイムイベント配信が高頻度でリクエストされた場合にも、配信対象が多いバッチ配信の影響を受けることなく配信できる必要があります。こうして、新たな要件に対応すべく、旧メール配信基盤を抜本的にリアーキテクチャすることにしました。

新メール配信基盤のアーキテクチャ

新メール配信基盤では、処理の性質に適した実装にすべくワークフローの形式をやめ、ジョブキューを使ってワーカー的なサーバーが連携して並列処理を行うようリアーキテクチャしました。新メール配信基盤のアーキテクチャは下図のとおりです。

新メール配信基盤アーキテクチャ

新メール配信基盤を構成する4つのワーカーはCloud RunサービスもしくはCloud Runジョブで作成しており、それぞれの役割は下記の通りです。サーバーを4つに分割したのは、インスタンスごとに同時処理数を制御することに加え、それぞれのワーカーの処理を非同期に行うことで、処理効率を最大化するためです。

ワーカー名 サービス 処理
配信トリガーくん Cloud Runサービス 処理中のタスク数が減少したら、その時最も優先順位が高い配信リクエストを少量ずつ抽出し、ジョブキュー(Cloud Tasks)に詰める。
配信リストアップロードくん Cloud Runサービス 配信リストを外部サービスにアップロードし、完了したら配信リクエストをジョブキュー(Pub/Sub)に詰める。
配信してくれるくん Cloud Runサービス(常時稼働CPU) Pub/Subからメッセージを受け取り、重複配信の制御と配信処理を行う。
配信完了作業くん Cloud Runジョブ 配信が完了したことを確認し、リクエストのステータスを変更したり、配信ログを記録するなどの後処理を行う。

新メール配信基盤における配信処理の流れ

配信トリガーくん

配信トリガーくんは、その時々で配信待ちリクエストの配信優先度を確認し、次に処理するリクエストを決定してCloud Tasksにタスクを送信します。キューの中にある処理待ちのタスクを最小限に保ち、随時その時最も優先度の高いリクエストを少量ずつ追加することで、柔軟な配信順序を実現しています。キューに入ったリクエストは、配信リストアップロードくんが既存のタスクを終えて新規にリクエストを受け付け可能な状態になったら、配信リストアップロード処理を開始します。

配信リストアップロードくん

配信処理の前半は、配信リストのアップロードです。メール配信のフローの中でも、配信前処理の配信リストのアップロードは、そのほかの処理と比較してかなり時間がかかります。さらに、1つのキャンペーンに対する配信先の会員数が多いほど、配信関連データのアップロードにかかる時間は増加する傾向があります。つまり、配信リストアップロードの処理が処理全体のボトルネックになります。この配信リストアップロード処理の外部APIを同時リクエスト数上限まで活用できるよう、配信リストアップロードくんはインスタンスが処理する同時リクエスト数に応じてオートスケールし、並列で処理を行います。オートスケール時の最大インスタンス数とCloud Runの最大同時実行リクエスト数の設定を掛けた数が、外部サービスAPIの同時リクエスト数上限を超えないよう設定しています。配信リストのアップロードの処理が完了したらPub/Subにメッセージをパブリッシュし、新しいリクエストを受け付けます。

配信してくれるくん

配信してくれるくんは、受信したリクエストが重複配信ではないことを確認してから、メール配信APIを実行します。外部の配信サービスから取得した配信履歴を確認することで、同じ配信先に対して複数回同じメールを配信してしまうことを防いでいます。

配信完了作業くん

配信完了作業くんは、配信してくれるくんがメール配信を行った後の処理を担当します。まず、外部サービスのAPIを叩いて、メール配信ステータスが配信済みに更新されたことを確認します。配信完了を確認したら、データベースに配信実績を記録し、配信リクエストのステータスを更新するなどの後処理を行います。配信処理と配信後の処理がキューを挟んで分かれていることで、配信ステータスの確認を行なっている間にも、配信してくれるくんは後続のリクエストを処理して配信処理を行うことができます。

リアーキテクチャの結果

新メール配信基盤では、配信量が多いバッチ配信と断続的に発生する少量のリアルタイムイベント配信を同時に実行しても、全ての配信においてそれぞれの要件を満たす時間内で配信ができるようになりました。また、配信全体の処理効率が向上していることも確認できました。

旧メール基盤のアーキテクチャの説明と同様の状況を例に説明します。同時実行数の上限が2のとき、バッチ配信4、リアルタイムイベント配信1リクエストが追加された状況で、新旧基盤での処理は下図のように異なります。

新旧メール配信基盤でのリソース効率の違い

処理効率の向上については、想定していた以上の改善が見られました。負荷テストの結果、バッチ配信で新旧基盤を用いて同じ配信数を捌き切るまでの時間を計測したところ、配信処理の開始から配信完了までの時間がおよそ40%短縮されました。

また、メール配信基盤がメール配信処理を一括で担うことにより、社内の他システムがメール配信のロジックを再実装する必要がなくなり、外部の配信サービスへの負荷もメール配信基盤で制御できるようになります。

まとめ

本記事では、メール配信システムのリアーキテクチャの事例を紹介しました。メール配信基盤は、ワークフローエンジンを用いたバッチ処理からワーカーをジョブキューで連携させ並列処理を行う非同期的なアーキテクチャに変更しました。これにより、処理効率を大幅に向上させ、配信の性質に応じて柔軟に処理を進めるシステムへと進化しました。アプリケーションの再実装は工数もかかりましたが、システムの最適化によってマーケティングメール配信が管理しやすくなり、保守面でも大きな恩恵がありました。大量のリクエストを処理する必要があるシステムの開発、スケーラブルで処理効率が高いアプリケーション設計に興味がある方の参考になれば幸いです。

ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー