1-Billion-Profile Scraping & Agent-Ready API

Thiết kế hệ thống thu thập ~1 tỷ hồ sơ LinkedIn và API tìm kiếm/enrich cho agent

Bài này thiết kế đầy đủ hệ thống theo yêu cầu — crawling, entity resolution, storage, freshness, API — nhưng không giả vờ rằng "1 tỷ LinkedIn profile" là một hệ thống hợp lệ chỉ vì nó chạy được về mặt kỹ thuật. Ràng buộc pháp lý (GDPR right-to-erasure, Điều 14, special category data) được coi là ràng buộc kiến trúc cứng, không phải một đoạn văn ở cuối bài — nó thay đổi schema, storage, entity resolution và API contract.

Lập trường: (B) — thiết kế đầy đủ, ràng buộc pháp lý định hình kiến trúc
Tác giả: Lương Quốc Dũng Design exercise — không phải kế hoạch triển khai thật

02Deletion / erasure workflow

Nghịch lý kiến trúc: muốn xóa dữ liệu của một người, hệ thống phải nhận diện họ chính xác hơn cả lúc lập chỉ mục. Nếu entity resolution sai, chính cơ chế dùng để bảo vệ quyền xóa có thể xóa nhầm người khác hoặc bỏ sót người cần xóa. Vì vậy entity resolution không chỉ là pipeline tối ưu search — nó là hạ tầng quyền riêng tư.

Xác minh người yêu cầu

Ít nhất hai tín hiệu độc lập, tùy nguồn: đăng nhập tài khoản nguồn; email/domain đã xác minh; mã xác nhận gửi tới địa chỉ công khai chứng minh được họ kiểm soát; source-specific profile URL; hoặc quy trình thủ công cho trường hợp rủi ro cao. Không yêu cầu người dùng gửi thêm dữ liệu nhạy cảm nếu không cần.

Ba mức hành động, theo độ chắc chắn của danh tính

Trạng thái yêu cầuHành động với hồ sơ
Chưa có bằng chứngKhông suppress; chỉ mở case. Không thông báo cho khách hàng rằng subject đã yêu cầu xóa; không để trạng thái yêu cầu thành metadata tìm kiếm được; đặt thời hạn xử lý nội bộ.
Đạt ngưỡng xác minh tối thiểuSoft suppression có phạm vi: ẩn candidate đã xác minh khỏi bulk export; chặn agent dùng nó để tự động outreach/ra quyết định; vẫn cho privacy review truy cập; record chưa xác nhận trong cluster không bị ảnh hưởng.
Xác minh đầy đủTạo deletion manifest → xóa derived data → xử lý raw theo chính sách → purge cache/index → thêm suppression fingerprint tối thiểu chống tái-ingest → trả biên nhận hoàn tất.
Vì sao không suppress ngay khi nhận yêu cầu Suppress ngay khi chưa xác minh là fail-safe quá mức và tạo ra một API DoS miễn phí — kẻ tấn công không cần xóa thật, chỉ cần gửi claim để đẩy hàng loạt profile vào trạng thái bị ẩn.

Suppression fingerprint — chống tái-ingest mà không giữ lại dữ liệu gốc

Chỉ lưu HMAC của một định danh đã xác minh, dùng khóa riêng có kiểm soát — không lưu email, URL hay identifier gốc. HMAC phù hợp hơn hash thường vì không thể dễ dàng dò ngược bằng danh sách email phổ biến. Đây vẫn là dữ liệu phục vụ một mục đích hẹp, có retention và access policy riêng — không gọi nó là "không còn dữ liệu cá nhân" một cách tuyệt đối.

Phòng chống lạm dụng quy trình

Hệ thống phát hiện: một người gửi claim hàng loạt profile không liên quan; nhiều claim dùng chung contact channel; tốc độ gửi bất thường; identifier không khớp lịch sử profile; claim nhắm vào profile có lượng truy vấn cao bất thường, nhiều candidate trùng tên, hoặc hậu quả business lớn nếu suppress nhầm (tiêu chí đo được, không phải nhãn cảm tính "người nổi tiếng"). Các case này chuyển sang manual review — không dùng "nghi ngờ gian lận" làm lý do trì hoãn vô thời hạn.

Nguyên tắc xuyên suốt "Safe" ở đây phải bảo vệ cả hai phía: không tiếp tục khuếch đại dữ liệu của subject thật, đồng thời không cho một claim chưa chứng minh xóa người khác khỏi hệ thống.

03Entity resolution ở quy mô 1 tỷ

Thesis Ở quy mô một tỷ, blocking không phải tối ưu hiệu năng — nó là điều kiện để hệ thống tồn tại. Nhưng blocking quá mạnh sẽ bỏ sót cùng một người, còn merge quá mạnh sẽ gộp nhầm hai người và làm sai cả search lẫn quyền xóa. Hệ thống phải tối ưu không chỉ độ chính xác trung bình, mà cả khả năng giải thích và đảo ngược từng quyết định merge.

1 · Giảm 10¹⁸ cặp xuống mức chạy được — multi-key blocking

Không dùng một blocking key duy nhất. Mỗi record được đưa vào nhiều nhóm; hai record trở thành candidate nếu trùng ít nhất một nhóm — giảm bỏ sót mà vẫn tránh so sánh toàn bộ.

KeyVí dụBỏ sót ai
Tên chuẩn hóa + công tynguyen-van-an | example-corpNgười đổi việc giữa hai lần thu thập; freelancer; công ty ghi khác tên chưa được nối
Tên chuẩn hóa + trường + khoảng nămnguyen-van-an | hanoi-university | 2012–2014Profile không có education; năm học bị ẩn/sai; học nhiều trường mà hai nguồn lấy khác nhau

Khi một nhóm vượt ngưỡng kích thước (tên phổ biến có thể tạo nhóm hàng chục nghìn record — so sánh mọi cặp trong nhóm 10.000 record đã là ~50 triệu cặp), không xử lý như block hợp lệ — chia tiếp bằng tín hiệu khác: quốc gia/vùng, ngành nghề, employer/education trong lịch sử, khoảng thời gian hoạt động.

Blocking chỉ trả lời "hai record này đáng được đưa ra so sánh hay không?" — chưa trả lời "hai record này chắc chắn là cùng một người."

2 · Tín hiệu để merge

Tín hiệu gần định danh (có thể tạo confirmed nếu không mâu thuẫn): cùng source profile ID/URL ổn định; cùng email đã xác minh (so sánh bằng HMAC); cùng số điện thoại chuẩn hóa được phép xử lý; stable person ID từ nguồn cấp phép. Vẫn cần điều kiện: email công ty có thể tái cấp cho người khác; URL/ID có thể bị nguồn tái sử dụng; profile có thể bị takeover.

Tín hiệu kết hợp (nhiều tín hiệu đồng thuận → probable): tên tương đồng + employer timeline nối tiếp hợp lý + cùng trường/khoảng năm + location tương thích + độ tương đồng headline/skills + cùng URL cá nhân ngoài nguồn chính.

Tín hiệu phản bác (chặn merge dù điểm khớp cao): hai công việc toàn thời gian không thể cùng tồn tại theo timeline/địa điểm; hai lịch sử học tập khác hẳn; hai identifier đã xác minh thuộc hai người khác nhau.

confirmed → tự động merge probable → giữ liên kết candidate, chưa gộp vật lý ambiguous → review / chờ thêm dữ liệu rejected → negative edge, có TTL, xét lại khi có tín hiệu mới

Chỉ confirmed mới hợp nhất thành một canonical identity; probable là một cạnh có trọng số, không phải sự thật đã ghi vào dữ liệu. Transitive merge (A~B, B~C) chỉ thực hiện khi từng cạnh đáp ứng ngưỡng confirmed và toàn cluster không có mâu thuẫn — chặn lỗi lan truyền qua bắc cầu.

Mâu thuẫn dựa trên dữ liệu có thể đổi (employer, location) có TTL, xét lại khi có tín hiệu mới. Mâu thuẫn dựa trên hai identifier đã xác minh giữ lâu hơn, nhưng vẫn có đường manual override.

3 · Lưu evidence để tách cluster được sau này

Chỉ lưu "lý do merge" là không đủ. Hệ thống giữ nguyên từng source record; canonical profile là một view được dựng từ các record, không phải kết quả ghi đè phá hủy bản gốc.

Nguyên lý cốt lõi Merge phải là liên kết có thể thu hồi giữa các record bất biến, không phải thao tác nghiền hai record thành một row rồi bỏ bản gốc.

Khi phát hiện merge sai: (1) đánh dấu edge gây lỗi revoked; (2) tính lại connected components từ edge còn hợp lệ; (3) tạo hai canonical profile mới; (4) dựng lại field values từ source records nguyên bản; (5) rebuild search/vector/feature/cache; (6) kiểm tra lại deletion/suppression manifest thuộc component nào.

4 · Đo lỗi khi không có ground truth cho 1 tỷ người

Tập mẫu review thủ công, lấy mẫu phân tầng (không ngẫu nhiên toàn hệ thống — phần lớn cặp rất dễ và sẽ che khuất lỗi ở vùng biên): theo trạng thái merge, tên phổ biến/hiếm, block nhỏ/lớn, quốc gia/ngôn ngữ, nguồn dữ liệu, score gần/xa threshold. Mỗi cặp ≥2 reviewer độc lập, bất đồng chuyển reviewer thứ ba.

  • False merge rate — trong cặp đã merge, tỷ lệ thực ra là hai người.
  • Missed merge rate — trong cặp cùng người đã biết, tỷ lệ không nối được.
  • Cluster contamination — tỷ lệ cluster chứa record của người khác.
  • Reversal rate — tỷ lệ edge confirmed phải revoke sau này.
Chi phí lỗi không đối xứng False merge nguy hiểm hơn missed merge: nó trộn dữ liệu hai người, trả enrichment sai, và có thể khiến yêu cầu xóa của một người ảnh hưởng người khác. Ngưỡng confirmed tối ưu cho precision rất cao, chấp nhận missed merge nhiều hơn.

Kiểm tra qua tác vụ thực tế, không chỉ pair metrics: chạy simulation gửi deletion claim trên tập đã biết, đo số record đúng được tìm thấy, số record người khác bị kéo vào, và việc revoke một edge có tách lại cluster chính xác không.

04Crawling & politeness

Phần này chỉ cover crawl lịch sự (tôn trọng robots.txt, tự rate-limit, backoff) — không đi vào kỹ thuật né phát hiện của một target cụ thể, phần đó thuộc phạm vi (A), không phải (B).

Frontier priority

priority =
  source_permission
+ business_value
+ freshness_need
+ change_probability
+ query_demand
- recent_failure_penalty
- estimated_fetch_cost

Thứ tự: nguồn đã cấp quyền và còn quota → profile mới có nhu cầu sử dụng thực tế → profile cũ có tín hiệu thay đổi hoặc sắp hết freshness SLA → profile ít thay đổi/ít truy vấn → URL lỗi lặp lại bị đẩy xuống dead-letter queue. Không crawl tuần tự theo ID vì vừa tạo burst lên một nguồn, vừa dùng tài nguyên cho profile giá trị thấp trước profile đang cần.

change_probability dựa trên lịch sử thay đổi quan sát được: profile đổi employer/headline hai lần trong sáu tháng có xác suất đổi tiếp cao hơn profile không đổi trong ba năm — cộng tín hiệu query demand và SLA sắp hết.

Politeness — hard ceiling, không phải gợi ý

max_rps · max_concurrency · daily_quota · robots_policy · retry_after · allowed_paths

Mặc định bảo thủ khi không có hợp đồng/hướng dẫn khác: 1 request/giây/origin, tối đa 2 request đồng thời. Gặp 429/503 đọc Retry-After; không có thì backoff tăng dần. Không tự tăng vượt mức mặc định chỉ vì server trả 200 — chỉ tăng khi có quyền/thỏa thuận rõ ràng.

Vì sao là hard ceiling Tất cả worker phải dùng chung một distributed rate limiter. Nếu mỗi worker tự giới hạn 1 RPS nhưng có 1.000 worker, hệ thống vẫn vô tình gửi 1.000 RPS tới cùng origin.

Discovery vs refresh — hai frontier tách biệt

Discovery crawlRefresh crawl
Mục tiêuTìm record/URL mớiKiểm tra record đã biết còn đúng không
Ưu tiênBreadth & coverageTuổi dữ liệu, xác suất thay đổi, query demand
Cơ chếDedup URL trước khi enqueue; lưu first_seen/sourceConditional request (ETag/Last-Modified) nếu nguồn hỗ trợ; không đổi thì chỉ cập nhật last_checked

Tách hai frontier giúp discovery không làm profile đang dùng bị cũ, và refresh không chiếm hết ngân sách khiến hệ thống ngừng mở rộng coverage. Hai luồng dùng chung quota nguồn, nhưng có ngân sách và SLA riêng.

05Storage tiering

TầngNội dungĐặc tính & chi phí
Hot cacheKết quả query/enrich gần đâyĐắt nhất mỗi GB, latency thấp, TTL ngắn
Search/indexInverted index, blocking index, vector nếu cầnĐắt do replication, RAM/SSD, write amplification
Canonical + identity graphCanonical view, provenance, merge edges, deletion stateHot/warm, cần consistency và truy cập theo ID
Recent rawSnapshot mới, parse artifacts, lịch sử gầnObject storage tiêu chuẩn, rẻ hơn database
Cold raw/historySnapshot cũ phục vụ audit/replayArchive storage rẻ, restore chậm

Chính sách theo tầng: cache purge ngay khi profile bị suppress/xóa; search/index xóa document và rebuild khi split cluster; canonical/graph giữ current state cùng evidence để giải thích và đảo merge; recent raw giữ đủ lâu để replay parser và điều tra lỗi; cold raw chỉ giữ khi license/retention/lawful purpose cho phép, hết hạn thì xóa hoặc crypto-shred.

Điểm chi phí quan trọng Raw storage chiếm nhiều byte nhưng tương đối rẻ; search, blocking indexes và canonical graph ít byte hơn nhưng đắt hơn vì cần SSD/RAM, replication và cập nhật liên tục. Capacity/cost không thể chỉ lấy "200TB × giá object storage" — phải tính riêng write amplification, nhiều blocking index song song, cache và chi phí rebuild khi refresh/deletion/split cluster.

06Agent-ready API

Hai endpoint chính: POST /v1/profiles/search và POST /v1/profiles/enrich.

Search — response không chỉ trả profile, mà trả trạng thái nhận dạng

POST /v1/profiles/search
{
  "query": { "title": ["staff engineer"], "skills": ["distributed systems"], "location": ["Singapore"] },
  "purpose": "recruiting_search",
  "limit": 20
}
{
  "results": [{
    "profile_id": "cp_123",
    "identity_status": "probable",
    "confidence": 0.87,
    "profile": { "name": "Example Person", "headline": "Staff Engineer" },
    "provenance": [{ "source": "licensed_partner_a", "observed_at": "2026-07-20" }],
    "allowed_actions": ["display_for_review"],
    "blocked_actions": ["automated_outreach", "employment_decision"],
    "next_step": "request_human_review"
  }]
}

Enrich

POST /v1/profiles/enrich
{
  "identifiers": { "verified_email_hmac": "hmac:...", "company_domain": "example.com" },
  "requested_fields": ["current_title", "employer"],
  "purpose": "crm_enrichment"
}

Response status: confirmed · probable · ambiguous · not_found.

Vì sao uncertainty phải là field máy đọc được, không chỉ một con số Nếu probable bị agent hiểu thành confirmed, nó có thể gửi outreach sai người, cập nhật CRM sai, trộn lịch sử nghề nghiệp, hoặc góp phần vào quyết định tuyển dụng sai. API phải trả allowed_actions/blocked_actions — chỉ confirmed mới đi tiếp vào automation được policy cho phép; probable/ambiguous bắt buộc dừng, review, hoặc yêu cầu thêm identifier.

Rate limit cho agent — khác người dùng tay

Agent có thể gọi lặp tốc độ cao và tự mở rộng truy vấn. Giới hạn theo: service account, agent run, tenant, purpose, endpoint, records returned, daily cost budget.

Interactive search:  60 request/phút, tối đa 100 record/request
Enrich:              1.000 identity/ngày/agent
Batch jobs:          async, quota riêng, cần phê duyệt
  • Cursor pagination, không offset sâu.
  • Idempotency key cho enrich.
  • Cost estimate trước batch; hard budget + kill switch cho agent loop.
  • Audit log ghi prompt purpose, query và profile IDs đã truy cập.
  • Không bulk export mặc định. 429 kèm thời điểm được thử lại.
  • Agent không được tự đổi purpose để né quota.
Câu chốt "Agent-ready" không có nghĩa là dễ gọi hơn; nó có nghĩa là API phải đóng gói uncertainty, quyền sử dụng và ngân sách thành các ràng buộc máy có thể thực thi, để agent không biến một match chưa chắc chắn thành một hành động chắc chắn.

07Capacity & cost — ước lượng bậc độ lớn

Không phải báo giá. Giả định 1 tỷ profile, trung bình 200KB/payload, nạp ban đầu trong 30 ngày, refresh ~10%/tháng.

Hạng mụcQuy môChi phí ước tính
Raw payload ban đầu~200 TB~$2k–5k/tháng hot storage; thấp hơn khi chuyển cold
Canonical + provenance + identity graph~2–10 TB~$5k–30k/tháng tùy database, replication, throughput
Blocking indexes~300–600 GB~$1k–5k/tháng; chi phí build/update đáng kể hơn số byte
Search indexes~5–20 TB logical, lớn hơn sau replication~$20k–80k/tháng
Cache~0.5–2 TB~$3k–15k/tháng
Parse + normalize ban đầu1.000–1.500 worker-slot/tháng~$20k–80k
Entity resolution + index buildNhiều blocking pass + candidate comparison~$30k–150k ban đầu
Compliance operationsDeletion, reindex, split cluster, audit~$5k–30k/tháng, phụ thuộc volume
Network cho 200 TBTheo nguồn được cấp phép~$10k–20k ở $0.05–0.10/GB
Data license/partner feedPhụ thuộc hợp đồng~$100k tới hàng triệu USD

Tổng storage logical

Raw                          ~200 TB
Canonical/graph                2–10 TB
Search                         5–20 TB
Blocking indexes             0.3–0.6 TB
Cache                         0.5–2 TB
History/headroom             20–80 TB
────────────────────────────────────
Tổng logical                ~230–310 TB

Raw chiếm nhiều byte nhất, nhưng search/index chiếm nhiều tiền vận hành nhất vì cần SSD/RAM, nhiều replica, shard management và cập nhật liên tục.

Bandwidth — hai mô hình khác hẳn nhau

Thiết kế được đề xuất (B): licensed feed/API Network 200TB khoảng $10k–20k; phí bản quyền dữ liệu mới có khả năng chiếm 60–90% tổng chi phí.
Scraping qua proxy thương mại (không phải hướng đề xuất) Ở $1–10/GB, 200TB có thể thành $200k–2M và áp đảo 60–90% chi phí ban đầu — nhưng đây là kiến trúc dựa trên việc vượt giới hạn của target cụ thể, không phải lựa chọn (B) của bài này.

Câu "proxy áp đảo mọi thứ" đúng với kiến trúc scraping né chặn; với lựa chọn (B), nó được thay bằng licensed data acquisition — vẫn là khoản chi phí lớn nhất, nhưng có quyền sử dụng rõ ràng.

Compute

fetch/decompress → parse/normalize → update nhiều blocking indexes → generate candidates → score identity edges → rebuild canonical views → update search/cache

Deletion và split cluster tạo write amplification: một edge bị revoke có thể yêu cầu dựng lại canonical profile, search document, vector, feature và cache.

Infrastructure ban đầu (chưa gồm phí dữ liệu và nhân sự): ~$100k–500k. Recurring platform cost: ~$40k–160k/tháng. Biên độ rộng vì search throughput, replication, SLA và số lần refresh ảnh hưởng lớn hơn dung lượng raw đơn thuần.

Một lần vs vĩnh viễn

Chi phí một lầnChi phí vĩnh viễn
Ingest 1 tỷ profile; parse/normalize; build blocking indexes; entity resolution ban đầu; build search index lần đầuRefresh thích ứng; cập nhật blocking/search index; entity-resolution lại khi có dữ liệu mới; license/partner feed; deletion/suppression/DSAR; reindex khi split cluster; audit, cache, API serving
Câu chốt Crawl ban đầu tạo ra 200TB dữ liệu; freshness, index maintenance và quyền dữ liệu tạo ra doanh nghiệp phải vận hành mãi mãi. Bài toán kinh tế thật không phải "lưu một tỷ profile tốn bao nhiêu", mà là "giữ một tỷ identity vừa mới, vừa tìm được, vừa xóa và tách đúng được tốn bao nhiêu mỗi tháng".

08Giới hạn & điều tôi không hứa

  • Đây là design exercise, không phải kế hoạch triển khai. Số liệu cost là ước lượng bậc độ lớn, không phải báo giá — biên độ rộng vì phụ thuộc nhà cung cấp, hợp đồng license, và throughput thực tế.
  • Bài chọn lập trường (B): thiết kế đầy đủ nhưng dựa trên nguồn dữ liệu được cấp phép, không phải kỹ thuật né bot-detection của một nền tảng cụ thể — phần "anti-blocking" trong bài chỉ cover crawl lịch sự.
  • Crypto-shredding là biện pháp giảm khả năng truy cập, không phải xóa vật lý tuyệt đối trong mọi chế độ pháp lý.
  • Entity resolution không thể chứng minh đúng trên 1 tỷ người — chỉ đo được bằng mẫu phân tầng, theo dõi reversal rate, và đặt ngưỡng theo chi phí lỗi không đối xứng.
  • Legal/ToS constraints được phân tích ở mức nguyên lý (right-to-erasure, Điều 14, special category data); các viện dẫn án lệ hoặc điều khoản cụ thể cần được kiểm chứng lại trước khi dùng làm căn cứ thật, không lấy từ suy luận của bài này.