01一次點擊到底以什麼為準——使用者按下的那一下,還是實際抵達廣告主網站?
使用者點擊廣告後會收到一個 302 轉址到廣告主網站——點擊在這一跳於伺服器端記錄,所以廣告攔截器與不穩定的客戶端都無法讓它遺失。
免費完整指南
設計廣告點擊聚合系統。請涵蓋High-throughput ingestion of K-M clicks/sec via a log/stream (Kafka) before aggregation, Exactly-once vs at-least-once delivery and idempotent aggregation sinks, Windowed aggregation...
面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。
不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。
01一次點擊到底以什麼為準——使用者按下的那一下,還是實際抵達廣告主網站?
使用者點擊廣告後會收到一個 302 轉址到廣告主網站——點擊在這一跳於伺服器端記錄,所以廣告攔截器與不穩定的客戶端都無法讓它遺失。
02廣告主需要什麼粒度與即時度?
廣告主可以查詢隨時間變化的點擊指標,最小粒度為一分鐘,並在點擊發生後約一分鐘內就能查到。
03同一次點擊送來兩次——連點或重試——應該只算一次嗎?
同一個曝光被重複點擊只算一次——每則顯示過的廣告本來就帶有唯一的 impression ID。
04只做點擊,還是也做曝光與 CTR?
先做點擊。曝光數與 CTR(click-through rate)之後再做,是已知的後續需求。
05一分鐘以外還需要哪些粒度——每小時、每日?
廣告主也需要小時與每日的檢視;一分鐘仍是最細的粒度。
06預算上限需要近乎即時地停掉廣告嗎?
要——打到預算上限的廣告必須在約一分鐘內停止投放。
範圍外廣告投放與廣告選擇(要顯示哪一則廣告) · 跨裝置點擊追蹤 · 線下行銷通路整合
01我們要按多高的尖峰點擊率來設計?
尖峰每秒 10K 次點擊,零遺失。點擊資料就是計費資料,所以整條管線打造成永不掉一次點擊。
02廣告主儀表板必須多快回應?
查詢在任意時間範圍內都要在一秒內回應。
03數字要多新鮮?
近乎即時:一次點擊應該在發生後約一分鐘內就能被查到。
04這些數字是直接拿去計費的吧——所以絕不允許多算?
不行——重複與重試絕不能灌大數字;這些是帳務等級的數字。
05數字要多精確——短暫誤差若事後會修正,可以接受嗎?
最初幾分鐘的微小暫時誤差可以容忍,但帳務數字必須精確,並在一天內收斂。
真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。
把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。
每日事件量
尖峰每秒 10K 次點擊,每日約 1 億次點擊100M events × ~100 B ≈ 10 GB/day 原始
原始事件放在資料湖永久保存的成本很低——這正是每日校正之所以可行的原因。
聚合列數
假設約 1000 萬則活躍廣告 × 每分鐘 1 列10M 檔廣告 × 一天 1,440 分鐘(24 小時 × 60 分)≈ 14B rows/day 最壞情況——但只有有點擊的廣告才會產出列,實際上 ≪ 1%
稀疏的分鐘列讓 OLAP store 小到足以做次秒級的範圍掃描。
去重快取大小
假設一個 impression ID 在約 1 小時的點擊窗口內有效10K/s × 3,600 s × ~50 B ≈ 1.8 GB
整個去重窗口能輕鬆放進一個 Redis 叢集。
串流處理器掛掉時的損失窗口
Flink 每個分鐘窗口做一次 checkpointcrash → 從上一個 checkpoint 重放 ≈ 最多重算 1 分鐘
Kafka 保留(數天)加上 checkpoint,意味著處理器崩潰只會重算,永不遺失。
為什麼不直接查原始事件
原始點擊表 vs 一次跨 30 天的廣告主儀表板查詢原始表每月成長 3B 列;即使是依廣告切分、以分鐘粒度建索引的切片,每次查詢仍要掃數百萬列——離次秒級差得遠
在這裡預先聚合不是一種優化——它就是設計本身。
決策範例
每秒一萬次點擊就是錢在流進來:每遺失一次點擊就是一筆沒計費的支出,而每重複計一次就是一個生氣的廣告主。
在轉址那一跳記錄點擊,並在做任何其他事之前先寫進一個持久的串流(Kafka)。接著串流處理器以一分鐘窗口持續聚合,把結果刷進儀表板查詢的 OLAP store。每則顯示過的廣告都帶一個簽章過的 impression ID,一個 Redis 檢查把重複的丟掉。每天一次,一個批次作業從資料湖重讀原始事件,校正串流出來的數字。
我不會做的事:把原始點擊存進資料庫、每次儀表板查詢都跑 GROUP BY。每次跨 30 天查詢要面對三十億列,會整個垮掉。我也不會純粹依 ad ID 分割串流,因為一則爆紅廣告就會把單一分區燒掉。修法很簡單:給熱門 ad ID 加一個隨機後綴,把它們鋪在 N 個分區上,寫聚合時再把後綴剝掉。整個問題都出在挑錯了分區 key。
如果廣告主接受 5 分鐘的新鮮度,我會把串流層整個拿掉、改跑 micro-batch——少一半的活動零件,同樣的準確度,只是慢一點。
先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。
完整全貌
路徑 1
路徑 2
在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。
GET/ads/{ad_id}/click?impression_id=
回應302 Location: advertiser_url
轉址那一跳把每次嘗試都持久記錄下來,接著串流處理器在任何計費聚合或 sink 寫入之前,先以 impression_id 去重。快取可以加速重複檢查,但計費的正確性必須在快取遺失與重放後依然成立。
GET/metrics?ad_id&from&to&granularity=1m
回應200 [{ minute, clicks, unique_users }]
由 OLAP store 服務,永遠不從原始事件——對數十億列做 GROUP BY 正是這個設計要避免的事。
核心實體
ClickEventimpression_id (PK) · ad_id · user_id · clicked_at
impression ID 在廣告顯示時鑄造並以 HMAC 簽章,所以點擊無法被偽造或重放。
MinuteAggregatead_id · minute · clicks · unique_users
OLAP store 服務的就是這個;每則廣告每分鐘一列。
在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。
使用者連點兩下,或一次重試觸發。計數怎麼維持在一?
每則顯示的廣告都帶簽章的 impression ID:去重快取會擋掉點擊窗口內的重複,分鐘級 upsert 也以這個 ID 冪等——重試永遠加不出第二筆。
以 user+ad 去重——再行銷會合理地把同一則廣告再次顯示給同一個使用者。
單一廣告突然吃掉全部點擊的一半。哪個元件最先到達極限?你又如何把負載鋪開?
把熱鍵加鹽——把 ad_id 拆成 ad_id#0..N 讓負載攤到多個分區,再用一個小小的合併步驟把部分計數折回同一分鐘列。
只依 ad ID 分割串流——一支爆紅廣告會把所有事件塞進同一個分區,自己造出熱鍵。
Flink 在窗口中途掛掉。有多少點擊遺失,你又怎麼知道?
什麼都不會丟:Kafka 留著原始事件,處理器從最後一個 checkpoint 重啟、最多重算一個視窗,冪等 upsert 用覆寫取代重複計數。
把串流當成真相來源——Kafka 保留加上 checkpoint 意味著重放,而非遺失。
串流已經即時聚合了——每日批次作業還能加上什麼?
串流數字會漂移——遲到事件、當機、bug。批次每天重讀原始事件並覆寫重算的每一分鐘:串流買到新鮮度,批次保證帳單正確。
跳過校正——計費等級的準確度不能寄託在盡力而為的串流上。
一個事件在它的點擊 5 分鐘後才落地。它算進哪一分鐘,窗口又要等多久?
以點擊的事件時間計數,而非到達時間:視窗會等一段有限的遲到時間(幾分鐘),更晚到的由每日對帳補回。
忽略事件時間與處理時間之分——以到達時間計數會悄悄把錢在分鐘之間挪來挪去。
把 廣告點擊聚合系統 大聲講一遍,讓 AI 為你的說明評分。