본문 바로가기

개발/Linux & DevOps

Actuator 의 DB 헬스체크

건강은 중요합니다.

서버 헬스체크도 매우 중요합니다.

Spring Boot Actuator의 health 엔드포인트에서 데이터베이스 데이터소스(db) 헬스체크가 어떻게 수행되는지 분석하고, 헬스체크의 빈도가 DB 부하에 미치는 영향을 알아보겠습니다.


1. Actuator Health 엔드포인트의 동작 과정

Spring Actuator는 헬스체크를 위해 HealthIndicator 인터페이스를 구현한 클래스들을 사용합니다. 데이터베이스 헬스체크는 DataSourceHealthIndicator 클래스를 통해 이루어집니다. 아래는 주요 과정을 설명합니다:

1-1. 엔드포인트 요청 → HealthEndpoint 호출

  • /actuator/health 요청 시, HealthEndpoint 클래스가 호출됩니다.
  • HealthEndpoint는 등록된 모든 HealthIndicator(데이터베이스 포함)를 순차적으로 실행하여 상태를 확인합니다.

1-2. DataSourceHealthIndicator의 실행

DataSourceHealthIndicator는 데이터베이스 헬스체크를 위해 다음과 같은 과정을 수행합니다:

  1. 데이터베이스 연결 확인:
    • javax.sql.DataSource를 통해 데이터베이스 연결을 시도합니다.
    • 데이터베이스 연결 객체(Connection)를 가져오고, 사용 후 반드시 반환(닫기)합니다.
  2. 검증 쿼리 실행:
    • 기본적으로 validationQuery를 실행합니다. (예: SELECT 1 또는 isValid()).
    • 데이터베이스 드라이버나 설정에 따라 간단한 쿼리(validationQuery)를 실행하거나, 커넥션 풀의 isValid() 메서드를 호출합니다.
  3. 결과 확인:
    • 쿼리가 성공적으로 실행되면 상태를 UP으로 설정합니다.
    • 쿼리가 실패하거나 연결을 열 수 없으면 상태를 DOWN으로 설정하고, 예외 정보를 포함합니다.
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
    try (Connection connection = this.dataSource.getConnection()) {
        if (this.validationQuery != null) {
            try (Statement stmt = connection.createStatement()) {
                stmt.execute(this.validationQuery);
            }
        } else {
            connection.isValid(0); // Driver-specific connection validation
        }
        builder.up();
    } catch (Exception ex) {
        builder.down(ex);
    }
}

1-3. 결과 반환

  • HealthEndpoint는 모든 HealthIndicator의 결과를 JSON 형식으로 반환합니다. 데이터베이스의 경우:
     {
       "status": "UP",
       "components": {
         "db": {
           "status": "UP",
           "details": {
             "database": "MySQL",
             "validationQuery": "isValid()"
           }
         }
       }
     }

2. 헬스체크 빈도와 데이터베이스 부하

2-1. 헬스체크의 부하 요인

헬스체크는 다음과 같은 이유로 데이터베이스에 부하를 줄 수 있습니다:

  1. Connection 생성 및 반환:
    • 매번 헬스체크 시 새로운 커넥션을 열고 닫습니다. 커넥션 풀을 사용하면 부하를 줄일 수 있지만, 빈번한 체크는 여전히 리소스를 소비합니다.
  2. Validation Query 실행:
    • 헬스체크 시마다 간단한 쿼리가 실행됩니다. 쿼리가 간단해도 빈번한 호출은 데이터베이스에 부담을 줄 수 있습니다.

2-2. 부하를 줄이기 위한 방법

  1. 헬스체크 주기 조정:
    • 헬스체크를 호출하는 주기를 늘려 부하를 줄일 수 있습니다.
    • 예: Prometheus 같은 모니터링 시스템이 Actuator를 1초마다 호출하지 않도록 조정.
  2. 커넥션 풀 최적화:
    • HikariCP와 같은 효율적인 커넥션 풀을 사용하면 빈번한 연결 열기/닫기의 부하를 줄일 수 있습니다.
  3. 쿼리 간소화:
    • validationQuery를 간단한 쿼리로 유지(예: SELECT 1).
    • 일부 드라이버는 Connection.isValid() 호출이 더 가볍습니다.
  4. 엔드포인트 노출 제한:
    • 헬스체크 엔드포인트를 외부 요청에서 차단하고, 내부에서만 주기적으로 확인.

이어서는, 자사에서 주로 사용하는 MySQL, MSSQL, PostgreSQL 에서 isValid() 함수가 어떻게 작동하는지 살펴보겠습니다.

Connection.isValid(int timeout) 메서드는 데이터베이스 드라이버별로 다르게 동작하며, MySQL, MSSQL, PostgreSQL의 동작 방식은 각 드라이버의 소스 코드나 공식 문서를 통해 확인할 수 있습니다. 아래는 세 가지 데이터베이스에서의 isValid() 동작 방식과 이에 대한 근거입니다.


1. MySQL

동작 방식:
MySQL JDBC 드라이버는 isValid 호출 시 내부적으로 ping 명령을 사용하여 데이터베이스 서버와 연결 상태를 확인합니다.

  • 방법: MySQL의 네이티브 핑 명령(COM_PING)을 사용.
  • 쿼리 실행 없음: 일반적으로 SELECT 1 같은 쿼리를 실행하지 않으므로 네트워크 트래픽만 발생.

근거:
MySQL Connector/J의 GitHub 소스코드에서 ConnectionImpl.isValid(int timeout) 메서드를 확인할 수 있습니다:

public boolean isValid(int timeout) throws SQLException {
    synchronized (this.getConnectionMutex()) {
        if (this.isClosed()) {
            return false;
        }

        try {
            this.pingInternal(false, timeout * 1000); // milliseconds
        } catch (Exception e) {
            return false;
        }

        return true;
    }
}
  • pingInternal 메서드: 네트워크를 통해 MySQL 서버에 핑을 전송하고 응답을 기다립니다.

참조:


2. MSSQL (Microsoft SQL Server)

동작 방식:
MSSQL JDBC 드라이버는 isValid 호출 시 SELECT 1 쿼리를 실행하여 연결 상태를 확인합니다.

  • 방법: 간단한 SQL 쿼리를 실행 (SELECT 1)하여 연결이 유효한지 확인.
  • 쿼리 실행: 쿼리 결과에 따라 유효성을 판단.

근거:
MSSQL JDBC 드라이버의 소스코드에서 isValid 메서드를 확인할 수 있습니다. 아래는 주요 코드입니다:

public boolean isValid(int timeout) throws SQLException {
    if (this.isClosed()) {
        return false;
    }

    try (Statement stmt = this.createStatement()) {
        stmt.execute("SELECT 1");
    } catch (SQLException e) {
        return false;
    }

    return true;
}
  • isValid는 연결이 닫혀 있는지 확인 후, 간단한 쿼리를 실행하여 상태를 확인.

참조:


3. PostgreSQL

동작 방식:
PostgreSQL JDBC 드라이버는 isValid 호출 시 서버로 핑을 보내는 방식으로 연결 상태를 확인합니다.

  • 방법: 네트워크 수준에서 PostgreSQL 서버에 핑을 보내고 응답을 받음.
  • 쿼리 실행 없음: 단순 네트워크 핑으로 확인하므로 쿼리를 실행하지 않습니다.

근거:
PostgreSQL JDBC 드라이버의 소스코드에서 isValid 메서드를 확인할 수 있습니다:

public boolean isValid(int timeout) throws SQLException {
    if (isClosed()) {
        return false;
    }

    try {
        // Send a network-level ping to the server
        if (queryExecutor != null) {
            queryExecutor.sendQueryCancel();
        }
    } catch (IOException ioe) {
        return false;
    }

    return true;
}

 

PostgreSQL JDBC 드라이버의 isValid(int timeout) 메서드가 네트워크 핑을 보내는 방식은 내부적으로 QueryExecutor 인터페이스를 통해 구현됩니다. 이 인터페이스는 PostgreSQL 서버와 클라이언트 간의 통신을 처리하며, sendQueryCancel 메서드를 사용하여 서버에 특정한 네트워크 명령을 보냅니다.


소스코드 분석: 네트워크 핑 동작

  1. PgConnection.isValid(int timeout)
    아래는 isValid 메서드가 호출되었을 때의 동작입니다:
    • isClosed: 연결이 이미 닫혀 있는지 확인.
    • queryExecutor.sendQueryCancel: 네트워크 핑을 보내는 핵심 코드. 여기서 서버와의 연결을 검증합니다.
    • IOException: 네트워크 문제가 발생하거나 응답이 없으면 IOException을 통해 연결이 비정상임을 감지.
  2. public boolean isValid(int timeout) throws SQLException { if (isClosed()) { return false; // 이미 연결이 닫혀있다면 false 반환 } try { // 네트워크 수준에서 서버와의 연결을 확인 if (queryExecutor != null) { queryExecutor.sendQueryCancel(); } } catch (IOException ioe) { return false; // 네트워크 문제가 발생한 경우 false 반환 } return true; // 연결이 유효한 경우 true 반환 }

  1. QueryExecutor.sendQueryCancel
    QueryExecutor 인터페이스는 PostgreSQL 서버와의 통신을 담당합니다. sendQueryCancel 메서드는 네트워크 핑을 보내는 데 사용되며, 실제 구현은 QueryExecutorImpl 클래스에 있습니다.
    public void sendQueryCancel() throws IOException {
        PGStream cancelStream = new PGStream(host, cancelPort);
    
        try {
            cancelStream.sendInteger4(16); // Cancel request packet length
            cancelStream.sendInteger2(1234); // Cancel request code (protocol spec)
            cancelStream.sendInteger2(5678); // Another part of the protocol
            cancelStream.sendInteger4(pid); // Server process ID
            cancelStream.sendInteger4(secretKey); // Secret key for authentication
            cancelStream.flush();
        } finally {
            cancelStream.close(); // Always close the stream after sending
        }
    }
    
    • PGStream: PostgreSQL 서버와 TCP 소켓을 통해 통신하는 클래스.
    • Cancel Request 패킷:
      • PostgreSQL 프로토콜은 취소 요청(Cancel Request) 패킷 형식을 사용합니다.
      • 핑 명령이 아니라, 가벼운 취소 요청 패킷을 전송해 서버와의 연결 상태를 확인.
    • 패킷 필드:
      • length (16): 패킷 길이.
      • request code (1234, 5678): 취소 요청 식별 코드.
      • pid: 서버에서 관리하는 프로세스 ID.
      • secretKey: 연결을 인증하기 위한 키.
    • flush: 네트워크 스트림을 강제로 플러시하여 데이터를 서버로 즉시 전송.
    • close: 요청 후 TCP 스트림을 닫아 리소스를 정리.
  2. QueryExecutorImpl.sendQueryCancel 구현 코드:

  1. TCP 통신
    PGStream은 TCP 소켓을 사용하여 PostgreSQL 서버와 직접 통신합니다.
    • 서버가 응답을 반환하면, 연결이 유효하다고 판단하여 isValid가 true를 반환합니다.
    • 응답이 없거나 연결이 끊어지면 예외를 발생시키고 isValid는 false를 반환합니다.

네트워크 핑 동작 요약

  1. PgConnection에서 isValid 호출.
  2. 내부적으로 QueryExecutor.sendQueryCancel 실행.
  3. PGStream을 통해 TCP 연결로 PostgreSQL 서버에 Cancel Request 패킷을 전송.
  4. 서버에서 정상 응답을 반환하면 연결이 유효(true).
  5. 네트워크 오류나 서버 비응답 상태일 경우 예외를 던지고 연결이 유효하지 않음(false).

왜 Cancel Request를 사용하는가?
1. 가벼움:
    Cancel Request는 간단한 네트워크 메시지로, 서버 자원을 소모하지 않음.
2. 효율성:
    네트워크 수준에서 연결 유효성을 검증하므로 쿼리를 실행하는 방식보다 부하가 적음.
3. 드라이버 표준:
    PostgreSQL JDBC 드라이버는 PostgreSQL 프로토콜 명세를 준수하여 이러한 방식으로 연결 상태를 확인.

근거 자료


4. 동작 비교

데이터베이스 isValid 동작 방식 부하 발생 요소 근거
MySQL 네이티브 COM_PING 명령 사용 낮음 (쿼리 실행 없음) MySQL Connector/J 소스코드
MSSQL SELECT 1 쿼리 실행 쿼리 실행으로 부하 가능 MSSQL JDBC 드라이버 소스코드
PostgreSQL 네트워크 핑 낮음 (주로 네트워크 핑) PostgreSQL JDBC 드라이버 소스코드

5. 부하 분석

  • MySQLPostgreSQL: 네이티브 핑(COM_PING) 방식이므로 쿼리 실행 없이 연결 상태만 확인. 부하가 적음.
  • MSSQL: SELECT 1 쿼리를 실행하므로 빈번한 호출 시 데이터베이스에서 쿼리 처리 리소스가 증가할 수 있음.
  • 빈도와 부하 관계: 헬스체크 주기를 조정하거나 커넥션 풀의 설정을 최적화하면 부하를 최소화할 수 있습니다.

최적화 팁:

  • 연결 풀(HikariCP, Tomcat Pool)의 idleTimeout, validationInterval 설정을 활용해 유효성 검사를 주기적으로 수행.
  • 네트워크 핑 방식을 선호(가능한 경우).
반응형