Skip to content
isdnetworks
Go back

백 초쯤 지나면 끊기던 연결

시세 화면이 가끔 멈춘다는 얘기가 들어왔다. 새로고침하면 다시 나오는데 열어 두고 보고 있으면 잘 나와서 재현이 안 됐다.

Table of contents

Open Table of contents

증상 — 재현이 안 됐다

앞에 두고 지켜보면 멈추지 않았다.

멈췄다는 사람은 있는데 재현이 안 되니 로그를 볼 시점도 못 잡는다. 무엇을 봐야 할지 정하려면 어떤 조건에서 나는지부터 좁혀야 했다.

문의를 다시 읽으니 공통점이 있었다.

화면을 열어 두고 다른 일을 하다가 돌아오면 멈춰 있다
거래가 활발한 종목에서는 안 그렇다

둘 다 그동안 아무것도 안 흘렀다는 뜻으로 읽혔다.

조사 — 조건 좁히기

거래가 없는 종목을 열어 두고 시계를 봤다.

0:00   연결 성공, 초기 데이터 수신
0:12   체결 하나 수신
...
1:40   (아무것도 안 옴)
1:47   연결 종료

마지막 프레임으로부터 100초쯤 뒤에 WebSocket 이 끊긴다.

거래가 활발하면 계속 데이터가 흐르니 100초를 채울 일이 없다. 그래서 지켜보고 있을 때는 재현이 안 됐다.

끊기는 시점이 일정하다는 것이 다음 단서였다. 값이 고정돼 있다면 어딘가에 그 값을 가진 설정이 있다.

원인 — 로그에 아무것도 없었다

서버 로그를 봤는데 오류도 없고 끊겼다는 기록도 없었다.

WebSocket connection closed: 1006

브라우저 콘솔에는 1006 이 찍혀 있었다.

1006 은 닫힘 프레임을 못 받았다는 뜻이다. 양쪽 애플리케이션이 서로 끊지 않았고 중간에서 누가 끊었다는 이야기가 된다.

로그가 비어 있다는 사실 자체가 단서였다. 우리가 끊었으면 우리 쪽에 남았을 것이다.

제약 — 경로마다 유휴 제한

경로에 무엇이 있는지 셌다.

브라우저 → CDN → 로드밸런서 → nginx → 애플리케이션

CDN 과 로드밸런서와 nginx 가 저마다 유휴 연결 제한을 갖고 있었다.

# nginx
proxy_read_timeout 60s;    # 기본값

proxy_read_timeout 말고 로드밸런서 쪽에도 기본 유휴 제한이 있었다.

이 값들은 그 시간 동안 아무것도 안 흐르면 끊는다는 뜻이다. 연결을 유지하는 데도 자원이 드니 노는 연결을 정리하는 것은 당연한 동작이다.

문제는 실시간 데이터 연결이 원래 가끔 조용한 성질이라는 점이었다. 거래가 없으면 보낼 것이 없는데 그것이 끊을 이유가 된다.

여럿이 경로에 걸쳐 있으면 그중 가장 짧은 값이 실제 한계가 된다.

조치 — 주기 신호로 채우기

조용해도 무언가를 보내면 된다.

pingpong 이 그 용도라 애플리케이션 데이터가 아니라 프로토콜 수준에서 오간다. 클라이언트 코드가 따로 처리할 필요가 없다.

const HEARTBEAT_INTERVAL = 30000;

wss.on('connection', function (ws) {
    ws.isAlive = true;

    ws.on('pong', function () {
        this.isAlive = true;
    });
    ...
});

setInterval(function () {
    wss.clients.forEach(function (ws) {
        if (ws.isAlive === false) {
            // 지난 주기에 pong 이 안 왔다. 죽은 연결이다
            return ws.terminate();
        }
        ws.isAlive = false;
        ws.ping();
    });
}, HEARTBEAT_INTERVAL);

HEARTBEAT_INTERVAL 30초는 가장 짧은 proxy_read_timeout 60초의 절반으로 잡았다.

제한보다 짧아야 하고 한 번 놓쳐도 괜찮게 절반 이하로 두는 편이 안전하다. 이 코드는 죽은 연결을 정리하는 역할도 한다.

클라이언트가 네트워크 단절로 사라지면 서버는 그것을 바로 모르고 한참 뒤에야 알게 된다. pong 이 안 온 연결을 다음 주기에 terminate 하니 그 사이 쌓이던 것이 사라졌다.

대응 — 클라이언트 쪽 복구

서버가 살아 있어도 중간에서 끊기는 경우는 남는다.

let retryDelay = 1000;

function connect() {
    const ws = new WebSocket(WS_URL);

    ws.onopen = function () {
        retryDelay = 1000;      // 성공하면 초기화
        subscribe(currentSymbols);
    };

    ws.onclose = function () {
        setTimeout(connect, retryDelay);
        retryDelay = Math.min(retryDelay * 2, 30000);   // 최대 30초
    };
}

여기서 두 가지를 지켰다.

retryDelay 를 늘려 가는 것이 첫째다. 서버가 죽어 있는데 수천 개 클라이언트가 1초마다 붙으면 서버가 살아나는 순간 다시 죽는다.

onopen 에서 subscribe 를 다시 부르는 것이 둘째다. 서버는 이것을 새 WebSocket 으로 보므로 이전에 무엇을 보고 있었는지 모른다.

이것을 빠뜨리면 연결은 살아 있는데 데이터가 안 오는 상태가 된다. 증상이 원래 문제와 똑같아서 고쳤는지 아닌지도 안 갈린다.

정리


Share this post on:

Previous Post
주석 처리가 폐지는 아니다
Next Post
단건 실행은 스코프를 안 거친다