소개
다운로드 창구는 이미 현장 노트를 받는 사람의 전송을 자르지 않고 닫아야 합니다. 즉시 연결을 닫는 동작을 Go의 일반적인 HTTP 종료 조정으로 바꾸고 원래 프로세스에서 받은 실제 완전한 다운로드를 유지합니다.
이 단원은 준비된 핸들러와 같은 두 문서를 가지고 독립적으로 시작합니다. 이전 단원의 정확한 PID 및 수락 요청 관찰 또는 동등한 경험에 더해 핸들러, 오류, 고루틴, 채널 지식이 필요합니다. 신호 전달, 유한 컨텍스트, 오류 대조, main의 대기 책임은 아래에서 설명합니다. 일반 HTTP를 다루며 WebSocket, 분산 트래픽 전환, 운영 환경 무중단 보장은 다루지 않습니다. 약 60분이라는 저자의 예상 시간입니다.
main이 종료 완료를 기다리게 하기
이 단계에서는 준비된 HTTP 핸들러를 유지하며 유한한 종료 조정을 구현합니다.
Downloads에서 시작해 독립 작업 공간으로 들어갑니다.
cd /home/labex/project/download-desk
cat lifecycle.go
수정할 수 있는 파일은 lifecycle.go뿐입니다. main.go의 핸들러, 두 문서, go.mod, 준비된 관찰기와 관찰 기록을 그대로 둡니다. 현재 함수는 SIGTERM을 받지만 Close를 호출해 활성 연결을 중단합니다. 신호 처리만으로 정상적인 종료 조정이 되는 것은 아닙니다.
http.Server는 리스너와 활성 HTTP 연결을 관리합니다. ListenAndServe는 작업을 받는 동안 블로킹합니다. Shutdown은 먼저 리스너와 기존 유휴 연결을 닫고 일반 활성 요청이 유휴 상태가 될 때까지 기다립니다. 종료를 시작하면 수락된 핸들러가 계속 실행 중이어도 ListenAndServe는 즉시 http.ErrServerClosed를 반환합니다. 이때 main이 반환하면 모든 고루틴이 사라져 전송이 끊깁니다. 따라서 주 실행 경로는 Shutdown 자체가 반환할 때까지 기다려야 합니다.
컨텍스트에는 취소 또는 시간 제한 예산이 담깁니다. context.Background()는 시작 컨텍스트를 제공합니다. context.WithTimeout(..., 45*time.Second)는 유한한 45초 종료 예산과 cancel 함수를 만듭니다. defer cancel()은 작업이 더 일찍 끝나도 함수 반환 시 관련 자원을 해제합니다. 취소는 완료의 증거가 아닙니다. 공개 상한은 중지 요청 후 60초입니다. 수락된 응답이 선택한 예산에 끝나지 않으면 오류를 알리고 상한 안에 0이 아닌 상태로 종료해야 합니다. 참고 구현은 45초이며 60초 이하의 동등한 유한 예산도 허용됩니다. 검사에는 60초 안에 끝날 수 없는 유한한 긴 응답이 포함되므로 context.Background()만 사용한 종료는 충분하지 않습니다. 격리된 시간 초과 시험에서는 부분 전송과 보고된 서버의 0이 아닌 종료 상태가 올바른 실패 결과이며, 정상적으로 기존 작업을 끝냈다는 뜻이 아닙니다. 예산이 만료되면 Shutdown이 오류를 반환하고 함수도 그 오류를 반환해 main이 0이 아닌 상태로 나갑니다. 이를 성공적인 처리 완료라고 하거나 서버를 조용히 재시작하지 않습니다.
signal.Notify(stopped, syscall.SIGTERM)은 Go에 해당 신호를 채널로 보내 달라고 요청합니다. 신호 전달은 수신자를 기다리면 안 되므로 채널 버퍼는 1입니다. 여기서는 대기 중인 중지 요청 하나면 충분합니다. signal.Stop(stopped)은 함수 종료 시 이 채널의 등록을 해제하며 HTTP 서버 중지와 다릅니다. 마찬가지로 버퍼가 있는 결과 채널은 main이 종료 작업을 하는 동안 서비스 고루틴이 하나의 결과를 보고하게 합니다.
첫 select는 서비스 오류 또는 중지 신호를 기다립니다. 바인딩/시작 오류가 신호 대기 뒤에 영원히 숨는 것도 막습니다. 정상 종료 후 errors.Is(err, http.ErrServerClosed)가 예상된 서비스 결과를 알아봅니다. 다른 오류는 성공으로 처리하지 않고 반환합니다. main이 runServer를 호출하므로 종료 완료 뒤에만 이 함수가 반환하게 해야 프로세스가 유지됩니다.
이 파일 하나를 교체합니다. 따옴표로 감싼 here-document 구분자는 마지막 단독 줄 GO까지 Go 소스를 그대로 쓰게 합니다. >는 lifecycle.go만 교체합니다.
cat > lifecycle.go <<'GO'
package main
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func runServer(server *http.Server) error {
stopped := make(chan os.Signal, 1)
signal.Notify(stopped, syscall.SIGTERM)
defer signal.Stop(stopped)
result := make(chan error, 1)
go func() { result <- server.ListenAndServe() }()
select {
case err := <-result:
return err
case <-stopped:
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
return err
}
err := <-result
if !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
GO
gofmt -w로 형식을 맞춥니다. -w는 정리된 파일을 다시 씁니다. 새 터미널이 숨겨진 PATH 설정에 의존하지 않도록 명시적인 실행 파일 경로로 현재 패키지를 빌드합니다.
/usr/local/go/bin/gofmt -w lifecycle.go
/usr/local/go/bin/go build -o desk .
문법 오류에는 파일과 줄 번호가 나옵니다. 전체 블록과 비교하고 필요하면 lifecycle.go만 다시 작성하세요. VM을 초기화하지 마세요. 검사는 격리된 임시 환경에서 원래 함수를 변경 없이 빌드하고 실제 다운로드 시간과 완료 순서를 달리합니다. 정상 성공과 긴 응답 실패 분기를 모두 검사하므로 약 1분을 기다리세요. 실행 중인 창구에 신호를 보내거나 소스를 바꾸지 않습니다.
다운로드를 보존하고 리스너 닫기
이 단계에서는 수락된 실제 다운로드를 보존하면서 새 연결을 거부하고 원래 서버를 기다립니다.
Server로 전환해 작업 공간에 들어가 새 관찰 시도를 시작합니다. 원래 프로세스가 끝날 때까지 이 터미널은 점유됩니다.
cd /home/labex/project/download-desk
python3 desk.py serve drained
실제 준비 메시지를 기다린 뒤 Downloads로 전환합니다. 다음 유한한 느린 전송을 시작하기 전에 예상된 프로브 실패와 복구를 포함해 이 단계 전체를 읽으세요. 이후 순서대로 실행하고 전송 시간에 새 개념을 배우지 마세요. 이전 단원처럼 유한한 느린 전송을 백그라운드로 시작합니다. 32바이트 쓰기 사이의 300ms는 평상시 읽을 시간을 주며 무제한 신호 대기가 아닙니다.
python3 desk.py download drained completed north-pier.txt 300 & download_job=$!
python3 desk.py accepted drained completed
관찰기는 중지 전에 실제 수락된 요청을 읽습니다. 원래 빌드된 프로세스의 식별 정보를 얻고 검사한 다음 이 터미널에서 SIGTERM을 보냅니다.
server_pid=$(python3 desk.py pid drained)
python3 desk.py identity drained
kill -TERM "$server_pid"
수락된 다운로드가 아직 진행 중일 때 새 연결을 시험합니다. 준비된 프로브는 최대 2초 동안 제한된 실제 연결을 시도하고 거부가 나올 때까지 모든 네이티브 결과를 저장합니다. Go가 전달된 신호를 처리한 후 첫 실패를 관찰할 여지를 줍니다. kill이 동기적으로 리스너를 닫는다고 가정하지 않습니다. 다음 명령은 연결 거부 메시지와 함께 7을 반환할 것으로 예상됩니다.
python3 desk.py probe drained
이는 새 연결 생성 거부이지 HTTP 403이나 수락된 전송 손상이 아닙니다. 원래 다운로드는 계속되어야 합니다. 백그라운드 작업을 기다리고 실제 저장 바이트를 원본과 비교합니다.
wait "$download_job"
cmp documents/north-pier.txt downloads/completed.txt
cat downloads/completed.txt
curl 네이티브 0, cmp 출력 없음, 완전한 마지막 장이 예상됩니다. Server에서는 이제 Original server native exit: 0을 보고해야 합니다. 클라이언트가 읽거나 파일을 플러시하기 전에 서버가 보내기를 끝낼 수 있습니다. 종료는 클라이언트 애플리케이션의 확인 응답을 기다리지 않습니다. 이는 별개 관찰이며 두 터미널 메시지의 순서를 요구하지 않습니다.
Downloads로 돌아옵니다. lifecycle.go, 완전한 다운로드, 네이티브 관찰을 유지합니다. 검사는 이 실제 결과를 읽고, 기존 유휴 연결과 서로 다른 순서의 수락 요청을 사용해 격리된 임의 포트에서 같은 소스를 시험합니다. 직접 Close, Shutdown 실행 중 main 반환, 작업 완료까지 열린 리스너, 추측한 시간만큼 잠자는 방식은 계약을 충족하지 않습니다.
accepted나 중지 작업 전에 다운로드가 이미 끝났다면 파일을 보존하고 실행 중인 시도에서 새 라벨을 선택하세요. 서버가 종료됐으면 Server에서 새 시도 이름을 명시적으로 시작하고 새 느린 출력과 identity, accepted, probe 명령에 그 이름을 사용합니다. 프로세스 종료 뒤에만 거부된 프로브로는 리스너의 조기 종료를 증명할 수 없습니다. 새 이름으로 활성 워크플로를 다시 관찰하세요. 이전 성공 파일을 덮어쓰거나 관찰 기록을 초기화하지 마세요.
이전 시도가 종료된 후 전체 재시도를 하려면 Server에서 실행합니다.
python3 desk.py serve drained-retry
준비 메시지 후 Downloads를 사용합니다. 모든 블록을 먼저 읽으세요. 새 이름은 이전 파일과 관찰을 보존합니다.
python3 desk.py download drained-retry completed-retry north-pier.txt 300 & download_job=$!
python3 desk.py accepted drained-retry completed-retry
server_pid=$(python3 desk.py pid drained-retry)
python3 desk.py identity drained-retry
kill -TERM "$server_pid"
다음 프로브는 7을 반환할 것으로 예상됩니다.
python3 desk.py probe drained-retry
wait "$download_job"
cmp documents/north-pier.txt downloads/completed-retry.txt
curl 0, 완전한 바이트 일치, Server의 원래 네이티브 종료 0이 예상됩니다. 이전 서버가 아직 동작하면 끝까지 읽은 뒤 해당 시도에서 새 느린 출력을 시작하세요. download와 accepted 모두 새 라벨을 쓰고 identity와 probe에는 시도 이름을 유지합니다. 이전 프로브가 이미 있으면 보존하고, 본인의 이전 시도를 명시적으로 중지해 종료를 기다린 후 위 전체 새 시도를 수행합니다.
실제 시작 오류는 서버의 기존 stderr에 나오며 0이 아닌 상태를 반환합니다. 설명된 소스/빌드 오류를 고치거나 본인의 앞선 시도가 끝날 때까지 기다린 후 8080을 다시 사용하세요. 같은 이름의 플랫폼 프로세스를 죽이면 안 됩니다. 종료 시간 초과는 유한 예산 안에 완료를 확인하지 못했다는 뜻입니다. 대체 프로세스나 과거 문서 복사로 대신하지 말고 사실대로 읽으세요.
요약
리스너, 수락된 HTTP 핸들러, 유한 종료 컨텍스트, main을 조정했습니다. 원래 프로세스는 새 연결 수락을 멈추면서 기존 작업을 완료했습니다. 정확한 최종 바이트와 네이티브 결과로 성과를 관찰할 수 있었습니다.



