Hello Interview の Design Ad Click Aggregator をきっかけに調べたことをまとめる。
情報のソースは主に Chat GPT。それぞれの解決策は簡略化されたもの。
前提
- クライアントのサイト内には広告が埋め込まれている。
- サイトを訪れたユーザーが広告をクリックすると、広告会社のサーバーにリクエストが送信される
- 広告会社のサーバーは (1) click を記録する (2) 302 リダイレクトを返す
- ユーザーは広告主の設定したページへとリダイレクトされる
要件
ユーザーによる複数クリックを検知したい。
なお、広告の複数クリックには、以下の2パターンがある。
- (i) 悪意がないユーザーによる重複クリック (e.g. 広告を複数回連続クリック)
- (ii) 悪意があるユーザーによる重複クリック (e.g. クリックを捏造するために人為的に複数のクリックを生成)
思考する
大枠としては、「idempotency key の生成」 + 「Redis で 重複検知」を使う。
この記事の論点となるのは、idempotency key (ad impression id と呼ばれる) の生成をどこで、どのように行うか。
解決策1. idempotency keyをフロントエンドで生成する
= idempotency key はフロントエンドで発行されたuuidを使う。Redisには、{ idempotency_key -> true } を保存し、データの容量を考え TTLを1時間に設定する。
このやり方だと、ユーザーが適当な key を指定することで、有効なリクエストを送信することができてしまう。したがって、これは問題ない場合のみ使える方法となる。
例えば、課金ボタンの重複クリックの検知など。これを捏造すると毎回課金が発生してしまうため、ユーザーがこれを捏造することはないだろう。
これは、今回の議題の広告基盤では使用できない。なぜなら悪意のあるユーザーによる重複クリックを排除できないから。
⭕️ 解決策2. idempotency keyをサーバーサイドで生成し、かつ発行済みのものをRedisで管理する。
以下の方法を取る。
- サーバーサイドでkeyを作成し(uuidなど)、作成したkeyをRedisに保存しておく。
- 広告にこのkeyを埋め込む。
- ユーザーが広告をクリックした際のリクエスト内にこのkeyを含める。
- クリック処理サーバーがリクエストを処理する際、
このkeyがRedis内に存在するかを確かめる && そうであればこのkeyを削除するをアトミックに実行する。もし存在していれば有効なクリックと判定する。
なお、Redisの代わりにRDBを使用すると、以下の問題点が発生するので注意。
- レースコンディションが発生しうる。
- クリックごとにDBアクセスが発生するため、高トラフィックとなり、システムのボトルネックになる可能性がある。
⭕️⭕️ 解決策3. idempotency keyをサーバーサイドで生成して署名する
ユーザーの画面に表示されるそれぞれの広告に、サーバーサイドで発行された idempotency keyが内蔵する。
この idempotency key は、nonce key などの情報から生成される JWT (= 署名+検証あり) を使用すれば良い。
以下は、Chat GPTが書いてくれたものに加筆したフローチャート。
[🟢 広告作成サーバー]
|
(JWT 元データ作成)
| {
| "ad_id": 123,
| "ts": 1694073600,
| "nonce": "一意UUID"
| }
|
v
(署名リクエスト) ---> [AWS KMS] (KMS 内部の秘密鍵を使う)
| |
| <---署名バイト列--|
v
(元データ + 署名バイト列を Base62 エンコードしてJWTを作成)
|
v
(広告クリック URL に "token" として埋め込む)
|
v
[🟢 ブラウザ内の広告]
|
(ユーザーが広告をクリック)
| POST /api/clicks?token={}
|
v
[🟢 クリック処理サーバー]
|
| JWT 検証リクエスト
| (tokenを Base62 → 元データ + 署名バイト列にデコード)
v
(署名検証リクエスト) -------> [AWS KMS] (KMS内部の公開鍵を使う)
| |
| <---検証結果(yes/no)---|
v
(idempotency key による重複チェック)
| idempotency key を Redis で重複チェック
|
v
✅ クリック処理 / リプレイ防止完了!
Redisには、{ nonce_key -> true } を保存し、データの容量を考え TTLを1時間に設定する。データの容量を考え、Redis TTLを1時間とかで設定する。(これは Product decision となる)
[追記] 「広告が発行されてから一時間以内のクリックのみを有効なものとしてカウントする」という仕様を加えることでも重複排除は可能。
この方法であれば、署名検証のプロセスが入っているため、悪意があるユーザーによる重複クリックも排除できる。
なお、KMS のスループットがボトルネックになる場合は、KMS から公開鍵を取ってきてサーバーにキャッシュしておけば良い。
また、同一ユーザーがページを何度もリロードして広告をクリックするケースに関しては、IPアドレスあるいはユーザーIDごとにrate limit を設定することで対処すれば良い。
補足: 署名アルゴリズム
Chat GPTによると、
