ZOZOTOWNにおけるマーケティングメール配信基盤の構築

OGP

はじめに

こんにちは、MA部の松岡(@pine0619)です。MA部ではマーケティングオートメーションシステムの開発・運用に従事しています。

ZOZOTOWNでは、マーケティングオートメーションシステム(以下、MAシステム)を使い、メールやLINE、アプリプッシュ通知といったチャネルへのキャンペーンを配信しています。

MA部では、複数のMAシステムが存在しており、MAシステムそれぞれに各チャネルへの配信ロジックが記述されていました。これにより、現状の運用保守ならびに今後の改修コストが高いかつ、使用している外部サービスのレートリミットの一元管理が出来ていないなどの問題を抱えていました。そのため、外部サービスへのリクエスト部分をチャネルごとにモジュールとして切り出し、複数のMAシステムから共通で使える配信基盤を作成しました。

また、社内の他チームの持つシステムからのキャンペーン配信の要望があったため、全社共通で利用できる配信基盤を作成しました。

その中でも本記事ではメール配信基盤にフォーカスし、以下についてご紹介します。

  • システム構成
  • マーケティングメールを配信する際の考慮ポイント
  • 複数の社内システムから利用される基盤作成の考慮ポイント
  • メール配信基盤の監視について

このメール配信基盤は注文完了などのトランザクションメールは対象外で、マーケティングメールのみを扱うシステムとなっています。

なお、アプリプッシュ通知やLINEの配信基盤については別の記事で紹介する予定です。

目次

メール配信基盤の作成の背景・目的

既存のマーケティングオートメーションシステムでの課題

導入部分でも触れたように、現在MA部では複数のMAシステムを使って配信しています。配信は特定のユーザーセグメント向けのマス配信と、個別のユーザーに最適化されたパーソナライズ配信の2種類が存在しています。この2種類の配信に加えて、チャネルによっては独立したシステムになっているという理由から複数のMAシステムの開発・運用をしています。

メール配信では外部のメール配信サービス(以下、メール配信サービス)を使用しています。このメール配信サービスを使った配信ロジックがパーソナライズ配信、マス配信の両方のMAシステムに入っていました。配信ロジックが2つのMAシステムに存在していることで、現状の運用保守ならびに今後の改修において工数がかかってしまうなどの問題がありました。

また、一般的に外部サービスにはリクエスト制限があります。このメール配信サービスにもリクエスト制限がありますが、現在のMAシステムでは、メール配信サービスへのリクエスト流量制限の一元化が出来ていませんでした。そのため、両方のシステムから同時に大量のメール配信をリクエストした際に、リクエスト制限を超過してしまい、配信遅延やエラーが発生するという問題もありました。

上記の課題を解決するため、配信基盤としてMAシステムから配信ロジックをモジュールとして切り出すことで、配信ロジックならびにリクエスト流量制限を一元化することにしました。

配信基盤とMAシステムの関係図

他の社内システムからの利用希望

上記で述べた課題の他に、社内の他チームの持つ社内システムからのキャンペーン配信の要望もありました。しかし、配信ロジックがMAシステム内に書かれていたため、そのままだと他の社内システムからの配信に対応出来ない状況でした。

この課題を解決するために、全社共通で利用できるメール配信基盤を作成することにしました。

メール配信基盤のシステム構成

今回作成したメール配信基盤についてご紹介します。

メール配信基盤では、ワークフローエンジンのDigdagを採用しています。既存のMAシステムでDigdagを採用しており、今回の配信基盤の開発でリソースを流用できるかつ、チームメンバーがDigdagに慣れており開発・運用コストを削減できるため採用を決めました。

以下がシステムの全体構成および処理の流れとなっています。

全体のアーキテクチャ図

メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。このテンプレートとパラメータを指定してメール配信サービスへ配信リクエストをすることで、配信時にテンプレートへパラメータが埋め込まれた状態でメール配信されます。

パラメータの詳細については以下のコンテンツパートを参照してください。

techblog.zozo.com

配信基盤へのリクエスト

配信基盤へ配信リクエストをする前に、社内システム側でメール配信サービスへのテンプレートのアップロードと、BigQueryの配信対象者テーブルを作成します。以下の図でいうと赤枠の部分になります。この配信対象者テーブルは、テンプレートのパラメータ、ユーザーID、メールアドレスに紐づいたIDを含みます。現状は社内システムから直接メール配信サービスへテンプレートをアップロードしていますが、こちらは後々、メール配信基盤を経由するように変更し、社内システムのメール配信サービスへの依存を無くす予定です。

配信リクエスト準備

その後、社内システムからの配信リクエストをAPIで受信します。以下の図でいうと赤枠の部分になります。この配信リクエストでは前述したテンプレートの情報や配信対象者テーブル名などを受け取ります。

配信リクエスト

次にリクエスト内容を保存するためのリクエストテーブルにリクエスト内容をINSERTして、社内システムにレスポンスを返します。その後、Digdagによるマイクロバッチでリクエストテーブルからリクエスト内容を取得し、配信処理しています。また、配信処理の他に配信履歴の確認や、分析目的で使用される配信実績テーブルへ実績を書き込みます。

ZOZOではBigQueryを全社共通のデータ基盤として利用しています。このデータ基盤上にメール内容の元となる商品データや会員データなどがあり、それらのデータを利用して配信します。また、メール配信基盤の配信実績データと、データ基盤上のデータを組み合わせて分析したいという要件もありました。以上の理由から、データ基盤とメール配信基盤間で円滑にデータを受け渡すために、メール配信基盤ではデータの管理にBigQueryを採用しています。

メール配信処理の流れ

メール配信処理は以下の流れとなっています。

配信フロー図

配信処理は、起動・配信前処理・配信処理を担う3つのワークフローから構成されています。各ワークフローの役割と実行タイミングについては以下となっています。

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

配信前処理と配信処理を分けているのは、後述するメール配信サービスへのリクエストの流量を制限するためです。

マーケティングメールを配信する際の考慮ポイント

マーケティングメールを配信する際に考慮すべき点と、メール配信基盤においてそれをどう実現したかについてご紹介します。

配信の可能時間チェック

メール配信基盤では、一律な配信の可能時間を設定しています。メール配信基盤や外部サービスでの障害などにより配信遅延が発生し、深夜帯にメール配信される場合が考えられます。しかし、深夜帯は多くのユーザーが活動時間外ということもあり、場合によってはメルマガ購読停止やクレームなどに繋がる恐れがあります。

そのためメール配信基盤では、Digdagのスケジュール機能を利用し、以下のようにスケジュール設定することで配信の可能時間を定めています。

schedule:
  cron>: '*/5 8-22 * * *'

配信の有効期限チェック

配信の可能時間チェックとは別に、社内システムからリクエストを受け取る際に配信の有効期限を受け取るようにしています。有効期限を受け取る理由は、配信リクエストを受け付けてから一定の時刻を過ぎた場合は配信しないようにするためです。

たとえばセール最終日のお知らせメールを送る場合について考えてみます。メール配信基盤や外部サービスでの障害などにより、セール終了時間までに配信完了しない場合が考えられます。その際にそのままメールを配信してしまうと、セールは終了しているため、事実と異なるメールがユーザーに届いてしまいます。それを防ぐために配信の有効期限をチェックし、有効期限切れの配信は送らないようにしています。

起動ワークフローでリクエストテーブルから配信対象を取得するクエリが以下です。このクエリで有効期限(expires_at)を過ぎていないかつ、statusが配信処理前を示すWAITINGになっているリクエストを取得することで、配信の有効期限チェックを実現しています。

SELECT *
FROM
  `リクエストテーブル`
WHERE
  expires_at > CURRENT_TIMESTAMP()
  AND status = 'WAITING'
ORDER BY expires_at ASC

前述の通り、メール配信基盤は必ず届かないと困るようなトランザクションメールではありません。そのため誤情報をユーザに送ってしまうことのほうが良くないため、有効時間を過ぎた場合は配信しないようにしています。

配信の重複制御

マーケティングメールにおいて重複配信は、会社への不信感や、メルマガ購読停止、クレームに繋がってしまう可能性があります。メール配信基盤では、配信リクエスト完了後の処理が失敗し処理全体をリトライした場合や、社内システムから同一リクエストを複数回受け取った場合に、重複配信の可能性がありました。そのため、重複制御の処理を入れることで配信の重複を防いでいます。

重複制御は以下の流れになっています。

重複制御フロー図

配信対象者テーブルは社内システムから連携されるテーブルです。この配信対象者テーブルでdeduplication_idというメール1通単位ごとに一意となるIDを受け取ります。このdeduplication_idと重複制御用テーブルを使って重複制御を行います。重複制御用テーブルには、既に配信処理されたdedeplication_idを保存しています。重複除外のため、配信対象者テーブル内のdeduplication_idが重複制御用テーブルに存在しているかどうかをチェックします。存在している場合は、該当のdeduplication_idを持つレコードを除外した重複除外済みテーブルを作成します。その後、重複制御用テーブルを更新しています。後続の配信処理ではこの重複除外済みテーブルを配信対象としてメールを配信します。

メールアドレスの解決

メール配信では配信対象者のメールアドレスが必要となります。メールアドレスは個人情報に当たるため、本当に必要な場合を除いて、参照できる状態は好ましくありません。そのため、社内システムと配信基盤間ではメールアドレスに紐づいたemail_idを使って配信対象者を連携し、配信基盤側でemail_idに紐づいたメールアドレスを全社共通のデータ基盤から取得しています。これにより社内システムと配信基盤間で個人情報を受け渡す必要が無くなります。

会員メール情報テーブルのメールアドレスカラムに関してはアクセス制御されています。メール配信基盤で使用するサービスアカウントにはあらかじめメールアドレスカラムへのアクセス権限を付与しているため、email_idに紐づいたメールアドレスが取得できます。

メールアドレスの取得ロジック

配信対象者の分割

メール配信サービスへのリクエストでは、配信対象者のメールアドレスおよび、キャンペーンやユーザごとに必要なパラメータのリストを連携する必要があります。この配信リストの連携ではファイルサイズ上限が定められています。ZOZOTOWNのマーケティングメールでは1回のキャンペーンで1000万通程度のメールを配信することもあり、何も考えずに配信リストを作成・連携するとファイルサイズ上限を超過してしまいます。そのため、ファイルサイズ上限を超過しないように、配信リスト作成時に配信対象者を適切に分割する必要がありました。

配信対象者のリストはBigQueryに配置されると説明しました。配信時にはそのデータをCloud Storageにエクスポートし、そのファイルをメール配信サービスへ連携します。BigQueryからCloud Storageへのエクスポート時に、エクスポートするファイルのサイズを制限する方法はGoogle Cloudのドキュメントを参考にしました。

配信基盤では、以下のステップで配信対象者を分割し、配信リストを作成しています。

  1. 配信リストテーブルのテーブルサイズの取得
  2. 分割数の決定
  3. パーティションを使ってテーブルを分割する
  4. 分割テーブルごとにファイルへエクスポートする

各ステップについてそれぞれご紹介します。

配信リストテーブルのテーブルサイズの取得

前述した重複制御・メールアドレスの解決の処理後、配信リストの元となる配信リストテーブルが作成されます。この配信リストテーブルを適切に分割し、ファイルへエクスポートするために、まずは配信リストテーブルのテーブルサイズを知る必要があります。テーブルサイズはINFORMATION_SCHEMA内のパーティションビューTOTAL_LOGICAL_BYTESから取得可能で、以下のクエリにて取得しています。

SELECT
  total_logical_bytes
FROM
  `{project}.{dataset}.INFORMATION_SCHEMA.PARTITIONS`
WHERE
  table_name = 配信リストテーブル

分割数の決定

上記で取得した配信リストテーブルのテーブルサイズと、メール配信サービスのファイルサイズ上限を元に分割数を決定します。メール配信基盤ではDigdagでPythonスクリプトを呼び出しており、以下のコードで分割数を決定しています。

file_size_limit_mb = 900 # ファイルサイズ上限

table_size_bytes = 配信リストテーブルのテーブルサイズ
table_size_mb = table_size_bytes / (1024 * 1024)
partition_count = int(table_size_mb // file_size_limit_mb) + 1

パーティションを使ってテーブルを分割する

分割数が決定した後は、以下のクエリでパーティション用のidであるpartition_idを各レコードへランダムに割り振ります。

CREATE OR REPLACE TABLE `パーティションテーブル`
PARTITION BY RANGE_BUCKET(partition_id, GENERATE_ARRAY(0, {partition_count}, 1))
CLUSTER BY partition_id
AS (
  SELECT
    *, CAST(FLOOR(n*RAND()) AS INT64) AS partition_id
  FROM
    `配信リストテーブル`
)

その後、partition_idごとにテーブルを作成することで配信対象者を分割しています。

CREATE OR REPLACE TABLE  `分割テーブル_{partition_id}`
AS (
  SELECT * EXCEPT(partition_id)
  FROM `パーティションテーブル`
  WHERE partition_id = {partition_id}
)

分割テーブルごとにファイルへエクスポートする

partition_idごとに分割したテーブルをCloud Storageにエクスポートします。メール配信基盤では、EXPORT DATAステートメントではなく、クライアントライブラリを使ってエクスポートしています。

EXPORT DATAステートメントでは、エクスポート先のCloud StorageのURIの指定で「単一のURI」が使用できず、「単一のワイルドカードURI」のみ対応しています。「単一のワイルドカードURI」の場合、以下のドキュメントの通り、複数のファイルに自動で分割され、ファイルサイズが一定ではなくなってしまいます。そのため、クライアントライブラリを使ってエクスポートしています。

エクスポートされるデータが最大値の 1 GB を超えそうな場合は、単一のワイルドカード URI を使用します。データは、指定したパターンに基づいて複数のファイルに分割されます。エクスポートされたファイルのサイズは一定ではありません。

引用:テーブルデータを Cloud Storage にエクスポートする  |  BigQuery  |  Google Cloud

メール配信サービスへのリクエストの流量制限

メール配信基盤では、外部のメール配信サービスを利用してメールを配信しています。前述したようにメール配信サービスへのリクエスト流量を制限しない場合、メール配信サービスのパフォーマンス悪化により、配信遅延やエラー発生の懸念がありました。

そのため配信の前処理と実際にメール配信サービスへリクエストする配信処理との間に配信キューを挟み、配信リクエストの流量を制限することで上記の課題を解決しました。

ここではPub/Subのようなメッセージングサービスではなく、BigQueryをキューとして使っています。これは、今後配信の優先度付けをする予定のため、条件でデータが取得できるデータベースの方が適していると判断しました。また、リクエストテーブルや配信対象者テーブルなどをBigQueryで管理しており、データの分散や運用対象リソースが増えるのを防ぎたくCloudSQLなど他のデータベースは利用しませんでした。

配信フロー制御

テスト配信

ユーザーへメールを送る前に、表示崩れや文字化けが無いかどうかを確認するため、施策担当者へテストメールを送りたいという要件がありました。そのため、テスト配信用のワークフローとユーザーテーブルを別途作成しました。

通常配信とテスト配信のワークフローを分けているのは、テスト配信では重複制御などの処理が不要なためです。テスト配信ワークフローでは、不要な処理を省略することで処理時間を短縮し、施策担当者がテストメールを受け取るまでの待ち時間を減らすことができました。

また、テストユーザーテーブルを用意しているのはテスト配信時に一般のユーザーへの誤配信を防ぐためです。通常の配信ではデータ基盤上の会員メール情報テーブルからメールアドレスを取得しますが、テスト配信ではテストユーザテーブルからメールアドレスを取得します。これによりテスト時は一般ユーザーの情報が入ったテーブルを参照する必要が無くなるため、誤配信を防ぐことができます。

通常配信とテスト配信の比較図

複数の社内システムから利用される基盤作成の考慮ポイント

今回のメール配信基盤は、複数の社内システムからの利用を考慮する必要がありました。その際の考慮ポイントについてご紹介します。

リクエスト元の識別およびデータ管理について

メール配信基盤が複数の社内システムから利用されるにあたって、どの社内システムからリクエストされたのかを識別する必要がありました。そのため配信基盤ではリクエスト元の識別にsourceという概念を導入しています。配信基盤ではsourceごとに作成したデータセット内へ配信実績テーブルやテストユーザテーブル、重複制御用テーブルを配置しています。以下の画像でいうと「ma_batch」がsourceに該当しています。

データセット&テーブル

このようにsourceごとのデータセットに分けた理由は権限管理の容易さにあります。BigQueryではデータセット単位のアクセス制御が可能です。これによりテーブルごとの権限付与が不要となります。また、全sourceで共通なテーブルにしてしまうと、レコード数によってはクエリコストが増えてしまう可能性があることから、このような設計にしました。

メール配信基盤ではTerraformによるインフラ管理をしており、以下のようにデータセットに対してviewer権限を付与しています。

resource "google_bigquery_dataset_iam_member" "bigquery_data_viewer_ma_batch" {
  for_each = toset(local.bigquery_data_viewer_ma_batch_members)

  project    = local.project
  member     = each.key
  dataset_id = "ma_batch"
  role       = "roles/bigquery.dataViewer"
}

メール配信基盤の監視について

メール配信基盤ではDigdagのワークフローが失敗した場合に、Slack通知およびPagerDutyによる電話通知をしており、オンコール当番が気付ける仕組みになっています。しかし、これだけだとシステム監視として不十分なため、以下の監視を導入しています。

監視内容 目的
有効期限切れにより配信されなかった配信の監視 未配信および配信遅延の検知
配信キュー内の未配信件数の監視 未配信検知および配信遅延の検知
重複配信の監視 重複配信の検知
配信成功率の監視 配信異常の確認

今後の展望

冒頭でも触れた通り、ZOZOTOWNのMAシステムには「マス配信」と「パーソナライズ配信」の2つがあります。現在MA部ではMAシステムのリプレイスを進めており、現在「マス配信」のリプレイスが完了し、「パーソナライズ配信」をリプレイスしています。MAシステムのリプレイスについて詳しくは以下の記事をご覧ください。

techblog.zozo.com

「パーソナライズ配信」の中にはリアルタイム性を求められるリアルタイム配信があります。現状の配信基盤では配信リクエスト順に処理しており、1000万通規模のメールを配信し終わるまでに時間がかかります。この1000万通のメール配信中にリアルタイム配信をリクエストされた場合、現状だとこの1000万通のメール配信が完了してからリアルタイム配信の配信が行われます。配信完了までのスピードがもっと早ければ現状の配信基盤でも対応できますが、メール配信サービスにはリクエスト上限があるため、これ以上の配信の速度向上は見込めません。

そこで配信の優先度を付け、優先度順に配信することでリアルタイム配信にも対応できるシステムにしていきたいと考えています。

また、今後の配信基盤の全体の展望として、他チャネルの拡充や改善を進め、ZOZOとお客様をつなぐコミュニケーションの窓口となる基盤を作り上げていきたいと考えています。

まとめ

本記事ではメール配信基盤について紹介しました。メール配信基盤の誕生により、メール配信サービスへの流量制限の一元化や、配信ロジックの集約による保守・運用コストの削減、複数の社内システムからの配信を実現できました。本記事が同じような状況・課題を持つ方、新たに配信基盤を作る方への参考になれば幸いです。

MA部では上記で挙げた理想の配信基盤を一緒に作り上げてくれる方を募集中です。ご興味のある方は、ぜひ以下のリンクからぜひご応募ください。

hrmos.co

hrmos.co

カテゴリー