diff --git a/internal/instance/supervise/supervise.go b/internal/instance/supervise/supervise.go index d249309..137b4f1 100644 --- a/internal/instance/supervise/supervise.go +++ b/internal/instance/supervise/supervise.go @@ -169,34 +169,44 @@ func NewReal(cfg Config) (*Real, error) { }, nil } -// cleanStaleSocket 는 postgres unix socket dir 의 stale lock + socket 파일을 -// 제거한다. INC-2026-05-09: EmptyDir 운영 환경 (instance manager pod) 에서 +// cleanStaleLocks 는 postgres unix socket lock 파일 + DataDir 의 postmaster.pid +// 를 child fork 직전 제거한다. +// +// socket lock (INC-2026-05-09): EmptyDir 운영 환경 (instance manager pod) 에서 // container restart 후 잔존 socket lock 이 다음 postmaster 의 bind 거부 의 // 원인 ("FATAL: lock file ... already exists"). PostgreSQL 자체는 *동일 // PID 의 lock* 만 stale 처리, *다른 PID 의 socket lock* 은 거부 — 따라서 // supervisor 가 명시적으로 정리해야 함. // -// 안전성: 본 메소드는 Start() 의 mu Lock 보호 하에서 child fork 직전 호출됨. -// instance manager (PID 1) 와 동일 pod 내 다른 postmaster 가 *동시 실행 불가 -// 능* (child 이전 없음) 이므로 모든 socket 파일은 stale. +// postmaster.pid (INC-2026-06-22, postgres-prod 7일 CrashLoopBackOff RCA): +// 컨테이너 PID 재활용으로 postgres 가 매 기동마다 *동일 PID* (예: 15) 를 얻어, +// 직전 crash 가 남긴 postmaster.pid 의 PID 를 PostgreSQL 의 kill(pid,0) liveness +// 검사가 "살아있음" 으로 오인 → "FATAL: lock file \"postmaster.pid\" already +// exists" 무한 fail. init 컨테이너의 1회성 정리는 *메인 컨테이너 재시작* +// (CrashLoopBackOff 시 init 미재실행) 을 못 막아, 최초 1회 crash 후 잔존 +// postmaster.pid 가 모든 후속 재시작을 영구 차단했음. PostgreSQL 의 PID-alive +// 검사가 컨테이너 PID 재활용 환경에서 신뢰 불가 → supervisor 가 명시적으로 정리. // -// DataDir 의 postmaster.pid 는 PostgreSQL 자체가 PID-alive 검사 후 stale 처리 -// 하므로 본 메소드 범위 외. -func (r *Real) cleanStaleSocket() { +// 안전성: 본 메소드는 Start() 의 mu Lock 보호 하에서 child fork *직전* 호출됨. +// instance manager (PID 1) 와 동일 pod 내 다른 postmaster 가 *동시 실행 불가능* +// (이 Start() 의 fork 이전 + 각 shard 전용 RWO PVC = cross-pod 데이터 dir 공유 +// 없음) 이므로 socket lock 과 postmaster.pid 모두 stale — 제거 안전. +func (r *Real) cleanStaleLocks() { + // socket lock 정리. socketDir := "/var/run/postgresql" port := r.cfg.Port if port == 0 { port = 5432 } pattern := filepath.Join(socketDir, fmt.Sprintf(".s.PGSQL.%d*", port)) - matches, err := filepath.Glob(pattern) - if err != nil { - // pattern syntax error — 무시 (defensive, 실제로 발생 불가). - return - } - for _, m := range matches { - _ = os.Remove(m) // best-effort — 파일 부재는 정상 (cold start). + if matches, err := filepath.Glob(pattern); err == nil { + for _, m := range matches { + _ = os.Remove(m) // best-effort — 파일 부재는 정상 (cold start). + } } + // DataDir postmaster.pid 정리 (PID 재활용 RCA — 위 doc 참조). + // best-effort: 파일 부재 (cold start / init 이 이미 제거) 는 정상. + _ = os.Remove(filepath.Join(r.cfg.DataDir, "postmaster.pid")) } // resolveBinPath 는 BinPath 또는 BinDir/postgres 를 결정한다. @@ -214,12 +224,12 @@ func (r *Real) Start(ctx context.Context) error { if r.started { return errors.New("supervise: already started") } - // INC-2026-05-09 (postgres CrashLoopBackOff 42h+): postgres unix socket - // dir 가 EmptyDir 운영 시 container restart 후 stale lock file 잔존 → - // "FATAL: lock file ... already exists" 무한 fail. Instance manager 가 PID 1 - // 이고 본 메소드는 *child fork 직전* 호출되므로 동일 pod 내 살아있는 - // postmaster 부재 — 무조건 정리 안전. - r.cleanStaleSocket() + // INC-2026-05-09 (socket lock) + INC-2026-06-22 (postmaster.pid, PID 재활용): + // container restart 후 잔존 lock file (socket + DataDir postmaster.pid) 이 + // 다음 postmaster 의 "FATAL: lock file ... already exists" 무한 fail 원인. + // Instance manager 가 PID 1 이고 본 호출은 *child fork 직전* 이므로 동일 pod + // 내 살아있는 postmaster 부재 — 무조건 정리 안전 (cleanStaleLocks doc 참조). + r.cleanStaleLocks() bin := r.resolveBinPath() args := []string{ "-D", r.cfg.DataDir, diff --git a/internal/instance/supervise/supervise_test.go b/internal/instance/supervise/supervise_test.go index 466b099..f4e364d 100644 --- a/internal/instance/supervise/supervise_test.go +++ b/internal/instance/supervise/supervise_test.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "errors" + "os" "path/filepath" "strings" "sync" @@ -131,6 +132,34 @@ func TestReal_PIDZeroBeforeStart(t *testing.T) { } } +// TestReal_cleanStaleLocks_RemovesPostmasterPid 는 INC-2026-06-22 회귀 가드: +// 컨테이너 PID 재활용 환경에서 직전 crash 가 남긴 postmaster.pid 를 +// cleanStaleLocks (Start 의 fork 직전) 가 제거해야 "FATAL: lock file already +// exists" 무한 CrashLoopBackOff 를 차단한다. 본 라인 제거 시 본 테스트 실패. +func TestReal_cleanStaleLocks_RemovesPostmasterPid(t *testing.T) { + r := newRealForTest(t) + pidPath := filepath.Join(r.cfg.DataDir, "postmaster.pid") + // 직전 crash 가 남긴 stale postmaster.pid 모사 (PID 15 = 컨테이너 재활용 PID). + if err := os.WriteFile(pidPath, []byte("15\n/var/lib/postgresql/data/pgdata\n"), 0o600); err != nil { + t.Fatalf("seed postmaster.pid: %v", err) + } + r.cleanStaleLocks() + if _, err := os.Stat(pidPath); !os.IsNotExist(err) { + t.Errorf("postmaster.pid 가 cleanStaleLocks 후 잔존 (stat err=%v) — 제거 기대", err) + } +} + +// TestReal_cleanStaleLocks_NoPidIsNoError 는 cold start (pid 부재) best-effort +// 안전성 — 파일 부재 시 panic/생성 없이 no-op. +func TestReal_cleanStaleLocks_NoPidIsNoError(t *testing.T) { + r := newRealForTest(t) + r.cleanStaleLocks() // pid 부재 상태 — error/panic 없어야. + pidPath := filepath.Join(r.cfg.DataDir, "postmaster.pid") + if _, err := os.Stat(pidPath); !os.IsNotExist(err) { + t.Errorf("부재 pid 가 생성됨: stat err=%v", err) + } +} + func TestReal_StartStop(t *testing.T) { r := newRealForTest(t) if err := r.Start(context.Background()); err != nil {