슬로쿼리 모니터링 인터페이스 제작

반응형

슬로 쿼리를 모니터링 인터페이스를 만들기위해 어떤 작업을 해야 할까요?

 

slow 쿼리는 어떻게 탐지할 수 있을까?

Slow Query는 단순히 실행 시간이 길었던 쿼리를 의미하는 것이 아니라, 데이터베이스 관리 시스템(DBMS)이 내부적으로 측정한 쿼리 실행 시간이 사전에 정의된 임계값을 초과했다고 판단해 기록한

b-programmer.tistory.com

DB에서 측정한 값을 보다 정확하게 기록하기 위해서는 db에서 슬로쿼리라고 작성된 값을 사용해야 정확한다고 생각하였습니다.
물론 AOP같은 애플리케이션을 이용해서 슬로 쿼리를 측정할 수 있습니다. 하지만 그것이 진정한 슬로 쿼리인지 정확하게 모르겠더라구요. 
정확히 말하면 애플리케이션이 DB에 명령을 쏘고 명령이 수신을 받은 시간을 작성이 되어지기 때문에 잘못된 값이 저장이 될거라 생각했습니다.

위 글에서도 알 수 있듯이 저는 슬로 쿼리를 DB에서 작성된 파일을 읽기로 결정하였습니다.

SlowQueryFileReader

이 클래스는 슬로 쿼리파일을 읽는 역할을 합니다.

public List<String> readNewLines() throws IOException {
    List<String> lines = new ArrayList<>();

    if (!Files.exists(logPath)) {
      return lines;
    }

    try (RandomAccessFile raf = new RandomAccessFile(logPath.toFile(), "r")) {
      long lastOffset = offsetStore.getOffset();

      // 로그 로테이션 대응 (파일이 줄어든 경우)
      if (raf.length() < lastOffset) {
        lastOffset = 0;
        offsetStore.reset();
      }

      raf.seek(lastOffset);

      // RandomAccessFile.readLine() 직접 사용 (중첩 스트림 제거!)
      String line;
      while ((line = raf.readLine()) != null) {
        // readLine()은 ISO-8859-1로 읽으므로 UTF-8로 변환
        lines.add(new String(line.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
      }

      // RandomAccessFile이 아직 열려있으므로 정상 동작!
      offsetStore.update(raf.getFilePointer());
    }

    return lines;
  }

- RandomAccessFile:  이미 처리한 로그를 다시 읽지 않고, 재시작 이후에도 동일한 로그를 중복 처리하지 않는 구조를 만들 수 있습니다.

그런데 어떻게 Offset을 알 수 있을까요?
현재 구현에서는 offset을 메모리에 기록하여 관리하고 있습니다.
이는 구조를 단순하게 유지하기 위한 선택으로, 모니터링 로직의 흐름과 동작 방식을 검증하는 데 초점을 둔 구현입니다.

이렇게 파일을 읽은 뒤에는 어떻게 해야 할까요?

SlowQueryBlockParser

SlowQueryBlockParser는 slow query 로그를 그대로 사용하지 않고,
모니터링에 필요한 정보만 추출하여 의미 있는 이벤트로 변환하기 위해 사용합니다.

MySQL slow query 로그는 하나의 쿼리가 여러 줄로 구성된 블록 형태이기 때문에,
먼저 로그를 쿼리 단위 블록으로 분리한 뒤, 각 블록을 SlowQueryEvent로 파싱하는 과정을 거칩니다.

public List<SlowQueryEvent> parse(List<String> lines) {
    List<List<String>> blocks = splitBlocks(lines);
    List<SlowQueryEvent> events = new ArrayList<>();

    for (List<String> block : blocks) {
      parseBlock(block).ifPresent(events::add);
    }

    return events;
}

이 과정을 통해 이후 처리 로직은 MySQL slow query 로그 포맷에 직접 의존하지 않고,
SlowQueryEvent라는 도메인 이벤트만을 다루도록 분리할 수 있습니다.

SlowQueryEventHandler

@FunctionalInterface
public interface SlowQueryEventHandler {
  void handle(SlowQueryEvent event);
}

 

SlowQueryEventHandler는 파싱이 완료된 SlowQueryEvent를 전달받아 후속 처리를 담당하는 인터페이스입니다.
모니터링 모듈은 이벤트를 생성하는 책임까지만 가지며, 이 이벤트를 어떻게 처리할지는 핸들러 구현체에 위임합니다.

모니터링 흐름 정리

결과적으로 slow query 모니터링은 다음 3단계 흐름로 구성됩니다.

  1. 로그 수집
    slow query 로그 파일을 읽어 아직 처리하지 않은 로그를 수집합니다.
  2. 이벤트 변환
    로그를 쿼리 단위 블록으로 분리하고,
    필요한 정보만 추출하여 SlowQueryEvent로 변환합니다.
  3. 이벤트 처리
    생성된 이벤트를 SlowQueryEventHandler에 전달하여 후속 작업을 수행합니다.

확장 방식

만약 slow query 발생 시 Slack, 이메일 등의 알람을 발송해야 한다면,
SlowQueryEventHandler의 구현체를 추가하는 방식으로 손쉽게 확장할 수 있습니다.

모니터링 모듈의 핵심 로직은 변경하지 않고, 핸들러 구현만 교체하거나 추가함으로써
알람, 로그 적재, 메시지 전달 등의 다양한 요구사항을 처리할 수 있습니다.

결론

Slow Query 모니터링을 설계하면서, 이를 실시간으로 보여줘야 하는지, 아니면 일정 시간이 지난 뒤 확인해도 충분한지에 대해 많은 고민을 했습니다. 결론적으로, slow query 알람에서 중요한 것은 얼마나 빠르게 알림이 전달되었는가보다는 전달되는 정보가 얼마나 정확한가라고 판단했습니다. 물론 AOP를 활용해 애플리케이션 레벨에서 쿼리 실행 시간을 측정하는 방식은 구현 관점에서 더 단순할 수 있습니다.
하지만 이 방식은 어디까지나 애플리케이션이 측정한 시간일 뿐, DB 엔진이 판단한 slow query와는 기준이 다를 수 있습니다. 반면, slow query 로그 파일은 DB 스스로가 내부 기준에 따라 "느리다"고 판단한 결과입니다. 즉, 애플리케이션 관점이 아닌 DB 관점에서의 객관적인 판정 결과라는 점에서 더 신뢰할 수 있다고 보았습니다. 이러한 이유로, 실시간 처리의 복잡성을 감수하기보다는 DB에 기록된 slow query 로그 파일을 읽고, 이를 파싱하여 이벤트로 변환하는 방식을 선택했습니다. 결과적으로 이 구조는 알람 시점이 다소 늦어질 수는 있지만, 부정확한 데이터보다 정확한 데이터를 전달하는 것에 더 초점을 둔 선택이었습니다.

반응형

댓글

Designed by JB FACTORY