배경
개발자님! 화면이 너무 늦게 떠요!!
이번 주에 매출 통계 기능을 개발해서 개발 서버에 올렸는데 테스팅을 하다가 갑자기 긴급 호출이 들어왔다. API 속도가 너무 느리다는데 얼마나 느리길래 그런지 확인해봤더니 무려 10~16초가 걸렸다.
신규 레포지토리를 파서 개발한 거라 설정에 문제가 있었나? VPC간 통신 때문에 지연되는 건가? 별의 별 생각이 다 들어서 각 모듈별로 실행되는 시간, 실제 쿼리하는 시간을 전부 측정해봤는데 10~16초나 걸릴만한 이유를 찾지 못했다. 실제 동작이 query string 받아서 SELECT * WHERE a = ? AND b = ? ORDER BY c DESC
쿼리해서 결과를 반환해주는 아주 간단한 API 였는데 너무 이상한 현상이었다.
로그를 아무리 뒤져봐도 에러도 보이지 않았고 코드의 각 부분이 실행되는 시간을 측정해봐도 2초 이내에 모든 실행이 완료되고 있었다. 이상한 점은 지정한 동작을 모두 완료한 후 응답을 바로 반환하지 않고 6초 이상 대기한 이후에야 응답을 반환하고 있었다는 것이다.
Connection Pool과 유휴 연결 문제
결론적으로 문제의 원인은 connection pooler의 유휴 연결이 끝나지 않아 lambda 함수가 응답을 반환하려고 하지만 이벤트 루프에 열린 데이터베이스 연결이 있어서 응답을 반환하지 못했던 것이다!
사용하고 있던 라이브러리의 pg.Pool 객체의 idleTimeoutMillis 옵션을 보면 기본값이 10초로 되어 있는 걸 볼 수 있다. 요청 받은 작업을 처리한 이후에도 응답을 보내지 못했던 이유가 이 유휴 연결 10초를 기다리고 있었기 때문이었던 것이다!
// number of milliseconds a client must sit idle in the pool and not be checked out
// before it is disconnected from the backend and discarded
// default is 10000 (10 seconds) - set to 0 to disable auto-disconnection of idle clients
idleTimeoutMillis?: number
해결방법
Lambda context.callbackWaitsForEmptyEventLoop = false
Lambda의 Context 객체는 함수의 이름, 버전, 메모리 같은 함수와 실행환경에 대한 정보를 제공하는 속성과 메서드로 구성되어 있는 객체이다. 이 객체의 callbackWaitsForEmptyEventLoop 속성의 설명을 보면 다음과 같다.
Node.js 이벤트 루프가 빌 때까지 대기하는 대신, 콜백이 실행될 때 즉시 응답을 보내려면 false로 설정합니다. 이것이 false인 경우, 대기 중인 이벤트는 다음 번 호출 중에 계속 실행됩니다.
즉, 이 값을 false로 설정하면 이벤트 루프가 빌때까지 대기하지 않고 바로 응답을 보내는 것이다!
유휴 연결시간 줄이기
유휴 연결시간 자체를 줄여서 이벤트 루프에 유휴 연결이 더 짧게 남아있을 수 있게도 해봤다.
const { Pool } = require('pg');
const pool = new Pool({
idleTimeoutMillis: 1000, // 1초로 설정
});
기본값으로 설정했을 때보다 확실히 응답시간이 줄어들긴 했지만 여전히 응답이 준비된 이후에 잠깐의 대기 시간이 생기는 문제가 있었다. 그렇다고 너무 짧게 설정하면 많은 비용이 드는 데이터베이스 연결을 lambda가 실행될 대마다 계속 시도해야 하는 문제가 있다.
결국은 첫 번째에 소개했던 context.callbackWaitsForEmptyEventLoop = false
로 응답을 바로 보내도록 설정하고 유휴 연결시간은 사용패턴에 따라 적절한 값으로 설정하는 게 가장 좋을 것 같다.
결론
삽질했던 시간보다 훨씬 간단하게 해결한 문제였다. 이 문제를 해결하면서 기존에 어드민 API 에서 사용하던 API도 위와 같은 문제가 있었다는 걸 발견했다. 그때는 fastify와 같은 프레임워크 없이 구현했기 떄문에 lambda의 콜드 스타트 시간이 굉장히 짧아 문제라고 생각을 못했었던 것 같다. 다음에는 API 개발할 때 응답속도도 항상 체크해야겠다.
이번에 겪은 일은 내가 사용하고 있는 플랫폼에 대한 이해가 부족해서 발생한 일이었다. 대부분 요즘은 AWS 서비스들이 너무 잘 되어 있다보니 이쪽에 원인이 있을 것이라고 생각을 못했하고 코드만 살펴보다보니 원인 파악이 좀 늦었다. 앞으로는 코드에만 집중하기보다 좀 더 문제를 다각도에서 바라보도록 노력해야 겠다고 생각했다.