서버리스용 데이터베이스를 설계할 때 가장 큰 과제는 수익성 있는 방식으로 요청당 가격을 지원하는 인프라를 구축하는 것이었습니다. 우리는 Upstash가 이를 달성했다고 믿습니다. 제품을 출시한 후 또 다른 주요 과제가 있음을 알게 되었습니다. 바로 데이터베이스 연결입니다!
아시다시피 Serverless Functions는 0에서 무한대로 확장됩니다. 즉, 함수에 많은 트래픽이 발생하면 클라우드 공급자가 새 컨테이너(람다 함수)를 병렬로 생성하고 백엔드를 확장합니다. 함수 내에서 새로운 데이터베이스 연결을 생성하면 데이터베이스의 연결 제한에 빠르게 도달할 수 있습니다.
람다 함수 외부에서 연결을 캐시하려고 하면 다른 문제가 발생합니다. AWS가 Lambda 함수를 정지할 때 연결을 닫지 않습니다. 따라서 여전히 위협할 수 있는 많은 유휴/좀비 연결로 끝날 수 있습니다.
이 문제는 Redis에만 국한되지 않고 TCP 연결(Mysql, Postgre, MongoDB 등)에 의존하는 모든 데이터베이스에 적용됩니다. 서버리스 커뮤니티가 serverless-mysql과 같은 솔루션을 만들고 있는 것을 볼 수 있습니다. 이것은 클라이언트 측 솔루션입니다. Upstash로서 우리는 서버 측을 구현하고 유지 관리할 수 있는 이점이 있습니다. 그래서 우리는 연결을 모니터링하고 유휴 연결을 제거하여 문제를 완화하기로 결정했습니다. 그래서 여기 알고리즘이 있습니다:max-concurrent-connection으로서, 우리는 데이터베이스에 대해 soft-limit와 hard-limit의 두 가지 제한이 있습니다. 데이터베이스가 소프트 한계에 도달하면 유휴 연결을 종료하기 시작합니다. 하드 한도에 도달할 때까지 새로운 연결 요청을 계속 수락합니다. 데이터베이스가 하드 제한에 도달하면 새로운 연결을 거부하기 시작합니다.
연결 제거 알고리즘
if( current_connection_count < SOFT_LIMIT ) {
ACCEPT_NEW_CONNECTIONS
}
if( current_connection_count > SOFT_LIMIT && current_connection_count < HARD_LIMIT ) {
ACCEPT_NEW_CONNECTIONS
START_EVICTING_IDLE_CONNECTIONS
}
if( current_connection_count > HARD_LIMIT ) {
REJECT_NEW_CONNECTIONS
}
Upstash 문서에 나열된 최대 동시 연결 제한은 소프트 제한입니다.
임시 연결
위의 알고리즘을 배포한 후 모든 지역에서 거부된 연결 수가 크게 감소했습니다. 그러나 여전히 안전한 편을 원하면 문제를 해결할 수도 있습니다. 연결을 재사용하는 대신 함수 내에서 Redis 연결을 열 수 있지만 아래와 같이 Redis 사용이 끝나면 닫을 수도 있습니다.
exports.handler = async (event) => {
const client = new Redis(process.env.REDIS_URL);
/*
do stuff with redis
*/
await client.quit();
/*
do other stuff
*/
return {
response: "response",
};
};
위의 코드는 동시 연결 수를 최소화하는 데 도움이 됩니다. 사람들은 새로운 연결의 대기 시간 오버헤드에 대해 묻습니다. Redis 연결은 매우 가벼운 것으로 알려져 있습니다.
Redis 연결이 정말 가볍습니까?
Redis 연결이 얼마나 가벼운지 알아보기 위해 벤치마크 테스트를 실행했습니다. 이 테스트에서는 두 가지 접근 방식의 지연 시간을 비교합니다.
1- 임시 연결:연결을 재사용하지 않습니다. 대신 각 명령에 대해 새 연결을 만들고 연결을 즉시 닫습니다. 클라이언트 생성 대기 시간, ping() 및 client.quit()를 함께 기록합니다. benchEphemeral()
참조 아래 코드 섹션의 메서드입니다.
2- 연결 재사용:연결을 한 번 만들고 모든 명령에 대해 동일한 연결을 재사용합니다. 여기에서 ping()
의 지연 시간을 기록합니다. 작업. benchReuse()
참조 아래 방법.
async function benchReuse() {
const client = new Redis(options);
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
client.ping();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
client.quit();
console.log(hist.outputPercentileDistribution(1, 1));
}
async function benchEphemeral() {
const hist = hdr.build();
for (let index = 0; index < total; index++) {
let start = performance.now() * 1000; // to μs
const client = new Redis(options);
client.ping();
client.quit();
let end = performance.now() * 1000; // to μs
hist.recordValue(end - start);
await delay(10);
}
console.log(hist.outputPercentileDistribution(1, 1));
}
벤치마크를 직접 실행하려면 리포지토리를 참조하세요.
두 가지 설정으로 AWS EU-WEST-1 리전에서 이 벤치마크 코드를 실행했습니다. 첫 번째 설정은 클라이언트와 데이터베이스가 동일한 가용성 영역에 있는 SAME ZONE입니다. 두 번째 설정은 클라이언트가 데이터베이스와 다른 가용성 영역에서 실행되는 INTER ZONE입니다. Upstash Standard 유형을 데이터베이스 서버로 사용했습니다.
우리는 새 연결을 만들고 닫는 오버헤드(임시 접근 방식)가 75마이크로초(99번째 백분위수)에 불과하다는 것을 확인했습니다. 오버헤드는 영역 간 설정(80마이크로초)에서 매우 유사합니다.
그런 다음 AWS Lambda 함수 내에서 동일한 테스트를 반복하기로 결정했습니다. 결과는 달랐습니다. 특히 Lambda 함수의 메모리를 낮게(128Mb) 설정했을 때 Redis 연결의 더 큰 오버헤드를 보았습니다. AWS Lambda 함수 내에서 최대 6-7msec의 지연 시간 오버헤드를 확인했습니다.
Redis 연결에 대한 결론:
- Redis 연결은 적절한 양의 CPU 성능을 갖춘 시스템에서 정말 가볍습니다. t2.micro에서도 가능합니다.
- 기본 AWS Lambda 구성의 CPU 성능이 매우 낮기 때문에 Lambda 함수의 총 실행 시간에 비해 TCP 연결 비용이 크게 증가합니다.
- 기본/최소 메모리로 Lambda 함수를 사용하는 경우 함수 외부에 Redis 연결을 캐시하는 것이 좋습니다.
냉동 컨테이너 => 좀비 연결
일부 AWS Lambda 설정에서 연결이 눈에 띄는 오버헤드를 가질 수 있다는 것을 깨닫고 우리는 reusing connection
에 대한 추가 테스트를 하기로 결정했습니다. AWS 람다에서. 다른 문제가 감지되었습니다. 이것은 아직 아무도 보고하지 않은 극단적인 사례였습니다.
다음은 어떻게 진행되는지 타임라인입니다.
1단계 - 타이머-0초: 람다 함수 외부에서 연결을 캐싱하여 요청을 보냅니다.
if (typeof client === "undefined") {
var client = new Redis("REDIS_URL");
}
module.exports.hello = async (event) => {
let response = await client.get("foo");
return { response: response + "-" + time };
};
2단계 - 타이머-5초: AWS는 잠시 후 컨테이너를 동결합니다.
3단계 - 시간-60초: Upstash는 유휴 연결에 대해 60초의 제한 시간이 있습니다. 따라서 연결을 종료하지만 고정되어 있으므로 클라이언트에서 ACK를 얻을 수 없습니다. 따라서 서버 연결은 FIN_WAIT_2 상태가 됩니다.
4단계 - 시간-90초: Upstash 서버는 연결을 완전히 종료하고 FIN_WAIT_2 상태를 종료합니다.
5단계 - 시간-95초: 클라이언트는 동일한 요청을 보내고 ETIMEDOUT 예외를 받습니다. 클라이언트는 연결이 열려 있다고 가정하지만 그렇지 않습니다. 🤦🏻 🤦🏻 🤦🏻
6단계 - 시간-396초: 마지막 요청 후 5분이 지나면 AWS가 컨테이너를 완전히 종료합니다.
STEP7 - 시간-400초: 클라이언트는 이번에 동일한 요청을 전송합니다. 컨테이너가 처음부터 생성되어 초기화 단계를 건너뛰지 않기 때문에 잘 작동합니다. 새로운 연결이 생성됩니다.
위에서 볼 수 있듯이 AWS는 컨테이너를 해동하고 연결을 재사용합니다. 그러나 서버 측에서 연결이 닫혔고 기능이 정지되어 통신할 수 없습니다. 따라서 Upstash가 유휴 연결을 제거하는 것과 AWS가 유휴 기능을 처리하는 사이에 동기화 문제가 있습니다. 따라서 AWS가 기능을 종료한 후에만 유휴 연결을 종료하면 아무런 문제가 없습니다.
AWS가 300초 후에 유휴 기능을 종료한다고 가정하여 Upstash 연결 제한 시간을 310초로 변경했습니다. 이 변경 후 문제가 사라졌습니다. 여기서 문제는 AWS가 유휴 기능을 종료할 때 투명하지 않다는 것입니다. 따라서 테스트를 계속하고 문제가 다시 발생하는지 감지해야 합니다.
이 문제는 serverless-mysql 라이브러리에서 볼 수 있는 문제와 매우 유사합니다. 주석에서 ETIMEDOUT 예외 시 요청을 다시 시도하도록 제안되었습니다. 그러나 재시도에는 두 가지 단점이 있습니다. 먼저 실제 네트워크 문제로 처리 및 시간 초과되었을 수 있는 쓰기 요청을 다시 시도할 수 있습니다. 두 번째 문제는 실패한 요청의 추가 대기 시간입니다.
GraphQL도 도움이 됩니다
연결 없는 API를 사용하기 위해 연결 문제를 제거하는 한 가지 방법입니다. Upstash는 Redis 프로토콜 외에도 GraphQL API를 지원합니다. GraphQL은 HTTP 기반이므로 연결 제한 문제가 없습니다. 지원되는 명령에 대해서는 문서를 확인하십시오. GraphQL API는 Redis 프로토콜에 비해 지연 오버헤드(약 5msec)가 있음을 유의하십시오.
결론
서버리스 애플리케이션을 위한 원활한 경험을 위해 Upstash 데이터베이스를 사용자 지정합니다. 우리의 새로운 서버 측 알고리즘은 AWS Lambda가 생성하는 비활성 연결을 많이 제거합니다. Lambda 함수 내에서 Redis 클라이언트를 열거나 닫아 연결 수를 최소화할 수 있지만 함수 메모리가 1GB 미만인 경우 지연 시간 오버헤드가 발생할 수 있습니다.
결론적으로 서버리스 사용 사례에 대한 권장 사항은 다음과 같습니다.
- 사용 사례가 지연 시간에 민감한 경우(예:6msec이 큰 경우) Redis 클라이언트를 재사용하세요.
- 동시 클라이언트 수가 매우 많으면(1000개 이상) Redis 클라이언트를 재사용하세요.
- 사용 사례가 지연 시간에 민감하지 않은 경우 함수 내에서 Redis 클라이언트를 열거나 닫습니다.
- 함수에 1GB 이상의 메모리가 있는 경우 함수 내에서 Redis 클라이언트를 열거나 닫습니다.
Twitter 또는 Discord에서 피드백을 알려주세요.