빙글은 단일 인스턴스로 서비스를 시작했다.
그리고 아직 그 구조에서 벗어나지 못했는데, 그 이유는 당장 도입해야 할 기능과 아직 더 버틸 수 있는 인프라 사이에서의 갈등 때문이었다.
당연히 종종 발생하는 서버 다운에 전혀 대비가 되지 않을 뿐더러, 인스턴스 자체의 로드가 서로에게 영향을 준다.
이런 구조는 실제로 유의미한 결과를 낳았다.
‘한 주에 각각 다른 이유로 세 번 다운되기’
더 이상 미룰 수 없다는 판단에, 이중화는 차치하고서라도 로그 롤링과 배포 환경을 우선 개선하기로 했다.
이건 그 두번째 이야기, 배포 환경 개선이다.
우리 머신은 2GB RAM을 가졌는데, 꾸역꾸역 4GB 스왑메모리를 잡고 개발서버와 운영서버를 모두 돌리고 있다.
아무리 NVMe SSD로 스왑 메모리가 충분히 빨라졌다고 하지만, 둘 사이에는 여전히 엄청난 속도 차이가 있으며,
OS는 둘을 동기화하기 위해 열일하고 있다.
빙글의 배포는 빌드 - 실행 - 트래픽 리다이렉션 - 기존 서버 종료 의 구조로 진행된다.
그러나 이 모든 과정을 단일 인스턴스에서 돌리다보니 빌드할 때 서버가 아주 느려지거나 다운되는 문제가 있다.
특히 빌드에 메모리를 많이 사용하기 때문인 것으로 보인다.
세상에 완벽한 건 (아직) 없고 스왑 메모리도 그렇다.
물리 메모리 부족을 어느정도 커버할 수는 있지만, 대체할 수는 없다.
페이지, 혹은 더 큰 단위로 디스크에서 푹 퍼오는 전략을 사용해 IO를 줄이려 노력하겠지만 물리 메모리 만큼 빠르지도, 안전하지도 않다.
잦은 스왑으로 인한 블로킹으로 오히려 전체 시스템이 느려지는 문제가 생기는 것이다.
그러다 삐끗하는 날엔 스타베이션 - 서버의 데드락 - 이어서 서버 동작에 문제가 생길 것이다.
아마 이런 이유로 원인 불명의 서버 다운이 있지 않았을까 예상한다.
그래서 이 문제를 해소하고자 빌드 과정을 분리해내기로 했다.
왜 이 생각을 미리 하지 못했을까. 고민이 부족했다.
테스트 및 빌드가 서버에 영향을 끼치지 않도록, 먼저 별도 runner에서 빌드한 jar를 artifact로서 업로드한다.
그러면 이어서 self-hosted runner에서 실행되는 배포 작업이 이 artifact를 다운로드, 실행, 개방 작업을 한다.
이렇게 하면 우리 작고 귀여운 t2.small은 자바 어플리케이션 실행만 하면 되니까 부담이 훨씬 감소한다.
이렇게 해서 배포 작업이 서버에 미치는 영향을 최소화 할 수 있었다.
아래는 그에 사용한 스크립트다. 보완할 점들이 있지만 참고가 되었으면 좋겠다.
name: Production Server Deploy on: workflow_dispatch: push: branches: [ "main" ] permissions: contents: read env: JAR_NAME: vingle-0.0.1-SNAPSHOT.jar JAR_DIRECTORY: /home/ubuntu/vingle-prod APPLICATION_PORT_A: 8082 APPLICATION_PORT_B: 8083 jobs: build: runs-on: ubuntu-latest steps: - name: Get token from Submodule Reader uses: actions/create-github-app-token@v1 id: app_token with: app-id: ${{ secrets.SUBMODULE_APP_ID }} private-key: ${{ secrets.SUBMODULE_APP_PEM }} owner: ${{ github.repository_owner }} - name: Checkout uses: actions/checkout@v4 with: submodules: true token: ${{ steps.app_token.outputs.token }} - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'corretto' - name: Gradle Caching uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Build run: | chmod +x ./gradlew ./gradlew bootjar - name: Upload Jar uses: actions/upload-artifact@v4 with: name: Jar path: ./build/libs/${{ env.JAR_NAME }} deploy: needs: build runs-on: dev steps: - name: Set build DateTime run: | echo "BUILD_DATETIME=$(date +'%Y-%m-%d-%H-%M')" >> "$GITHUB_ENV" - name: Download Jar uses: actions/download-artifact@v4 with: name: Jar - name: Copy jar shell: bash {0} run: | mkdir $JAR_DIRECTORY cp ./$JAR_NAME $JAR_DIRECTORY/$BUILD_DATETIME-$JAR_NAME - name: Download Datadog Java Agent working-directory: ${{ env.JAR_DIRECTORY }} run: | wget -O dd-java-agent.jar 'https://dtdg.co/latest-java-tracer' - name: 현재 사용중인 어플리케이션 포트 확인 shell: bash {0} run: | if [ -n "$(lsof -ti:$APPLICATION_PORT_A)" ]; then echo "BLUE_PORT=$APPLICATION_PORT_A" >> "$GITHUB_ENV" echo "GREEN_PORT=$APPLICATION_PORT_B" >> "$GITHUB_ENV" else echo "BLUE_PORT=$APPLICATION_PORT_B" >> "$GITHUB_ENV" echo "GREEN_PORT=$APPLICATION_PORT_A" >> "$GITHUB_ENV" fi - name: 그린 어플리케이션 실행 env: RUNNER_TRACKING_ID: "" shell: bash working-directory: ${{ env.JAR_DIRECTORY }} run: | nohup java -javaagent:dd-java-agent.jar \ -Dspring.profiles.active=prod \ -Dserver.port=$GREEN_PORT \ -Ddd.profiling.enabled=true \ -XX:FlightRecorderOptions=stackdepth=256 \ -Ddd.logs.injection=true \ -Ddd.appsec.enabled=true \ -Ddd.iast.enabled=true \ -Ddd.service=vingle \ -Ddd.env=production \ -jar $BUILD_DATETIME-$JAR_NAME > ~/prod-server.log & - name: 그린 어플리케이션이 접속 가능할 때까지 기다린다 shell: bash {0} run: | PROCESS_ID="$(lsof -i:$GREEN_PORT -t)" while [ "$(curl -o /dev/null -s -w %{http_code} localhost:$GREEN_PORT/v2/products?zeroBasedPageNumber=0\&pageSize=10)" != 200 ] do if [ -n "$PROCESS_ID" ]; then echo "::error title=배포 실패::블루 어플리케이션으로 롤백합니다."; exit 1; fi echo "새로운 어플리케이션을 띄우는 중입니다."; sleep 10; done - name: 리버스 프록시 설정 변경 shell: bash {0} working-directory: ${{ env.JAR_DIRECTORY }} run: | echo "proxy_pass http://localhost:$GREEN_PORT;" > port.inc; sudo nginx -s reload; - name: 블루 어플리케이션 종료 shell: bash {0} run: | PROCESS_ID="$(lsof -i:$BLUE_PORT -t)" if [ -n "$PROCESS_ID" ]; then sudo kill -15 $PROCESS_ID echo "구동중인 애플리케이션을 종료했습니다. (pid : $PROCESS_ID)\n" fi
- 서로 다른 runner에서의 job들이 needs를 활용해 동기적으로 실행된다는 점이 특징이고
- 서버의 UP을 실제 API를 사용해 확인한다는 점이 보완이 필요한 부분이다.
- Gradle Caching도 잘 안되는데, 이건 캐시 저장과 히트는 잘 되는데 적용(복원)이 안되는거라 조사가 좀 필요할 듯하다.