Spring Cloud, Ansible, Docker를 사용해 서버 무중단 배포 Trouble Shooting

salgu·2023년 2월 4일
3

Spring Cloud

목록 보기
8/9
post-thumbnail

서론

계기

현재 개발서버에서 코드가 합쳐질때마다 곧장 자동으로 배포가 되고 있어 배포가 되는 과정에서 서버가 잠시 내려가 여러 유관팀들이 생산성이 저하되어 있는 상태였고
현재 프로젝트에 적용되어 있는 기술로도 충분히 무중단 배포를 적용할 수 있다는 판단에 적용하게 되었습니다.

처음에는 무중단배포가 굉장히 간단할거라고 생각하고 적용했지만 쉽지않았고
문제들을 기록으로 남깁니다.

환경

현재 프로젝트가 Spring Cloud Eureka(discovery server), Spring Cloud Gateway가 적용되어 있었고 배포환경은 Jenkins, Ansible, Docker가 적용되어 있습니다.

Spring Cloud Eureka 란 : https://velog.io/@salgu1998/MSA-Spring-Cloud-Eureka-Service-discovery-pattern
Spring Cloud Gateway 란 : https://velog.io/@salgu1998/MSA-API-Gateway

구상

  • Eureka에 API-SERVICE1가 떠있는 상태에서 새로운 버전의 API-SERVICE2를 동시에 띄웁니다.
  • Eureka에 API-SERVICE2가 등록될 시간을 기다려 줍니다.(약 60초)
  • API-SERVICE1을 종료해줍니다.

문제

포트 중복, 도커 포트포워딩 문제

API-SERVICE가 고정 포트로 Application이 실행되고 있어 같은 host에 두개 이상을 띄울 수 없는 상황이였습니다.

server:
  port: 0

포트 중복문제를 피하기 위해 Spring Application의 application.yml 파일에 랜덤포트(0)로 지정해줍니다.

docker run -d --network=host {image}

또한 Docker로 컨테이너가 실행될 때 network를 host로 실행해주어 포트포워딩을 하지않아도 되도록 변경해줍니다.

컨테이너가 종료될 때 Spring application이 SIGTERM 신호를 받지 못하는 문제

API-SERVICE1와 API-SERVICE2가 정상적으로 실행되고 Eureka에 등록되어 로드밸런싱이 되는걸 확인하고 Ansible에서 API-SERVICE1의 컨테이너를 종료를 했으나 Eureka에서 application이 삭제되지 않고 계속 떠있는 현상을 발견했습니다.

spring application의 로그를 확인해보니 컨테이너가 종료가 될때 컨테이너 내부의 Spring application에서 SIGTERM 신호를 받지 못하고 컨테이너가 종료되어 Eureka에 등록을 종료하는 로직이 정상적으로 실행되지 않아 생긴 문제였습니다.

 - name: stop spring application.
    shell: |
      container_id=$(docker ps -a | grep -v "$container_id" | grep -v 'CONTAINER' | awk '{print $1}')
      docker exec -i "$container_id" kill -15 $(docker exec -i "$container_id" lsof -i | grep java | awk 'NR==1 {print $2}')
    ignore_errors: yes

  - name: Waiting for disconnect eureka.
    pause:
      seconds: 10

  - name: delete container.
    shell: |
      container_id=$(docker ps -a | grep -v 'API-SERVICE' | grep -v 'CONTAINER' | awk '{print $1}')
      docker rm -f "$container_id"

Ansible에서 해당 API-SERVICE 컨테이너에 spring application을 종료하는 명령어를 전달하는 방법으로 해결하였습니다.

Eureka에서 Spring application이 제거 되었지만 유레카 캐시가 제거되지않아 종료되어있는 주소로 약 30초간 라우팅 하는 현상

위의 방식으로 정상적으로 Spring application이 SIGTERM을 받고 종료되었고 Eureka에서도 정상적으로 shutdown 되었지만 약 30초간 이미 종료된 주소로 request를 보내 500 오류를 뱉는 현상이 생겼습니다.

해당 현상을 해결하기 위해 Gateway에 Retry filter를 추가하였습니다.

routes:
  - id: api-service
    uri: lb://API-SERVICE
    predicates:
      - Path=/api/**
    filters:
      - RewritePath=/api/(?<segment>.*), /$\{segment}
      - name: Retry
        args:
          retries: 2
          statuses: INTERNAL_SERVER_ERROR,SERVICE_UNAVAILABLE
          methods: GET,POST,PUT,PATCH,DELETE
          backoff:
            firstBackoff: 100ms
            maxBackoff: 100ms
            factor: 2

Retry filter를 추가하게 되면 gateway에서 API-SERVICE로 호출했을때 statuses에 해당하는 오류를 받게되면 retries에 설정해둔 숫자만큼 재시도를 하게 됩니다.

uri 설정에 API-SERVICE 앞에 lb를 붙혀 로드밸런싱이 되어있기때문에 재시도를 할때 로드밸런스되어 정상적으로 동작하는 서비스로 요청이 가게되어 주소록에서 제거되는 시간동안 정상적으로 Client가 응답을 받을 수 있게 되었습니다.

Gateway Retry filter 옵션 설명

  • Retry: 구성하려는 필터의 이름입니다.
  • retries: 시도할 재시도 횟수입니다.
  • statuses: 재시도를 트리거해야 하는 HTTP 상태 코드입니다.
  • series: 재시도를 트리거해야 하는 HTTP 시리즈(1xx, 2xx, 3xx, 4xx 또는 5xx)입니다.
    • backoff: 재시도에 대한 백오프 정책입니다.
    • firstBackoff: 초기 백오프 간격입니다.
    • maxBackoff: 최대 백오프 간격입니다.
    • factor: 각 재시도 후 백오프 간격이 증가해야 하는 요소입니다.
profile
https://github.com/leeeesanggyu, leeeesanggyu@gmail.com

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

좋은 글 감사합니다.

답글 달기