Repository navigation
[refactor] @Async 풀 분리, 거부정책, MDC 전파로 비동기 작업 격리 - #167
Conversation
외부 I/O와 내부 DB 작업을 별도 스레드 풀로 분리해 상호 간섭을 막는다. - Discord는 externalExecutor, 내부 작업(분석·조회수·응모)은 generalExecutor로 격리 - 풀 포화 시 예외가 요청으로 전파되지 않도록 거부정책 지정 - Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 general을 max 4로 제한 - 종료 시 진행 중 작업을 기다리도록 graceful shutdown 설정
@async는 작업을 다른 풀 스레드로 넘겨 ThreadLocal인 MDC가 끊긴다. 부모 스레드의 MDC를 복사·복원해 async 로그에도 traceId가 남게 한다. - MdcTaskDecorator를 general·external 풀에 적용 - 풀 스레드 재사용 시 이전 요청 컨텍스트가 남지 않도록 실행 후 MDC 정리
| executor.setRejectedExecutionHandler(rejectedHandler); | ||
| executor.setWaitForTasksToCompleteOnShutdown(true); | ||
| executor.setAwaitTerminationSeconds(SHUTDOWN_AWAIT_SECONDS); | ||
| executor.initialize(); |
There was a problem hiding this comment.
참고용) 스프링 빈으로 등록하는 경우에는 자동으로 initialize()가 호출되긴 해
| .threadNamePrefix("Async-") | ||
| .build(); | ||
| public ThreadPoolTaskExecutor externalExecutor() { | ||
| return buildExecutor("ext-", 1, 2, 30, new ThreadPoolExecutor.DiscardPolicy()); |
There was a problem hiding this comment.
커스텀으로 정의한 loggingDiscardPolicy는 로그로 풀 포화 및 작업 유실을 인지할 수 있는데 이거는 조용히 무시하는 거라 인지가 어려울 것 같네
external 스레드 풀은 인지할 필요가 없다고 생각해서 DiscardPolicy를 사용한 거야?
There was a problem hiding this comment.
알림 쪽이라 유실되는 거 몰라도 상관 없다고 생각해서 사용한 거 맞습니당
근데 다시 생각해보니 유실되는 거 알면 좋을 것 같아서 로그 남기는 쪽으로 바꾸는 것도 괜찮을 것 같네여
| } | ||
|
|
||
| @Async | ||
| @Async("generalExecutor") |
There was a problem hiding this comment.
상수화하는 건 어떨까?
스프링 빈 정의할 때도 메서드 네이밍으로 빈 이름을 지정한 것 같은데 만약 메서드 이름이 변경되는 경우 @Async에 설정된 이름을 같이 변경해주는 걸 누락하면 빈 이름이 틀어질 수 있을 것 같아
| @Bean | ||
| public ThreadPoolTaskExecutor generalExecutor() { | ||
| // Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한 | ||
| return buildExecutor("async-", 3, 4, 200, loggingDiscardPolicy()); |
There was a problem hiding this comment.
풀 사이즈와 큐 용량은 운영해보면서 설정 조정해봐도 좋을 것 같아
generalExecutor는 DB에서 조회수 증가시키는 작업을 수행하는 것 같은데, 만약 DB 지연이 발생하면(사용자들 조회가 많다고 가정했을 때) 스레드 3개로 부족할 거고 큐가 빠른 속도로 찰 수 있는데 max는 4개밖에 안 돼서 쉽게 유실될 수 있을 것 같아
물론 조회수 증가라 엄청 크리티컬하지는 않지만
There was a problem hiding this comment.
넵 이거는 일단 이렇게 나눠두고 운영하면서 조정해보려고 했습니다!
ㄹㅈㄷ 비호 |
요약
단일 공용 스레드 풀을 성격별로 2개로 분리(격벽)하고, 풀 포화 시 거부정책을 지정하며, 스레드 경계를 넘어도 요청 traceId가 이어지도록 MDC를 전파한다.
What: 무엇을 바꿨나
@Async스레드 풀을 2개로 분리externalExecutor: Discord 전용(외부 I/O 격리), core 1 / max 2 / queue 30, DiscardPolicygeneralExecutor(기본): 분석, 조회수, 응모, core 3 / max 4 / queue 200, log & discard@Async지점에 사용할 풀을 명시적으로 지정MdcTaskDecorator로 두 풀에 MDC(traceId) 전파 + 실행 후 정리AsyncConfigurationTest), MDC 전파 단위 검증(MdcTaskDecoratorTest)Why: 왜 필요한가
.block())가 스레드를 오래 점유하면 조회수, 분석 같은 짧은 내부 작업까지 같은 큐에 밀려 지연, 유실됐다.AbortPolicy)이라 풀 포화 시RejectedExecutionException이 요청 흐름으로 전파될 수 있었다.@Async로 스레드가 바뀌면 MDC가 끊겨 async 내부 로그를 원 요청과 연결할 수 없었다.How: 어떻게 구현했나
refactor(풀 분리, 거부정책) 다음feat(MDC 전파), 안정성 변경과 관측성 변경을 나눔generalExecutor는 DB 트랜잭션을 점유하므로, Hikari 기본 커넥션 풀(10)을 웹 요청과 나눠 쓰도록 max 4로 제한(웹용 커넥션 확보)TaskDecorator가 제출 스레드의 MDC를 복사해 풀 스레드에서 복원, 재사용 오염 방지를 위해 실행 후clear, [feature] 요청별 traceId MDC 로깅 도입 #163(요청별 traceId)과 연결되어 실효체크리스트
./gradlew build)관련 이슈
기타 / 리뷰 포인트
ViewCountService가@Transactional(REQUIRES_NEW)이고getPlaceDetail의 활성 트랜잭션 안에서 호출되어,CallerRunsPolicy는 커넥션 2개 동시 요구로 Hikari(10) 포화 시 데드락 위험이 있어 배제했다.CallerRunsPolicy가 세트여야 하나 t2.micro라 별도 범위로 둔다. 이 선택이 적절한지 리뷰 요망hikaricp.connections.pending등) 보고 조정 필요