1. Intro
AWS Lambda에 Spring Boot Application을 실행하는 것은 Cold Start 문제로 인해 많은 최적화가 필요했습니다.
불필요한 종속성을 줄이고, 무거운 라이브러리는 교체하는 튜닝 작업들을 진행 했었습니다.
Spring과 같은 무거운 Framework를 버리고, 다른 Framework를 사용하거나 Native 코드에 필요한 라이브러리만을 추가하여 사용하기도 했습니다.
Framework를 제거하면 초기화 시간이 단축되지만 구현을 위한 노력이 증가하여 개발 생산성은 낮아지는 단점이 있었습니다.
Cold Start 문제를 해결하고 Lambda와 Spring Boot Application을 사용하기 위한 최적화 방안에 대해서 알아보겠습니다.
2. Spring Handler 전역 변수 구성
Lambda의 호출을 받을 수 있는 handleRequest Mothod를 포함하여 LambdaHandler Class를 구성합니다.
SpringBoot을 위한 handler를 전역변수로 선언하고, 정의하는 코드는 Class의 생성자에 추가하였습니다.
handlerRequest가 아닌 생성자에서 정의하는 이유는 무엇일까요?
2.1 생성자와 handleRequest Method 실행시점
Lambda의 Cold Start 시간은 위의 그림의 INIT 단계를 의미합니다.
INIT 단계에서 Lambda 실행 환경 구성 및 런타임 초기화, 코드 초기화가 진행됩니다.
INIT 단계가 완료된 이후에는 Lambda 호출이 발생하면 INIT 단계는 제외하고 INVOKE 단계를 실행합니다.
생성자는 Java의 Class를 객체 초기화하는 작업으로 INIT 단계의 코드 초기화 부분에서 처리가 되며, 생성된 handler 객체는 메모리에 남아 있게 됩니다.
handleRequest는 Lambda의 이벤트를 처리하는 Method로 Lambda Function이 호출(invoke)되면 실행되는 영역입니다.
INVOKE 단계에서 handleRequest Method를 실행하고, 메모리에 저장된 SpringBoot을 사용할 수 있게 됩니다.
INIT 단계가 끝난 인스턴스에서는 INVOKE 단계만 실행하므로 빠른 처리가 가능합니다.
이처럼 코드를 생성자와 handleRequest Method로 분리해서 성능 최적화를 할 수 있습니다.
AWS Lambda에서 다른 프로그래밍 언어를 사용하더라도 실행시간과 목적에 맞게 코드의 위치를 결정할 필요가 있습니다.
3. SnapStart 적용
AWS Lambda의 SnapStart 기능을 사용하여 Cold Start 실행시간을 줄일 수 있는데요.
Spring Boot Application에서 사용하는 코드의 변경은 최소화하고, 초기화된 실행 환경을 스냅샷으로 생성하여 Lambda 시작 시간을 단축할 수 있게 되었습니다.
SnapStart에 대한 상세한 내용은 이전에 포스팅한 글을 참고해 주세요. (LGCNS Blog > AWS Lambda SnapStart를 사용해야 하는 이유)
AWS Lambda를 배포하기 위해서 SAM, CloudFormation, Terraform, CDK 등 다양한 도구를 사용할 수 있습니다.
여기서는 SAM을 이용한 Lambda SnapStart 적용 방법과 실행시간을 비교해 보겠습니다.
3.1 SAM 으로 배포하기
SAM(Serverless Application Model)이란 Serverless Application을 구축하기 위한 오픈소스 Framework입니다.
SAM template 파일을 이용하여 배포하는데 필요한 AWS 리소스를 정의합니다.
배포를 하게 되면 template 파일은 CloudFormation 으로 변환되어 Serverless Application 환경을 생성할 수 있습니다.
아래와 같이 SAM template 파일을 구성하여 Lambda를 정의합니다.
SnapStart.ApplyOn 항목에 PublishedVersions를 지정하고, AutoPublishAlias에 publish 한 후 사용할 이름을 지정합니다.
lambda snapstart 실행 시 Lambda 버전을 지정해야 되며, AutoPublishAlias에 지정된 이름을 버전 대신 사용할 수 있습니다.
Lambda Console 화면에서 SnapStart의 값이 PublishedVersions로 표시가 된 것을 확인할 수 있습니다.
deploy를 여러 번 할 경우 아래의 이미지처럼 버전이 생성되고, 마지막 버전에 alias로 지정한 이름으로 보이게 됩니다. (alias: SnapStart)
3.2 Lambda snapstart 실행하기
3.2.1 CLI로 실행하기
Lambda snapstart 버전을 실행하는 방법은 기존의 Lambda 실행방법과 유사합니다.
다만 실행할 버전을 지정해야 된다는 점이 다릅니다.
아래 CLI에서 마지막 부분의 ${alias-name} 부분이 추가되는 것을 확인할 수 있습니다.
3.2.2 Lambda Console에서 실행하기
Lambda Console에서도 테스트 할 수 있는데요.
Lambda Function 메인화면에 보이는 Test 메뉴는 Snapstart 미적용 버전의 테스트이기 때문에 주의해야 합니다.
Lambda SnapStart 버전 테스트는 아래의 내용을 따라서 진행합니다.
위에서 첨부했던 Versions 메뉴에서 테스트하고자 하는 버전을 클릭합니다.
아래의 그림과 같이 상단에 “Alias: SnapStart” 표시가 됩니다. 저는 Alias로 SnapStart라고 지정했습니다.
이 화면에 보이는 Test 버튼을 이용하여 테스트를 진행하면 SnapStart 버전으로 실행된 것을 확인할 수 있습니다.
3.3 실행시간 비교
3.3.1 SnapStart 미적용, Cold Start 발생된 Case
SnapStart를 적용하지 않은 경우에는 Cold Start로 인해서 새로운 인스턴스 실행 시 실행시간이 오래 걸립니다.
아래의 Log 는 Cold Start 가 발생한 경우이며, 새로운 인스턴스가 실행되어 Spring Boot이 Bootstrap 하는 Log를 확인할 수 있습니다.
테스트로 사용한 Spring 코드는 아주 간단한 구조이기 때문에 테스트 효과를 높이기 위해서 불필요하지만 무거운 Library들을 포함시켜서 구성을 했습니다.
Java 의 Library의 수가 늘어나거나 Spring의 Bean의 수가 늘어날수록 Cold Start 시간은 늘어나게 됩니다.
Cold Start로 인한 실행시간은 약 9.6초의 시간이 걸렸습니다.
Cold Start에 발생하는 ‘Init Duration’ 시간을 Log 마지막 부분에서 확인할 수 있습니다.
3.3.2 SnapStart 미적용, Warm Start 발생된 Case
Warm Start 실행 조건은 기존에 실행이 되었던 Lambda 인스턴스가 있고, idle 상태일 때입니다.
Lambda는 실행 이후 대략 5분 정도의 idle time을 갖고 있고, 그 이후에 호출이 없을 경우에는 해당 인스턴스를 소멸합니다.
Lambda는 한 번에 한 개의 요청만을 처리합니다.
더 많은 요청이 들어온다면 새로운 Lambda 인스턴스가 실행이 되어야 하며 Cold Start가 발생합니다.
Warm Start 가 된 경우에는 8ms로 아주 빠르게 실행되는 것을 확인할 수 있습니다.
3.3.3 SnapStart 적용, Restore 발생된 Case
SnapStart 기능이 활성화된 경우에는 Restore Duration이 포함된 것을 확인할 수 있습니다.
Restore 단계는 SnapStart의 Cold Start 영역으로 스냅샷을 이용하여 초기 상태로 복원하는 과정입니다.
Restore 단계에서 약 754ms가 소요되고, invoke 단계에서 약 106ms 시간이 소요되었습니다.
3.3.4 SnapStart 적용, Invoke만 실행된 Case
Restore 단계를 거친 인스턴스는 이후에 invoke 단계만을 실행하게 됩니다.
아래의 로그는 invoke 단계의 실행 Case로 약 26ms 소요되었습니다.
4. Hook 을 이용한 Init 시간 줄이기
Lambda에서는 SnapStart를 사용하는 경우에 beforeCheckpoint, afterRestore 2가지 런타임 Hook을 제공합니다.
이 기능은 OpenSource CRaC Project의 일부입니다.
4.1 beforeCheckpoint
beforeCheckpoint는 스냅샷을 생성하기 이전인 코드 초기화 단계에서 실행되는 hook입니다.
Timeout 시간은 코드 초기화 Timeout인 최대 15분에 포함됩니다.
코드 초기화 단계에 수행할 코드들을 추가하여 SnapStart Cold Start 시간을 줄일 수 있습니다.
앞에서 Spring Handler 전역 변수 구성한 것과 비슷한 효과를 볼 수 있습니다.
beforeCheckpoint hook을 적용하기 위한 예를 들면 아래와 같습니다.
DB 조회한 대량의 데이터를 메모리에 Caching 후 사용하는 경우에 beforeCheckpoint Method를 통해 객체를 생성해서 데이터를 담고 나중에 호출할 수 있는 형태로 구성할 수 있습니다.
단, 스냅샷에 저장되는 데이터이기 때문에 호출 시점에는 최신 데이터가 아닌 오래된 데이터를 참조할 가능성이 있습니다.
변경이 빈번하지 않은 데이터를 사용하거나, 변경될 경우 Refresh를 하기 위한 별도의 절차가 필요합니다.
4.2 afterRestore
afterRestore는 스냅샷을 Restore 한 후에 실행되는 hook입니다.
SnapStart Cold Start 가 발생한 경우에 한하여 실행이 됩니다.
afterRestore는 10초의 Timeout이 있습니다.
스냅샷 기반의 인스턴스 생성시 초기에 한 번 수행이 필요한 것들를 넣을 수 있는데요.
DB의 Connection Pool 생성과 같이 invoke 단계 이전에 수행해야 되지만, 스냅샷보다는 최신의 상태를 유지해야 되는 경우에 사용할 수 있습니다.
afterRestore hook은 Cold Start 시간을 늘어나게 하므로 사용 시 주의가 필요합니다.
4.3 적용예제
Hook을 사용하기 위해서 org.crac dependency를 추가해야 합니다.
아래의 코드는 maven을 사용하는 예제이며, pom.xml에 추가합니다.
다음의 예제 소스에서 beforeCheckpoint()와 afterRestore() 코드를 볼 수 있습니다.
Lambda Handler Class의 static 영역에서 globalContext에 Hook resource를 등록합니다.
5. outro
Lambda에서 Spring Boot Application 실행 시 제약사항이었던 Cold Start를 극복하기 위한 몇 가지 방법에 대해서 알아봤는데요.
Lambda의 새로운 기능인 Sanpstart의 뛰어난 성능도 함께 확인할 수 있었습니다.
이렇게 SnapStart 기능과 Hook 기능을 이용하여 Java 기반인 Spring Boot도 부담 없이 사용할 수 있게 되었습니다.
Spring의 편리함에 Lambda의 저렴한 비용과 성능이 합쳐진 더욱 강력한 Serverless를 경험해 보세요.