...
알다시피, 람다함수가 들어있는 서버리스의 항상 활성화 되어 있지 않다.
그래서 사용자의 요청이 들어왔을때 람다함수를 실행하기 위한 부수적인 준비 세팅이 필요하고 이로인해 대략 수초~수십초 정도 코드 딜레이가 발생되게 된다.
바로 이 상태를 Cold Start 라고 불리우며, 이 때문에 람다의 최대 단점인 상대적으로 느린 response를 제공받게 되는 것이다.
그럼 이 콜드 스타트를 최소화하고 어떤 방식으로 함수 로직을 구현해야 빠르고 최적화된 코드실행을 할수 있을까?
지금 부터 람다 성능을 높이는 전략에 대해 알아보자.
Lambda 메모리 늘리기
Lambda의 사양을 올리는 가장 확실하면서 간단한 방법은 해당 Lambda에 적용되는 메모리의 양을 늘리는 것이다.
메모리를 올리면 인스턴스의 사양이 올라가 처리 속도 자체가 빨라질 수 있다고 한다. 그렇기 때문인지, 현업에서 Cold start문제가 생겼을 때, 메모리를 올리는 것으로 대응해서 해결된 사례가 꽤 나오는 편이다.
메모리를 올린다는 것은 비용이 증가하는 것을 의미하니 잘 고려해야 한다.
그렇지만 Lambda의 과금 방식은 (메모리 별 가격) * (요청이 처리되는 시간) * (요청 수) 이므로, 메모리를 늘림으로 인해 요청이 처리되는 시간이 줄어든다면 오히려 최종 비용이 떨어질 수도 있다. 왜냐하면 람다는 메모리를 늘릴수록 CPU도 같이 성능이 향상되는 구조를 갖고 있기 떄문이다. (단 서비스의 특성마다 상이하다)
따라서, 비용 계산은 각자 상황에 맞게 계산기를 잘 두드려봐야한다.
람다 메모리 설정은, 구성 메뉴에가서 편집을 누르면 간단히 설정 할 수 있다. (최대 10기가)
람다 컨테이너 재사용하기
AWS 람다는 한번 호출되고, 다음 호출이 5분 이내인 경우 그 후속 호출에 람다 컨테이너를 재사용 하는 특징을 가지고 있다.
이러한 특징을 이용해 편법을 사용해 람다 성능을 늘릴수 있다.
즉, 람다를 요청(request)이 없어도 5분마다 강제로 실행하도록 설정 하면 Cold Start를 해결할 수 있는 것이다.
예를들어 ColudWatch를 통해 람다를 5분마다 한번씩 지속적으로 호출을 하거나, Route 53에서 Health Check를 람다 함수 API Gateway Endpoint 로도 설정하는 식으로 다양하게 설정이 가능하다.
하지만 5분마다 함수를 계속 실행한다는 것은 쓸데없는 지출이 늘어난다는 점이다.
프리티어를 고려하지 않고 메모리 1024로 람다가 약 10초간 실행되는 함수를 10개 Warm 상태를 유지하는경우 대략적인 비용은 아래와 같다고 한다.
- 함수 호출 비용 = 0.18$
- 함수에 사용한 컴퓨팅 비용= 14.4$
- 합계 14.58$
AWS Lambda 동작 원리
람다 컨테이너 재사용이란 것이 대체 무슨 뜻이며, 어떤식으로 콜드 스타트를 해결하여 람다 성능을 향상 시킬 수 있는지 차근차근 알아보자.
람다 핸들러의 실행 순서
앞서 람다함수 코드의 동작 과정을 살펴보도록 하자.
람다함수 코드의 상세한 동작 원리를 알게되면, 추후에 람다 성능을 최적화하는데 도움이 된다.
Lambda 핸들러는 크게 아래의 그림처럼 크게 4단계를 거쳐서 실행된다.
- Download your code : Lambda 핸들러가 실행될 AWS의 내부 인스턴스에서 개발자가 작성하고 업로드한 코드를 다운로드하는 단계
- Start new execution environment : 새로운 execution environment를 생성하는 단계. (execution environment란 Lambda 핸들러가 실행될 환경으로써, 메모리, 런타임 등을 구성)
- Execute initialization code : Lambda 핸들러 바깥의 전역 코드를 실행하는 단계
- Execute handler code : Lambda 핸들러의 내부 함수 코드를 실행하는 단계
위 과정을 실제 요청 그림으로 그려보면 다음과 같다.
파란 점선 안의 과정(1,2,3번)이 바로 Cold Start 과정이 되는 것이다. 그리고 4번의 과정을 Warm Start 과정이 된다.
Cold Start : 람다를 호출 할때 배포 패키지의 크기와 코드 실행 시간 및 코드의 초기화 시간에 따라, 새 실행 환경으로 호출을 라우팅할 때 발생하는 지연 시간
Warm Start : 이미 실행준비가 완료된 상태. 바로 코드를 실행
백번 이론만 봐봤자 한번 실행하는 것보다 못하다.
정말로 람다가 위와 같이 실행되는지 확인해 보자.
먼저 람다의 index.js의 코드를 다음과 같이 적어준다.
Execute initialization code 부분(전역)에 isWarmStart 라는 전역변수를 선언하고, handler 함수가 한번 실행하면 isWarmStart 의 상태값을 true로 바꾸는 로직이다.
let isWarmStart = false;
exports.handler = async () => {
let message;
if (isWarmStart) {
message = "warm start";
} else {
message = "cold start";
isWarmStart = true;
}
return {
statusCode: 200,
body: JSON.stringify({ message }),
};
};
Test 버튼을 눌러 테스트 트리거를 실행해보자.
isWarmStart 가 false이니 message로 'cold start'가 출력되었으며 동시에 isWarmStart 값이 true로 업데이트 되게 된다.
다시 한번 Test 버튼을 눌러보자.
이번엔 message가 'warm start' 가 되었다.
이로써, 정말로 위의 그림처럼 람다 함수를 빈번히 실행시키면 Execute initialization code 부분이 스킾(전역 값이 유지)이 되며 곧바로 Execute handler code 부분이 실행 된다는 점을 실제로 확인하였다.
람다 연속 호출을 그림으로 표현하자면 다음과 같이 된다.
위의 그림에서 연속으로 람다 함수를 호출하면, 람다 컨테이너가 계속 유지되서 곧바로 warm start가 되어 빠르게 코드를 실행되는 모습을 확인 할 수 있다. (콜드 스타트 문제를 해결한 것이다!)
한번 5분이 지나고 다시 람다 Test를 실행해보자.
그러면 다시 message가 cold start 로 출력될 것이다. (람다 컨테이너 초기화 = cold start)
전역 초기화 코드의 사용
이로써 람다 함수를 빈번하게 호출하면 Execute handler code 부분만 실행되서 warm start를 유지한다는 것을 배웠다.
위의 과정에서 핵심은 시간이 오래걸리는 코드는 전역으로 뺀다는 점이다.
예를들어 데이터베이스 연결 같은 코드는 매 함수 호출될때마다 실행하는 것보다 아예 전역에서 한번 커넥션을 하고 그 정보를 변수에 저장해서 이용하는 것이 당연히 효과적이기 때문이다.
직접 코드로 예시를 보면서 이해해보자. (파이썬 코드)
먼저 람다에는 이벤트를 처리하는 함수 코드의 메서드인 핸들러(handler)함수가 존재한다.
이 핸들러 함수에 코드를 추가하면 매번 실행을 할 때마다 함수에 있는 코드를 실행하게 된다.
아래 예시 코드에서는 DB와 연결하기 위해 DB를 연결하는 초기화 작업을 한다고 가정한다.
import json
import time
def db_connection():
time.sleep(2)
def lambda_handler(event, context):
db_connction()
return "handler_outside_initialize_test"
해당 함수를 3번 실행시키고 로그를 확인해보자.
연결하는데 시간이 2초 걸린다고 가정하면 함수는 실행할 때마다 매번 2초의 초기화 코드를 실행해야 된다.
Duration 값을 잘 살펴보면, 매번 Function 실행때 마자 약 2000ms(2초)가 걸린다는 것을 확인 할 수 있다.
이번에는 db_connection 함수를 핸들러 함수 밖(전역)으로 내빼고 실행을 해보자.
import json
import time
def db_connection():
time.sleep(2)
db_connction() # 전역 코드
def lambda_handler(event, context):
return "handler_outside_initialize_test"
이번에도 3번 함수를 실행하고 전의 로그와 어떤점이 다른지 비교해 보자.
핸들러 함수 밖에서 db_connection함수를 실행하면 함수를 여러번 실행했을 때 db_connection함수는 초기화되어 있는 상태에서 핸들러 안에 있는 함수만 실행하게 된다.
Duration 값을 보면 처음에는 거의 2초(Init Duration: 2113.04 ms) 정도가 걸렸지만, 그 후의 함수 호출은 1초 미만으로 매우 빠른시간으로 함수가 실행됨을 확인 할 수 있다.
이렇게 되는 큰 이유는 db_connection 함수를 핸들러 함수 밖에서 실행함으로써 첫 실행에서 초기화를 하기 때문이다.
그래서 그 다음부터는 db_connection 함수의 실행을 하지 않고 핸들러 함수의 실행만 하기 때문에 빠른 시간으로 함수를 실행할 수 있게 되는 것이다.
이로써 람다 컨테이너 재사용 원리를 총정리하자면,
첫째로 시간이 오래걸리는 코드는 전역으로 빼야 하며,
둘째는 5분마다 빈번하게 호출하여 람다 컨테이너가 죽지 않도록 유지하게 하여 Cold Start 부분을 스킾하고 Warm Start를 유지하게 하는 것이다.
하지만 앞서 언급했듯이 이러한 편법은 역시 비용이 문제이다.
고객이 서비스를 이용하지도 않는데 지속적으로 람다를 호출하는 것은 아무리 봐도 '낭비' 로 보여질수 밖에 없다.
또한 빈번히 람다를 호출한다고 해도, 컨테이너가 계속 유지한다는 보장도 없다.
따라서 아마존에서는 아예 프로비저닝된 동시성(Provisioned Concurrency) 이라는 기능을 제공해준다.
프로비저닝된 동시성은 간단히 말하면, 위와 같은 편법을 쓰지말고 아예 람다 컨테이너를 쭉 유지시켜주는 방식이다.
프로비저닝된 동시성 기능 활성
서버리스의 Cold start는 결국 꺼져있는 상태 인것이 문제이다.
그러면 그냥 EC2 처럼 람다 함수를 쭉 켜두면 되지 않을까 에서부터 시작되어 추가된 기능이 바로 프로비저닝된 동시성 이다.
프로비저닝된 동시성(Provisioned Concurrency)은 동시에 몇 개의 요청에 cold start 없이 즉시 응답할 수 있도록 할지 설정해두는 방식이다.
프로비저닝된 동시성을 활성화하면, 설정한 수 만큼 미리 인스턴스를 초기화 해두고 대기시켜 둔다. 따라서 람다 실행에 있어 항상 warm start가 되게 된다.
즉, 우리가 cold start를 최소화하기 위해서 사용할 수 있는 권장 솔루션이 바로 프로비저닝된 동시성 인 것이다.
그러나 이 역시 항상 인스턴스를 켜놓고 있으니, 요청이 없더라도 요금이 발생하게 된다.
1,536MB 의 메모리 용량으로 동시성 100을 8시간동안 구성했을 때 18달러가 과금 되는 것을 알 수 있다.
한달 내내 켜놓았을때 54달러 * 30일 = 1,620 달러가 과금된다.
그다지 비싸다고 느끼지 않을 수도 있지만 EC2의 m6g.12xlarge 인스턴스를 한달 켜놨을때와 거의 동일한 금액이다.
여담으로, 동시성 최대 할당량은 제한되어 있는데, 할당량의 기본값은 1000개로 정해져 있다.
서비스 특성상 1000개도 부족하다면 아마존 본사에 직접 요청해서 늘릴수도 있다.
프로비저닝된 동시성 활성화 하기
AWS Lamda에서 Provisioned Concurrency 활성화 하는 방법은 아래 포스팅을 참고하길 바란다.
프로비저닝 오토스케일
사실은 이 방법이 람다 성능을 올리는 가장 정석적인 솔루션이다.
프로비저닝된 동시성을 미리 예약하고 켜두자니 고정 지출이 발생하기에, 아예 프로비저닝을 트래픽이 급증하는 시간대에 맞춰 프로비저닝을 오토스케일 할 수 있다.
이렇게 하면 하루종일 동시성을 동일하게 유지하는것보다는 훨씬 저렴한 가격이 된다.
다만 이 단계 서부터 설정 난이도가 전문 수준으로 뛰어올라 포스팅에서 다루기에는 까다로워 대신 공식 링크를 걸어놓아 본다.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.