---
title: "我们如何重构历史日志以提升速度"
description: "我们重构了 eth_getLogs 背后的存储和查询引擎，使大范围历史日志读取在所有地区的 p99 延迟提升超过 2 倍。"
---

# 我们如何重构历史日志以提升速度

<ImageBlock
  src="https://media.alchemy.com/blog/how-we-rebuilt-historical-logs-for-speed-16x9-20260821.png"
  alt="我们如何重建历史日志以提升速度"
  width={1920}
  height={1080}
  priority
/>

问任何一个在构建索引器、DEX 聚合器或链上监控系统的团队,他们的应用在哪里变慢,答案几乎总是一样的:大范围的 `eth_getLogs`。

这是那种要扫描成千上万甚至几十万个区块才能回答一个问题的调用。上个月哪些转账命中了这个合约?哪些池子在这个范围内改变了状态?自上线以来这个地址触发了哪些事件?这些读取是真实应用的骨架,也是最容易卡住的读取。于是我们重建了支撑它们的引擎,让这个方法在每个地区的 p99 延迟都提升了 2 倍以上。以下是我们怎么做的。

## eth_getLogs 是什么,为什么重要?

`eth_getLogs` 是一个 JSON-RPC 方法,返回智能合约在运行时发出的事件:一次代币转账、一次兑换、一次授权,或者合约定义的任何自定义事件。你可以按合约地址、按事件签名或索引的 topic 值(比如 `Transfer` 或 `Approval`)筛选,并用 `fromBlock` 和 `toBlock` 指定区块范围。说得直白点:告诉它要找什么、往回找多远,它就返回所有匹配的事件。

这是 Ethereum 上事件驱动索引的基础。没有它,你就得逐笔扫描交易来找出你关心的那些。有了它,你才能构建活动和转账 feed、授权监控,以及任何实时或历史事件索引器。这是我们提供的使用最频繁、对运行至关重要的方法之一。如果你对 logs 和 topics 还不熟悉,我们的[《深入理解 `eth_getLogs`》](https://www.alchemy.com/docs/reference/eth-getlogs)通过一个完整的示例讲解了请求和响应的结构。

这个方法的其余部分决定了工程上的难点。很多提供商会限制单次查询能覆盖的区块数。索引器必须等待确认来处理链重组。topic 过滤器只匹配索引字段,非索引参数需要从原始数据中解码。而靠近链头的调用对延迟很敏感,路由到错误的节点类型可能导致报错或空响应。

对延迟影响最大的因素是范围大小。查询一个区块几乎是瞬时的。但对于要回放历史的索引器,或是要监控合约数周活动的监控系统,范围很快就会变得很宽,查询的开销也随之攀升。这时延迟就不再是可以忽略的误差,而是决定客户能以多快速度构建应用的瓶颈。

所有人都提供这个方法。但谈论其底层引擎的人不多,因为重建这个引擎代价高昂,大多数提供商都没有做过这件事。

## 为什么 p99 延迟比平均延迟更重要?

真正伤人的数字不是平均值,而是 p99:把所有请求按速度从快到慢排序,p99 就是那最慢的百分之一里的最慢一个。平均值会把它掩盖掉。你可以让 99 个请求都在 60 毫秒内完成,却眼看着第一百个请求耗时整整一秒。

那个最慢的请求,正是让仪表盘卡住、让索引器落后于链的那个,也正是你的用户在盯着看的那个。平均延迟可能看起来没问题,尾部延迟却在悄悄拖垮整个工作负载。对于运行索引器和监控系统的团队来说,尾部延迟就是工作负载本身。

## 我们是如何优化 eth_getLogs 的?

我们重建了支撑这个方法的存储和查询引擎,而不是对原有的引擎做调优。

要让这样一个调用变快,有几种常规做法:调整查询参数、加缓存、限制可接受的范围,或者堆更多同类硬件。这些做法大多只能换来一点点余量,而不会改变根本问题。

我们做得更深入。我们没有依赖一个把历史日志读取当成普通工作负载来处理的通用托管数据库,而是自建并运营了一个专门针对 `getLogs` 访问模式调优的存储和查询层:在大规模数据集上进行快速的过滤扫描,运行在我们自己掌控的专用基础设施上。

我们没有去调一个参数或者在旧系统上加个缓存了事。我们在方法这一层级上投入,重建了支撑客户最关键调用之一的底层引擎。这是昂贵的、按方法级别投入的基础设施工作,也正因如此,改进体现在尾部延迟上,而不只是平均值上。

## 结果如何?

结论很简单:在每个地区,p99 延迟都提升了 2 倍以上。

如果你运行的是索引器或监控系统,p99 就是拖垮你工作负载的那个延迟指标。更好的平均值固然不错,但更好的尾部延迟才能让仪表盘保持响应、让索引器跟得上链的进度。

在大规模场景下运行这套系统的团队已经感受到了变化。一家全球最大的加密货币交易所之一,在我们的引擎和自建节点之间对比测试了历史日志读取,并在自己的工作负载上测得了显著的 p99 提升。这是在生产环境中、基于客户自身埋点测得的证据,不只是我们仪表盘上的数字。

## 这对你意味着什么?

如果你的应用依赖历史日志读取来做索引、聚合或监控,你会获得更快的读取速度,以及在每个地区都显著改善的 p99 尾部延迟——这一切都建立在我们专门为这个方法重建的基础设施之上。以下是你在使用时值得了解的几点:

1. 在按量付费和企业版计划中,`eth_getLogs` 在 Ethereum、Base、Optimism、Arbitrum、Polygon、Robinhood 及其他主流链上支持单次查询无限制区块范围,回放历史数据无需手写分页逻辑。
2. 对于链头附近的日志,[WebSocket 订阅](https://www.alchemy.com/docs/reference/subscription-api)会在新日志产生时主动推送给你,这样你就可以把 `eth_getLogs` 留给它本来擅长的历史读取场景。
3. 你的集成方式无需任何改动。方法还是同一个,请求结构也不变,只是背后的引擎更快了。

在方法这一层级投入的意义就在于:定义你工作负载的那些读取,正是我们调优引擎所针对的那些读取。[创建一个免费的 Alchemy 账户](https://dashboard.alchemy.com/signup),把你的索引器指向我们,在你自己的埋点上感受这个差异。

## 常见问题

### eth_getLogs 返回什么?

一个符合你筛选条件的事件日志数组:按合约地址、事件签名和索引的 topic 值,以及区块范围筛选。每条日志都包含发出该事件的地址、topics、数据负载,以及它所在的区块和交易。完整的请求和响应规范见 [API 参考文档](https://www.alchemy.com/docs/reference/eth-getlogs)。

### 为什么 p99 延迟对索引器来说比平均延迟更重要?

因为索引器是按顺序或批量处理请求的,最慢的那些请求决定了整体节奏。平均值低但尾部长,意味着索引器会经常卡在那最慢的百分之一上,落后于链头,并提供过时的数据。p99 衡量的正是这些请求。

### 在 Alchemy 上,单次 eth_getLogs 调用可以查询多大的区块范围?

在免费套餐中,单次查询限制为 10 个区块。在按量付费和企业版计划中,Ethereum、Base、Optimism、Arbitrum、Polygon、Worldchain、BNB 及其他多条链上的区块范围没有限制,响应上限为 150MB。完整的分链对照表见 [`eth_getLogs` 参考文档](https://www.alchemy.com/docs/reference/eth-getlogs)。

### 如何让大范围的历史查询保持快速?

尽可能收紧过滤条件:指定你需要的合约地址和事件 topics,而不只是区块范围。对于不支持无限区块范围的链上的超大范围查询,把查询拆分成更小的区块。对于向前的新日志,使用 [WebSocket 订阅](https://www.alchemy.com/docs/reference/subscription-api),而不是轮询。
