Case Study: Design Ad Click Event Aggregation

“1 tỷ lượt click mỗi ngày, mỗi click phải được đếm đúng 1 lần, kết quả phải có trong vài phút. Đây là bài toán mà sai 0.1% cũng có thể mất hàng triệu đô quảng cáo.”

Tags: system-design ad-click event-aggregation streaming exactly-once mapreduce alex-xu-vol2 Student: Hieu Source: Alex Xu — System Design Interview Volume 2, Chapter 3 Prerequisite: Tuan-08-Message-Queue · Tuan-02-Back-of-the-envelope · Tuan-01-Scale-From-Zero-To-Millions Lien quan: Tuan-13-Monitoring-Observability · Case-Design-Metrics-Monitoring-Alerting · Tuan-07-Database-Sharding-Replication


1. Context & Why — Tại sao Ad Click Event Aggregation quan trọng?

1.1 Analogy: Đếm phiếu bầu cử quốc gia real-time

Hiếu, bạn hãy tưởng tượng bài toán này như việc đếm phiếu bầu cử quốc gia diễn ra real-time:

Bầu cử quốc giaAd Click AggregationĐiểm chung
Hàng triệu phiếu từ khắp cả nướcHàng tỷ click từ khắp thế giớiVolume cực lớn, đến liên tục
Mỗi phiếu chỉ được đếm 1 lần — đếm 2 lần = gian lậnMỗi click chỉ được đếm 1 lần — đếm 2 lần = tính tiền sai nhà quảng cáoExactly-once là yêu cầu bắt buộc
Kết quả phải cập nhật liên tục — báo chí cần biết ai đang dẫn mỗi phútDashboard phải cập nhật liên tục — advertiser cần biết bao nhiêu click trong 1 phút quaReal-time aggregation với latency thấp
Chia theo khu vực — đếm theo tỉnh, theo thành phốChia theo filter — đếm theo ad_id, campaign_id, countryMulti-dimensional aggregation
Phiếu đến muộn — có văn phòng gửi kết quả chậm vì xaLate events — click event đến muộn do network delayLate event handling là thách thức lớn
Kiểm tra chéo — kết quả đếm thủ công phải khớp với máyReconciliation — batch job kiểm tra lại kết quả streamingReconciliation đảm bảo chính xác
Có thể bầu trùng — một người đi bầu 2 lầnClick fraud — bot click nhiều lầnDeduplication cần xử lý

Aha Moment: Bài toán này không phải là “đếm click” đơn giản. Nó là bài toán đếm đúng, đếm đủ, đếm nhanh ở quy mô hàng tỷ event mỗi ngày. Sai một chút = mất tiền thật — hoặc nhà quảng cáo bị tính thừa, hoặc platform mất doanh thu.

1.2 Tại sao bài toán này khó?

Hiếu, đếm số tưởng như đơn giản — nhưng đếm số ở quy mô lớn, real-time, và chính xác tuyệt đối lại là một trong những bài toán khó nhất của distributed systems:

Thách thứcGiải thích
Volume cực lớn1 tỷ click/ngày = ~11,574 click/giây trung bình, peak có thể 50K+/giây
Real-timeAdvertiser muốn thấy kết quả trong vài phút, không phải cuối ngày
Exactly-onceĐếm thừa = tính tiền sai. Đếm thiếu = mất doanh thu. Cả hai đều không chấp nhận được
Late eventsEvent có thể đến muộn 5 phút, 1 giờ, thậm chí 1 ngày do mobile network, CDN delay
Multi-dimensionalCần aggregate theo nhiều chiều: ad_id, campaign_id, country, device_type, time window
Fault toleranceHệ thống không được mất data khi server crash, network partition
Hot spotsQuảng cáo viral có thể nhận 100x traffic bình thường — tạo hot partition
Consistency vs LatencyCần cân bằng giữa độ chính xác và tốc độ trả kết quả

1.3 Business context — Tại sao advertiser quan tâm?

Trong mô hình quảng cáo online (Google Ads, Facebook Ads, TikTok Ads), advertiser trả tiền theo CPC (Cost Per Click). Mỗi click = một khoản tiền. Ví dụ:

  • Advertiser A chạy quảng cáo với CPC = $0.50
  • 1 ngày có 1 triệu click vào quảng cáo của A
  • Tổng chi phí = $500,000/ngày

Nếu hệ thống đếm sai 1% → sai 1.8 triệu/năm. Với hàng triệu advertiser, con số này nhân lên gấp bội. Đây là lý do data accuracy là non-negotiable.

1.4 Scope của bài toán

Thuộc tínhTrong scopeNgoài scope
Click event aggregation (đếm click theo ad_id, time window)Yes
Real-time query (advertiser xem dashboard)Yes
Filtering (theo campaign_id, country)Yes
Click fraud detection (bot, click farms)Liên quan (Section 4)Deep dive riêng
Conversion tracking (click → purchase)Bài toán khác
Ad serving (chọn quảng cáo nào hiển thị)Bài toán khác
Billing (tính tiền advertiser)Downstream của aggregation

2. Deep Dive — Alex Xu 4-Step Framework

Step 1 — Understand the Problem & Establish Design Scope

2.1.1 Functional Requirements

Chức năngMô tả chi tiết
Aggregate ad clicksĐếm số lượng click và unique click cho mỗi ad_id trong các time window: 1 phút, 5 phút, 1 giờ
Return aggregated dataQuery API trả về click_count và unique_click_count cho một ad_id trong một khoảng thời gian
Support filteringFilter kết quả theo ad_id, campaign_id, country, device_type
Support multi-dimensional aggregationAggregate theo nhiều chiều cùng lúc: ví dụ click count per ad_id per country per minute
Store raw eventsLưu raw click event để reconciliation và re-processing

Các câu hỏi bạn nên hỏi interviewer:

Câu hỏiCâu trả lời giả địnhTại sao hỏi
Bao nhiêu click/ngày?1 billion (1 tỷ) click/ngàyQuyết định throughput của toàn hệ thống
Bao nhiêu ad đang active?2 triệu adQuyết định cardinality của aggregation
Aggregation window nào?1 phút, 5 phút, 1 giờQuyết định windowing strategy
Latency yêu cầu?< vài phút từ click đến queryableQuyết định streaming vs batch
Data accuracy?Exactly-once — không được đếm thừa hay thiếuQuyết định delivery semantics
Cần store raw data không?Có — để reconciliation và ad-hoc analysisQuyết định storage strategy
Bao lâu giữ raw data?Raw: 7 ngày, Aggregated: yearsQuyết định data retention
Click fraud có trong scope không?Basic dedup có, advanced fraud detection ngoài scopeRanh giới thiết kế

2.1.2 Non-Functional Requirements

RequirementTargetLý do
Throughput1B clicks/ngày, peak 50K clicks/secVolume lớn, bursty traffic
LatencyEnd-to-end < vài phút (từ click đến queryable)Near-real-time dashboard
AccuracyExactly-once processingLiên quan đến tiền bạc
Availability99.99% cho ingestion pipelineMất event = mất doanh thu
ScalabilityScale horizontal khi traffic tăngMùa sale, viral ads
Fault toleranceKhông mất data khi node failureZero data loss
IdempotencyRe-process không thay đổi kết quảRetry safety
QueryabilityQuery response < 1 giây cho single ad_idDashboard UX

2.1.3 API Design

API 1: Aggregate click count cho một ad_id

GET /v1/ads/{ad_id}/aggregated_count

Parameters:

ParameterTypeMô tả
fromlong (epoch)Thời điểm bắt đầu
tolong (epoch)Thời điểm kết thúc
filterobjectOptional: {campaign_id: "xxx", country: "VN"}

Response:

FieldTypeMô tả
ad_idstringID của quảng cáo
click_countlongTổng số click
unique_click_countlongSố click từ distinct users

API 2: Top N clicked ads trong khoảng thời gian

GET /v1/ads/top_clicked

Parameters:

ParameterTypeMô tả
countintSố lượng top ads cần trả về (N)
windowintKhoảng thời gian tính bằng phút (1, 5, 60)
filterobjectOptional filter

Step 2 — High-Level Design

2.2.1 Kiến trúc tổng quan

Hiếu, kiến trúc này giống như dây chuyền xử lý cá trong nhà máy thủy sản:

  • Tàu cá (Client) mang cá về cảng → Click events từ user browsers/apps
  • Cảng cá (Load Balancer) phân phối cá vào các băng chuyền → Ingestion Service nhận events
  • Kho lạnh (Message Queue) giữ cá tươi trước khi chế biến → Kafka buffer events
  • Dây chuyền chế biến (Aggregation Service) cắt, lọc, đóng gói → Map-Reduce aggregate clicks
  • Kho thành phẩm (Database) lưu sản phẩm đã đóng gói → Aggregated DB lưu kết quả
  • Quầy bán hàng (Query Service) phục vụ khách hàng → Query API trả kết quả cho advertiser
flowchart LR
    subgraph "Data Generation"
        C1["User Clicks<br/>(Browsers/Apps)"]
    end

    subgraph "Log Ingestion"
        LB["Load Balancer"]
        IS1["Ingestion<br/>Service 1"]
        IS2["Ingestion<br/>Service 2"]
        ISN["Ingestion<br/>Service N"]
    end

    subgraph "Message Queue"
        K["Apache Kafka<br/>(Partitioned by ad_id)"]
    end

    subgraph "Streaming Aggregation"
        AG1["Aggregation Node 1<br/>(Map + Aggregate)"]
        AG2["Aggregation Node 2<br/>(Map + Aggregate)"]
        AGN["Aggregation Node N<br/>(Map + Aggregate)"]
    end

    subgraph "Second Message Queue (Optional)"
        K2["Kafka Topic 2<br/>(Aggregated Results)"]
    end

    subgraph "Storage"
        RAW[("Raw Data Store<br/>(Cassandra / S3)")]
        AGG[("Aggregated DB<br/>(Cassandra / ClickHouse)")]
    end

    subgraph "Serving"
        QS["Query Service"]
        CACHE[("Redis Cache")]
        DASH["Advertiser<br/>Dashboard"]
    end

    C1 --> LB
    LB --> IS1 & IS2 & ISN
    IS1 & IS2 & ISN --> K
    K --> AG1 & AG2 & AGN
    K -->|"Raw events"| RAW
    AG1 & AG2 & AGN --> K2
    K2 --> AGG
    AGG --> QS
    QS --> CACHE
    QS --> DASH

    style K fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff
    style K2 fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff
    style AGG fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff
    style QS fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff

2.2.2 Data Flow — End to End

sequenceDiagram
    participant User as User Browser
    participant IS as Ingestion Service
    participant Kafka as Kafka (Raw Events)
    participant AGG as Aggregation Service
    participant Kafka2 as Kafka (Aggregated)
    participant DB as Aggregated DB
    participant QS as Query Service
    participant ADV as Advertiser Dashboard

    User->>IS: Click event (ad_id, timestamp, user_id, ip, country)
    IS->>IS: Validate & enrich event
    IS->>Kafka: Produce to partition (hash ad_id)

    Note over Kafka,AGG: Streaming Aggregation (per minute window)
    Kafka->>AGG: Consume batch of events
    AGG->>AGG: Map: filter + transform
    AGG->>AGG: Aggregate: count by ad_id per window
    AGG->>AGG: Reduce: merge partial results
    AGG->>Kafka2: Produce aggregated result

    Kafka2->>DB: Write aggregated data
    Note over DB: (ad_id, window_start, click_count, unique_count)

    ADV->>QS: GET /v1/ads/123/aggregated_count?from=...&to=...
    QS->>DB: Query aggregated data
    DB-->>QS: Results
    QS-->>ADV: JSON response

2.2.3 Component Responsibilities

ComponentTrách nhiệmTechnology choices
Ingestion ServiceNhận click event từ client, validate, enrich (geo lookup), gửi vào KafkaCustom service (Go/Java), stateless, horizontally scalable
Kafka (Raw)Buffer raw events, decouple ingestion từ processing, replay capabilityApache Kafka, partitioned by ad_id
Raw Data StoreLưu raw events cho reconciliation, ad-hoc query, re-processingCassandra (write-heavy), hoặc S3 (cost-effective)
Aggregation ServiceStream processing: map, aggregate, reduce trong time windowApache Flink, Spark Streaming
Kafka (Aggregated)Buffer aggregated results trước khi ghi DB, decouple aggregation từ storageApache Kafka
Aggregated DBLưu kết quả aggregation, phục vụ read queriesCassandra, ClickHouse
Query ServiceParse query, fetch data từ DB, apply filter, return resultsCustom service với caching layer
Redis CacheCache hot queries (top ads, recent aggregations)Redis cluster

2.2.4 Tại sao cần 2 Kafka topics?

Lợi íchGiải thích
DecouplingAggregation service và DB writer có thể scale độc lập
Exactly-onceKafka transactions đảm bảo aggregated result được ghi chính xác 1 lần
ReplayNếu DB writer lỗi, có thể replay từ Kafka topic 2 mà không cần re-aggregate
Multiple consumersNhiều downstream systems có thể đọc aggregated data (alerting, billing, analytics)

Step 3 — Design Deep Dive

2.3.1 Data Model

Raw Click Event
FieldTypeMô tảVí dụ
ad_idstringID của quảng cáo”ad_001”
click_timestamplong (epoch ms)Thời điểm user click (event time)1678886400000
user_idstringID của user (cookie/device ID)“user_abc123”
ipstringIP address của user”203.113.152.4”
countrystringCountry code (từ IP geo lookup)“VN”
device_typestringLoại thiết bị”mobile”
campaign_idstringCampaign chứa ad này”camp_xyz”

Kích thước trung bình: ~0.5-1 KB per event.

Aggregated Data
FieldTypeMô tảVí dụ
ad_idstringID của quảng cáo”ad_001”
window_startlong (epoch)Bắt đầu của time window1678886400
window_sizeintKích thước window (phút)1
click_countlongTổng số click trong window45,231
unique_click_countlongSố click từ distinct users38,102
filter_idstringComposite key của filter dimensions”country=VN”

Aha Moment: Raw event và aggregated data có đặc điểm hoàn toàn khác nhau. Raw event write-heavy, append-only, high volume. Aggregated data read-heavy, update-in-place (upsert), lower volume. Đây là lý do chúng cần databases khác nhau — hoặc ít nhất là tables khác nhau với access patterns khác nhau.

Star Schema cho Aggregation

Hiếu, data model cho aggregation tương tự star schema trong data warehousing:

Thành phầnVai tròVí dụ
Fact tableLưu metric (click_count)aggregated_clicks (ad_id, window, click_count, unique_count)
Dimension tablesLưu thông tin filterads (ad_id, campaign_id, advertiser_id), campaigns (campaign_id, budget)

Query pattern: join fact table với dimension tables để filter theo campaign_id, advertiser_id, country.

2.3.2 Choosing the Right Database

Yêu cầuRaw DataAggregated Data
Write patternAppend-only, sequentialUpsert (update or insert)
Read patternRare (reconciliation, re-processing)Frequent (dashboard queries)
VolumeCực lớn (~1TB/ngày)Nhỏ hơn nhiều (~vài GB/ngày)
Query patternFull scan, time-range scanPoint query (ad_id + time range), top-N
Retention7 ngày (sau đó archive)Years
ConsistencyEventual OKStrong preferred
Raw Data → Cassandra hoặc S3
Lựa chọnƯu điểmNhược điểm
CassandraWrite-optimized (LSM-tree), time-series friendly, TTL support, distributedQuery flexibility hạn chế, expensive storage
S3 + ParquetCực rẻ, unlimited storage, Athena/Spark có thể queryLatency cao, không phù hợp real-time query
Hybrid (Cassandra 7 ngày + S3 archive)Kết hợp cả haiPhức tạp operations

Kết luận: Dùng Cassandra cho raw data với TTL 7 ngày. Archive sang S3 (Parquet format) cho long-term storage. Cassandra write-heavy performance rất phù hợp với use case này — partition key là ad_id, clustering key là click_timestamp.

Aggregated Data → Cassandra hoặc ClickHouse
Lựa chọnƯu điểmNhược điểm
CassandraConsistent với raw data store, write-optimizedOLAP queries chậm (không có secondary index tốt)
ClickHouseOLAP-optimized, column-oriented, blazing fast aggregation queriesWrite pattern khác (batch insert tốt hơn single upsert)
DruidReal-time OLAP, pre-aggregation tại ingestionPhức tạp operations

Kết luận: ClickHouse là lựa chọn tốt nhất cho aggregated data vì:

  1. Column-oriented storage nên tối ưu cho aggregation queries (SUM, COUNT, GROUP BY)
  2. Query speed cực nhanh cho dashboard use case
  3. Built-in materialized views có thể tự động aggregate
  4. Compression ratio cao cho time-series data

Trade-off: Nếu team chưa có kinh nghiệm với ClickHouse, dùng Cassandra cho cả raw và aggregated data để giảm operational complexity. Performance không tốt bằng nhưng đơn giản hơn.

2.3.3 Aggregation Service — MapReduce Model

Đây là trái tim của hệ thống. Aggregation service dùng mô hình MapReduce để xử lý events theo 3 giai đoạn:

Giai đoạn 1: Map Node (Filter + Transform)
Nhiệm vụChi tiết
Nhận raw event từ KafkaMỗi Map node nhận events từ 1 hoặc nhiều Kafka partitions
FilterLoại bỏ invalid events (missing ad_id, invalid timestamp)
TransformEnrich data (geo lookup từ IP → country), normalize fields
Emit key-valueOutput: (ad_id, 1) — nghĩa là “ad này được click 1 lần”
Giai đoạn 2: Aggregate Node (Count by ad_id per window)
Nhiệm vụChi tiết
Nhận (ad_id, 1) từ MapGroup by ad_id
Aggregate trong time windowĐếm số lượng clicks và unique clicks trong từng window (1 phút)
Maintain stateDùng in-memory state (Flink state backend) để lưu partial aggregation
Emit partial resultOutput: (ad_id, window, partial_count, partial_unique_count)
Giai đoạn 3: Reduce Node (Merge Results)
Nhiệm vụChi tiết
Nhận partial results từ nhiều Aggregate nodesTrường hợp ad_id bị chia ra nhiều node
MergeCộng tất cả partial_count lại, merge HyperLogLog cho unique count
Output final result(ad_id, window, final_click_count, final_unique_count)
Ghi vào Kafka topic 2Aggregated result sẵn sàng ghi DB
flowchart TB
    subgraph "Kafka Partitions"
        P1["Partition 0<br/>ad_001, ad_005, ad_009..."]
        P2["Partition 1<br/>ad_002, ad_006, ad_010..."]
        P3["Partition 2<br/>ad_003, ad_007, ad_011..."]
        P4["Partition 3<br/>ad_004, ad_008, ad_012..."]
    end

    subgraph "Map Nodes"
        M1["Map Node 1<br/>Filter + Transform<br/>Emit (ad_id, 1)"]
        M2["Map Node 2<br/>Filter + Transform<br/>Emit (ad_id, 1)"]
        M3["Map Node 3<br/>Filter + Transform<br/>Emit (ad_id, 1)"]
        M4["Map Node 4<br/>Filter + Transform<br/>Emit (ad_id, 1)"]
    end

    subgraph "Aggregate Nodes (Windowed)"
        A1["Aggregate Node 1<br/>Window: 1 min<br/>Count by ad_id"]
        A2["Aggregate Node 2<br/>Window: 1 min<br/>Count by ad_id"]
    end

    subgraph "Reduce Nodes"
        R1["Reduce Node<br/>Merge partial counts<br/>Final result per ad_id"]
    end

    subgraph "Output"
        K2["Kafka Topic 2<br/>(Aggregated Results)"]
        DB[("ClickHouse")]
    end

    P1 --> M1
    P2 --> M2
    P3 --> M3
    P4 --> M4

    M1 & M2 --> A1
    M3 & M4 --> A2

    A1 & A2 --> R1
    R1 --> K2
    K2 --> DB

    style M1 fill:#42A5F5,color:#fff
    style M2 fill:#42A5F5,color:#fff
    style M3 fill:#42A5F5,color:#fff
    style M4 fill:#42A5F5,color:#fff
    style A1 fill:#FFA726,color:#fff
    style A2 fill:#FFA726,color:#fff
    style R1 fill:#EF5350,color:#fff

Aha Moment: Trong thực tế, khi dùng Apache Flink, Map và Aggregate thường merge thành 1 operator. Flink tự động làm chuyện này qua operator chaining để giảm network overhead. Mô hình 3 giai đoạn là để hiểu concept — implementation thực tế compact hơn.

Unique Click Count — HyperLogLog

Đếm unique clicks (distinct user_id) là bài toán khó vì:

  • Không thể giữ tất cả user_id trong memory (quá nhiều)
  • Exact count yêu cầu O(n) memory per ad_id per window

Giải pháp: Dùng HyperLogLog (HLL) — probabilistic data structure:

Thuộc tínhGiá trị
Memory~12 KB per counter (cố định, bất kể bao nhiêu elements)
AccuracySai số ~0.81% (với 16K registers)
MergeableCó thể merge 2 HLL counters → perfect cho distributed aggregation
Trade-offMất độ chính xác tuyệt đối, nhưng tiết kiệm memory cực lớn

Với 2 triệu active ads, mỗi ad 1 HLL counter = 2M x 12KB = ~24 GB memory — hoàn toàn khả thi.

Nếu cần exact unique count (một số advertiser trả thêm tiền cho độ chính xác), dùng bitmap hoặc Bloom filter với memory lớn hơn.

2.3.4 Streaming vs Batching — Lambda vs Kappa Architecture

Đây là quyết định kiến trúc quan trọng nhất của bài toán. Hiếu cần hiểu 2 trường phái:

Lambda Architecture (Batch + Speed Layer)
flowchart TB
    subgraph "Data Source"
        EVENTS["Click Events"]
    end

    subgraph "Batch Layer"
        BATCH_STORE[("Batch Storage<br/>(HDFS / S3)")]
        BATCH_PROC["Batch Processing<br/>(Spark / MapReduce)<br/>Chạy mỗi giờ hoặc mỗi ngày"]
        BATCH_VIEW[("Batch View<br/>(accurate, complete)")]
    end

    subgraph "Speed Layer"
        STREAM_PROC["Stream Processing<br/>(Flink / Storm)<br/>Real-time"]
        RT_VIEW[("Real-time View<br/>(approximate, fast)")]
    end

    subgraph "Serving Layer"
        MERGE["Merge Results<br/>Batch View + Real-time View"]
        QS["Query Service"]
    end

    EVENTS --> BATCH_STORE
    EVENTS --> STREAM_PROC
    BATCH_STORE --> BATCH_PROC
    BATCH_PROC --> BATCH_VIEW
    STREAM_PROC --> RT_VIEW
    BATCH_VIEW --> MERGE
    RT_VIEW --> MERGE
    MERGE --> QS

    style BATCH_PROC fill:#4CAF50,color:#fff
    style STREAM_PROC fill:#FF9800,color:#fff
    style MERGE fill:#2196F3,color:#fff
Ưu điểmNhược điểm
Batch layer đảm bảo accuracy2 codebases — phải maintain 2 pipeline (batch + streaming)
Speed layer đảm bảo low latencyLogic trùng lặp — cùng 1 aggregation nhưng viết 2 lần
Nếu streaming sai, batch sẽ sửaPhức tạp debug — sai ở layer nào?
Merge logic không đơn giản
Kappa Architecture (Streaming Only)
flowchart LR
    subgraph "Data Source"
        EVENTS["Click Events"]
    end

    subgraph "Streaming Layer (Only Layer)"
        KAFKA["Kafka<br/>(Immutable Log)"]
        FLINK["Stream Processing<br/>(Apache Flink)<br/>Real-time Aggregation"]
    end

    subgraph "Serving Layer"
        DB[("Aggregated DB")]
        QS["Query Service"]
    end

    subgraph "Reconciliation (Periodic)"
        RECON["Batch Reconciliation<br/>(Hourly / Daily)<br/>Verify streaming results"]
    end

    EVENTS --> KAFKA
    KAFKA --> FLINK
    FLINK --> DB
    DB --> QS
    KAFKA -.->|"Replay if needed"| FLINK
    DB -.-> RECON
    KAFKA -.-> RECON

    style KAFKA fill:#FF9800,color:#fff
    style FLINK fill:#4CAF50,color:#fff
    style RECON fill:#9E9E9E,color:#fff
Ưu điểmNhược điểm
1 codebase — chỉ cần maintain 1 pipelineCần streaming framework mature (Flink có exactly-once)
Đơn giản hơn Lambda nhiềuNếu cần re-process, replay từ Kafka (cần đủ retention)
Kafka là immutable log — có thể replay bất kỳ lúc nào
Apache Flink hỗ trợ exactly-once processing

Kết luận của Alex Xu: Kappa architecture là lựa chọn ưu tiên cho bài toán này. Lý do: Apache Flink đã mature đủ để đảm bảo exactly-once processing. Lambda chỉ cần thiết khi streaming framework chưa đủ tin cậy — điều này không còn đúng với Flink 2024+.

Aha Moment: Lambda architecture ra đời vì streaming frameworks ngày xưa (Storm, Samza) không đảm bảo exactly-once. Khi Flink giải quyết vấn đề này, lý do tồn tại của Lambda giảm đi đáng kể. Tuy nhiên, reconciliation (batch job kiểm tra lại) vẫn cần thiết như safety net — đây không phải là Lambda, mà là best practice.

2.3.5 Watermark và Late Events

Đây là khái niệm khó nhất trong stream processing. Hiếu cần hiểu rõ:

Event Time vs Processing Time
Khái niệmĐịnh nghĩaVí dụ
Event timeThời điểm sự kiện thực sự xảy ra (user click)14:00:00 — user click vào quảng cáo
Processing timeThời điểm hệ thống nhận được event14:00:05 — event đến Flink (trễ 5 giây do network)
Ingestion timeThời điểm event vào Kafka14:00:02 — Kafka nhận event

Tại sao quan trọng? Vì bạn aggregate theo time window. Nếu dùng processing time, 1 click xảy ra lúc 13:59:59 nhưng đến lúc 14:00:05 sẽ bị đếm vào window 14:00-14:01 thay vì 13:59-14:00. Sai window = sai kết quả.

Kết luận: Luôn dùng event time cho aggregation. Processing time chỉ dùng cho monitoring hệ thống.

Watermark là gì?

Watermark = lời tuyên bố của hệ thống rằng: “Tất cả event với event_time W đã đến hết”.

Ví dụ: Watermark = 14:05:00 có nghĩa là hệ thống tin rằng không còn event nào với timestamp trước 14:05:00 sẽ đến nữa. Hệ thống có thể đóng (close) tất cả window trước 14:05:00 và emit kết quả.

Vấn đề: Làm sao biết tất cả event đã đến? Không thể biết chắc chắn. Vì vậy watermark luôn có allowed lateness (độ trễ cho phép).

Watermark strategyMô tảTrade-off
Punctuated watermarkEmit watermark dựa trên event data (ví dụ: max event_time - 5s)Phù hợp traffic đều
Periodic watermarkEmit watermark định kỳ (ví dụ: mỗi 200ms, watermark = max event_time seen - 10s)Phổ biến nhất, đơn giản
Watermark = max_event_time - allowed_latenessVí dụ: max_event_time - 5 phútBalance giữa latency và completeness
Late Event Handling

Khi event đến sau watermark (late event), có 3 cách xử lý:

StrategyMô tảKhi nào dùng
DropBỏ qua late eventKhi data accuracy không quan trọng (dashboard xấp xỉ)
Update aggregationMở lại window, cập nhật kết quả, emit lạiKhi cần accuracy cao — dùng cho ad click
Side outputGửi late event sang 1 stream riêng để xử lý sauKhi late event cần xử lý khác (reconciliation, alerting)

Trong bài toán ad click, Alex Xu khuyên dùng kết hợp Update + Side output:

  • Late event trong 5 phút: update aggregation (mở lại window, đếm lại)
  • Late event quá 5 phút: gửi vào side output → reconciliation batch job xử lý

Aha Moment: Watermark là khái niệm khó nhất trong stream processing vì nó liên quan đến trade-off giữa latency và completeness. Watermark quá nhanh (allowed_lateness nhỏ) → nhiều late events bị bỏ → sai kết quả. Watermark quá chậm (allowed_lateness lớn) → latency cao → dashboard cập nhật chậm. Không có giá trị “đúng” — chỉ có giá trị “phù hợp với business yêu cầu”.

2.3.6 Exactly-Once Processing

Trong hệ thống phân tán, exactly-once là cực kỳ khó vì có nhiều điểm failure:

Điểm failureVấn đềGiải pháp
Kafka consumer đọc eventConsumer crash sau khi xử lý nhưng trước khi commit offsetFlink checkpointing (snapshot state + Kafka offset cùng lúc)
Aggregation serviceNode crash giữa chừng xử lýFlink savepoints/checkpoints — resume từ last consistent state
Ghi vào DBGhi thành công nhưng chưa ackIdempotent writes — ghi lại cùng data không thay đổi kết quả
Kafka producer (ghi aggregated result)Producer crash sau khi Kafka nhận nhưng trước khi ackKafka idempotent producer (enable.idempotence=true)
Exactly-Once End-to-End Flow
flowchart TB
    subgraph "Source: Kafka Consumer"
        KC["Kafka Consumer<br/>Track offset per partition"]
    end

    subgraph "Processing: Flink"
        CP["Checkpoint Manager<br/>(Periodic snapshots)"]
        STATE["Flink State Backend<br/>(RocksDB / Heap)"]
        OP["Aggregation Operator<br/>(Stateful processing)"]
    end

    subgraph "Sink: Kafka Producer + DB"
        KP["Kafka Producer<br/>(Idempotent + Transactional)"]
        DB[("ClickHouse<br/>(Idempotent Upsert)")]
    end

    subgraph "Checkpoint Flow"
        BARRIER["Checkpoint Barrier<br/>(injected into stream)"]
    end

    KC -->|"Events"| OP
    OP --> STATE
    OP --> KP
    KP --> DB

    CP -->|"Trigger checkpoint"| BARRIER
    BARRIER -->|"Snapshot: offset + state + pending writes"| CP
    CP -->|"On success: commit Kafka offset + flush writes"| KC
    CP -->|"On success: commit Kafka transaction"| KP

    style CP fill:#e53935,color:#fff
    style STATE fill:#1e88e5,color:#fff
    style OP fill:#43a047,color:#fff

Cơ chế Flink Checkpoint hoạt động như thế nào:

  1. Checkpoint Manager định kỳ (ví dụ mỗi 30 giây) inject checkpoint barrier vào stream
  2. Khi barrier đi qua operator, operator snapshot state (partial aggregation, HLL counters) vào durable storage (HDFS/S3)
  3. Đồng thời lưu Kafka consumer offset tại thời điểm đó
  4. Khi tất cả operators đã snapshot xong → checkpoint hoàn thành
  5. Nếu failure xảy ra → Flink rollback về last successful checkpoint:
    • Reset Kafka consumer offset về checkpoint offset (đọc lại events từ đó)
    • Restore operator state từ checkpoint
    • Xử lý lại events từ checkpoint offset → kết quả giống hệt vì state giống hệt

Quan trọng: Exactly-once trong Flink không có nghĩa là mỗi event chỉ được xử lý 1 lần. Nó có nghĩa là kết quả cuối cùng giống như mỗi event chỉ được xử lý 1 lần. Thực tế, events có thể được xử lý lại (sau checkpoint restore) nhưng kết quả không thay đổi nhờ idempotent writes.

2.3.7 Deduplication — Cùng 1 click bị gửi 2 lần

Khác với exactly-once (vấn đề của hệ thống), deduplication xử lý vấn đề từ phía client:

Nguyên nhânMô tảVí dụ
Client retryUser click, request timeout, client gửi lại1 click → 2 events
Double clickUser click nhanh 2 lần2 clicks thực tế nhưng chỉ nên đếm 1
Page reloadUser refresh trang có quảng cáoClick event gửi lại
CDN/Proxy retryIntermediate server retry request1 click → nhiều events
Dedup Strategy
Cách tiếp cậnMô tảTrade-off
Dedup key: (ad_id, user_id, click_timestamp)2 events có cùng 3 field = duplicateCần lưu seen keys trong window → memory
Dedup windowChỉ check duplicate trong 1 khoảng thời gian (ví dụ 5 phút)Duplicate xa hơn window không bị bắt
Bloom filterProbabilistic — check “đã thấy chưa?” với false positive rate thấpTiết kiệm memory, nhưng có false positive (bỏ nhầm event thật)
External dedup store (Redis)Lưu dedup key trong Redis với TTLChính xác hơn Bloom filter, nhưng thêm latency và dependency

Kết luận: Dùng Bloom filter trong Flink operator với dedup window 5 phút. False positive rate ~0.1% là chấp nhận được (bỏ nhầm 1/1000 click thật — insignificant với 1 tỷ clicks/ngày).

Cho trường hợp cần chính xác tuyệt đối (billing), dùng Redis dedup với key {ad_id}:{user_id}:{minute_bucket} và TTL 10 phút.

2.3.8 Time Window Types

Flink (và các streaming framework khác) hỗ trợ nhiều loại time window:

Tumbling Window
Thuộc tínhGiá trị
Định nghĩaFixed-size, non-overlapping windows
Ví dụWindow 1 phút: [14:00-14:01), [14:01-14:02), [14:02-14:03)
Khi nào dùngAd click aggregation — đếm click trong từng phút, từng giờ
Đặc điểmMỗi event thuộc đúng 1 window. Đơn giản nhất, efficient nhất
Sliding Window
Thuộc tínhGiá trị
Định nghĩaFixed-size windows nhưng overlap với nhau
Ví dụWindow 5 phút, slide mỗi 1 phút: [14:00-14:05), [14:01-14:06), [14:02-14:07)
Khi nào dùngKhi cần “moving average” — ví dụ “số click trong 5 phút gần nhất” cập nhật mỗi phút
Đặc điểmMỗi event thuộc nhiều windows. Tốn memory hơn tumbling
Session Window
Thuộc tínhGiá trị
Định nghĩaWindow đóng khi không có event mới trong 1 khoảng thời gian (gap)
Ví dụSession gap 30 phút: events lúc 14:00, 14:05, 14:10 thuộc 1 session. Event lúc 14:50 bắt đầu session mới
Khi nào dùngUser behavior analysis — 1 session browsing của user
Đặc điểmKích thước window động (dynamic), không cố định

Cho bài toán ad click: Dùng tumbling window cho 1 phút, 5 phút, 1 giờ. Đây là lựa chọn tự nhiên nhất vì:

  • Mỗi click chỉ thuộc 1 window → đơn giản, không duplicate counting
  • Window 5 phút = aggregate 5 windows 1-phút → có thể tính từ window nhỏ hơn
  • Window 1 giờ = aggregate 60 windows 1-phút → tiếp tục cascade

Aha Moment: Không cần 3 pipeline riêng cho 3 window sizes. Chỉ cần aggregate window 1 phút (smallest), sau đó cascade aggregation: 5 windows 1-phút → 1 window 5-phút, 12 windows 5-phút → 1 window 1-giờ. Đây tiết kiệm compute và storage đáng kể.

2.3.9 Scaling — Xử lý traffic lớn

Kafka Partitioning
Chiến lượcMô tảƯu/Nhược
Partition by ad_idHash(ad_id) % num_partitionsTất cả clicks của 1 ad vào cùng partition → aggregation đơn giản, NHƯNG hot ads tạo hot partition
Partition by ad_id + minuteHash(ad_id + minute_bucket)Phân tán hơn, nhưng cần reduce step để merge
Random partitionRound-robinPhân tán đều nhất, nhưng aggregation phải shuffle data (expensive)

Kết luận: Dùng partition by ad_id làm default. Xử lý hot partition riêng (Section 2.3.11).

ComponentParallelismLý do
Source (Kafka consumer)= Số Kafka partitionsMỗi consumer đọc 1 partition
Map operator= Số Kafka partitions1:1 với source
Aggregate operatorTùy throughputkeyBy(ad_id) tự động distribute
Sink (Kafka producer)Tùy throughputCó thể nhỏ hơn source

Khi cần scale:

  1. Tăng Kafka partitions (ví dụ từ 100 → 200)
  2. Tăng Flink parallelism tương ứng
  3. Tăng consumer group instances
  4. Flink auto-scaling (reactive mode) — Flink 1.13+ hỗ trợ
Aggregation Node Horizontal Scaling

Flink tự động distribute state dựa trên key group. Khi tăng parallelism:

  • State của key group A được migrate từ node 1 sang node 2
  • Trong quá trình migration, có brief pause (managed bởi checkpoint/restore)
  • Không cần manual resharding như với database

2.3.10 Fault Tolerance

Cơ chếMứcMô tả
Flink CheckpointsStreamingPeriodic snapshot của tất cả operator state + Kafka offsets. Automatic recovery
Flink SavepointsStreamingManual checkpoint cho planned maintenance (upgrade, config change)
Kafka Consumer Group RebalancingMessagingKhi 1 consumer die, partitions được redistribute cho consumers còn lại
Kafka ReplicationMessagingMỗi partition có 3 replicas — tolerate 2 broker failures
DB ReplicationStorageCassandra/ClickHouse replication factor 3
End-to-end exactly-onceToàn hệ thốngFlink checkpoint + Kafka transaction + idempotent DB write

Failure scenarios và recovery:

ScenarioHệ thống phản ứng
1 Flink TaskManager crashJobManager detect (heartbeat timeout) → redeploy tasks → restore từ last checkpoint
Flink JobManager crashStandby JobManager take over (HA mode với ZooKeeper) → restore job từ last checkpoint
1 Kafka broker crashISR (In-Sync Replicas) → leader election → consumer reconnect → continue từ last committed offset
DB node crashReplication → query route sang replica → repair node sau
Network partition (Flink ↔ Kafka)Consumer timeout → rebalance → reconnect → replay từ last offset
Entire data center failureCross-DC replication (Kafka MirrorMaker, Cassandra multi-DC) → failover sang DC khác

2.3.11 Reconciliation — Verify Streaming Results

Dù Flink có exactly-once, vẫn cần reconciliation vì:

  • Bug trong aggregation logic
  • Clock skew giữa servers
  • Edge cases trong watermark/late event handling
  • Data corruption
Reconciliation Flow
BướcMô tả
1. Batch job chạy mỗi giờĐọc raw events từ Cassandra/S3 cho window vừa qua
2. Re-aggregateTính lại click_count và unique_count bằng batch processing (Spark)
3. So sánhCompare batch result với streaming result trong aggregated DB
4. Detect driftNếu sai lệch > threshold (ví dụ 0.1%) → alert
5. Auto-correct (optional)Ghi đè kết quả streaming bằng kết quả batch (batch là source of truth)
MetricThresholdAction
Drift < 0.01%NormalLog only
Drift 0.01% - 0.1%WarningAlert team
Drift > 0.1%CriticalAlert + auto-correct + investigate root cause

Aha Moment: Reconciliation là safety net, không phải primary mechanism. Trong hệ thống tốt, reconciliation luôn cho kết quả khớp (drift ~ 0%). Nếu drift thường xuyên > 0, có bug trong streaming pipeline.

2.3.12 Hot Shard Problem — Quảng cáo viral

Vấn đề: Quảng cáo viral (ví dụ Super Bowl ad) có thể nhận 100x-1000x traffic bình thường. Vì partition by ad_id, tất cả clicks của ad này vào 1 Kafka partition1 Flink subtask → hot shard → bottleneck.

Triệu chứngHậu quả
1 Kafka partition nhận quá nhiều dataConsumer lag tăng, latency tăng
1 Flink subtask dùng quá nhiều CPU/memoryBackpressure, checkpoint timeout
Các partition/subtask khác idleWaste resources
Giải pháp: Salted Keys + Secondary Aggregation
BướcMô tả
1. Detect hot keyMonitor click rate per ad_id. Nếu vượt threshold (ví dụ 10x average) → mark as hot
2. Salt the keyThay vì partition by ad_id, partition by ad_id + random(0..N) với N = số shards mong muốn
3. First aggregationMỗi shard aggregate riêng → N partial results cho 1 ad_id
4. Secondary aggregation1 reducer merge N partial results → final result cho ad_id

Ví dụ với ad_001 là hot key, N=10:

  • Thay vì 1 partition nhận tất cả clicks của ad_001
  • 10 partitions nhận clicks: ad_001_0, ad_001_1, …, ad_001_9
  • 10 aggregation nodes đếm riêng
  • 1 reduce node cộng 10 partial counts lại

Trade-off:

  • Pro: Traffic phân tán đều, không còn hot shard
  • Con: Thêm 1 aggregation step, logic phức tạp hơn, latency tăng chút ít
  • Con: Unique count (HLL) cần merge — nhưng HLL hỗ trợ merge tốt nên không vấn đề

Aha Moment: Hot shard problem không chỉ xảy ra trong bài toán này. Nó xuất hiện bất cứ khi nào bạn partition by key và 1 key có traffic bất cân xứng. Giải pháp “salted keys + secondary aggregation” là general pattern áp dụng được cho nhiều bài toán khác.


3. Back-of-the-Envelope Estimation

3.1 Traffic Estimation

Assumptions:

  • 1 tỷ (1 billion) clicks/ngày
  • Peak traffic = 5x average

3.2 Kafka Throughput

Raw event size: ~1 KB (ad_id + timestamp + user_id + ip + country + metadata)

Kafka partition estimation:

  • Mỗi partition handle ~10 MB/sec (conservative)
  • Số partitions cần thiết cho peak: partitions tối thiểu
  • Thực tế dùng 100-200 partitions để scale consumers và đảm bảo parallelism cho Flink

Kafka retention:

  • Raw topic retention: 7 ngày
  • Storage per day:
  • 7-day retention:
  • Với replication factor 3: Kafka cluster storage

3.3 Raw Data Storage

3.4 Aggregated Data Storage

Assumptions:

  • 2 triệu active ads
  • Aggregation windows: 1 phút, 5 phút, 1 giờ
  • Mỗi aggregated record: ~100 bytes (ad_id + window + counts)

Với ClickHouse compression ratio ~10:1:

HyperLogLog cho unique count:

Partial aggregation state (tumbling window 1 min):

Tổng memory Flink state: ~25 GB — phân bổ trên nhiều TaskManagers, hoàn toàn khả thi.

3.6 Tổng hợp

ResourceGiá trị
Traffic11.5K avg / 58K peak clicks/sec
Kafka cluster storage21 TB (7-day retention, RF=3)
Kafka throughput11 MB/s avg / 57 MB/s peak
Kafka partitions100-200
Raw data (Cassandra)7 TB (7-day TTL)
Raw data (S3 archive)73 TB/year (compressed)
Aggregated data (ClickHouse)10.5 TB/year (compressed)
Flink state memory~25 GB (distributed)
Flink TaskManagers10-20 (tùy CPU/memory spec)

4. Security & Data Privacy

4.1 Click Fraud Detection

Click fraud là mối đe dọa lớn nhất đối với hệ thống quảng cáo. Nếu không phát hiện, advertiser mất tiền, platform mất uy tín.

Loại fraudMô tảDấu hiệu nhận biết
Bot clicksAutomated scripts click quảng cáo liên tụcClick rate bất thường từ 1 IP, không có mouse movement/scroll
Click farmsNhóm người được trả tiền để clickNhiều clicks từ cùng geo location, pattern giống nhau
Competitor clickingĐối thủ click quảng cáo của bạn để hao ngân sáchClicks từ cùng IP range, không conversion
Ad stackingNhiều quảng cáo chồng lên nhau, 1 click đếm cho nhiều adClick coordinates giống nhau cho nhiều ads
Pixel stuffingQuảng cáo hiển thị 1x1 pixel, không ai thấy nhưng đếm là “impression”Impression không tương ứng với click
Anti-Fraud Strategies
StrategyMô tảKhi nào áp dụng
IP-based rate limitingGiới hạn số click từ 1 IP per ad per time windowReal-time, tại ingestion layer
User-agent fingerprintingPhát hiện bot dựa trên browser fingerprintReal-time, tại ingestion layer
Click-through rate (CTR) anomalyCTR bất thường cao cho 1 ad → suspiciousNear-real-time, tại aggregation layer
Geo anomalyClick từ country không phù hợp với target audienceNear-real-time
Session analysisPhân tích hành vi sau click (bounce rate, time on page)Batch analysis
Machine learning modelTrain model phát hiện fraud patternsBatch + real-time inference

Implementation: Fraud detection không làm trong aggregation pipeline chính. Thay vào đó:

  1. Raw events đi qua fraud detection service (parallel với aggregation)
  2. Fraud service flag suspicious clicks
  3. Flagged clicks không được đếm trong aggregation (hoặc đếm riêng)
  4. Advertiser có thể dispute và review flagged clicks

4.2 User Privacy

Nguyên tắcMô tả
Aggregate onlyReport chỉ chứa aggregated numbers (click_count), KHÔNG chứa individual user data
No PII in reportsuser_id, ip KHÔNG xuất hiện trong aggregated results
PII only for dedupuser_id chỉ dùng trong dedup window (5 phút), sau đó discard
IP hashingIP address được hash trước khi lưu (không lưu raw IP)
Data minimizationChỉ thu thập data cần thiết cho aggregation, không hơn
GDPR/CCPA complianceUser có quyền yêu cầu xóa data. Raw data TTL 7 ngày hỗ trợ điều này
Consent-based trackingChỉ track click khi user đã consent (cookie consent banner)

4.3 Data Retention Policies

Data typeRetentionLý doStorage
Raw click events7 ngày (hot) + 90 ngày (archive)Reconciliation + dispute resolutionCassandra → S3
Aggregated data (1-min)1 nămDashboard + reportingClickHouse
Aggregated data (1-hour)3 nămLong-term analyticsClickHouse (compressed)
Aggregated data (1-day)Vĩnh viễnHistorical trendClickHouse (cold storage)
Dedup keys10 phútDedup windowRedis (TTL)
Fraud detection logs1 nămAudit + disputeS3

5. DevOps & Monitoring

5.1 Key Metrics cần monitor

Kafka Metrics

MetricMô tảAlert threshold
Consumer lagSố messages chưa được consume> 100K messages → Warning, > 1M → Critical
Under-replicated partitionsPartitions chưa đủ replicas> 0 → Warning
Broker disk usageDisk usage của Kafka broker> 80% → Warning, > 90% → Critical
Producer error rateTỷ lệ lỗi khi produce message> 0.1% → Warning
Request latency (p99)Latency của produce/consume request> 100ms → Warning
MetricMô tảAlert threshold
Checkpoint durationThời gian hoàn thành 1 checkpoint> 30s → Warning, > 60s → Critical
Checkpoint failure rateTỷ lệ checkpoint thất bại> 0 trong 1 giờ → Critical
BackpressureOperator đang bị chậm, buffer đầyisBackPressured = true > 5 phút → Warning
Records per secondThroughput của Flink jobGiảm > 50% so với baseline → Warning
State sizeKích thước state của operatorsTăng > 2x so với baseline → Warning
Late events droppedSố late events bị drop> 1% total events → Warning
TaskManager failuresSố TaskManager crash> 0 → Alert

Data Freshness & Accuracy

MetricMô tảAlert threshold
End-to-end latencyThời gian từ click xảy ra đến data queryable trong DB> 5 phút → Warning, > 10 phút → Critical
Data freshnessKhoảng cách giữa “now” và timestamp của record mới nhất trong DB> 3 phút → Warning
Reconciliation drift% chênh lệch giữa streaming và batch result> 0.01% → Warning, > 0.1% → Critical
Query latency (p99)Latency của query service> 500ms → Warning, > 2s → Critical

Database Metrics

MetricMô tảAlert threshold
Write latency (p99)Latency ghi vào ClickHouse/Cassandra> 100ms → Warning
Disk usageStorage usage> 75% → Warning
Compaction lag (Cassandra)Pending compactions> 10 → Warning
Query queue size (ClickHouse)Số queries đang chờ> 50 → Warning

5.2 Alerting Strategy

SeverityVí dụNotificationResponse time
P1 - CriticalConsumer lag > 1M, checkpoint failures, data freshness > 10 minPagerDuty (phone call)< 15 phút
P2 - WarningConsumer lag > 100K, reconciliation drift > 0.01%, high backpressureSlack + PagerDuty (SMS)< 1 giờ
P3 - InfoTraffic spike, scheduled maintenanceSlackNext business day

5.3 Runbook — Common Issues

Vấn đềNguyên nhân thường gặpCách xử lý
Consumer lag tăng đột ngộtTraffic spike (viral ad), slow consumer, GC pauseScale Flink parallelism, check backpressure, check GC logs
Checkpoint failureState quá lớn, slow storage, network issueTăng checkpoint timeout, check state size, check HDFS/S3 health
Reconciliation driftBug trong aggregation logic, clock skew, late eventsSo sánh raw events với aggregated results, check watermark config
High query latencyClickHouse overloaded, missing index, large result setAdd materialized view, optimize query, scale read replicas
Kafka broker downDisk full, hardware failure, network partitionCheck disk, failover sang replica, add new broker

5.4 Deployment Strategy

Chiến lượcMô tảKhi nào dùng
Blue-green deploymentChạy 2 Flink jobs song song (old + new), compare results, switch trafficMajor logic changes
Canary deploymentRoute 5% traffic sang new version, monitor metricsMinor changes
Savepoint + restartTake savepoint của Flink job, deploy new version, restore từ savepointConfig changes, minor updates
A/B testing2 pipelines xử lý cùng data, compare resultsTesting new aggregation logic

6. Mermaid Diagrams — Tổng hợp

6.1 High-Level Architecture (Complete)

flowchart TB
    subgraph "Clients"
        BROWSER["User Browsers"]
        APP["Mobile Apps"]
    end

    subgraph "Ingestion Layer"
        LB["Load Balancer<br/>(L7)"]
        IS["Ingestion Service Cluster<br/>(Stateless, Auto-scale)"]
    end

    subgraph "Message Queue Layer"
        KR["Kafka — Raw Events Topic<br/>(100+ partitions, RF=3)"]
    end

    subgraph "Processing Layer"
        FRAUD["Fraud Detection<br/>Service (Async)"]
        FLINK["Apache Flink Cluster<br/>Map → Aggregate → Reduce"]
    end

    subgraph "Storage Layer"
        RAW_CASS[("Cassandra<br/>Raw Events<br/>TTL 7 days")]
        RAW_S3[("S3 Archive<br/>Raw Events<br/>Parquet")]
        AGG_CH[("ClickHouse<br/>Aggregated Data")]
    end

    subgraph "Serving Layer"
        QS["Query Service"]
        REDIS[("Redis Cache")]
    end

    subgraph "Reconciliation"
        SPARK["Spark Batch Job<br/>(Hourly)"]
    end

    subgraph "Consumers"
        DASH["Advertiser Dashboard"]
        BILLING["Billing Service"]
        ANALYTICS["Analytics Platform"]
    end

    BROWSER & APP --> LB
    LB --> IS
    IS --> KR
    KR --> FRAUD
    KR --> FLINK
    KR --> RAW_CASS
    RAW_CASS -.->|"Archive"| RAW_S3
    FLINK --> AGG_CH
    AGG_CH --> QS
    QS --> REDIS
    QS --> DASH & BILLING & ANALYTICS

    RAW_S3 -.-> SPARK
    AGG_CH -.-> SPARK

    style KR fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff
    style FLINK fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff
    style AGG_CH fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff
    style FRAUD fill:#f44336,stroke:#333,stroke-width:2px,color:#fff
    style SPARK fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff

6.2 Exactly-Once End-to-End Flow (Detailed)

sequenceDiagram
    participant Kafka as Kafka (Source)
    participant Flink as Flink Operator
    participant State as Flink State (RocksDB)
    participant CM as Checkpoint Manager
    participant Storage as Durable Storage (HDFS)
    participant Sink as Kafka (Sink) + DB

    Note over Kafka,Sink: Normal Processing
    Kafka->>Flink: Event batch (offset 100-200)
    Flink->>State: Update aggregation state
    Flink->>Sink: Emit partial result (uncommitted)

    Note over CM,Storage: Checkpoint Triggered (every 30s)
    CM->>Flink: Inject checkpoint barrier
    Flink->>State: Snapshot current state
    State->>Storage: Persist state snapshot
    Flink->>CM: Report: offset=200, state=snapshot_42
    CM->>Kafka: Commit consumer offset=200
    CM->>Sink: Commit Kafka transaction (flush writes to DB)

    Note over Kafka,Sink: Failure & Recovery
    Flink->>Flink: TaskManager CRASH!
    CM->>Storage: Load last checkpoint (snapshot_42, offset=200)
    CM->>Kafka: Reset consumer to offset=200
    CM->>Flink: Restore state from snapshot_42
    Kafka->>Flink: Replay events from offset 200
    Flink->>State: Re-process (same state → same result)
    Flink->>Sink: Re-emit results (idempotent write → no duplicate)

6.3 Lambda vs Kappa Architecture Comparison

flowchart TB
    subgraph "Lambda Architecture"
        direction TB
        L_SRC["Events"] --> L_BATCH["Batch Layer<br/>(Spark, chạy mỗi giờ)"]
        L_SRC --> L_SPEED["Speed Layer<br/>(Flink, real-time)"]
        L_BATCH --> L_BV["Batch View<br/>(accurate, stale)"]
        L_SPEED --> L_RV["Real-time View<br/>(fast, approximate)"]
        L_BV --> L_MERGE["Merge"]
        L_RV --> L_MERGE
        L_MERGE --> L_QS["Query"]

        style L_BATCH fill:#4CAF50,color:#fff
        style L_SPEED fill:#FF9800,color:#fff
        style L_MERGE fill:#f44336,color:#fff
    end

    subgraph "Kappa Architecture (Preferred)"
        direction TB
        K_SRC["Events"] --> K_KAFKA["Kafka<br/>(Immutable Log)"]
        K_KAFKA --> K_STREAM["Stream Layer<br/>(Flink, real-time<br/>exactly-once)"]
        K_STREAM --> K_DB["Serving DB"]
        K_DB --> K_QS["Query"]
        K_KAFKA -.->|"Replay<br/>if needed"| K_STREAM
        K_DB -.-> K_RECON["Reconciliation<br/>(Safety net)"]

        style K_KAFKA fill:#FF9800,color:#fff
        style K_STREAM fill:#4CAF50,color:#fff
        style K_RECON fill:#9E9E9E,color:#fff
    end

6.4 Hot Shard Solution — Salted Keys

flowchart TB
    subgraph "Problem: Hot Shard"
        direction LR
        HOT_AD["ad_001 (viral)<br/>100K clicks/sec"] --> P1_HOT["Partition 0<br/>OVERLOADED"]
        NORMAL1["ad_002<br/>10 clicks/sec"] --> P2["Partition 1<br/>idle"]
        NORMAL2["ad_003<br/>15 clicks/sec"] --> P3["Partition 2<br/>idle"]

        style P1_HOT fill:#f44336,color:#fff
    end

    subgraph "Solution: Salted Keys + Secondary Aggregation"
        direction TB
        SALT["ad_001 → ad_001_0, ad_001_1, ..., ad_001_9"]

        subgraph "First Aggregation (Parallel)"
            S0["ad_001_0<br/>count=10K"]
            S1["ad_001_1<br/>count=10K"]
            S9["ad_001_9<br/>count=10K"]
        end

        subgraph "Secondary Aggregation (Merge)"
            REDUCE["Reduce Node<br/>10K x 10 = 100K"]
        end

        SALT --> S0 & S1 & S9
        S0 & S1 & S9 --> REDUCE

        style SALT fill:#FF9800,color:#fff
        style REDUCE fill:#4CAF50,color:#fff
    end

7. Aha Moments & Pitfalls

7.1 Aha Moments — Những điều Hiếu cần nhớ

#InsightGiải thích
1Kappa > Lambda cho hầu hết trường hợpLambda architecture ra đời vì streaming frameworks chưa mature. Với Flink exactly-once, Kappa đơn giản hơn và đủ chính xác. Chỉ cần thêm reconciliation batch job làm safety net — đây không phải Lambda
2Watermark là concept khó nhấtNó đại diện cho trade-off giữa latency và completeness. Không có giá trị “đúng” — chỉ có giá trị phù hợp với business. Cách duy nhất hiểu watermark: thực hành với Flink
3Hot shard có thể giết performance1 quảng cáo viral → 1 partition overloaded → toàn bộ pipeline chậm lại (backpressure). Salted keys là giải pháp, nhưng tăng complexity. Monitor click rate per ad_id để detect sớm
4Reconciliation là safety net bắt buộcDù streaming exactly-once, vẫn cần batch job verify. Bug trong logic, clock skew, edge cases — tất cả có thể gây sai lệch nhỏ. Reconciliation phát hiện trước khi advertiser phát hiện
5Exactly-once không có nghĩa “xử lý 1 lần”Nó có nghĩa “kết quả giống như xử lý 1 lần”. Events có thể bị replay (sau checkpoint restore) nhưng kết quả không đổi nhờ idempotent writes. Hiểu sai concept này → thiết kế sai
6Cascade aggregation tiết kiệm computeAggregate 1-min window, rồi tính 5-min từ 5x 1-min, 1-hour từ 12x 5-min. Không cần 3 pipeline riêng — 1 pipeline + cascade
7HyperLogLog là hero ẩn mìnhĐếm unique users trong 12KB memory per counter với sai số < 1%. Không có HLL, bài toán unique count ở scale này gần như bất khả thi với memory hợp lý
8Raw data là bảo hiểmLuôn lưu raw events (dù đắt). Khi logic sai, khi cần re-process, khi advertiser dispute — raw data là nguồn sự thật duy nhất

7.2 Pitfalls — Những lỗi thường gặp

#PitfallHậu quảCách tránh
1Dùng processing time thay vì event timeClick xảy ra lúc 13:59 bị đếm vào window 14:00Luôn dùng event time cho aggregation. Processing time chỉ cho system monitoring
2Không xử lý late eventsClick đến muộn bị mất → under-count → advertiser khiếu nạiConfigure allowed lateness, dùng side output cho late events
3Không có reconciliationBug trong streaming làm sai kết quả mà không ai biếtChạy batch reconciliation mỗi giờ, alert khi drift > threshold
4Partition by random keyMỗi aggregation node cần dữ liệu của mọi ad → shuffle quá nhiều dataPartition by ad_id để data locality. Chỉ salt khi cần xử lý hot keys
5Checkpoint interval quá lớnKhi failure, phải replay nhiều events → recovery chậm, duplicate window lớnCheckpoint mỗi 30s-1 phút. Balance giữa overhead và recovery time
6Không monitor consumer lagPipeline chậm mà không biết → data stale → advertiser thấy số cũConsumer lag là metric #1 cần monitor. Alert ngay khi tăng bất thường
7Single point of failure cho fraud detectionFraud service down → tất cả clicks được chấp nhận → mất tiềnFraud detection async, không block aggregation pipeline. Fallback: flag và review sau
8Unique count dùng exact countingO(n) memory per ad per window → memory explosion với 2M adsDùng HyperLogLog. Chỉ dùng exact counting cho billing-critical use cases
9Không lưu raw dataKhông thể re-process khi logic sai, không thể reconcileLuôn lưu raw events — Cassandra (7 ngày) + S3 (archive)
10Scale vertically thay vì horizontallyHit hardware limit, single point of failureKafka partitions + Flink parallelism cho horizontal scaling. Vertical chỉ cho DB optimization

7.3 Interview Tips

TipGiải thích
Bắt đầu với requirementsHỏi rõ: bao nhiêu clicks/ngày, latency target, accuracy yêu cầu. Không nhảy vào solution ngay
Nói về trade-offsLambda vs Kappa, HLL vs exact count, Cassandra vs ClickHouse — interviewer muốn nghe bạn cân nhắc, không phải chỉ chọn 1
Vẽ diagram trước, chi tiết sauHigh-level flow: Ingestion → Queue → Aggregation → DB → Query. Rồi dive vào từng component
Nhắc đến exactly-once sớmĐây là yêu cầu khó nhất của bài toán. Nếu bạn biết giải thích Flink checkpoint + idempotent write → ấn tượng lớn
Hot shard là bonus pointNhiều ứng viên không nghĩ tới. Nếu bạn tự đề cập và giải pháp salted keys → interviewer biết bạn có kinh nghiệm thực tế
Reconciliation cho thấy bạn là seniorJunior engineer chỉ nghĩ streaming là đủ. Senior biết cần safety net. Nhắc đến reconciliation batch job → chứng tỏ production experience

8. Tổng kết — Summary Table

Khía cạnhQuyết địnhLý do
ArchitectureKappa (streaming only + reconciliation)Đơn giản hơn Lambda, Flink exactly-once mature
Message queueApache KafkaDurable, partitioned, replayable
Streaming engineApache FlinkExactly-once, stateful processing, checkpoint/savepoint
Raw data storeCassandra (7 days) + S3 (archive)Write-heavy, time-series, cost-effective
Aggregated data storeClickHouseOLAP-optimized, fast aggregation queries
Partitioning strategyBy ad_id (salted for hot keys)Data locality cho aggregation
Time windowTumbling 1-min, cascade to 5-min and 1-hourSimple, no overlap, efficient
Unique countingHyperLogLogMemory efficient, mergeable, ~1% error OK
DeduplicationBloom filter (5-min window)Memory efficient, low false positive
Late eventsWatermark + allowed lateness 5 min + side outputBalance latency và completeness
ReconciliationSpark batch job hourlySafety net, detect drift
Fraud detectionAsync, parallel pipelineKhông block aggregation

Prerequisite (đã học)

Liên quan (tham khảo chéo)

Concepts để tìm hiểu thêm

  • Apache Flink — Stateful stream processing, checkpointing, exactly-once semantics
  • ClickHouse — Column-oriented OLAP database, materialized views
  • HyperLogLog — Probabilistic cardinality estimation
  • Kafka Streams vs Flink — Khi nào dùng cái nào
  • Event sourcing — Lưu tất cả events, derive state từ events (Kafka là event log)

“Hệ thống tốt không phải là hệ thống không bao giờ sai — mà là hệ thống biết khi nào nó sai, và tự sửa được. Reconciliation chính là cơ chế tự sửa đó.”