首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >企业隧道代理怎么选?从本地 IP 池切换到统一入口,架构上要看什么?

企业隧道代理怎么选?从本地 IP 池切换到统一入口,架构上要看什么?

原创
作者头像
爱漫游的安安
发布于 2026-10-14 17:54:33
发布于 2026-10-14 17:54:33
780
举报

刚开始用短效代理时,我一直觉得自己维护 IP 池也没多复杂。

调接口取一批 IP,顺手记下过期时间;某个节点连接失败,就换下一个。一个脚本这么写没问题,两个脚本也能继续凑合。等任务开始长时间运行,进程多起来,麻烦才慢慢冒出来。

哪个 IP 正在被使用,哪个已经连续失败,指定城市的资源要放进哪个池子,不同进程怎么避免反复拿到同一条记录……业务代码没增加多少,维护代理的代码倒是越来越长。

也是到了这一步,我才会考虑把本地 IP 池换成隧道代理。

隧道代理的思路不复杂:程序不再直接拿到一批出口 IP,而是始终连接一个固定入口。具体从哪个出口转发、什么时候切换,由服务端处理。

代码确实会干净不少。

代码语言:javascript
复制
import os
​
import requests
​
​
proxy_url = os.environ["TUNNEL_PROXY_URL"]
​
proxies = {
    "http": proxy_url,
    "https": proxy_url,
}
​
response = requests.get(
    "https://example.com/public-page",
    proxies=proxies,
    timeout=(5, 15),
)
​
print(response.status_code)

拿极安代理的国内隧道产品来说,它采用的就是这种统一入口方式,出口切换和异常节点处理放在云端,程序侧不再需要维护具体 IP。同时支持 HTTP、HTTPS、SOCKS5,以及白名单和账密两种常见鉴权方式。原来的程序已经支持标准代理配置时,通常改一下地址和认证信息就能接入。

不过,我不会因为这十几行代码能跑通,就认定产品可以上线。

固定入口只解决了“程序不用自己存一堆 IP”这件事。出口多久换一次、峰值请求能不能接住、连接异常以后程序怎么处理,这些问题还在。

我最先问的,是出口多久换一次

隧道代理并不都是每个请求更换出口。

有的产品默认每次请求切换,有的可以保持几十秒或几分钟,还有的能根据地区设置转发规则。这个区别看起来只是后台多了几个选项,放进程序里,影响却很直接。

如果每个请求互不相关,比如逐个读取一批公开页面,每次换出口一般没什么问题。

但有些程序不是这么跑的。它可能先访问列表页,拿到 Cookie 或其他状态,再继续请求详情页。即使代码始终使用同一个 Session,也不代表代理出口会保持不变。

代码语言:javascript
复制
import requests
​
​
def create_session(proxies):
    session = requests.Session()
    session.proxies.update(proxies)
    session.headers.update({
        "User-Agent": "DataClient/1.0",
    })
​
    return session
​
​
def fetch_pages(session):
    list_page = session.get(
        "https://example.com/public-list",
        timeout=(5, 15),
    )
    list_page.raise_for_status()
​
    detail_page = session.get(
        "https://example.com/public-detail",
        timeout=(5, 15),
    )
    detail_page.raise_for_status()
​
    return detail_page.text

要是隧道设置成每次请求换出口,上面两次请求就可能来自不同 IP。程序本身没写错,代理也正常工作,但会话就是接不上。

所以我通常先把任务分成两类。

请求彼此独立,优先测试按请求切换;一组请求需要保持状态,就看能不能按固定周期切换。如果产品只能每次请求更换,而业务偏偏依赖连续会话,我一般不会继续研究套餐,直接换产品形态更省时间。

自动换 IP,不等于程序不用处理异常

以前自己维护 IP 池时,节点失败以后要手动替换。换成隧道代理,服务端确实能承担一部分异常出口切换,但应用侧的超时和重试还是不能删。

我比较反感把所有异常都塞进一个 except Exception,然后统一重试。这样写起来省事,真出问题时几乎没有排查线索。

至少要把代理错误、连接超时和读取超时分开。

代码语言:javascript
复制
import time
from typing import Any
​
import requests
from requests.exceptions import (
    ConnectTimeout,
    ProxyError,
    ReadTimeout,
    RequestException,
)
​
​
def request_once(
    session: requests.Session,
    url: str,
) -> dict[str, Any]:
    started_at = time.perf_counter()
​
    try:
        response = session.get(
            url,
            timeout=(5, 15),
        )
​
        elapsed_ms = (
            time.perf_counter() - started_at
        ) * 1000
​
        return {
            "type": "response",
            "status_code": response.status_code,
            "elapsed_ms": round(elapsed_ms, 2),
            "response_bytes": len(response.content),
        }
​
    except ProxyError as exc:
        return {
            "type": "proxy_error",
            "message": str(exc),
        }
​
    except ConnectTimeout:
        return {
            "type": "connect_timeout",
        }
​
    except ReadTimeout:
        return {
            "type": "read_timeout",
        }
​
    except RequestException as exc:
        return {
            "type": "request_error",
            "message": str(exc),
        }

代理连接失败,更接近入口、鉴权或转发问题。读取超时则不一定是代理导致的,目标页面响应慢、内容较大或者超时设得太短,都有可能。

这也是我不太愿意直接拿供应商公布的 IP 可用率判断业务效果的原因。IP 可以建立连接,不代表目标页面一定能按预期返回。两件事最好分开记录。

重试也别写成死循环。我通常只给网络类异常有限的机会:

代码语言:javascript
复制
import time
​
​
RETRYABLE_ERRORS = {
    "proxy_error",
    "connect_timeout",
    "read_timeout",
}
​
​
def fetch_with_retry(
    session,
    url,
    max_attempts=3,
):
    last_result = None
​
    for attempt in range(max_attempts):
        result = request_once(session, url)
        last_result = result
​
        if result["type"] == "response":
            return result
​
        if result["type"] not in RETRYABLE_ERRORS:
            return result
​
        if attempt < max_attempts - 1:
            time.sleep(0.5 * (2 ** attempt))
​
    return last_result

配置错误反复重试没有意义,明确的业务状态也不能一律靠换出口解决。隧道代理可以减少 IP 调度工作,但不能代替程序本身的错误处理。

“不限终端”这句话,我一般只看一半

不少产品会写不限终端。这个条件对企业确实有用,多台服务器不用分别购买独立账号。

但不限终端不等于不限请求。

假设有五台服务器,每台每秒发出两个请求,最后占用的仍然是每秒十次的整体请求频率。采购时如果只拿单台机器的频率去选套餐,上线后很容易碰到容量问题。

我一般会先从日志里拿两个数字:正常运行时平均每秒多少请求,峰值时每秒多少请求。峰值偶尔出现和连续半小时维持在高位,也不是一回事。

测试时,可以用线程池先观察并发下的异常分布:

代码语言:javascript
复制
from concurrent.futures import (
    ThreadPoolExecutor,
    as_completed,
)
from statistics import median
​
​
def percentile(values, ratio):
    if not values:
        return None
​
    index = int((len(values) - 1) * ratio)
    return values[index]
​
​
def run_probe(
    session_factory,
    url,
    total_requests=200,
    workers=5,
):
    def execute():
        session = session_factory()
        return request_once(session, url)
​
    results = []
​
    with ThreadPoolExecutor(
        max_workers=workers,
    ) as executor:
        futures = [
            executor.submit(execute)
            for _ in range(total_requests)
        ]
​
        for future in as_completed(futures):
            results.append(future.result())
​
    responses = [
        item
        for item in results
        if item["type"] == "response"
    ]
​
    elapsed = sorted(
        item["elapsed_ms"]
        for item in responses
    )
​
    return {
        "total": len(results),
        "responses": len(responses),
        "proxy_errors": sum(
            item["type"] == "proxy_error"
            for item in results
        ),
        "connect_timeouts": sum(
            item["type"] == "connect_timeout"
            for item in results
        ),
        "read_timeouts": sum(
            item["type"] == "read_timeout"
            for item in results
        ),
        "median_ms": (
            median(elapsed)
            if elapsed
            else None
        ),
        "p95_ms": percentile(elapsed, 0.95),
    }

线程数不能直接等同于每秒请求数,但这段测试能先看出,多任务一起运行时异常会不会明显增加。

正式测套餐时,我会再加一个限速器,让请求稳定在计划频率附近,不会一上来就把所有请求瞬间发出去。

代码语言:javascript
复制
import threading
import time
​
​
class RateLimiter:
    def __init__(self, requests_per_second):
        self.interval = 1 / requests_per_second
        self.lock = threading.Lock()
        self.next_time = time.perf_counter()
​
    def wait(self):
        with self.lock:
            now = time.perf_counter()
​
            if now < self.next_time:
                time.sleep(self.next_time - now)
​
            self.next_time = (
                max(now, self.next_time)
                + self.interval
            )

在真正发出请求前调用:

代码语言:javascript
复制
limiter = RateLimiter(
    requests_per_second=5,
)
​
​
def limited_request(session, url):
    limiter.wait()
    return request_once(session, url)

平均响应时间我也不会单独看。

大部分请求很快、少量请求特别慢时,平均数很容易把问题盖住。中位数、P95 和超时次数更有参考价值。常用地区是否能够正常分配、出口变化是否符合会话要求,也应该单独记录。

最后,我会这样判断要不要切换

如果本地 IP 池已经占用了不少维护时间,而程序本身并不需要精确控制每一条出口,隧道代理通常值得测试。

测试也不用做得特别复杂。我会拿现有程序最常跑的几个公开页面,保持地区、请求频率和运行时间不变,换成隧道入口连续跑一段时间。重点看连接超时、读取超时、P95 响应时间,以及出口切换会不会影响会话。

测试结果如果与原来的本地 IP 池接近,同时代码和日常维护明显简单了,切换就有意义。

如果程序依赖固定出口、长时间会话,或者需要自己精确决定每条 IP 的使用方式,我不会为了省几段管理代码强行换隧道。

说到底,统一入口帮我省掉的是 IP 调度,不是整个程序的稳定性设计。超时、有限重试、日志和容量统计,该保留的一个也不能少。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 我最先问的,是出口多久换一次
  • 自动换 IP,不等于程序不用处理异常
  • “不限终端”这句话,我一般只看一半
  • 最后,我会这样判断要不要切换
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档