개발 블로깅/Improving Performance

GCP CloudBuild에서 Test CI 시간 단축시킨 과정 정리

Hello이뇽 2023. 2. 2. 17:27

 

GCP 프로젝트에서 Test CI 마다 항상 새롭게 디펜던시를 설치하고 빌드함으로써 많은 시간을 소요하는 것이 마음에 들지 않았다.

시간이 오래 걸리면 개발 생산성에도 영향을 미치지만, 무엇보다 CI로 소요된 시간만큼 비용 산정이 되므로 시간이 길어질수록 인프라 비용이 늘어나는 문제가 발생한다.

 

문제점

CI Test의 가장 큰 문제점은, CI가 돌 때마다 항상 아무것도 없는 상태에서 새롭게 시작한다는 것이다.
그런데 생각해 보면, 대부분 변경되는 사항은 비즈니스적 로직과 코드 단이지, 디펜던시 모듈이 자주 변경되지 않는다.

대부분 큰 차이가 없을 node_modules를 항상 새롭게 설치할 필요가 있을까?라는 생각이 들었다.

이미 이전에 생성했든 node_modules를 그대로 보유한 채 계속해서 다음 CI 단에서 재활용할 수 있다면, 그것만으로도 그 길다란 install 시간을 단축시킬 수 있을 것 같다.

 

그러나 각 CI는 독립적인 환경인데, 어떻게 node_modules를 물려줄 수 있을까?

 

새로운 CI 단계에 node_modules를 유지하기 위한 원리

아주 간단한 원리로 생각해 보았다.

'node_modules를 Cloud Bucket에 저장시키고, 다음 CI환경에서 다운로드 받도록 하자'

그냥 디렉토리 상태 그대로 Bucket에 업로드하면 용량도 크고 굉장히 오래 걸릴 테니, 압축을 해서 올리자.

 

해당 원리로 인해 발생하는 이슈

그러나 여기서 문제가 있다.
node_modules 디렉토리의 전체 용량은 내 프로젝트 기준 600MB 정도이고, 이를 압축하면 약 120MB로 줄일 수는 있지만, 문제는 압축하고 푸는 데 걸리는 시간과 해당 압축파일을 업로드하고 다운로드하는 데 걸리는 시간이다.

- 압축시간: 약 40초
- 압축 풀기 시간: 약 8초
- 압축파일 업로드 시간: 약 20초
- 압축파일 다운로드 시간: 약 14초

node_modules 설치가 약 90초 정도 소요되는데, 이 90초를 제거하기 위해 약 82초(40 + 8 + 20 + 14)의 허들 시간이 생성되면서, 전체적으로 겨우 8초 정도밖에 줄이지 못하게 된다.

이러면 큰 의미가 없을 것 같다. 어떤 좋은 방법이 있을까.

 

압축파일 다운로드 시간 무효화

// Dockerfile

FROM cypress/included:10.2.0 as runner
WORKDIR /app

...

 

기존에 Cypress Test CI를 돌리기 위한 docker build 과정에서 도커라이징할 때, cypress image를 pulling 받도록 되어있었다.

그리고 빌드의 각 레이어 처리 시간을 뜯어본 결과, Cypress pulling 받는 시간이 약 30초 정도 걸리는 것을 확인할 수 있었다.

어차피 image pulling으로 걸려야 하는 30초라면, 이때 같이 압축파일 다운로드도 동시에 할 수 있지 않을까?

 

그래서 cloudbuild 제일 앞단에 cypress image pull 하는 Step을 추가하고, 압축파일 다운로드 하는 Step과 병렬로 수행되도록 해보았고, 내 예상은 적중했다.

steps:
  - id: 'prepare:pull-image'
    name: 'gcr.io/cloud-builders/docker'
    args:
      [
        'pull',
        'cypress/included:10.3.0',
      ]

  - id: 'prepare:pull-cache'
    name: 'gcr.io/cloud-builders/gsutil'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        gsutil -m cp gs://mathpresso-frontend/qanda-ai-web/cached/*.tar.gz .    
    waitFor: ['-']

 

image를 pulling하면서 동시에 압축파일을 다운로드하고, pulling이 끝나자마자 바로 도커라이징을 진행한다.

이로써, 압축파일 다운로드 하는 약 14초의 시간을 무효화시킬 수 있었다.

 

압축파일 업로드 시간 무효화

그러면 이와 비슷하게 업로드하는 시간도 어차피 걸려야 하는 시간에 포함시켜 업로드를 처리하면 어떨까 생각이 들었고, 
도커라이징 후 테스트 코드를 돌리는 시간(약 1분)동안 동시에 업로드를 하면 좋겠다고 생각했다.

 

// cloudbuild.yaml

  ...

// 테스트코드 돌리는 Step
- id: 'run-test'
    name: 'gcr.io/cloud-builders/docker'
    args: ['run', 'asia-northeast3-docker.pkg.dev/qanda-project:latest']
    waitFor: ['test-builder-container']

// docker image 임시로 run 시킨 뒤 컨테이너에 압축파일만 꺼내오는 Step
- id: 'run-upload:get-tar'
    name: 'gcr.io/cloud-builders/docker'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        tempContainer=$(docker run --rm -d --entrypoint="sleep" asia-northeast3-docker.pkg.dev/qanda-project:latest 120)
        docker cp $tempContainer:/app/cache.tar.gz .
        
        docker kill $tempContainer
    waitFor: ['test-builder-container']

// 압축파일을 Bucket에 업로드 하는 Step
  - id: 'run-upload'
    name: 'gcr.io/cloud-builders/gsutil'
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        gsutil -m cp *.tar.gz gs://qanda-project/cached/

    waitFor: ['run-upload:get-tar']

 

도커라이징한 이미지를 가지고 있으니, 병렬로 돌릴 별도의 Step을 생성한 뒤 해당 Step에서 docker run 시켜서 압축 파일을 빼온 후 업로드하는 Step을 추가했다.
이를 통해 테스트 코드를 돌리는 동안 압축파일을 업로드시키도록 해서, 약 20초 시간을 무효화시킬 수 있었다.

34초 정도 무효화를 시킴으로써, 남은 허들시간 약 50초 (40초 + 8초)을 생성하는 대신 인스톨 시간 약 90초를 없앨 수 있으니, 이것만으로도 50초 정도 단축시킬 수 있었으니 나름 좋은 성과가 아닐까 했다.

 

압축 시간 무효화

그런데 문득 압축시간인 40초도 어떻게 병렬시간을 이용하여 무효화시킬 수 없을까 고민이 되었다.

현재 압축을 시도하는 위치는 도커라이징 할 때 yarn install 이후이다.

// Dockerfile

...

# Cache files
COPY *.tar.gz ./

# 압축 묶을 때, cache디렉토리와 install-state.gz 파일은 .yarn 디렉토리 안에 있는 상태에서 경로 지정으로 묶기 때문에
# 압축 풀때도 .yarn 디렉토리를 유지한 상태로 풀림. (기존 .yarn 덮어쓰지 않고 바로 .yarn 디렉토리 안으로 추가만 됨)
RUN tar -zxf ./cache.tar.gz

RUN yarn workspaces focus @mathpresso/qanda-ai-web

RUN tar -zcf cache.tar.gz node_modules .yarn/cache

...

 

그리고 cloudbuild 단에서 압축된 파일을 업로드하는 마지막 스텝에서, 테스트코드(약 1분)를 돌리는 동시에 압축 파일 업로드(약 20초)를 하고 난 뒤 남는 여유시간이 약 40초가 있다.

어차피 돌아가야 할 40초라면, 압축하는 시간 역시 이때 병렬로 돌릴 수 있지 않을까?

그러나 막상 압축 단계를 테스트 코드 Step으로 빼보려고 하니 쉽지가 않다.

기존에 압축을 수행하는 단계는 도커라이징을 하는 과정에서 압축을 진행했지만, 병렬로 압축을 처리할 수 있는 단계는 도커라이징이 끝난 후 테스트코드가 돌아가는 단계이다.

처음에는 병렬로 돌릴 새로운 스탭에서 컨테이너를 실행 후, 컨테이너 내에 설치되어 있는 node_modules를 cloudbuild 환경으로 카피를 해서 가져온 뒤 압축 후 올리면 해결될 줄 알았다.

그러나 그 무거운 node_modules를 카피를 하는 행위에서 이미 더 많은 시간이 소요되는 것을 확인한 뒤 사용할 수 없는 방법이란 것을 알게 되었다.

 

결국 이 방법을 해결하기 위해서는 아래 두 가지 조건이 만족되어야 한다.

1. 컨테이너 환경 안에서 직접 node_modules를 압축 처리를 할 수 있어야 한다.

2. 1번을 도커라이징이 끝난 뒤에 수행할 수 있어야 한다.

 

처음에는 이게 말이 되나? 싶었는데, 놀랍게도 docker에서 컨테이너 특정 환경에 쉘 명령어를 수행할 수 있는 기능을 제공한다는 것을 알게 되었다.

// cloudbuild.yarm

cloudbuild.yarm

tempContainer=$(docker run --rm -d --entrypoint="sleep" qanda-project 120)
        
docker exec -i $tempContainer bash tar -zcf cache.tar.gz node_modules .yarn/cache
docker cp $tempContainer:/app/cache.tar.gz .

 

exec.. 컨테이너 밖에서 컨테이너 안으로 쉘 명령어를 수행시킬 수 있다.
굉장히 멋지고 재미있는 명령어다..!

테스트코드가 돌아가는 동시에 별도의 병렬 스텝에서 임의로 컨테이너를 수행시키고 exec 명령어를 이용해서 컨테이너에 파일 압축 명령어를 수행시킨다. 수행이 끝나면 컨테이너 밖으로 압축된 파일을 가져온다.

 

그러나 새롭게 생긴 다른 허들 시간

분명 도커라이징 과정에서 압축하는 시간은 약 40초 정도였으나, cloudbuild 병렬 스텝에서 압축을 수행하는 시간은 약 1분이 살짝 넘어가는 것이었다.

처음에는 CI 컨디션 상태로 일시적인 문제인 줄 알았는데, 여러 번 테스트를 해보면서, 압축시간뿐 아니라 테스트코드 수행시간도 약 10초 정도 늘어난 것을 보고, Cloudbuild 자체에 병렬처리가 많아지면서 제한된 CPU 리소스 할당으로 인해 지연된 시간이란 것을 알 수 있었다.

이때 굉장히 이것저것 해결방안을 가지고 여러 가지 시도해 보았던 것 같다.

node_modules 일부 일부를 쪼개서 병렬로 가져오고, 병렬로 압축한 뒤, 병렬로 업로드를 한다던가, 혹은 도커라이징 과정에서 코드 빌드하는 동시에 압축을 하는 것도 시도해보았지만(요건 두 프로세스 동시에 하나의 File 접근에 대한 문제가 있어서 하지 못함.), 모두 실패했다.

그래서 그냥 파일 압축을 빠르게 해 버리는 방법이 없나 구글링을 해보니, 놀랍게도, 파일을 병렬처리 알고리즘을 통해 빠르게 압축해 버리는 pigz라는 녀석을 알게 되었다...

테스트해 보니 굉장히 빨랐다. 일반 압축으로 20초 정도 걸리는 것이 pigz로 압축하면 3초밖에 걸리지 않았다.

 

tar --use-compress-program=pigz -cf cache.tar.gz node_modules ./.yarn/cache

 

사실 이 정도면 그냥 도커라이징 중에도 압축 묶기를 해버려도 괜찮을 것 같았지만, 그래도 3초 정도 줄이는 작업을 위해, 병렬 스텝에서 압축을 처리하는 식으로 적용을 했다. 이로 인해 기존에 걸렸던 압축하는 시간 40초까지 무효화시킬 수 있었다.

 

코드 빌드 시간 단축

무엇을 더 최적화시켜볼 수 있을까 샅샅이 뒤져보다가, 코드 빌드시간에서 60초 정도 소비하는 것을 알 수 있었다.

요것도 node_modules와 동일하게, 이전에 빌드했던 코드를 유지하고 있다면 이후 빌드 시간은 굉장히 빠르게 마칠 수 있다.

그래서 node_modules를 압축하고 CI 단에 올릴 때, 빌드해서 나온 코드도 같이 추가해서 올리도록 했다.

tar --use-compress-program=pigz -cf cache.tar.gz node_modules ./.yarn/cache ./next

 

빌드도 캐시를 해버리니 약 1분에서 20초까지 줄여버렸다...

 

 

이렇게 하나하나씩 처리해서 나온 결과.

Before

 

After

 

약 5분 30초에서 약 3분 10초까지 줄일 수 있었다.

 

아직 해결하지 못한 허들 시간 하나

node_modules를 캐시 처리하는 과정에서 발생한 허들 시간을 거의 모두 처리함으로써, 기존의 빌드 시간에서 온전히 node_modules 인스톨 시간을 없애버리고, 덤으로 코드 빌드 시간도 단축시킬 수 있었다. 

그러나 아직 해결하지 못한 허들시간 한 가지가 다운로드된 압축 파일을 푸는 시간이다. 비교적 다른 허들 시간에 비해 짧은 약 8초 정도 이긴 하지만, 요것도 깔끔하게 해결해 보고자 많은 대안을 고민해 봤는데, 아직은 해결하지 못했다.

반응형