멀티 RPC 로테이션과 폴백 설계 — 죽는 노드에도 살아남는 온체인 봇
온체인 봇을 오래 돌려 보면 전략보다 인프라가 먼저 발목을 잡습니다. 그중 최악은 RPC 노드입니다. 공개 RPC는 갑자기 타임아웃이 나고, 블록이 몇 개씩 뒤처지고, 어떤 날은 그냥 죽습니다. 단일 노드에 봇을 묶어 두면 그 노드가 죽는 순간 봇도 같이 죽죠. 해법은 여러 엔드포인트를 두고 건강한 놈으로 자동 전환하는 것입니다.
노드 하나의 건강 상태를 재는 법
"살아 있다"는 두 가지로 판단합니다. 응답이 오는가(지연 시간), 그리고 최신 블록을 알고 있는가. 연결은 되는데 블록이 20개 뒤처진 노드는, 죽은 노드만큼이나 위험합니다. 오래된 상태로 견적을 계산하면 이미 사라진 기회를 좇게 되니까요.
type RPCNode struct {
URL string
Block uint64 // 이 노드가 아는 최신 블록 높이
Latency time.Duration // 응답 지연
Healthy bool
}
모든 노드를 병렬로 헬스체크
엔드포인트를 하나씩 순서대로 확인하면, 앞쪽의 느린 노드가 뒤쪽 점검을 다 막습니다. 전부 동시에 찔러 보고 결과를 모읍니다. 각 점검에는 타임아웃을 걸어, 죽은 노드가 영원히 매달리지 않게 합니다.
func (b *Bot) refreshRPCHealth(ctx context.Context) {
var wg sync.WaitGroup
results := make([]RPCNode, len(b.rpcURLs))
for i, url := range b.rpcURLs {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
node := RPCNode{URL: u, Healthy: false}
start := time.Now()
c, err := ethclient.Dial(u)
if err != nil {
results[idx] = node // 연결 실패 → unhealthy
return
}
defer c.Close()
// 3초 안에 최신 블록을 못 주면 탈락
checkCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
block, err := c.BlockNumber(checkCtx)
if err != nil {
results[idx] = node
return
}
node.Block = block
node.Latency = time.Since(start)
node.Healthy = true
results[idx] = node
}(i, url)
}
wg.Wait()
// ... 아래에서 순위 정렬
}
순위 규칙 — 건강, 그다음 최신, 그다음 속도
결과를 정렬해 "가장 좋은 노드"를 맨 앞으로 보냅니다. 우선순위는 명확합니다. 건강한 노드가 무조건 먼저, 그다음 블록이 더 높은 노드, 마지막으로 지연이 낮은 노드.
sort.Slice(results, func(i, j int) bool {
// 1) 건강한 노드를 앞으로
if !results[i].Healthy && results[j].Healthy {
return false
}
if results[i].Healthy && !results[j].Healthy {
return true
}
// 2) 블록이 더 높은(최신) 노드를 앞으로
if results[i].Block != results[j].Block {
return results[i].Block > results[j].Block
}
// 3) 지연이 낮은 노드를 앞으로
return results[i].Latency < results[j].Latency
})
사용 시점의 폴백
정렬해 둔 목록에서 클라이언트를 꺼낼 때, 맨 앞부터 훑으며 연결에 성공하는 첫 건강한 노드를 씁니다. 순위표가 최신 헬스체크 기준이라도, 그 찰나에 노드가 죽었을 수 있으니 실제 연결까지 확인하는 거죠.
func (b *Bot) getClient() *ethclient.Client {
b.rpcMu.RLock()
nodes := b.rpcNodes
b.rpcMu.RUnlock()
for _, n := range nodes {
if !n.Healthy {
continue
}
if c, err := ethclient.Dial(n.URL); err == nil {
return c // 살아 있는 최선의 노드
}
}
// 전부 실패하면 인덱스 로테이션으로라도 시도 (최후의 수단)
url := b.rpcURLs[b.rpcIdx%len(b.rpcURLs)]
b.rpcIdx++
c, _ := ethclient.Dial(url)
return c
}
헬스체크는 주기적으로(예: 몇 라운드마다) 다시 돌려 순위표를 갱신합니다. 이러면 한때 죽었던 노드가 살아나면 다시 후보로 복귀하고, 느려진 노드는 뒤로 밀립니다.
실전 팁
- 공급자를 섞어라. 같은 회사의 노드 5개는 그 회사가 장애 나면 전부 죽습니다. 서로 다른 제공자의 엔드포인트를 섞으세요.
- 읽기와 쓰기를 나눠라. 조회는 아무 건강한 노드로, 트랜잭션 전송은 더 신뢰할 수 있는(가능하면 유료) 노드로 보내는 편이 안전합니다.
- 블록 지연 임계치를 둬라. 최고 블록 대비 N블록 이상 뒤처진 노드는 healthy여도 제외하는 가드를 추가할 수 있습니다.
노드 하나에 기대지 않는 것 — 이 단순한 원칙 하나가 봇의 가동 시간을 극적으로 끌어올립니다. 화려한 전략보다, 죽지 않고 계속 도는 게 먼저입니다.
이 글은 교육·정보 목적이며 투자 권유가 아닙니다. 온체인 자동매매에는 원금 손실 위험이 있고, 결과에 대한 책임은 본인에게 있습니다.
댓글
댓글 쓰기