WebSocket 사용

이 페이지에서는 Cloud Run에서 WebSockets 또는 기타 스트리밍 서비스를 실행하고 해당 서비스에 대한 클라이언트를 작성하기 위한 가이드와 권장사항을 설명합니다.

WebSocket은 Cloud Run에서 기본 지원되므로 추가 구성은 필요하지 않습니다. 하지만 WebSockets 스트림은 Cloud Run 서비스에 구성된 요청 시간 제한의 적용을 받는 HTTP 요청이므로 WebSocket 사용 시 이 설정이 재연결 구현 등에서 제대로 작동하는지 확인해야 합니다.

Cloud Run에서는 세션 어피니티를 사용할 수 없으므로 기본 제공되는 부하 분산으로 인해 WebSocket 요청이 다른 컨테이너 인스턴스로 전달될 수 있습니다. 이 문제를 해결하려면 컨테이너 인스턴스 간에 데이터를 동기화해야 합니다.

참고로 Cloud Load Balancing을 사용하는 경우 Cloud Run에서의 WebSocket도 지원됩니다.

샘플 WebSockets 서비스 배포

Cloud Shell을 사용하면 Cloud Run과 함께 WebSocket을 사용하는 샘플 화이트보드 서비스를 빠르게 배포할 수 있습니다. 샘플 배포

또는 샘플 화이트보드 서비스를 수동으로 배포하려면 다음 안내를 따르세요.

  1. git 명령줄 도구를 사용하여 로컬에서 Socket.IO 저장소를 클론합니다.

    git clone https://github.com/socketio/socket.io.git
    
  2. 샘플 디렉터리로 이동합니다.

    cd socket.io/examples/whiteboard/
    
  3. gcloud 명령줄 도구로 소스 코드에서 서비스를 빌드하여 새 Cloud Run 서비스를 배포합니다.

    gcloud run deploy whiteboard --allow-unauthenticated --source=.
    
  4. 서비스가 배포되면 두 개의 개별 브라우저 탭을 열고 서비스 URL로 이동합니다. 클라이언트가 WebSockets를 통해 동일한 컨테이너 인스턴스에 연결되었으므로 한 탭에서 그리는 모든 것은 다른 탭으로 전파되어야 합니다(또는 그 반대).

WebSockets 채팅 샘플 전체 튜토리얼

전체 코드 둘러보기는 Cloud Run용 WebSocket 채팅 서비스 튜토리얼 주제에서 추가 코드 샘플을 참조하세요.

권장사항

Cloud Run에서 WebSocket 서비스를 만들 때 가장 어려운 점은 여러 Cloud Run 컨테이너 인스턴스 간에 데이터를 동기화하는 것입니다. 컨테이너 인스턴스의 자동 확장 및 스테이트리스(Stateless) 특성과 동시 실행요청 시간 제한의 제한으로 인해 이 작업은 까다롭습니다.

요청 시간 제한 및 클라이언트 재연결 처리

WebSocket 요청은 Cloud Run에서 장기 실행 HTTP 요청으로 처리됩니다. 애플리케이션 서버에 시간 제한이 적용되지 않더라도 요청 제한 시간(현재는 최대 60분, 기본값은 5분)이 적용됩니다.

따라서 클라이언트가 연결을 Cloud Run 서비스에 구성된 필수 제한 시간보다 길게 유지하면 요청이 타임아웃될 때 클라이언트의 연결이 끊어집니다.

따라서 Cloud Run에 연결하는 WebSocket 클라이언트는 요청 시간이 초과되거나 서버 연결이 끊기는 경우 서버 재연결을 처리해야 합니다. 이는 브라우저 기반 클라이언트에서 reconnecting-websocket 같은 라이브러리를 사용하거나, SocketIO 라이브러리를 사용하는 경우에는 "연결 해제" 이벤트를 처리하여 달성할 수 있습니다.

WebSockets 사용 시 요금 발생

하나 이상의 WebSocket 연결이 열려 있는 Cloud Run 인스턴스는 활성 상태로 간주되어 요금이 청구됩니다.

동시 실행 극대화

WebSockets 서비스는 일반적으로 많은 연결을 동시에 처리하도록 설계됩니다. Cloud Run은 동시 연결(컨테이너당 최대 1000개)을 지원하므로 서비스가 지정된 리소스로 부하를 처리할 수 있는 경우 기본값보다 높은 값으로 컨테이너의 최대 동시 실행 설정을 늘리는 것이 좋습니다.

고정 세션(세션 어피니티) 정보

WebSockets 연결은 스테이트풀(Stateful)이므로 클라이언트는 연결 수명 동안 Cloud Run의 동일한 컨테이너에 계속 연결을 유지합니다. 이는 단일 WebSocket 연결의 컨텍스트 내에서 세션 고정성을 자연스럽게 제공합니다.

하지만 Cloud Run은 컨테이너 인스턴스를 자동으로 확장하고 사용 가능한 컨테이너 인스턴스 간에 모든 요청을 부하 분산하므로 요청 간 세션 고정성을 제공하지 않습니다. 따라서 동일한 클라이언트의 후속 요청은 컨테이너 인스턴스 간에 무작위로 부하 분산하고 WebSocket을 다른 컨테이너 인스턴스로 열 수 있습니다.

컨테이너 인스턴스 간 부하 분산으로 인해 Cloud Run 서비스에 연결된 클라이언트는 데이터를 조율하거나 공유하지 않는 다른 컨테이너 인스턴스의 서비스를 받게 될 수 있습니다. 이 문제를 완화하려면 외부 데이터 저장소를 사용하여 Cloud Run 인스턴스 간에 상태를 동기화해야 합니다. 이에 대한 설명은 다음 섹션에서 이어집니다.

컨테이너 인스턴스 간 데이터 동기화

Cloud Run 컨테이너 인스턴스의 스테이트리스(Stateless) 자동 확장 특성으로 인해 Cloud Run 서비스에 연결된 클라이언트는 WebSockets 연결에서 동일한 데이터를 받지 못할 수 있습니다.

예를 들어 WebSockets를 사용하여 채팅방 서비스를 빌드하고 최대 동시 실행 설정을 1000명으로 지정했는데, 1000명이 넘는 사용자가 동시에 이 서비스에 접속하는 경우, 유저들은 다른 컨테이너 인스턴스로 분산되므로 채팅방에서 동일한 메시지를 볼 수 없게 됩니다.

모든 인스턴스에서 채팅방에 게시된 메시지를 수신하는 경우와 같이 Cloud Run 컨테이너 인스턴스 간에 데이터를 동기화하려면 데이터베이스 또는 메시지 큐와 같은 외부 데이터 스토리지 시스템이 필요합니다.

Cloud SQL과 같은 외부 데이터베이스를 사용하면 데이터베이스에 메시지를 보내고 데이터베이스에서 주기적으로 폴링할 수 있습니다. 그러나 컨테이너가 요청을 처리하지 않는 경우 Cloud Run 인스턴스에는 CPU가 없습니다. 서비스에서 주로 WebSockets 요청을 처리하는 경우 클라이언트가 최소 1개 이상 연결되어 있는 한 컨테이너에 CPU가 할당된 것입니다.

외부 메시지 큐는 각 컨테이너 인스턴스의 데이터를 '푸시'할 수 없기 때문에 메시지 큐를 사용하는 것이 Cloud Run 컨테이너 간에 실시간으로 데이터를 동기화하는 데 더 효과적입니다. 서비스는 메시지 큐에 연결을 설정하여 메시지 큐에서 새 메시지를 '가져와야' 합니다.

Google에서는 컨테이너 인스턴스가 시작한 연결을 통해 모든 인스턴스에 업데이트를 전달할 수 있는 Firestore 실시간 업데이트 또는 Redis Pub/Sub(Memorystore) 같은 외부 메시지 대기열 시스템을 권장합니다.

Redis Pub/Sub 사용

WebSockets 채팅방 서비스 아키텍처

Memorystore에서 Redis 인스턴스를 만들어 Redis Pub/Sub 메커니즘을 사용할 수 있습니다. WebSockets에 Socket.IO 라이브러리를 사용하는 경우 Redis 어댑터를 사용할 수 있습니다.

이 Redis 기반 아키텍처에서 각 Cloud Run 컨테이너 인스턴스는 수신된 메시지가 포함된 Redis 채널에 장기 실행 중인 연결을 설정합니다(SUBSCRIBE 명령어 사용). 컨테이너 인스턴스가 채널에서 새 메시지를 수신하면 실시간으로 WebSocket을 통해 클라이언트에 메시지를 보낼 수 있습니다.

마찬가지로 클라이언트가 WebSockets를 사용하여 메시지를 내보내면 메시지를 수신하는 컨테이너 인스턴스가 이 메시지를 Redis 채널에 게시하고(PUBLISH 명령어 사용) 이 채널을 구독하는 다른 컨테이너 인스턴스가 이 메시지를 수신합니다.

전체 코드 둘러보기는 Cloud Run용 WebSocket 채팅 서비스 튜토리얼 주제에서 추가 코드 샘플을 참조하세요.