Solana getProgramAccounts:先变慢,然后被限流

为什么不加过滤的调用是最快被限流的方式、memcmp 和 dataSize 怎么收窄结果,以及什么时候干脆别用它。

BoltTx Team··13 min read
solanagetProgramAccountsrpcmemcmp限流索引

getProgramAccounts 返回某个程序拥有的每一个账户。在一个热门程序上,那可能是几十万个账户,而 RPC 必须把它们全扫一遍才能回答

★它是标准 JSON-RPC 接口里最贵的一个调用,也是最容易让你被限流的那个。★

它实际在做什么

它背后没有索引。 节点会遍历该程序拥有的账户,一边遍历一边套用你的过滤条件。

// ★不要在热门程序上这么干。★
const all = await connection.getProgramAccounts(PROGRAM_ID);

这个调用会传输每一个账户的完整数据。在一个繁忙的 DEX 程序上,那是几十 MB、要好几秒,并且重重地计入你的配额

响应的代价你要付两次 —— 一次是服务商配额,一次是你自己进程里的解析时间。

过滤在服务端执行

两个过滤器承担了主要工作,而且它们可以组合:

const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
  filters: [
    { dataSize: 165 },                       // ★只要正好这个大小的账户★
    {
      memcmp: {
        offset: 32,                          // ★账户数据里的字节偏移★
        bytes: owner.toBase58(),             // 要匹配的值,base58 编码
      },
    },
  ],
});

dataSize 匹配数据长度恰好是这么多字节的账户。同一个程序里不同的账户类型几乎总是大小不同,所以光这一条往往就能排除掉大部分。

memcmp 匹配固定偏移处的原始字节。★这就是你找出"某个钱包拥有的所有代币账户"的方式 —— 代币账户里偏移 32 就是 owner 字段。★

★两个过滤器都在服务商那一侧求值,所以它们同时减少传输量、你的解析成本,通常还有你的计费用量。★

如果你的读取已经调好、问题在交易落不了块,免费领个 BoltTx key 改一行就能测测提交路径。

dataSlice 是那个被忘掉的过滤器

如果你只需要每个账户里的几个字节,就只要那几个字节:

const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
  filters: [{ dataSize: 165 }],
  dataSlice: { offset: 64, length: 8 },     // ★只要 amount 字段★
});

★匹配到一千个账户、每个只需要八字节却传完整数据,传输量白白多出上百倍。★ dataSlice 削减的是响应,不改变哪些账户被匹配。

让这个调用保持可负担的组合,是三个一起用:dataSize 收窄类型,memcmp 收窄集合,dataSlice 收窄载荷。

把偏移量弄对

memcmp 的偏移是账户序列化布局里的字节位置,而弄错它返回的是空结果,不是错误

对一个 SPL 代币账户:

偏移 0   mint          32 字节
偏移 32  ★owner★        32 字节
偏移 64  amount         8 字节
偏移 72  delegate       36 字节
...
        ★共 165 字节★

对一个 Anchor 账户,★开头 8 个字节是 discriminator★,所以每个字段的偏移都要相对结构体定义往后挪 8。忘了这一点,是过滤器静默匹配不到任何东西最常见的原因。

// ★Anchor:先跳过 8 字节 discriminator。★
{ memcmp: { offset: 8, bytes: authority.toBase58() } }

在信任一个过滤器之前,先拿一个已知账户验证。 取一个你确定应该匹配的账户、解码它,确认那个值确实在你以为的位置上

什么时候干脆别用它

getProgramAccounts 回答的是"现在存在什么"。对三类常见需求,它都是错的工具。★

盯变化。 定时轮询它又贵又慢。programSubscribe 会在变化发生时推给你,而且支持同样的过滤选项。

查历史。 它只看得见当前状态。任何和时间相关的需求都要你自己存。

高频查同一批。 如果你要反复拿同一个集合,取一次然后靠订阅维护,而不是反复重扫。

一个对机器人很好用的模式:

// ★一次昂贵的扫描,拿到快照。★
const initial = await connection.getProgramAccounts(PROGRAM_ID, { filters });

// ★之后是廉价的增量更新。★
connection.onProgramAccountChange(
  PROGRAM_ID,
  (info) => updateCache(info),
  "processed",
  filters,
);

★先快照一次,然后转成流。★ 这把一个反复的重调用,变成了一次调用加一个订阅 —— 而这通常就是"装得进限流"和"装不进"之间的差别。

它失败的时候

症状 原因
429 ★扫得太多,或者没加过滤★
超时 结果集太大
空结果 ★偏移错了,常见是漏了 Anchor discriminator★
账户不对 dataSize 把另一种类型也匹配进来了
慢但能用 少了 dataSlice

★"空结果"那一行值得记牢,因为它看起来像"没有账户存在",而不是"你的过滤器写错了"。★ 一个错的偏移和一个真的空集合,返回的都是 []

有些服务商在共享套餐上禁用或限制这个方法,正是因为它的代价如果它在一个端点能用、在另一个不能用,那通常是策略而不是 bug。

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

★读和发是两条独立的路径,各有各的限制。★ 一个在 getProgramAccounts 上被限流的机器人也会发不出交易 —— 不是因为发送被限流了,而是因为它和那些扫描共用同一份被耗尽的配额。

BoltTx 在这里的位置

我们负责提交,不做索引。你读取用什么,继续用什么。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。★因为端点互相独立,繁重的读取流量耗尽不了你交易发出去的那条路径。★

你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

// 读取那边不用动,只换发送端点。
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

getProgramAccounts 是做什么的? 返回某个程序拥有的每一个账户,可以带过滤。它背后没有索引,节点靠扫描该程序的账户来回答 —— 这就是它贵的原因。

getProgramAccounts 为什么这么慢? 因为它是扫描而不是查找。在热门程序上那是几十万个账户,而且不加过滤时还会把它们的数据全传过来。

memcmp 过滤器怎么工作? 它匹配账户数据里固定偏移处的原始字节。代币账户偏移 32 是 owner,所以在那里做 memcmp 就能找出属于某个钱包的全部代币账户。

我的 memcmp 过滤器为什么什么都返回不了? 通常是偏移错了。Anchor 账户开头 8 字节是 discriminator,所以每个字段都要相对结构体定义挪 8。偏移错了返回的是空数组,不是错误。

dataSize 是干什么用的? 匹配数据长度恰好是这么多字节的账户。同一程序里不同账户类型通常大小不同,所以它往往是排除掉大部分的最便宜方式。

怎么减小响应体积?dataSlice 只请求你需要的那几个字节。匹配一千个账户、每个只要八字节却传完整数据,是一笔巨大且可以避免的浪费。

getProgramAccounts 为什么会被限流? 因为每次调用都很重,服务商也按这个定价。在服务端过滤、加上 dataSlice,并把反复扫描换成"一次快照 + 一个订阅"。

getProgramAccounts 适合用来盯变化吗? 不适合。它回答的是"现在存在什么"。programSubscribe 配同样的过滤器,在变化发生时接收推送,而不是定时重扫。

能用 getProgramAccounts 查历史状态吗? 不能,它只看得见当前状态。任何和时间相关的需求都要你自己保存快照,因为这个方法没有"过去某个 slot"的概念。

为什么它在一家 RPC 能用、另一家不能? 很多服务商因为它的代价,在共享套餐上限制或禁用它。同样的调用在别处能成功,通常是策略差异而不是 bug。

Anchor 的 discriminator 偏移是什么? 每个 Anchor 账户开头的 8 个字节,用来标识账户类型。针对 Anchor 数据的过滤器,要在你按结构体算出的偏移上加 8。

怎么找到过滤器该用的偏移? 从账户布局来 —— 程序的 IDL 或者它文档里的结构体。信任这个过滤器之前,先取一个已知账户确认那个值就在你以为的位置。

能同时用多个过滤器吗? 能,而且一般都该。dataSizememcmp 可以组合,每多一个过滤器都会同时减少扫描结果和传输数据。

需要当前状态的机器人,最佳模式是什么? 先用带过滤的 getProgramAccounts 快照一次,然后用 onProgramAccountChange 保持它最新。这把反复的重扫换成了一次调用加一条流。

getProgramAccounts 和发送交易共用配额吗? 在共享端点上通常是,这就是繁重读取会饿死你的发送的原因。把读路径和发送路径分开,就完全避开了这种相互影响。

getProgramAccounts 最多能返回多少账户? 没有固定上限 —— 而这正是问题所在:大结果集会超时或被服务商截断。过滤到结果在设计上就是有界的。

延伸阅读

返回博客列表