본문 바로가기

프로그래밍/golang

[Golang] 자식 프로세스 생명 주기 관리 (Shutdown)

 

 

지난 포스팅에서는 간단한 서비스 Shutdown 로직에 대해서, 그 중에서 단일 프로세스 종료 절차 구현에 대해서 알아보았습니다.
이번에는 자식 프로세스(Child Process)를 관리하는 부모 프로세스의 종료 구현에 대해서 알아보려고 합니다.

 

1.  프로세스 종료 방법

자식 프로세스가 있는 부모 프로세스는 반드시 자식에게 종료가 완료 될 수 있는 시간을 제공해 주어야 합니다.
이유는 만약 상위 부모 프로세스만 먼저 종료가 되고, 자식 프로세스는 종료가 올바르게 되었는지 아니었는지 확인이 제대로 안되면
좀비 프로세스가 되어 서버 리소스를 계속 사용할 수 있습니다.

 

2.  3단계 에스컬레이션

  1. Sigterm : 자식에게 종료 신호를 보냅니다.

  2. Timout : 자식에게 종료 잘 될 수 있도록 유예시간을 줍니다.
    (만약 데이터베이스를 통해서 데이터를 작성 또는 수정 요청을 같은 중요한 수행 작업이 진행되고 있을 수 있습니다.)

  3. Sigkill : 유예시간이 지나도 응답이 없으면, 자식 프로세스를 강제로 종료하고 시스템 자원을 회수 합니다.

 

3. 코드 예제

package main

import (
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// 0. temp child process
	child := exec.Command("sleep", "5")
	if err := child.Start(); err != nil {
		fmt.Printf("Failed to start child process: %v\n", err)
		return
	}
	fmt.Printf("Child process started (PID: %d)\n", child.Process.Pid)

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

	done := make(chan error, 1)
	go func() {
		done <- child.Wait()
	}()

	select {
	case <-sigCh: // close signal
		// 1. child shutdown
		fmt.Println("Sending SIGTERM to child...")
		child.Process.Signal(syscall.SIGTERM)

		// 2. child가 완전히 종료 될 때까지 2초 정도 시간 제공
		select {
		case err := <-done:
			// 정상 종료
			fmt.Printf("Child terminated safely. (Result: %v)\n", err)
		case <-time.After(2 * time.Second):
			// 응답이 없으면 강제 종료
			fmt.Println("Grace period exceeded. Escalating to SIGKILL!")
			child.Process.Kill()
		}

	case err := <-done:
		// 외부 시그널 없이 자식 프로세스가 먼저 스스로 종료된 경우
		fmt.Printf("Child exited unexpectedly or finished first. (Result: %v)\n", err)
	}
	fmt.Println("End")
}