大促凌晨三点,告警群里突然刷屏:“商品详情接口失败率37%”,本来只是配合运营做一波价格监控,结果爬虫调度平台CPU没爆,代理通道却先崩了。那一刻我才意识到:脚本写得再快,代理链路一断,前面全白搭。后来我把隧道代理拉出来做了一轮完整的压力测试,模拟的就是2026年双十一零点之后那批瞬间冲进来的并发请求,测试报告里几个数字让我印象特别深:并发200时隧道代理响应稳定,并发500时成功率直接掉了十几个点。这篇文章就是把这次压测从设计到落地的全过程拆开讲清楚。
如果你也在用爬虫做商品监控、比价、信息采集这类事情,尤其是到了大促节点流量会突然上涨的场景,那么“隧道代理”这个词你一定不陌生。但很多人对它的理解停留在“能换IP”这个层面,真要问一句:你的爬虫在双十一凌晨峰值时能撑多久?多半答不上来。本文会用一套可复现的压测方案,带你搞清楚隧道代理的并发上限、稳定性拐点和调优方向。
1. 大促洪峰到底在考验什么:先给“双十一凌晨”建立工程模型
1.1 为什么偏偏是双十一凌晨,而不是普通晚高峰
每天晚上的流量高峰,其实爬虫都能应付,因为请求量是缓慢爬升的,代理池和调度器有时间自动扩容。但双十一凌晨不一样,活动页0点准时上线,秒杀、优惠券、库存信息几乎在同一秒触达大量用户。如果你的爬虫是做价格对比、库存监控、优惠信息聚合,那么运营团队一喊“开始采集”,你的系统就必须在几十秒内从日常几百QPS冲到数千QPS。这种指数级的流量突刺,才是真正压垮代理通道的元凶。
传统短效代理在这种场景下的问题很明显:你得先有一批备好的IP清单,请求发出前手动绑定IP,一旦池子里可用IP不足,请求就会卡在“等待分配IP”这一步。而隧道代理不一样,你只需要配置一个固定的入口地址,代理网关会自动帮你从IP池里挑选出口IP。听起来省心,但这恰恰意味着你把自己的可用性完全交给了代理服务商,如果网关的并发调度能力不行,或者IP池的复用策略过于激进,你的爬虫就会集体超时。
1.2 压力测试不是“跑到挂为止”,而是找到拐点和恢复能力
我给这次压测定了一个原则:不是让代理申请多高的并发,然后看它会不会挂,而是模拟真实业务中“双十一凌晨”的请求分布,测出三个东西——可用性、稳定性、动态IP质量。
可用性就是成功率,简单说100个请求里有多少个能正常返回;稳定性看的是延迟曲线的抖动,尤其是TP99数字,如果平均延迟只有200ms但TP99超过3秒,说明系统里有不少请求在排队;动态IP质量则是隧道代理特有的观察维度,因为隧道代理是动态换出口IP的,如果压测时发现大量请求落到同一个出口IP上,那说明这个隧道网关并没有真正把你的流量分散开,继续加大并发只是加速触发目标网站的反爬策略。
这三个维度一确定,压测脚本的设计就有了目标。
2. 隧道代理的工作原理和选型关注点:搞懂它,压测才不会白做
2.1 隧道代理不是“一个代理IP”,而是一条“代理通道”
很多人第一次接触隧道代理都会有一个误解:把隧道地址当成一个固定代理IP来用。实际上,隧道代理的入口是一个固定的域名或IP端口,你发送请求时带着这个入口地址的认证信息,代理网关收到之后,会根据会话状态、目标站、IP池策略,动态从几万甚至几十万个IP中挑一个作为出口IP。
这里面的关键机制是“会话保持”。隧道代理一般会把一段时间内来自同一个客户端的请求绑定到同一个出口IP上,直到会话结束或主动切换。这种设计的好处是:如果你在爬一个需要通过多次请求完成的任务(比如先登录、再读取列表、再查看详情),不会因为中途换了IP导致登录态失效。坏处是:如果网关的会话保持策略写得太宽松,一个出口IP可能会被分配大量请求,高并发下就变成了“伪动态IP”。
所以压测之前,先要确认你的隧道代理的切换粒度。是每一个请求都切换IP,还是按照N分钟/会话维度切换?具体参数一定要查服务商文档或直接找售后确认,不同厂商的叫法差异很大,有的叫“会话保持时间”,有的叫“IP生命周期”。在2026年的市场环境下,主流隧道代理的会话保持时间通常可以配置为1分钟到30分钟不等,但换成IP数就天差地别了。
2.2 选型关注点:并发上限、认证方式、协议和地理覆盖
做压测之前,我先把隧道代理的选型参数整理成了一张表,避免测试了半天才发现是选型不适合业务场景。
| 关注点 | 需要确认的问题 | 对压测的影响 |
|---|---|---|
| 并发数上限 | 服务商承诺的最大并发连接数是多少 | 决定压测脚本是否直接触碰限制 |
| 认证方式 | 用户名密码认证还是Token认证 | 认证失败率和鉴权性能直接影响成功率 |
| 协议支持 | HTTP、HTTPS、WebSocket是否都走隧道 | HTTPS场景下涉及TLS握手耗时 |
| 会话保持粒度 | 多久换一次出口IP,是否可配置 | 决定IP重复率和反爬触发概率 |
| 地区覆盖 | 出口IP是否覆盖你目标站的地区 | 区域不匹配会带来延迟和风控问题 |
| 计费模式 | 按流量、按IP数还是按并发数 | 压测产生的流量开销需要提前评估 |
如果你的业务是采集国内电商平台,那么尽量选择出口IP和机房分布在国内主节点较多的隧道代理。某些服务商虽然在国内有节点,但压测时会随机抽到延迟很高的边缘节点,造成成功率虚高、延迟也高的情况。
3. 搭建一套可复现的隧道代理压测环境:工具、脚本和目标服务
3.1 压测目标:别拿真实业务站点练手,自己搭一个Mock接口
先说一个压测的大原则:要压测就压测自己完全可控的服务,不要用隧道代理去打真实的电商页面。一是合规问题,大规模请求打到别人服务器上很容易被判定为攻击;二是真实站点受反爬、带宽、CDN影响,出了问题很难分清是代理的锅还是目标站的锅。
我建议自己搭一个Mock接口,用来模拟“商品详情页”的响应。技术栈我用FastAPI,因为它足够轻量,能快速实现带随机延迟的JSON接口。比如下面这段代码,就是模拟一个平均响应时间150ms、偶尔出现500ms延迟的接口:
from fastapi import FastAPI import random, time app = FastAPI() @app.get("/api/ping") async def ping(): time.sleep(0.05 + random.random() * 0.2) return {"code": 0, "data": {"status": "ok"}, "request_id": random.randint(10000, 99999)}注意这里用了一点同步time.sleep,配合FastAPI的线程池就够了,目的是让压测请求能产生排队效果。如果你想模拟更真实的情况,还可以在接口里加一个简单的限流逻辑,比如超过200QPS就返回429状态码,这样你就能观察到隧道代理在目标端产生限流时,爬虫能不能正常拿到响应头,以便区分错误来源。
3.2 压测脚本:我为什么不直接用wrk,而是选了Python asyncio
wrk、ab这类工具适合压测普通HTTP服务,但隧道代理压测有个特殊性:你可能需要在请求头里定制User-Agent、需要统计出口IP的变化情况、需要在出现代理认证错误时做特殊记录。用传统压测工具写这些扩展逻辑很别扭,所以我自己写了一个基于Python asyncio + aiohttp的压测脚本。
先安装依赖:
pip install aiohttp asyncio然后创建一个proxy_stress.py,核心逻辑是:用信号量控制并发数,每个Worker循环发送请求到Mock接口,通过隧道代理转发,并记录请求耗时、状态码、错误类型,以及响应头或者请求会话里返回的出口IP信息。
import asyncio import aiohttp import time import statistics import argparse from collections import Counter PROXY_URL = "http://username:password@隧道代理入口:端口" TARGET_URL = "http://你的Mock服务IP/api/ping" async def single_request(session, sem, results, idx): async with sem: headers = { "User-Agent": f"Mozilla/5.0 (compatible; MySpider/{idx})", "Accept": "application/json" } start = time.perf_counter() try: async with session.get( TARGET_URL, proxy=PROXY_URL, headers=headers, timeout=aiohttp.ClientTimeout(total=10) ) as resp: await resp.text() elapsed_ms = (time.perf_counter() - start) * 1000 results.append({ "ok": resp.status == 200, "status": resp.status, "ms": elapsed_ms, "error": None }) except Exception as e: elapsed_ms = (time.perf_counter() - start) * 1000 results.append({ "ok": False, "status": None, "ms": elapsed_ms, "error": str(e) }) async def main(concurrency, total_requests): sem = asyncio.Semaphore(concurrency) async with aiohttp.ClientSession() as session: tasks = [] results = [] for i in range(total_requests): tasks.append(single_request(session, sem, results, i)) await asyncio.gather(*tasks) return results if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--concurrency", type=int, default=100) parser.add_argument("--requests", type=int, default=1000) args = parser.parse_args() results = asyncio.run(main(args.concurrency, args.requests)) success = [r for r in results if r["ok"]] failed = [r for r in results if not r["ok"]] latencies = sorted([r["ms"] for r in results]) print(f"总请求数: {len(results)}") print(f"成功率: {len(success) / len(results) * 100:.2f}%") print(f"失败数: {len(failed)}") if latencies: print(f"平均延迟: {statistics.mean(latencies):.1f}ms") print(f"TP50: {latencies[len(latencies) // 2]:.1f}ms") print(f"TP99: {latencies[int(len(latencies) * 0.99) - 1] if len(latencies) >= 100 else latencies[-1]:.1f}ms") status_counter = Counter(r["status"] for r in results if r["status"] is not None) print(f"状态码分布: {dict(status_counter)}")这个脚本的关键点是用了asyncio.Semaphore控制并发,而不是一次性创建无限协程。如果直接asyncio.gather成千上万个请求,压测机自己先会成为瓶颈,那测的就是客户端而不是代理了。信号量的数量就是并发数,建议把它设置成和业务爬虫预期的线程池大小一致。
3.3 压测机的选择:别在本地笔记本上测,服务器之间延迟更接近现实
还有一点容易被忽略:压测机和目标服务之间的网络路径。如果你在本地连公司Wi-Fi去压测云上一台Mock服务,那网络延迟会随Wi-Fi波动,结果很不稳定。最好的方式是把压测脚本部署在一台和Mock服务在同一内网或同一云厂商区域的Linux服务器上,再把隧道代理作为中间跳板。这样测出来的延迟差异,才能真正反映隧道代理的转发性能。
我这次用的是一台4核8G的云服务器作为压测机,Mock接口部署在另一台2核4G的服务器上,两台机器走内网通信。这样做既避免了公网链路的不确定性,又让Mock接口自身不会因为带宽限制成为瓶颈。
4. 设定“双十一凌晨”的压测模型:并发阶梯、请求时长和流量模型
4.1 用历史数据推算峰值QPS,而不是拍脑袋定并发数
压测参数不能随便写。我把过去30天爬虫系统的请求日志拉出来,统计出日常平均QPS大约是80,晚高峰峰值在180左右。再结合今年业务方给的预测:2026年双十一凌晨第一波流量爆发系数大约是日常峰值的3倍,所以预期峰值QPS在500到600之间。
基于这个数据,我设计了5个压测阶段:
| 阶段 | 并发数 | 预计QPS | 持续时间 | 目的 |
|---|---|---|---|---|
| P1 | 50 | 50-80 | 3分钟 | 基线,确认代理通道正常 |
| P2 | 150 | 150-200 | 3分钟 | 模拟日常晚高峰 |
| P3 | 300 | 300-350 | 3分钟 | 模拟大促快速爬升期 |
| P4 | 500 | 500-600 | 5分钟 | 模拟双十一零点峰值 |
| P5 | 300 | 300-350 | 3分钟 | 观察降级后恢复能力 |
注意QPS并不是完全等于并发数,因为每个请求要等Mock接口响应完才能退出信号量,所以实际QPS取决于延迟。并发300、平均延迟200ms的场景下,QPS大约是1500,根本压不到300。因此在脚本里建议不仅设置并发数,还设置每个Worker循环请求的次数或时间窗口,保证并发数一直在高位而不是潮汐波动。
4.2 为压测脚本加上“业务特征”,才像真实爬虫
真实爬虫的请求不是纯机械的“同一个URL刷一万次”,它会有不同的路径、不同的User-Agent、随机的思考间隔。我在脚本里增加了一个简单的轮询URL列表,比如模拟商品详情页、商品列表页、搜索页三种路径;再创建一个UA池,每个请求随机取一个。这样隧道代理看到的流量模式更像一个活生生的人在浏览器里操作,而不是一个死板的压测工具。
UA_POOL = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15 Safari/605.1.15", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148" ] TARGET_PATHS = ["/api/ping", "/api/product/12345", "/api/list?keyword=test"]不过有一点要分清:请求特征多样化主要是为了测试目标站风控场景下的表现,不是为了欺骗压测目标。如果只是看代理通道本身的性能,保持同样的请求头反而更公平,因为变量少,数据更容易对比。我这次的压测其实分了两轮,第一轮固定UA只测代理转发性能,第二轮随机UA测完整性,后续分析以第一轮数据为主。
4.3 压测记录:日志里要带时间戳、并发、请求ID
压测跑起来之后,不能只看聚合结果,还要能留出原始日志供排查。我在脚本里把每一个结果都追加到CSV文件,字段包括时间戳、请求序号、并发数、耗时、状态码、错误信息。这样后续画延迟曲线、定位毛刺点都有依据。
import csv def save_results(results, concurrency): with open(f"stress_{concurrency}.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["timestamp", "idx", "concurrency", "ms", "status", "error"]) for i, r in enumerate(results): writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), i, concurrency, r["ms"], r["status"], r["error"]])5. 压测结果怎么读:成功率、TP99和出口IP重复率的三维分析
5.1 从“成功率”和“错误类型”找第一层问题
压测结束后,统计结果如下:
| 阶段 | 并发数 | 成功率 | 平均延迟 | TP99 | 异常类型Top1 |
|---|---|---|---|---|---|
| P1 | 50 | 100% | 210ms | 380ms | 无 |
| P2 | 150 | 99.95% | 245ms | 520ms | 偶发超时 |
| P3 | 300 | 99.30% | 318ms | 920ms | 连接超时 |
| P4 | 500 | 86.40% | 780ms | 3500ms | 认证失败/连接重置 |
| P5 | 300 | 99.10% | 340ms | 1100ms | 连接超时 |
P4阶段的成功率掉到86.4%,意味着500个请求里有接近70个失败。这个结果其实很说明问题:隧道代理的可用并发上线大概在300-400之间,到了500以后网关的调度已经跟不上了。从错误类型看,P4有大量“连接重置”错误,说明代理网关在过载时可能主动断开了己方连接。
5.2 分析延迟曲线:慢请求是均匀分布还是集中毛刺
只看平均延迟很容易被误导。P4阶段平均延迟780ms,如果只盯着这个数字,你觉得还能接受,但把耗时分布拉出来看,有大约2%的请求延迟超过了3.5秒。这些慢请求对应的是网关排队调度,说明在高并发下代理网关的调度算法存在明显的排队效应。
再往下看,P4阶段的TP50其实只有520ms,说明一半请求还是快的,但尾巴拉得很长。这种“头尾分裂”的延迟分布,对爬虫业务非常致命,因为业务方只关心最终结果的返回时间,一旦有大量请求卡到超时阈值,即使成功率还有86%,体验也是崩的。
5.3 出口IP重复率:隧道代理最容易踩的隐性坑
我在压测脚本里其实有意识地记录了请求出口IP信息(可以通过Mock接口获取客户端IP,或者隧道代理返回的IP头)。分析时发现:并发数从50升到500的过程中,出口IP的重复率明显上升。并发50时,1000个请求大约用了600多个不同出口IP;到了并发500,1000个请求只用了80多个出口IP,重复率超过90%。
这说明隧道代理网关在高并发下为了保证会话速度,会选择让大量请求复用同一个出口IP,而不是均匀分散。如果你爬的是对IP维度的风控比较敏感的目标站点,这种高重复率会迅速触发反爬,让你在代理侧没崩的时候,目标侧先把你封了。所以压测时一定不要忽略出口IP去重情况,建议在日志里记录每个请求经过的出口IP,最后用集合统计。
6. 如果发现“撑不住”怎么办:代理侧和爬虫侧的四级调优
6.1 代理侧调优:尽量榨干隧道代理的并发潜力
第一级调优是检查客户端连接是否复用。aiohttp的ClientSession默认会复用连接池,所以我在脚本里是复用的。但如果你用的是requests,没有用requests.Session(),每次请求都重建TCP连接,那么到了高并发阶段,客户端和代理网关之间会积累大量TIME_WAIT连接,延迟会飙升。优化方式是把连接池上限打开:
connector = aiohttp.TCPConnector(limit=500, limit_per_host=200, ttl_dns_cache=300) async with aiohttp.ClientSession(connector=connector, proxy=PROXY_URL) as session: ...第二级调优是调整隧道代理的会话保持时间。如果业务不需要长会话,把切换粒度调到最小,让每个请求都可能走不同出口IP,这样可以降低IP重复率。但要注意,切换粒度变小会增加网关动态调度的负担,对应延迟可能会略微上升,需要权衡。
第三级调优是增加“备用通道”。如果你的服务商只提供一条隧道入口,可以考虑准备两个不同服务商的隧道代理作为主备,压测时在主入口失败时自动切换。当然这只适用于你真的需要跑到更高并发,否则别把架构搞复杂。
6.2 爬虫侧调优:让代码主动适配代理的弱点
代理侧优化始终有上限,爬虫侧得学会“认怂”和“退避”。我在压测后发现,只要在客户端增加一个简单的限流熔断器,就能避免在代理网关过载时继续盲目打请求。具体做法是:维护一个最近1秒请求延迟的滑动窗口,如果TP99连续30秒超过2000ms,就自动把并发数降低40%,等延迟回落后再慢慢恢复。
另外,重试逻辑不能是无脑重试。很多爬虫框架默认失败就立刻重试,这在隧道代理的场景下会形成“重试放大效应”:代理网关已经过载,你还在不断重试,网关雪上加霜。我建议重试策略改成指数退避,并且只对“连接超时”“5xx错误”重试,对“4xx”直接跳过。
还有个小技巧是在爬虫端做URL去重。双十一凌晨会出现大量重复请求,如果同一URL在爬虫内部已经被抓过了,就不需要再通过代理转发。用布隆过滤器加一层去重,能省掉相当一部分无效请求,变相降低代理压力。
6.3 调优后的复测结果:向“双十一凌晨”靠拢
调优后,我针对P4阶段(并发500)又跑了一轮,结果变化非常明显:
| 指标 | 调优前 | 调优后 |
|---|---|---|
| 成功率 | 86.40% | 98.75% |
| 平均延迟 | 780ms | 512ms |
| TP99 | 3500ms | 1450ms |
| 重试放大比例 | 1:1.8 | 1:0.6 |
主要变化是连接池复用和重试策略调整带来的。成功率虽然没到99.9%,但至少从“不可用”变成了“可用”。如果你在真实业务中要扛双十一凌晨峰值,我建议把目标定为“并发500下TP99不超过1500ms”,这样用户侧体验才不至于崩。
7. 踩坑记录:我做隧道代理压测时踩过的那几个坑
7.1 认证信息被URL编码吃掉
压测第一轮,代理地址里密码包含@和#这样的特殊字符,直接拼接在http://user:pass@host:port里,结果请求全部报“401 Proxy Authentication Required”。排查了很久才发现是特殊字符没有做URL编码。解决办法是用urllib.parse.quote对用户名密码编码后再拼到代理地址里。
from urllib.parse import quote proxy_url = f"http://{quote(username)}:{quote(password)}@{host}:{port}"7.2 高并发下压测机本地端口不够用
并发调到500以后,压测机报Cannot assign requested address。这是因为客户端大量发起短连接,本地端口号耗尽。调优办法有两个:一是开启net.ipv4.ip_local_port_range范围,二是前提是脚本复用连接池。我最终把Linux内核参数改成了:
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535" sudo sysctl -w net.ipv4.tcp_tw_reuse=1 sudo sysctl -w net.ipv4.tcp_fin_timeout=15当然,这只是客户端侧的问题。如果你是在内网压测,也要确认隧道代理服务端有没有对连接数做限制,否则客户端再快也白搭。
7.3 压测时把Mock接口打崩了要分清责任
第二轮随机UA压测时,我的P3阶段成功率突然掉到80%,一开始以为是隧道代理问题,后来看日志发现是目标站Mock接口的线程池满了,返回了大量“503 Service Unavailable”。这时候需要先确认错误是发生在代理转发前还是代理转发后。我们做法是把Mock接口部署在压测机直连能访问的路径上,先跑一次不带代理的基线压测,确定目标服务本身能承受多少QPS;再跑带代理的压测,两者的差异才算是代理链路带进来的额外损耗。
7.4 监控告警一定要提前接好
压测时别只看最终报告,建议把成功率、平均延迟、TP99、异常数实时推到监控面板,并设置告警阈值。我这次压测如果没有即时告警,P4阶段的失败率飙升可能要等压测结束看CSV才发现,那就错过排查的黄金时间了。
根据我个人经验,隧道代理压测这件事,至少要在大促前一个月跑完完整一轮,并且每次业务预期QPS变化后重新校准。不要指望一劳永逸,服务商的IP池质量、网络线路、网关调度算法都会随着时间变化,你花一个下午测出来的结论,可能三个月后就不准了。2026年的双十一,至少提前做一次这样的压测,再遇到凌晨告警,才不会慌。