web3-infra
本机 RPC 正常,另一台 EC2 访问超时
一次 Erigon 私有 JSON-RPC 超时排查:把客户端错误、节点状态、端口监听和 security group 分开确认。
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 group | AWS 绑定在实例网卡上的防火墙规则,控制入站和出站流量。 |
| Source security group | 作为入站来源的安全组。带有这个安全组的网卡发来的流量会被匹配。 |
| Socket binding | 进程监听的地址,例如 127.0.0.1:8545 只允许本机访问,0.0.0.0:8545 表示所有网卡。 |
| Timeout | 客户端发起请求后没有及时得到响应。EC2 内网访问里,它经常指向过滤或路由问题,而不是应用直接拒绝。 |
先看错误本身
Indexer 日志里有两个关键信息:
- 目标是节点内网地址的
8545 - 错误是
i/o timeout
timeout 和 connection 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 构建这些方向。 这些检查有价值,但它们回答的是另一类问题。
更短的顺序是:
- 确认 RPC 错误和目标地址。
- 确认节点本机 RPC。
- 确认端口不是只监听 loopback。
- 确认调用方打不开 TCP。
- 对照 security group 和预期数据路径。
- 增加一条收口的入站规则。
- 用同样的 TCP 和 RPC 命令复测。
边界很简单:私有 RPC 只应该被需要它的机器访问,其它地方不应该访问。 本机 RPC 正常、远端 RPC 超时时,先看网络路径,再动节点进程。