web3-infra

本机 RPC 正常,另一台 EC2 访问超时

一次 Erigon 私有 JSON-RPC 超时排查:把客户端错误、节点状态、端口监听和 security group 分开确认。

Jun 23, 2026
AWSSecurity GroupsErigonJSON-RPCtroubleshooting

Indexer 重试 Ethereum JSON-RPC 失败后退出。 第一眼看像节点问题,因为失败发生在通过 HTTP 调 eth_getLogs 的地方。

rpc error after retry, exiting
Post "http://<node-private-ip>:8545": dial tcp <node-private-ip>:8545: i/o timeout

节点本身已经同步完成。 eth_syncing 返回 false,Erigon 日志也持续出现新的 head validated,block age 很低。 所以问题不在 Erigon 追块链路里,而在两台 EC2 之间的访问路径上。

用语说明

用语含义
JSON-RPC客户端查询链上数据的 HTTP API。Erigon 常见端口是 TCP 8545
Security groupAWS 绑定在实例网卡上的防火墙规则,控制入站和出站流量。
Source security group作为入站来源的安全组。带有这个安全组的网卡发来的流量会被匹配。
Socket binding进程监听的地址,例如 127.0.0.1:8545 只允许本机访问,0.0.0.0:8545 表示所有网卡。
Timeout客户端发起请求后没有及时得到响应。EC2 内网访问里,它经常指向过滤或路由问题,而不是应用直接拒绝。

先看错误本身

Indexer 日志里有两个关键信息:

  • 目标是节点内网地址的 8545
  • 错误是 i/o timeout

timeoutconnection refused 不一样。 connection refused 通常说明包到了主机,但端口没人监听。 i/o timeout 通常说明客户端没有收到可用响应。 常见位置是 security group、network ACL、route、主机防火墙,或者进程只监听了 loopback,但客户端访问的是内网地址。

这次用到的命令

下面这些命令构成了排查主线。 里面的标识都换成了占位符。

先找节点机器和调用方机器:

aws ec2 describe-instances \
  --profile <aws-profile> \
  --region <region> \
  --instance-ids <node-instance-id> \
  --query 'Reservations[].Instances[].{InstanceId:InstanceId,Name:Tags[?Key==`Name`]|[0].Value,PrivateIp:PrivateIpAddress,SecurityGroups:SecurityGroups[].GroupId,Subnet:SubnetId,Vpc:VpcId}' \
  --output json

aws ec2 describe-instances \
  --profile <aws-profile> \
  --region <region> \
  --filters 'Name=private-ip-address,Values=<caller-private-ip>' \
  --query 'Reservations[].Instances[].{InstanceId:InstanceId,Name:Tags[?Key==`Name`]|[0].Value,State:State.Name,PrivateIp:PrivateIpAddress,SecurityGroups:SecurityGroups[].GroupId,Subnet:SubnetId,Vpc:VpcId}' \
  --output json

读取两侧 security group:

aws ec2 describe-security-groups \
  --profile <aws-profile> \
  --region <region> \
  --group-ids <node-sg-id> <caller-sg-id> \
  --query 'SecurityGroups[].{GroupId:GroupId,Name:GroupName,Ingress:IpPermissions,Egress:IpPermissionsEgress}' \
  --output json

通过 SSM 在节点机器上做只读检查。 wrapper 命令负责下发远程 shell 命令,并返回 command ID:

aws ssm send-command \
  --profile <aws-profile> \
  --region <region> \
  --instance-ids <node-instance-id> \
  --document-name AWS-RunShellScript \
  --comment 'read-only rpc listen check' \
  --parameters commands='<json-array-of-shell-commands>'

aws ssm get-command-invocation \
  --profile <aws-profile> \
  --region <region> \
  --command-id <command-id> \
  --instance-id <node-instance-id>

节点机器上实际跑的是这些 shell 命令:

ss -lntp | egrep ':8545|:8546|:8551' || true

curl -sS -m 3 \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://127.0.0.1:8545

docker ps --format 'table {{.Names}}\t{{.Ports}}'

调用方机器也用同样的 SSM wrapper 下发。 实际跑的是这些 shell 命令:

timeout 5 bash -lc '</dev/tcp/<node-private-ip>/8545' \
  && echo tcp_ok || echo tcp_failed

curl -sS -m 5 \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://<node-private-ip>:8545

确认问题在 security group 后,只加一条收口的入站规则:

aws ec2 authorize-security-group-ingress \
  --profile <aws-profile> \
  --region <region> \
  --group-id <node-sg-id> \
  --ip-permissions 'IpProtocol=tcp,FromPort=8545,ToPort=8545,UserIdGroupPairs=[{GroupId=<caller-sg-id>,Description="JSON-RPC from application server"}]'

最后重新跑调用方检查,预期是 tcp_ok,并且 eth_blockNumber 正常返回。

改网络前先确认节点

第一步是在节点机器本机调用 RPC。

curl -sS -m 3 \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://127.0.0.1:8545

节点返回了区块高度。 eth_syncing 返回 false。 日志也正常:

head validated ... age=2s
Timings: Forkchoice Commit ... commit=1s

这一步把链同步问题排除掉。 节点进程活着,本机 RPC 能返回,Erigon 正在跟随最新链头。

看端口监听在哪个地址

本机能访问,不代表另一台机器也能访问。 进程可能只监听 127.0.0.1

ss -lntp | egrep ':8545|:8546|:8551'
docker ps --format 'table {{.Names}}\t{{.Ports}}'

需要看的信号是:

0.0.0.0:8545
[::]:8545

这表示容器已经把 RPC 端口发布到主机所有网卡。 如果这里只看到 127.0.0.1:8545,就要改节点或容器的 bind 设置。 这次端口监听没有问题。

从调用方机器验证

调用方机器仍然打不开 TCP 连接:

timeout 5 bash -lc '</dev/tcp/<node-private-ip>/8545' \
  && echo tcp_ok || echo tcp_failed

返回:

tcp_failed

同一台机器发 JSON-RPC 也超时:

curl -sS -m 5 \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
  http://<node-private-ip>:8545

这时链路形状已经很清楚:

检查项返回
节点本机 RPC正常
节点端口监听0.0.0.0:8545
调用方到节点 TCP超时
调用方到节点 JSON-RPC超时

问题在两台机器之间。

把 security group 当成数据路径读

两台实例在同一个 VPC 和 subnet。 调用方有出站访问。 节点的 security group 放行了 P2P 和 metrics,但没有放行来自调用方的 JSON-RPC。

这是私有区块链节点里很常见的拆分。 P2P 可以对公网开放,metrics 只给监控机,JSON-RPC 应该只给真正需要调用的服务。 正确的改动不是复用全开放安全组,也不是把 8545 发到公网。 只需要一条入站规则:

node security group
TCP 8545
source: application server security group
description: JSON-RPC from application server

用 source security group 比写死内网 IP 更稳。 应用服务器替换以后,只要新网卡仍然挂同一个安全组,这条规则就继续生效。

从调用方复测

规则加完后,同样的调用方检查变成:

tcp_ok

RPC 也返回正常:

{"jsonrpc":"2.0","id":1,"result":"0x..."}
{"jsonrpc":"2.0","id":2,"result":false}

第一个响应是当前区块高度。 第二个响应是 eth_syncing=false。 这说明调用方已经能访问节点,节点也已经在链头附近。

这次顺序为什么有用

这种超时很容易把人带到 Erigon 日志、磁盘 IO、snapshot 构建这些方向。 这些检查有价值,但它们回答的是另一类问题。

更短的顺序是:

  1. 确认 RPC 错误和目标地址。
  2. 确认节点本机 RPC。
  3. 确认端口不是只监听 loopback。
  4. 确认调用方打不开 TCP。
  5. 对照 security group 和预期数据路径。
  6. 增加一条收口的入站规则。
  7. 用同样的 TCP 和 RPC 命令复测。

边界很简单:私有 RPC 只应该被需要它的机器访问,其它地方不应该访问。 本机 RPC 正常、远端 RPC 超时时,先看网络路径,再动节点进程。