面對百萬級日 PV、高並發寫入場景,單庫單表 MySQL 早已力不從心。本文記錄某團隊從「集群方案不可行 → 分而治之維護成本高 → 同步寫入鎖表 → 異步文件落地 + 批量 Load Data」的架構演進全過程,揭示「換思路勝過換硬件」的工程智慧。
| 階段 | 方案 | 核心痛點 | 結果 |
|---|---|---|---|
| 1.0 | MySQL Cluster / 主從同步集群 | 成本高、技術門檻高、運維複雜 | ❌ 否決 |
| 2.0 | 業務拆分:不同統計模塊部署不同廉價服務器 | 數據聚合極其痛苦、非長久之計 | ⚠️ 緩解但未根治 |
| 3.0 | 改同步寫入為「來一條插一條」,去掉判重邏輯 | 索引維護開銷大、單表 5000萬行 Insert 慢、鎖爭用劇烈 | ❌ 服務器仍跑死 |
| 4.0 | 異步文件落地 + 定時批量 Load Data 入庫 | 無 | ✅ 徹底解決,成本極低 |
[客戶端請求]
│
▼
[PHP/Java 應用層] ──▶ 組裝 CSV/JSON 行數據
│
▼
[error_log() 寫入本地磁盤文件] ◀─── 關鍵技巧:非阻塞、高性能、無依賴
│
▼
[文件命名規則:日期小時_IP前綴.log]
│
▼
[定時任務] 掃描文件 → 去重/聚合 → LOAD DATA INFILE 入庫
│
▼
[MySQL 核心業務表 / 數據倉庫]
error_log() 妙用// PHP 示例
$line = sprintf("%s,%s,%s\r\n", $ip, $referer, $ua);
error_log($line, 3, "/data/logs/stat_2023101514_221.log");
// 文件名:stat_YYYYMMDDHH_IP前3位.log
$filename = sprintf("stat_%s_%s.log", date('YmdH'), substr($ip, 0, strrpos($ip, '.')));
# Shell/Python 掃描處理邏輯
for file in stat_2023101514_*.log; do
# 1. 去重(同 IP+URL+UA 視為同一 PV)
sort -u $file > $file.uniq
# 2. 聚合(同 IP 多頁面合併為一行 Session)
python aggregate.py $file.uniq > $file.agg
# 3. 入庫
mysql -e "LOAD DATA INFILE '$file.agg' INTO TABLE stats
FIELDS TERMINATED BY ',' LINES TERMINATED BY '\r\n'
(ip, referer, ua, pv_count, session_start, session_end);"
done
LOAD DATA INFILE 極速入庫LOAD DATA INFILE '/data/logs/stat_2023101514_221.agg'
INTO TABLE stats
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\r\n'
(ip, referer, ua, pv_count, session_start, session_end);
SET SESSION sql_log_bin=0)| 維度 | 同步直寫 | 異步文件 + Load Data |
|---|---|---|
| 寫入延遲 | 毫秒級(含索引維護) | 微秒級(僅追加文件) |
| 並發承載 | 受限於鎖/索引 | 近無上限(文件系統並發寫入) |
| 數據不丟失 | 依賴 Binlog/雙寫 | 文件落地即持久化,可備份/重放 |
| 入庫效率 | 單行 Insert,索引碎片化 | 批量 Load Data,索引緊湊 |
| 運維成本 | 高(需集群/分庫分表) | 低(單機文件 + 定時腳本) |
logrotate 或腳本定時壓縮/清理歷史文件百萬級流量並不一定需要分庫分表、NewSQL 或昂貴集群。「寫文件異步化 + 批量 Load Data 入庫」 以極低成本、極簡架構、極高吞吐,完美解決了高並發寫入瓶頸。這體現了工程師的核心價值:在約束條件下,用最簡單的方案解決最複雜的問題。