你配置了轮换代理,发送了十个请求,结果每一个都报告相同的出口地址。显而易见的结论是轮换出了故障。但通常并非如此。多数情况下,网关正是按要求分配了新地址,而你的代码与网关之间的某个环节阻止了这一切发生,最常见的元凶是你的HTTP客户端中一个原本为了提速而存在的功能。
以下按照值得排查的先后顺序列出各种原因,以及各自的修复方法。
首先,正确地检查它
在诊断之前,先确保测试本身是可靠的。每次调用只发送一个命令行请求是最干净的检查方式,因为每次运行都是一个独立的进程,没有共享状态:
for i in 1 2 3 4 5; do
curl -s -x customer-USERNAME:PASSWORD@p.shifter.io:443 https://ipinfo.io/json \
| python3 -c "import sys,json; print(json.load(sys.stdin)['ip'])"
done
得到五个不同的地址,说明轮换正常,问题出在你的应用程序上。得到五个相同的地址,说明问题出在配置上。仅凭这一区别就能省下大部分调试时间,所以先运行这个测试。
原因一:用户名中的会话标识符
最简单的解释。如果你的用户名中包含sid标志,那你已经明确要求了粘性会话,此时保持地址不变是正确行为,而非故障。
customer-USERNAME-country-us-sid-abc123 # 粘性:按设计使用同一IP
customer-USERNAME-country-us # 轮换:每次请求使用新IP
这种情况常见于从文档中复制了示例,或使用某个默认添加会话的辅助工具构建了用户名。去掉sid标志,同时去掉ttl,因为TTL只有配合会话才有意义。相关语义可参见粘性代理与轮换代理对比以及会话文档。
一种更隐蔽的情况:你的会话标识符本应变化,但实际上是恒定的。如果你为每个任务生成一个标识符,但该任务运行了大量请求,那么该任务中的每个请求都会共享同一个地址,这是正确的,但可能并非你的本意。
原因二:连接复用,这是几乎所有人都会遇到的坑
上面curl循环能正常轮换,而你的代码却不能,这种情况下的答案多半就是它。
现代HTTP客户端会保持连接存活并复用它们,因为为每个请求重新建立TCP和TLS连接太慢了。当你的客户端复用一个已建立的到代理的隧道时,请求会通过那个已经建立的连接传输,而该连接已经绑定了一个出口地址。轮换是按连接进行的,而不是按通过现有连接发送的请求进行的,因此一个维护连接池的会话对象会忠实地把每个请求都发送到同一个出口。
修复方法取决于你想要多大程度的控制。可以禁用keep-alive,或强制每个请求使用新连接,或让每个逻辑请求使用一个全新的客户端。
import requests
PROXY = "http://customer-USERNAME:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
# 复用同一个连接:每个请求都是相同的出口地址
s = requests.Session()
for _ in range(5):
print(s.get("https://ipinfo.io/json", proxies=PROXIES).json()["ip"])
# 每次请求使用新连接:按预期轮换
for _ in range(5):
r = requests.get("https://ipinfo.io/json", proxies=PROXIES,
headers={"Connection": "close"}, timeout=20)
print(r.json()["ip"])
同样的道理也适用于其他环境:Node中带keep-alive的http.Agent、.NET或Java中共享的HttpClient、Go中的连接池。如果你所用的语言有一个默认会池化连接的客户端,而它们几乎都是这样,这就是首先要在应用程序代码中检查的地方。
值得明确指出的是:这是一种权衡,而非缺陷。连接复用速度更快、成本更低。如果你的工作确实需要每个请求都使用新地址,那你就得为每次新建连接付出代价;如果不需要,复用是没问题的,通常还更可取。
原因三:你检查得太快,或者对于该筛选条件而言池子比你想的要小
这里有两个相关的效应。
如果你把筛选条件设置得很严格,比如限定到一个小城市或特定的ASN,那么能为你服务的地址集合就比整体池子小得多,因此同一个地址合理地会更频繁地重复出现。这不是故障,而是算术问题。放宽筛选条件,重复现象就会消失。
同时要记住,住宅代理池由真实的、时来时去的连接组成,因此在大量请求中两次看到同一个地址是预期之中的,而非可疑现象。轮换意味着下一个请求会被独立选择,而不是意味着某个地址永远不会再次出现。如果你的工作负载需要保证地址的唯一性,那这是需要你在自己的代码中处理的设计约束。
原因四:上游有某个环节在缓存
如果你是通过浏览器、扩展程序、系统级代理设置或企业网络进行测试,请求实际发出的路径可能与你以为的不同。浏览器尤其会激进地保持连接开启并跨标签页复用,所以浏览器是验证轮换最糟糕的环境。先从命令行测试,再在你的应用程序中测试,最后才在浏览器中测试。
同样,如果你的代码既设置了代理环境变量,又显式传递了配置,其中一个可能会覆盖另一个,导致流量并未经过你所配置的网关,而是走了别的路径。
原因五:地址相同,但请求根本没有发出去
一个值得排除的直白情况:如果代理实际上根本没有被使用,那么每个请求报告的地址都相同,也就是你自己的地址。请确认你看到的地址不是你自己服务器的地址。如果是,说明代理配置根本没有生效,这是另一个问题,通常是缺少与http条目并列的https条目,或者客户端对你所用协议的代理设置视而不见。
原因六:粘性会话尚未过期
如果你是有意使用粘性会话,并期望它在一段时间后轮换,请记住地址会一直保持到TTL到期为止。除非你显式设置ttl,否则默认生命周期为120秒。如果你想更快获得新地址,应该更改会话标识符,而不是等待,因为新的标识符意味着新的会话。
另请注意,粘性地址可能会比其TTL更早失效,如果底层连接断开的话,因为这些是真实的家庭连接,而非专用基础设施。粘性意味着在请求的时长内尽力而为,而非保证。
快速判断路径
先运行curl循环。如果它能轮换而你的代码不能,那你遇到的是连接复用问题,即原因二。如果两者都不轮换,先检查用户名中是否有sid标志,然后确认你看到的不是自己的地址,再放宽任何过窄的地理筛选条件。如果轮换频率低于你的预期,但确实在轮换,那你面对的是该筛选条件下池子大小的问题,而非故障。
其他情况,相关行为记录在轮换机制如何运作中,如果是请求失败而非重复,为什么请求会超时和修复407错误涵盖了两种最常见的失败模式。
结论
轮换问题通常并非轮换本身的问题。先用独立进程测试,确定网关是否在轮换,如果是,再检查你的HTTP客户端,因为连接复用是迄今为止最常见的原因:通过已开启隧道发送的请求会保留该隧道已经绑定的出口地址。之后,检查是否有多余的sid标志,确认你看到的不是自己的地址,并记住非常狭窄的地理筛选条件会从小得多的集合中抽取,因此重复是正常的。粘性会话保持地址不变是按设计运作的,更改标识符会立即给你一个新地址。
轮换机制本身及控制它的标志位于住宅代理网络之上,在那里会话行为是按请求设置的参数,而非套餐设置,计费方式是按GB计费,与你轮换的频率无关。