DE

[DE Blog #14] Thiết Kế Hệ Thống Clickstream Analytics & User Behavior Tracking Ở Quy Mô Petabyte

10 views
[DE Blog #14] Thiết Kế Hệ Thống Clickstream Analytics & User Behavior Tracking Ở Quy Mô Petabyte

1. Bối cảnh thực tế (Context & Problem Statement)

Trong các nền tảng thương mại điện tử (Shopee, Amazon, Tiki), mạng xã hội (TikTok, Meta) hoặc ứng dụng gọi xe (Grab, Uber), dữ liệu hành vi người dùng (Clickstream Logs: Click, View, Search, Add to Cart, Checkout) là nguồn tài nguyên quý giá nhất để tối ưu hóa trải nghiệm, xây dựng hệ thống gợi ý (Recommendation) và phân tích phễu chuyển đổi (Funnel Analysis).

Các thách thức kỹ thuật đỉnh cao:

  1. Lưu lượng khổng lồ và biến động mạnh (Traffic Spikes): Hệ thống phải hấp thụ hàng trăm nghìn sự kiện mỗi giây (100,000+ RPS100,000+\text{ RPS}) trong các dịp Mega Sale mà không bị mất mát dữ liệu.
  2. Bài toán Sessionization (Gom phiên): Người dùng thực hiện hàng loạt hành động rải rác. Làm thế nào để gom chúng thành các "Phiên làm việc" (Sessions) với điều kiện: Nếu người dùng không hoạt động quá 30 phút, phiên tiếp theo sẽ được tính là một session mới?
  3. Đếm người dùng duy nhất (Unique Visitors / DAU): Phép toán COUNT(DISTINCT user_id) trên hàng chục tỷ dòng dữ liệu gây tràn RAM và sập hệ thống nếu không áp dụng thuật toán tối ưu.

2. Các Khái Niệm & Cơ Chế Cốt Lõi

2.1. Cấu Trúc Bản Ghi Clickstream Chuẩn Hóa

Một sự kiện Clickstream chuẩn (theo mô hình Snowplow/Segment) cần bao gồm:

  • Event Metadata: event_id (UUID), event_name (page_view, click_banner, add_to_cart), event_timestamp.
  • Identity Context: anonymous_id (Cookie/Device ID), user_id (Account ID nếu đã đăng nhập), ip_address, user_agent.
  • Page / Device Context: page_url, referrer_url, app_version, device_os.
  • Custom Properties (JSON / Struct): product_id, category_id, price, cart_value.

2.2. Kỹ Thuật Sessionization (Phân Chia Phiên Người Dùng)

  • Khái niệm: Session là chuỗi các tương tác liên tục của một người dùng trong một khoảng thời gian nhất định.
  • Quy tắc 30-Minute Inactivity Window:
    • Nếu khoảng cách thời gian giữa Event NN và Event N+1N+1 của cùng một user >30 phuˊt> 30\text{ phút}, một session_id mới sẽ được khởi tạo.
    • Được triển khai thông qua:
      • Batch Mode (Spark / SQL): Sử dụng các hàm LAG()SUM() OVER (...) lũy kế.
      • Stream Mode (Flink): Sử dụng Processing/Event Time Session Windows với gap 30 phuˊt30\text{ phút}.

2.3. Thuật Toán Đếm Xấp Xỉ HyperLogLog (HLL)

  • Vấn đề của COUNT(DISTINCT) truyền thống: Để đếm chính xác số lượng phần tử duy nhất (Cardinality), Database phải lưu toàn bộ các giá trị đã thấy vào một bảng băm (Hash Set) trong bộ nhớ. Khi số lượng user lên đến hàng trăm triệu, bộ nhớ RAM yêu cầu có thể lên tới hàng chục Gigabytes.
  • Giải pháp với HyperLogLog:
    • HLL là thuật toán xác suất (Probabilistic Algorithm) ước tính số lượng phần tử duy nhất dựa trên việc quan sát số lượng số 0 liên tiếp ở đầu chuỗi băm nhị phân của từng phần tử.
    • Hiệu quả vượt trội: Chỉ tốn khoảng 1.5 KB1.5\text{ KB} bộ nhớ RAM để đếm hàng tỷ phần tử với sai số chuẩn cực nhỏ (1%2%\approx 1\% - 2\%).
    • Được tích hợp sẵn dưới dạng hàm: APPROX_COUNT_DISTINCT() trong Spark/BigQuery, hoặc kiểu dữ liệu AggregateFunction(uniq, UInt64) trong ClickHouse.

2.4. Kỹ Thuật User Identity Stitching (Hợp Nhất Danh Tính)

  • Người dùng thường duyệt web ẩn danh (gán anonymous_id = 'anon_123'), thêm hàng vào giỏ, sau đó 20 phút mới tiến hành Đăng nhập (gán user_id = 'user_888').
  • Identity Graph / Graph Stitching: Data Pipeline chạy một tiến trình nối ngược (Backward Stitching) để gán toàn bộ các sự kiện trước đó của anon_123 vào tài khoản user_888, giúp báo cáo hành vi không bị đứt đoạn.

3. Các Bảng Markdown So Sánh Chi Tiết

📊 Bảng 1: So Sánh Các Giải Pháp Thu Thập Clickstream

Giải phápCơ chế hoạt độngƯu điểmNhược điểmChi phí
Google Analytics (GA4)SaaS có sẵnDễ cài đặt, UI báo cáo mạnhDữ liệu bị lấy mẫu (Sampling), khó truy cập dữ liệu thô cấp thấpMiễn phí hoặc rất đắt (GA360)
Segment / RudderStackCustomer Data Platform (CDP)Định tuyến linh hoạt sang nhiều công cụ marketingPhụ thuộc bên thứ baTính tiền theo lượng Monthly Tracked Users (MTU)
In-House Ingestion Engine (Client SDK + Kafka + S3)Tự xây dựng toàn bộToàn quyền kiểm soát 100% dữ liệu thô, bảo mật tối đa, dễ scaleTốn công sức xây dựng và vận hành hạ tầngRẻ nhất ở quy mô lớn (chỉ tốn chi phí hạ tầng Cloud)

📊 Bảng 2: So Sánh COUNT(DISTINCT) vs. APPROX_COUNT_DISTINCT (HyperLogLog)

Tiêu chíCOUNT(DISTINCT user_id)APPROX_COUNT_DISTINCT (HyperLogLog)
Độ chính xácChính xác tuyệt đối (100%)Xấp xỉ (Sai số điển hình 0.5%2%0.5\% - 2\%)
Tiêu tốn bộ nhớ (RAM)O(N)O(N) (Tăng tuyến tính theo số lượng phần tử độc nhất)O(1)O(1) (Cố định khoảng 1.5 KB16 KB1.5\text{ KB} - 16\text{ KB})
Tốc độ xử lýChậm, dễ gây OOM và Shuffle cực nặngNhanh gấp 10x - 50x, dễ dàng cộng dồn (Mergeable)
Trường hợp sử dụngBáo cáo tài chính, quyết toán thanh toán, kiểm toánBáo cáo DAU/MAU, Phân tích Clickstream, User Tracking (Chuẩn phỏng vấn)

📊 Bảng 3: So Sánh 2 Chiến Lược Triển Khai Sessionization

Tiêu chíBatch Sessionization (Spark / dbt Daily Job)Stream Sessionization (Apache Flink Real-time)
Độ trễ (Latency)Vài giờ đến 24 giờ (chạy qua đêm)Gần thời gian thực (vài giây sau khi session đóng)
Độ phức tạpTrung bình (viết câu lệnh SQL Window Function)Cao (quản lý Flink Session Window State và Late Data)
Chi phí tính toánThấp hơn (chạy theo lịch định kỳ)Cao hơn (cụm Flink Workers phải chạy liên tục 24/7)
Use-case tối ưuBáo cáo tổng kết tuần/tháng, Phân tích Funnel dài hạnPhát hiện hành vi bỏ giỏ hàng (Cart Abandonment) để gửi voucher ngay

4. Kiến Trúc Thiết Kế Toàn Diện (Full System Design Walkthrough)

[Clients: Web / iOS / Android Apps]
               |
               v (HTTPS POST /events - Batch of 20 events)
[Cloud Load Balancer (ALB) / CDN Edge]
               |
               v
[Ingestion Microservices (Go/Rust/Java Netty)]
               |
               +---> (1) Validation & Bot Filtering (GeoIP, User-Agent)
               v
[Apache Kafka: Topic "raw-clickstream" (64 Partitions, Key=anonymous_id)]
               |
       +-------+------------------------------------------+
       | (Stream Path)                                    | (Batch Path)
       v                                                  v
[Apache Flink Stream Job]                       [Kafka Connect (S3 Sink)]
 - Real-Time Sessionization (30m gap)                     |
 - HyperLogLog DAU Aggregation                            v
 - Trigger Cart Abandonment Alerts              [Bronze Raw Layer: Parquet on S3]
       |                                                  |
       v                                                  v [Spark Batch Jobs]
[Serving: ClickHouse Cluster]                   [Silver: Deduplicated & Cleaned]
 - Real-Time Metrics & Dashboards                         |
 - Funnel Analytics Queries                               v
       |                                        [Gold: Star Schema Data Marts]
       v                                                  |
[Product Analytics UI / Superset]                         v
                                                [Trino / Lakehouse BI Queries]

Các bước xử lý dữ liệu chi tiết:

  1. Client-side Batching: SDK trên điện thoại gom khoảng 10–20 sự kiện (hoặc gửi sau mỗi 5 giây) để giảm tải kết nối HTTP lên API Gateway.
  2. Ingestion & Buffer: Ingestion Service kiểm tra schema sơ bộ, gắn thêm metadata IP/Timestamp rồi đẩy vào Kafka với Key = anonymous_id để bảo toàn thứ tự click của từng thiết bị.
  3. Xử lý Real-time (Speed Layer): Apache Flink đọc từ Kafka, chạy thuật toán tính toán DAU thời gian thực qua HyperLogLog và nạp vào ClickHouse.
  4. Lưu trữ & Phân tích chuyên sâu (Batch Layer): Dữ liệu được nạp ra S3 Data Lakehouse (Apache Iceberg). Hàng đêm, Spark Job chạy thuật toán User Identity Stitching và tính toán các chỉ số kinh doanh dài hạn.

5. Góc Ôn Luyện Phỏng Vấn (Interview Corner)

❓ Câu hỏi 1: Giải thích cách giải quyết bài toán Sessionization (gom phiên duyệt web với khoảng ngắt 30 phút) bằng SQL trên Apache Spark hoặc Data Warehouse.

  • Lời giải & Giải thích truy vấn 3 bước:
    WITH EventWithLag AS (
        -- Bước 1: Lấy timestamp của sự kiện liền trước của cùng user:
        SELECT 
            user_id,
            event_timestamp,
            event_name,
            LAG(event_timestamp) OVER (
                PARTITION BY user_id 
                ORDER BY event_timestamp
            ) AS prev_event_time
        FROM raw_events
    ),
    SessionMarkers AS (
        -- Bước 2: Đánh dấu 1 nếu khoảng cách > 30 phút (khởi đầu session mới), ngược lại là 0:
        SELECT 
            *,
            CASE 
                WHEN prev_event_time IS NULL THEN 1
                WHEN DATEDIFF('minute', prev_event_time, event_timestamp) > 30 THEN 1
                ELSE 0 
            END AS is_new_session
        FROM EventWithLag
    )
    -- Bước 3: Dùng SUM() lũy kế để sinh Session ID tăng dần cho từng user:
    SELECT 
        user_id,
        event_timestamp,
        event_name,
        CONCAT(user_id, '_', SUM(is_new_session) OVER (
            PARTITION BY user_id 
            ORDER BY event_timestamp
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        )) AS session_id
    FROM SessionMarkers;
    
    • Giải thích: Phép SUM(is_new_session) lũy kế sẽ giữ nguyên giá trị khi gặp số 0 (vẫn trong cùng 1 session) và tự động tăng thêm 1 mỗi khi phát hiện một khoảng cách thời gian vượt quá 30 phút.

❓ Câu hỏi 2: Thuật toán HyperLogLog (HLL) hoạt động dựa trên nguyên lý xác suất nào? Tại sao nó có thể tính toán xấp xỉ số lượng người dùng duy nhất với bộ nhớ cố định cực nhỏ?

  • Gợi ý trả lời:
    • Nguyên lý cốt lõi:
      • Giả sử ta tung một đồng xu nhiều lần. Xác suất để xuất hiện chuỗi liên tiếp kk mặt ngửa ở những lần tung đầu tiên là 2k2^{-k}. Nếu ta quan sát thấy chuỗi dài nhất có kk lần ngửa liên tiếp, ta có thể ước tính đã có khoảng 2k2^k lần tung đồng xu.
    • Áp dụng vào Big Data:
      1. Khi một user_id đi vào, thuật toán băm (hash) nó thành một chuỗi nhị phân 64-bit đồng đều.
      2. Thuật toán đếm số lượng số 0 liên tiếp ở đầu chuỗi băm (RR). Giá trị ước tính số lượng user duy nhất xấp xỉ bằng 2Rmax2^{R_{max}}.
      3. Để giảm sai số ngẫu nhiên, HLL chia chuỗi băm thành hàng nghìn "thùng" nhỏ (Buckets), tính giá trị RmaxR_{max} trong từng thùng và lấy trung bình điều hòa (Harmonic Mean) của tất cả các thùng.
    • Khả năng cộng gộp (Mergeability): Hai cấu trúc HLL của ngày 1 và ngày 2 có thể được gộp lại với nhau bằng phép toán BITWISE OR cực nhanh để tính số lượng user độc nhất của cả tuần mà không cần quét lại dữ liệu thô!

❓ Câu hỏi 3: Làm thế nào để thiết kế một truy vấn phân tích Phễu chuyển đổi (Funnel Analysis) nhiều bước (ví dụ: View Product \rightarrow Add to Cart \rightarrow Checkout) có tính tuần tự về thời gian trên ClickHouse?

  • Gợi ý trả lời:
    • ClickHouse cung cấp hàm chuyên dụng cực mạnh windowFunnel() được tối ưu hóa ở tầng C++:
    SELECT 
        level,
        COUNT(1) AS user_count
    FROM (
        SELECT 
            user_id,
            windowFunnel(1800)( -- Khung thời gian tối đa 1800s (30 phút)
                event_timestamp,
                event_name = 'view_product',
                event_name = 'add_to_cart',
                event_name = 'checkout_completed'
            ) AS level
        FROM clickstream_events
        WHERE event_date = '2026-08-20'
        GROUP BY user_id
    )
    GROUP BY level
    ORDER BY level;
    
    • Cơ chế: Hàm windowFunnel kiểm tra xem người dùng có hoàn thành tuần tự các bước 1, 2, 3 trong khoảng thời gian quy định (1800 giây) hay không và trả về cấp độ cao nhất mà người dùng đạt được (1, 2 hoặc 3), giúp việc vẽ biểu đồ phễu chuyển đổi trở nên vô cùng nhanh chóng.

6. Tóm Tắt Ghi Nhớ Nhanh (Key Takeaways)

  1. Client-side Batching kết hợp Kafka Partitioning theo Device/Anonymous ID là chìa khóa để xử lý 100k+ RPS100\text{k}+\text{ RPS} an toàn.
  2. Sessionization trong SQL được giải quyết thanh lịch bằng tổ hợp LAG() + CASE WHEN + Cumulative SUM().
  3. HyperLogLog (HLL) là tiêu chuẩn vàng để tính toán các chỉ số DAU/MAU và Cardinality quy mô lớn với bộ nhớ cố định 1.5 KB\approx 1.5\text{ KB}.
  4. Tận dụng ClickHouse với hàm windowFunnel() để đạt hiệu năng phân tích hành vi người dùng thời gian thực với độ trễ dưới 1 giây.
0Claps