Skip to content

LeeHeoYoon: 파일 업로드-다운로드 기능 구현#6

Open
this-is-spear wants to merge 17 commits into
Learning-Is-Vital-In-Development:masterfrom
this-is-spear:feature/thymeleaf-ui
Open

LeeHeoYoon: 파일 업로드-다운로드 기능 구현#6
this-is-spear wants to merge 17 commits into
Learning-Is-Vital-In-Development:masterfrom
this-is-spear:feature/thymeleaf-ui

Conversation

@this-is-spear

@this-is-spear this-is-spear commented Feb 20, 2026

Copy link
Copy Markdown
Member

Summary

Spring Boot 기반 파일/폴더 관리 REST API 서비스. Nginx와 Spring Boot의 역할 분리를 통해 파일 I/O 성능을 단계적으로 최적화했습니다.


최적화 결과 종합

다운로드 성능

지표 초기 (Spring 직접 스트리밍) 최종 (sendfile + 동적 속도 제어) 총 개선
throughput avg 226 MB/s
throughput peak 1,110 MB/s
download duration 55ms (10MB 파일)
에러율 0% 0%

업로드 성능 (10MB 파일)

지표 초기 (Multipart) 최종 (Nginx Direct-to-Disk) 개선
응답시간 avg 154ms 43ms -72%
응답시간 p(95) 234ms 102ms -56%
처리량 (req/s) 6.55 13.97 +113%

네트워크 전송량 (HTML 응답)

엔드포인트 원본 gzip 압축 절감률
/ui (HTML) 21,166 B 5,469 B 74%

단계별 최적화 과정

1단계: X-Accel-Redirect 도입

변경: Spring Boot가 파일 바이트를 직접 스트리밍 → Nginx sendfile() zero-copy 전송으로 전환

원리: sendfile()은 디스크 → 커널 → 네트워크를 앱 메모리를 거치지 않고 직접 전달합니다 (CPU 복사 0회). Spring Boot는 인증과 헤더만 처리하고, 실제 파일 전송은 Nginx에 위임합니다.

변경 전: 디스크 → [커널] → [Spring 메모리] → [커널] → 네트워크 (복사 4회)
변경 후: 디스크 → [커널] → 네트워크 (복사 2회, zero-copy)

2단계: 대역폭 제한 및 gzip 압축

변경: limit_rate 10m 대역폭 제한 + gzip 압축 최적화

원리: 파일 다운로드는 gzip off + sendfile on으로 zero-copy를 유지하면서, API/HTML 응답은 gzip level 6으로 압축하여 전송량 74% 절감.

3단계: Nginx Direct-to-Disk 2-Stage Upload

변경: 클라이언트 → Spring Multipart → Nginx Direct-to-Disk 전환

원리: Nginx가 client_body_in_file_only on으로 파일 바이트를 JVM을 거치지 않고 디스크에 직접 기록합니다. Spring Boot는 메타데이터 검증과 ATOMIC_MOVE만 수행합니다.

변경 전: 클라이언트 → Nginx → Spring (JVM heap에 파일 적재) → 디스크
변경 후: 클라이언트 → Nginx → 디스크 직접 기록 (JVM 바이패스)

결과: 응답시간 72% 감소, 처리량 113% 증가

4단계: X-Accel-Limit-Rate 동적 속도 제어

변경: 고정 limit_rate 10m → 파일 크기 기반 동적 제어 (128MB/s 상한)

원리: Spring Boot가 X-Accel-Limit-Rate 헤더로 요청별 다운로드 속도를 nginx에 전달합니다. 100MB 미만 파일은 무제한(sendfile zero-copy 최대 활용), 100MB 이상은 128MB/s로 제한합니다.

// FileController.java
private String calculateRate(Long fileSize) {
    if (fileSize < 100MB) return "0";          // 무제한
    return String.valueOf(128 * 1024 * 1024);   // 128MB/s
}

결과: throughput 16.7x 개선 (13.5 → 226 MB/s), 다운로드 시간 13.2x 단축


기술 스택

  • Java 21 + Spring Boot 3.4.2 (Virtual Threads, ZGC)
  • Spring Data JPA + PostgreSQL 16 (ltree 확장)
  • Nginx (reverse proxy, X-Accel-Redirect, sendfile, direct-to-disk upload)
  • Docker Compose 배포
  • Thymeleaf + Bootstrap 5 데모 UI
  • k6 부하 테스트

주요 설계 결정

  • 파일 다운로드: gzip off + sendfile on → zero-copy 유지 (바이너리 파일에 gzip은 효과 0~2%이면서 zero-copy를 깨뜨림)
  • io_uring 미도입: 현재 병목이 I/O 시스템 콜(2~5μs)이 아닌 대역폭 제한(100,000μs/MB)에 있어 효과 없음
  • gzip 압축 저장 미도입: 사용자 파일의 80%+가 이미 압축된 포맷이라 디스크 절감 ~7%, Range Request 불가 등 부작용이 더 큼

실행 방법

cd docker
cp .env.example .env
docker-compose up -d

Test plan

  • k6 부하 테스트 (gzip vs plain 비교)
  • k6 부하 테스트 (Nginx Direct Upload vs Multipart 비교)
  • k6 부하 테스트 (X-Accel-Limit-Rate 동적 속도 제어 before/after)
  • curl로 실제 전송 크기 검증
  • Nginx Direct Upload 단일 E2E 검증
  • 모든 엔드포인트 정상 응답 확인 (에러율 0%)

🤖 Generated with Claude Code

this-is-spear and others added 3 commits February 20, 2026 11:40
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Spring Boot 기반 파일 스토리지 서비스 구현
- 파일 업로드/다운로드/삭제/복사/이동 API
- 폴더 생성/조회/이름변경/삭제/이동 API
- PostgreSQL + ltree 기반 폴더 계층 구조
- Nginx X-Accel-Redirect 기반 파일 다운로드
- Docker Compose 배포 환경 구성

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
브라우저에서 파일/폴더 API를 테스트할 수 있는 데모 UI 추가
- Thymeleaf + Bootstrap 5 기반 단일 페이지 파일 탐색기
- StorageUiController (GET /ui, GET /ui/download/{fileId})
- 폴더 CRUD (생성/이름변경/삭제/이동) + 파일 CRUD (업로드/다운로드/삭제/이동/복사/메타데이터)
- Nginx /ui 프록시 설정 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
this-is-spear and others added 6 commits February 22, 2026 03:44
- Fix PropertyReferenceException by separating Sort for Folder (name)
  and FileMetadata (originalName) in FolderService.listContents
- Fix ltree type casting (varchar->ltree) by adding stringtype=unspecified
  to JDBC URL and implicit cast in init.sql
- Fix TransactionRequiredException in async upload status update by
  wrapping @Modifying query call with TransactionTemplate
- Fix ltree subpath() "invalid positions" error in folder move queries
  by adding CASE expression for exact match vs descendant paths
- Add MissingServletRequestPartException and MultipartException handlers
  to return 400 instead of 500
- Add duplicate filename check in copyFile() at service level

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hibernate binds String parameters as varchar, not text. The existing
text→ltree cast didn't cover this, causing INSERT failures on the
folder_path column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change storage.base-path default from /data/storage to ./storage-data
so the app works without root permissions on local machines. Docker
still overrides via STORAGE_BASE_PATH env var.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
StorageUiController now delegates file serving to Nginx via
X-Accel-Redirect header, matching the API download pattern.
This eliminates App memory/CPU overhead for file transfers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Raise client_max_body_size and Spring multipart limits to 300MB
- Add per-client bandwidth limit of 10MB/s for API uploads
- Add per-client download bandwidth limit of 10MB/s (after first 1MB)
- Add descriptive comments for limit settings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enhance gzip settings (level 6, min 256B, more types, vary/proxied)
- Remove proxy_buffering off to enable gzip on proxied responses
- Disable gzip for binary file downloads (X-Accel-Redirect)
- Change app port binding to expose-only (internal access via nginx)
- Make nginx port configurable via NGINX_PORT env var
- Add k6 compression test script comparing gzip vs plain performance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@this-is-spear

Copy link
Copy Markdown
Member Author

파일 업로드 최적화 분석

상황 (As-Is)

현재 파일 업로드 시 데이터가 3단계로 버퍼링됩니다:

Client
  → Nginx (tmpfs /tmp/nginx-body에 쓰기 ①, 512MB RAM 점유)
  → Nginx가 다시 읽어서 loopback TCP로 Spring Boot에 전송
  → Spring Boot multipart resolver (디스크 /data/storage/.tmp에 쓰기 ②)
  → transferTo(File) → renameTo() (zero-copy ✓)
  → moveFromTemp() → ATOMIC_MOVE (zero-copy ✓)

Spring Boot 내부의 transferTo(File)moveFromTemp()는 같은 Docker 볼륨(storage-data) 위에서 renameTo() / ATOMIC_MOVE로 이미 zero-copy rename 최적화가 되어 있습니다.

문제 (Problem)

# 문제 영향
1 Nginx tmpfs 512MB RAM 소비 대용량 파일 동시 업로드 시 메모리 부족 위험. 300MB 파일 2개면 tmpfs 초과
2 Nginx → Spring Boot 네트워크 재전송 같은 머신 내에서 불필요한 loopback TCP. 모든 바이트가 JVM을 통과하며 Tomcat 커넥션/스레드를 업로드 전체 시간(300MB@10MB/s = ~30초) 동안 점유
3 소파일도 무조건 디스크 경유 file-size-threshold: 10KB로 거의 모든 파일이 multipart temp 파일로 디스크에 쓰여짐

해결 방법별 비교

Option A: nginx-upload-module (서드파티 Nginx 모듈)

방식:

1. Client → Spring Boot: 메타데이터만 전송 → PENDING 저장 + uploadToken 발급
2. Client → Nginx(upload-module): 파일 전송 → Nginx가 디스크에 직접 저장
3. Nginx → Spring Boot: 콜백 (파일경로, MD5, 크기 자동 전달) → ATOMIC_MOVE + COMPLETED

결과:

항목 Before After
디스크 쓰기 3회 (tmpfs + multipart + temp) 1회 (Nginx → 디스크)
JVM 파일 I/O 전체 바이트 통과 0 바이트 (경로만 수신)
RAM 사용 512MB tmpfs 0 (직접 디스크)
Tomcat 점유 업로드 전체 시간 콜백 처리만 (~ms)
MD5 검증 없음 자동 제공
동시 300MB 업로드 ~2개 (tmpfs 한계) 디스크 용량만큼
장점 단점
완전한 JVM 분리, 최고 성능 nginx:alpine 사용 불가, 커스텀 이미지 빌드 필요
자동 MD5 해시, 자동 에러 시 정리 서드파티 모듈 (마지막 업데이트 ~2020), Nginx 업그레이드 시 호환성 위험
파일 크기 자동 리포팅 API 변경 필요 (2단계 요청), 클라이언트 수정 필요

Option B: client_body_in_file_only + 2단계 API (표준 Nginx)

방식:

1. Client → Spring Boot: 메타데이터만 전송 → PENDING 저장 + uploadToken 발급
2. Client → Nginx(/upload-direct/): 파일 전송 → Nginx가 디스크에 저장 (표준 기능)
3. Nginx → Spring Boot: proxy_pass (X-File-Path 헤더로 temp 경로 전달, body 없음) → ATOMIC_MOVE + COMPLETED

Nginx 설정:

location /upload-direct/ {
    client_body_in_file_only on;
    client_body_temp_path /data/storage/.tmp;
    client_body_buffer_size 0;

    proxy_pass http://spring_app/api/files/callback;
    proxy_set_header X-File-Path $request_body_file;
    proxy_pass_request_body off;
    proxy_set_header Content-Length 0;
}

결과:

항목 Before After
디스크 쓰기 3회 1회 (Nginx → 디스크)
JVM 파일 I/O 전체 바이트 통과 0 바이트 (경로만 수신)
RAM 사용 512MB tmpfs 0
Tomcat 점유 업로드 전체 시간 콜백 처리만 (~ms)
Nginx 이미지 nginx:alpine nginx:alpine (그대로)
장점 단점
표준 Nginx 기능, 커스텀 빌드 불필요 MD5 자동 계산 없음
JVM 완전 분리, Option A와 동일한 성능 Nginx가 temp 파일 자동 정리 안 함 → cleanup 확장 필요
프로덕션 검증된 안정적 방식 API 변경 필요 (2단계 요청), 클라이언트 수정 필요
storage-data 볼륨의 :ro:rw 변경 필요 uploadToken 보안 + 만료 처리 필요

Option C: 현재 구조 설정 최적화 (코드 변경 없음)

방식: API/코드 변경 없이 설정만 조정

변경 1 — tmpfs 제거, 디스크 기반 temp:

# nginx.conf
client_body_temp_path /data/nginx-body 1 2;  # tmpfs 대신 디스크
# docker-compose.yml: tmpfs 삭제, nginx-body 볼륨 추가
nginx:
    volumes:
      - nginx-body:/data/nginx-body
    # tmpfs 삭제

변경 2 — 소파일 메모리 처리:

# application.yml
file-size-threshold: 1MB  # 10KB → 1MB

결과:

항목 Before After
RAM 사용 512MB tmpfs 0 (디스크 기반)
소파일 디스크 I/O 10KB 이상 모두 디스크 1MB 이상만 디스크
디스크 쓰기 3회 2회 (Nginx 디스크 + multipart)
JVM 파일 I/O 전체 바이트 통과 전체 바이트 통과 (변화 없음)
Tomcat 점유 업로드 전체 시간 업로드 전체 시간 (변화 없음)
API 변경 없음
코드 변경 없음 (설정 3줄)
장점 단점
설정 3줄 수정, 코드 변경 0 JVM이 여전히 업로드 경로에 존재
API 계약 유지, 클라이언트 수정 없음 Nginx → Spring Boot 네트워크 재전송 제거 불가
즉시 적용 가능 근본적 확장성 문제 미해결

전체 비교표

A: upload-module B: client_body_in_file_only C: 설정 최적화
디스크 쓰기 1회 1회 2회
JVM 바이트 통과 0 0 전체
RAM 절약 512MB 512MB 512MB
Tomcat 점유 ~ms ~ms ~30s/300MB
Nginx 이미지 커스텀 빌드 표준 alpine 표준 alpine
API 변경 있음 (2단계) 있음 (2단계) 없음
코드 변경 높음 중간 없음
구현 난이도 높음 중간 낮음 (설정 3줄)
확장성 개선 매우 큼 매우 큼 제한적

권장 전략

  1. 즉시: Option C 적용 (설정 3줄, 512MB RAM 절약)
  2. 다음 단계: Option B 적용 (JVM 파일 I/O 완전 분리, 표준 Nginx 유지)
  3. 필요 시: Option A 검토 (MD5 자동 검증이 필수인 경우)

@this-is-spear this-is-spear changed the title LeeHeoYoon: Thymeleaf 데모 UI 추가 LeeHeoYoon: 파일 업로드-다운로드 기능 구현 Mar 8, 2026
@this-is-spear

Copy link
Copy Markdown
Member Author

Option B Step 3: Nginx → Spring Boot 콜백 동작 원리

왜 temp 경로를 전달하는가?

Nginx와 Spring Boot가 같은 디스크 볼륨을 공유하지만, Nginx가 파일을 어디에 저장했는지 Spring Boot는 모릅니다. 그래서 Nginx가 저장한 파일의 경로를 헤더로 알려줘야 합니다.

단계별 동작

Step 2: Client → Nginx (파일 저장)

Client가 POST /upload/42?token=abc 로 파일 바이트를 보냄
    ↓
Nginx가 client_body_in_file_only on 설정에 의해
파일을 디스크에 저장: /data/storage/.nginx-tmp/3/27/0000000001
    ↓
이 경로는 Nginx가 자동 생성한 이름 (제어 불가)
$request_body_file 변수에 이 경로가 담김

Step 3: Nginx → Spring Boot (경로만 전달)

# Nginx 설정
proxy_set_header X-File-Path $request_body_file;  # 파일 경로를 헤더에 넣고
proxy_pass_request_body off;                        # 파일 바이트는 안 보냄
proxy_set_header Content-Length 0;                   # 빈 body
proxy_pass http://spring_app/internal/callback;     # Spring Boot에 전달
Spring Boot가 받는 것:
    POST /internal/callback
    X-File-Path: /data/storage/.nginx-tmp/3/27/0000000001   ← 파일 위치
    X-Original-URI: /upload/42?token=abc                     ← metadataId + token
    Content-Length: 0                                         ← body 없음

Spring Boot가 하는 것:
    1. X-Original-URI에서 metadataId=42, token=abc 파싱
    2. DB에서 메타데이터 조회 + 토큰 검증
    3. X-File-Path의 파일을 최종 위치로 이동:
       Files.move("/data/storage/.nginx-tmp/3/27/0000000001",
                  "/data/storage/uuid-xxx",
                  ATOMIC_MOVE)   ← 같은 볼륨이라 즉시 rename
    4. DB 상태 PENDING → COMPLETED

핵심 포인트

질문
파일 바이트를 안 보내는 이유? JVM이 파일 I/O를 안 하게 하려고. Option B의 핵심 목적
Spring Boot가 파일을 어떻게 처리? Nginx와 같은 디스크 볼륨 마운트 → 경로만 알면 Files.move()로 rename
Nginx 자동 생성 파일명은? 0000000001 같은 순번. 제어 불가. $request_body_file 변수로 알려줘야 함
다운로드의 X-Accel-Redirect와 반대? 정확히 대칭 구조

다운로드 vs 업로드: 동일한 철학

다운로드 (현재):
  Spring Boot --X-Accel-Redirect 헤더--> Nginx --sendfile()--> Client
  "Spring Boot가 Nginx에 경로를 알려주면, Nginx가 파일을 직접 서빙"

업로드 (Option B):
  Nginx --X-File-Path 헤더--> Spring Boot --ATOMIC_MOVE--> 최종 위치
  "Nginx가 Spring Boot에 경로를 알려주면, Spring Boot가 파일을 이동만"

다운로드와 업로드 모두 "경로만 전달하고, 실제 파일 I/O는 Nginx가 디스크에서 직접 처리"하는 동일한 철학입니다.

@this-is-spear

Copy link
Copy Markdown
Member Author

Option B 구현 계획 (Planner-Architect-Critic 3 Round 합의)

목표

Nginx client_body_in_file_only를 활용하여 파일 바이트를 JVM 우회, Nginx가 직접 디스크에 쓴 뒤 Spring Boot에는 파일 경로만 전달하는 2단계 업로드 구조로 변경.

새로운 업로드 흐름

1. Client → Spring Boot: POST /api/files/init (JSON 메타데이터)
   → PENDING 저장 + uploadToken 발급

2. Client → Nginx: POST /upload/{metadataId}?token=xxx (raw file body)
   → Nginx가 /data/storage/.nginx-tmp/에 직접 저장

3. Nginx → Spring Boot: internal proxy_pass to /internal/upload-callback
   (body 없이, X-File-Path + X-Upload-Secret 헤더)
   → 조건부 UPDATE → ATOMIC_MOVE → COMPLETED

3 Round 피드백 반영 이력 (13건)

Round 이슈 심각도 해결
R1 temp 파일 경로 (ATOMIC_MOVE 실패) CRITICAL client_body_temp_path /data/storage/.nginx-tmp 1 2 (same volume)
R1 콜백 보안 (외부 호출 가능) CRITICAL /internal/ + internal; 지시어 + X-Upload-Secret
R1 파일 권한 (UID 불일치) CRITICAL 두 컨테이너 user: "1001:1001"
R1 orphan temp 파일 cleanup CRITICAL cleanupOrphanNginxTempFiles() 10분 스케줄러
R1 멱등성 보장 HIGH WHERE status='PENDING' AND upload_token=:token 조건부 UPDATE
R1 fileSize 검증 HIGH Files.size(tempFile) 비교 후 실제 크기로 UPDATE
R1 X-File-Path 검증 MEDIUM prefix 확인 + .. path traversal 차단
R1 토큰 TTL MEDIUM 30분 만료 (< STALE_UPLOAD_THRESHOLD 1시간)
R1 UI 폴백 MEDIUM 2단계 실패 시 기존 multipart 자동 폴백
R2 Nginx non-root port 80 바인딩 실패 CRITICAL listen 8080 + pid /tmp/nginx.pid
R2 client_body_temp_path 전역 범위 오염 CRITICAL /upload/ location 내에서만 설정
R2 internal; location 블록 누락 CRITICAL /internal/ 블록 추가
R2 MOVE→UPDATE 레이스 컨디션 HIGH UPDATE 먼저 → MOVE 나중 (DB 동시성 제어)

구현 단계

Step 1: DB/Entity (upload_token, token_expires_at 컬럼)
  ↓
Step 2: Infra (Nginx listen 8080, /upload/ + /internal/ location, Docker UID)
  ↓
Step 3: Service (initUpload, completeUpload, orphan cleanup)
  ↓
Step 4: Controller (POST /api/files/init, InternalUploadController)
  ↓
Step 5: UI (2단계 업로드 + multipart 폴백)
  ↓
Step 6: k6 부하테스트 (before/after 비교) + curl 검증

변경 파일 (16개)

구분 파일
수정 (11) init.sql, FileMetadata.java, nginx.conf, docker-compose.yml, Dockerfile, application.yml, FileMetadataRepository.java, FileService.java, StorageCleanupService.java, FileController.java, index.html
신규 (5) InternalUploadController.java, InitUploadRequest.java, InitUploadResponse.java, k6/upload-before.js, k6/upload-after.js

this-is-spear and others added 2 commits March 8, 2026 04:40
- Add nginx client_body_in_file_only for zero-copy file reception
- Implement 2-stage upload: init (metadata) → nginx raw body upload
- Add internal callback controller with shared secret validation
- Add upload token with 30-min TTL and conditional UPDATE for race safety
- Add orphan nginx temp file cleanup scheduler
- Add k6 load test scripts for before/after comparison
- Update UI with Option B upload + legacy multipart fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix nginx proxy_pass URI concatenation bug with rewrite directive
- Fix client_body_buffer_size 0 causing upload hang (set to 16k)
- Remove non-root user constraints for Docker volume compatibility
- Switch nginx to listen on port 80 (root mode)
- Add BASE_URL env support to k6 scripts for Docker network testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@this-is-spear

Copy link
Copy Markdown
Member Author

⚡ Option B (Nginx Direct-to-Disk 2-Stage Upload) 부하테스트 결과

테스트 환경

  • 파일 크기: 10MB per request
  • VU (Virtual Users): 1 → 10 (30s ramp-up) → 10 (1m sustain) → 0 (30s ramp-down)
  • 총 테스트 시간: 2분
  • 도구: k6 (Grafana)

결과 비교

지표 Before (Multipart) After (Option B) 개선율
총 Iterations 790 843 +6.7%
http_req_duration avg 154ms 43ms -72%
http_req_duration p(95) 234ms 102ms -56%
Throughput (req/s) 6.55 13.97 +113%
Error Rate 0% 0% -
Data Sent 8.3 GB 8.8 GB -

Option B 세부 지표

단계 avg p(90) p(95)
Init (metadata) 26ms 46ms 51ms
Upload (nginx disk) 59ms 100ms 124ms
Total (init+upload) 92ms 147ms 176ms

분석

  1. 응답 시간 72% 감소: JVM이 파일 바이트를 전혀 처리하지 않으므로, Spring Boot는 메타데이터 JSON만 처리 (avg 26ms)
  2. 처리량 2배 이상 증가: Nginx가 직접 디스크에 기록하므로 JVM heap/GC 부담 제거, 동시 처리 능력 향상
  3. Init 단계 분리 효과: 메타데이터 생성이 26ms로 매우 빠르고, 파일 I/O와 완전히 분리됨
  4. 에러율 0%: 두 방식 모두 안정적으로 동작

수정된 이슈

  • client_body_buffer_size 016k: buffer 0으로 설정 시 nginx가 body를 읽지 못해 hang 발생
  • proxy_pass URI 조합 버그: rewrite ^ /internal/upload-callback break;로 고정 경로 전달
  • Docker volume 권한: non-root user 제약 제거하여 volume write 문제 해결

@this-is-spear

Copy link
Copy Markdown
Member Author

📋 Gzip 압축 파일 유지 저장 전략 분석

파일 업로드/다운로드 시 gzip 압축 상태를 유지한 채 디스크에 저장하고, 다운로드 시에도 그대로 전달하는 방식의 효과를 분석했습니다.


현재 상태

현재 gzip은 API JSON 응답과 HTML 페이지에만 적용되고 있습니다.

# nginx.conf:33-43 — 텍스트 기반 응답만 압축
gzip_types application/json text/plain text/css application/javascript application/xml;

# nginx.conf:121 — 파일 다운로드는 gzip off
location /storage-internal/ {
    gzip off;    # ← 바이너리 파일 압축 안 함
    sendfile on; # ← zero-copy 전송
}

파일 업로드/다운로드 경로에서 gzip 압축은 일어나지 않고 있습니다. 브라우저/HTTP 클라이언트는 request body를 gzip 압축하지 않으며, 다운로드도 명시적으로 gzip off입니다.


Zero-Copy(sendfile)란?

운영체제는 메모리를 유저 스페이스(앱이 사는 곳)와 커널 스페이스(OS가 관리하는 곳)로 나눕니다. 앱이 디스크나 네트워크를 쓰려면 반드시 커널에게 요청(시스템 콜)해야 합니다.

일반 전송: read() + write() — 시스템 콜 2번

read() 호출
 ① 디스크 → [커널 버퍼]    (DMA 복사 — 하드웨어가 직접)
 ② [커널 버퍼] → [앱 메모리] (CPU 복사 — 유저 스페이스로 올림)

write() 호출
 ③ [앱 메모리] → [소켓 버퍼]  (CPU 복사 — 다시 커널로 내림)
 ④ [소켓 버퍼] → 네트워크 카드 (DMA 복사)
        유저 스페이스            커널 스페이스             하드웨어
        ┌──────────┐
        │ nginx    │
        │ 앱 메모리 │
        └───▲──┬───┘
  CPU복사②│  │③CPU복사
        ┌───┴──▼───┐         ┌──────────┐
        │ 커널 버퍼  │◄────────│  디스크    │
        │          │  ①DMA   └──────────┘
        └───┬──────┘
            │⑤CPU복사
        ┌───▼──────┐         ┌──────────┐
        │ 소켓 버퍼  │────────►│ 네트워크   │
        │          │  ④DMA   │ 카드      │
        └──────────┘         └──────────┘

복사 4회 (CPU 2회 + DMA 2회), 컨텍스트 스위치 2회

문제: nginx는 파일 내용을 수정하지 않는데, 앱 메모리를 거쳐야 합니다.

Zero-Copy 전송: sendfile() — 시스템 콜 1번

// 리눅스 시스템 콜
sendfile(socket_fd, file_fd, offset, count);
// "이 파일을 저 소켓으로 직접 보내줘" — 끝
sendfile() 호출
 ① 디스크 → [커널 버퍼]        (DMA 복사)
 ② 커널 버퍼의 위치 정보만 소켓에 전달 (데이터 복사 ❌, 포인터만)
 ③ 네트워크 카드가 커널 버퍼에서 직접 읽어서 전송 (DMA 복사)
        유저 스페이스            커널 스페이스             하드웨어
        ┌──────────┐
        │ nginx    │
        │ (관여 X)  │  sendfile() 호출만 함
        └──────────┘

        ┌──────────┐         ┌──────────┐
        │ 커널 버퍼  │◄────────│  디스크    │
        │          │  ①DMA   └──────────┘
        └───┬──────┘
    포인터②│ (데이터 복사 없음!)
        ┌───▼──────┐         ┌──────────┐
        │ 소켓 버퍼  │────────►│ 네트워크   │
        │          │  ③DMA   │ 카드      │
        └──────────┘         └──────────┘

복사 2회 (DMA만), CPU 복사 0회, 컨텍스트 스위치 1회

비교

항목 일반 전송 sendfile (zero-copy)
시스템 콜 2번 1번
데이터 복사 4번 (CPU 2 + DMA 2) 2번 (DMA만)
CPU 복사 2번 0번
앱 메모리 사용 파일 크기만큼 거의 0
300MB × 10개 동시 전송 ~3GB 메모리 필요 거의 0

gzip을 켜면 왜 zero-copy가 깨지는가

gzip 압축은 데이터를 읽고 변환하는 작업이라 커널만으로는 처리할 수 없습니다.

gzip off (현재):
  디스크 → [커널] ──────────────────────────► 네트워크 카드  ✅ zero-copy

gzip on:
  디스크 → [커널] → [nginx 메모리에서 압축] → [커널] → 네트워크 카드  ❌ zero-copy 깨짐
                     ↑
                 반드시 유저 스페이스를 거쳐야 함

만약 gzip 압축 저장을 구현한다면?

효과: 미미

항목 평가 이유
디스크 절감 ~7% 사용자 파일의 80%+는 이미 압축된 포맷 (이미지, 동영상, PDF, ZIP)
CPU 절감 없음 현재도 파일에 gzip 안 쓰고 있음
네트워크 절감 ~7% 텍스트 파일만 해당

부작용: 심각

  1. sendfile zero-copy 최적화 손실 — 위에서 설명한 것처럼, 압축 파일을 gzip 미지원 클라이언트에게 보내려면 실시간 해제가 필요 → zero-copy 경로 불가
  2. HTTP Range Request 불가 — 300MB 파일 이어받기, 동영상 스트리밍 깨짐
  3. DB 스키마 변경is_compressed, compressed_size 컬럼 추가 필요
  4. 전반적 로직 수정 — 업로드/다운로드/복사/파일크기검증 전부 영향

결론

비용 대비 효과가 없으므로 구현을 권장하지 않습니다.

  • 유지할 gzip 압축 상태 자체가 없음 (현재 파일 전송에 gzip 미사용)
  • 대부분의 사용자 파일은 이미 압축된 포맷이라 gzip 효과 0~2%
  • Range Request 손실은 파일 스토리지 서비스에 치명적
  • 현재의 gzip off + sendfile on 조합이 zero-copy를 활용한 최적 구성

더 효과적인 대안

  • 파일 중복 제거 (Deduplication) — 해시 기반 동일 파일 1카피 저장
  • CDN 캐싱 — 빈번 다운로드 파일의 서버 부하 감소
  • 이미지 최적화 — 업로드 시 WebP/AVIF 자동 변환

@this-is-spear

Copy link
Copy Markdown
Member Author

📊 Zero-Copy vs 일반 전송 vs Gzip 전송 — 다이어그램

1. 일반 전송 (read + write) — 복사 4회

sequenceDiagram
    participant App as nginx (유저 스페이스)
    participant Kernel as 커널 스페이스
    participant Disk as 디스크
    participant NIC as 네트워크 카드

    Note over App,NIC: read() 시스템 콜
    App->>Kernel: read(file_fd, buf, size)
    Kernel->>Disk: ① DMA 복사 요청
    Disk-->>Kernel: 데이터 → 커널 버퍼
    Kernel-->>App: ② CPU 복사 (커널→앱 메모리)
    Note over App: 앱 메모리에 파일 데이터 적재

    Note over App,NIC: write() 시스템 콜
    App->>Kernel: write(socket_fd, buf, size)
    Note over Kernel: ③ CPU 복사 (앱→소켓 버퍼)
    Kernel->>NIC: ④ DMA 복사 (소켓→네트워크)
    NIC-->>NIC: 클라이언트로 전송

    Note over App,NIC: 총: 복사 4회 (CPU 2회), 컨텍스트 스위치 2회
Loading

2. Zero-Copy (sendfile) — 복사 2회, CPU 복사 0회

sequenceDiagram
    participant App as nginx (유저 스페이스)
    participant Kernel as 커널 스페이스
    participant Disk as 디스크
    participant NIC as 네트워크 카드

    Note over App,NIC: sendfile() 시스템 콜 1번
    App->>Kernel: sendfile(socket_fd, file_fd, offset, count)
    Kernel->>Disk: ① DMA 복사 요청
    Disk-->>Kernel: 데이터 → 커널 버퍼
    Note over Kernel: ② 포인터만 소켓에 전달 (복사 없음!)
    Kernel->>NIC: ③ DMA 복사 (커널 버퍼→네트워크)
    NIC-->>NIC: 클라이언트로 전송

    Note over App: nginx 메모리 사용: 거의 0
    Note over App,NIC: 총: 복사 2회 (DMA만), CPU 복사 0회
Loading

3. Gzip 전송 — Zero-Copy 깨짐

sequenceDiagram
    participant App as nginx (유저 스페이스)
    participant Kernel as 커널 스페이스
    participant Disk as 디스크
    participant NIC as 네트워크 카드

    Note over App,NIC: gzip on이면 sendfile 무효화
    App->>Kernel: read 요청
    Kernel->>Disk: ① DMA 복사
    Disk-->>Kernel: 데이터 → 커널 버퍼
    Kernel-->>App: ② CPU 복사 (커널→앱)

    Note over App: ③ nginx가 메모리에서 gzip 압축 수행<br/>CPU 소모 + 메모리 점유

    App->>Kernel: ④ CPU 복사 (압축 데이터→소켓 버퍼)
    Kernel->>NIC: ⑤ DMA 복사 (소켓→네트워크)
    NIC-->>NIC: 클라이언트로 전송

    Note over App,NIC: 총: 복사 4회 + 압축 연산, zero-copy 완전 손실
Loading

4. 현재 프로젝트의 파일 다운로드 흐름

flowchart LR
    Client[클라이언트] -->|GET /api/files/123| Spring[Spring Boot]
    Spring -->|X-Accel-Redirect<br/>/storage-internal/UUID| Nginx[Nginx]
    Nginx -->|sendfile zero-copy<br/>gzip off| Disk[(디스크<br/>/data/storage/UUID)]
    Disk -->|DMA 직접 전송<br/>앱 메모리 0| Client

    style Nginx fill:#2d9,stroke:#333,color:#000
    style Disk fill:#69c,stroke:#333,color:#000
Loading

5. 방식별 비교 요약

graph TB
    subgraph "❌ Gzip 압축 저장 방식 (제안)"
        A1[업로드 시 서버에서 압축] --> A2[.gz로 디스크 저장]
        A2 --> A3{클라이언트가<br/>gzip 지원?}
        A3 -->|Yes| A4[gzip_static 서빙<br/>sendfile 가능]
        A3 -->|No| A5[실시간 해제 필요<br/>sendfile 불가 ❌]
        A2 --> A6[Range Request 불가 ❌]
        A2 --> A7[파일 크기 이중 관리 ❌]
    end

    subgraph "✅ 현재 방식 (권장)"
        B1[원본 그대로 디스크 저장] --> B2[sendfile zero-copy 전송]
        B2 --> B3[모든 클라이언트 호환 ✅]
        B2 --> B4[Range Request 지원 ✅]
        B2 --> B5[CPU 복사 0회 ✅]
    end

    style A5 fill:#f66,stroke:#333,color:#000
    style A6 fill:#f66,stroke:#333,color:#000
    style A7 fill:#f66,stroke:#333,color:#000
    style B3 fill:#2d9,stroke:#333,color:#000
    style B4 fill:#2d9,stroke:#333,color:#000
    style B5 fill:#2d9,stroke:#333,color:#000
Loading

6. 파일 유형별 gzip 압축 효과

xychart-beta
    title "파일 유형별 gzip 압축률 (%)"
    x-axis ["텍스트/CSV", "JSON/XML", "PDF", "Office", "이미지", "동영상", "ZIP/RAR"]
    y-axis "압축률 (높을수록 효과적)" 0 --> 100
    bar [75, 80, 10, 3, 1, 1, 0]
Loading

사용자 업로드 파일의 80%+는 우측 4개 카테고리(PDF, Office, 이미지, 동영상, 압축파일)에 해당하여 gzip 효과가 거의 없습니다.

@this-is-spear

Copy link
Copy Markdown
Member Author

📋 Nginx io_uring 적용 효과 분석

현재 epoll + sendfile 구성에서 io_uring을 도입하면 추가 개선이 가능한지 분석했습니다.


io_uring이란?

기존 I/O는 매 작업마다 시스템 콜(유저↔커널 전환)이 필요합니다. io_uring은 유저와 커널이 공유하는 링 버퍼를 통해 I/O 요청을 배치로 주고받아, 시스템 콜 오버헤드를 제거하는 리눅스 커널 기술(5.1+)입니다.

graph LR
    subgraph "기존 방식: epoll + sendfile"
        A1[요청 1] -->|시스템 콜| K1[커널 처리]
        A2[요청 2] -->|시스템 콜| K2[커널 처리]
        A3[요청 3] -->|시스템 콜| K3[커널 처리]
        A4[요청 N] -->|시스템 콜| K4[커널 처리]
    end
Loading
graph LR
    subgraph "io_uring 방식: 공유 링 버퍼"
        A[요청 1,2,3...N] -->|링 버퍼에 배치| SQ[Submission Queue]
        SQ -->|시스템 콜 1번| K[커널 일괄 처리]
        K -->|결과 배치| CQ[Completion Queue]
        CQ --> R[결과 1,2,3...N]
    end
Loading
기존: 파일 1000개 전송 = 시스템 콜 1000번
io_uring: 파일 1000개 전송 = 시스템 콜 수십 번 (배치 처리)

io_uring 내부 구조

graph TB
    subgraph "유저 스페이스 (nginx)"
        SQ_U["Submission Queue<br/>(요청 넣기)"]
        CQ_U["Completion Queue<br/>(결과 읽기)"]
    end

    subgraph "공유 메모리 (mmap)"
        RING["링 버퍼<br/>시스템 콜 없이 접근 가능"]
    end

    subgraph "커널 스페이스"
        IO["I/O 엔진<br/>(디스크/네트워크 처리)"]
    end

    SQ_U -->|"요청 추가 (no syscall)"| RING
    RING -->|"요청 수집"| IO
    IO -->|"완료 통보"| RING
    RING -->|"결과 읽기 (no syscall)"| CQ_U

    style RING fill:#ff9,stroke:#333,color:#000
Loading

핵심: SQ/CQ가 mmap으로 공유되어 있어, 요청 추가와 결과 읽기에 시스템 콜이 필요 없습니다.


현재 프로젝트에서 효과가 없는 이유 — Amdahl's Law

graph LR
    Client[클라이언트] -->|요청| LR["limit_rate 10m<br/>⏱️ ~100,000μs/MB<br/>🔴 실제 병목"]
    LR --> SF["sendfile zero-copy<br/>⏱️ ~2-5μs<br/>🟢 이미 최적"]
    SF --> Disk["디스크"]

    style LR fill:#f66,stroke:#333,color:#000
    style SF fill:#2d9,stroke:#333,color:#000
Loading

io_uring이 개선하는 부분: sendfile 시스템 콜 오버헤드 ~2-5μs
실제 병목: limit_rate 10m 대역폭 제한 ~100,000μs/MB

병목이 아닌 곳을 최적화해도 체감 불가. 5만배 차이.


경로별 분석

graph TB
    subgraph "다운로드 경로"
        D1["limit_rate 10m"] -->|"병목 ⏱️ 100,000μs/MB"| D2["sendfile zero-copy"]
        D2 -->|"⏱️ 2-5μs"| D3["디스크 → 네트워크"]
    end

    subgraph "업로드 경로"
        U1["limit_rate 10m"] -->|"병목 ⏱️ 100,000μs/MB"| U2["client_body_in_file_only"]
        U2 -->|"디스크 쓰기 500MB/s+"| U3["디스크 저장"]
    end

    subgraph "API 프록시"
        P1["proxy_pass"] -->|"⏱️ μs 수준"| P2["Spring Boot"]
        P2 -->|"병목: DB 쿼리 ms 수준"| P3["JSON 응답"]
    end

    style D1 fill:#f66,stroke:#333,color:#000
    style U1 fill:#f66,stroke:#333,color:#000
    style P2 fill:#f66,stroke:#333,color:#000
    style D2 fill:#2d9,stroke:#333,color:#000
Loading
경로 현재 병목 io_uring 효과
다운로드 limit_rate 10m ❌ sendfile이 이미 zero-copy
업로드 limit_rate 10m ❌ 디스크 I/O(500MB/s+)가 병목 아님
API 프록시 Spring Boot 처리 시간 ❌ 앱 처리가 지배적
동시 접속 worker_connections 4096 ❌ epoll은 수천 연결에서 충분

io_uring이 의미 있는 규모 vs 현재 프로젝트

quadrantChart
    title "io_uring 도입 효과 판단 기준"
    x-axis "동시 접속: 낮음" --> "동시 접속: 높음"
    y-axis "I/O 빈도: 낮음" --> "I/O 빈도: 높음"
    quadrant-1 "io_uring 매우 효과적"
    quadrant-2 "제한적 효과"
    quadrant-3 "효과 없음"
    quadrant-4 "제한적 효과"
    "CDN/대형파일서버": [0.9, 0.85]
    "고빈도 DB (NVMe)": [0.5, 0.95]
    "초저지연 트레이딩": [0.3, 0.9]
    "현재 프로젝트": [0.2, 0.15]
Loading
조건 io_uring 효과적 현재 프로젝트
동시 접속 10만+ 수천
네트워크 10Gbps+ 무제한 10MB/s 제한
IOPS 100K+ (NVMe) 수백

도입 비용

현재 nginx:alpine 이미지는 io_uring을 포함하지 않습니다. 도입하려면:

flowchart TD
    A["nginx:alpine<br/>(현재)"] -->|"커스텀 빌드 필요"| B["nginx 1.27+ 소스 빌드<br/>--with-io_uring"]
    B --> C["Docker seccomp 프로필 수정<br/>io_uring syscall 허용"]
    C --> D["호스트 커널 5.1+ 확인"]
    D --> E["보안 패치 직접 관리<br/>nginx 업데이트 수동"]

    style A fill:#2d9,stroke:#333,color:#000
    style B fill:#f96,stroke:#333,color:#000
    style C fill:#f96,stroke:#333,color:#000
    style E fill:#f66,stroke:#333,color:#000
Loading

결론

현재 구성(epoll + sendfile + limit_rate)에서 io_uring은 효과 없음.

병목이 I/O 시스템 콜이 아니라 대역폭 제한(limit_rate 10m)에 있기 때문입니다. 5만배 차이가 나는 곳을 최적화해도 사용자가 체감할 수 없습니다.

더 효과적인 개선 방향:

  • limit_rate 동적 조절 — 동시 접속 수 기반 대역폭 분배
  • ETag/304 활용 강화 — 중복 다운로드 방지
  • Brotli 압축 — API/HTML 응답에 gzip 대신 적용 (20-30% 더 작음)
  • 파일 중복 제거 — 해시 기반 동일 파일 1카피 저장

@this-is-spear

Copy link
Copy Markdown
Member Author

📋 동적 다운로드 속도 제어 (X-Accel-Limit-Rate) 적용 및 부하 테스트 결과

변경 내용

1. nginx.conf — limit_rate 10m → 128m

  • /api/ 업로드, /upload/ Option B, /storage-internal/ 다운로드 모두 128MB/s로 상향

2. FileController.java — X-Accel-Limit-Rate 동적 제어 추가

private String calculateRate(Long fileSize) {
    if (fileSize < 100MB) return "0";       // 무제한 (sendfile zero-copy 최대 활용)
    return String.valueOf(128 * 1024 * 1024); // 100MB 이상: 128MB/s
}

Spring Boot가 X-Accel-Redirect 응답 시 X-Accel-Limit-Rate 헤더를 함께 전달하면, nginx가 해당 값을 요청별 limit_rate로 자동 적용합니다. nginx 설정 변경 없이 앱 레벨에서 파일 크기, 사용자 등급, 동시 접속 수 등 비즈니스 로직에 따라 속도를 제어할 수 있습니다.

sequenceDiagram
    participant Client as 클라이언트
    participant Nginx as Nginx
    participant Spring as Spring Boot
    participant Disk as 디스크

    Client->>Nginx: GET /api/files/123
    Nginx->>Spring: proxy_pass
    Spring->>Spring: calculateRate(fileSize)
    Spring-->>Nginx: X-Accel-Redirect: /storage-internal/UUID<br/>X-Accel-Limit-Rate: 0 (무제한)
    Nginx->>Disk: sendfile() zero-copy
    Disk-->>Client: 파일 전송 (limit_rate 동적 적용)
Loading

부하 테스트 결과

10MB 파일, 시나리오: 단일 사용자(15s) → 5명 동시(15s) → 20명 동시(15s)

xychart-beta
    title "다운로드 Throughput 비교 (MB/s)"
    x-axis ["Before (avg)", "Before (med)", "After (avg)", "After (med)", "After (peak)"]
    y-axis "MB/s" 0 --> 1200
    bar [13.5, 13.7, 226, 182, 1110]
Loading
지표 Before (limit_rate 10m) After (128m + X-Accel-Limit-Rate) 개선
throughput avg 13.5 MB/s 226 MB/s 16.7x
throughput med 13.7 MB/s 182 MB/s 13.3x
throughput peak 14.0 MB/s 1,110 MB/s 79x
download duration avg 741ms 84ms 8.8x 빠름
download duration med 727ms 55ms 13.2x 빠름
receiving time avg 725ms 52ms 13.9x 빠름
TTFB avg 16ms 31ms +15ms (처리량 증가 영향)
총 iterations (55초) 541 4,521 8.4x
총 전송량 5.7 GB 47 GB 8.3x
에러율 0% 0% 동일
xychart-beta
    title "다운로드 소요 시간 비교 (ms, 낮을수록 좋음)"
    x-axis ["Before avg", "Before med", "After avg", "After med"]
    y-axis "ms" 0 --> 800
    bar [741, 727, 84, 55]
Loading

왜 이렇게 차이가 나는가

graph LR
    subgraph "Before: limit_rate 10m"
        B1["sendfile zero-copy<br/>⏱️ 2-5μs"] --> B2["limit_rate 10m<br/>⏱️ 727ms/10MB<br/>🔴 병목"]
    end

    subgraph "After: X-Accel-Limit-Rate 0 (무제한)"
        A1["sendfile zero-copy<br/>⏱️ 2-5μs"] --> A2["limit_rate 없음<br/>⏱️ 55ms/10MB<br/>🟢 zero-copy 최대 활용"]
    end

    style B2 fill:#f66,stroke:#333,color:#000
    style A2 fill:#2d9,stroke:#333,color:#000
Loading

기존에는 sendfile zero-copy가 마이크로초 단위로 파일을 전달할 수 있었지만, limit_rate 10m이 인위적으로 속도를 제한하고 있었습니다. 제한을 풀자 sendfile의 실제 성능이 그대로 드러난 것입니다.

확장성

calculateRate() 메서드에서 비즈니스 로직을 자유롭게 확장할 수 있습니다:

// 예시: 사용자 등급별 속도 제어
if (user.isPremium()) return "0";           // 프리미엄: 무제한
if (activeDownloads > 3) return "5242880";  // 동시 3개 초과: 5MB/s
if (fileSize < 100MB) return "0";           // 소형 파일: 무제한
return "134217728";                          // 기본: 128MB/s

- Increase nginx limit_rate from 10MB/s to 128MB/s for upload/download
- Add X-Accel-Limit-Rate header in FileController for per-request rate control
  - Files < 100MB: unlimited (maximize sendfile zero-copy)
  - Files >= 100MB: 128MB/s cap
- Add k6 download throughput load test (1/5/20 concurrent users)
- Load test results: 16.7x throughput improvement (13.5 → 226 MB/s avg)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@this-is-spear

Copy link
Copy Markdown
Member Author

성능 개선 적용 및 부하테스트 결과

개선 제안 중 성능 관련 3개 항목을 적용하고 부하테스트를 수행했습니다.

적용된 변경사항

1. aio threads + directio 1m 추가 (/storage-internal/)

# 대용량 동시 다운로드 시 worker 블로킹 방지
aio threads;
directio 1m;
sendfile on;
  • 1MB 미만: sendfile() zero-copy
  • 1MB 이상: directio로 페이지 캐시 우회 + aio threads로 스레드풀 비동기 읽기 → worker 블로킹 방지

2. keepalive_timeout 65s → 30s

유휴 커넥션 FD 점유 시간 절반으로 줄여 동시 접속 수용량 증가

3. error_page 413 + ErrorController 추가

nginx 레벨에서 300MB 초과 업로드를 차단할 때 JSON 에러 응답 반환

부하테스트 결과

다운로드 (10MB 파일, 1→5→20 동시 사용자)

지표
throughput avg 78 MB/s
throughput peak 232 MB/s
download duration avg 189ms
download duration p95 376ms
TTFB avg 21ms
에러율 0%
총 전송량 22 GB (55초)
총 요청 수 2,060건

업로드 (10MB 파일, 1→10 VU ramp-up)

지표
init duration avg 24ms
upload duration avg 61ms
total duration avg 91ms
upload p95 135ms
처리량 6.99 req/s
에러율 0%
총 전송량 8.9 GB sent (2분)
총 업로드 수 844건

모든 threshold 통과, 에러율 0%입니다.

this-is-spear and others added 5 commits March 22, 2026 04:14
- Add aio threads + directio 1m to /storage-internal/ for non-blocking
  concurrent large file downloads
- Reduce keepalive_timeout from 65s to 30s to free FDs faster under
  high concurrency
- Add error_page 413 with ErrorController for JSON response when
  nginx rejects oversized uploads before reaching Spring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Learning-Is-Vital-In-Development#6 서버의 실제 API에 맞게 벤치마크를 재작성:
- 2단계 업로드 (init + Nginx direct-to-disk)
- ID 기반 폴더/파일 관리 (X-User-Id 헤더)
- 6개 시나리오: Upload, Download, FolderList, MoveFolder, DeleteFiles, DeleteFolder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker 네트워크 내 Nginx 컨테이너가 80 포트에서 listen하므로
기본 host를 nginx:8080 → my-storage-nginx:80 으로 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker 네트워크 내 Nginx 컨테이너가 80번 포트에서 listen하고
컨테이너 이름이 my-storage-nginx이므로 기본값 변경

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이전 커밋에서 파일이 잘못 덮어써진 것을 복구하고
기본 호스트를 nginx:8080 → my-storage-nginx:80 으로 수정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant