web3-infra
ローカル RPC は動くのに別の EC2 からは timeout する
Erigon の private JSON-RPC timeout を、client、node health、socket binding、security group に分けて確認した記録。
Indexer が Ethereum JSON-RPC を retry したあとに終了した。
失敗した処理は HTTP 経由の eth_getLogs だったので、最初は node 側の問題に見えた。
rpc error after retry, exiting
Post "http://<node-private-ip>:8545": dial tcp <node-private-ip>:8545: i/o timeout
node 自体はすでに同期済みだった。
eth_syncing は false を返し、Erigon の log には低い block age の
head validated が出続けていた。
問題は Erigon の sync path ではなく、2 台の EC2 の間にあった。
用語の説明
| 用語 | 意味 |
|---|---|
| JSON-RPC | client が chain data を読むための HTTP API。Erigon では TCP 8545 がよく使われる。 |
| Security group | instance の network interface に付く AWS の firewall。inbound と outbound の通信を制御する。 |
| Source security group | inbound rule の送信元として使う security group。その group が付いた network interface からの通信が許可される。 |
| Socket binding | process が listen する address。127.0.0.1:8545 は local only、0.0.0.0:8545 は全 interface。 |
| Timeout | client が request を出したが、時間内に応答が返らない状態。EC2 内部通信では filtering や routing を疑うことが多い。 |
まず error を読む
Indexer log には見るべき情報が 2 つあった。
- destination は node の private address と port
8545 - error は
i/o timeout
timeout は connection refused と違う。
connection refused は、host には届いたが port で listen していない時によく出る。
i/o timeout は、有効な応答が返ってこなかった時に出る。
security group、network ACL、route、host firewall、または process が loopback にしか bind していない可能性がある。
使った command
調査の軸になった command は以下。 識別子は placeholder に置き換えている。
node と caller の instance を見る。
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 経由で node host 上の状態を見る。 wrapper command は remote shell command を実行し、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>
node host 上で実行した shell command はこれ。
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}}'
caller 側も同じ SSM wrapper で実行した。 実際の shell command はこれ。
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 が詰まりどころだと分かったら、narrow な inbound rule を 1 本だけ追加する。
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"}]'
最後に caller-side check をもう一度実行し、tcp_ok と eth_blockNumber の応答を確認する。
network を変える前に node を見る
最初に node host 上で local 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
node は block number を返した。
eth_syncing は false だった。
log も正常だった。
head validated ... age=2s
Timings: Forkchoice Commit ... commit=1s
ここで chain sync は疑いから外れた。 node process は生きていて、local RPC は返り、Erigon は head を追えている。
port の bind address を見る
local で成功しても、別 host から届くとは限らない。
process が 127.0.0.1 だけに bind していることがある。
ss -lntp | egrep ':8545|:8546|:8551'
docker ps --format 'table {{.Names}}\t{{.Ports}}'
見る signal はこれだった。
0.0.0.0:8545
[::]:8545
container は RPC port を host の全 interface に publish していた。
もし 127.0.0.1:8545 しか出ていなければ、node または container の bind 設定を直す場面だった。
今回は binding は詰まりどころではなかった。
caller 側から確認する
caller host からは TCP connection を開けなかった。
timeout 5 bash -lc '</dev/tcp/<node-private-ip>/8545' \
&& echo tcp_ok || echo tcp_failed
結果はこうだった。
tcp_failed
同じ host から JSON-RPC を投げても timeout した。
curl -sS -m 5 \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://<node-private-ip>:8545
この時点で状態はかなり絞れた。
| Check | Result |
|---|---|
| Node local RPC | OK |
| Node socket binding | 0.0.0.0:8545 |
| Caller to node TCP | timeout |
| Caller to node JSON-RPC | timeout |
詰まりは 2 台の host の間にある。
security group を data path として読む
2 台の instance は同じ VPC と subnet にいた。 caller は outbound を出せる。 node 側の security group は P2P と metrics を許可していたが、caller からの JSON-RPC は許可していなかった。
private blockchain node では、この分け方は自然だ。
P2P は public、metrics は monitoring host、JSON-RPC は必要な service だけに絞る。
ここでやることは open な security group を使い回すことでも、8545 を internet に出すことでもなかった。
必要なのは 1 本の inbound rule だった。
node security group
TCP 8545
source: application server security group
description: JSON-RPC from application server
source security group を使うと、rule は IP ではなく caller の役割に寄る。 application server が置き換わっても、新しい network interface に同じ security group が付いていれば通信は通る。
caller 側から再確認する
rule を追加したあと、同じ caller-side check はこう変わった。
tcp_ok
RPC も返った。
{"jsonrpc":"2.0","id":1,"result":"0x..."}
{"jsonrpc":"2.0","id":2,"result":false}
1 つ目は current block number。
2 つ目は eth_syncing=false。
caller が node に到達でき、node も head 付近にいることを確認できた。
この順序で切り分ける
この timeout は、Erigon log、disk I/O、snapshot build に意識を持っていかれやすい。 それらを見る場面もあるが、今回の問いは network path だった。
短い順序はこれで足りた。
- RPC error と destination を確認する。
- node の local RPC を確認する。
- port が loopback-only ではないことを確認する。
- caller から TCP を開けないことを確認する。
- security group と意図した data path を照合する。
- narrow な inbound rule を 1 本追加する。
- 同じ TCP と RPC の command で再確認する。
private RPC は、必要な machine からだけ届く状態がいい。 local RPC が返り、remote RPC が timeout する時は、node process を触る前に network path を見る。