코딩관계론

AWS - cpu utilization over 80%(search service가 자꾸 죽어요...!) 본문

개발/Hot-Stock

AWS - cpu utilization over 80%(search service가 자꾸 죽어요...!)

개발자_티모 2024. 12. 18. 13:37
반응형

서버 특정 시간대 오류 분석 및 해결 과정

문제 상황

Search 서버가 특정 시간대에 주기적으로 중단되는 현상이 발생했습니다. 초기에는 빈번하게 발생했지만 시간이 지나며 발생 빈도가 줄어들었습니다. 최근 데이터베이스 마이그레이션 이후 다시 동일한 문제가 발생하여, 원인 분석 및 해결을 진행했습니다.

초기 의심

  1. 로컬 테스트와 AWS 환경 차이
    • 로컬 테스트에서는 문제없이 작동했으나, AWS 환경에서 업로드 후 문제가 발생.
    • AWS 환경 문제를 의심하여 top 명령어로 리소스 사용량 로그를 추적했지만 특이점이 발견되지 않았습니다.
  2. 오류 빈도의 감소
    • 시간이 지나며 오류 발생 빈도가 줄어들었기에 급한 일 처리 후 원인 분석을 유보했었습니다.

상세 원인 분석

데이터베이스 마이그레이션 후 오류가 재발하여 다시 분석에 돌입했습니다.
문제가 발생한 코드는 일정 시간마다 주식 가격 정보를 갱신하는 스케줄러 메소드입니다.

@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 사용량과 메모리 소비가 급증.
    • 결국 메모리 초과로 서버가 중단되는 문제가 발생.

해결 방법

트랜잭션 분리

문제 해결을 위해 트랜잭션 전파 옵션을 조정했습니다.

  1. 기존 트랜잭션 방식의 문제
    • 트랜잭션이 전체 saveStockInfoAndChartData 메소드에 걸쳐 있어, 단일 차트 갱신 실패 시 전체 롤백이 발생.
    • 메모리 부담이 클 뿐만 아니라 불필요한 롤백이 성능을 저하시킴.
  2. 트랜잭션 전파 설정: 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 사용

  1. 트랜잭션을 readOnly=true로 설정하여 즉시 INSERT를 방지.
  2. 갱신 저장 시 한 번에 MERGE를 통해 배치 쿼리를 실행하도록 변경.

결과 및 추가 고려 사항

문제 해결

  1. 트랜잭션 분리: 메모리 부담을 줄이고, 트랜잭션 실패 영향을 최소화.
  2. 쿼리 배치 처리: 즉시 쿼리 실행 문제를 해결하며 성능 최적화
반응형