Skip to content

워크로드별 async executor 분리 (AI 연동 / 이벤트 후속 처리) - #262

Merged
ckdals4600 merged 1 commit into
mainfrom
feature/#260-split-excutor
Jul 27, 2026
Merged

워크로드별 async executor 분리 (AI 연동 / 이벤트 후속 처리)#262
ckdals4600 merged 1 commit into
mainfrom
feature/#260-split-excutor

Conversation

@ckdals4600

Copy link
Copy Markdown
Contributor

관련 이슈

PR 설명

느린 AI 연동 작업과 빠른 이벤트 후속 처리가 단일 전역 executor 를 공유하던 구조를 분리했습니다. AI 전용 executor(aiTaskExecutor)를 신설해 느린 작업을 격리하고, 포화 관측(대시보드·알림)을 executor별로 확장했습니다. (#256 PR 리뷰에서 도출)

배경

  • 모든 @Async 작업이 단일 applicationTaskExecutor(core5/max20/queue100)를 공유했습니다.
  • AI 서버 호출(Feign, 최대 60s)과 웹소켓 푸시(ms 단위)가 같은 풀·큐를 쓰므로, AI 호출이 풀을 점유하면 실시간성이 중요한 푸시가 큐에서 대기하고, 포화 시 CallerRunsPolicy 로 호출/이벤트 스레드가 느린 작업을 직접 실행하게 됩니다.
  • #256에서 추가한 executor 포화 지표로 관측 기반은 갖춰졌고, 이번에 구조적 격리를 진행합니다.

워크로드 인벤토리 (@async 전수 조사)

지점 하는 일 소요 배치
LinkSyncEventListener.handleLinkSyncEvent link-sync AI 동기화 (Feign + 재시도) 최대 60s × 3 aiTaskExecutor
RagChatService.generateAnswer 채팅 답변 AI 생성 (Feign) 최대 60s aiTaskExecutor
SummaryStatusEventListener.handleSummaryStatusEvent 웹소켓 푸시 ms applicationTaskExecutor (유지)

LinkEventListener(동기, @async 없음)와 SummaryWorker(전용 스레드)는 executor 를 타지 않아 대상이 아닙니다.

변경 사항

1. aiTaskExecutor 신설

  • core 3 / max 8 / queue 30, prefix ai-async-
  • 사이징 근거: AI 호출은 스레드가 아니라 AI 서버 처리량이 병목입니다. 동시성을 낮게(max 8) 잡고 큐(30)로 흡수합니다 — 스레드를 늘리면 AI 서버에 동시 요청만 몰려 타임아웃을 유발합니다. 초기값이며 포화 지표 관측 후 튜닝합니다(비동기 Executor 안정화 및 AI 응답 실패 처리 전환 #256 컨벤션 동일).
  • CallerRunsPolicy 유지: link-sync 는 유실 불가 정합성 작업(백프레셔 필요), 채팅 답변은 caller-runs 로 동기 강등되어도 사용자가 어차피 응답을 기다리는 요청이라 수용 가능합니다.
  • MdcTaskDecorator·graceful shutdown(30s) 동일 적용. awaitTermination 은 AI 호출(60s)보다 짧지만, 종료 시 잘린 작업은 기존 @Recover/dead-letter 경로로 수습되는 구조이므로 배포 속도를 위해 30s 를 유지했습니다.

2. 기본 executor 는 opt-in 구조로 유지

  • 이름 없는 @Async 는 계속 applicationTaskExecutor(빠른 작업)로 갑니다. 느린 작업만 @Async("aiTaskExecutor")명시 지정합니다.
  • 지정을 누락하면 빠른 풀로 가는데, 이는 포화 지표로 관측되며 반대 방향(빠른 작업이 AI 풀에 섞임)보다 피해가 작습니다.

3. 포화 관측 executor별 확장

  • 큐 포화 알림을 절대값(queued >= 80)에서 사용률 기반(queued / (queued + remaining) >= 0.8)으로 일반화 — executor별 큐 용량(100/30)이 달라도 룰 하나로 커버되고, executor 가 늘어도 정규식에 이름만 추가하면 됩니다.
  • 풀 max 알림·대시보드 패널(큐 사용률/스레드)을 name 라벨로 executor별 분리 표시.

확인

  • ./gradlew test 전체 통과
  • 앱 기동 후 /actuator/prometheus 에서 executor_*{name="aiTaskExecutor"} 지표 노출 확인
  • link-sync·채팅 답변 요청 시 로그 스레드명이 ai-async-* 인 것 확인, 웹소켓 푸시는 async-* 유지 확인
  • Prometheus Status→Rules 에서 확장된 포화 룰 2종 로드 확인, Grafana 패널에서 executor별 분리 표시 확인

참고

  • 코드 내 설명 주석은 main 머지 시 전부 삭제 예정이며, 설계 근거(사이징·CallerRunsPolicy 유지·opt-in 구조)는 본 PR 본문을 기록으로 남깁니다.
  • executor별 사이징 재조정은 운영 포화 지표 축적 후 후속으로 진행합니다.

@ckdals4600
ckdals4600 requested a review from Goder-0 July 17, 2026 01:55
@ckdals4600 ckdals4600 self-assigned this Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

📊 코드 커버리지 리포트

Overall Project 93.83% 🍏
Files changed 100% 🍏

File Coverage
LinkSyncEventListener.java 100% 🍏
RagChatService.java 100% 🍏

@Goder-0

Goder-0 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

현재 PR에 async.task.failuresaction 라벨이 항상 존재한다는 전제의 관측 변경이 포함되어 있습니다.

다만 이 전제는 #261에서 SummaryWorker, LinkEventListener까지 task/action 키셋으로 통일된 뒤에만 성립합니다.
#262 자체 diff에는 해당 메트릭 등록부 변경이 없어서, #262가 #261보다 먼저 머지되면 기존 action 없는 시계열 때문에 알림 summary/description이나 Grafana legend가 비는 문제가 생길 수 있습니다.

순서는 다음 중 하나로 하면 될 것 같습니다.

  1. #261을 먼저 머지한 뒤, #262를 최신 main 기준으로 rebase/merge해서 관측 파일 충돌을 정리한다.
  2. 또는 #262에서는 AsyncTaskFinalFailure와 패널4 변경을 제거하고, executor 분리 및 executor 포화 관측 변경만 남겨 독립 머지 가능하게 만든다.

즉 #262는 현재 상태 기준으로는 #261 이후 상태에 의존하고 있어 머지 순서를 명확히 하거나 diff를 분리하는 게 안전해 보입니다.

@ckdals4600

Copy link
Copy Markdown
Contributor Author

현재 PR에 async.task.failuresaction 라벨이 항상 존재한다는 전제의 관측 변경이 포함되어 있습니다.

다만 이 전제는 #261에서 SummaryWorker, LinkEventListener까지 task/action 키셋으로 통일된 뒤에만 성립합니다. #262 자체 diff에는 해당 메트릭 등록부 변경이 없어서, #262가 #261보다 먼저 머지되면 기존 action 없는 시계열 때문에 알림 summary/description이나 Grafana legend가 비는 문제가 생길 수 있습니다.

순서는 다음 중 하나로 하면 될 것 같습니다.

  1. #261을 먼저 머지한 뒤, #262를 최신 main 기준으로 rebase/merge해서 관측 파일 충돌을 정리한다.
  2. 또는 #262에서는 AsyncTaskFinalFailure와 패널4 변경을 제거하고, executor 분리 및 executor 포화 관측 변경만 남겨 독립 머지 가능하게 만든다.

즉 #262는 현재 상태 기준으로는 #261 이후 상태에 의존하고 있어 머지 순서를 명확히 하거나 diff를 분리하는 게 안전해 보입니다.

정확한 지적 감사합니다. 말씀하신 대로입니다 — 이 브랜치는 #261(feature/#259) 위에서 분기해 작업했고, 관측 파일(AsyncTaskFinalFailure description·패널4의 task/action)도 #261에서 태그 키셋이 task·action으로 통일된 상태를 전제로 반영되어 있습니다. 그래서 #262 diff에 메트릭 등록부 변경 없이 관측 변경만 보이는 것이 맞습니다.

순서는 제안해주신 1번으로 진행하겠습니다. #261을 먼저 머지한 뒤, #262를 최신 main 기준으로 rebase해서 관측 파일 충돌을 정리하고 머지하겠습니다. #261 머지 전까지 이 PR은 머지하지 않겠습니다.

느린 AI 호출(최대 60s)이 기본 executor 를 점유해 빠른 이벤트 처리(웹소켓
푸시)를 지연시키는 것을 격리.

- aiTaskExecutor 신설: core3 / max8 / queue30, prefix ai-async-
  (AI 서버 처리량이 병목이므로 동시성을 낮게 잡고 큐로 흡수)
- 이주: LinkSyncEventListener.handleLinkSyncEvent, RagChatService.generateAnswer
  → @async("aiTaskExecutor"). 빠른 작업(SummaryStatusEventListener)은 기본 유지
- CallerRunsPolicy·graceful shutdown 30s·MdcTaskDecorator 기본 executor 와 동일
- 기본 executor 는 opt-in 구조 유지: 이름 없는 @async 는 빠른 풀로
- 포화 관측 executor별 확장: 큐 포화 알림을 절대값 → 사용률 기반
  (queued/(queued+remaining) >= 0.8)으로 일반화, 패널·룰 name 정규식 분리
@ckdals4600
ckdals4600 force-pushed the feature/#260-split-excutor branch from daa1b1d to a7599ab Compare July 27, 2026 13:56
@ckdals4600
ckdals4600 merged commit 33683e0 into main Jul 27, 2026
2 checks passed
@ckdals4600
ckdals4600 deleted the feature/#260-split-excutor branch July 27, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

워크로드별 async executor 분리 (AI 연동 / 이벤트 후속 처리)

2 participants