이전 글에서 이어집니다.
블루-그린 배포 구현하기
저희는 오래된 버전(이하 블루)과 새로운 버전(이하 그린)의 구분을 인스턴스가 아닌 포트를 통해 해결해보기로 결정하였고 첫 설정으로 블루는 8080포트, 그린은 8081 포트를 통해 트래픽을 보내기로 했습니다.
현재 사용중인 어플리케이션 포트확인
- name: 현재 사용중인 어플리케이션 포트 확인
shell: bash {0}
run: |
if [ -n "$(lsof -ti:${{ vars.APPLICATION_PORT_A }})" ]; then
echo "BLUE_PORT=${{ vars.APPLICATION_PORT_A }}" >> "$GITHUB_ENV"
echo "GREEN_PORT=${{ vars.APPLICATION_PORT_B }}" >> "$GITHUB_ENV"
else
echo "BLUE_PORT=${{ vars.APPLICATION_PORT_B }}" >> "$GITHUB_ENV"
echo "GREEN_PORT=${{ vars.APPLICATION_PORT_A }}" >> "$GITHUB_ENV"
fi
- 현재 사용중인 포트를 PID를 통해 확인해줍니다.
- 현재 사용중인 포트가 PORT_A인 경우 BLUE_PORT를 PORT_A로 지정해주고 GREEN_PORT를 PORT_B로 지정해줍니다.
- 그렇지 않은 경우에는 반대로 지정해줍니다.
그린 어플리케이션 실행
무중단 배포를 도입하기 전과 동일한 과정을 거쳐 실행하고 포트만 GREEN_PORT로 설정하여줍니다.
리버스 프록시 설정 변경
- name: 리버스 프록시 설정 변경
shell: bash {0}
run: |
echo "proxy_pass http://localhost:$GREEN_PORT;" | sudo tee /etc/nginx/conf.d/snackgame-port.inc;
sudo nginx -s reload;
proxy_pass를 기존 블루 어플리케이션에서 그린 어플리케이션으로 변경해줍니다.
블루 어플리케이션 종료
- name: 블루 어플리케이션 종료
shell: bash {0}
run: |
PROCESS_ID="$(lsof -i:$BLUE_PORT -t)"
if [ -n "$PROCESS_ID" ]; then
sudo kill -9 $PROCESS_ID
echo "구동중인 애플리케이션을 종료했습니다. (pid : $PROCESS_ID)\n"
fi
블루 어플리케이션의 포트를 이용하여 listen하고 있는 PID를 찾아 종료해줍니다.
여전히 오류가 발생한다
위에서 구현한 로직에 문제가 없다고 생각하고 배포를 진행하며 Jmeter를 사용해 부하테스트를 해보았습니다.
하지만 배포 도중 서버가 응답을 못하여 에러가 발생하였고 원래 의도한 무중단 배포를 완성하지 못하였습니다.
😨 로직 자체는 문제가 없는것 같은데 왜지…?
원인
생각을 해보니 스프링 서버를 부팅하는데에도 시간이 필요하나 그 시간을 고려하지 않고 아직 실행되지 않은 그린 어플리케이션으로 트래픽을 바꾸니 당연하게도 서버가 응답하지 못한것이었습니다.
해결
그래서 저희는 그린 어플리케이션에 http요청을 보내 HttpStatus.OK
를 응답할 때 까지 대기하도록 단계를 추가하였습니다.
결과
드디어 완성!!
배포 시작부터 끝까지 중단 없이 서비스가 잘 되고 있는 모습입니다.
추가(2023.01.12)
그린 어플리케이션 검증
이전에는 그린 어플리케이션의 배포가 실패하였을때에 후속 조치가 따로 없었습니다.
그래서 만약 실패하면 이 부분에서 무한정 대기를 하였는데 이 문제를 해결하고자 그린 어플리케이션의 검증을 추가하였습니다.
- name: 그린 어플리케이션 실행 검증
shell: bash {0}
run: |
PROCESS_ID="$(lsof -i:$GREEN_PORT -t)"
if [ -n "$PROCESS_ID" ]; then
echo "::error title=배포 실패::블루 어플리케이션으로 롤백합니다.";
exit 1;
fi
포트를 확인하여 배포가 실패할 시 기존에 사용하던 블루 어플리케이션을 유지하는 방식입니다.
추가(2023.01.18)
그린 어플리케이션 검증 수정
실제로 그린 어플리케이션이 완전히 실행되기 전에 검증을 마치고 접속을 대기하는 문제가 발생하였습니다.
위의 로직은 포트를 확인하여 PID를 검사하기에 열려있는 포트만 찾으면 다음 단계로 넘어가는데
그린 어플리케이션이 결과적으로 실행되지 않아도 포트가 먼저 열리는데 그 시점에 확인을 하여 제대로된 검증을 못한 것이었습니다.
- name: 그린 어플리케이션이 접속 가능할 때까지 기다린다
shell: bash {0}
run: |
PROCESS_ID="$(lsof -i:$GREEN_PORT -t)"
while [ "$(curl -o /dev/null -s -w %{http_code} localhost:$GREEN_PORT/rankings?by=BEST_SCORE)" != 200 ]
do
echo "새로운 어플리케이션을 띄우는 중입니다.";
sleep 5;
if [ -n "$PROCESS_ID" ]; then
echo "::error title=배포 실패::블루 어플리케이션으로 롤백합니다.";
exit 1;
fi
echo "새로운 어플리케이션을 띄우는 중입니다.";
sleep 5;
done
그래서 검증 로직을 ‘그린 어플리케이션이 접속 가능할 때까지 기다린다’ 단계안으로 이동시켜 어플리케이션이 완전히 실행될 때까지 지속적으로 검증하도록 수정하였습니다.
한계
Keep-alive
HTTP/1.0+ 부터 추가된 Connection: keep-alive는 요청이 처리된 후에도 connection을 유지하게 해주는데 이 부분이 저희에겐 문제가 된다고 생각했습니다. 배포시에 블루 어플리케이션을 사용하던 사용자의 connection이 close 되지 않은 상태에서 블루 어플리케이션을 종료해버리면 TCP 연결이 끊어지기 때문입니다.
처음에는 아주짧은 시간(3s정도)동안 만약 클라이언트의 request가 처리가 지연되어 서버가 종료되면 keep-alive옵션으로 인해 문제가 발생한다고 생각하였습니다. 하지만 다시 한번 생각을 해보니 애초에 이런 상황이 만들어지는 것은 API 자체의 문제임을 깨달았습니다.
다시 돌아와서 그럼 keep-alive로 생기는 문제는 어떻게 처리할 수 있을까에 대해 고민해봤을때 두가지 방법을 찾았습니다.
Keep-alive를 사용하지 않도록 강제한다
근본적으로 문제를 해결하기 위해 keep-alive를 사용하지 않도록 하면 해결되지 않을까라고 생각했습니다.
하지만 요청의 시간이 오래걸리는 경우(ex 파일업로드)는 keep-alive의 유무에 관계없이 결국 연결이 끊기게 됩니다. 그래서 keep-alive 자체에 대한 조치는 적합하지 않다고 판단했습니다.
Graceful shutdown 적용
그래서 Spring Boot에 있는 Graceful shutdown기능을 활용하기로 하였습니다. Graceful shutdown에 대해서는 추후에 적용 후 더 자세하게 알아보도록 하겠습니다.