B站用户信息爬虫实战:C++分布式架构与亿级数据挑战
1. 项目背景与挑战
B站作为国内领先的视频社区平台,拥有超过7亿注册用户。想要获取如此庞大的用户数据,传统单机爬虫会遇到几个致命问题:IP封锁、请求频率限制、数据存储压力以及任务调度复杂度。我在实际项目中遇到过这样的情况——单机爬取速度达到每秒1000请求时,不到5分钟就会被封禁IP。
分布式架构的核心思路是将任务拆分为三个层级:
- 调度服务器:负责任务分配和状态管理
- 工作节点:执行实际爬取任务
- 数据存储层:处理海量数据写入
这种架构下,我们实测每天能稳定获取3000万条用户数据,且不会对目标服务器造成过大压力。下面这段C++代码展示了基础的任务结构体设计:
struct BiliTask { int clientId; // 工作节点ID unsigned midStart; // 起始用户ID unsigned midEnd; // 结束用户ID time_t ttl; // 任务有效期 };2. 核心架构设计
2.1 代理IP池管理
代理IP是爬虫的"生命线"。我们的方案采用动态评分机制:
- 新获取的IP初始分数为100分
- 每次请求失败扣20分
- 分数低于30分自动淘汰
- 每小时补充新IP
实测发现,优质代理IP的可用时长差异巨大。有些只能坚持2-3分钟,而稳定的IP能持续工作数小时。我们通过多供应商采购解决了这个问题,以下是IP池的核心数据结构:
class ProxyPool { std::map<ProxyInfo, int> activeProxies; // <IP信息, 当前分数> std::mutex poolMutex; // 线程安全锁 void refreshProxies() { // 从多个供应商获取新IP } };2.2 心跳检测机制
分布式系统最怕节点"假死"。我们的解决方案包含三层检测:
- 主动心跳:每30秒上报状态
- 任务心跳:每次请求任务视为一次心跳
- 超时剔除:连续3次无响应则移除节点
当工作节点异常离线时,调度器会自动将其未完成任务重新分配。这个过程对整体系统完全透明,开发者只需关注业务逻辑。
3. 关键技术实现
3.1 任务分片算法
面对7亿用户ID,我们采用动态分片策略:
- 初始按10000个ID为单元分片
- 根据节点性能动态调整分片大小
- 失败任务自动拆分为更小单元
这种设计使得8核机器能同时处理200+个微任务,CPU利用率保持在70%左右。分片调度器的核心逻辑如下:
class TaskScheduler { std::queue<BiliTask> pendingTasks; // 待处理任务 std::map<int, BiliTask> activeTasks; // 执行中任务 void dispatchTask(WorkerNode& node) { if(pendingTasks.empty()) { // 自动生成新任务批次 generateTasks(10000); } auto task = pendingTasks.front(); node.assignTask(task); activeTasks[node.id] = task; } };3.2 数据存储优化
原始方案使用MySQL单表存储,在数据量超过5000万条后性能急剧下降。我们最终采用的方案是:
- 按用户ID范围分表(每200万用户一个表)
- 空数据单独压缩存储
- 使用内存缓存热点数据
这使写入速度从最初的2000条/秒提升到15000条/秒。以下是表结构设计示例:
CREATE TABLE bili_user_1_200w ( uid BIGINT PRIMARY KEY, name VARCHAR(64) CHARACTER SET utf8mb4, gender TINYINT, level TINYINT, sign TEXT, -- 其他字段... ) ENGINE=InnoDB PARTITION BY KEY(uid);4. 性能调优实战
4.1 连接池优化
初期每个请求都新建连接,导致TCP连接数爆炸。我们引入的连接池方案包含:
- 预建立50个HTTP连接
- 异步IO复用通道
- 智能DNS缓存
调整后,网络延迟从平均300ms降至80ms。关键实现代码:
class ConnectionPool { std::vector<CURL*> handles; std::queue<CURL*> freeHandles; CURL* getHandle() { if(freeHandles.empty()) { return curl_easy_init(); } auto h = freeHandles.front(); freeHandles.pop(); return h; } };4.2 内存管理技巧
持续运行一周后出现内存泄漏,我们通过以下手段解决:
- 使用智能指针管理资源
- 每处理10万请求主动释放缓存
- 定制化内存分配器
Valgrind检测显示内存使用稳定在2GB左右,不再持续增长。这是我们的内存监控方案:
class MemoryMonitor { static void checkMemoryUsage() { if(currentUsage > threshold) { cleanupCache(); curl_global_cleanup(); // 其他清理操作... } } };5. 异常处理经验
5.1 反爬虫策略应对
B站的反爬机制会随时间变化,我们总结出这些规律:
- 频繁访问相同API会触发验证码
- 请求头缺失Referer会返回空数据
- 异地登录行为会导致临时封禁
解决方案是构建"拟人行为模式":
- 随机延迟(100-500ms)
- 轮换UserAgent
- 模拟真实用户点击流
5.2 数据一致性保障
网络波动可能导致数据丢失,我们采用:
- 本地SQLite临时存储
- 断点续传机制
- 数据校验哈希值
这套方案使得即使在网络中断1小时后,恢复工作也能从断点继续。
6. 部署方案
6.1 混合环境部署
我们的生产环境包含:
- 1台调度服务器(Linux)
- 3台高配工作节点(Windows)
- 1台数据库服务器(Linux)
- 多台廉价VPS作为代理节点
这种组合既保证了核心服务稳定性,又降低了代理IP成本。实测每天代理费用控制在50元以内。
6.2 监控系统搭建
使用Prometheus+Grafana构建监控看板,关键指标包括:
- 实时爬取速度
- 代理IP健康度
- 任务队列深度
- 节点存活状态
当系统出现异常时,Telegram机器人会立即发送告警。
7. 踩坑记录
在开发过程中最耗时的三个问题:
- CURL多线程崩溃:需要设置CURLOPT_NOSIGNAL
- MySQL连接耗尽:调整wait_timeout参数
- 代理IP突发失效:实现快速切换机制
有个有趣的发现:工作节点在Windows下的性能比Linux高15%,这可能与TCP协议栈实现差异有关。
8. 扩展思考
这套架构稍作修改就能适配其他场景:
- 电商价格监控(替换解析模块)
- 新闻舆情分析(增加NLP组件)
- 社交网络挖掘(调整爬取策略)
最近我们在尝试将核心模块移植到Rust,初步测试显示内存安全性有显著提升。
