aiohttp signedURL 테스트 및 미반영 결론 도출 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 테스트 세팅 변경 사항

  • aiohttp + signedURL
    • signedURL을 k6 서버에서 사전 생성
    • k6 서버에서 signedURL을 이용하여 FastAPI 서버에 요청 전송
    • FastAPI 서버에서 signedURL을 aiohttp를 이용해 비동기 read

a. VM 인프라 구성

역할 인스턴스 스펙 설명
FastAPI 서버 n2d-standard-4 (vCPU 4개, 메모리 16GB) GCS에서 이미지 30~50개 다운로드 처리
K6 서버 e2-medium (vCPU 2개, 메모리 4GB) 테스트 트래픽 생성기

b. FastAPI 서버 코드 요약

from fastapi import FastAPI, Query
from typing import List
from fastapi.responses import JSONResponse
import aiohttp
import asyncio

app = FastAPI()

print("서버 준비 완료")

@app.get("/")
async def ping():
    return {"message": "FastAPI is running"}

@app.post("/load-images")
async def load_images(signed_urls: List[str]):
    """
    클라이언트가 전달한 signed URL들을 통해 이미지 다운로드 요청.
    FastAPI는 다운로드만 수행하고 signed URL은 클라이언트가 미리 발급받음.
    """

    async def fetch(session, url: str):
        async with session.get(url) as response:
            _ = await response.read()
            return url

    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in signed_urls]
        result = await asyncio.gather(*tasks)

    return JSONResponse(
        status_code=200,
        content={"message": f"{len(result)}장 이미지 로딩 완료"}
    )                                                                     
  • 요청당 30~50장의 이미지 다운로드
  • 실패 시 즉시 500 응답 반환

c. K6 부하 테스트 스크립트

import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';

export const options = {
  vus: 30,
  duration: '10m',
};

// JSON 객체 리스트에서 url만 추출
const signedUrls = new SharedArray('signedUrls', function () {
  const raw = JSON.parse(open('./signed_urls_100.json'));
  return raw.map(item => item.url);  // "url"만 추출해서 사용
});

function getRandomUrlList() {
  const total = Math.floor(Math.random() * 21) + 30;
  const copy = Array.from(signedUrls);                // 복제
  const shuffled = copy.sort(() => 0.5 - Math.random()); //  복제본 정렬
  return shuffled.slice(0, total);
}

export default function () {
  const urls = getRandomUrlList();
  const payload = JSON.stringify(urls);  // 서버가 요구하는 구조
  const headers = { 'Content-Type': 'application/json' };

  const res = http.post('http://10.178.0.2:8000/load-images', payload, { headers });

  check(res, {
    'status is 200': (r) => r.status === 200,
  });

  sleep(1);
}

2. 테스트 결과 요약

  • 첨부 파일:

    image

  • 평균 응답 시간(P95): 약 2초 후반

  • 요청 성공률: 요청 실패 발생

    지표 수치
    P95 응답 시간 2초 후반

3. 테스트 시도 이유

  • 기존 방식: download_byte (S3 SDK)
  • 기존 방식의 단점
    • download_byte는 동기 함수: i/o 차단 발생
    • 이로 인해 네트워크 요청/응답 처리 시간이 길어질 수 있음
  • 개선 시도: signed URL + aiohttp (비동기 방식)
  • 기대 효과
    • S3 객체 접근용 signed URL을 서버에서 미리 발급
    • 클라이언트는 aiohttp를 이용해 비동기적으로 read 처리
    • read()는 asyncio 기반 → I/O 블로킹 최소화
    • 요청 시 인증 헤더 없이 URL에 서명 포함전송 요청이 더 가벼움

4. 병목 원인 분석

  • 각 signed URL 요청마다 개별 HTTP 커넥션이 생성됨
  • 따라서 매 요청마다 TCP 3-way handshake + TLS handshake
  • 커넥션 생성 비용이 누적되어 응답 시간 증가

5. 결론

항목 평가
signed URL + aiohttp 비동기 처리와 요청 경량화는 장점이지만, 커넥션 재사용 불가로 오히려 지연 발생
download_byte 동기 방식이지만, persistent connection 유지 덕분에 안정적인 성능 확보

6. 부하테스트 회고

  • 단순히 비동기 처리가 항상 빠르지 않다
  • 커넥션 수립 비용, TLS 핸드셰이크, 연결 재사용 여부가 중요한 변수
  • 단순히 "비동기"라는 개념 자체만을 근거로 기술을 도입하기보다는, P95, P99와 같은 지연 시간 지표를 기반으로 실제 성능을 분석하고, 그 결과를 바탕으로 기술 선택을 결정하는 것이 중요하다.