每个代理开多少线程?测出安全的代理并发数
每个代理开多少线程才安全?从低并发开始,错误率和响应时间都平稳时再翻倍,最后按 IP 限定并发。附已测试的 asyncio 与 httpx 代码。
每个代理开多少线程,取决于目标网站,而不取决于代理本身。先让每个 IP 同时跑两到四个请求;只要错误率接近零、响应时间保持平稳,就把并发数翻倍;两者中任何一个开始变化,就停在那一级。实际运行时,用那一级的一半左右。
这个答案要配上测试才有用,所以本文给你一个:一段简短的 Python 脚本,对你自己的目标网站逐级提高代理并发数,报告在哪一级开始撑不住。文章还会讲轮换的住宅 IP 和静态 IP 分别怎么影响这笔账,以及一个会悄悄让轮换失效的连接池细节。代码里的 HOST、PORT、USERNAME 和 PASSWORD,换成控制台里服务页面上的值。
每个代理开多少线程,为什么没有统一答案
有四样东西会限制你的并发,最低的那个说了算:
- 目标网站对单个 IP 的限制。 大多数网站按地址计数。一个 IP 同时挂着十个请求,比十个请求分散在十个 IP 上显眼得多。
- 目标网站的整体承受力。 反爬规则、一台小服务器、一个很慢的搜索页,都可能不管你用多少 IP 都顶回来。
- 你自己的机器。 连接池、打开文件数上限、你的上行带宽,往往比代理先用完。代理超时怎么排查讲了从你这边看是什么样子。
- 服务商的套餐。 有些服务商按套餐限制线程数或端口数。在我们这里,常见问题中“有端口费、线程费或月费吗?”的回答是:不收端口费,不收线程费,也没有月度门槛。
真正决定这个数字的是前两项,而只有目标网站能告诉你它们是多少。所以答案只能靠测量。
并发、速率和线程是三个不同的数
**并发**是同一时刻正在进行的请求数。速率是每秒发出多少个请求。两者由每个请求花的时间联系在一起:
速率 ≈ 并发数 ÷ 每个请求的秒数
同时挂着四个请求、每个半秒,大约就是每秒八个请求。如果网站慢到每页两秒,同样四个并发就降到每秒两个。你的代码设定的是并发,而网站通常限制的是速率;网站在压力下往往先变慢,然后才开始拒绝。所以下面的逐级测试会盯着响应时间。
线程是获得并发的一种方式。八个线程的线程池,就是同时八个请求:
from concurrent.futures import ThreadPoolExecutor
import requests
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
PROXIES = {"http": PROXY, "https": PROXY}
THREADS = 4
def status(url):
try:
return requests.get(url, proxies=PROXIES, timeout=(5, 30)).status_code
except requests.RequestException as error:
return type(error).__name__
urls = [f"https://TARGET_SITE/page/{n}" for n in range(1, 41)]
with ThreadPoolExecutor(max_workers=THREADS) as pool:
for url, result in zip(urls, pool.map(status, urls)):
print(result, url)
用 asyncio 的话,信号量(Semaphore)不用线程也能做同样的事,本文后面都用这种写法。
轮换住宅 IP 和静态 IP 的区别
住宅代理,Randomize IP 模式。 每个新连接都从不同的家庭地址出去,你的并发分散在许多出口上,没有哪个 IP 承担很多。按 IP 的限制基本不再是问题;限制变成网站的整体承受力和你按 GB 计费的流量。出口都是普通家庭网络,有快有慢,响应时间的波动会比静态 IP 大。
静态 ISP 或数据中心 IP。 同一个 IP 发出的每个请求都算在它头上。总并发由你租的这些 IP 分摊,所以真正该问的是“每个 IP 多少个”,总数再乘以 IP 数量。下单时可以选国家、地区和城市,结果也更容易前后对比。两者怎么选,见 ISP 还是数据中心代理?
再说一句实话,我们的诚信页面上写着:我们的住宅 IP 池比大厂小。在防守严密的目标上跑很高并发时,这个差距是真实存在的。为一个任务下单之前,先测一测。
连接池会让轮换失效
HTTP 客户端会复用连接。对 https 网站来说,复用的连接就是穿过代理的同一条隧道,也就是同一个出口 IP。用 httpx.AsyncClient 同时跑四个请求时,二十个请求大约走四条隧道,也就是四个出口各跑五个请求。用 Python 做代理轮换讲了 requests.Session 的同一个坑。
如果想每个请求都换一个新出口,就在客户端里关掉保持连接:
import httpx
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
limits = httpx.Limits(max_keepalive_connections=0)
client = httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=httpx.Timeout(30.0, connect=5.0))
我们用一个会记录每条隧道的本地代理核对过:四个并发、二十个请求,默认设置下开了 4 条隧道,关掉保持连接后开了 20 条。代价是每个请求都要重新建立连接和 TLS 握手,会多花时间;如果每个工作协程固定一个出口对目标网站没问题,就保留默认设置。
用逐级加压测出你的数字
脚本在每个并发级别发一批请求,从 2 开始翻倍,每一级测三样东西:每秒页面数、响应时间中位数、失败比例。一出现问题的迹象就停下。
pip install httpx
import asyncio
import statistics
import time
from itertools import cycle, islice
import httpx
PROXY = "http://USERNAME:PASSWORD@HOST:PORT"
SAMPLE_URLS = [
"https://TARGET_SITE/page/1",
"https://TARGET_SITE/page/2",
"https://TARGET_SITE/page/3",
]
START, CEILING = 2, 32
REQUESTS_PER_SLOT = 10
MAX_ERROR_RATE = 0.02
MAX_SLOWDOWN = 2.0
MIN_GAIN = 1.2
COOL_DOWN = 30
async def timed_get(client, gate, url):
async with gate:
start = time.monotonic()
try:
response = await client.get(url)
status = response.status_code
except httpx.HTTPError as error:
status = type(error).__name__
return status, time.monotonic() - start
async def run_level(concurrency):
urls = list(islice(cycle(SAMPLE_URLS), concurrency * REQUESTS_PER_SLOT))
gate = asyncio.Semaphore(concurrency)
timeout = httpx.Timeout(30.0, connect=5.0)
started = time.monotonic()
async with httpx.AsyncClient(proxy=PROXY, timeout=timeout) as client:
results = await asyncio.gather(*(timed_get(client, gate, url) for url in urls))
elapsed = time.monotonic() - started
failed = sum(1 for status, _ in results if status != 200)
median = statistics.median(seconds for _, seconds in results)
return len(results) / elapsed, median, failed / len(results)
async def ramp():
safe, baseline, best_rate = None, None, 0.0
level = START
while level <= CEILING:
rate, median, error_rate = await run_level(level)
baseline = baseline or median
print(f"{level:>3} at once: {rate:5.1f} req/s, median {median:.2f}s, {error_rate:.0%} failed")
if error_rate > MAX_ERROR_RATE:
print(" errors: the site is pushing back")
break
if median > baseline * MAX_SLOWDOWN:
print(" responses slowed down: the site is queuing you")
break
if best_rate and rate < best_rate * MIN_GAIN:
print(" no real gain in throughput: more workers only wait longer")
break
safe, best_rate = level, rate
level *= 2
await asyncio.sleep(COOL_DOWN)
print(f"Highest level that held up: {safe}")
asyncio.run(ramp())
在 SAMPLE_URLS 里放三到十个目标网站上有代表性的页面。每一级按每个并发位发十个请求,所以第 8 级发 80 个,一直跑到第 32 级总共大约 600 个。把 CEILING 设成你实际最多会用的并发数;另外记住,在按 GB 计费的线路上,测试流量和其他流量一样计费。
怎么看结果
脚本会因为三种原因之一停下:
- 错误率超过 2%。 429、403、超时和连接错误都算。网站在往回推。
- 响应时间中位数比第一级翻了一倍。 网站或某个慢出口在让你排队,错误通常紧随其后。
- 每秒页面数的提升不到 20%。 多出来的并发只是在等。这是最安静的一种:没有任何失败,也没有变快。
下面是对一台本地测试服务器跑的结果,它一次处理六个请求,同时超过十个就拒绝:
2 at once: 8.0 req/s, median 0.24s, 0% failed
4 at once: 16.4 req/s, median 0.23s, 0% failed
8 at once: 25.8 req/s, median 0.28s, 0% failed
16 at once: 68.5 req/s, median 0.05s, 60% failed
errors: the site is pushing back
Highest level that held up: 8
注意第 16 级每秒页面数的跳升。拒绝来得很快,所以一个正在失败的级别看起来反而像最快的一级。脚本因此先检查错误率。
从结果到实际设置
- 按撑住的那一级的一半到四分之三来跑。 测试很短,而你的任务要跑好几个小时,负载持续下去,网站也会收紧。
- 用静态 IP 时,如果测试只用了其中一个 IP,结果就是每个 IP 的数字,给每个 IP 分配各自的份额。
- 在并发之上再加速率限制。 并发限制的是同时进行的请求数,页面很快时它管不住速度。429 Too Many Requests 怎么解决里有一个按主机限速的限速器。
- 情况变了就重新测:换了目标网站、换了线路,或者 429 连着一周在增加。
把任务分摊到多个静态 IP 上
有一组静态 IP 时,给每个 IP 固定数量的工作协程,这样没有哪个 IP 会超过自己的份额。所有工作协程从同一个队列里取下一个网址:
import asyncio
from collections import Counter
import httpx
PROXIES = [
"http://USERNAME:PASSWORD@HOST_1:PORT_1",
"http://USERNAME:PASSWORD@HOST_2:PORT_2",
"http://USERNAME:PASSWORD@HOST_3:PORT_3",
]
PER_IP = 2
async def worker(proxy, queue, results):
exit_label = proxy.rsplit("@", 1)[1]
timeout = httpx.Timeout(30.0, connect=5.0)
async with httpx.AsyncClient(proxy=proxy, timeout=timeout) as client:
while not queue.empty():
url = queue.get_nowait()
try:
response = await client.get(url)
results.append((exit_label, url, response.status_code))
except httpx.HTTPError as error:
results.append((exit_label, url, type(error).__name__))
async def main(urls):
queue = asyncio.Queue()
for url in urls:
queue.put_nowait(url)
results = []
await asyncio.gather(*(
worker(proxy, queue, results) for proxy in PROXIES for _ in range(PER_IP)
))
return results
urls = [f"https://TARGET_SITE/page/{n}" for n in range(1, 61)]
results = asyncio.run(main(urls))
print(Counter((label, status) for label, _, status in results))
三个 IP、PER_IP = 2,总共同时六个请求,每个地址从不超过两个。我们对一台会统计在途请求数的本地服务器跑过,峰值正好是六。想更快就加 IP;提高 PER_IP 只会让每个地址更显眼。
快速问答
住宅代理每个开多少线程? 不存在按 IP 的数字可找,因为每个连接都换一个新出口。用脚本逐级提高总并发,盯着 429 和变慢的响应。
每个数据中心或 ISP IP 开多少线程? 从每个 IP 两个开始往上测。很多网站接受几个;繁忙或防守严的网站只接受一个。
ProxyPanda 限制线程数吗? 我们的价格里不收端口费,不收线程费,也没有月度门槛。实际的上限在目标网站那边。
线程越多,爬虫越快吗? 只到网站或你的机器成为瓶颈为止。过了那个点,多出来的线程只会增加等待,然后是错误。
先做一次小测试
下更大的单之前,先通过一项服务对你的目标网站跑一遍逐级测试。各条线路的价格见价格页面;如果测出来的数字看着不对劲,带上目标网站和所用线路,发到 Discord 一起看看。