플러팅 AI/Flask Server

🚀 Google Cloud Run 비용 절감 및 서버 자동 종료 문제 해결

Solo.dev 2025. 2. 5. 21:07

📌 상황: Cloud Run 인스턴스가 예상보다 많은 비용을 차지

🔍 문제 발견

  • Google Cloud Billing을 확인해보니 예상보다 많은 비용이 청구됨.
  • 사용자가 요청을 보낸 후 바로 서버가 종료되길 원했지만, Cloud Run 특성상 15분 동안 유지됨.
  • 즉, 1분만 사용해도 15분+ 추가 시간만큼 비용이 발생하는 상황 🚨
  • 예상보다 비용이 많이 나와서 서비스 지속 시 적자가 날 가능성이 있음.

🔥 문제 원인: 서버가 즉시 종료되지 않음

  • 서버 종료를 명확히 하는 코드가 없었음.
  • Cloud Run의 자동 스케일링 정책 때문에 사용자가 없어도 15분 동안 인스턴스가 유지됨.
  • os._exit(0)을 사용하여 서버를 종료하려 했지만, Flask 프로세스만 종료되고 Cloud Run 컨테이너는 종료되지 않음.
  • Cloud Run의 idle timeout을 조정하는 기능이 없었음15분 기본 유지 (구글 공식 문서에서도 수정 불가능한 것으로 확인됨).

해결 방법: 사용자가 없으면 즉시 종료하는 방식으로 변경

1️⃣ 사용자 요청이 없으면 서버 즉시 종료

  • 사용자가 앱을 완전히 종료하거나(백그라운드 제거) 또는 120초 동안 요청이 없으면 서버를 종료.
  • 이를 위해 WebSocket 연결 관리 및 큐 시스템을 활용하여 자동 종료 로직 추가.

2️⃣ exit(0) 대신 os.kill(os.getpid(), signal.SIGTERM) 사용

  • exit(0)을 사용하면 Flask 프로세스만 종료되고 Cloud Run 인스턴스는 유지됨 ❌.
  • os.kill(os.getpid(), signal.SIGTERM)을 사용하면 Cloud Run 인스턴스 자체가 종료됨 ✅.

🚀 구현 코드: 클라이언트가 없으면 서버 종료

import os
import signal
import eventlet
from flask_socketio import SocketIO

socketio = SocketIO()

# 클라이언트 연결 상태 관리
connected_clients = set()
client_queues = {}

def cleanup_client(client_id):
    """사용자가 없으면 서버 종료"""
    if client_id in client_queues:
        del client_queues[client_id]  # 큐에서 제거

    if client_id in connected_clients:
        connected_clients.remove(client_id)  # 클라이언트 목록에서 삭제

    # 모든 사용자가 연결을 끊었으면 서버 종료
    if len(connected_clients) == 0:
        logger.info("모든 클라이언트가 끊어졌습니다. 웹소켓 닫기 중...")

        # 🔥 **현재 연결된 모든 웹소켓을 닫음**
        active_clients = list(socketio.server.manager.get_participants('/', '/'))
        for sid in active_clients:
            logger.info(f"Closing WebSocket connection for {sid}")
            socketio.server.disconnect(sid)

        eventlet.sleep(3)  # 3초 대기 후 종료
        logger.info("서버 종료 진행 중...")

        # 🚀 **Cloud Run 인스턴스 종료**
        os.kill(os.getpid(), signal.SIGTERM)

📊 결과: 서버가 즉시 종료되면서 비용 절감 성공

이전 방식

  • 사용자가 요청을 보낸 후 Cloud Run이 15분 동안 유지됨
  • 짧게 사용해도 15분 비용이 청구됨
  • exit(0)을 사용했지만 Cloud Run 컨테이너가 유지됨 ❌

새로운 방식

  • 사용자가 앱을 종료하거나 요청이 없으면 즉시 종료
  • 120초 동안 요청이 없으면 자동으로 WebSocket과 서버 종료
  • os.kill(os.getpid(), signal.SIGTERM)을 사용하여 Cloud Run 인스턴스까지 종료
  • 불필요한 비용 절감 성공 💰

🔍 추가적인 Cloud Run 관련 이슈 정리

Cloud Run의 idle timeout(15분)은 변경 불가능

  • 공식 문서를 확인한 결과, Cloud Run의 idle timeout(유휴 시간 후 자동 종료)은 기본 15분으로 고정됨.
  • gcloud run deploy 명령어에서 --max-instance-idle-timeout 같은 옵션이 없었음.
  • 즉, Cloud Run 자체적으로 15분을 강제하는 것을 조정할 방법이 없음.

 


🎯 결론: Cloud Run 비용 절감을 위한 최적화 방법

1️⃣ 모든 클라이언트(WebSocket 연결)가 끊어지면 서버 종료
120초 타임아웃 후 서버 종료하도록 큐 기반 로직 추가

2️⃣ os._exit(0) 대신 os.kill(os.getpid(), signal.SIGTERM) 사용
Flask 종료가 아니라 Cloud Run 컨테이너 자체 종료하도록 변경

3️⃣ Cloud Run의 idle timeout(15분)은 변경 불가능
따라서 서버가 요청이 없을 때 직접 종료하는 방식으로 해결

 


🚀 최종 정리

  • 처음에는 Cloud Run이 15분 동안 유지되어 불필요한 비용이 발생.
  • os._exit(0)을 사용했지만 Cloud Run 컨테이너가 종료되지 않음.
  • os.kill(os.getpid(), signal.SIGTERM)을 사용하여 Cloud Run 인스턴스를 즉시 종료하도록 수정.
  • 비용 절감에 성공하여 불필요한 서버 비용을 없앰! 🎉

👉 이제 유저가 없으면 Cloud Run 인스턴스가 즉시 종료됨! 💰