Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- docker
- 검색어 추천
- 구현
- 완전탐색
- 알람 시스템
- piplining
- spring event
- 카카오
- BFS
- JPA
- 주식
- 디버깅
- AWS
- 백준
- jwt 표준
- branch 전략
- 숫자 블록
- 트랜잭샨
- 좋은 코드 나쁜 코드
- 결제서비스
- 쿠키
- prg 패턴
- 객체지향패러다임
- gRPC
- 레디스 동시성
- 이분탐색
- 프로그래머스
- 누적합
- 셀러리
- 깊게 생각해보기
Archives
- Today
- Total
코딩관계론
AWS - cpu utilization over 80%(search service가 자꾸 죽어요...!) 본문
반응형
서버 특정 시간대 오류 분석 및 해결 과정
문제 상황
Search 서버가 특정 시간대에 주기적으로 중단되는 현상이 발생했습니다. 초기에는 빈번하게 발생했지만 시간이 지나며 발생 빈도가 줄어들었습니다. 최근 데이터베이스 마이그레이션 이후 다시 동일한 문제가 발생하여, 원인 분석 및 해결을 진행했습니다.
초기 의심
- 로컬 테스트와 AWS 환경 차이
- 로컬 테스트에서는 문제없이 작동했으나, AWS 환경에서 업로드 후 문제가 발생.
- AWS 환경 문제를 의심하여 top 명령어로 리소스 사용량 로그를 추적했지만 특이점이 발견되지 않았습니다.
- 오류 빈도의 감소
- 시간이 지나며 오류 발생 빈도가 줄어들었기에 급한 일 처리 후 원인 분석을 유보했었습니다.
상세 원인 분석
데이터베이스 마이그레이션 후 오류가 재발하여 다시 분석에 돌입했습니다.
문제가 발생한 코드는 일정 시간마다 주식 가격 정보를 갱신하는 스케줄러 메소드입니다.
@Scheduled(fixedDelay = 300000)
@Transactional
public void saveStockInfoAndChartData() {
Map<String, Stock> stocks = loadEntities(stockRepository.findAll(), Stock::getCode);
updateStockInfo(stocks);
log.info("Renew Stocks Success: {}", stocks.size());
for (Stock stock : stocks.values()) {
//쿼리 한번
StockChart stockChart = stockChartRepository.loadStockChart(stock.getCode())
.orElseGet(() -> new StockChart(stock.getCode(), new ArrayList<>()));
StockChartQueryCommand stockChartQueryConfig = new StockChartQueryCommand(stock,
stockChart.getLastUpdateDate(),
LocalDate.now(AppConfig.ZONE_ID));
StockChart chart = apiServerPort.loadStockChart(stockChartQueryConfig);
stockChart.mergeOhlc(chart);
stockChartRepository.save(stockChart);
}
stockRepository.saveAll(stocks.values());
log.info("All Stocks was renewed: {}", stocks.size());
}
문제 지점: 데이터 로드 시 메모리 과부하
- StockChartQueryCommand는 stockChart.getLastUpdateDate() 값을 기준으로 마지막 업데이트 이후부터 오늘까지의 데이터를 API 서버에 요청합니다.
- 차트 데이터가 없는 경우
- 약 1년치 데이터를 요청.
- 초기 설정 시 200개의 주식 정보 × 365개의 OHLC 데이터를 메모리에 적재하며 CPU 사용량과 메모리 소비가 급증.
- 결국 메모리 초과로 서버가 중단되는 문제가 발생.
해결 방법
트랜잭션 분리
문제 해결을 위해 트랜잭션 전파 옵션을 조정했습니다.
- 기존 트랜잭션 방식의 문제
- 트랜잭션이 전체 saveStockInfoAndChartData 메소드에 걸쳐 있어, 단일 차트 갱신 실패 시 전체 롤백이 발생.
- 메모리 부담이 클 뿐만 아니라 불필요한 롤백이 성능을 저하시킴.
- 트랜잭션 전파 설정: REQUIRES_NEW
- @Transactional(propagation = Propagation.REQUIRES_NEW) 옵션을 사용하여 개별 차트 갱신 로직에 새로운 트랜잭션을 부여.
- 다른 주식 차트의 갱신 정보 저장이 실패하더라도 다른 트랜잭션이 영향을 받지 않음.
@Scheduled(fixedDelay = 300000)
@Transactional
public void saveStockInfoAndChartData() {
Map<String, Stock> stocks = loadEntities(stockRepository.findAll(), Stock::getCode);
updateStockInfo(stocks);
log.info("Renew Stocks Success: {}", stocks.size());
for (Stock stock : stocks.values()) {
//쿼리 한번
StockChart stockChart = stockChartRepository.loadStockChart(stock.getCode())
.orElseGet(() -> new StockChart(stock.getCode(), new ArrayList<>()));
StockChartQueryCommand stockChartQueryConfig = new StockChartQueryCommand(stock,
stockChart.getLastUpdateDate(),
LocalDate.now(AppConfig.ZONE_ID));
StockChart chart = apiServerPort.loadStockChart(stockChartQueryConfig);
stockChart.mergeOhlc(chart);
stockChartRepository.save(stockChart);
}
stockRepository.saveAll(stocks.values());
log.info("All Stocks was renewed: {}", stocks.size());
}
성능 최적화
문제: stockChart.mergeohlc(chart)함수를 호출하면 즉시 insert 쿼리나 발생하는 현상
- StockChart 도메인에 @OneToMany 관계로 설정된 ohlcList가 원인.
- StockChart가 영속 상태이기 때문에 새로운 OHLC 데이터 추가 시, 영속 상태가 되어 즉시 INSERT 쿼리가 발생.
stockChart.mergeOhlc(chart);
public void mergeOhlc(StockChart stockChart) {
Map<LocalDate, OHLC> existingOhlcMap = ohlcList.stream()
.collect(Collectors.toMap(OHLC::getDate, Function.identity()));
for (OHLC stockOhlc : stockChart.getOhlcList()) {
if (this.getLastUpdateDate().isBefore(stockOhlc.getDate())) {
lastUpdateDate = stockOhlc.getDate();
}
OHLC existingOhlc = existingOhlcMap.get(stockOhlc.getDate());
if (existingOhlc == null) {
stockOhlc.addChart(this);
ohlcList.add(stockOhlc);
}
}
}
@OneToMany(fetch = FetchType.LAZY, mappedBy = "chart", cascade = CascadeType.ALL)
@BatchSize(size = 100)
@OrderBy("date ASC")
private List<OHLC> ohlcList = new ArrayList<>();
해결 방안: 트랜잭션 ReadOnly 및 Merge 사용
- 트랜잭션을 readOnly=true로 설정하여 즉시 INSERT를 방지.
- 갱신 저장 시 한 번에 MERGE를 통해 배치 쿼리를 실행하도록 변경.
결과 및 추가 고려 사항
문제 해결
- 트랜잭션 분리: 메모리 부담을 줄이고, 트랜잭션 실패 영향을 최소화.
- 쿼리 배치 처리: 즉시 쿼리 실행 문제를 해결하며 성능 최적화
반응형
'개발 > Hot-Stock' 카테고리의 다른 글
트리맵의 최적화 (1) | 2024.12.19 |
---|---|
증권 뉴스 분석 자동화 사례: TOT 방식 적용과 정확도 향상 과정 (4) | 2024.12.18 |
중복 뉴스 필터링을 위한 최적화 방안 (3) | 2024.12.09 |
Virtual Thread를 사용한 크롤링 성능 80% 향상 (1) | 2024.09.16 |
메시지 발행과 데이터베이스의 트랜잭션을 어떻게 원자적으로 처리할까? (Transactional Outbox Pattern) (0) | 2024.09.10 |