Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions internal/instance/supervise/supervise.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 를 결정한다.
Expand All @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions internal/instance/supervise/supervise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
Expand Down Expand Up @@ -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 {
Expand Down