오라클 이중화 리스너 Fail-Over 스크립트 적용

반응형

오라클 이중화 리스너 Failover 스크립트 적용하기

오라클 데이터베이스를 이중화했더라도 애플리케이션이 장애가 발생한 리스너 주소만 계속 바라보고 있다면 실제 서비스 전환은 이루어지지 않는다. 이 글에서는 운영 서버에서 Oracle Listener와 데이터베이스 서비스를 점검한 뒤 애플리케이션용 TNS 접속 정보를 자동으로 전환하는 Failover 스크립트를 구성한다.

핵심 구성
  • Primary와 Standby 접속 별칭을 각각 구성한다.
  • 단순 포트가 아니라 실제 SQL 접속 결과로 정상 여부를 판단한다.
  • 정상 노드의 접속 정보를 임시 파일에 생성한 후 원자적으로 교체한다.
  • flock으로 스크립트 중복 실행을 방지한다.
  • cron 또는 systemd timer로 주기적인 상태 점검을 수행한다.

먼저 확인할 점: 스크립트보다 Oracle Net Failover가 우선이다

Oracle Net은 하나의 접속 기술자에 여러 리스너 주소를 정의하고 FAILOVER=ON을 설정하는 Connect-Time Failover 기능을 제공한다. 여러 주소가 설정된 경우 첫 번째 주소 연결에 실패하면 다음 주소로 연결을 시도할 수 있다. LOAD_BALANCE=OFF를 함께 설정하면 주소가 작성된 순서대로 접속을 시도한다. 0

APPDB =

(DESCRIPTION = (CONNECT_TIMEOUT = 5) (TRANSPORT_CONNECT_TIMEOUT = 3) (RETRY_COUNT = 1) (ADDRESS_LIST = (LOAD_BALANCE = OFF) (FAILOVER = ON) (ADDRESS = (PROTOCOL = TCP)(HOST = db-primary.example.com)(PORT = 1521)) (ADDRESS = (PROTOCOL = TCP)(HOST = db-standby.example.com)(PORT = 1521)) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = appdb_service) ) )

신규 연결만 전환하면 되는 환경에서는 위와 같은 Oracle Net 설정이 스크립트보다 단순하고 안정적이다. CONNECT_TIMEOUTTRANSPORT_CONNECT_TIMEOUT은 접속 실패를 감지하는 시간을 제한하며, 타임아웃 값은 네트워크 지연과 리스너 처리 시간을 고려해 운영 환경에서 조정해야 한다. 1

주의: Connect-Time Failover는 새로운 접속을 다른 주소로 시도하는 기능이다. 이미 실행 중인 세션이나 처리 중인 트랜잭션을 자동으로 다른 데이터베이스에 옮기는 기능은 아니다. 기존 세션 보호가 필요하면 RAC, Data Guard, FAN, Application Continuity 또는 애플리케이션의 재연결 정책을 별도로 검토해야 한다.

다음과 같은 레거시 환경에서는 운영 스크립트를 함께 사용할 수 있다.

  • 애플리케이션이 다중 ADDRESS_LIST 접속 문자열을 지원하지 않는 경우
  • 항상 하나의 고정된 TNS 별칭만 사용해야 하는 경우
  • 주 서버 복구 후 자동 원복 여부를 운영 정책으로 제어해야 하는 경우
  • 전환 이력과 장애 감지 결과를 별도 로그로 관리해야 하는 경우

구성 시나리오

예제 운영 환경
구분 Primary Standby
호스트 db-primary.example.com db-standby.example.com
리스너 포트 1521 1521
서비스 이름 appdb_service appdb_service
점검 별칭 DB_PRIMARY_CHECK DB_STANDBY_CHECK
애플리케이션 별칭 APPDB

이 스크립트는 애플리케이션 서버에서 실행한다. Oracle Client와 SQL*Plus가 설치되어 있고, TNS_ADMIN 디렉터리를 애플리케이션이 동일하게 사용한다는 전제다.

1. 작업 디렉터리 생성

sudo mkdir -p /opt/app/oracle/network/admin

sudo mkdir -p /var/log/oracle-failover sudo chown -R appuser:appgroup /opt/app/oracle/network sudo chown -R appuser:appgroup /var/log/oracle-failover sudo chmod 750 /opt/app/oracle/network/admin sudo chmod 750 /var/log/oracle-failover

예제의 실행 계정은 appuser다. 실제 운영 환경에서는 Oracle Client를 사용하는 애플리케이션 계정에 맞게 소유자와 권한을 변경한다.

2. 상태 점검용 TNS 별칭 구성

다음 파일은 스크립트가 유지해야 할 고정 설정이다. 애플리케이션용 APPDB 항목은 스크립트가 이 파일 뒤에 추가한다.

파일: /opt/app/oracle/network/admin/tnsnames.static.ora

DB_PRIMARY_CHECK =

(DESCRIPTION = (CONNECT_TIMEOUT = 5) (TRANSPORT_CONNECT_TIMEOUT = 3) (RETRY_COUNT = 0) (ADDRESS = (PROTOCOL = TCP) (HOST = db-primary.example.com) (PORT = 1521) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = appdb_service) ) )

DB_STANDBY_CHECK = (DESCRIPTION = (CONNECT_TIMEOUT = 5) (TRANSPORT_CONNECT_TIMEOUT = 3) (RETRY_COUNT = 0) (ADDRESS = (PROTOCOL = TCP) (HOST = db-standby.example.com) (PORT = 1521) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = appdb_service) ) )

lsnrctl status는 리스너 상태와 등록 서비스 정보를 확인할 때 유용하다. 다만 애플리케이션 서버에서 실제 서비스 접속 가능 여부까지 판단하려면 SQL*Plus로 SELECT 1 FROM DUAL을 실행하는 방식이 더 적합하다. Oracle 문서에서도 STATUS는 리스너 상태를, SERVICES는 등록된 서비스와 인스턴스를 확인하는 명령으로 구분한다. 2

3. 최초 tnsnames.ora 생성

스크립트가 처음 실행되기 전에도 상태 점검 별칭을 찾을 수 있도록 고정 파일을 tnsnames.ora로 복사한다.

cp /opt/app/oracle/network/admin/tnsnames.static.ora \

/opt/app/oracle/network/admin/tnsnames.ora

chmod 640 /opt/app/oracle/network/admin/tnsnames.static.ora chmod 640 /opt/app/oracle/network/admin/tnsnames.ora
TNS_ADMIN 적용: 애플리케이션과 스크립트 모두 /opt/app/oracle/network/adminTNS_ADMIN으로 사용해야 한다. Oracle Client는 TNS_ADMIN이 설정되어 있으면 해당 디렉터리의 tnsnames.ora를 우선 확인한다. 3

4. DB 계정 비밀번호를 스크립트에 작성하지 않는다

운영 스크립트 안에 데이터베이스 사용자명과 비밀번호를 평문으로 저장하면 안 된다. 예제는 Oracle Wallet의 외부 인증 정보를 사용해 다음 형식으로 접속한다고 가정한다.

sqlplus -L -s /@DB_PRIMARY_CHECK

Wallet을 사용하지 않는 환경이라면 보안팀 정책에 맞는 Secret Manager나 자격 증명 공급 방식을 적용해야 한다. 환경 변수에 비밀번호를 직접 노출하거나 명령행 인수로 전달하는 방식도 운영 서버에서는 피하는 것이 좋다.

5. Listener Failover 스크립트 작성

파일: /opt/app/oracle/bin/oracle-listener-failover.sh

#!/usr/bin/env bash

set -Eeuo pipefail umask 027

------------------------------------------------------------

Oracle Client 환경

------------------------------------------------------------

ORACLE_HOME="/opt/oracle/instantclient" TNS_ADMIN="/opt/app/oracle/network/admin" PATH="${ORACLE_HOME}:${PATH}"

export ORACLE_HOME export TNS_ADMIN export PATH

------------------------------------------------------------

TNS 및 상태 파일

------------------------------------------------------------

STATIC_TNS="${TNS_ADMIN}/tnsnames.static.ora" ACTIVE_TNS="${TNS_ADMIN}/tnsnames.ora" STATE_FILE="${TNS_ADMIN}/.active-db" LOCK_FILE="/var/run/oracle-listener-failover.lock" LOG_FILE="/var/log/oracle-failover/failover.log"

PRIMARY_ALIAS="DB_PRIMARY_CHECK" STANDBY_ALIAS="DB_STANDBY_CHECK" APP_ALIAS="APPDB"

PRIMARY_HOST="db-primary.example.com" STANDBY_HOST="db-standby.example.com" LISTENER_PORT="1521" SERVICE_NAME="appdb_service"

SQL*Plus 프로세스 전체 제한 시간

CHECK_TIMEOUT="10"

true: Primary가 복구되면 자동 원복

false: Standby 사용 중에는 Standby 장애 전까지 유지

AUTO_FAILBACK="false"

------------------------------------------------------------

공통 함수

------------------------------------------------------------

log() { local level="$1" shift

printf '%s [%s] %s\n' 
"$(date '+%Y-%m-%d %H:%M:%S')" 
"${level}" 
"$*" | tee -a "${LOG_FILE}" }

validate_environment() { if [[ ! -x "${ORACLE_HOME}/sqlplus" ]]; then log "ERROR" "sqlplus 실행 파일을 찾을 수 없습니다: ${ORACLE_HOME}/sqlplus" exit 10 fi

if [[ ! -r "${STATIC_TNS}" ]]; then log "ERROR" "고정 TNS 파일을 읽을 수 없습니다: ${STATIC_TNS}" exit 11 fi

mkdir -p "$(dirname "${LOG_FILE}")" }

check_database() { local alias="$1"

timeout "${CHECK_TIMEOUT}" 
"${ORACLE_HOME}/sqlplus" -L -s "/@${alias}" >/dev/null 2>&1 <<'SQL' WHENEVER OSERROR EXIT FAILURE ROLLBACK WHENEVER SQLERROR EXIT FAILURE ROLLBACK

SET HEADING OFF SET FEEDBACK OFF SET PAGESIZE 0 SET VERIFY OFF SET ECHO OFF

SELECT 1 FROM dual;

EXIT SUCCESS SQL }

write_app_alias() { local target="$1" local host="" local temp_file=""

case "${target}" in PRIMARY) host="${PRIMARY_HOST}" ;; STANDBY) host="${STANDBY_HOST}" ;; *) log "ERROR" "지원하지 않는 전환 대상입니다: ${target}" return 1 ;; esac

temp_file="$(mktemp "${TNS_ADMIN}/tnsnames.ora.tmp.XXXXXX")"

{ cat "${STATIC_TNS}"

cat <<EOF

${APP_ALIAS} = (DESCRIPTION = (CONNECT_TIMEOUT = 5) (TRANSPORT_CONNECT_TIMEOUT = 3) (RETRY_COUNT = 1) (ADDRESS = (PROTOCOL = TCP) (HOST = ${host}) (PORT = ${LISTENER_PORT}) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = ${SERVICE_NAME}) ) ) EOF } >"${temp_file}"

chmod 640 "${temp_file}"

같은 파일시스템에서 mv를 사용해 설정 파일을 원자적으로 교체한다.

mv -f "${temp_file}" "${ACTIVE_TNS}"

printf '%s\n' "${target}" >"${STATE_FILE}" chmod 640 "${STATE_FILE}"

log "INFO" "${APP_ALIAS} 접속 대상을 ${target}(${host})로 설정했습니다." }

read_current_target() { if [[ -r "${STATE_FILE}" ]]; then tr -d '[:space:]' <"${STATE_FILE}" else printf '%s\n' "UNKNOWN" fi }

select_target() { local current_target="$1"

자동 원복을 사용하는 경우 Primary를 항상 먼저 점검한다.

if [[ "${AUTO_FAILBACK}" == "true" ]]; then if check_database "${PRIMARY_ALIAS}"; then printf '%s\n' "PRIMARY" return 0 fi

if check_database "${STANDBY_ALIAS}"; then
  printf '%s\n' "STANDBY"
  return 0
fi

return 1

fi

자동 원복을 사용하지 않고 현재 Standby인 경우 Standby를 우선 유지한다.

if [[ "${current_target}" == "STANDBY" ]]; then if check_database "${STANDBY_ALIAS}"; then printf '%s\n' "STANDBY" return 0 fi

if check_database "${PRIMARY_ALIAS}"; then
  printf '%s\n' "PRIMARY"
  return 0
fi

return 1

fi

최초 실행 또는 현재 Primary인 경우 Primary를 먼저 점검한다.

if check_database "${PRIMARY_ALIAS}"; then printf '%s\n' "PRIMARY" return 0 fi

if check_database "${STANDBY_ALIAS}"; then printf '%s\n' "STANDBY" return 0 fi

return 1 }

main() { local current_target="" local selected_target=""

validate_environment

동일 스크립트의 중복 실행을 방지한다.

exec 9>"${LOCK_FILE}"

if ! flock -n 9; then log "WARN" "이전 Failover 점검 프로세스가 실행 중이므로 종료합니다." exit 20 fi

current_target="$(read_current_target)" log "INFO" "점검을 시작합니다. 현재 대상=${current_target}"

if ! selected_target="$(select_target "${current_target}")"; then log "ERROR" "Primary와 Standby 모두 SQL 접속에 실패했습니다."

# 양쪽 모두 실패한 경우 기존 설정을 임의로 변경하지 않는다.
exit 30

fi

if [[ "${selected_target}" == "${current_target}" ]] 
&& [[ -s "${ACTIVE_TNS}" ]]; then log "INFO" "현재 대상이 정상입니다. 설정 변경 없이 종료합니다." exit 0 fi

write_app_alias "${selected_target}" log "INFO" "Failover 점검을 정상적으로 완료했습니다." }

main "$@"

스크립트는 두 데이터베이스 모두 접속에 실패했을 때 기존 tnsnames.ora를 유지한다. 장애 판단이 불가능한 상황에서 임의의 접속 대상으로 변경하지 않기 위한 조치다.

6. 실행 권한과 디렉터리 권한 설정

sudo mkdir -p /opt/app/oracle/bin

sudo chown appuser:appgroup 
/opt/app/oracle/bin/oracle-listener-failover.sh

sudo chmod 750 
/opt/app/oracle/bin/oracle-listener-failover.sh

로그 파일과 TNS 파일에는 데이터베이스 구성 정보가 포함될 수 있으므로 일반 사용자에게 쓰기 권한을 부여하지 않는다.

7. 수동 실행 테스트

구문 검사

bash -n /opt/app/oracle/bin/oracle-listener-failover.sh

실행 테스트

sudo -u appuser \

/opt/app/oracle/bin/oracle-listener-failover.sh

생성된 접속 정보 확인

cat /opt/app/oracle/network/admin/.active-db

grep -A 15 '^APPDB' 
/opt/app/oracle/network/admin/tnsnames.ora

애플리케이션 별칭으로 SQL 접속 확인

export TNS_ADMIN=/opt/app/oracle/network/admin

sqlplus -L -s /@APPDB <<'SQL' SET HEADING OFF SET FEEDBACK OFF SELECT sys_context('USERENV', 'DB_UNIQUE_NAME') FROM dual; EXIT SQL

출력된 DB_UNIQUE_NAME을 확인하면 실제로 어느 데이터베이스에 접속했는지 검증할 수 있다. 운영 환경에 따라 DATABASE_ROLE도 함께 확인할 수 있다.

SELECT
name,
db_unique_name,
database_role,
open_mode

FROM v$database;

8. cron으로 주기 실행

예를 들어 1분마다 상태를 점검하려면 애플리케이션 계정의 crontab에 다음 항목을 추가한다.

crontab -e
* * * * * /opt/app/oracle/bin/oracle-listener-failover.sh >/dev/null 2>&1

스크립트 자체에서 로그를 기록하므로 cron 출력은 별도로 저장하지 않았다. 단, 운영 표준에 따라 syslog 또는 중앙 로그 수집 시스템으로 전달하도록 변경할 수 있다.

cron의 실행 환경에는 로그인 셸의 환경 변수가 자동으로 적용되지 않는다. 따라서 스크립트 내부에서 ORACLE_HOME, TNS_ADMIN, PATH를 명시해야 한다.

9. systemd timer로 실행하는 방법

장애 처리 로그와 실행 상태를 systemd에서 관리하려면 cron 대신 timer를 사용할 수 있다.

Service 파일

/etc/systemd/system/oracle-listener-failover.service

[Unit]

Description=Oracle Listener Failover Check After=network-online.target Wants=network-online.target

[Service] Type=oneshot User=appuser Group=appgroup ExecStart=/opt/app/oracle/bin/oracle-listener-failover.sh TimeoutStartSec=30

[Install] WantedBy=multi-user.target

Timer 파일

/etc/systemd/system/oracle-listener-failover.timer

[Unit]

Description=Run Oracle Listener Failover Check Every Minute

[Timer] OnBootSec=30s OnUnitActiveSec=60s AccuracySec=5s Persistent=true Unit=oracle-listener-failover.service

[Install] WantedBy=timers.target

등록 및 실행

sudo systemctl daemon-reload

sudo systemctl enable --now oracle-listener-failover.timer

sudo systemctl status oracle-listener-failover.timer sudo systemctl list-timers oracle-listener-failover.timer

실행 로그 확인

sudo journalctl \

-u oracle-listener-failover.service 
--since today

10. 실제 장애 전환 테스트

운영 반영 전에는 반드시 별도의 검증 환경에서 장애 상황을 재현한다. 단순히 리스너 프로세스를 중지하는 것뿐 아니라 네트워크 단절, 서비스 미등록, 데이터베이스 미오픈 상태도 구분해서 확인해야 한다.

  1. Primary와 Standby가 모두 정상인 상태에서 Primary가 선택되는지 확인한다.
  2. Primary 리스너를 중지하거나 테스트 방화벽으로 1521 포트를 차단한다.
  3. 스크립트 실행 후 .active-db 값이 STANDBY인지 확인한다.
  4. APPDB 별칭으로 새로운 SQL 접속이 가능한지 확인한다.
  5. 애플리케이션 커넥션 풀의 기존 연결이 제거되고 재생성되는지 확인한다.
  6. Primary를 복구한 후 자동 원복 정책에 맞게 동작하는지 확인한다.
  7. 양쪽 접속을 모두 차단했을 때 기존 TNS 설정이 유지되는지 확인한다.

리스너의 현재 상태는 lsnrctl STATUS로 확인할 수 있으며, 등록 서비스와 인스턴스 정보는 lsnrctl SERVICES로 확인할 수 있다. 4

lsnrctl status LISTENER

lsnrctl services LISTENER

운영 환경에서 반드시 보완할 항목

Failover 스크립트 운영 체크 항목
항목 권장 사항
장애 판정 포트 점검만 사용하지 말고 실제 서비스명으로 SQL 접속을 수행한다.
오탐 방지 한 번의 실패로 즉시 전환하지 않고 연속 실패 횟수 또는 모니터링 시스템 결과를 반영한다.
자동 원복 Primary 복구 즉시 원복할지 운영자 승인 후 원복할지 사전에 결정한다.
DB 역할 확인 Standby가 실제 서비스 제공이 가능한 역할과 Open Mode인지 확인한다.
커넥션 풀 TNS 파일 변경만으로 기존 커넥션이 바뀌지 않으므로 연결 검증과 재생성 정책을 적용한다.
동시 실행 flock이나 systemd 실행 제어로 중복 파일 변경을 방지한다.
설정 파일 교체 임시 파일을 완성한 뒤 동일 파일시스템에서 mv로 교체한다.
인증 정보 비밀번호를 스크립트에 넣지 말고 Wallet 또는 승인된 Secret 저장소를 사용한다.
모니터링 전환 성공, 양쪽 장애, 자동 원복 발생 시 운영 알림을 전송한다.

Failover가 적용됐는데도 접속이 바뀌지 않는 원인

기존 커넥션 풀이 남아 있는 경우

tnsnames.ora 변경은 이후 생성되는 연결에 적용된다. 애플리케이션 서버가 기존 커넥션을 계속 재사용하면 접속 대상이 즉시 바뀌지 않을 수 있다. 커넥션 검증 쿼리, 연결 최대 수명, 장애 연결 제거 정책을 함께 설정해야 한다.

TNS_ADMIN 경로가 다른 경우

스크립트가 수정하는 파일과 애플리케이션이 읽는 파일이 다르면 전환 효과가 없다. 애플리케이션 프로세스의 환경 변수와 Oracle Client 설정을 기준으로 실제 tnsnames.ora 위치를 확인한다.

리스너는 정상이나 서비스가 등록되지 않은 경우

TCP 1521 포트가 열려 있더라도 요청한 SERVICE_NAME이 리스너에 등록되지 않으면 애플리케이션 연결은 실패한다. 따라서 단순 포트 점검이 아니라 SQL 접속 또는 lsnrctl services 결과를 확인해야 한다.

Standby가 Read Only 또는 Mounted 상태인 경우

리스너 접속 가능 여부와 업무 수행 가능 여부는 다르다. Data Guard 구성에서는 데이터베이스 역할과 OPEN_MODE, 서비스 시작 조건을 기준으로 서비스 제공 가능 여부를 검증해야 한다.

자동 원복으로 접속이 반복 전환되는 경우

Primary 상태가 불안정할 때 자동 원복을 활성화하면 Primary와 Standby 사이에서 접속 대상이 반복 변경될 수 있다. 운영 환경에서는 연속 성공 횟수, 안정화 대기 시간 또는 운영자 승인 절차를 추가하는 것이 안전하다.

최종 점검 체크리스트

  • Primary와 Standby에 동일한 업무용 서비스 이름이 준비되어 있는가?
  • 스크립트가 사용하는 접속 계정에 최소 권한만 부여했는가?
  • 비밀번호가 소스와 프로세스 목록에 노출되지 않는가?
  • 애플리케이션과 스크립트가 동일한 TNS_ADMIN을 사용하는가?
  • 양쪽 리스너 장애 시 기존 설정을 유지하는가?
  • 전환 로그와 운영 알림이 정상적으로 생성되는가?
  • 커넥션 풀이 장애 연결을 폐기하고 새 연결을 생성하는가?
  • Primary 복구 후 자동 원복 정책이 명확한가?
  • 스위치오버와 비정상 Failover 시나리오를 각각 테스트했는가?
  • 롤백 절차와 수동 전환 절차를 운영 문서에 기록했는가?

정리

오라클 이중화 환경에서는 우선 다중 ADDRESSFAILOVER=ON을 이용한 Oracle Net Connect-Time Failover 적용을 검토하는 것이 좋다. Oracle은 고가용성 접속 문자열에서 접속 타임아웃과 재시도 설정을 함께 사용하는 구성을 안내하고 있다. 5

레거시 애플리케이션처럼 하나의 고정 접속 정보만 사용할 수 있다면 상태 점검 스크립트로 애플리케이션용 TNS 별칭을 전환할 수 있다. 이때 장애 판단 기준은 리스너 포트가 아니라 실제 SQL 접속이어야 하며, 설정 파일은 임시 파일 작성 후 원자적으로 교체해야 한다.

마지막으로 TNS 전환은 새로운 데이터베이스 연결의 목적지를 변경하는 작업이다. 기존 세션과 처리 중인 트랜잭션까지 보호하려면 데이터베이스 고가용성 구성과 애플리케이션 재연결 정책을 함께 설계해야 한다.

참고 자료

태그: 오라클, Oracle, 오라클이중화, OracleListener, ListenerFailover, tnsnames, TNS_ADMIN, DataGuard, 데이터베이스이중화, 리스너장애, Failover스크립트, 오라클운영, DBA
반응형