비동기 Executor 안정화 및 AI 연동 실패 처리 전환#257
Conversation
e5d1aea to
5ce77ed
Compare
AI 연동 실패를 명시적 예외로 전환하여 실패 흐름을 명확화. - 통신 오류는 EXTERNAL_API_COMMUNICATION_ERROR로 전파 - 빈 응답은 EmptyAiResponseException으로 구분, 재시도에서 제외 - 변경된 실패 처리에 맞게 단위 테스트 수정
5ce77ed to
0e71134
Compare
📊 코드 커버리지 리포트
|
|
현재 AI 호출 수를 기록하는 |
| executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); | ||
|
|
||
| executor.setWaitForTasksToCompleteOnShutdown(true); | ||
| executor.setAwaitTerminationSeconds(30); | ||
|
|
||
| executor.setTaskDecorator(taskDecorator); | ||
|
|
||
| executor.initialize(); | ||
| return executor; | ||
| } |
There was a problem hiding this comment.
CallerRunsPolicy 자체가 잘못된 건 아니지만, 현재처럼 AI 연동이나 후속 이벤트 처리까지 같은 전역 async executor에 태우는 구조에서는 포화 시 비동기 작업이 요청/이벤트 스레드에서 직접 실행될 수 있다는 점이 조금 우려됩니다.
이 작업들이 사용자 응답 시간과 분리되어야 하는 성격이라면, 워크로드별로 executor를 분리하거나 포화 시에는 명시적으로 reject/관측 가능하게 처리하는 쪽이 운영상 더 예측 가능할 것 같습니다.
만약 CallerRunsPolicy를 유지하는 게 의도라면, 포화 시에도 호출 스레드 실행을 허용해도 괜찮은 이유가 함께 있으면 좋겠습니다.
There was a problem hiding this comment.
좋은 지적 감사합니다.
먼저 CallerRunsPolicy를 선택한 이유는 해당 executor에 올라가는 작업들의 성격때문입니다.
현재 이 전역 async executor에 태우는 작업들(링크 동기화, 요약 큐 적재, 상태 알림 등)은 대부분 유실되면 안 되는 정합성 작업입니다.
특히 이들은 대부분 @TransactionalEventListener(AFTER_COMMIT)에서 트리거되는데, 여기서 AbortPolicy로 드롭되면 트랜잭션은 이미 커밋됐는데 후속 작업만 유실되어 정합성이 깨져 동기화 누락이나 요약 영구 미실행 등의 문제가 발생할 수 있습니다.
게다가 해당 rejection은 submit 시점에 호출 스레드로 던져져 리스너의 @Retryable로도 잡기 불가능합니다.
이러한 이유로 포화 시 작업을 잃기보다 프로듀서를 느리게 만드는 백프레셔로 CallerRunsPolicy를 택했습니다.
또한 큐의 경우도 bounded(100)으로 설정하여 무한정 쌓이지 않고, 각 설정 값에 대해서도 아래와 같은 의 보수적sizing이라 caller-runs 발동은 지속 포화 상황의 edge 케이스라고 생각됩니다.
- core 5(평상시 상주 스레드 5개)
- max 20(부하 시 최대 20개까지 확장)
- queue 100(스레드가 다 찼을 때 대기 작업을 최대 100개까지 queuing)
다만 말씀하신 워크로드 혼재와 포화 시 요청/이벤트 스레드 지연 문제에 대해서는 충분히 발생 가능한 문제로 확인되어 아래와 같은 방법을 통해 보완하도록 하겠습니다.
- 관측:
applicationTaskExecutor는 자동계측되어executor_active_threads/executor_queued_tasks/executor_pool_size_threads원본 지표가/actuator/prometheus에 노출됩니다. 다만 이걸 보는 대시보드 패널과 포화 알림 룰은 존재하지 않아, executor 포화 패널과 알림 룰(queued가 capacity(100)에·active가 max(20)에 근접)을 이번 PR에 함께 작업해서 올리도록 하겠습니다. - 분리: AI 연동 계열과 이벤트 후속 처리 계열을 별도 executor로 나누는 것에 대해서는 동의합니다. 다만 이번 PR 범위가 "전역 executor bounded화 + graceful shutdown 안정화"라, executor 분리 작업은 별도 이슈를 통해 작업하는 것이 좋을 것 같습니다.
There was a problem hiding this comment.
@Goder-0 말씀 주신 포화 관측 작업 진행했고, PR에도 반영했습니다.
- Grafana: AI/Async 대시보드에 executor 포화 패널 3개 추가 (큐 대기 / 스레드 active·pool·max / 큐 사용률 게이지)
- Prometheus: 포화 알림 룰 2개 추가 (
AsyncExecutorQueueSaturation,AsyncExecutorPoolExhausted,for: 2m)
이제 CallerRunsPolicy가 발동하는 포화 상황을 그래프·알림으로 사전 감지할 수 있습니다.
워크로드별 executor 분리는 이번 PR 범위와 성격이 달라 별도 이슈(#260)로 분리했습니다.
정확한 지적 감사합니다. |
관련 이슈
PR 설명
비동기 처리의 자원 사용을 제어하고, AI 연동 실패를 null 반환에서 명시적 예외로
전환하여 실패 흐름을 명확하게 만들었습니다. 부하 시 자원 통제와 실패 추적의 기반을 마련합니다.
배경
변경 사항
1. 비동기 Executor 안정화
CallerRunsPolicy를 선택했습니다. AI 동기화·요약 작업은 유실되면 안 되므로, 큐·풀이 포화되면 작업을 버리는 대신 호출 스레드가 직접 처리하여 백프레셔로 처리량을 자연스럽게 조절합니다.2. AI 연동 실패 → typed exception 전환
EXTERNAL_API_COMMUNICATION_ERROREmptyAiResponseException(신규)3. 빈 응답 재시도 제외
EmptyAiResponseException)를 별도 타입으로 분리하고@Retryable의noRetryFor에 지정하여, 불필요한 재시도 비용과 외부 서버 부하를 줄였습니다.4. (버그 수정) 비동기 작업 실패 메트릭 집계 위치
async.task.failures카운터가 try 진입 전에 무조건 증가되어, 성공·실패와 무관하게 모든 작업을 실패로 집계하던 버그를 수정했습니다.5. Async Executor 포화 관측
executor_*지표 사용, 애플리케이션 코드 변경 없음)AsyncExecutorQueueSaturation(큐 대기 ≥ 80, 2m),AsyncExecutorPoolExhausted(풀 = max, 2m). 지속 포화 시 CallerRunsPolicy 발동(요청/이벤트 스레드 직접 실행)을 사전 감지.6. 정책상 변경하지 않은 클라이언트 (의도적)
RagTitleClient: 제목 생성 실패 시 기본 제목(truncate) fallback 을 유지합니다. 제목은 부가 기능이므로 예외로 전환하면 채팅 생성 자체가 실패합니다. 부가 기능 실패가 주요 흐름을 막지 않도록 fallback 을 유지했습니다.RagLinkSyncClient: void 반환이며 이미 예외를 전파하고, 호출부(LinkSyncEventListener)가 @retryable·@recover 로 최종 실패 처리까지 갖추고 있어 변경하지 않았습니다.