paint-brush
Go에서 우아한 종료 마스터링: 쿠버네티스를 위한 포괄적인 가이드~에 의해@gopher

Go에서 우아한 종료 마스터링: 쿠버네티스를 위한 포괄적인 가이드

~에 의해 Alex6m2024/08/14
Read on Terminal Reader

너무 오래; 읽다

이 가이드에서는 우아한 종료의 세계를 탐구하며, 특히 Kubernetes에서 실행되는 Go 애플리케이션에서의 구현에 초점을 맞춥니다.
featured image - Go에서 우아한 종료 마스터링: 쿠버네티스를 위한 포괄적인 가이드
Alex HackerNoon profile picture

좌절해서 컴퓨터에서 전원 코드를 뽑아본 적이 있나요? 빠른 해결책처럼 보일 수 있지만, 데이터 손실과 시스템 불안정으로 이어질 수 있습니다. 소프트웨어 세계에서도 비슷한 개념이 있습니다. 바로 하드 셧다운입니다. 이 갑작스러운 종료는 물리적인 종료와 마찬가지로 문제를 일으킬 수 있습니다. 다행히도 더 나은 방법이 있습니다. 우아한 종료입니다.


우아한 종료를 통합함으로써, 우리는 서비스에 사전 알림을 제공합니다. 이를 통해 진행 중인 요청을 완료하고, 잠재적으로 디스크에 상태 정보를 저장하고, 궁극적으로 종료 중에 데이터 손상을 방지할 수 있습니다.


이 가이드에서는 우아한 종료의 세계를 탐구하며, 특히 Kubernetes에서 실행되는 Go 애플리케이션 에서의 구현에 초점을 맞춥니다.

유닉스 시스템의 신호

Unix 기반 시스템에서 우아한 종료를 달성하기 위한 핵심 도구 중 하나는 신호라는 개념으로, 간단히 말해, 한 프로세스에서 다른 프로세스로 특정한 것을 전달하는 간단한 방법입니다. 신호가 어떻게 작동하는지 이해하면, 이를 활용하여 애플리케이션 내에서 제어된 종료 절차를 구현하여 원활하고 데이터 안전한 종료 프로세스를 보장할 수 있습니다.


신호는 여러 가지가 있으며 여기에서 찾을 수 있지만 우리가 다루는 것은 종료 신호뿐입니다.

  • SIGTERM - 프로세스에 종료를 요청하기 위해 전송. 가장 일반적으로 사용되며, 나중에 집중적으로 다룰 것입니다.
  • SIGKILL - "즉시 종료", 방해할 수 없음.
  • SIGINT - 인터럽트 신호(Ctrl+C와 같은)
  • SIGQUIT - 종료 신호(Ctrl+D와 같은)


이러한 신호는 사용자(Ctrl+C / Ctrl+D), 다른 프로그램/프로세스 또는 시스템 자체(커널/OS)에서 전송될 수 있습니다. 예를 들어, SIGSEGV (세그먼트 오류)는 OS에서 전송됩니다.


우리의 기니피그 서비스

실용적인 환경에서 우아한 종료의 세계를 탐험하기 위해 실험할 수 있는 간단한 서비스를 만들어 보겠습니다. 이 "기니피그" 서비스는 Redis의 INCR 명령을 호출하여 일부 실제 작업을 시뮬레이션하는 단일 엔드포인트를 갖게 됩니다(약간의 지연을 추가합니다). 또한 플랫폼이 종료 신호를 처리하는 방식을 테스트하기 위한 기본 Kubernetes 구성도 제공합니다.


궁극적인 목표: 요청/데이터를 잃지 않고 서비스가 우아하게 종료를 처리하도록 보장하는 것입니다. Redis에서 병렬로 전송된 요청 수를 최종 카운터 값과 비교함으로써, 우아하게 종료 구현이 성공적인지 확인할 수 있을 것입니다.

Kubernetes 클러스터와 Redis를 설정하는 방법에 대한 자세한 내용은 다루지 않지만, Github 저장소에서 전체 설정을 확인할 수 있습니다.


검증 과정은 다음과 같습니다.

  1. Kubernetes에 Redis와 Go 애플리케이션을 배포합니다.
  2. Vegeta를 사용하여 1000개의 요청(40초 동안 25/초)을 보냅니다.
  3. vegeta가 실행되는 동안 이미지 태그를 업데이트하여 Kubernetes 롤링 업데이트를 초기화합니다.
  4. Redis에 연결하여 "카운터"를 확인하세요. 1000이어야 합니다.


기본 Go HTTP 서버부터 시작해 보겠습니다.

하드 셧다운/메인.고

 package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }

이 코드를 사용하여 검증 절차를 실행하면 일부 요청이 실패하고 카운터가 1000보다 작음을 알 수 있습니다(숫자는 각 실행마다 다를 수 있음).


이는 롤링 업데이트 중에 일부 데이터가 손실되었다는 것을 분명히 의미합니다.😢

Go에서 신호 처리하기

Go는 Unix Signals를 처리할 수 있는 signal 패키지를 제공합니다. 기본적으로 SIGINT 및 SIGTERM 신호는 Go 프로그램을 종료시킨다는 점에 유의하는 것이 중요합니다. 그리고 Go 애플리케이션이 너무 갑자기 종료되지 않도록 하려면 들어오는 신호를 처리해야 합니다.

이를 위해서는 두 가지 옵션이 있습니다.


채널 사용:

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


맥락 활용(오늘날 선호되는 접근 방식):

 ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()


NotifyContext는 나열된 신호 중 하나가 도착하거나, 반환된 stop() 함수가 호출되거나, 부모 컨텍스트의 Done 채널이 닫힐 때(둘 중 먼저 발생하는 경우) 완료로 표시된(Done 채널이 닫힘) 부모 컨텍스트의 사본을 반환합니다.


현재 HTTP 서버 구현에는 몇 가지 문제가 있습니다.

  1. 느린 processRequest 고루틴이 있고, 종료 신호를 처리하지 않기 때문에 프로그램이 자동으로 종료되고, 실행 중인 모든 고루틴도 종료됩니다.
  2. 이 프로그램은 어떤 연결도 닫지 않습니다.


다시 써 보겠습니다.


우아한 종료/main.go

 package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }


업데이트 요약은 다음과 같습니다.

  • SIGTERM 종료 신호를 수신하기 위해 signal.NotifyContext를 추가했습니다.
  • 진행 중인 요청을 추적하기 위해 sync.WaitGroup 을 도입했습니다(processRequest 고루틴).
  • 서버를 고루틴으로 감싸고 컨텍스트와 함께 server.Shutdown 을 사용하여 새로운 연결 허용을 우아하게 중단했습니다.
  • wg.Wait()를 사용하여 진행 중인 모든 요청(processRequest 고루틴)이 계속 진행되기 전에 완료되도록 보장합니다.
  • 리소스 정리: Redis 연결을 종료하기 전에 제대로 닫기 위해 redisdb.Close()를 추가했습니다.
  • Clean Exit: os.Exit(0)을 사용하여 성공적인 종료를 나타냅니다.

이제 검증 과정을 반복하면 1000개의 요청이 모두 올바르게 처리되는 것을 볼 수 있습니다. 🎉


웹 프레임워크 / HTTP 라이브러리

Echo, Gin, Fiber 등의 프레임워크는 들어오는 각 요청에 대해 고루틴을 생성하여 컨텍스트를 제공한 다음 결정한 라우팅에 따라 함수/핸들러를 호출합니다. 우리의 경우 "/incr" 경로에 대해 HandleFunc에 제공된 익명 함수가 됩니다.


SIGTERM 신호를 가로채서 프레임워크에 정상적으로 종료하도록 요청하면 두 가지 중요한 일이 발생합니다(간단히 말해서):

  • 프레임워크가 들어오는 요청을 수락하지 않습니다.
  • 기존에 들어오는 모든 요청이 완료될 때까지 기다립니다(암묵적으로 고루틴이 종료될 때까지 기다립니다).


참고: Kubernetes는 로드 밸런서에서 종료 중이라는 라벨이 지정되면 로드 밸런서에서 Pod로 들어오는 트래픽을 더 이상 전송하지 않습니다.

선택 사항: 종료 시간 초과

프로세스 종료는 복잡할 수 있는데, 특히 연결 종료와 같이 많은 단계가 포함된 경우 더욱 그렇습니다. 모든 것이 원활하게 진행되도록 하려면 타임아웃을 설정할 수 있습니다. 이 타임아웃은 안전망 역할을 하여 예상보다 오래 걸리는 경우 프로세스를 우아하게 종료합니다.


 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }

쿠버네티스 종료 라이프사이클

Kubernetes를 사용하여 서비스를 배포했으므로, Kubernetes가 Pod를 종료하는 방법을 자세히 살펴보겠습니다. Kubernetes가 Pod를 종료하기로 결정하면 다음 이벤트가 발생합니다.

  1. Pod는 "종료" 상태로 설정되고 모든 서비스의 엔드포인트 목록에서 제거됩니다.
  2. preStop Hook이 정의된 경우 실행됩니다.
  3. SIGTERM 신호가 포드로 전송됩니다. 하지만 이제 우리 애플리케이션은 무엇을 해야 할지 압니다!
  4. Kubernetes는 기본적으로 30초인 유예 기간( terminationGracePeriodSeconds )을 기다립니다.
  5. SIGKILL 신호가 포드로 전송되고 포드가 제거됩니다.

보시다시피 장시간 실행되는 종료 프로세스가 있는 경우 terminateGracePeriodSeconds 설정을 늘려서 애플리케이션이 정상적으로 종료될 수 있는 충분한 시간을 확보해야 할 수 있습니다.

결론

우아한 종료는 데이터 무결성을 보호하고, 원활한 사용자 경험을 유지하며, 리소스 관리를 최적화합니다. 풍부한 표준 라이브러리와 동시성에 대한 강조를 통해 Go는 개발자가 Kubernetes와 같은 컨테이너화되거나 조정된 환경에 배포된 애플리케이션에 필요한 우아한 종료 관행을 손쉽게 통합할 수 있도록 지원합니다.

Go 코드와 Kubernetes 매니페스트는 Github 저장소 에서 찾을 수 있습니다.

자원