Kubernetes 를 위한 Spring Boot 개발 (feat. 무중단 배포/운영)

Thomas Kim·2022년 2월 21일
0

들어가며

현재 Kubernetes, Istio를 사용하여 Java, Vue, Python, Go 등으로 개발된 서비스를 개발/운영하고 있다. 우선 여기에서는 (Part 1) Spring Boot 설정 및 개발에 관련된 내용을 정리하고 Part 2에서는 kubernetes 에서 필요한 설정을 정리한다.

Spring Boot 을 Kubernetes 에 넣기 위해서는 아래와 같은 설정 및 개발을 기존 코드에 추가해야 한다.

Dockerize

Spring Boot Application 을 dockerize 하는 방법은 Jib, Buildpacks, Dockerfile 이렇게 세가지가 있다. 이 중 가장? 간단하고 build time이 빠르고 (즉, 효율적인 layering), image 사이즈가 가장 작아지는 Jib 을 사용한다. Jib 의 사용법은 간단하다. 아래와 같이 build.gradlejib gradle plugin을 추가하면 된다.

plugins {
  id 'com.google.cloud.tools.jib' version '3.2.0'
}

그리고 ./gradlew jib을 통해 image를 빌드 할 수 있다.

참고로 로컬 kubernetes 개발 환경에서 skaffold 를 사용한다면 아래와 같이 jib: {} 만 추가하면 알아서 로컬 kubernetes 에 빌드/배포해준다.

apiVersion: skaffold/v2beta27
kind: Config
build:
  local:
    push: false
  artifacts:
    - image: example/image-name
      context: ./example-app
      jib: {}

아래는 Github Actions를 사용하여 이미지를 build 하고 AWS ECR에 image를 push 하는 코드의 일부이다

- name: Set up JDK 11
  uses: actions/setup-java@v1
  with:
    java-version: 11
- name: Grant execute permission for gradlew
  run: chmod +x gradlew
- name: Build and Push with Gradle
  id: build-and-push-to-ecr
  run: ./gradlew jib -x test --image $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

Java 11 이상 사용을 권장한다. Java 8 은 containerize 된 환경에 최적화되지 않아 JVM이 효율적으로 운영되지 못한다.

Health Check APIs

Kubernetes 의 readiness, liveness 설정을 위해 Spring Boot application 의 health check API 가 필요하다. 다행히 Spring Boot Actuator 에서 이 기능을 제공한다. 아래와 같이 build.gradle 에 dependency로 추가만 하면 된다.

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

Spring Boot 2 부터는 Actuator의 /health/info 를 제외한 모든 endpoint가 disable 되어 있다. 만약 다른 endpoint 도 사용하고 싶다면 application.properties 에 추가적인 설정이 필요하다. 그렇지 않다면 그냥 dependenty 만 추가하고 그 외 추가적인 설정이나 개발은 필요없다.

kubernetes 의 readiness 설정을 하지 않으면 rolling update 시 또는 autoscale을 통해 새로 pod 가 생길때 spring boot application이 완전히 뜨기 전에 request가 들어가고 이렇게 들어온 request는 ingress가 503을 리턴하게 된다. 또한 liveness 설정이 없으면 예기치못한 상황으로 spring boot application 이 죽었을 때 서비스가 새로 시작하지 않고 계속 죽어있게 된다.

Graceful shutdown

운영환경에서 Pod가 termination 되는 상황은 rolling update로 배포를 하거나 kubernetes autoscale 을 통해 늘어난 Pod가 줄어들 때 등이 있을수 있다. 이때 kubernetes는 SIGTERM 시그널을 보내고 Pod안의 Spring Boot Application 은 종료가 된다.

하지만 Spring Boot의 default 설정은 시그널을 받자마자 종료되도록 되어 있고, 만약 종료될때 들어온 request가 완료되기 전에 Spring Boot application 이 내려가면 해당 request를 보낸쪽에서는 HTTP STATUS 503을 받게 된다.

실제 운영환경 (또는 load testing 시)에서 이 문제는 application log 에서는 확인하지 못하고 kubernetes ingress 에서 503 확인이 가능하다.

이를 graceful 하게 처리하기 위해서는 graceful shutdown 설정을 해야 한다. 이것 또한 간단하다. 아래와 같이 application.properties 파일에 아래와 같이 추가하면 된다. (단, Spring Boot 2.3 부터 가능한 option 이다.)

server.shutdown=graceful

해당 설정이 추가되면 tomcat 이 종료 시그널을 받았을때 처리중인 request가 있다면 이를 모두 처리하고 application 이 종료된다. 하지만 들어온 request가 종료될 때까지 무한정 기다리는것은 아니다. default 설정은 30초간 기다리고 그때까지 종료하지 못한다면 강제 종료된다. (일반적인 경우는 default 설정이면 충분하다) 이 설정은 아래와 같이 변경 가능하다.

spring.lifecycle.timeout-per-shutdown-phase=1m

Loading HikariCP

Spring Boot 2부터 Hikari 가 default DataSouce 구현체이다. spring-boot-starter-data-jpaspring-boot-starter-jdbc를 사용한다면 별도의 설정없이 Hikari를 사용하게 된다. Hikari는 Connection Pool (HikariCP)을 사용하여 DB connection 을 관리하는데 Spring Boot application 이 설정/개발에 따라 Hikari connection pool을 Spring Boot 이 시작할 때 바로 만들지 않고, DB 관련 request가 처음 들어와서 처리 할 때 그제서야 Hikari를 initialize 하면서 connection pool을 생성하기도 한다. (경험으로는 Hibernate를 사용하면 application 이 올라갈때 바로 connection pool을 생성하고 MyBatis는 그렇지 않았다)

사실 이러한 과정은 일반적인 상황에서는 문제가 되지 않는다. 하지만 요청이 폭발하는 상황에서 kubernetes 가 autoscale을 통해 새로운 pod를 생성하고 이렇게 생성된 pod가 바로 많은 요청을 받는 상황에서는 몇초간 latency가 매우 높아진다.

우선 현재 Spring Boot application 이 언제 connection pool을 생성하는지 확인하려면 application.properties 에 아래와 같이 설정을 추가하여 hikari log를 남기고, application을 실행해 보자. (테스트 후 반드시 해당 설정을 제거하자. 특히 운영환경에서는...)

logging.level.com.zaxxer.hikari.HikariConfig=DEBUG 
logging.level.com.zaxxer.hikari=TRACE

만약 application 이 시작하면서 Hikari 설정 관련 log 가 나오면서 connection pool 이 생성된다면 문제가 되지 않는다. 하지만 그렇지 않다면 아래와 같이 connection pool을 application 이 시작할 때 만들어주어 몇초동안 latency가 급격히 높아지는 현상을 줄일 수 있다.

@Component
public class HikariLoader implements ApplicationRunner {

    private final HikariDataSource hikariDataSource;

    public HikariLoader(HikariDataSource hikariDataSource) {
        this.hikariDataSource = hikariDataSource;
    }

    @Autowired
    public void run(ApplicationArguments args) throws SQLException {
        hikariDataSource.getConnection();
    }
}

마치며

Kubernets가 운영에 필요한 많은 일들을 해주지만 이렇게 추가적인 개발/설정 없이는 제대로 운영할 수가 없다. 그래도 Java는 다른 언어에 비해 굉장히 간단하게 이러한 설정들을 추가 할 수가 있다. Part 2에서는 실제 운영환경에서 필요한 Kubernetes 관련 설정들을 정리하려고 한다.

profile
Software Developer at SK Telecom

0개의 댓글