# 通过住宅代理实现并发和吞吐量

> 运行多少并发请求穿过住宅代理：李特尔定律，单主机限制，自适应并发代码及每小时GB数。

- URL: https://crawlrelay.com/zh/docs/concurrency/
- 最后更新: 2026-10-13
- Publisher: CrawlRelay (https://crawlrelay.com)

网关按 GB 计费，而不是按线程计费，因此您的实际限制是每个目标网站。根据所需吞吐量调整并发量，按每个主机设定上限，并通过响应情况决定何时减速。

**快速回答 — 我可以通过住宅代理运行多少并发请求？**

根据每个目标网站的承受能力：**CrawlRelay** 按住宅代理流量 GB 计费（50 GB 起US$0.50/GB），而非按线程或连接计费。使用 Little 定律计算并发请求数，飞行中的请求 = 每秒页面数 × 每页面秒数，然后为每个主机设定上限，初始值为2到4。遇到429或503响应时降低并发。支持加密货币支付，无需 KYC。

## 并发一览

| 属性 | 值 |
| --- | --- |
| 计费方式 | 按流量 GB 计费；线程和连接不计费 |
| 规模规则 | 飞行中的请求 = 吞吐量 × 延迟（Little 定律） |
| 每主机起始值 | 2–4 并行请求；小型网站每请求加延迟 |
| 减速信号 | 429、503响应、响应时间上升、挑战页面 |
| 带宽计算 | 每秒页面数 × 每页面字节数 × 3,600 = 每小时字节数 |
| 住宅代理中位响应时间 | 代理网络 0.93 秒，真实目标 3.21 秒（Proxyway 2026） |

## 如何根据所需吞吐量确定并发规模？

使用 Little 定律：飞行中请求数等于目标速率乘以每个请求所需时间。Proxyway 2026 年代理市场研究显示，住宅代理访问真实目标的中位请求时间为 3.21 秒，因此规划时以 2 到 4 秒计算真实网站，而非仅考虑网络的亚秒级响应。

| 目标 | 每请求时间 | 飞行中请求数 |
| --- | --- | --- |
| 每秒 5 页 | 2 秒 | 10 |
| 每秒 20 页 | 2 秒 | 40 |
| 每秒 20 页 | 4 秒 | 80 |
| 每秒 100 页 | 3 秒 | 300 |

然后将请求分布于不同主机。分布到 200 个站点上的 40 个飞行请求是温和的；40 个请求集中在一个小站点则是其未授权的负载测试。

## 该吞吐量使用多少带宽？

将每秒页面数乘以每页面字节数。以每秒 20 个 HTML 页面，每页 50 KB 计算，爬取速度为每秒 1 MB 即每小时 3.6 GB，约每日 86 GB。若每渲染页为 2 MB，则同速率带宽为每秒 40 MB。

用结果乘以计划中每 GB 价格，US$0.50/GB 起始 50 GB 为 CrawlRelay，即可算出每小时费用。[带宽计算器指南](https://crawlrelay.com/zh/guides/bandwidth-calculator/) 提供按页面类型的典型页面大小。

## 每主机限额应从多少开始？

| 目标 | 每主机并行请求数 | 请求间延迟 | 备注 |
| --- | --- | --- | --- |
| 小型网站（博客、本地商店） | 1 | 2–5 秒 | 若存在则遵守 `Crawl-delay` 中的 robots.txt |
| 中等网站 | 2–4 | 0.5–2 秒 | 监控响应时间；当响应时间翻倍时降低请求速度 |
| 拥有 CDN 的大型平台 | 4–16 | 自动节流或自适应 | 仅在 429 和 5xx 率保持平稳时增加速度 |
| 公共 API | 文档中说明的限制 | 如文档所述 | 使用服务商提供的 API 密钥和限制 |

## 如何自动调整并发数？

使用加法增加和乘法减少，这也是 TCP 处理拥塞的规则：在一系列正常响应后加一个槽位，遇到 429 或 503 时减半限制。以下限速器为每个主机维护一个限制，对该主机的每个请求暂停 `Retry-After` 延迟，重试一次，并且绝不超过您设置的上限。网络错误作为返回值出现，所以一次超时不会中断整个批次。

*Python asyncio：配合 httpx 的自适应限制*

```python
import asyncio
import random
from urllib.parse import urlsplit

import httpx

PROXY = "http://USERNAME-country-us:PASSWORD@gw.crawlrelay.com:7777"


class AdaptiveLimit:
    """One per host: +1 slot after 20 clean responses, halve on 429/503, pause for Retry-After."""

    def __init__(self, start=2, low=1, high=16):
        self.limit, self.low, self.high = start, low, high
        self.in_flight, self.clean = 0, 0
        self.pause_until = 0.0
        self.cond = asyncio.Condition()

    async def __aenter__(self):
        loop = asyncio.get_running_loop()
        while True:
            async with self.cond:
                await self.cond.wait_for(lambda: self.in_flight < self.limit)
                delay = self.pause_until - loop.time()
                if delay <= 0:
                    self.in_flight += 1
                    return self
            await asyncio.sleep(delay)  # the host asked for a pause: wait outside the lock

    async def __aexit__(self, *exc):
        async with self.cond:
            self.in_flight -= 1
            self.cond.notify_all()

    def record(self, status, retry_after=None):
        if status in (429, 503):
            self.limit, self.clean = max(self.low, self.limit // 2), 0
            wait = int(retry_after) if retry_after and retry_after.isdigit() else 5 + random.random()
            now = asyncio.get_running_loop().time()
            self.pause_until = max(self.pause_until, now + wait)
        elif status < 400:
            self.clean += 1
            if self.clean >= 20 and self.limit < self.high:
                self.limit, self.clean = self.limit + 1, 0


gates = {}


def gate_for(url):
    host = urlsplit(url).hostname
    if host not in gates:
        gates[host] = AdaptiveLimit()
    return gates[host]


async def fetch(client, url, attempts=2):
    gate = gate_for(url)
    status = None
    for _ in range(attempts):
        try:
            async with gate:
                r = await client.get(url)
        except httpx.HTTPError as exc:
            return "error: %s" % type(exc).__name__
        status = r.status_code
        gate.record(status, r.headers.get("Retry-After"))
        if status not in (429, 503):
            break  # the retry, if any, waits for the host's pause in __aenter__
    return status


async def main(urls):
    timeout = httpx.Timeout(30.0, connect=10.0)
    async with httpx.AsyncClient(proxy=PROXY, timeout=timeout) as client:
        print(await asyncio.gather(*(fetch(client, u) for u in urls)))
    print({host: gate.limit for host, gate in gates.items()})


asyncio.run(main(["https://example.com/"] * 10))
```

## 代理爬取除了目标主机外，什么会导致速度变慢？

- **TLS 握手。** 每次请求建立一个新连接，且每次请求都需要一个新 IP，这会增加与目标网站的握手次数。当 IP 多样性不重要时，对同一网站的多页请求保持粘性会话和长连接可以避免多次握手。
- **慢出口。** 住宅代理和 4G 移动代理是真实的家庭和移动网络链接。使用超时设置并在新连接上重试，而不是等待。
- **范围太窄。** 一个小城市或单个 ASN 的出口比整个国家少。仅在数据发生变化的区域精准投放。
- **页面渲染。** 无头浏览器消耗的 CPU、内存和流量远高于 HTTP 客户端。仅渲染需要 JavaScript 的页面。
- **DNS。** 通过 `socks5h://` 或 HTTP 端口，DNS 解析发生在出口处；无需为目标主机缓存本地解析器。

## 并发相关问题

### CrawlRelay 是否限制线程数或连接数？

计费按流量 GB 计算，不按线程、连接或请求计费，发布的套餐未设线程数限制。实际限制取决于每个目标主机的容忍度和您自身设备性能。如果计划大规模爬取，请先写信给 support@crawlrelay.com，以便我们核查您的目标范围。

### 为什么我增加线程后爬取速度反而变慢？

通常是因为目标网站开始限速：响应时间延长，出现 429 或 503 返回码，重试增加负载。另一常见原因是自己设备 CPU 或连接资源不足，尤其是使用无头浏览器时。增加线程前请先测量每个主机的延迟和错误率。

### 每个请求一个连接会比长连接慢吗？

会的，每次请求增加一个 TCP 和 TLS 握手，大约延迟几百毫秒，尤其是通过住宅代理出口时。这是拥有每次请求新 IP 的代价。在 IP 多样性不重要的情况下，对同一网站的多页请求使用粘性会话和长连接更快且节省流量。

### 在并发爬虫中如何遵守 robots.txt 的 crawl-delay？

每个主机解析 robots.txt 一次，缓存最长 24 小时，符合 RFC 9309 建议。将 `Crawl-delay` 值转换为单线程请求之间的最小间隔。Python 的 `urllib.robotparser` 可通过 `crawl_delay()` 访问该数据。

**按流量 GB 计费实现扩展** US$0.50/GB 从 50 GB 到 1 TB，流量永不过期。 [查看价格](https://crawlrelay.com/zh/pricing/) · [带宽计算器](https://crawlrelay.com/zh/guides/bandwidth-calculator/)

---

CrawlRelay: CrawlRelay 是为开发者设计的住宅代理和 4G/5G 移动代理轮换服务，适用于爬虫和 AI 数据管道：按需付费且流量永不过期，最低流量从 US$0.50/GB 起，支持加密货币支付，无需上传身份证明。无需 KYC —— 无需身份证、自拍和任何证件。支持加密货币支付。
