본문 바로가기

웹 개발

백엔드 서버가 예고 없이 종료되는 현상, PostgreSQL 자동 업그레이드와의 연관성

반응형

문제 상황: 로그 없는 서버 종료

프로덕션 환경에서 이상한 현상이 2번이나 발생했습니다. NestJS 기반 백엔드 서버가 갑자기 종료되는데, 가장 당황스러운 점은 로깅 파일인 app.log에 아무런 기록이 남지 않았다는 것이었습니다.

서버 상태 확인

$ ps aux | grep node
# 프로세스가 사라져 있음

$tail -100 app.log
# 마지막 로그 이후 아무런 기록 없음

 

일반적인 애플리케이션 오류라면 스택 트레이스나 에러 메시지가 남아야 하는데, 완전히 깨끗한 상태였습니다. 마치 누군가 kill -9로 프로세스를 강제 종료한 것처럼 말이죠.

 

디버깅 과정, 메모리 누수부터 시스템 로그까지

1단계: 메모리 누수 의심
처음에는 Node.js의 고질적 문제인 메모리 누수를 의심했습니다. 서버가 1-2일 주기로 종료되는 패턴을 보였기 때문입니다.

  // main.ts에 메모리 모니터링 추가
  const logMemoryUsage = () => {
    const used = process.memoryUsage();
    const mb = (bytes: number) => Math.round(bytes / 1024 / 1024 * 100) / 100;
    const uptimeHours = Math.round(process.uptime() / 3600 * 100) / 100;

    logger.log(`📊 Memory Usage - RSS: ${mb(used.rss)}MB, Heap: ${mb(used.heapUsed)}/${mb(used.heapTotal)}MB, External: ${mb(used.external)}MB, Uptime: ${uptimeHours}h`);
  };

  setInterval(logMemoryUsage, 30 * 60 * 1000); // 30분마다

 

결과: 15시간 동안 단 4.5MB만 증가한 매우 안정적인 메모리 패턴을 확인했습니다. 메모리 누수는 완전히 배제되었습니다.

 

2단계: PostgreSQL 로그 분석의 반전

애플리케이션 로그가 아닌 시스템 로그를 확인해보니 특이한점을 발견했습니다.

# PostgreSQL 로그
2025-09-10 06:25:49 UTC [40982] LOG: received fast shutdown request
2025-09-10 06:25:49 UTC [40982] LOG: database system is shut down
2025-09-10 06:25:50 UTC [40982] LOG: starting PostgreSQL 16.10

 

PostgreSQL이 백엔드 서버 종료 의심 시점에 재시작되고 있었습니다.

 

3단계: 범인 특정 - APT Unattended Upgrades

더 깊이 파고들어 /var/log/apt/history.log를 확인한 결과, 결정적 원인을 발견했습니다.

  Start-Date: 2025-09-10  06:25:49
  Commandline: /usr/bin/unattended-upgrade
  Upgrade: postgresql-16:amd64 (16.9-0ubuntu0.24.04.1, 16.10-0ubuntu0.24.04.1)
  End-Date: 2025-09-10  06:25:52

 

타임스탬프가 정확히 일치했습니다. PostgreSQL 자동 업그레이드가 서버 종료와 동일한 시점에 발생했던 것입니다.

 

기술적 분석: 왜 서버가 크래시될까?

NestJS-TypeORM-PostgreSQL 연결 체인의 취약점

  // app.module.ts의 TypeORM 설정
  TypeOrmModule.forRootAsync({
    // ... 기타 설정
    retryAttempts: 3,     // 단 3번만 재시도
    retryDelay: 3000,     // 3초 간격
  })


PostgreSQL이 업그레이드로 재시작되면 다음과 같은 연쇄 반응이 발생합니다.

  1. Connection Pool 무효화: 기존 모든 DB 연결이 끊어짐
  2. 새 요청 시 연결 실패: API 요청이 들어오면 DB 연결 시도
  3. 재시도 한계 도달: TypeORM이 3번 재시도 후 포기
  4. UnhandledPromiseRejection: 처리되지 않은 Promise 거부
  5. Node.js 프로세스 종료: Node.js v15+에서 기본 동작
  // Node.js의 기본 동작
  process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection:', reason);
    process.exit(1); // 즉시 종료, 로그 버퍼 플러시 안됨
  });

 

로그가 남지 않는 이유

process.exit(1)로 강제 종료되면

  • 버퍼링된 로그가 디스크에 기록되지 않음
  • Graceful shutdown 과정 생략
  • 파일 시스템 동기화 없이 종료

이것이 app.log에 아무런 기록이 남지 않았던 이유입니다.

 

VM 환경에서 자동 업그레이드가 활성화된 이유

Ubuntu Server의 기본 보안 정책

  # /etc/apt/apt.conf.d/20auto-upgrades
  APT::Periodic::Update-Package-Lists "1";
  APT::Periodic::Unattended-Upgrade "1";


Ubuntu Server는 보안상의 이유로 기본적으로 자동 업그레이드가 활성화되어 있습니다. 이는 다음과 같은 이유 때문입니다.

  1. 보안 패치 자동 적용: 중요한 보안 업데이트를 놓치지 않기 위함
  2. 관리 부담 감소: 시스템 관리자의 수동 개입 최소화
  3. 컴플라이언스 요구사항: 기업 환경에서의 보안 정책 준수


가상화 환경의 특수성

가상머신 환경에서는 호스트 시스템과 분리되어 있어 더욱 적극적인 자동 업데이트 정책을 적용하는 경우가 많습니다.

  # systemd 타이머로 관리되는 자동 업그레이드
  $ systemctl list-timers | grep apt
  apt-daily-upgrade.timer
  apt-daily.timer

 

해결 방법: 단계별 대응 전략

1. 즉시 적용 가능한 해결책

A. PostgreSQL 패키지 블랙리스트 추가

  # /etc/apt/apt.conf.d/50unattended-upgrades 수정
  sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

  # 다음 섹션에 추가:
  Unattended-Upgrade::Package-Blacklist {
      "postgresql*";
      "libpq*";
      "postgresql-client*";
  };


B. 패키지 홀드 설정

  # PostgreSQL 관련 패키지를 홀드 상태로 설정
  sudo apt-mark hold postgresql-16 postgresql-client-16 libpq5

  # 설정 확인
  apt-mark showhold

 

2. 애플리케이션 레벨 개선

A. TypeORM 연결 복원력 강화

  // app.module.ts
  TypeOrmModule.forRootAsync({
    useFactory: (configService: ConfigService) => ({
      // ... 기존 설정
      retryAttempts: 10,           // 3 → 10으로 증가
      retryDelay: 5000,            // 3초 → 5초로 증가
      keepConnectionAlive: true,   // 연결 유지 시도
      extra: {
        connectionLimit: 10,       // 연결 풀 크기
        acquireTimeout: 60000,     // 연결 대기 시간
        timeout: 60000,            // 쿼리 타임아웃
        reconnect: true,           // 자동 재연결
        idleTimeout: 300000,       // 유휴 연결 타임아웃
      }
    }),
  })


B. Graceful Error Handling

  // 글로벌 에러 핸들러 추가
  import { Logger } from '@nestjs/common';

  process.on('unhandledRejection', (reason, promise) => {
    const logger = new Logger('UnhandledRejection');
    logger.error('Unhandled Rejection at:', promise, 'reason:', reason);

    // 즉시 종료 대신 graceful shutdown 시도
    setTimeout(() => {
      logger.error('Forcing exit due to unhandled rejection');
      process.exit(1);
    }, 5000);
  });

  process.on('uncaughtException', (error) => {
    const logger = new Logger('UncaughtException');
    logger.error('Uncaught Exception:', error);
    process.exit(1);
  });

 

3. 모니터링 및 알림 체계 구축

A. 데이터베이스 연결 상태 모니터링

  @Injectable()
  export class HealthService {
    constructor(
      @InjectConnection() private connection: Connection,
    ) {}

    @Cron('*/30 * * * * *') // 30초마다
    async checkDatabaseHealth() {
      try {
        await this.connection.query('SELECT 1');
        // 연결 정상
      } catch (error) {
        // 연결 실패 알림
        this.logger.error('Database connection failed:', error);
        // Slack/Email 알림 발송
      }
    }
  }


B. 시스템 업그레이드 사전 알림

  # 업그레이드 전 알림 스크립트
  cat > /etc/apt/apt.conf.d/99notify-upgrade << 'EOF'
  DPkg::Pre-Install-Pkgs {
    "/usr/local/bin/notify-upgrade.sh";
  };
  EOF

  # 알림 스크립트 작성
  sudo nano /usr/local/bin/notify-upgrade.sh

 

결론 및 교훈

이번 사건을 통해 얻은 주요 교훈들

 

1. 인프라 레벨 이벤트의 중요성
애플리케이션 로그만으로는 모든 문제를 파악할 수 없습니다. 시스템 로그, 패키지 관리자 로그, 데이터베이스 로그를 종합적으로 분석해야 합니다.

 

2. 자동화의 양면성

자동 업그레이드는 보안에는 도움이 되지만, 프로덕션 안정성에는 위험 요소가 될 수 있습니다. 적절한 제어와 모니터링이 필요합니다.

 

3. 연결 의존성 관리

마이크로서비스 환경에서 데이터베이스 의존성을 가진 서비스는 연결 복원력(Connection Resilience)을 반드시 고려해야 합니다.

 

4. 모니터링의 다층화

  • 애플리케이션 레벨: 메모리, CPU, 응답 시간
  • 인프라 레벨: 시스템 이벤트, 네트워크, 디스크
  • 서비스 레벨: 데이터베이스 연결, 외부 API 상태

이제 서버는 PostgreSQL 자동 업그레이드로부터 안전하며, 더 강력한 연결 복구 메커니즘을 갖추게 되었습니다. 무엇보다 "로그 없는 장애"라는 것은 없다는 것을 깨달았습니다. 단지 우리가 올바른 곳을 보지 못했을 뿐이었죠.

 

이 글이 비슷한 문제를 겪고 있는 개발자들에게 도움이 되기를 바랍니다. 특히 Ubuntu Server 환경에서 NestJS를 운영하는 분들은 자동 업그레이드 설정을 한 번 점검해보시기 바랍니다.

반응형