Skip to content
isdnetworks
Go back

묻기 대신 밀어 주기

(수정됨)

거래소 시세 화면이 setInterval 로 초당 한 번씩 GET /api/ticker 를 물어보고 있었다. 동시 접속이 늘면서 API 서버가 그 요청량을 버티지 못했다.

Table of contents

Open Table of contents

증상 — 초당 한 번씩 묻던 화면

화면 하나가 초당 한 번을 부르므로 접속자 수가 그대로 초당 요청 수가 된다. 값이 안 바뀐 시간에도 같은 양을 부른다.

새벽처럼 거의 아무것도 안 바뀌는 시간에도 부하가 동일했다. 요청량이 실제 변화량이 아니라 접속자 수에 묶여 있는 구조였다.

판단 기준 — 부하와 신선도가 반대로 간다

두 가지 방법이 있었다. 주기를 3초로 늘리는 것과, 묻는 방식 자체를 SSE 로 밀어 주는 방식으로 바꾸는 것이다. 앞쪽은 부하가 그만큼 줄지만 화면이 그만큼 늦게 갱신되고, 시세 화면에서 몇 초의 지연은 받아들이기 어려웠다. 뒤쪽을 골랐다. 이유는 주기를 조절하는 안에서는 부하와 신선도가 항상 반대로 움직여서다. 대신 연결을 오래 열어 두는 구조가 새로 생기는 것을 감수했다.

주기를 조절하는 안에서는 부하와 신선도가 항상 반대로 움직이므로 둘 다 필요하면 구조를 바꾸는 수밖에 없다. WebSocket 이 아니라 SSE 로 간 것은 시세가 서버에서 화면으로만 흐르는 한 방향이라서다.

조치 — 밀어 주는 구조와 재연결

Spring MVC 의 SseEmitter 로 연결을 열어 두고 값이 바뀔 때만 보내게 하니 안 바뀌는 시간에는 트래픽이 거의 없어졌다. 바뀌면 다음 주기를 기다리지 않고 바로 나간다.

연결을 오래 열어 두니 끊기는 경우가 생겼는데, 브라우저의 EventSource 는 끊기면 스스로 다시 붙고 WHATWG 명세대로 retry: 필드로 그 간격을 서버가 정할 수 있다. 끊긴 동안 놓친 값이 있으므로 다시 붙을 때 Last-Event-ID 를 보고 현재값을 한 번 보내고 나서 스트림을 잇는 순서로 잡았다.

중간 장비가 끊는 연결

앞단 nginx 가 proxy_read_timeout 기본 60초 동안 아무것도 안 오는 연결을 끊고 있었는데 값이 안 바뀌면 정말로 아무것도 안 간다. 그래서 30초마다 : keep-alive 주석 줄을 보내 연결이 살아 있음을 알렸다. 명세대로 콜론으로 시작하는 줄은 클라이언트가 무시한다.

proxy_buffering off 도 필요했는데 켜져 있으면 nginx 가 응답을 모아서 내보내느라 실시간이 아니게 된다. 중간에 있는 장비들이 전부 이 구조를 전제로 설정돼 있어야 동작했다.

연결 수와 보내는 양

연결 수가 곧 부하가 되므로 ss -s 로 지표로 세고 ulimit -n 파일 서술자 상한도 확인했다. 상한이 목표 동시 접속보다 낮아서 올렸고 올리기 전에 연결당 메모리도 재 봤다.

보내는 양도 줄였는데 전체 시세를 매번 보내던 것을 바뀐 마켓만 보내고 JSON 필드 이름도 p·v 처럼 짧게 했다. 초당 수천 건이 나가는 자리에서는 한 건의 크기가 그대로 대역폭이 된다.

정리


Share this post on:

Previous Post
로그인 계정을 한 곳으로 모으면
Next Post
선행 컬럼이 없으면 인덱스가 죽는다