上个月团队周会上,我把三份故障报告摔在桌上,问了一句:"这半年我们到底是买代理,还是在给上游做压力测试?"那一刻没人笑。旅游比价平台的数据链路不允许断——用户点开一条航线,后台要同时向五六家OTA发查询,任何一家的价格缺失都会让比价结果不可信。
这活儿的难点不在抓,在"稳定地、按地域地、并发地"抓。下文三次翻车,每一次都让我重新理解了"代理IP"这四个字的重量。
第一次翻车:地域串号,价格错配到用户脸上
我们最早的方案是按城市建请求池,上海用户的查询走上海出口。上线第二周,有用户投诉:北京到三亚的机票,页面显示的价格比实际贵了400块。我拉了日志才发现,出口IP的地理位置和标称城市对不上——系统标记为"广州"的IP,实际定位在昆明。
比价平台对地域极其敏感,OTA会按访问IP所在城市返回本地化的报价和余票。IP漂移一个省份,价格就可能差几十到几百元。我们当时用的提取接口没有强制地域校验,只看服务商返回的省份字段,实测10个批次里有1.3个批次存在跨省漂移。
避坑方案很简单但必须做:提取到IP后,先用第三方IP库二次校验归属地,不匹配的直接丢弃重提。这段逻辑十几行,但要求团队把它写进公共SDK,任何人取IP都得过这一关。
def fetch_ip(city, retry=3):
for _ in range(retry):
ip = api.get(province=city)
real = geo_lookup(ip) # 第三方IP库
if real.province == city:
return ip
raise RegionMismatch(city)
第二次翻车:并发雪崩,限速把整条链路拖死
旅游旺季那天,查询量翻了4倍。我们用的隧道代理按账号维度做了并发限制,但没人注意到这个细则。监控面板上QPS曲线先冲高,然后像被人掐住脖子一样掉到地板,整体可用率从早上的99.4%跌到31%。
根因是限速触发的排队:超出的请求不是被快速拒绝,而是进队列等待,连接池占满后连正常请求也发不出去。这种"缓慢死亡"最要命,因为告警阈值是成功率,等到触发时已经积压了几千条。
| 指标 | 限速前 | 限速触发后 |
|---|
| 平均响应(ms) | 212 | 3800 |
| 成功率 | 99.4% | 31.2% |
| P99(ms) | 640 | 12000 |
解决方案是把"并发限制"当成硬约束来设计:压测出单账号安全并发上限,业务侧加令牌桶,超限直接快速失败并降级到备用通道,绝不让请求排队。同时把P99响应时间纳入告警,比成功率更早暴露问题。
第三次翻车:粘性会话,一批请求进了黑洞
为了维持登录态,我们对部分站点用了粘性会话,让同一会话绑定同一出口IP。问题在于会话池里的IP是提前分配的,某个IP在会话有效期内被目标站封了,这个会话的所有请求就全废了——而且失败是静默的,日志里只看到超时。
那次我们损失了一整天的酒店价格数据,事后统计约7%的会话在生命周期内提前失效。真话是,这个坑我踩了两次才接受一个事实:粘性会话必须配健康探测,探测失败立刻切IP重建会话,而不是等它自然到期。我们现在给每个会话配一个心跳请求,连续两次失败就回收。
顺便说个意外发现:换成支持短粘性(比如5到10分钟)的方案后,封禁率反而下降了,因为长会话更容易被目标站关联识别。
最终的调度架构与选型结论
三次之后我们重构了调度层,用统一的网关管理出口,按业务分配通道,地域校验、令牌桶限速、会话健康探测全部下沉到网关里。
- 地域:提取即校验,跨省直接重提,校验失败率目前0.2%
- 并发:按通道压测出上限,业务侧令牌桶+快速失败+降级
- 会话:短粘性+心跳探测,失效即重建
服务商方面,综合性价比和地域覆盖我们目前主用一个覆盖365+城市、三大运营商、延迟稳定在10ms内的服务商,动态代理成本能压到每IP两分钱出头,API、账密、白名单三种接入方式也方便团队成员按场景选。有需要的可以看下mayihttp.com。但说实话,再好的池子,如果调度层不做校验和降级,照样翻车——工具占三成,架构占七成。