TCP 소켓을 장기간 유지해야 하는 서버를 만들고 있었음. 소켓이 끊겼을 때 수동 재시작 없이 자동으로 재연결되도록 Exponential Backoff 기반 재연결 루프를 구현함.
구조
재연결 루프는 별도 고루틴으로 실행하고, 연결 끊김 이벤트를 채널로 수신하는 구조임.
1 2 3 4 5 6 7 8 9 10
| type RQProtocol struct { reconnectCh chan struct{} reconnecting atomic.Bool config RQConfig }
type RQConfig struct { ReconnectInitialDelay time.Duration ReconnectMaxDelay time.Duration }
|
StartReconnectLoop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| func (rq *RQProtocol) StartReconnectLoop() { go func() { delay := rq.config.ReconnectInitialDelay for { select { case <-rq.reconnectCh: rq.reconnecting.Store(true) case <-rq.shutdownCh: return }
for { rq.log.Info("재연결 시도", zap.Duration("delay", delay)) time.Sleep(delay)
if err := rq.connect(); err != nil { delay = min(delay*2, rq.config.ReconnectMaxDelay) continue }
if err := rq.PerformHandshake(); err != nil { rq.log.Warn("핸드셰이크 실패, 재시도", zap.Error(err)) delay = min(delay*2, rq.config.ReconnectMaxDelay) continue }
delay = rq.config.ReconnectInitialDelay rq.reconnecting.Store(false) break } } }() }
|
TriggerReconnect
연결 끊김을 감지한 곳(readLoop, Send 실패 등)에서 호출함. 채널이 non-blocking이라 이미 재연결 중이면 중복 트리거는 무시됨.
1 2 3 4 5 6
| func (rq *RQProtocol) TriggerReconnect() { select { case rq.reconnectCh <- struct{}{}: default: } }
|
초기 연결 실패 처리
서버 시작 시 상대 서버가 아직 안 떠있을 수 있음. 초기 연결 실패도 재연결 루프에서 처리하도록 했음.
1 2 3 4 5 6 7 8 9 10 11 12
| rqProtocol.StartReconnectLoop()
if err := rqProtocol.Start(); err != nil { log.Warn("초기 TCP 연결 실패, 재연결 루프에서 재시도", zap.Error(err)) rqProtocol.StartReadLoops() rqProtocol.TriggerReconnect() } else { if err := rqProtocol.PerformHandshake(); err != nil { rqProtocol.TriggerReconnect() } }
|
Health API에 재연결 상태 노출
1 2 3 4 5
| type HealthResponse struct { Status string `json:"status"` PbSessionState string `json:"pb_session_state"` PbReconnecting bool `json:"pb_reconnecting"` }
|
pb_reconnecting: true가 보이면 재연결 중이라는 의미임. 모니터링에서 이 필드를 감시하면 소켓 단절 시 즉시 파악 가능함.
동작 흐름
1 2 3 4 5 6
| TCP 끊김 감지 → TriggerReconnect() → 5초 대기 후 connect() 시도 → 실패 시 10초, 20초, 40초... (최대 60초) → 성공 시 PerformHandshake() → 세션 복구 완료, delay 5초로 초기화
|
재연결 성공 후 pending 중이던 요청들은 timeout으로 빠져나오고, 클라이언트에서 재시도하는 구조임.
오늘의 개발 메모 끝~
eod