0. Table Of Contents

1. Coldstart

FaaS Service는 쓰지 않는 상태일 때는 function instance 대기 상태가 아닌 생성되지 않은 상태로 유지되다가 request가 들어올 경우, function instance가 생성되며 이에 대한 요청을 핸들링하기 시작한다. 이 때, request를 handling할 instance가 없으면 delay가 생길 수 있다.

 

cloud function은 다음과 같은 경우 새로운 함수 인스턴스가 생성된다.

  • cloud function을 새로 배포할 경우
  • auto scaling으로 인해 확장되는 경우
  • 긴 시간동안 function이 호출이 되지 않았을 경우
  • 내부적인 오류로 인스턴스를 대체할 경우

 

위의 경우에 대해 대책을 세우지 않으면 무거운 FaaS를 설계하였을 때, delay가 길어질 수 있으니 주의해야한다.

이를 극복할 수 있는 방법으로 해당 function instance가 cold start가 되지 않도록 지속적으로 health check를 하는 방법이 있다.

 

 

2. Stateless Environment

function이 실행되더라도 각 요청마다 다른 함수 인스턴스에서 요청처리를 할 수 있기 때문에, 전역변수를 이용한 출력은 매번 달라질 수 있다. 그렇기 때문에 이러한 데이터는 db, cloud storage 등의 서비스를 이용하여 제어를 해야하는 것이 바람직하다. 그에 대한 이유는 아래 코드 및 실행결과를 보도록 하자.

 

cloud function sample code

 

7월 15일 오후 2시 37분 실행했을 때의 상태

 

 

 

7월 15일 오후 6시 8분 실행했을 때의 상태

 

 

2시 37분 cloud function 을 invoke하고 나서, 약 3시간 30분 정도를 실행하고 있지 않다가 실행한 결과, 전역변수가 초기화 된 것을 볼 수 있다. 즉, function instance를 cold start를 하였기 때문에 전역변수가 초기화 된 것을 볼 수 있다.

이를 방지하기 위해 상태값을 가진 변수 및 객체는 DB를 이용하여 다른 곳에서 캐싱 또는 저장이 되어있어야 한다.

 

 

 

3. 한도

GCP Cloud Function의 한도의 경우, 아래 사진을 참고하도록 한다. 자유자재로 customizing 한 다른 resource와 달리, FaaS Service는 경량화된 서비스이기 때문에 이러한 부분에서 약점을 지닌다. 서비스 설계시 정말로 FaaS를 사용해서 설계를 해도 문제가 없는 아키텍처인지 분석이 필요하다.

 

 

  1. 누구게여 2021.07.17 01:56

    멋진사람 !!

0. Table Of Contents

 

 

1. 문제 현황 분석

 

1.1. 문제 상황

docker version을 업그레이드 하고 나서 다음과 같이 잘 되던 .env file parsing이 잘 되지 않는 오류가 발생하였다.

 

 

1.2. 기존 docker version과 upgrade한 docker version

Mac Docker Desktop 기준으로 작성되었습니다.
  • 기존 Docker version : 3.0.0
    • Docker compose version : 1.27.4
    • Docker version : ?
  • 업그레이드 된 Docker version : 3.4.0
    • Docker compose version : 1.29.0
    • Docker version : 20.10.7

 

1.3. 프로젝트 폴더 구조

 

1.4. 명령어 call 순서

  • docker.sh를 이용하여 docker-start-local.sh 실행
  • docker-start-local.sh./docker-compose/docker-compose.postgres.yml를 참조하여 아래와 같은 명령어를 실행시킨다.
  • 업그레이드 전 아래 명령어는 정상적으로 동작하여 .env파일을 정상적으로 파싱하고 있었다.
docker-compose \
  -p {PROJECT_NAME} \
  -f ./docker-compose/docker-compose.postgres.yml \
  -f ./docker-compose/api/docker-compose.base.yml \
  -f ./docker-compose/api/docker-compose.local.yml \
  up $build --remove-orphans

 

 

1.5. 주요 파일 구성 확인

혹시나 싶어서 docker-compose.postgres.yml 파일과 .env가 제대로 구성이 안되어있는지 확인을 해본 결과 다음과 같았다.

version: '3'

services:
  postgres:
    image: <PROJECT_NAME>/postgres
    container_name: "<PROJECT_NAME>-postgres"
    build:
      context: ../docker/postgres
      dockerfile: Dockerfile
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ../data/postgres/data:/var/lib/postgresql/data
      - ../docker/postgres/init/:/docker-entrypoint-initdb.d/
    ports:
      - "${POSTGRES_PORT}:${POSTGRES_PORT}"
    restart: unless-stopped
    ulimits:
      nproc: 65535
      nofile:
        soft: 65535
        hard: 65535
    healthcheck:
      test: ["CMD", "docker-healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

 

# .env
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

 

위와 같이 .envdocker-comspose.postgres.yml의 환경변수가 잘 매핑이 되어 있었기 때문에 parsing error가 날리가 없다고 생각했다.

 

 

 

 

2. 문제 해결 삽질

 

2.1. 문제 해결을 위한 searching

지금까지 어깨 너머로 배워서 docker-dompose를 사용하였지만, 문서를 상세히 보고 사용한 적이 없기 때문에 공식 document를 보면서 제일 먼저 정리하기로 했다.

Docker-Compose Command로 env를 정의하고, 실행했기 때문에, env file configuration 쪽을 찾아보는 도중 다음과 같은 문구를 발견 할 수 있었다.

 

출처 : Docker-compose Document

 

위 문서 중 내가 겪은 문제와 관련된 부분을 한글화하여 요약하면 다음과 같다.

1.28 미만의 Docker Compose의 경우, command가 실행된 현재 작업중인 directory에서 .env파일을 가지고 오거나, --project-directory argument에서 설장된 path에서 .env 파일을 가지고 옵니다.


이러한 모호함을 1.28 이상의 버전에서는 .env의 default path를 project directory로 제한하는 것으로 개선하였습니다. --env-file 옵션을 이용하여 기본 .env default path를 override 할 수 있습니다.


project directory는 다음 순서로 정의됩니다.

1. --project-directory flag에 정의된 path

2. 첫번째 --file (-f)로 flag에 정의된 directory

3. 현재 directory

 

 

2.2. 문제 디버깅

기존에 사용한 docker-compose version은 1.27.4였기 때문에 위 사항에 일치하였다. 위 사항을 이용하여 docker desktop 업데이트 이후 발생한 오류를 디버깅 해보면 다음과 같다.

  • 프로젝트 루트 폴더에서 shell script를 이용하여 docker-compose command를 실행시켰다.
  • 위에서 정의된 docker-compose command에는 project directory가 정의되어 있지 않다.
  • docker compose에 이용할 yml파일로 ./docker-compose/docker-compose.postgres.yml가 입력되었다.
  • project directory 정의 순서에 따라, ./docker-compose/가 project directory로 정의된다.
  • ./docker-compose/에는 .env 파일이 없다.
  • 없는 파일을 참조하였기 때문에 yaml파일 내부에서 ${}로 정의한 변수들이 전부 빈 string으로 대체된다.
  • postgresql port는 필수로 필요하지만, 입력이 되지 않았기 때문에 에러가 발생하였다.

 

 

2.3. 디버깅내용이 맞는지에 대한 검증

project directory인 ./docker-compose/에 루트 프로젝트 디렉토리에 있는 .env 파일을 다음 사진과 같이 복붙하여 .env파일을 하나 더 만든 다음 기존에 만들어 놓은 쉘스크립트를 이용하여 docker-compose를 실행시킨 결과, 정상적으로 docker가 빌드가 되어 정상적으로 실행까지 되었다.

 

2.4. 최종 문제 해결 flow

docker-compose 1.28.6 version release note를 보면 --env-file flag는 현재 작업중인 directory를 참조하도록 고쳐졌다고 한다.

 

 

env file을 제대로 인식 시켜주지 못하고 있기 때문에 이에 대해 명확히 설정을 해 줄 필요가 있다. compose에 사용되는 파일이 root project directory에 존재하기 때문에, docker-compose command 를 실행시킬 때, --env-file flag를 이용하여 명확하게 내가 사용하고자 하는 .env를 아래와 같이 명시해주었다.

 

 

 

3. 삽질을 통해 얻은 결론 및 개인적인 프로젝트 구조에 대한 회고

이처럼 실행환경에 대해서 정의할 때에는 최대한 사용할 파일들에 대해 command 또는 yaml, script에 정확히 명시를 하여 이러한 오류를 피해가게 하고, 다른 사람이 script를 읽었을 때 어떤 파일을 사용하는지 이해하기 쉽게 하는 것이 중요하다고 생각이 되었다. 향후, 위 문제처럼 다른 파일을 참조하고 있지만 묵시적인 방식으로 파일을 참조하고 있다면 명시적으로 나는 이걸 쓸거다라는 코드를 추가를 해야겠다.

 

+ env에 docker compose에서 쓰는 environment와 Dockerfile에서 사용하는 environment가 혼재하고있는데 향후 체계적으로 environment를 관리하기 위해서는 이를 분리하여 명확하게 어디서 쓰는 environment인지 정의하면 훨씬 더 좋을거같다.

 

 

 

4. Reference

Environment variables in Compose

Docker Compose release notes

 

  1. 민트초코 2021.06.24 18:17

    저한테는 어려운 말이지만 무척 멋있네요!!

  2. 크로커스개발자 2021.06.28 10:50

    오우 엄청난 글이네요

 

0. Table Of Content

 

 

 

1. Server Gateway Interface가 왜 필요한가?

일반적으로 우리가 보고 있는 웹 서비스는 브라우저를 통해서 흘러나온 웹서버의 내용들이다.
대부분의 어플리케이션의 경우 웹과 소통하는 미들웨어를 가장 널리 사용되는 tomcat, apache를 채택하여 사용하고 있다. 아쉽게도 Tomcat, Apache는 Java기반으로 만들어졌기 때문에, Python기반의 프레임워크에서는 가장 널리 사용되는 웹 서버를 사용하기 위해서는 중간에서 Java기반 미들웨어가 말하는 것을 해석해 줄 또다른 미들웨어가 필요하게 된 것이다. 물론, 파이썬 기반의 미들웨어를 사용해도 괜찮지만, 이미 검증된 것을 포기할만큼 매력이 없거나 큰 리스크를 동반해야하기 때문에 Apache, Tomcat을 그대로 사용하고 이를 중간에서 번역해주는 python framework 전용 미들웨어를 하나 만들게 되었다. 그것이 바로 Server Gateway Interface이다. Python Server Gateway Interface의 경우, 널리 사용되는 것이 WSGI와 ASGI가 있는데, 상세 설명은 다음 챕터부터 진행 하겠다.

 

 

 

 

2. WSGI (Web Server Gateway Interface)

기존 python 기반 framework가 Java Middleware와 통신하기 위해서는 Medusa(Python으로 작성된 middleware), mod_python(embed Python), CGI / FastCGI(invoke Python via a gateway protocol) 같은 API를 사용해야 했었다. 그러나 위에서 명시된 API들은 특정 요소만을 고려해서 제작된 API기 때문에, 해당 API에 맞는 부분만을 개발자가 바라보게 했기 때문에, 개발자들이 선호하는 특정 영역에만 시야가 한정되었다.
그러나, 범용으로 쓰일 수 있는 WSGI의 등장으로 위의 문제점들이 사라지게 되었으며 WSGI는 PEP3333에 정식으로 채용(?)이 되었다.

 

 

 

3. ASGI (Asynchronous Server Gateway Interface)

그러나 WSGI도 시간이 지나면서 문제점이 발생하기 시작했다.

 

3.1. 기존 WSGI에는 어떤 문제점이 있었는가?

WSGI가 개발 중일 당시, WSGI는 오직 웹개발을 위한 공통 기반을 제공하는 프로토콜을 만드는 것이었다. 이 덕분에 파이썬 기반 웹개발자는 프레임워크 세부사항에 신경 쓰지 않고 여러 프레임워크에서 쉽게 작업을 할 수 있었다. 그러나, WebSocket 개념이 웹 개발자 사이에서 인기를 얻기 시작했을 때, WSGI는 single, synchoronous callable한 특성을 가지고 있었기 때문에, 다음과 같은 특성을 지니고 있어 webSocket과는 맞지 않았다.

  • HTTP는 Connection이 짧게 유지되는 특성을 지니고 있었기 때문에, Long-Polling HTTP와 WebSocket 같이 상대적으로 connection이 긴 특성을 지닌 Protocol과는 맞지 않았다.
  • HTTP Request는 application내부에서 오직 하나의 path를 가질 수 있기 때문에, 여러개의 path를 통해 이벤트를 수신하는 WebSocket의 이벤트를 처리할 수 없었다.



3.2. ASGI는 어떤 방식으로 WSGI의 문제점을 해결했는가?

ASGI의 구성요소와 책임은 다음과 같다.

  • 소켓을 종료하고 이를 connection에 매핑하는 프로토콜 서버
  • 포로토콜 서버 내부에서 실행되는 어플리케이션 연결을 인스턴스화(per 1 connection) 하며, 이벤트 메시지의 처리


WSGI와 비슷하게 ASGI도 기능이 비슷한 것처럼 보인다. 그러나, 다음요소에서 차이가 난다.

  • Connection의 lifetime과 protocol을 정의하는 Connection Scope
  • Application으로 보내지는 Connection 동안 일어날 사건에 대한 명세 Event

 

 

 

 

ASGI Application은 단일, 비동기 callable 속성을 지니고 있다. 수신 요청에 대한 정보를 포함하는 scope를 accept하고, 클라이언트에 이벤트를 보내고 받을 수 있는 awaitable를 보내고 받을 수 있다. 이 덕분에, ASGI Application은 WSGI의 한계점을 뛰어넘는 수신 / 발신 event를 허영할 수 있다. 그 뿐만아니라 ASGI Application은 background coroutine을 허용하기 때문에 application은 요청을 처리하면서 background에서 다른 작업도 수행할 수 있게 되었다. (ex. 외부 event를 listening 하고 있는 redis queue 등)

ASGI Application을 통해서 보내거나 받는 모든 event는 Python Dictionary Type이다. 이러한 사전 정의된 event의 format은 ASGI Application가 쉽게 다른 웹 서버에서 다른 웹서버로 쉽게 전환할 수 있게 한다.

 

 

 

3.3. 간단한 ASGI Application 예제

async def application(scope, receive, send):
    event = await receive()    
    ....     
    await send({"type": "websocket.send", ...}

 

 

 

4. Reference

+ Recent posts