Lambda에서 구성 문제 해결

Lambda에서 구성 문제 해결

함수 구성 설정은 Lambda 함수의 전반적인 성능과 동작에 영향을 줄 수 있어요. 실제 함수 오류를 일으키진 않더라도, 예상치 못한 타임아웃과 결과를 초래할 수 있죠.

다음 주제는 Lambda 함수 구성 설정과 관련해 마주칠 수 있는 일반적인 문제에 대한 해결 조언을 제공해요.

출처: AWS Lambda 개발자 안내서

본문

Topics(주제)

메모리 구성

Lambda 함수를 128MB에서 10,240MB 사이의 메모리로 구성할 수 있어요. 기본적으로 콘솔에서 만든 모든 함수에는 가장 적은 양의 메모리가 할당돼요. 많은 Lambda 함수가 이 최저 설정에서도 성능이 좋아요. 하지만 대용량 코드 라이브러리를 가져오거나 메모리 집약적 작업을 완료한다면 128MB로는 부족해요.

함수가 예상보다 훨씬 느리게 실행된다면 첫 번째 단계는 메모리 설정을 늘리는 거예요. 메모리 바운드 함수의 경우 이렇게 하면 병목이 해결되고 함수 성능이 개선될 수 있어요.

CPU 바운드 구성

계산 집약적 작업에서 함수 성능이 예상보다 느리다면 함수가 CPU 바운드이기 때문일 수 있어요. 이 경우 함수의 계산 용량이 작업을 따라가지 못하는 거죠.

Lambda는 CPU 구성을 직접 수정하게 해주진 않지만, CPU는 메모리 설정을 통해 간접적으로 제어돼요. Lambda 서비스는 메모리를 더 많이 할당할수록 더 많은 가상 CPU를 비례적으로 할당해요. 1.8GB 메모리에서 Lambda 함수는 전체 vCPU 하나가 할당되고, 이 수준 이상에서는 둘 이상의 vCPU 코어에 접근할 수 있어요. 10,240MB에서는 6개의 vCPU를 사용할 수 있죠. 즉, 함수가 모든 메모리를 사용하지 않아도 메모리 할당을 늘리면 성능을 개선할 수 있어요.

타임아웃

Lambda 함수의 타임아웃은 1초에서 900초(15분) 사이로 설정할 수 있어요. AWS Lambda Managed Instances를 사용하는 함수의 경우 비동기식 호출과 이벤트 소스 매핑 호출(Amazon MQ와 Amazon DocumentDB 제외)은 최대 5,400초(90분) 값을 지원해요. 기본적으로 Lambda 콘솔은 이를 3초로 설정해요. 타임아웃 값은 함수가 무한정 실행되지 않도록 보장하는 안전 밸브예요. 타임아웃 값에 도달하면 Lambda가 함수 호출을 중지해요.

타임아웃 값이 함수의 평균 실행 시간에 가깝게 설정되면 함수가 예기치 않게 타임아웃될 위험이 커져요. 함수 실행 시간은 데이터 전송·처리량과 함수가 상호작용하는 서비스의 지연 시간에 따라 달라질 수 있죠. 타임아웃의 일반적인 원인은 다음과 같아요:

  • S3 버킷이나 다른 데이터 저장소에서 데이터를 다운로드할 때, 다운로드가 평균보다 크거나 오래 걸리는 경우.
  • 함수가 다른 서비스에 요청을 보내는데 그 서비스가 응답하는 데 더 오래 걸리는 경우.
  • 함수에 제공된 매개변수가 함수에서 더 복잡한 계산을 요구해 호출이 더 오래 걸리는 경우.

애플리케이션을 테스트할 때는 테스트가 데이터의 크기와 양, 현실적인 매개변수 값을 정확히 반영하는지 확인하세요. 특히 워크로드에 대해 합리적으로 기대되는 상한에 가까운 데이터 세트를 사용하세요.

또한 실용적인 범위에서 워크로드에 상한을 구현하세요. 이 예제에서 애플리케이션은 각 파일 유형에 대해 최대 크기 제한을 사용할 수 있어요. 그런 다음 최대 한도까지 포함한 다양한 예상 파일 크기 범위에서 애플리케이션 성능을 테스트할 수 있죠.

호출 간 메모리 누수

Lambda 호출의 INIT 단계에 저장된 전역 변수와 객체는 웜(warm) 호출 사이에 상태를 유지해요. 이들은 실행 환경이 처음 실행될 때("콜드 스타트")만 완전히 재설정되죠. 핸들러에 저장된 변수는 핸들러가 종료될 때 파괴돼요. 모범 사례는 INIT 단계를 사용해 데이터베이스 연결을 설정하고, 라이브러리를 로드하고, 캐시를 만들고, 변경 불가능한 자산을 로드하는 거예요.

같은 실행 환경의 여러 호출에 걸쳐 서드파티 라이브러리를 사용할 때는 서버리스 컴퓨팅 환경에서의 사용에 대해 해당 문서를 확인하세요. 일부 데이터베이스 연결 및 로깅 라이브러리는 중간 호출 결과와 다른 데이터를 저장할 수 있어요. 이로 인해 이러한 라이브러리의 메모리 사용량이 후속 웜 호출에 따라 증가할 수 있죠. 이런 경우 커스텀 코드가 변수를 올바르게 처리하고 있어도 Lambda 함수가 메모리 부족으로 실행될 수 있어요.

이 문제는 웜 실행 환경에서 발생하는 호출에 영향을 줘요. 예를 들어 다음 코드는 호출 간 메모리 누수를 만들어요. Lambda 함수는 전역 배열의 크기를 늘려 호출할 때마다 추가 메모리를 소비하죠:

let a = []

exports.handler = async (event) => {
    a.push(Array(100000).fill(1))
}

128MB 메모리로 구성된 상태에서 이 함수를 1,000번 호출하면 Lambda 함수의 Monitoring(모니터링) 탭은 메모리 누수가 발생할 때 호출, 지속 시간, 오류 수의 전형적인 변화를 보여줘요:

메모리 누수 중 호출 감소, 지속 시간 증가, 오류 수 증가를 보여주는 Lambda 콘솔 모니터링 탭.

  1. Invocations(호출) – 안정적인 트랜잭션 속도는 호출이 완료되는 데 더 오래 걸리면서 주기적으로 중단돼요. 정상 상태 동안에는 메모리 누수가 함수의 할당된 메모리를 모두 소비하지 않아요. 성능이 저하됨에 따라 운영 체제가 함수가 필요로 하는 증가하는 메모리를 수용하기 위해 로컬 저장소를 페이징하면서 완료되는 트랜잭션이 줄어들어요.
  2. Duration(지속 시간) – 함수가 메모리 부족이 되기 전에는 두 자릿수 밀리초의 꾸준한 속도로 호출을 완료해요. 페이징이 발생하면 지속 시간이 한 자릿수 더 길어져요.
  3. Error count(오류 수) – 메모리 누수가 할당된 메모리를 초과하면 결국 계산이 타임아웃을 초과하거나 실행 환경이 함수를 중지하면서 함수가 오류를 내요.

오류 후 Lambda가 실행 환경을 재시작하므로 세 그래프 모두 원래 상태로 돌아가는 걸 볼 수 있어요. CloudWatch의 지속 시간 지표를 확장하면 최소, 최대, 평균 지속 시간 통계에 대한 더 많은 세부 정보를 얻을 수 있습니다:

메모리 누수 기간 동안 최소, 최대, 평균 통계에 스파이크가 있는 CloudWatch 지속 시간 지표.

1,000번의 호출에서 생성된 오류를 찾으려면 CloudWatch Insights 쿼리 언어를 사용할 수 있어요. 다음 쿼리는 정보성 로그를 제외하고 오류만 보고해요:

fields @timestamp, @message
| sort @timestamp desc
| filter @message not like 'EXTENSION'
| filter @message not like 'Lambda Insights'
| filter @message not like 'INFO' 
| filter @message not like 'REPORT'
| filter @message not like 'END'
| filter @message not like 'START'

이 함수의 로그 그룹에 대해 실행하면 주기적인 오류의 원인이 타임아웃이었다는 것을 보여줘요:

Lambda 함수의 타임아웃 오류를 보여주는 CloudWatch Logs Insights 쿼리 결과.

후속 호출로 반환되는 비동기 결과

비동기 패턴을 사용하는 함수 코드의 경우 한 호출의 콜백 결과가 이후 호출에서 반환될 수 있어요. 이 예제는 Node.js를 사용하지만, 같은 로직은 비동기 패턴을 사용하는 다른 런타임에도 적용될 수 있어요. 함수는 JavaScript의 전통적인 콜백 구문을 사용해요. 호출 수를 추적하는 증가 카운터로 비동기 함수를 호출하죠:

let seqId = 0

exports.handler = async (event, context) => {
    console.log(`Starting: sequence Id=${++seqId}`)
    doWork(seqId, function(id) {
        console.log(`Work done: sequence Id=${id}`)
    })
}

function doWork(id, callback) {
    setTimeout(() => callback(id), 3000)
}

연속으로 여러 번 호출하면 콜백의 결과가 후속 호출에서 발생해요:

한 호출의 콜백 결과가 후속 호출에 나타나는 것을 보여주는 CloudWatch 로그.

  1. 코드가 doWork 함수를 호출하면서 마지막 매개변수로 콜백 함수를 제공해요.
  2. doWork 함수는 콜백을 호출하기 전에 어느 정도 시간이 걸려요.
  3. 함수의 로깅은 doWork 함수가 실행을 끝내기 전에 호출이 끝나는 것을 나타내요. 또한 반복을 시작한 후에는 로그에서 볼 수 있듯 이전 반복의 콜백이 처리되고 있어요.

JavaScript에서는 비동기 콜백이 이벤트 루프로 처리돼요. 다른 런타임은 동시성을 처리하기 위해 다른 메커니즘을 사용하죠. 함수의 실행 환경이 끝나면 다음 호출까지 Lambda가 환경을 동결(freeze)해요.

재개 후 JavaScript는 이벤트 루프 처리를 계속하는데, 여기에는 이전 호출의 비동기 콜백이 포함될 수 있어요. 이 맥락이 없으면 함수가 이유 없이 코드를 실행하고 임의의 데이터를 반환하는 것처럼 보일 수 있어요. 실제로는 런타임 동시성과 실행 환경이 상호작용하는 방식의 산물이에요.

이로 인해 이전 호출의 프라이빗 데이터가 후속 호출에 나타날 가능성이 생겨요. 이 동작을 방지하거나 감지하는 방법은 두 가지가 있어요. 첫째, JavaScript는 async 및 await 키워드를 제공해 비동기 개발을 단순화하고 코드 실행이 비동기 호출 완료를 기다리도록 강제해요. 위 함수는 이 방법을 사용해 다음과 같이 다시 쓸 수 있어요:

let seqId = 0
exports.handler = async (event) => {
    console.log(`Starting: sequence Id=${++seqId}`)
    const result = await doWork(seqId)
    console.log(`Work done: sequence Id=${result}`)
}

function doWork(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve(id), 4000)
  })
}

이 구문을 사용하면 비동기 함수가 끝나기 전에 핸들러가 종료되는 것을 방지해요. 이 경우 콜백이 Lambda 함수의 타임아웃보다 오래 걸리면 함수는 이후 호출에서 콜백 결과를 반환하는 대신 오류를 던져요:

await 사용 시 함수가 타임아웃되어 콜백이 이후 호출로 누출되는 것을 방지하는 CloudWatch 로그.

  1. 코드가 핸들러에서 await 키워드로 비동기 doWork 함수를 호출해요.
  2. doWork 함수는 promise를 resolve하기 전에 어느 정도 시간이 걸려요.
  3. doWork가 타임아웃 한도보다 오래 걸리므로 함수가 타임아웃되고, 콜백 결과는 이후 호출에서 반환되지 않아요.

일반적으로 코드가 종료되기 전에 코드의 백그라운드 프로세스나 콜백이 완료되도록 해야 해요. 사용 사례에서 이게 불가능하다면 식별자를 사용해 콜백이 현재 호출에 속하도록 보장할 수 있어요. 그러려면 컨텍스트 객체가 제공하는 awsRequestId를 사용하세요. 이 값을 비동기 콜백에 전달하면 전달된 값을 현재 값과 비교해 콜백이 다른 호출에서 온 것인지 감지할 수 있어요:

let currentContext

exports.handler = async (event, context) => {
    console.log(`Starting: request id=${context.awsRequestId}`)
    currentContext = context

    doWork(context.awsRequestId, function(id) {
        if (id != currentContext.awsRequestId) {
            console.info(`This callback is from another invocation.`)
        }
    })

}

function doWork(id, callback) {
    setTimeout(() => callback(id), 3000)

}

함수가 콜백이 다른 호출에서 왔을 때 감지하고 기록하는 CloudWatch 로그.

  1. Lambda 함수 핸들러는 고유 호출 요청 ID에 접근할 수 있게 하는 context 매개변수를 받아요.
  2. awsRequestId가 doWork 함수에 전달돼요. 콜백에서 이 ID는 현재 호출의 awsRequestId와 비교돼요. 값이 다르면 코드가 그에 따라 조치를 취할 수 있어요.

더 알아보기 (Learn more)

  • 메모리·CPU 바운드 구성, 타임아웃, 호출 간 메모리 누수, 비동기 콜백 결과가 후속 호출로 새는 문제와 해결법을 익혀 보세요.