- IntegrationVesselService에 전용 DataSource 자체 생성/관리 추가 - vessel.integration.datasource.* 설정으로 별도 DB 연결 가능 - @PostConstruct에서 경량 HikariDataSource 생성 (max 3, min 1) - 미설정 시 queryDataSource 자동 폴백 (기존 동작 유지) - prod: 별도 DB (mdadb2 gis.t_ship_integration_sub) → 전용 DataSource - dev/prod-mpr: queryDB signal 스키마 → queryDataSource 폴백 - 기존 DataSourceConfig 4개 파일 수정 없이 프로파일 완벽 호환 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
211 lines
9.1 KiB
Markdown
211 lines
9.1 KiB
Markdown
# WebSocket 부하 제어 개선 — 구현 진행 상황
|
|
|
|
> 브랜치: `feat/websocket-load-control`
|
|
> 시작일: 2026-02-06
|
|
|
|
---
|
|
|
|
## DB 커넥션 풀 분배 설계 (총 250개)
|
|
|
|
| DataSource | AS-IS | TO-BE | 비고 |
|
|
|------------|-------|-------|------|
|
|
| **Query** | 60 (min 10) | 120 (min 20) | WebSocket 스트리밍 + REST API 주 사용 |
|
|
| **Collect** | 20 (min 5) | 80 (min 15) | 배치 Reader, 신호 수집 조회 |
|
|
| **Batch** | 20 (min 10) | 30 (min 5) | Spring Batch 메타데이터 |
|
|
| **예비** | - | 20 | 운영 여유분 |
|
|
| **합계** | 100 | 250 | DB 서버 500 중 250 사용 |
|
|
|
|
### 글로벌 동시 쿼리 제한 산정
|
|
- Query 풀 120개 / 쿼리당 평균 3커넥션 = 40
|
|
- 보수적 적용: **30개** (REST API, 헬스체크 등에 여유분 확보)
|
|
|
|
---
|
|
|
|
## Phase 1 — 긴급 안정화
|
|
|
|
- [x] **1.1** 글로벌 동시 쿼리 제한 (Semaphore + Fair Queue)
|
|
- ActiveQueryManager에 Fair Semaphore 기반 글로벌 동시 쿼리 제한 추가 (기본 30개)
|
|
- @Async 메서드 내에서 슬롯 획득 (인바운드 채널 블로킹 방지 설계)
|
|
- application-prod.yml에 websocket.query 설정 외부화
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **1.2** 쿼리 완료 시 리소스 반환 보장
|
|
- ChunkedTrackStreamingService finally 블록에 releaseQuery() + releaseQuerySlot() 추가
|
|
- StompTrackStreamingService finally 블록에 releaseQuery() + releaseQuerySlot() 추가
|
|
- 세션별 쿼리 카운트 감소 누락 수정
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **1.3** CachedThreadPool → 제한된 ThreadPoolExecutor 교체
|
|
- CancellableQueryManager: newCachedThreadPool → ThreadPoolExecutor(core:5, max:20, queue:100)
|
|
- CallerRunsPolicy로 큐 포화 시 자연 백프레셔
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **1.4** DB 커넥션 풀 재분배 (prod)
|
|
- Query: 60→120(min 20), Collect: 20→80(min 15), Batch: 20→30(min 5)
|
|
- 총 230/250, 예비 20개
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 2 — 취소 및 정리 로직 완성
|
|
|
|
- [x] **2.1** ChunkedTrackStreamingService 쿼리 취소 구현
|
|
- queryCancelFlags(ConcurrentHashMap<String, AtomicBoolean>) 추가
|
|
- streamChunkedTracks 시작 시 등록, 전략별 루프 전 확인, finally에서 정리
|
|
- cancelQuery()에 실제 플래그 설정 로직 구현 (기존 TODO 해소)
|
|
- isQueryCancelled()에 취소 플래그 통합 확인
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **2.2** 쿼리 관리 시스템 통합
|
|
- StompTrackController의 중복 completeQuery() 제거
|
|
- 리소스 정리를 서비스 finally 블록에서 일괄 처리하도록 단일화
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 3 — 백프레셔 고도화
|
|
|
|
- [x] **3.1** 콜백 기반 버퍼 추적
|
|
- CompletableFuture+Thread.sleep(100) → try-finally 즉시 감소로 전환
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **3.2** 적응형 전송 지연
|
|
- ChunkedTrack: 버퍼 사용률(%) 기반 4단계 적응형 지연 (10~200ms)
|
|
- StompTrack: 큐 사용률 + 데이터 크기 복합 적응형 지연
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 4 — 설정 외부화 및 모니터링
|
|
|
|
- [x] **4.1** WebSocketProperties 설정 클래스
|
|
- @ConfigurationProperties(prefix = "websocket")로 query/transport/backpressure 설정 바인딩
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **4.2** 모니터링 엔드포인트
|
|
- GET /api/websocket/load-control — 글로벌 동시 제한, 대기 큐, 활성 쿼리 상세, 메모리
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 5 — 대기열 기반 쿼리 관리 + 타임아웃 최적화
|
|
|
|
- [x] **5.1** QueryStatusUpdate에 `queuePosition`, `totalInQueue` 필드 추가
|
|
- QUEUED 상태에서 대기열 순번 정보 전달
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.2** ActiveQueryManager 대기열 추적 구현
|
|
- ConcurrentLinkedQueue 기반 대기열 추적
|
|
- `tryAcquireQuerySlotImmediate()`: 즉시 슬롯 획득
|
|
- `waitForSlotWithQueue()`: 2초 간격 슬롯 대기
|
|
- `getQueuePosition()`, `getQueueSize()`: 순번 조회
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.3** ChunkedTrackStreamingService 거부 → 대기열 전환
|
|
- 즉시 슬롯 획득 실패 → QUEUED 상태 2초 간격 전송하며 최대 2분 대기
|
|
- 대기 중에도 순번/전체 큐 크기 안내
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.4** StompTrackStreamingService 동일 패턴 적용
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.5** WebSocketProperties에 SessionProperties 추가
|
|
- idleTimeoutMs, serverHeartbeatMs, clientHeartbeatMs, sockjsDisconnectDelayMs, sendTimeLimitSeconds
|
|
- 기본값을 하드코딩에서 설정 바인딩으로 전환
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.6** WebSocketStompConfig 하드코딩 제거
|
|
- 모든 타임아웃/하트비트/풀 설정을 WebSocketProperties에서 주입
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.7** application-prod.yml 설정 변경
|
|
- Query 풀: 120 → 180, global: 30 → 60, max-per-session: 3 → 20
|
|
- Session idle: 60s → 15s, Heartbeat: 10s → 5s, SockJS disconnect: 30s → 5s
|
|
- Send time limit: 120s → 30s
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **5.8** AsyncConfig 스레드 풀 확장
|
|
- core: 15 → 40, max: 30 → 120, queue: 500 → 100, keepAlive: 40s → 30s
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 6 — 일일 데이터 인메모리 캐시
|
|
|
|
- [x] **6.1** DailyTrackCacheManager 신규 구현
|
|
- @Service + @Async 비동기 캐시 매니저
|
|
- ApplicationReadyEvent에서 비동기 워밍업 (D-1 → D-7 순차 로드)
|
|
- 날짜별 CompactVesselTrack 캐시 (ConcurrentHashMap)
|
|
- 뷰포트 필터링 지원, 다중 날짜 병합 조회
|
|
- splitQueryRange(): 요청 범위 → (캐시구간, DB구간, 오늘구간) 분리
|
|
- refreshAfterDailyJob(): 배치 완료 후 캐시 갱신
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.2** DailyTrackCacheProperties 신규 구현
|
|
- @ConfigurationProperties(prefix = "cache.daily-track")
|
|
- enabled, retentionDays, maxMemoryGb, warmupAsync
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.3** DailyAggregationJobConfig에 캐시 갱신 리스너 추가
|
|
- JobExecutionListener로 배치 완료 후 refreshAfterDailyJob() 호출
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.4** ChunkedTrackStreamingService 캐시 통합
|
|
- processDailyStrategy: 날짜별 캐시 히트 체크 → 메모리 조회 / DB 폴백
|
|
- processQueryInChunks: 동일 캐시 우선 패턴
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.5** StompTrackStreamingService 캐시 통합
|
|
- Daily 전략 처리 시 날짜별 캐시 히트 → 메모리 조회, 미스만 DB 처리
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.6** application-prod.yml 캐시 설정 추가
|
|
- cache.daily-track: enabled=true, retention-days=7, max-memory-gb=5, warmup-async=true
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
- [x] **6.7** WebSocketMonitoringController 캐시 모니터링 엔드포인트
|
|
- GET /api/websocket/daily-cache — 캐시 상태, 날짜별 선박수/메모리, 로드 시각
|
|
- 상태: **완료** (2026-02-06)
|
|
|
|
---
|
|
|
|
## Phase 7 — 통합선박(IntegrationVessel) 전용 DataSource
|
|
|
|
- [x] **7.1** IntegrationVesselService 전용 DataSource 지원
|
|
- `vessel.integration.datasource.*` 설정으로 별도 DB 연결 가능
|
|
- `@PostConstruct`에서 전용 HikariDataSource 생성 (max 3, min 1)
|
|
- 미설정 시 queryDataSource 자동 폴백 (기존 동작 유지)
|
|
- `@PreDestroy`에서 전용 DataSource 정리
|
|
- 상태: **완료** (2026-02-07)
|
|
|
|
- [x] **7.2** 설정 기반 테이블명
|
|
- `vessel.integration.table-name` 프로파일별 분리
|
|
- prod: `t_ship_integration_sub` (gis 스키마는 jdbc-url currentSchema)
|
|
- dev/prod-mpr/local/query: `signal.t_ship_integration_sub` (기본값)
|
|
- 상태: **완료** (2026-02-07)
|
|
|
|
- [x] **7.3** 프로파일 호환성 유지
|
|
- prod: 별도 DB (10.188.141.230:5432/mdadb2 gis) → 전용 DataSource
|
|
- dev/prod-mpr: queryDB signal 스키마 → queryDataSource 폴백
|
|
- local/query: integration 비활성화 (기본값)
|
|
- 기존 DataSourceConfig 4개 파일 수정 없음
|
|
- 상태: **완료** (2026-02-07)
|
|
|
|
---
|
|
|
|
## 커밋 이력
|
|
|
|
| 날짜 | Phase | 커밋 메시지 | 해시 |
|
|
|------|-------|------------|------|
|
|
| 2026-02-06 | 1.1+1.2 | feat: 글로벌 동시 쿼리 제한(Semaphore) 및 리소스 반환 보장 | 78ff307 |
|
|
| 2026-02-06 | 1.3 | fix: CachedThreadPool → 제한된 ThreadPoolExecutor 교체 | 2191671 |
|
|
| 2026-02-06 | 1.4 | perf: DB 커넥션 풀 재분배 (총 250개, prod) | 122a247 |
|
|
| 2026-02-06 | 2.1 | feat: ChunkedTrackStreamingService 쿼리 취소 로직 구현 | e073007 |
|
|
| 2026-02-06 | 2.2 | refactor: 쿼리 생명주기 관리 서비스 finally 블록으로 단일화 | 28908e1 |
|
|
| 2026-02-06 | 3 | perf: 백프레셔 고도화 - 정확한 버퍼 추적 및 적응형 지연 | 7b7e283 |
|
|
| 2026-02-06 | 4 | feat: WebSocket 설정 외부화 및 부하 제어 모니터링 엔드포인트 | c92bf0e |
|
|
| 2026-02-06 | 5 | feat: 대기열 기반 쿼리 관리 및 타임아웃 최적화 | 7bd7bf5 |
|
|
| 2026-02-06 | 6 | feat: 일일 데이터 인메모리 캐시 구현 | 03b14e6 |
|
|
| 2026-02-06 | 5~6 | docs: Phase 5~6 구현 진행 문서 및 성능 보고서 업데이트 | dc586dd |
|
|
| 2026-02-06 | 6 | fix: 캐시-DB 하이브리드 조회 시 뷰포트 2-pass 필터링 정합성 보장 | e9d5d36 |
|