최근 운용중인 서버 중 한 곳에서 `socket hang up` 에러가 발생했다.
여러 시행착오를 거쳐 결국 문제를 해결하였고, 해당 해결법에 대한 한국어 레퍼런스는 없길래 기록하고자 한다.
환경
먼저 서버에서 HTTP Client로는 Axios / node 18 LTS 버전을 사용하고 있다.
`hang up`은 통상적으로 서버가 실행중이지만, 어떤 요청 시에 아무런 응답이 없다는 것을 의미한다.
`socket hang up`이라는 에러 역시 통신을 시도하는 동안 통신이 실패한 것을 의미하고, 응답이 오지 않아서 발생한 에러로 유추할 수 있었다.
에러가 발생한 코드는 다음과 같은 구조로 이뤄져 있다.
export async function callBack (resData) {
try {
const response = await axios({
url: `${URL}/api/public/callback`,
method: 'POST',
headers: {
Authorization: `Bearer ${token.access_token}`,
},
data: resData,
});
if (response.status !== 200) {
//
} else {
//
}
} catch (err) {
//
}
}
첫 번째 시도
노드 서버의 `keep-alive` 시간이 너무 짧아서 발생한 에러로 생각했다.
Node.js는 `keep-alive`를 따로 설정하지 않으면 기본값인 5초로 설정되어 있고, Express나 NestJS 같은 프레임워크를 사용하는 경우에도 5초로 설정되어 있다.
server.keepAliveTimeout = 60 * 1000; // 1분
따라서 해당 시간을 5초 -> 60초로 설정하였지만 문제가 해결되지 않았다.
두번째 시도
어쨌든 해당 코드에선 axios를 호출하는 단을 제외하면 에러가 발생할만한 부분이 보이지 않았고, 기존에는 기본 axios 인스턴스를 사용하여 바로 호출하였기 때문에 axios와 관련지어 해결책를 찾아보았다.
import https from 'https';
const httpsAgent = new https.Agent({ keepAlive: true });
const axiosInstance = axios.create({
httpsAgent,
timeout: 20 * 1000, // 기본 타임아웃 설정
});
const response = await axiosInstance({
url: `${URL}/api/public/callback`,
method: 'POST',
headers: {
Authorization: `Bearer ${token.access_token}`,
},
data: resData,
});
기존 코드에선 기본 axios 인스턴스를 사용했다면, 이번에는 HTTPS 에이전트를 명시적으로 지정한 뒤 Axios 인스턴스를 직접 정의하여 사용했다.
먼저 Node.js의 `https.Agent`의 경우 keepAlive 기능이 기본적으로 비활성화되어 있다.
이 경우 TCP 연결 생성 및 SSL/TSL 핸드셰이크를 매번 수행해야 하므로 오버헤드가 증가한다.
만약 `keepAlive: ture` 옵션을 설정하여 활성화하게 되면 요청을 종료한 후에도 연결을 닫지 않고 유지하여, 동일한 호스트로의 후속 요청에서 동일한 연결을 사용한다.
기본 에이전트와 비교하면, 연결을 재사용하므로 짧은 시간 내 다수의 요청을 보내는 서버에서는 더 효율적이라고 볼 수 있다.
호출해야 하는 url이 http일 경우 https가 아닌 http 모듈을 사용하여 적용하면 된다.
다음으로 직접 정의한 Axios 인스턴스를 사용했다.
기본 인스턴스에서는 타임아웃 설정이 명시되지 않았으므로, axios의 기본 타임아웃이 무제한으로 적용된다.
만약 서버가 응답하지 않으면 요청은 무한히 대기 상태로 남게 되고, 타임아웃이 필요한 환경에서는 잠재적으로 문제가 될 수 있다.
따라서 `timeout: 20 * 1000` 설정을 추가하여 요청이 20초 내에 응답하지 않으면 ECONNABORTED 오류를 발생시켜 강제로 종료되도록 했다.
서버 문제나 네트워크 지연 등으로 인해 요청이 무기한 대기 상태가 되는 것을 방지하고자 했다.
결론
다른 서버에서도 비슷하게 axios를 활용하여 통신하지만, 해당 서버에서만 발생했다는 점에서 여러 원인을 유추해볼 수 있다.
`socket hang up` 에러가 발생했던 가능성을 살펴보고자 한다.
첫 번째로는 keepAlive가 활성화되지 않고 매 요청마다 새로운 TCP 연결을 생성했기 때문에 서버와의 연결이 끊어지거나, 연결을 유지하지 못해 에러가 발생했다는 것이다.
두 번째로는 타임아웃 설정이 없었기 때문에, 서버가 일정 시간 응답하지 않아 연결이 종료되지 않고 대기 상태로 남아 에러로 이어졌다는 것이다.
이번 경험을 통해 정확한 로깅과 지속적인 모니터링의 중요성을 다시 한번 느끼게 되었고, axios 통신에 대해서도 다시금 고찰해볼 수 있었다.
HTTP Client는 외부 라이브러리 중 자주 교체되는 영역인데 Node.js에서는 `request -> Axios -> Got -> Fetch -> ?` 순서로 대세가 교체되었다.
예전엔 fetch를 썼지만, 어느 순간부턴 axios가 간결하고 편리하며 오류처리를 쉽게 할 수 있고, json 파싱을 제공해준다는 것 때문에 주로 axios를 사용해왔다.
서버의 규모나 요청 횟수를 고려하여 axios를 활용하는 방식 역시 제각각 다르게 세팅했는데, 앞으로는 좀 더 넓은 시야로 적절하게 활용해야 겠다는 생각이 들었다.
HTTP Client 또한 보다 안정적인 서버 운용에 적합한 방식은 무엇인지 고민해보려고 한다.
HTTP Client를 어떻게 변화에 대응하기 쉬운 형태로 감싸서 사용하는지에 대한 글 역시 이 고민에 도움이 되리라 생각된다.
