Skip to content

Latest commit

 

History

History
1179 lines (880 loc) · 53.3 KB

File metadata and controls

1179 lines (880 loc) · 53.3 KB

SpeedCam 시스템 성능 분석 보고서

목차

  1. 프로젝트 개요
  2. 시스템 아키텍처
  3. 서버 인프라 스펙
  4. 부하 테스트
  5. 아키텍처 비교 분석
  6. 모니터링 지표
  7. 최대 TPS 및 용량 분석
  8. 발견된 이슈 및 개선점
  9. 결론

1. 프로젝트 개요

SpeedCam은 도로 위 과속 차량을 실시간으로 감지하고 번호판을 인식하여 사용자에게 알림을 전송하는 이벤트 기반 실시간 시스템입니다.

핵심 특징

  • Event Driven Architecture: MQTT + AMQP 기반 비동기 메시지 처리
  • 분산 시스템: GCE 인스턴스 6대로 구성된 마이크로서비스 아키텍처
  • 실시간 OCR 처리: EasyOCR을 활용한 한국어 번호판 인식
  • 완전한 관측성: Prometheus, Grafana, Loki, Jaeger를 통한 통합 모니터링

기술 스택

  • Backend: Django 4.2 + Gunicorn
  • Message Broker: RabbitMQ 3.13 (MQTT Plugin + AMQP)
  • Database: MySQL 8.0
  • OCR Engine: EasyOCR (Korean + English)
  • Monitoring: Prometheus, Grafana, Loki, Jaeger, OpenTelemetry
  • Infra: GCP Compute Engine (6 instances), Docker Compose
  • Load Testing: k6 (Grafana k6), Python paho-mqtt

2. 시스템 아키텍처

2.1 기존 아키텍처 (Before)

기존 시스템은 Django 모놀리식 구조로, OCR 처리가 동기적으로 수행되어 다음과 같은 구조적 한계가 있었습니다.

graph TB
    subgraph Edge["Edge Device (Raspberry Pi)"]
        Camera["과속 카메라"]
    end

    subgraph Backend["backend (Django)"]
        API["API Handler"]
        OCR["OCR 처리<br/>(동기 실행)"]
    end

    subgraph Workers["Celery Workers"]
        CW["celery_worker<br/>(알림 전송)"]
        DLQ["celery_worker_dlq"]
    end

    Camera -->|"HTTP POST"| API
    API --> OCR
    Backend --> RMQ["RabbitMQ"]
    CW --> RMQ
    Backend --> MySQL[("MySQL")]
    CW --> MySQL

    style Backend fill:#ffcccc,stroke:#cc0000
    style OCR fill:#ff9999
Loading

주요 문제점

문제 영역 상세 내용
OCR 동기 처리 OCR 작업(약 3초)이 HTTP 스레드를 점유하여 서버 처리량 저하
Edge Device 블로킹 서버 응답 대기(3초+)로 인한 연속 감지 불가, 데이터 유실 위험
HTTP 기반 IoT 통신 요청마다 TCP 연결, 메시지 보장 없음, 오프라인 처리 불가
장애 전파 OCR 장애 시 API 서비스 전체 영향, 독립 확장 불가

성능 지표 (Before)아키텍처 구조 기반 추정값

기존 아키텍처는 현재 운영 환경에서 별도로 부하 테스트를 수행하지 않았습니다. 아래 수치는 동기 OCR 처리 시간(EasyOCR CPU 기준 ~3초)과 아키텍처 구조로부터 도출한 설계 기반 추정값입니다.

  • 이벤트 처리 시간: 3,000ms 이상 (HTTP 수신 → OCR 완료까지 동기 처리)
  • Edge Device 블로킹: 3,000ms 이상 (HTTP 응답 대기)
  • 메시지 보장: 없음
  • 장애 격리: 불가능 (모놀리식 구조)

2.2 현재 아키텍처 (After) - Event Driven Architecture

기존 문제를 해결하기 위해 Event Driven Architecture로 전환하여 MQTT 기반 IoT 통신과 AMQP 기반 비동기 메시지 처리를 구현했습니다.

graph TB
    subgraph Edge["Edge Device"]
        Camera["과속 카메라"]
    end

    subgraph Main["main (Django)"]
        API["API Handler"]
        MQTT_Sub["MQTT Subscriber"]
        Publisher["Event Publisher"]
    end

    subgraph Workers["Event Processors"]
        OCR["ocr-worker<br/>• 감지 이벤트 처리<br/>• OCR 수행<br/>• detections.completed 발행"]
        subgraph AlertWorker["alert-worker"]
            KombuConsumer["Kombu Consumer<br/>(단일 스레드)<br/>domain event 구독"]
            CeleryGevent["Celery gevent pool<br/>(concurrency=100)<br/>FCM 병렬 전송"]
        end
    end

    subgraph MessageBroker["RabbitMQ"]
        MQTT["MQTT Plugin"]
        Queue1[("감지 이벤트 큐")]
        DomainEvents[("domain_events exchange<br/>detections.completed")]
    end

    subgraph Storage["Google Cloud Storage"]
        GCS[("GCS Bucket<br/>번호판 이미지")]
    end

    Camera -->|"MQTT Publish"| MQTT
    Camera -->|"이미지 업로드"| GCS
    MQTT --> MQTT_Sub
    Publisher --> Queue1
    Queue1 --> OCR
    OCR -->|"이미지 다운로드"| GCS
    OCR -->|"domain event 발행"| DomainEvents
    DomainEvents -->|"choreography"| KombuConsumer
    KombuConsumer -->|"send_notification.delay()"| CeleryGevent

    Main --> DB1[("default")]
    Main --> DB2[("vehicles_db")]
    OCR --> DB3[("detections_db")]
    AlertWorker --> DB4[("notifications_db")]

    style Main fill:#90EE90
    style OCR fill:#87CEEB
    style AlertWorker fill:#DDA0DD
    style KombuConsumer fill:#C8A2C8
    style CeleryGevent fill:#DDA0DD
    style MessageBroker fill:#FFB6C1
    style Storage fill:#FFFACD
Loading

아키텍처 특징

컴포넌트 역할 프로토콜 특징
Edge Device 과속 차량 감지 MQTT QoS 1, 경량, 영구 연결
main (Django) API + MQTT 구독 HTTP + MQTT 이벤트 발행만 담당
ocr-worker 번호판 OCR 처리 AMQP 비동기 처리, concurrency=1
alert-worker FCM 푸시 알림 AMQP domain events choreography 패턴, gevent concurrency=100
alert-worker (Kombu) domain event 구독 AMQP (domain_events exchange) 단일 스레드 Kombu Consumer
alert-worker (Celery) FCM 병렬 전송 - gevent pool, send_notification.delay()
RabbitMQ 메시지 브로커 MQTT + AMQP At-Least-Once 보장

End-to-End 이벤트 흐름

sequenceDiagram
    participant Edge as Edge Device
    participant RMQ as RabbitMQ
    participant Main as main
    participant OCR as ocr-worker
    participant Kombu as alert-worker<br/>(Kombu Consumer)
    participant Celery as alert-worker<br/>(Celery gevent)
    participant User as 사용자 앱

    Edge->>RMQ: MQTT Publish (과속 차량 감지)
    RMQ-->>Edge: PUBACK (즉시)
    RMQ->>Main: 메시지 전달 (subscribe)
    Main->>Main: DB 저장 (pending)
    Main->>RMQ: 감지 이벤트 발행 (AMQP)

    RMQ->>OCR: 감지 이벤트 수신
    OCR->>OCR: 번호판 OCR 처리
    OCR->>OCR: DB 업데이트 (completed)
    OCR->>RMQ: detections.completed 발행 (domain_events exchange)

    Note over RMQ,Kombu: Choreography 패턴 — Main Service 불개입
    RMQ->>Kombu: domain event 수신 (직접 구독)
    Kombu->>Celery: send_notification.delay()
    Celery->>User: FCM Push 알림 (gevent 병렬)
Loading

3. 서버 인프라 스펙

총 6대의 GCE 인스턴스로 구성된 분산 시스템입니다. 모든 인스턴스는 asia-northeast3-a 존에 위치하며 Ubuntu 22.04 LTS, Kernel 6.8.0-1045-gcp, Docker 기반으로 운영됩니다.

3.1 인스턴스 상세 스펙

인스턴스 머신 타입 vCPU RAM 디스크 디스크 사용률 내부 IP 역할
speedcam-app e2-small 2 2GB 20GB 34% (6.4GB) 10.178.0.4 API 서버
speedcam-db e2-medium 2 4GB 29GB 20% (5.8GB) 10.178.0.2 데이터베이스
speedcam-mq e2-small 2 2GB 20GB 26% (4.9GB) 10.178.0.7 메시지 브로커
speedcam-ocr e2-small 2 2GB 20GB 87% (17GB) 10.178.0.3 OCR Worker
speedcam-alert e2-small 2 2GB 20GB 31% (5.9GB) 10.178.0.6 Alert Worker
speedcam-mon e2-small 2 2GB 20GB 37% (7.0GB) 10.178.0.5 모니터링

3.2 주요 컨테이너 구성

인스턴스 컨테이너 역할
speedcam-app Django + Gunicorn REST API (GUNICORN_WORKERS=2)
Traefik 리버스 프록시
Flower Celery 모니터링
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집
speedcam-db MySQL 8.0 메인 데이터베이스
mysqld-exporter MySQL 메트릭 수집
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집
speedcam-mq RabbitMQ 3.13 MQTT + AMQP 브로커
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집
speedcam-ocr Celery OCR Worker EasyOCR 처리 (concurrency=1)
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집
speedcam-alert Kombu Consumer + Celery gevent Worker FCM 알림 발송 (gevent concurrency=100)
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집
speedcam-mon Prometheus 메트릭 수집
Grafana 시각화 대시보드
Loki 로그 수집
Jaeger 분산 추적
OpenTelemetry Collector 텔레메트리 수집
Promtail 로그 수집 에이전트
cAdvisor 컨테이너 메트릭 수집

3.3 리소스 사용 현황

재측정 예정 — 아래 수치는 참고용이며 현재 상태와 다를 수 있습니다.

인스턴스 RAM 사용 RAM 여유 메모리 집약적 프로세스 비고
speedcam-app 661MB/2GB 1.1GB Gunicorn 2 workers 안정적
speedcam-db 853MB/4GB 2.6GB MySQL 버퍼풀 충분한 여유
speedcam-mq 471MB/2GB 1.2GB RabbitMQ 안정적
speedcam-ocr 1.0GB/2GB 721MB EasyOCR 모델 (1.5GB) 메모리 부족 위험
speedcam-alert 433MB/2GB 1.3GB Kombu Consumer + Celery gevent 충분한 여유
speedcam-mon 1.5GB/2GB 264MB Prometheus + Grafana 메모리 부족 위험

주의사항:

  • speedcam-ocr: EasyOCR 모델 로딩으로 인한 높은 메모리 사용률, concurrency를 1로 제한
  • speedcam-mon: 모니터링 스택의 메모리 집약적 특성으로 264MB 여유분만 확보

4. 부하 테스트

4.1 테스트 목적

실제 운영 환경에서의 시스템 성능과 안정성을 검증하기 위해 다음 목표로 부하 테스트를 수행했습니다.

목표 세부 내용
성능 한계 파악 각 컴포넌트별 최대 처리량 측정
병목 지점 식별 Event Driven 파이프라인 각 단계별 소요 시간 분석
아키텍처 검증 기존 동기 처리 대비 비동기 이벤트 기반 처리의 성능 개선 정도 확인
안정성 확인 스파이크 트래픽 발생 시 시스템의 안정성 검증

4.2 테스트 도구

도구 용도 특징
k6 (Grafana k6) HTTP API 부하 테스트 Prometheus Remote Write로 메트릭 실시간 전송, 웹 대시보드 + Grafana 연동
Python + paho-mqtt MQTT 파이프라인 부하 테스트 실제 한국어 번호판 이미지를 GCS에 저장하여 실 파이프라인 테스트, EasyOCR 실제 동작 검증

4.2.1 테스트 환경 및 조건

항목 상세
테스트 일시 (v2) 2026-02-12 (k6 4시나리오 + MQTT 3시나리오)
테스트 일시 (v1) 2026-02-19 14:03:38 ~ 14:18:02 UTC (KST 23:03:38 ~ 23:18:02) (TEST_ID: v1-baseline-1771509818)
k6 실행 위치 speedcam-app 인스턴스 내부 (localhost 호출)
v1 k6 실행 위치 speedcam-v1-app (10.178.0.8) 인스턴스 내부 (localhost 호출)
MQTT 테스트 실행 위치 speedcam-app → speedcam-mq (내부 IP 10.178.0.7)
네트워크 환경 동일 VPC (asia-northeast3), 인스턴스 간 지연 <1ms
부하 발생기 → 서버 지연 k6: ~0ms (localhost), MQTT: <1ms (같은 VPC)
시스템 상태 테스트 외 트래픽 없음 (전용 테스트 환경)

참고: k6 HTTP 테스트는 speedcam-app 자체에서 localhost로 호출하였으므로, 측정된 응답 시간은 순수 서버 처리 시간에 가깝습니다. 실제 클라이언트에서의 응답 시간은 네트워크 지연이 추가됩니다.


4.3 HTTP API 부하 테스트 (k6)

Django REST API의 처리 성능과 응답 시간을 측정하기 위해 4가지 시나리오로 부하 테스트를 수행했습니다.

4.3.1 테스트 시나리오

시나리오 VUs Executor 지속시간 시작 시점 설명
dashboard_polling 3 (constant) constant-vus 2분 0s 대시보드 폴링 (감지목록 5초, 알림 10초, 통계 30초 주기)
admin_ops 2 constant-arrival-rate (2/min) 2분 0s 관리자 작업 (차량 등록 + FCM 토큰 업데이트)
mixed_workload 0→5→9→9→0 ramping-vus 2분30초 2m 읽기 60% + 파이프라인 상태 30% + 쓰기 10%
spike_resilience 0→3→15→15→3→0 ramping-vus 1분10초 4m30s 급격한 트래픽 증가 시 회복력 (15 VUs = 4 핸들러 대비 3.75배)

총 테스트 시간: 5분 40초, 최대 동시 VUs: 18

4.3.2 전체 결과 요약

테스트 후 기록

4.3.3 시나리오별 상세 결과

응답 시간 분포

메트릭 avg min med max p(90) p(95)
dashboard_req_duration - - - - - -
admin_req_duration - - - - - -
detections_list_duration - - - - - -
statistics_req_duration - - - - - -
pending_read_duration - - - - - -
spike_resilience (overall) - - - - - -
http_req_duration (전체) - - - - - -

[스크린샷: k6 Grafana 대시보드 - 4 시나리오 응답시간 그래프]

임계치(Threshold) 검증 결과:

임계치 기준 실측 판정
dashboard_req_duration p(95) < 200ms - -
detections_list_duration p(95) < 300ms - -
statistics_req_duration p(95) < 500ms - -
pending_read_duration p(95) < 500ms - -
admin_req_duration p(95) < 300ms - -
spike_resilience p(95) < 1500ms - -
errors (전체) < 5% - -
errors (dashboard) < 1% - -
errors (spike) < 10% - -

주요 인사이트:

테스트 후 기록

4.3.4 Checks 결과

Check 항목 성공/전체 성공률 비고
서버 헬스체크 - - -
차량 등록 (201) - - admin_ops + mixed 시나리오
FCM 토큰 업데이트 (200) - - PATCH 엔드포인트
감지 목록 (200) - - dashboard + spike 시나리오
알림 목록 (200) - - dashboard 시나리오
통계 조회 (200) - - dashboard + spike 시나리오
대기 목록 (200) - - mixed 시나리오
혼합 읽기 (200) - - mixed 시나리오
혼합 차량 등록 (201) - - mixed 시나리오
스파이크 감지 목록 - - -
스파이크 알림 목록 - - -
스파이크 통계 - - -

테스트 후 기록

4.3.5 HTTP API 최대 TPS 분석

항목 근거
현재 설정 GUNICORN_WORKERS=2 (각 2 threads = 총 4 HTTP handlers) 배포 환경 (env.example 기본값=4와 다름)
4시나리오 테스트 - k6 4시나리오 실측 후 기록
스트레스 테스트 - k6 stress_ramp 실측 후 기록
포화점 - 재측정 예정
안정 최대 TPS - 재측정 예정
이론 최대 TPS ~80-100 req/s 4 handlers × 평균 20ms 기준
주요 병목 Gunicorn 핸들러 포화 + DB 커넥션 (CONN_MAX_AGE 미설정) 스트레스 테스트 분석

측정 근거: 테스트 수행 후 기록 예정

확장 방법:

  1. CONN_MAX_AGE 설정으로 커넥션 풀링 활성화
  2. GUNICORN_WORKERS 증가 (CPU 코어당 1-2개 권장)
  3. 인스턴스 업그레이드 (e2-medium 이상)

4.3.6 HTTP API 스트레스 테스트 (한계점 탐색)

기존 테스트(최대 15 VUs)에서는 시스템이 여유 있게 처리하여 실제 한계점을 파악하지 못했습니다. 이를 보완하기 위해 VUs를 점진적으로 50까지 올리는 스트레스 테스트를 수행했습니다.

주의: k6가 speedcam-app 동일 인스턴스(e2-small, 2 vCPU)에서 실행되므로, k6 자체의 CPU/메모리 사용이 결과에 영향을 줄 수 있습니다.

테스트 구성

Phase 시나리오 VUs 지속시간 요청 유형
Phase 1 stress_ramp (읽기 전용) 0→10→30→50→0 3분30초 GET 읽기 100%
Phase 2 stress_mixed (혼합) 0→10→30→50→0 3분 읽기 80% + 쓰기 20%

v1에도 동일한 스트레스 테스트가 추가되었습니다 (depoly-v1/k6/load-test-v1.js의 stress_ramp, stress_mixed 시나리오). v1 스트레스 결과와 비교하면 아키텍처 전환의 한계점 차이를 확인할 수 있습니다. 핵심 비교: v1 stress_mixed의 20% 동기 OCR 쓰기 vs v2 stress_mixed의 20% 차량등록 쓰기

전체 결과 (Prometheus Remote Write 활성, Grafana 메트릭 기록됨)

테스트 후 기록

응답 시간 분포

메트릭 avg med p(90) p(95) max
전체 (req_duration) - - - - -
읽기 (read_latency) - - - - -
쓰기 (write_latency) - - - - -

Phase별 에러율

Phase Check 성공률 실패율
stress_ramp (50 VUs, 읽기) status is 200 - -
stress_mixed (50 VUs, 읽기) read 200 - -
stress_mixed (50 VUs, 쓰기) write 201 - -

테스트 환경 영향 참고: k6와 Prometheus Remote Write가 동일 인스턴스(e2-small, 2 vCPU)에서 실행되어, k6의 요청 생성 속도가 제한됩니다. 이로 인해 서버에 실제 도달하는 부하가 줄어 에러율은 낮아지나, 시스템 전체 리소스 경합으로 응답 시간(p95)은 증가합니다.

[스크린샷: k6 Grafana 대시보드 - VUs 변화에 따른 응답시간/에러율 그래프]

[스크린샷: Container Metrics - speedcam-app의 CPU/Memory 그래프 (스트레스 테스트 구간)]

부하 수준별 성능 비교 (실측)

VUs 시나리오 p95 에러율 처리량 판정
15 spike_resilience - - - -
50 stress_ramp - - - -
50 stress_mixed - - - -

핵심 발견:

테스트 후 기록


4.4 MQTT 이벤트 파이프라인 테스트

실제 Edge Device에서 발생하는 과속 감지 이벤트부터 OCR 처리, 알림 발송까지 End-to-End 파이프라인 성능을 측정했습니다.

4.4.1 테스트 환경

항목 상세
테스트 방식 단건 순차 발행 (동시 부하 아님)
테스트 샘플 수 5건 (통계적 유의성보다는 파이프라인 각 단계별 동작 검증 목적)
MQTT 발행 위치 speedcam-app (10.178.0.4) → speedcam-mq (10.178.0.7), 동일 VPC
테스트 이미지 한국어 번호판 합성 이미지 10장 (PIL로 생성)
이미지 특징 고대비 흰 배경 + 검정 텍스트 (OCR 최적화)
GCS 버킷 gs://speedcam-bucket-4f918446/detections/
OCR Worker EasyOCR (Korean + English), concurrency=1, Warm 상태 (모델 사전 로딩)
인증 방식 GCE ADC (메타데이터 서버, JSON 키 없음)
측정 방법 각 컨테이너 로그의 타임스탬프 비교 (Loki 수집)

참고: 본 테스트는 동시 다발적인 부하 상황이 아닌, 파이프라인 각 단계의 단위 처리 시간 측정에 초점을 맞추었습니다. 대량 동시 처리 시의 성능은 큐 깊이 증가와 OCR Worker 대기 시간 등의 추가 요소가 발생합니다.

4.4.2 파이프라인 단계별 성능 측정

전체 파이프라인은 다음과 같이 3단계로 구성됩니다:

Stage 1: MQTT 수신 → Detection 생성 → OCR Task 디스패치
Stage 2: AMQP 전달 (Subscriber → OCR Worker)
Stage 3: OCR 처리 (GCS 다운로드 + EasyOCR 추론)

Stage 1: MQTT 수신 → Detection 생성 → OCR Task 디스패치 (Subscriber)

Detection ID MQTT 수신 시각 Detection 생성 OCR 디스패치 총 Subscriber 처리 시간
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -

평균 Subscriber 처리 시간: 테스트 후 기록

  • JSON 파싱 + DB Insert + AMQP Publish 포함
  • Cold Start 시 DB 커넥션 수립 시간 포함으로 이상값 발생 가능 (이후 안정화)

Stage 2: AMQP 전달 (Subscriber → OCR Worker)

Detection ID 디스패치 시각 Worker 수신 시각 AMQP 전달 시간
- - - -
- - - -
- - - -

평균 AMQP 전달 시간: 테스트 후 기록

  • RabbitMQ 내부 라우팅 오버헤드 매우 낮음

Stage 3: OCR 처리 (GCS 다운로드 + EasyOCR 추론)

Detection ID 이미지 OCR 처리 시간 인식 결과 신뢰도 비고
- - - - - -
- - - - - -
- - - - - -
- - - - - -
- - - - - -

OCR 성능 요약:

지표
Cold Start (모델 로딩 포함) 재측정 예정
Warm OCR 평균 재측정 예정
OCR 최대 TPS 재측정 예정
고대비 한국어 번호판 인식률 재측정 예정
평균 신뢰도 재측정 예정

주요 인사이트:

테스트 후 기록


4.4.3 End-to-End 파이프라인 타이밍

전체 파이프라인의 각 단계별 소요 시간을 정리하면 다음과 같습니다.

Edge Device
    ↓ MQTT Publish (network)
RabbitMQ MQTT Plugin
    ↓ Internal routing
Django Subscriber (MQTT → DB → AMQP)
    ↓ (JSON parse + DB insert + AMQP publish)
RabbitMQ AMQP Queue
    ↓ (queue routing)
OCR Worker
    ↓ (GCS download + EasyOCR inference)
DB Update (completed)
    ↓
RabbitMQ domain_events exchange (detections.completed)
    ↓ Choreography
Alert Worker Kombu Consumer
    ↓ send_notification.delay()
Alert Worker Celery gevent pool → FCM Notification

Total E2E: 재측정 예정

병목 지점:

  • OCR Worker: 전체 파이프라인의 지배적 병목 (GCS 다운로드 + EasyOCR 추론)
  • 구체적 수치는 재측정 후 기록

개선 방안:

  1. GPU 인스턴스 전환: CPU → GPU로 OCR 추론 시간 단축 (5s → <1s 목표)
  2. 경량 OCR 모델: PaddleOCR 등 더 빠른 모델 검토
  3. 이미지 전처리: Edge Device에서 고대비 전처리 수행

4.4.4 MQTT 동시 파이프라인 부하 테스트 (3 시나리오)

단건 순차 테스트(4.4.2)에서 측정한 단위 처리 시간을 바탕으로, 20대 카메라가 동시 운영되는 실제 사용 패턴에서의 파이프라인 성능을 3단계 시나리오로 측정했습니다.

중요: 모든 MQTT 테스트는 실제 EasyOCR 환경에서 수행되었습니다 (OCR_MOCK=false).

테스트 구성

시나리오 카메라 수 발행 속도 지속시간 예상 메시지 목적
Normal 20대 1건/분/카메라 (0.33 msg/s) 120초 40건 정상 운영 패턴
Rush Hour 20대 5건/분/카메라 (1.67 msg/s) 120초 200건 러시아워 트래픽
Burst 20대 1건/초/카메라 (20 msg/s) 60초 1,200건 극한 스트레스

공통 설정: 실제 GCS 번호판 이미지 10장 순환 사용, API 통계 폴링 + RabbitMQ 큐 깊이 모니터링, 파이프라인 완료 대기 타임아웃 300초


시나리오별 발행 결과

시나리오 발행 성공 발행 실패 평균 발행 지연 실측 발행 속도
Normal - - - -
Rush Hour - - - -
Burst - - - -

테스트 후 기록


시나리오별 파이프라인 처리 결과 (가설 vs 실측)

지표 Normal 가설 Normal 실측 Rush Hour 가설 Rush Hour 실측 Burst 가설 Burst 실측
발행 성공률 100% - 100% - 100% -
완료율 (300s) 100% - 95% - 100% (drain) -
E2E 완료 시간 60초 - 120초 - 300초 -
OCR 큐 피크 < 5 - < 50 - 200-500 -
DLQ 메시지 0 - 0 - 0 -

가설은 OCR_MOCK=true 기준으로 작성. 실제 EasyOCR 환경에서의 실측값은 테스트 후 기록.


Normal 시나리오 - OCR 큐 드레인 추이

시간(s)  완료  대기  OCR큐  FCM큐  실효 처리속도
──────────────────────────────────────────────
  10       -     -     -     -     -
  50       -     -     -     -     -
 100       -     -     -     -     -
 150       -     -     -     -     -
 200       -     -     -     -     -
 250       -     -     -     -     -
 300       -     -     -     -     -

Rush Hour 시나리오 - OCR 큐 드레인 추이

시간(s)  완료  대기  OCR큐  FCM큐
──────────────────────────────────
  10       -     -     -     -
  60       -     -     -     -
 120       -     -     -     -
 180       -     -     -     -
 240       -     -     -     -
 300       -     -     -     -

Burst 시나리오 - OCR 큐 드레인 추이

시간(s)  완료  대기   OCR큐    FCM큐
──────────────────────────────────────
  10       -     -      -        -
  60       -     -      -        -
 120       -     -      -        -
 180       -     -      -        -
 240       -     -      -        -
 300       -     -      -        -

[스크린샷: RabbitMQ 대시보드 - OCR 큐 깊이 변화 (3 시나리오 전체 구간)]

[스크린샷: Celery Workers 대시보드 - OCR Task 처리 속도 (테스트 구간)]

[스크린샷: Container Metrics - speedcam-ocr CPU/Memory (테스트 구간)]


핵심 발견 — OCR 처리 속도 비교

지표 단건 (4.4.2) Normal Rush Hour Burst
OCR 처리 속도 - - - -
OCR 큐 피크 - - - -
파이프라인 완료율 - - - -
부하 시 성능 저하 - - - -

동시 부하 시 OCR 처리 속도 저하 원인 분석:

  1. 메모리 압박: e2-small(2GB)에서 EasyOCR 모델(1.5GB) + 큐 버퍼 → 메모리 여유분 소진
  2. GCS 다운로드 경합: 연속 다운로드 시 네트워크/API 지연 증가
  3. CPU 경합: OCR 추론 중 Celery 큐 관리 오버헤드
  4. 큐 백로그 누적: Rush Hour/Burst 후 큐 드레인에 수 시간 소요

결론: OCR Worker가 전체 파이프라인의 지배적 병목. OCR Worker 확장(수평 또는 GPU 전환)은 선택이 아닌 필수입니다.


4.5 v1 부하 테스트 결과 (Before — 동기 OCR 아키텍처)

4.5.1 테스트 메타데이터

항목 상세
테스트 일시 2026-02-19 14:03:38 ~ 14:18:02 UTC (KST 23:03:38 ~ 23:18:02)
TEST_ID v1-baseline-1771509818
인스턴스 speedcam-v1-app (10.178.0.8)
모니터링 Prometheus / Grafana at 10.178.0.9 / 10.178.0.9:3000
총 소요 시간 14분 24.1초
시나리오 수 6개
최대 VUs 50
종료 코드 99 (임계치 위반 3건 — 정상 범위)

측정 도구별 수치 참고: v1 OCR 응답시간 수치는 Grafana Django Application Metrics 대시보드에서 관측된 값(서버 측 histogram 기반)을 사용합니다. k6 커스텀 메트릭 원본 값(avg 12.17s, p95 20.83s)과 차이가 있으며, 이는 Django histogram 버킷 보간과 측정 범위 차이 때문입니다.

4.5.2 임계치(Threshold) 검증 결과

임계치 설정 기준: v1 임계치는 "이 정도면 합격"이라는 품질 기준이 아니라, v1 동기 OCR 아키텍처의 한계를 정량적으로 드러내기 위한 측정 기준입니다. 특히 스트레스 시나리오의 기준은 의도적으로 관대하게 설정하여, 관대한 기준조차 통과하지 못하는 항목이 곧 아키텍처 전환이 필요한 근거가 됩니다.

임계치 기준 실측 판정 기준 설정 근거
차량 목록 조회 p95 < 300ms 215.87ms PASS 단순 DB 페이지네이션 조회
대시보드 응답 p95 < 200ms 2.66s FAIL 순수 읽기, OCR 없는 정상 응답 기대
전체 에러율 < 5% 0.55% PASS 전체 요청 대비 허용 실패율
에러율 (대시보드 폴링) < 1% 0.00% PASS 읽기 전용, 실패 불허
에러율 (스파이크) < 10% 0.00% PASS 15 VUs 급증 시 큐잉 타임아웃 허용
에러율 (스트레스 혼합) < 30% 19.51% PASS 50 VUs + OCR → 시스템 붕괴 관측 목적
에러율 (스트레스 읽기) < 20% 0.00% PASS 50 VUs 읽기 과부하 허용
에러율 (동기 OCR) < 20% 0.00% PASS 실제 이미지 OCR, 네트워크 실패만 허용
스파이크 응답 p95 < 2s 128.06ms PASS 15 VUs 큐잉 포함
동기 OCR 응답 p95 < 10s 24.2s FAIL EasyOCR CPU 추론 ~3s 기준, 관대하게 10s
스트레스 읽기 p95 < 5s 337.41ms PASS 50 VUs 극한 큐잉 허용
스트레스 쓰기 p95 < 30s 30s FAIL k6 요청 타임아웃 상한 = 사실상 "타임아웃 전 완료" 기준

FAIL 분석:

FAIL 항목 원인 의미
대시보드 응답 p95 (2.66s > 200ms) OCR이 HTTP 스레드를 점유하면 읽기 요청도 대기 OCR 부하가 읽기 API에 전파되는 구조적 문제
동기 OCR 응답 p95 (24.2s > 10s) GCS 다운로드 + EasyOCR 추론이 HTTP 스레드에서 동기 실행 관대한 10s 기준도 2.4배 초과
스트레스 쓰기 p95 (30s = 30s) 50 VUs 혼합 부하에서 OCR 요청이 타임아웃 상한에 도달 타임아웃까지 허용해도 FAIL → 사실상 처리 불가

4.5.3 커스텀 메트릭 응답 시간

메트릭 avg min med max p90 p95
차량 목록 조회 160.1ms 9.87ms 27.71ms 11.64s 91.73ms 215.87ms
대시보드 응답 402.28ms 9.87ms 23.56ms 11.64s 133.26ms 2.66s
동기 OCR 응답 15.1s 6.16s 15.4s 25s 20s 24.2s
스트레스 읽기 837.57ms 9.01ms 22.41ms 60s 97.43ms 337.41ms
스트레스 쓰기 (OCR) 26.93s 13.27s 30s 30s 30s 30s
미확인 목록 조회 127.66ms 15.36ms 30.62ms 12.18s 105.8ms 149.37ms

stress_write_duration avg=26.93s, med=30s — 대부분의 OCR 동기 쓰기 요청이 타임아웃 상한(30s)에 도달했음을 나타냅니다.


4.5.4 시나리오별 상세 분석

A. dashboard_polling — 대시보드 폴링

  • 결과: 에러율 0% (0/108), 정상 완료
  • 응답 시간: dashboard_req_duration p95=2.66s (임계치 200ms 초과 FAIL)
  • 인사이트: VUs=3의 낮은 부하에서도 p95가 2.66s에 달했습니다. 대다수 요청의 med=23.56ms임을 감안하면 일부 요청이 OCR 처리 중인 HTTP 스레드 대기로 인해 극단적인 응답 지연을 겪었음을 나타냅니다. 동기 OCR이 HTTP 스레드를 점유하는 구조적 문제가 폴링 요청에도 직접 영향을 미쳤습니다.

[캡처 A-1] k6 Prometheus Dashboard → HTTP Request Duration 패널

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: dashboard_polling 구간(초기 2분) p95 응답시간 분포, OCR 요청 발생 시 폴링 응답시간 급등 여부

B. sync_ocr_stress — 동기 OCR 핵심 병목

  • 결과: 에러율 0% (0/12), 정상 완료
  • 응답 시간: OCR POST avg=15.1s, p95=24.2s (임계치 10,000ms FAIL)
  • 인사이트: HTTP 스레드에서 동기적으로 EasyOCR을 실행하는 구조에서 단 12건의 OCR 요청만으로도 평균 15.1초가 소요되었습니다. OCR이 HTTP 스레드를 점유하는 동안 다른 모든 요청이 큐잉되어 대기합니다. 요청 수가 적어 에러율 0%를 달성했지만, 처리 시간 자체가 임계치를 2.4배 초과하는 병목을 확인했습니다.

[캡처 B-1] k6 Prometheus Dashboard → ocr_req_duration 패널

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: sync_ocr_stress 구간의 OCR 응답시간 분포, avg 15.1s / p95 24.2s 확인

[캡처 B-2] Container Metrics → speedcam-v1-app CPU 사용률

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: OCR 동기 처리 구간의 CPU 점유율, Gunicorn 스레드 포화 여부

C. mixed_workload — 혼합 워크로드

  • 결과: 전체 에러율 0.55% (40/7248)의 대부분이 stress_mixed에 집중
  • 인사이트: 읽기와 OCR 쓰기가 혼합된 환경에서 OCR 동기 처리가 읽기 요청의 응답시간에도 영향을 미쳤습니다. 별도 커스텀 메트릭이 없어 전체 http_req_duration 기준으로 평가됩니다.

D. spike_resilience — 스파이크 내성

  • 결과: 에러율 0% (0/1731), 완전 성공
  • 응답 시간: p95=128.06ms, avg=51.31ms
  • 인사이트: 읽기 전용 스파이크(0→15 VUs)에서는 우수한 성능을 보였습니다. OCR 요청이 없는 순수 읽기 부하에서는 v1 아키텍처도 안정적으로 동작합니다. 이는 OCR 동기 처리가 정확히 병목임을 역설적으로 증명합니다.

[캡처 D-1] k6 Prometheus Dashboard → spike_resilience 구간 VUs 및 응답시간

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: VUs 급증 시 응답시간 변화, p95=128.06ms 확인

E. stress_ramp — 50 VUs 읽기 전용 스트레스

  • 결과: 에러율 0% (0/5022), 완전 성공
  • 응답 시간: stress_read_duration p95=337.41ms (임계치 5,000ms PASS)
  • 인사이트: 읽기 전용 요청에서 50 VUs까지 에러 없이 처리했습니다. stress_read_duration의 max=60s는 타임아웃 발생을 나타내지만 p95=337ms로 대부분 정상 처리되었습니다. OCR이 개입하지 않으면 v1도 50 VUs 읽기 부하를 수용할 수 있음을 확인했습니다.

[캡처 E-1] k6 Prometheus Dashboard → stress_ramp 구간 응답시간 및 VUs

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: 0→50 VUs 램프업 구간의 응답시간 추이, stress_read_duration p95=337ms 확인

F. stress_mixed — 50 VUs 혼합 (핵심 발견)

  • 결과: 에러율 19.51% (40/205) — 이 테스트의 핵심 발견
  • 응답 시간: stress_write_duration p95=30s, avg=26.93s (임계치 30,000ms FAIL)
  • 체크 성공률:
    • 스트레스 혼합 읽기 200: 91% (155/170) — 9% fail
    • 스트레스 혼합 OCR POST 성공: 28% (10/35) — OCR 쓰기 요청 72% 실패
  • 인사이트: 50 VUs에서 80% 읽기 + 20% OCR 쓰기가 혼합될 때 시스템이 사실상 붕괴합니다. OCR POST 성공률 28%는 동기 OCR이 HTTP 스레드를 점유하여 4개의 Gunicorn 스레드가 포화 상태가 됨으로써 나머지 요청이 모두 타임아웃되는 구조적 한계를 보여줍니다. 이 데이터가 v2 비동기 EDA 전환의 핵심 근거입니다.

[캡처 F-1] k6 Prometheus Dashboard → stress_mixed 구간 에러율 및 응답시간

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: stress_mixed 구간 에러율 급등(19.51%), OCR 쓰기 응답시간 30s 도달, 읽기 응답시간 동반 상승 여부

[캡처 F-2] Container Metrics → speedcam-v1-app CPU / Memory

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: stress_mixed 구간 CPU 포화, 메모리 압박, Gunicorn 스레드 포화 지표

Django Application Metrics NaN 공백 — 서버 측 병목 증거:

stress_mixed 구간에서 k6 OCR POST Response Time 패널은 연속 데이터(30s)가 존재하지만, Django OCR POST Latency 패널에는 약 1분간 NaN 공백이 발생합니다. k6는 클라이언트 측 timeout을 기록하지만, Django histogram은 응답 완료 시에만 counter가 증가하기 때문입니다. 50 VU 혼합 부하에서 4개 Gunicorn 스레드가 전부 OCR에 점유되면 새 응답 완료가 없는 구간이 발생하고, rate(histogram[5m])=0histogram_quantile=NaN이 됩니다. 이 NaN 공백 자체가 v1 동기 처리 병목의 직접적 증거이며, 대시보드 폴링 p95가 2.66s(임계치 200ms의 13배)로 치솟는 현상과 같은 근본 원인입니다.


4.5.5 전체 HTTP 요약

항목
총 요청 수 7,248건
전체 처리량 8.39 req/s
http_req_failed 0.55% (40/7,248)
http_req_duration avg 811.67ms
http_req_duration med 23.31ms
http_req_duration p90 102.26ms
http_req_duration p95 351.21ms
http_req_duration max 60s
전체 iterations 6,056 완료 / 6 중단
checks 성공률 99.44% (7,220/7,260)

4.5.6 핵심 발견

  • 동기 OCR이 전체 시스템 병목: OCR 처리(avg 15.1s)가 Gunicorn HTTP 스레드를 점유하여 스레드 수(4개) 이상의 동시 OCR 요청 시 시스템 전체가 응답 불가 상태로 전락합니다.
  • stress_mixed에서 OCR POST 성공률 28%: 50 VUs 혼합 부하에서 OCR 쓰기의 72%가 실패합니다. 읽기 요청도 동반 영향을 받아 스트레스 읽기 에러 9%(155/170)가 발생했습니다.
  • 순수 읽기 부하는 안정적: spike_resilience(15 VUs, p95=128.06ms), stress_ramp(50 VUs, p95=337.41ms) 모두 에러율 0%로 OCR이 없으면 v1도 충분한 읽기 성능을 보입니다.
  • v2 비동기 EDA 전환의 정량적 근거 확보: 동기 OCR POST 성공률 28% vs v2의 차량등록(OCR 분리) 쓰기 성공률 비교로 아키텍처 전환 효과를 정량화할 수 있습니다.

[캡처 G-1] Grafana → k6 Prometheus Dashboard 전체 뷰 (14분 테스트 전 구간)

  • Grafana URL: http://10.178.0.9:3000
  • 시간 범위: 2026-02-19 23:03:00 ~ 23:19:00 KST
  • 확인 포인트: 6개 시나리오 전환 시점, stress_mixed 구간의 응답시간 및 에러율 급등, 전체 VU 추이

5. 아키텍처 비교 분석 (Before vs After)

Event Driven Architecture 전환을 통해 기존 모놀리식 구조의 모든 핵심 문제를 해결했습니다.

5.1 성능 비교

대시보드 폴링 비교 (시나리오 A)

메트릭 v1 (동기 OCR) v2 (비동기 EDA) 개선율 비고
dashboard p95 2.66s - - 3 VUs, 동일 조건
목록 조회 p95 215.87ms - - v1:cars / v2:detections
미처리 목록 p95 149.37ms - - v1:unchecked / v2:pending
에러율 0% - -

혼합 워크로드 비교 (시나리오 C)

메트릭 v1 (동기 OCR) v2 (비동기 EDA) 개선율 비고
읽기 p95 351.21ms (전체 http p95) - - 0→9 VUs, 별도 커스텀 메트릭 없음
쓰기 p95 24.2s (OCR POST p95) - - v1:OCR POST / v2:차량등록
에러율 0.55% (전체) - -

스파이크 내성 비교 (시나리오 D)

메트릭 v1 (동기 OCR) v2 (비동기 EDA) 개선율 비고
p95 응답시간 128.06ms - - 0→15 VUs, 읽기 전용
에러율 0% - -
최대 RPS - - - 테스트 후 기록

스트레스 테스트 비교 (시나리오 E, F)

메트릭 v1 (동기 OCR) v2 (비동기 EDA) 개선율 비고
stress_ramp 읽기 p95 337.41ms - - 0→50 VUs, 읽기 전용
stress_ramp 에러율 0% - -
stress_mixed 읽기 p95 - (전체 p95 351.21ms) - - 0→50 VUs, 80% 읽기
stress_mixed 쓰기 p95 30s - - v1: OCR POST / v2: 차량등록
stress_mixed 에러율 19.51% - -
안정 최대 TPS - - - 테스트 후 기록

핵심 비교 포인트: stress_mixed 시나리오에서 v1의 20% OCR 쓰기가 전체 시스템 응답시간에 미치는 영향 vs v2에서 OCR이 분리되어 쓰기(차량등록)가 시스템에 미치는 영향이 최소화되는 차이를 확인하세요.

비교 기준 참고: v1 수치는 depoly-v1/k6/load-test-v1.js 실행 결과, v2 수치는 backend/docker/k6/load-test.js 실행 결과입니다.

5.2 OCR 처리 방식 비교

비교 항목 v1 (동기 HTTP) v2 (비동기 MQTT+AMQP)
OCR 실행 위치 Django HTTP 스레드 내 전용 ocr-worker (별도 인스턴스)
HTTP 스레드 점유 OCR 완료까지 점유 (3~10초) OCR과 무관 (즉시 응답)
Edge Device 블로킹 응답 대기 3초+ MQTT PUBACK 즉시 (<1ms)
OCR 부하 시 API 영향 전체 API 응답시간 증가 API 영향 없음
동시 OCR 처리 Gunicorn 스레드 수에 종속 Worker concurrency로 독립 제어
OCR 장애 시 API 전체 장애 API 정상, OCR 큐에 보존
확장 방법 Django 서버 전체 스케일 아웃 OCR Worker만 독립 스케일 아웃

stress_mixed에서의 차이:

  • v1: 20% OCR POST → HTTP 스레드 3~10초 점유 → 나머지 80% 읽기 요청도 큐잉 → 시스템 전체 응답시간 급등
  • v2: 20% 차량등록 POST → <300ms 처리 → 읽기 요청에 영향 미미 → 시스템 안정

5.2.1 인프라 비용 대비 성능 비교

항목 v1 (모놀리식) v2 (분산 EDA) 비고
인스턴스 수 1 (e2-standard-2) 6 (e2-small)
총 vCPU 2 12 6배
총 RAM 4 GB 12 GB (+ e2-medium 4GB DB) 4배
HTTP API TPS - - [테스트 후 기록]
이벤트 처리량 동기 OCR 제약 - [테스트 후 기록]
장애 격리 불가 (모놀리식) 컴포넌트별 격리 구조적 개선
독립 확장 불가 Worker별 확장 구조적 개선

5.3 아키텍처 전환 핵심 성과

graph LR
    subgraph Before["기존 아키텍처"]
        B1["Django<br/>(API + OCR)"]
        B2["3초+ 응답"]
        B3["HTTP 오버헤드"]
        B4["장애 전파"]
        style B1 fill:#ffcccc
        style B2 fill:#ffcccc
        style B3 fill:#ffcccc
        style B4 fill:#ffcccc
    end

    subgraph After["Event Driven Architecture"]
        A1["Django<br/>(API만)"]
        A2["[실측값]ms 처리"]
        A3["MQTT+AMQP"]
        A4["장애 격리"]
        style A1 fill:#90EE90
        style A2 fill:#90EE90
        style A3 fill:#90EE90
        style A4 fill:#90EE90
    end

    Before -->|"아키텍처 전환"| After
Loading

문제별 해결 방법

기존 문제 해결 방법 효과
OCR 동기 처리 OCR Worker 분리 + AMQP 비동기 처리 이벤트 처리시간 대폭 단축 (재측정 예정)
Edge Device 블로킹 MQTT QoS 1 + 즉시 ACK 연속 감지 가능, 데이터 유실 방지
HTTP IoT 통신 MQTT 프로토콜 도입 경량 프로토콜, 메시지 보장, 오프라인 버퍼링
장애 전파 컴포넌트 분리 + 이벤트 큐 보존 OCR 장애 시에도 API 정상 운영

6. 모니터링 지표

6.1 Grafana 대시보드

총 7개의 커스텀 대시보드를 운영하여 시스템의 모든 계층을 모니터링합니다.

대시보드 용도
k6 Prometheus Dashboard HTTP API 메트릭 실시간 시각화
System Overview 전체 시스템 리소스 현황
Container Metrics Docker 컨테이너별 CPU/Memory/Network
MySQL Performance 쿼리 성능, 커넥션, 슬로우 쿼리
RabbitMQ Monitoring 메시지 큐 깊이, 처리량, 컨슈머
Celery Workers Task 처리량, 지연 시간, 실패율
Application Logs Loki 기반 통합 로그 검색

[스크린샷: System Overview 대시보드 - 6개 인스턴스 CPU/Memory 전체 현황]

[스크린샷: MySQL Performance 대시보드 - 커넥션 수 변화 (부하 테스트 구간)]

6.2 Prometheus 타겟 상태

총 11개 타겟 (All UP)

타겟 인스턴스 상태
cAdvisor speedcam-app ✅ UP
cAdvisor speedcam-db ✅ UP
cAdvisor speedcam-mq ✅ UP
cAdvisor speedcam-ocr ✅ UP
cAdvisor speedcam-alert ✅ UP
cAdvisor speedcam-mon ✅ UP
django speedcam-app ✅ UP
mysql speedcam-db ✅ UP
rabbitmq speedcam-mq ✅ UP
celery speedcam-ocr ✅ UP
otel speedcam-mon ✅ UP

[스크린샷: Prometheus → Status → Targets 페이지 (11개 타겟 All UP)]

6.3 로그 수집 현황

총 16개 컨테이너 로그 수집 (Promtail → Loki)

  • Django, Gunicorn, Celery Workers
  • MySQL, RabbitMQ
  • Traefik, Flower
  • Prometheus, Grafana, Loki, Jaeger, OpenTelemetry Collector

7. 최대 TPS 및 용량 분석

각 컴포넌트별 최대 처리 성능과 병목 지점을 분석했습니다.

7.1 컴포넌트별 최대 TPS

컴포넌트 이론값 실측값 근거 병목 요인
HTTP API (Django) ~80-100 req/s - 재측정 예정 Gunicorn 4 handlers + k6 리소스 경합
HTTP API (15VUs) - - 재측정 예정 sleep 간격으로 낮은 req/s, 응답은 빠름
MQTT Subscriber ~40 msg/s - 재측정 예정 단일 스레드 loop_forever()
MQTT Publish - - 재측정 예정 지연 무시 가능
AMQP Broker ~10,000 msg/s - RabbitMQ 공식 벤치마크 참고 충분한 여유 (병목 없음)
OCR Worker (단건) ~0.2 msg/s - 재측정 예정 EasyOCR CPU 추론
OCR Worker (부하 시) - - 재측정 예정 메모리 압박 + GCS 경합
Alert Worker ~100 msg/s - 추정 (Celery gevent concurrency=100 설정) FCM API 호출
MySQL ~500 qps - 추정 (e2-medium 벤치마크) e2-medium 4GB RAM

참고: HTTP 실측값은 k6가 동일 인스턴스(e2-small)에서 실행된 결과로 별도 클라이언트 사용 시 더 높을 수 있음. OCR Worker가 전체 파이프라인의 지배적 병목으로 예상.

7.2 파이프라인 전체 병목

현재 병목: OCR Worker

graph LR
    A["HTTP API<br/>(재측정 예정)"] ~~~ B
    B["MQTT Subscriber<br/>(재측정 예정)"] -->|"병목"| C["OCR Worker<br/>(재측정 예정)"]
    C --> D["Alert Worker<br/>~100 msg/s (추정)"]

    style C fill:#ff6666
Loading

병목 분석 (구조 기반):

  • OCR Worker가 전체 파이프라인의 지배적 병목으로 예상
  • e2-small(2GB)에서 EasyOCR concurrency=1만 가능 (메모리 제약)
  • 동시 부하 시 메모리 압박 + GCS 경합으로 성능 저하 발생 예상
  • 구체적 수치는 테스트 수행 후 기록

해결 방안:

방법 예상 개선 비용 난이도
OCR 인스턴스 추가 (horizontal) 처리량 N배 향상 낮음
GPU 인스턴스 전환 OCR 추론 시간 대폭 단축 (CPU 대비 5x+ 목표)
e2-medium 업그레이드 메모리 여유로 부하 시 성능 저하 완화 낮음
경량 OCR 모델 (PaddleOCR) ~2-3x 빠름
Edge 전처리 이미지 크기 감소 낮음

8. 발견된 이슈 및 개선점

8.1 해결된 이슈

이슈 원인 해결 방법
MQTT Subscriber Stale DB Connection 장기 실행 스레드에서 MySQL 연결 만료 close_old_connections() 추가로 해결
OCR Worker OOM EasyOCR 모델 × 4 workers = 6GB (e2-small 2GB 초과) concurrency=1로 조정
GCS 인증 JSON 키 파일 없음 ADC(메타데이터 서버) 활용으로 해결

8.2 개선이 필요한 부분

8.2.1 긴급 (High Priority)

이슈 현재 상태 영향도 개선 방안
FCM 토큰 업데이트 API PATCH 엔드포인트 0% 성공률 🔴 High API endpoint 로직 수정
OCR Worker 확장성 단일 워커, 메모리 제약으로 concurrency=1 🔴 High GPU 인스턴스 또는 경량 OCR 모델 검토
모니터링 인스턴스 메모리 264MB 여유 (메모리 부족 위험) 🟡 Medium e2-medium 업그레이드 권장

8.2.2 최적화 (Medium Priority)

이슈 현재 상태 영향도 개선 방안
CONN_MAX_AGE 미설정 매 요청 새 DB 커넥션 🟡 Medium 커넥션 풀링 설정 (성능 10-20% 개선 예상)
MQTT Subscriber 단일 스레드 병목 시 메시지 큐잉 🟡 Medium 스레드풀 or 멀티 프로세스 검토
speedcam-ocr 디스크 사용률 87% (17GB/20GB) 🟡 Medium 디스크 정리 또는 확장

8.2.3 장기 개선 (Low Priority)

항목 목표 예상 효과
읽기 복제본 추가 MySQL 읽기 부하 분산 쿼리 성능 향상
Redis 캐싱 통계 조회 캐싱 API 응답 속도 향상
Celery Beat 추가 주기적 작업 자동화 운영 효율성 향상

9. 결론

9.1 핵심 성과

SpeedCam 시스템은 기존 동기식 HTTP 기반 모놀리식 아키텍처에서 Event Driven Architecture로 성공적으로 전환하였습니다.

정량적 성과:

지표 v1 (동기 OCR) v2 (비동기 EDA) 개선 근거
이벤트 처리 시간 OCR avg 15.1s (동기) 재측정 예정 - v1: 실측 / v2: 재측정 예정
Edge Device 블로킹 OCR 완료까지 대기 (avg 15.1s) 0ms 완전 해소 v1: 실측 / v2: MQTT PUBACK
HTTP API p95 (스파이크) 128.06ms (읽기 전용) 재측정 예정 - v1: spike_resilience 실측
OCR 동시 처리 성공률 28% (stress_mixed, 50 VUs) 재측정 예정 - v1: 실측 / v2: 재측정 예정
메시지 보장 없음 QoS 1 무손실 프로토콜 사양
스파이크 에러율 0% (읽기 전용) / 19.51% (OCR 혼합) 재측정 예정 - v1: 실측
stress_mixed OCR 에러율 19.51% 재측정 예정 - v1: 실측 (28% OCR POST 성공)

정성적 성과:

  1. 장애 격리: OCR 장애 시에도 API 정상 운영 가능
  2. 독립 확장: Worker별 독립적 스케일 아웃
  3. 완전한 관측성: Prometheus + Grafana + Loki + Jaeger 통합 모니터링
  4. IoT 최적화: MQTT QoS 1로 메시지 전달 보장
  5. Choreography 패턴: OCR Worker → Alert Worker 직접 domain event 전달 (Main Service 불개입)

9.2 개선 로드맵

Phase 1: 즉시 개선 (1-2주)

  • FCM 토큰 업데이트 API 버그 수정
  • CONN_MAX_AGE 설정으로 DB 커넥션 풀링 활성화
  • speedcam-ocr 디스크 정리

Phase 2: 성능 개선 (1개월)

  • OCR Worker GPU 인스턴스 전환 (5s → <1s 목표)
  • 모니터링 인스턴스 e2-medium 업그레이드
  • MQTT Subscriber 멀티스레딩 구현

Phase 3: 장기 최적화 (2-3개월)

  • Redis 캐싱 레이어 추가
  • MySQL 읽기 복제본 구성
  • Celery Beat 스케줄러 추가
  • 이미지 전처리 파이프라인 구축

9.3 최종 평가

SpeedCam 프로젝트는 Event Driven Architecture를 통해 기존 모놀리식 구조의 근본적 한계를 극복하고, 실시간 IoT 시스템으로서 요구되는 높은 응답성, 메시지 보장, 장애 격리를 모두 달성했습니다.

특히 완전한 비동기 처리, Choreography 패턴 기반 도메인 이벤트 흐름 (OCR Worker → Alert Worker Kombu Consumer), 컴포넌트별 독립 확장이라는 핵심 목표를 성공적으로 구현하여, 프로덕션 환경에서 안정적으로 운영 가능한 시스템으로 발전했습니다.

앞으로 OCR Worker GPU 전환과 DB 커넥션 풀링 최적화를 통해 더욱 빠르고 효율적인 시스템으로 발전할 것으로 기대됩니다. 정량적 성과 수치는 재측정 후 기록됩니다.


문서 버전: 3.0 최종 수정일: 2026-02-19 테스트 일시: 2026-02-12 (k6 HTTP 4시나리오 + MQTT 3시나리오) 작성자: SpeedCam Backend Team 관련 문서: ARCHITECTURE_COMPARISON.md