Gemini Web 反代并发与带宽容量上限 (Gemini Web2API Relay Concurrency & Bandwidth Capacity Limits)
遭遇场景
对 ai.6767.chat 做本机压测,想知道「Gemini web 并发上限」和「服务器带宽对吞吐的影响,尤其是大 prompt payload」。测试发生在 2026-09-17,服务端为 tx 上的 gemini-web2api-go。
一句话结论:瓶颈是配置里的并发配额,不是带宽;带宽在 128KB prompt 上限处就已经被封顶,永远不可能成为主因。
请求链路 (Request Path)
客户端
│ HTTPS /v1/chat/completions
▼
nginx (tx, ai.6767.chat, client_max_body_size 50m)
│ proxy_pass http://127.0.0.1:3000
▼
New API (:3000, Docker) ← 鉴权 / 计费 / 渠道路由
│ 渠道转发
▼
gemini-web2api-go (:8083, systemd) ← 并发与配额限制真正所在
│ HTTP CONNECT,模拟 Chrome 146 TLS 指纹
▼
Mihomo (:7890) → gemini.google.com排查时必须确认限制落在哪一跳。本例中 429 是第 4 跳自己拒绝的,根本没出公网。
限制清单(实测值)
| 限制项 | 位置 | 原值 | 说明 |
|---|---|---|---|
per_ip_concurrent | gemini-web2api-go | 5 | 超出立刻 429,不排队;最关键 |
per_ip_rpm | 同上 | 30 | 每分钟请求数 |
per_ip_rph | 同上 | 80 | 每小时请求数,Google 封号护栏 |
max_prompt_bytes | 同上 | 128000 | UTF-8 字节;超出 400,不发上游 |
client_max_body_size | nginx | 50m | 远不是瓶颈 |
| 上游代理池 | 同上 | 1 个 | 扩容手段是加代理 |
自省方法:先问服务,再压测 (Introspect Before You Load-Test)
不要靠压测反推限制。这类反代通常自带管理 API,直接读比试出来快得多,而且不会白白消耗配额。
A="Authorization: Bearer <admin_token>"
curl -s -H "$A" http://127.0.0.1:8083/admin/api/config # 全部配置项
curl -s -H "$A" http://127.0.0.1:8083/admin/api/usage # 实时 inflight / limit_* / rph / rpm
curl -s -H "$A" http://127.0.0.1:8083/admin/api/stats # 聚合统计 + error_breakdown
curl -s -H "$A" http://127.0.0.1:8083/admin/api/proxies # 代理池/admin/api/usage 会直接给出答案:
{"inflight":0,"limit_concurrent":5,"limit_rph":80,"limit_rpm":30,"name":"启动参数","rph":21,"rpm":0}当文档缺失时,从二进制里挖路由(Go 单二进制同样有效):
strings ./gemini-web2api-go | grep -o "/admin/api/[a-z_/]*" | sort -u这一招在本例中直接挖出了 /admin/api/config、/admin/api/usage 和 /admin/api/cookies,比翻源码快。同理 strings ... | grep -i "json:\"" 可以还原结构体字段名。
428/429 文案会骗人
实测错误:
{"error":{"message":"all proxy slots full: rph limit reached","code":"ip_slot_full"}}文案说的是 rph(每小时配额),但当时 rph=21 / limit_rph=80,根本没到。真正触发的是并发槽位(limit_concurrent=5)。
判据是数字,不是文案:c=6 恰好挂 1 个、c=8 恰好挂 3 个、c=16(改后)恰好挂 4 个(16-12)。拒绝数 = 并发 - limit_concurrent,误差为 0 —— 这才是并发限制的指纹。
带宽为什么不是瓶颈
带宽是不对称的
| 方向 | 实测 | 说明 |
|---|---|---|
| tx 入口(下行) | 16.0 MB/s | 响应回程 |
| tx 出口(上行) | 0.375 MB/s ≈ 3 Mbps | prompt 恰恰走这一侧 |
直连公网与走 Mihomo 代理测出来都是 290–380 KB/s → 说明是云实例的带宽上限,不是代理的问题。腾讯云轻量这类套餐 3 Mbps 出口很常见。
但绝对量太小,够不着
- 一次满额 128KB prompt 走慢速侧只需 0.34 s;实测 upload 阶段 0.13–0.18 s。
- 对比 TTFT 量级 5 s,占比不到 7%。
对照实验:c=4 时
- 1KB prompt → 聚合 225.2 tok/s
- 真实 ~122KB 代码文件 → 聚合 212.7 tok/s
- 100 倍 payload 只掉 5.5%。
结论:只有当「并发 × 大 payload」同时拉满时出口才显露 —— N 个满额 128KB 需要 N×0.34s 串行。12 并发满额 = 4s,才勉强能和 TTFT 同量级。优化方向是限制 payload,不是加带宽。
测量方法:把上传和首 token 分开 (Separating Upload from TTFT)
压测这类流式接口时,总耗时 无法区分「我传得慢」和「上游想得慢」。做法是把请求体做成异步可迭代对象,在最后一个字节交给传输层的瞬间打点:
class BodyStream:
def __init__(self, data, chunk=32768):
self.data, self.chunk, self.t_done = data, chunk, None
async def __aiter__(self):
mv = memoryview(self.data)
for i in range(0, len(mv), self.chunk):
yield bytes(mv[i:i+self.chunk])
self.t_done = time.perf_counter() # 上传完成时刻配 httpx 使用:client.stream("POST", url, content=BodyStream(body))。于是可分离出:
upload_s= 上传完成 − 请求开始 → 带宽指标ttft_s= 首个内容增量 − 上传完成 → 上游延迟指标tps= token 数 / 解码窗口 → 生成速度指标
坑:httpx.AsyncClient 只接受异步可迭代体。写成同步 __iter__ 会报 Attempted to send an sync request with an AsyncClient instance。
另一个硬限制:prompt 长度按 UTF-8 字节算
超过 128,000 字节直接 400,且不发上游:
prompt is 134647 bytes, over the 128000-byte per-request limit of the
Gemini web protocol (the limit is on UTF-8 bytes, not tokens).- 实测 65.6KB 通过;261.7KB / 1MB / 4MB 全部 400。
- 中文注释的代码约 1.22 字节/字符(110000 字符 = 134,647 字节),比纯 ASCII 早约 22% 触顶。用中文注释的项目在估算 payload 时要按这个系数算。
- 该限制是 relay 主动加的护栏,因为 Gemini web 上游会静默从尾部截断,导致丢掉最新一条消息、答非所问。所以不要为了塞长文去调大它。
流式行为:不是逐 token
2688 字符的回答只产生 21–27 个 chunk(约 100–130 字符/chunk),部分请求整个回答一次性到达(TTFT ≈ total,仅 5 个 chunk)。
原因是 Gemini web 用的是累积式文本协议(每次都回全量),relay 再做前缀 diff 抽出增量。因此客户端感知是「等 4–6 秒,然后一次性刷出来」,而不是顺滑逐字。排查「流式卡顿」时不要误判为网络问题。
症状 → 根因 对照表
| 症状 | 根因 | 判据 |
|---|---|---|
| 并发一高就 429,且拒绝数固定 | per_ip_concurrent | 拒绝数 = 并发 − limit |
| 429 文案说 rph,但 rph 没到 | 文案 bug,实为并发槽位 | 查 /admin/api/usage |
| 大 prompt 直接 400 | max_prompt_bytes(UTF-8 字节) | 报错里直接写字节数 |
| TTFT 高且抖动大(3.6–14s) | 账号 cookie 失效 → 匿名回退 | /admin/api/cookies 看 status |
| 回答一次性刷出 | 累积式协议 + 前缀 diff | chunk 数远小于字符数 |
| 上游偶发失败后长时间不可用 | proxy_cooldown_min(120 分钟) | 代理池冷却 |
账号状态常被忽略
本例的重大发现:
{"label":"lokaqipuxuf242@gmail.com (Sharice)","status":"disabled","enabled":0,
"fail_count":3,"last_error":"no SNlM0e in page (cookie expired or not signed in)"}服务重启日志直接写 Cookie: none (anonymous)。所有请求都在匿名回退上跑,这才是 TTFT 偏高且抖动的真正来源。调并发之前应该先修 cookie —— 有效账号的延迟更低、限额更宽。
变更与回滚
配置:/home/ubuntu/workspace/host/gemini-web2api-go/config.json,启动时读取,改完必须重启。
cp config.json config.json.bak.$(date +%Y%m%d-%H%M%S)
# 改 per_ip_concurrent / per_ip_rpm / per_ip_rph
sudo systemctl restart gemini-web2api-go.service
curl -s -H "Authorization: Bearer <admin_token>" http://127.0.0.1:8083/admin/api/config # 验证本次调整:并发 5 → 12、rpm 30 → 60、rph 80 → 150。
效果:聚合吞吐 225.2 → 484.8 tok/s(2.15x),12 并发 12/12 成功、TTFT 5.69s。
注意 per_ip_rph 是Google 封号护栏,往上调要确认不会触发上游风控;per_ip_concurrent 才是纯粹的本地闸门,是安全的首选调节项。
对照:另一条上游完全不是一套限制
同一入口下的 gpt-5.6-luna(走的是别的渠道,非 gemini-web2api):
| 并发 | 结果 |
|---|---|
| 4 / 8 / 16 | 全部 100% 成功,吞吐近似线性(17.8 → 31.2 → 67.2 tok/s) |
context 能吃 273,561 token(523KB 输入),2MB 才因 context_too_large 拒绝。做容量规划时必须按模型/渠道分别测,不能用一个模型的结论套另一个。
相关笔记
- OpenAI 兼容反代的协议兼容与语义保真 (Protocol Compatibility and Semantic Fidelity)
- New API 本地安装 (Windows)
- LLM 调用日志可观测性 (LLM Invocation Observability)
- Gemini 网页端 Veo 视频生成逆向与 MCP 封装 (Gemini Veo Video Generation Reverse Engineering and MCP Tooling)
证据
- 压测报告:
C:\Users\18086\Documents\Codex\2026-09-17\ya-c\outputs\ai-6767-loadtest-report.md - 探针脚本:
C:\Users\18086\Documents\Codex\2026-09-17\ya-c\work\probe.py - 配置备份:
tx:/home/ubuntu/workspace/host/gemini-web2api-go/config.json.bak.20260917-211028
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps