업무를 하다보니 파일을 읽어들이고 데이터를 생성하는 Lambda가 필요해졌다. S3에서 파일을 가져오는 방식으로 설계했지만 실패하고 결국 파일 시스템을 사용하게 됐다. 그 과정에서 생긴 이슈와 내가 놓친 부분들을 보려고 한다.

 

Lambda와 S3 네트워크

 

나는 vpc endpoint gateway 유형을 사용하고 있었기에 Lambda에서 S3를 호출할 경우 Public 네트워크가 아닌 내부 네트워크를 이용하게 된다. 나는 이점에서 빠른 데이터를 다운로드 받을 수 있다고 생각했다. 하지만 난 틀렸다. 실제로 S3는 100Gbps를 지원한다. 내가 간과한 것은 Lambda의 네트워크 대역폭이었다. 실제로 AWS 측에서 세부적으로 밝힌 것은 없지만, 커뮤니티들을 살펴봤을 때 Lambda의 메모리를 올렸을 때 어느정도 네트워크 대역폭이 증가하지만 일정이상 증가하지 않는다는 것을 유저들이 밴치마크한 기록이 있다. 

 

따라서 Lambda의 네트워크 대역폭으로 인해서 S3를 이용했을 때 API 한 개의 요청에 110초 정도가 소요돼서 API Gateway 요청이 실패됐다. ㅋㅋㅋ 실패한 설계였다. 그래서 파일 시스템을 변경 후 평균 3초의 응답을 가지게 됐다.

 

Lambda와 파일 시스템

 

Lambda에 바로 마운트 가능한 파일 시스템은 대표적으로 EFS가 있다. 단지 EFS로 했을 때 약간의 INIT 시간이 증가할 수 있다. EFS에 마운트하는 데 시간이 조금 소요되기 때문인 것으로 추측되는데, 유의미한 차이는 아니라고 느꼈다. 실제로 단순한 rust 이미지로 올렸을 때 INIT 시간은 10ms 정도 차이밖에 발생 안 했다. 그래서 S3와 파일시스템으로 파일을 가공하기 전까지 했을 때 시간 차이가 얼마나 발생하는 지 테스트를 해보려고 한다.

 

S3 vs EFS  파일 접근 성능 테스트

 

S3에 5GB의 파일을 생성해뒀다.

 

1. Lambda는 S3에서 파일을 다운로드 하는 시간

2. EFS를 마운트한 Lambda가 초기화되는 시간

 

비교할 예정이다.

 

S3의 Lambda 코드는 아래와 같다.

use aws_sdk_s3::Client;
use lambda_runtime::{run, service_fn, Error as LambdaError, LambdaEvent};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[derive(Deserialize, Serialize)]
struct S3Object {
    bucket: String,
    key: String,
}
async fn function_handler(event: LambdaEvent<S3Object>) -> Result<Value, LambdaError> {
    let config = aws_config::load_from_env().await;
    let client = Client::new(&config);
    let s3_event = event.payload;
    // S3에서 객체 다운로드
    let output = client.download_object()
        .bucket(&s3_event.bucket)
        .key(&s3_event.key)
        .into_file("/tmp/downloaded_file")
        .send()
        .await
        .map_err(|e| LambdaError::from(format!("Failed to download object: {:?}", e)))?;
    let byte_count = output.body_length();
    Ok(json!({
        "result": 1,
        "message": "Download completed successfully",
        "bytes_downloaded": byte_count
    }))
}
#[tokio::main]
async fn main() -> Result<(), LambdaError> {
    run(service_fn(function_handler)).await
}

 

단순히 S3의 파일을 다운로드하고 시간을 측정하는 rust 코드다.

 

EFS 마운트하는 Lambda는 초기화 시간만 보면 되니까 별다른 코드가 있지는 않다.

use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::json;
use std::path::Path;

async fn function_handler(event: LambdaEvent<serde_json::Value>) -> Result<serde_json::Value, Error> {
    let efs_mount_path = Path::new("/mnt/test");

    let is_mounted = efs_mount_path.exists() && efs_mount_path.is_dir();

    let result = if is_mounted { 1 } else { 0 };

    Ok(json!({ "result": result }))
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(function_handler)).await
}

 

이제 Cold Start를 진행시키기 위해서 동시에 여러번의 요청을 보냈다.

 

아래의 값은 데이터를 가공하기위해 파일에 접근할 수 있는 직전까지의 시간을 측정한다. Lambda의 메모리는 3008MB로 설정했다.

 

S3 + Lambda -> S3에서 객체 다운 완료된 순간

EFS + Lambda -> 람다 실행이 완료된 순간

 

결과는 아래와 같았다. 

 

Lambda 유형 평균 (Avg)ms 최고 (Best)ms 최악 (Worst)ms 중간값 (Middle)ms
S3 + Lambda 70682.39 68542.12 73291.44 70021.42
EFS + Lambda 48 44 57 51

 

데이터를 바이트 형태로 다운로드 받다보니 메모리가 영향이 있겠지만, 결론적으로 대용량의 파일을 가공하는 것은 파일시스템을 사용하는 게 좋은 성능을 발휘할 수 있다.

 

결론

 

Lambda에서 대용량 파일을 가공한다면, 성능은 EFS이고 비용 측면에서는 S3가 유리하다.

 

사용한 아키텍처의 Terraform는 Github에서 확인할 수 있다.