摘要
来自 Solana JSON-RPC 端点的 429 意味着提供方正在限制你的请求速率,而不是你的 JSON 负载格式错误。Python 客户端最常遇到这种情况是在紧密循环中轮询、批量请求过于激进,或者多个工作进程共享一个公共端点时。解决方法通常是结合客户端退避、请求整形,以及迁移到容量与你的工作负载匹配的端点。本文展示如何读取错误、在 Python 中应用重试和抖动、减少请求量,并决定何时共享或专用端点是正确的下一步。
来自 Solana JSON-RPC 端点的 429 响应是提供方的速率限制信号,而不是你的 JSON 正文的问题。服务器理解了你的请求,但因为你超出了与 API 密钥、IP 或套餐相关的允许量而选择拒绝。在 Python 中,这通常表现为 requests.exceptions.HTTPError: 429 Client Error: Too Many Requests,或在某些提供方上表现为带有代码 -32429 的 JSON-RPC 错误对象。实际问题不是“429 是什么意思”,而是“接下来我应该改变什么”——退避、请求整形,还是更换端点。
首先诊断:是速率限制、错误的 JSON,还是端点失效?
在重写代码之前,确认你实际遇到的是什么。这三种故障在日志中看起来相似,但需要不同的修复方法。
| Python 中的症状 | 可能原因 | 首先检查什么 |
|---|---|---|
HTTP 429 带有 Retry-After 或速率限制头 | 提供方限流 | 每秒请求数和突发大小 |
JSON-RPC 错误 -32600 / -32700 | JSON 格式错误或批量请求错误 | json.dumps 输出和内容类型 |
| 超时、连接重置、5xx | 端点或网络问题 | 端点 URL、DNS、提供方状态 |
| 仅在并发下出现 429 | 跨工作进程共享密钥或 IP | 有多少进程共享一个端点 |
突发 getProgramAccounts 后出现 429 | 方法成本高 | 方法组合,而不仅仅是请求数 |
使用 curl 快速探测可以将端点与你的 Python 代码隔离开来:
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://solana.api.onfinality.io/public \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'
如果单个请求返回 200 但你的脚本返回 429,那么问题在于你的请求模式,而不是端点。如果即使单个请求也返回 429,那么你与其他流量共享了一个被限流的密钥或 IP。
正确读取 429 响应体
许多 Solana 提供方返回 JSON-RPC 错误对象,而不是普通的 HTTP 错误。在 Python 中解析这两层,以免吞掉真正的原因。
import requests
ENDPOINT = "https://solana.api.onfinality.io/public"
payload = {"jsonrpc": "2.0", "id": 1, "method": "getSlot"}
resp = requests.post(ENDPOINT, json=payload, timeout=10)
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After")
print("Throttled. Retry-After:", retry_after)
print("Body:", resp.text[:300])
else:
data = resp.json()
if "error" in data:
print("JSON-RPC error:", data["error"])
else:
print("Slot:", data["result"])
记录状态码、Retry-After 头(如果存在)以及 JSON-RPC 错误代码。这个组合告诉你限制是按秒、按分钟还是按方法权重计算的。
修复 1:Python 中带抖动的指数退避
立即重试会使限流更糟。使用带抖动的指数退避,并限制尝试次数,以便持续 429 时大声失败而不是挂起。
import random
import time
import requests
ENDPOINT = "https://solana.api.onfinality.io/public"
def rpc_call(method, params=None, max_attempts=5):
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}
for attempt in range(max_attempts):
resp = requests.post(ENDPOINT, json=payload, timeout=10)
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After")
wait = float(retry_after) if retry_after else min(2 ** attempt, 30)
wait += random.uniform(0, 0.5) # jitter
time.sleep(wait)
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]
raise RuntimeError("Exceeded retry attempts after repeated 429s")
当许多工作进程同时重试时,抖动很重要。没有它,你的进程会同步并重新触发相同的限制。
修复 2:在更换提供方之前减少请求量
限流通常是不高效轮询的症状。在升级基础设施之前,检查是否可以发送更少、更好的请求。
- 批量处理独立调用。 Solana JSON-RPC 接受请求数组。批量处理彼此不依赖的
getSlot、getBalance和getAccountInfo调用。保持批量适中——非常大的批量可能被拒绝或超时。 - 使用 WebSocket 订阅而不是轮询。
slotSubscribe、accountSubscribe和logsSubscribe通过wss://solana.api.onfinality.io/public-ws推送更新,消除了紧密轮询循环的需要。 - 缓存稳定数据。 代币元数据、程序 ID 和 epoch 信息变化缓慢。缓存它们,而不是每个请求重新获取。
- 避免无限制的
getProgramAccounts。 这些开销很大。使用dataSlice和filters进行过滤,以便节点每次调用做更少的工作。 - 错开启动突发。 在启动时触发数百次调用的服务在速率限制器看来像攻击。添加小的爬坡延迟。
以下是批量调用模式:
batch = [
{"jsonrpc": "2.0", "id": 1, "method": "getSlot"},
{"jsonrpc": "2.0", "id": 2, "method": "getVersion"},
{"jsonrpc": "2.0", "id": 3, "method": "getHealth"},
]
resp = requests.post(ENDPOINT, json=batch, timeout=10)
for item in resp.json():
print(item.get("id"), item.get("result") or item.get("error"))
修复 3:分离密钥和工作进程
如果多个服务、容器或 CI 作业共享一个 API 密钥或一个公共 IP,它们会竞争相同的允许量。在提供方支持的情况下,为每个工作负载分配自己的密钥,并避免针对与生产流量相同的端点运行测试套件。一个失控的循环可能会限制同一密钥后面的所有内容。
何时离开共享公共端点
客户端修复有局限性。如果你已经批处理、缓存和退避,但在正常流量期间仍然遇到 429,那么端点的容量不再匹配你的工作负载。此时,决策是关于端点类型,而不是代码。
| 情况 | 共享公共端点 | 专用节点 |
|---|---|---|
| 原型设计、低请求量 | 通常可以 | 过度 |
| 具有突发的稳定生产流量 | 负载下可能限流 | 可预测的容量 |
大量 getProgramAccounts 或归档查询 | 经常被限流 | 更适合 |
| 严格的延迟或隔离需求 | 共享资源 | 隔离资源 |
| 多个团队使用一个密钥 | 争用风险 | 更清晰的分离 |
OnFinality 通过共享 RPC API 服务 和 专用节点 提供 Solana RPC,当你需要隔离容量时。你可以查看 Solana 网络页面 了解支持的传输和端点详情,并在 RPC 定价 上比较套餐。如果你仍在评估选项,提供方选择指南 涵盖了生产工作负载重要的标准。
一个最小的生产就绪 Python 客户端
结合退避、批处理和带有连接池的会话。重用会话,这样你不会每次调用都打开新的 TCP 连接。
import random
import time
import requests
class SolanaRPC:
def __init__(self, endpoint, max_attempts=5):
self.endpoint = endpoint
self.max_attempts = max_attempts
self.session = requests.Session()
def call(self, method, params=None):
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}
for attempt in range(self.max_attempts):
resp = self.session.post(self.endpoint, json=payload, timeout=10)
if resp.status_code == 429:
wait = float(resp.headers.get("Retry-After", min(2 ** attempt, 30)))
time.sleep(wait + random.uniform(0, 0.5))
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]
raise RuntimeError("Repeated 429 responses")
rpc = SolanaRPC("https://solana.api.onfinality.io/public")
print(rpc.call("getSlot"))
监控,让 429 不会让你意外
将限流率作为一等指标跟踪,而不仅仅是错误计数。有用的信号:
- 每分钟 429 响应数,按方法拆分。
- 每次成功调用的重试次数。
- 最重方法的 p95 和 p99 延迟。
- 如果使用订阅,WebSocket 重连频率。
当正常流量期间 429 上升时发出警报,而不仅仅是在已知峰值期间。缓慢上升的趋势通常意味着你的工作负载增长超过了端点的允许量。
关键要点
- Solana 429 是提供方速率限制信号,不是 JSON 格式错误。
- 在 Python 中解析 HTTP 状态和 JSON-RPC 错误体。
- 使用带抖动的指数退避和重试上限。
- 通过批处理、WebSocket 订阅、缓存和过滤的
getProgramAccounts减少请求量。 - 分离 API 密钥和工作进程,这样一个循环不会限制所有内容。
- 如果客户端修复后限流仍然存在,评估与你的工作负载匹配的共享或专用端点。
常见问题
429 意味着我的 JSON 无效吗?
不。无效 JSON 通常返回解析错误,如 -32700。429 意味着请求被理解但由于速率限制被拒绝。
重试前应该等待多久?
如果存在 Retry-After 头,请遵守它。否则从短延迟开始,指数增加,上限为合理的最大值,并带有随机抖动。
批处理会使 429 更糟吗?
大批量可能被计为多个请求或超过负载限制。保持批量适中,并将重方法拆分为单独的调用。
一看到 429 就应该切换提供方吗?
不。首先确认不是客户端模式问题。如果在正常、优化的流量下限流仍然存在,那么比较端点容量并考虑专用节点。
WebSocket 订阅能避免 429 吗?
它们减少轮询,从而降低请求量,但它们有自己的连接限制。将它们与退避一起使用,而不是替代退避。