2025.03.08 - [인프라] - [쿠버네티스] Application 개발자가 꼭 알아야하는 Kubernetes Pod 기능 (1) - Pod 정보 조회하기
[쿠버네티스] Application 개발자가 꼭 알아야하는 Kubernetes Pod 기능 (1) - Pod 정보 조회하기
개요 Pod에서 어플리케이션의 동작을 구성하는 기본적인 요소들을 정리해보면 다음과 같다.Pod 생성Pod가 생성될 때 nodeSelector로 특정 노드를 지정할 수 있다.worker node의 resource를 얼마나 사용할지
kangth97.tistory.com
위 Pod 정보 조회 포스팅에 이어 Pod 종료와 관련된 기능을 알아보자.
내 Application 안정적으로 종료하기
pod 종료와 관련된 기능에는 두 가지가 있다. Prestop, GracefulShutdown.
두 기능이 어떻게 동작하고 사용해야 하는지 pod 삭제 과정 시나리오를 통해 알아보자.
lifecycle.preStop
Pod 삭제 요청이 들어오면 kubelet과 kube-proxy가 동시에 호출된다.
kubelet은 파드 삭제, kube-proxy는 파드로 가는 트래픽을 중단시키는 역할을 수행한다.
트래픽은 iptables를 통해 파드로 들어오기 때문에 삭제되는 pod의 ip를 iptables에서 제외시켜 트래픽이 들어가지 않도록 한다.
이 과정에서 delay가 발생할 수 있기 때문에 kubelet이 먼저 pod를 삭제해버리면 트래픽이 유실된다.
따라서 kubelet이 pod를 삭제할 때 delay를 주도록 preStop 커맨드를 설정한다. (sleep 5)
preStop이후 kubelet이 종료신호를 보내면 SIGTERM이라는 신호가 컨테이너에 전달된다. 이는 app에게 이제 종료할 준비를 알리는 신호이다.
app이 해당 신호를 못받는 경우가 발생할 수 있다. 예를 들어 톰캣 was 위에 app이 올라갈 때 sigterm을 was가 받게 된다. 이 때 was가 app에 전달하지 않고 자기혼자 종료할 수 있다.(버전 문제)
따라서 app에 graceful-shutdown 이라는 api를 만들어서 정상종료 로직을 만든다.
preStop 커맨드에 sleep 말고도 curl을 통해 graceful-shutdown api를 호출하게 하면 preStop 5초 이후에 정상적인 종료를 수행할 수 있다.
혼동할 수 있는 점은 preStop이 graceful shutdown 기능을 대체하기 때문에 app에 해당 기능을 넣지 않아도 된다고 이해할 수 있다. 이는 사실이 아니며 app에 무조건 정상종료에 대한 기능을 별도로 구현해야 한다. preStop이 항상 정상 종료를 보장하지는 않는다.
GracefulShutdown
graceful-shutdown 로직이 app 내부에서 수행하는 동작은 다음과 같다.
- 트래픽 수신 중지 -> 스프링을 사용하는 경우 알아서 해주기 때문에 로직적으로 처리할 것은 없다.
- 처리중인 쓰레드 대기 설정 -> 무한정 기다리지는 않고, 타임아웃을 설정한다.
- 외부 리소스와의 연결 해제 -> DB connection을 해제하지 않고 app이 죽으면 db connection pool이 마비된다. 따라서 미해제시 리소스가 누수될 수 있는 위험을 없애야 한다.
graceful shutdown 수행 후 정상적으로 종료되면 종료 코드 0을 반환하고, container는 Terminated-Reason: Completed, Pod는 Succeeded 상태가 된다. 이는 Pod가 삭제되기 전 아주 잠깐만 볼 수 있다.
Pod 내부 app이 graceful-shutdown 수행 중에는 Pod가 Terminating 상태가 된다.
쓰레드 대기 설정에서 타임아웃을 설정하지 않고 무한 loop에 빠지게 되면 app이 terminating 상태에서 벗어나지 못한다.
이를 방지하기 위해 terminationGracePeriodSeconds: 60 같은 설정을 두면 kubelet이 60초 동안 대기하고 있다가 pod를 강제로 삭제한다.
이 때 SIGKILL 신호를 전달하고 이는 앱을 바로 강제종료하는 신호이다. 이러면 container는 Terminated-Reason: Error가 된다.
grace period 시간을 잘 고려해야 무거운 app의 경우에도 리소스 누수가 되지 않고 정상적으로 종료된다.
만약 pod 삭제 시 이런 대기시간을 무시하고 싶다면 아래 명령어를 사용하자.
kubectl delete pod <pod> --grace-period=0 --force
비정상적인 종료 유형은 다음과 같다.
- Memory Leak
- CPU Overload
- 너무 많은 request 발생
이 때 app은 시스템을 보호하기 위해 갑자기 종료될 수 있다. Pod의 restart 횟수가 늘어나며 container를 다시 시작한다. 이러면 이전 container의 log가 유실되기 때문에 이전 컨테이너 로그를 보는 명령어가 필요하다.
kubectl log <pod> -c <container> —previous
파드 속성 중 terminationMessagePath에 '/usr/src/myapp/log/termination.message' 같은 경로를 연결하고 빚어상 종료에 대한 로그를 기록하게 할 수 있다.
혹은 kubectl get pod <pod> -o jsonpath=’{.status.containerStatuse[*].lastState}’ 명령어를 사용해 etcd에 저장되어 있는 pod 스펙을 조회하여 내용을 출력할 수 있다.
kubectl log는 pod의 이름으로 worker node에 저장된 app의 로그 데이터를 보여준다는 점에서 차이가 있다. 일반적으로 kubectl log를 많이 사용한다.
비정상적인 종료가 됐을 때 app은 1 이상의 종료코드를 리턴한다.
container는 app에서 종료코드 1 이상의 값을 수신하면 Terminated-Error 상태로 바뀌고 Pod는 Failed 상태로 바뀐다.
다만 Spring boot app에서는 sigterm으로 종료해도 143 코드를 반환하기 때문에 컨테이너 결과가 error로 끝나게 된다.
따라서 graceful-shutdown api에서 직접 시스템을 종료하고 0을 return하게 되면 종료 코드 0으로 succceded 상태로 만들 수 있다.
예제
상태 변경 과정을 모니터링 해보자.
먼저 Prestop을 주지 않고 파드 삭제 시 아래와 같은 로그가 출력된다.
kubectl get -n anotherclass-321 -o yaml -w pod <pod_name>
kubectl logs -n anotherclass-321 -c api-tester-3212 --follow --tail 20 <pod_name>
state:
terminated:
containerID: containerd://b787db5c735d72358ae9dba5904ae9719b4f9e904cf70387ac0dee745f6fd830
exitCode: 143
finishedAt: "2025-02-18T11:20:15Z"
reason: Error
startedAt: "2025-02-18T11:14:32Z"
위에서 언급했듯이 spring은 143 코드를 반환하기 때문에 reason: Error 상태로 종료된다.
deployment 설정에 lifecycle.preStop을 추가하고 graceful-shutdown을 수행하도록 해보자.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5; curl localhost:8080/graceful-shutdown; sleep 5"]
state:
terminated:
containerID: containerd://3310ea803abff9cafbf0a84fd31e7af72cb58eed1d74fb19ef5e79c03567f761
exitCode: 0
finishedAt: "2025-02-18T11:25:11Z"
reason: Completed
startedAt: "2025-02-18T11:22:47Z"
그러면 정상적으로 exitCode가 0으로 출력된다.
graceful-shutdown api는 다음과 같이 구현한다.
@Component
public class ShutdownHook {
private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());
@Autowired
private FileUtils fileUtils;
@PreDestroy
public void cleanup() {
try {
Thread.sleep(1000);
log.info("Database connection has been safely released. - {}", java.time.LocalDateTime.now());
Thread.sleep(1000);
log.info("File stream has been safely released. - {}", java.time.LocalDateTime.now());
Thread.sleep(1000);
log.info("Message Queue has been safely released. - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Thread is safely releasing.... - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Running Thread... (4/5). - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Running Thread... (3/5). - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Running Thread... (2/5). - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Running Thread... (1/5). - {}", java.time.LocalDateTime.now());
Thread.sleep(2000);
log.info("Thread has been safely released.. - {}", java.time.LocalDateTime.now());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 여기에 정상 종료 코드 반환한다고, 아래 로직 넣으면 종료 무한 루프에 빠짐
// System.exit(0);
}
}
위와 같이 @PreDestroy annotation으로 앱이 종료되기 전 무조건 실행되는 hook을 등록한다.
다만 주의해야할 점은 해당 코드에서 정상 종료 코드를 반환하기 위해 system.exit(0)을 추가하게 되면, 다시 위 코드의 clenup() 함수가 실행되는 무한 loop가 발생한다.
따라서 정상 종료인 코드 0을 반환하는 로직은 controller 단에서 반환해야 한다.
@GetMapping("/graceful-shutdown")
public void gracefulShutdown() {
log.info("Received internal shutdown API (/graceful-shutdown)");
// 내부의 종료 로직 호출 (정상 종료)
System.exit(0);
// 이후 ShutdownHook 컴포넌트에서 자원 해제 로직이 실행됨
}
이렇게 controller의 api 함수에서 system.exit(0)을 추가하면 이후 ShutdownHook의 cleanup() 로직이 실행된다.
memory-leak이 발생하는 경우 아래와 같이 error와 사유를 확인할 수 있다.
lastState:
terminated:
containerID: containerd://ac188634b4344e8146264767f5f1cf6b18d96225d8751e21477d384468dc8551
exitCode: 1
finishedAt: "2025-02-18T13:26:05Z"
message: The system has been shut down due to a memory leak.
reason: Error
startedAt: "2025-02-18T11:24:17Z"
@GetMapping("/unexpected-shutdown")
public void unexpectedShutdown() {
try {
throw new RuntimeException("The system has been shut down due to a memory leak.");
} catch (RuntimeException e) {
e.printStackTrace();
// 종료 메세지가 terminationMessagePath에 저장됨
fileUtils.writeTerminationMessage(e.getMessage());
// 애플리케이션을 즉시 종료하며, Shutdown 훅이 실행되지 않음 (비정상 종료)
Runtime.getRuntime().halt(1);
}
}
위와 같이 비정상 종료를 발생시키는 api를 만들어 보자.
종료메시지를 terminationMessagePath에 저장하여 파드가 app이 어떤 이유 때문에 종료되었는지 확인할 수 있다.
deployment에 terminationMessagePath 속성을 추가하고 종료 메시지가 위치할 path 값을 부여한다.
configmap에 동일한 path를 넣어주고 app에서 환경변수로 해당 값을 읽어와 write로 메시지를 파일에 써주면 된다.
apiVersion: v1
kind: ConfigMap
metadata:
namespace: anotherclass-321
name: api-tester-3212-properties
data:
spring_profiles_active: "dev"
application_role: "ALL"
postgresql_filepath: "/usr/src/myapp/datasource/postgresql-info.yaml"
termination_message-path: "/usr/src/myapp/log/termination.message"
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api-tester-3212
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: api-tester-3212-properties
terminationMessagePath: "/usr/src/myapp/log/termination.message"
위와 같이 configmap과 deployment의 template.spec.containers에 terminationMessagePath를 동일한 경로로 설정한다.
@Value("${termination.message-path}")
private String terminationMessagePath;
public void writeTerminationMessage(String message) {
Path messagePath = Path.of(terminationMessagePath);
try {
// 폴더 없으면 생성
Path parentDir = messagePath.getParent();
if ( parentDir == null ) {
return;
}
File path = new File(parentDir.toString());
if(!path.exists()) {
path.mkdirs();
}
// 파일 쓰기 (파일이 이미 있으면 대체, 없으면 생성)
Files.writeString(messagePath, message, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
log.error("An error occurred while writing to the file: " + e.getMessage());
}
}
비정상 종료 로직에서 호출되는 종료 메시지 write 메소드는 위와 같이 정의할 수 있다.
그러면 비정상 종료 시 "message: The system has been shut down due to a memory leak."와 같은 exception message를 확인할 수 있다.
'인프라' 카테고리의 다른 글
[쿠버네티스] Kubernetes에서 가장 이해하기 어려운 Ingress와 Nginx의 수 많은 기능들 (0) | 2025.03.11 |
---|---|
[쿠버네티스] 인프라 구성으로 배우는 Kubernetes Service의 거의 모든 기능들 (0) | 2025.03.09 |
[쿠버네티스] Application 개발자가 꼭 알아야하는 Kubernetes Pod 기능 (1) - Pod 정보 조회하기 (1) | 2025.03.08 |
개발자 쿠버네티스 개발/테스트 환경 구축하기 (0) | 2025.03.07 |
쿠버네티스에서의 컨테이너, 가상화 기술 정리 (0) | 2025.03.04 |