내부 통신 시 발생하는 네트워크 에러 해결하기 (Node.js)

gompro·2024년 8월 31일
0
post-thumbnail

마이크로서비스 기반의 서비스를 운영하다보면 트래픽이 증가함에 따라 타임아웃 요청이 같이 증가하곤 한다.

타임아웃이 발생하는 이유는 여러가지가 있지만 네트워크 이슈에 집중해서 살펴보자.

ECONNRESET

node.js 문서에 따르면 위 에러는 리모트 소켓이 닫힌 상태에서 요청을 보낼 때 발생한다.

ECONNRESET (Connection reset by peer): A connection was forcibly closed by a peer. This normally results from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered via the http and net modules.

이 문제를 해결하려면 ECONNRESET 에러가 발생했을 때 요청을 재시도하거나 혹은 리모트 서버의 소켓 timeout 값보다 일찍 소켓을 닫을 수 있다.
(리모트 소켓이 닫힌 상태로 요청이 아예 가지 않게끔)

const http = require('http');
const Agent = require('agentkeepalive');
const agent = new Agent();

const req = http
  .get('http://localhost:3000', { agent }, (res) => {
    // ...
  })
  .on('error', (err) => {
    if (req.reusedSocket && err.code === 'ECONNRESET') {
      // retry the request or anything else...
    }
  })

위와 같이 요청 에러 발생 시 에러 코드를 확인해서 재시도할 수 있다.

물론 요청을 보낼 때마다 재시도 하는 것보단 아예 재시도할 일이 없게끔 사용되지 않는 (idle) 소켓을 정리하는 것이 훨씬 더 쉬운 방법이다.

agentkeepalive 라이브러리는 Node.js의 기본 http 모듈에 몇 가지 편의 기능을 추가하는 라이브러리이다.

위 라이브러리를 사용하면 위에서 얘기한 소켓 정리를 손쉽게 할 수 있다.

const http = require('http');
const Agent = require('agentkeepalive');

const keepaliveAgent = new Agent({
  freeSocketTimeout: 30000, // free socket keepalive for 30 seconds
});

위와 같이 셋팅하면 소켓이 사용되지 않은지 30초가 지나면 정리된다.

FreeSocketTimeout?

그렇다면 위 설정값을 몇 초로 정해야 ECONNRESET 에러를 피할 수 있을까?

내부 통신을 하는 경우에는 어떤 형태로든 내부의 통합된 게이트웨이를 거치는 경우가 많을텐데 해당 게이트웨이가 연결을 유지하는 시간보다 짧게 셋팅하면 된다.

예를 들어 Node.js 서버의 경우 연결 유지시간이 기본 5초로 셋팅되어 있으므로 4초로 셋팅할 수 있다.

혹은 nginx 서버를 사용하는 경우 keepalive_timeout 값을 기반으로 정할 수 있다.

http {
  keepalive_timeout 10s;
  
  server {
    // ..
  }
}

마지막으로 AWS ALB의 경우 idle timeout 값을 기본 60초로 유지하므로 60초보다 짧게 설정해주면 된다.

TCP keep-alive

위에서 agentkeepalive를 사용하면 사용되지 않은 소켓을 간단하게 정리할 수 있다고 했는데 해당 라이브러리를 사용하면 또 다른 이점이 있다.

바로 TCP keep-alive를 활용할 수 있다는 점이다.

Node.js의 경우 자바와 달리 런타임에서 기본적으로 DNS 캐싱을 지원하지 않기 때문에 DNS 룩업으로 인한 오버헤드가 다소 존재하는 편이다.

TCP keep-alive를 사용하면 DNS 룩업이나 핸드쉐이크 횟수를 줄일 수 있기 때문에 서버의 부하를 다소 줄여주는 장점도 있다.

복잡한 네트워크 토폴로지

마이크로서비스의 경우 인증, 서비스 디스커버리, 경로 재설정 등의 이유로 프록시 서버를 사용하는 경우가 많은데 각 서버마다 연결 유지를 위한 타임아웃 값을 맞춰주지 않으면 ECONNRESET 에러가 발생할 수 있다.

해당 에러는 주로 502 혹은 504 에러로 확인되는데, 타임아웃 설정만 잘 맞춰줘도 대부분 해결할 수 있다.

예를 들어 AWS ALB <-> Node.js 서버 와 같이 ALB와 Node.js 서버가 직접 연결된 경우 Node.js 서버의 타임아웃 값이 더 높게 셋팅되어야 한다.

그 이유는 ALB <-> Node.js 서버 간 연결 시 Node.js 서버의 타임아웃이 5초로 설정돼있으므로 ALB가 연결을 재활용할 경우 Node.js 서버가 먼저 소켓을 닫을 수 있기 때문이다.

그러므로

const express = require("express");

const app = express();
const server = app.listen(8080);

server.keepAliveTimeout = 61 * 1000;
server.headersTimeout = 65 * 1000;

위 코드와 같이 keepAliveTimeout 값을 더 높게 셋팅해줘야한다.

혹은 nginx 프록시 서버를 사용하는 경우 nginx 서버의 keepalive_timeout 값을 맞춰줘야한다.

연결 / 요청 타임아웃 적절하게 설정하기

위 설정을 모두 마치고나면 타임아웃 에러가 많이 줄어들 것이다.

여기에 추가적으로 연결 / 요청 타임아웃 값을 분리해서 설정해준다면 더 효율적으로 타임아웃을 관리할 수 있다.

요청 타임아웃 (read timeout)의 경우 axios를 사용하는 경우 아주 쉽게 설정할 수 있다.

const axios = require('axios');

axios({
  url: 'https://example.com/api/data',
  method: 'get',
  timeout: 5000
});

위와 같이 설정하면 서버의 응답을 5초간 기다리다가 해당 시간 내로 응답을 받지 못할 경우 timeout 에러를 발생시킬 것이다.

이것만으로 응답을 무한정 기다리는 경우는 방지할 수 있지만 방화벽 등으로 인해 연결이 아예 생성되지 않는 경우에는 비효율이 발생하게 된다. (연결 실패의 경우 전체 응답을 대기하는 것보다 훨씬 일찍 발견되므로)

그렇기 때문에 짧은 연결 타임아웃 값과 상대적으로 더 긴 요청 타임아웃을 설정해주는 것이 바람직하다.

axios의 경우 http 모듈의 agent를 커스텀하는 방식으로 연결 타임아웃을 구현할 수 있다.

깃헙 코멘트를 참고하면 아래 코드와 같이 작성할 수 있다.

axios.defaults.timeout = 3000
axios.defaults.httpAgent = new http.Agent({ timeout: 500 })
axios.defaults.httpsAgent = new https.Agent({ timeout: 500 })

axios.get("https://www.google.com:81")
   .catch(console.log)

여기서 연결 타임아웃은 500ms, 전체 요청에 대한 타임아웃은 3초가 된다.

다만 현재 시점 (24.09.01) 기준으로 해결되지 않은 이슈가 있기 때문에 follow-redirects 라이브러리 버전을 아래처럼 고정해줘야 정상적으로 동작을 확인할 수 있다.

  "resolutions": {
    "follow-redirects": "1.13.3"
  }
profile
다양한 것들을 시도합니다

0개의 댓글