Skip to content

fix(router): deploy pg-router image + PGROUTER_* env + dedicated RBAC (B-01)#282

Open
leedg wants to merge 25 commits into
KeiaiLab:mainfrom
leedg:feat/router-deployment-fix
Open

fix(router): deploy pg-router image + PGROUTER_* env + dedicated RBAC (B-01)#282
leedg wants to merge 25 commits into
KeiaiLab:mainfrom
leedg:feat/router-deployment-fix

Conversation

@leedg

@leedg leedg commented Jul 13, 2026

Copy link
Copy Markdown

문제 (4노드 라이브에서 발견)

dev 4노드 클러스터(10.31.20.11~14) Phase 1 검증에서 router Pod 가 CrashLoopBackOff. 근본원인 2겹:

  1. postgrescluster_controller.gobuildRouterDeploymentresolvedImage.Image(= instance PG 이미지)를 넘긴다. PG 엔트리포인트가 POD_NAME 등 instance 전용 env 를 요구해 기동 실패. (코드 주석에도 P12-T2 까지 PG 베이스 이미지 placeholder 로 남아 있었음)
  2. 이미지를 pg-router 로 바꿔도 cmd/pg-router 의 env 계약(PGROUTER_*)이 미주입이라 토폴로지·백엔드 해석 불가.

이 때문에 Suite D(라우팅)·E(리샤딩) 검증이 통째로 막혀 있다.

수정

  • routerImage()ROUTER_IMAGE env 로 주입(기존 reshardCopyImage() 와 동일 패턴). CRD 변경 0.
  • routerEnv()PGROUTER_{NAMESPACE,CLUSTER,KEYSPACE,LISTEN,METRICS_ADDR} + TOPOLOGY=crd + BACKEND=status. CRD/status 기반으로 고정해 reshard·failover 후에도 라우팅 테이블이 따라가게 한다(정적 env 토폴로지는 갱신 불가).
  • router 전용 ServiceAccount/Role/RoleBindingshardranges, postgresclusters(+status) 읽기 전용. instance SA 와 분리(라우터는 lease/PVC fence 권한 불요 = 최소권한).
  • reconcile 이 Deployment 보다 SA/Role/RB 를 먼저 upsert (SA 부재 시 Pod 403).

테스트

신규 3건: routerImage env override/기본값 · Deployment 의 SA + PGROUTER_* env + PVC 미마운트(ADR 0003) · Role 읽기전용 + RoleBinding 결선.

⚠️ 작성 환경(Windows)에 Go 툴체인이 없어 컴파일·테스트는 CI/노드에서 검증 예정입니다. CI 결과 확인 후 머지 부탁드립니다.

NetworkModuleTest Developer and others added 15 commits July 8, 2026 09:18
AutoSplit 을 스키마/admission 골격에서 동작하는 제어 루프로 완성한다.
§6.9 "AutoSplit / 자동 shard 확장 미구현" 해소.

관측 파이프라인 (size 트리거):
- statusapi.Status 에 SizeBytes 추가 — instance manager 가 primary 에서
  pg_database_size 를 보고(Supervisor.DatabaseSizeBytes: Real=SQL, Mock=필드).
- aggregate_status 가 primary 보고값을 ShardStatus.SizeBytes 로 집계.
  (SizeBytes 필드는 API 에 존재했으나 아무도 채우지 않던 끊긴 파이프라인.)

autosplit.go (제어 루프):
- ShardMetricsObserver 인터페이스 + default statusShardObserver(status.Shards
  읽기, 순수·테스트가능). CPU/P99 latency 는 metrics 소스 미결선이라 0(관측 시
  AND 조건상 오탐 없이 미발동, condition 메시지로 노출).
- 트리거 AND 평가 + durationMinutes 지속 추적(shouldPromoteAfterDebounce 미러).
- router.SplitHashRange 로 hash 범위 중점 분할 → 결정론·DNS-safe target ID →
  멱등 ShardSplitJob 생성(owner=cluster). requireApproval 이면 approval=required
  표식 → SSJ 컨트롤러가 승인 annotation 전까지 Pending 유지.
- AutoSplitEligible condition 갱신(reason: SplitCandidate/NoCandidate/
  SplitInProgress/UnsupportedVindex/MetricsSourceMissing).
- cluster 당 한 번에 하나의 split(진행 중이면 새 job 생성 skip).

검증(Windows go1.26.4 + envtest):
- go test ./internal/router (SplitHashRange + 보존 불변식) ok
- go test ./internal/controller 전체 envtest ok 36.4s (AutoSplit 유닛 8종 +
  fake-client reconcile 3종: 승인게이트 job 생성/멱등, 임계미달 무생성, cpu
  미결선 MetricsSourceMissing)
- go test ./cmd/instance ./api/v1alpha1 ./cmd/pg-router ok, go vet clean
- supervise fork/exec 2건은 Windows .sh 한계(baseline 동일, 회귀 아님)

RBAC: cluster reconciler 에 shardsplitjobs create/delete + shardranges 조회 추가.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
라우터 HPA 를 CPU 외에 active client-connection 수로도 스케일할 수 있게 한다.
§6.9 "active-connection custom metric 노출/어댑터 (현재는 CPU utilization 기준)" 해소.

메트릭 노출 (pg-router):
- cmd/pg-router/metrics.go — active-connection 게이지(atomic) + /metrics(Prometheus
  텍스트, zero-dep) + /healthz HTTP 서버(PGROUTER_METRICS_ADDR, 기본 :9187).
- trackConn 래퍼가 연결 수명 동안 게이지 inc/dec(panic-safe defer).
- 게이지 이름 = v1alpha1.RouterActiveConnectionsMetric(HPA 와 공유 상수 → 불일치 방지).

HPA 결선 (opt-in, 비파괴):
- RouterAutoscaleSpec.ScaleOnActiveConnections(기본 false) — true 일 때만 buildRouterHPA
  가 CPU 메트릭에 더해 Pods 메트릭(RouterActiveConnectionsMetric, AverageValue
  targetActiveConnections)을 추가. 기본은 CPU-only(기존 동작·테스트 보존).
- 라우터 Deployment 에 metrics 포트(9187) + prometheus.io/{scrape,port,path} annotation.
- config/router/{deployment.yaml,README.md} 에 standalone 매니페스트 + prometheus-adapter
  규칙 예시 문서화(어댑터 부재 시 Pods 메트릭 unavailable → CPU fallback).

검증(Windows go1.26.4 + envtest):
- go test ./cmd/pg-router (metrics handler=게이지 3 노출 / trackConn inc·복원 /
  빈 addr no-op) ok
- go test ./internal/controller 전체 envtest ok 34.6s (HPA active-conn Pods 메트릭
  2종 + 기본 CPU-only + Deployment metrics 포트/annotation)
- go build ./... + go vet clean
- api/v1alpha1 test exe 는 WDAC(App Control) 실행 차단(§6.8 환경 이슈, 코드 무관) —
  vet 컴파일 통과 + 새 필드/상수는 controller·pg-router 테스트 실행으로 간접 검증

CRD: scaleOnActiveConnections 프로퍼티 수동 반영(controller-gen v0.20.1 이 Go1.26
로더와 비호환 — 스키마 diff 는 bool 필드 1개 추가로 결정론적).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
…s/abort 라이브 테스트

§6.7 "abort 누수" + §6.8 "source-down 강제 제거 fallback" 미구현 갭 해소 + §6.7
"PK-없는 target 동시쓰기 경로 미검증" 라이브 테스트 추가(§6.8 Batch 3 설계 실체화).

ForceDropSubscription (source-down abort fallback):
- 일반 DROP SUBSCRIPTION 은 연관 원격 slot 정리를 위해 publisher 에 접속하므로
  source 가 죽으면 실패/지연 → AbortCleanup=False 로 누수. ForceDropSubscription 은
  DISABLE → slot_name=NONE(원격 slot detach) → DROP 순으로 publisher 접속 없이
  target subscription 을 확실히 제거한다(멱등, 부재 시 no-op).
- trade-off: 원격 slot 은 orphan 으로 남아 source 복구 후 정리(target 정리 우선).
- cmd/reshard-copy-poc cdc-abort: 정상 DropSubscription 실패 시 force fallback,
  publication drop 은 best-effort → source-down 에도 abort cleanup 완료.

라이브 테스트 (env-guard RESHARD_LIVE_*, TestCDCLive idiom — kind/make 불요):
- TestReshardPKlessTargetConcurrentLive: PK 없는 target 으로의 *동시* UPDATE/DELETE
  논리복제(seq-scan 경로) 수렴 + 이후 ReplicateIndexes 로 PK 복제 검증.
- TestReshardAbortSourceDownLive: ForceDropSubscription 이 slot detach 후 target
  subscription 제거(멱등) — source-down fallback 메커니즘 잠금.

검증(Windows go1.26.4):
- go build ./... + go vet ./internal/router ./cmd/reshard-copy-poc = 0
- TestCDC_RejectsInjection PASS(ForceDropSubscription injection guard 포함) +
  두 라이브 테스트 SKIP(env 미설정, 정상) + router/reshard-copy-poc 전체 PASS
  (WDAC App Control 우회: 테스트 exe 를 bin/ 로 빌드해 실행 — §6.8 admin
  allowlist 미적용 환경 대응)

남은 kind-live 게이트(별도 체크포인트): native 라우터 무중단 cutover 클라이언트
쓰기 실증, target 승격 후 chaos/failover drill. 위 라이브 테스트는 Linux/컨테이너
(postgres:18 wal_level=logical 2개 + RESHARD_LIVE_* env)에서 실행.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
3 개 feat 커밋(dc75010 AutoSplit / 86f0add active-connection HPA 메트릭 /
ff4f3e2 source-down abort fallback + 라이브 테스트)의 완료 상태를 WORK_HANDOFF
§6.9 에 동기. 남은 kind-live 게이트(native 무중단 cutover 실증, 승격 후 chaos
drill)만 미완으로 명시.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
사용 스크립트(go1.26.4 경로·GOTMPDIR/GOCACHE·test-windows.ps1·WDAC bin/ 우회),
방법, 예시 데이터(라이브 테스트 docker/env·설정 YAML·prometheus-adapter 규칙),
세션 전체 검증 결과, 남은 kind-live 게이트를 docs/IMPL_LOG_2026-07-08 에 기록.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
…B.6)

resharding target 승격의 fence-vs-adopt race 를 닫는다(KeiaiLab#220-class split-brain 방지).
§6.7 "다음 작업 진입점 1: ADR-0029 P-B precondition/fence 강화".

문제: 기존 promotePreconditionsMet 는 ShardRange active set 에서 source 제외만
확인했다. ShardRange flip 과 cluster reconciler 의 source STS scale-0 + status 제외
(P-C.1) 사이 창에서 source·target 이 같은 shard-id 로 동시 관측되면 aggregate_status
가 primary 2개(split-brain)로 오판할 수 있었다.

해법: sourceObservationExcluded — 각 source shard 가 PostgresCluster.status.shards 의
Ready primary 로 아직 관측되면 target adopt 를 보류(phase 유지 + requeue). 운영 관측
SSOT(status.shards) 기준으로 source 가 빠질 때까지 fence. cluster CR 부재 시 관측
없음 → fence 충족(격리/삭제 경로 안전).

검증(Windows go1.26.4 + envtest):
- TestSourceObservationExcluded (fake client, 4 케이스) + _ClusterNotFound PASS
- go test ./internal/controller 전체 envtest PASS 44.2s (Promote 4 spec 포함, 회귀 0)
- go build ./... + go vet clean

ADR-0029 §P-B.6 기록. 잔여: source PDB/PVC 삭제(opt-in) + 승격 중 pod kill live chaos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
AutoSplit 의 cpuPercent 트리거를 실제 관측 소스에 결선한다. §6.9 "AutoSplit
CPU 트리거 metrics 소스 미결선" 해소. size 에 이어 cpu 까지 실동작.

cpuAugmentingObserver (autosplit_cpu.go):
- base observer(statusShardObserver, size)를 감싸 각 shard primary Pod 의 CPU% 를
  보강: metrics.k8s.io PodMetrics(사용량) ÷ Pod CPU request × 100.
- 의존성 0 — k8s.io/metrics 모듈 추가 없이 controller-runtime unstructured GET 으로
  PodMetrics 읽음(레포 미니멀리즘, SQL 파서 25-dep 기각과 동일 철학).
- graceful degrade: metrics-server 부재(NoMatch/NotFound) / Pod metrics 없음 /
  CPU request 미설정 → CPU 관측 0. AND 조건상 오탐 없이 CPU 트리거 미발동.
- ObserveShards 에 ctx 추가(관측 I/O). newDefaultShardObserver 가 size+cpu 합성 —
  reconciler 기본 observer 로 주입(SetupWithManager + fallback).
- P99 latency 는 라우터 per-shard 지연 히스토그램 필요 → 여전히 미결선. latency 단독
  활성 시 AutoSplitEligible=MetricsSourceMissing 로 사유 노출.

RBAC: metrics.k8s.io/pods get;list 추가.

검증(Windows go1.26.4 + envtest):
- TestCPUAugmentingObserver (fake metrics + fake client: 800m/1000m→80%,
  500m/250m→200%, 미관측→0, request 미설정→0) + _NoPrimaryPod PASS
- AutoSplit 유닛 전체 PASS(UnsourcedMetricsReason 을 cpu→latency 로 정정)
- go test ./internal/controller 전체 envtest PASS 37.1s (회귀 0)
- go build ./... + go vet clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
능력 사다리 3단계 — scatter-gather 의 집계 재결합. 지금까지 concat(UNION ALL)/
order-by/limit 만 있어 `SELECT count(*) FROM t` 를 scatter 하면 shard 별 부분 count
가 N 행으로 나와 틀렸다. 부분 집계를 함수별로 재결합해 정답 1행(또는 GROUP BY
그룹당 1행)을 만든다.

scatter_aggregate.go:
- MergeAggregate 전략 + ScatterGather.Aggregates []AggregateFunc(컬럼별 함수,
  AggNone=GROUP BY key/passthrough).
- COUNT/SUM=부분값 합산, MIN=부분 min 의 min, MAX=부분 max 의 max. GROUP BY 는
  non-aggregate 컬럼으로 그룹핑(그룹 순서=최초 등장, 결정론).
- 정수 정밀도 유지(모두 정수면 int64, 실수 등장 시 float64 승격). SQL 시맨틱:
  COUNT-no-rows=0, SUM-no-rows=NULL, MIN/MAX-no-rows=NULL, NULL 무시.
- MergeAggregate 시 LIMIT pushdown 자동 비활성(부분 집계 전 truncate 방지).
- AVG 는 부분 평균 재결합 불가(가중 필요) → SUM/COUNT rewrite 필요, 범위 밖.

검증(Windows go1.26.4):
- TestScatterMergeAggregate 7종(scalar count/sum, min/max, GROUP BY,
  SUM-null-no-rows, float 승격, GROUP BY+Limit, 빈 Aggregates→concat fallback) PASS
- go test ./internal/router 전체 PASS(회귀 0), go build ./... + go vet clean

남은 것: planner 가 SELECT 리스트를 분석해 Aggregates 를 세팅하는 결선(후속).
merge 능력 자체는 tested building block(placement/metadata_store 와 동류).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
라우터 HA 강화 — pg-router 의 readiness 가 라우팅 가능 상태를 반영한다. 지금까지
metrics 서버의 /healthz 만 있어(항상 200) 토폴로지 미확보 상태에서도 k8s 가 트래픽을
보낼 수 있었다. /readyz 를 추가해 라우팅 테이블 확보 전에는 503 → Service endpoint
에서 제외되어 라우팅 불가 Pod 로 트래픽이 가지 않는다.

- cmd/pg-router/metrics.go: routerReady(atomic.Bool) + /readyz handler(미확보 503).
  /healthz 는 liveness(항상 200)로 유지 — 표준 k8s liveness/readiness 분리.
- main.go: static 토폴로지 즉시 ready, crd 는 초기 Refresh 성공 시 ready(실패 시
  refreshLoop 가 이후 확보하면 회복). 확보하면 캐시 서빙이라 일시 refresh 실패로 안 내림.
- 라우터 Deployment(operator + config/router/deployment.yaml)에 readinessProbe
  (httpGet /readyz:9187) 결선.

검증(Windows go1.26.4 + envtest):
- TestReadyzHandler_ReflectsRoutingReadiness (미확보 503 / 확보 200) PASS
- TestBuildRouterDeployment 에 readiness probe 단언 추가 PASS
- cmd/pg-router 전체 PASS + controller envtest 전체 PASS(회귀 0), build/vet clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
…iness)

IMPL_LOG 에 §7(P-B.6 fence) / §1 갱신(CPU 결선) / §8(scatter 집계 재merge) /
§9(라우터 readiness) 과정+결과 추가. §5 남은 작업에 informer·per-shard primary
Service 를 "live 검증 필요로 blind 보류"로 명시.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
…-follow)

각 샤드의 현재 Ready primary 를 가리키는 안정 Service 를 operator 가 publish 하고
failover 시 갱신한다 → 라우터/클라가 status polling 없이 DNS 로 primary 추종
(§6 백로그 "stable per-shard primary Service").

- names.go: ShardPrimaryServiceName(`<cluster>-<shard>-primary`).
- builders.go: buildShardPrimaryService — ExternalName Service(primary Pod 의 안정
  headless DNS 로의 CNAME alias) + primaryEndpointHost(status Endpoint "host:port"→host).
- postgrescluster_controller.go: reconcileShardPrimaryServices — 각 shard 의 Ready
  primary 에 ExternalName Service upsert(not-ready/부재 skip, flap 방지). copySpec 의
  Service case 에 ExternalName 동기화 추가(failover 갱신 핵심). status 집계 후 best-effort.

정체성 안전: pod 라벨 미변경(operator 단일권한 ExternalName alias) → KeiaiLab#220-class
split-brain 무관. selectorless 라 endpoint controller 무경합. RBAC 는 기존 services 충족.
설계: ExternalName(최소 surface, Pod IP/EndpointSlice 관리 불요) — primary Pod 이 이미
headless per-pod DNS 보유하므로 CNAME alias 만 operator 관리.

검증(Windows go1.26.4 + envtest):
- TestReconcileShardPrimaryServices_PublishAndFailoverUpdate(publish + pod-0→pod-1
  failover 시 ExternalName 갱신) + _SkipNotReadyOrNoPrimary + TestBuildShardPrimaryService
  + TestPrimaryEndpointHost PASS
- controller envtest 전체 PASS 29.7s(회귀 0), go build ./... + go vet clean

남은 것: 라우터가 이 primary Service DNS 를 backend 로 소비하는 config 결선(후속).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
WORK_HANDOFF §6.9 에 scatter 집계·readiness·per-shard primary Service·P-B.6 fence
완료 + "dev-완결 백로그 소진, informer 만 live 검증 필요" 를 index 로 반영(상세 SSOT
는 ROUTER-GAP-ANALYSIS §6 / IMPL_LOG §7~§10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
라우터가 interval polling(PGROUTER_REFRESH 10s)만으로 토폴로지/primary status 를
다시 읽던 것을 watch 기반 즉시 hot-reload 로 보강한다 → failover / resharding flip
반영 지연 단축. interval 은 watch 드롭 대비 fallback 으로 유지(§6 백로그).

- cmd/pg-router/watch.go:
  - watchReloader{connect, backoff, name}: 한 리소스 watch 유지 + 변경 이벤트를 notify
    로 전달. 세션 닫힘/Error → backoff 재접속, ctx 취소 → 종료. connect 를 함수로
    추상화(fake watcher 테스트). drain 은 non-blocking notify(changeCh cap 1 coalesce).
  - watchShardRangesAndCluster: ShardRange + PostgresCluster 각각 goroutine watch.
- main.go: newK8sClient → client.NewWithWatch. buildRouting 이 changeCh(cap 1) +
  watcher 기동 + refreshLoop 전달. refreshLoop 은 select{ticker(fallback)|changeCh(즉시)}
  + coalesce(debounce PGROUTER_WATCH_DEBOUNCE 200ms)로 버스트 1회 refresh.

결정론 검증(watch.FakeWatcher, live API 불요):
- TestWatchReloader_ForwardsEventsAndReconnects(이벤트→notify + 세션닫힘→재접속) /
  _CtxCancelExits / _ReconnectsOnConnectError(connect 실패 backoff 재시도) /
  TestCoalesce_AbsorbsBurst(5신호→1) / _CtxCancelReturns
- go test ./cmd/pg-router 전체 PASS, go build ./... + go vet clean

이로써 dev+envtest 검증 가능 백로그 전부 소진. 남은 것은 kind-live 게이트뿐
(실 watch 410/resourceVersion 최종 확인 포함 — interval fallback 이 안전망).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
두 완료 기능의 dev-완결 후속 결선:

(A) scatter 집계 planner 감지 (internal/router/aggregate_detect.go):
- DetectAggregates — SELECT 리스트를 토크나이저로 분석해 컬럼별 AggregateFunc 추출.
  순수 `fn(...)`+옵션 alias 만 집계로 인정, 표현식/중첩/SELECT */AVG/다중문/DISTINCT 는
  보수적으로 ok=false(일반 scatter degrade — 틀린 재결합 방지). 이로써 scatter 집계
  재merge 가 SELECT 로부터 Aggregates 를 자동 도출(라이브러리 레벨 end-to-end 완결).
- 유닛: count/sum/min/max·GROUP BY(단일·복수 key)·alias·qualified arg·대소문자 +
  부정 케이스 9종 + DetectAggregates→MergeAggregate end-to-end 정합.
- 남은 것: live wire-proxy(scattermode byte 경로)가 DataRow 디코드해 Row-merge 소비 =
  §(E) 두 반쪽 통합(더 큰 작업).

(B) primary-service backend 모드 (cmd/pg-router/main.go):
- PGROUTER_BACKEND=primary-service → primaryServiceResolver 가 각 shard 를
  `<cluster>-<shard>-primary.<ns>.svc.cluster.local:5432`(operator ExternalName
  Service, ShardPrimaryServiceName 정합)로 해석 → status polling 없이 DNS failover-follow.
- 유닛: TestPrimaryServiceResolver(ordinal + named shard). config/router README + 상단
  주석에 모드 문서화.

검증(Windows go1.26.4):
- go test ./internal/router 전체 PASS(DetectAggregates 12+end-to-end) +
  go test ./cmd/pg-router 전체 PASS(primary-service resolver)
- go build ./... + go vet clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122iPkb14PKrkTbiwVQf7Vf
…포 (B-01)

4노드 라이브 검증(Cluster repo Phase 1)에서 router Pod 가 CrashLoopBackOff.
근본원인 2겹:

  (a) postgrescluster_controller.go 가 buildRouterDeployment 에 resolvedImage.Image
      (= instance PG 이미지) 를 넘김. PG 엔트리포인트가 POD_NAME 등 instance 전용
      env 를 요구해 기동 실패.
  (b) cmd/pg-router 의 env 계약(PGROUTER_*) 미주입 — 이미지를 고쳐도 토폴로지/백엔드
      해석 불가.

수정:
  - routerImage(): ROUTER_IMAGE env 주입 (reshardCopyImage() 와 동일 패턴, CRD 변경 0)
  - routerEnv(): PGROUTER_{NAMESPACE,CLUSTER,KEYSPACE,TOPOLOGY=crd,BACKEND=status,
    LISTEN,METRICS_ADDR}. topology=crd + backend=status 로 고정해 reshard/failover 후
    라우팅 테이블이 CR·status 를 따라가게 한다.
  - router 전용 SA/Role/RoleBinding: shardranges + postgresclusters(+status) 읽기 전용.
    instance SA 와 분리(최소권한 — 라우터는 lease/PVC fence 권한 불요).
  - reconcile 이 Deployment 보다 SA/Role/RB 를 먼저 upsert (SA 부재 시 403).

테스트(신규 3): routerImage env override/기본값, Deployment 의 SA·PGROUTER_* env·PVC
미마운트(ADR 0003), Role 읽기전용 + RoleBinding 결선.

Refs: Cluster repo docs/test-results/INDEX.md B-01

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3bb50b3052

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +47 to +50
readinessProbe:
httpGet:
path: /readyz
port: 9187

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the duplicate readiness probe

In the standalone config/router Deployment this new /readyz probe is followed by another readinessProbe on the same container later in the file, so YAML decoding will keep only one of the duplicate keys (the later TCP probe). In that deployment path the router can still become Ready as soon as port 5432 is open, before the CRD topology has been loaded, which is exactly the case /readyz is meant to block; remove the old TCP readiness probe or merge it into this one.

Useful? React with 👍 / 👎.

Comment on lines +157 to +158
if aliasOnly(seg[close+1:]) {
return af.toAggregateFunc(), true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject DISTINCT aggregates before merging

When a scatter query contains COUNT(DISTINCT ...) or SUM(DISTINCT ...), this accepts it as a normal AggCount/AggSum because the aggregate arguments are not inspected. The merge step will then sum per-shard distinct results, which overcounts whenever the same distinct value appears on more than one shard; either reject aggregate-level DISTINCT here or implement a distinct-aware merge path.

Useful? React with 👍 / 👎.

@leedg leedg force-pushed the feat/router-deployment-fix branch 2 times, most recently from 1114c16 to 3bb50b3 Compare July 13, 2026 16:49
eastroad1 and others added 10 commits July 14, 2026 09:13
라이브 4노드 실측: operator 가 router Role 생성 시 거부됐다 —
  roles.rbac.authorization.k8s.io "dc-sharded-router" is forbidden: user
  "system:serviceaccount:postgres-operator-system:postgres-operator" is attempting to
  grant RBAC permissions not currently held: {APIGroups:["postgres.keiailab.io"] ...}

operator 자신이 shardranges/status·postgresclusters/status 권한을 보유하지 않아 RBAC
escalation 방지에 걸린 것. 애초에 불필요하다 — .status 는 부모 리소스를 GET/LIST 할 때 함께
실려오므로 읽기에는 서브리소스 권한이 필요 없다(서브리소스 권한은 status *쓰기* 용).

테스트 1건 추가(회귀 차단): Role 에 */status 리소스가 없어야 한다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
connection 모드는 접속 시 *dbname* 을 샤딩 키로 삼는 PoC 다 — 실 dbname 과 샤딩 키가
충돌하므로 오퍼레이터가 띄우는 라우터의 기본값으로 부적합하다. 분산 SQL 라우팅(WHERE 키
per-query 라우팅 / 무키 SELECT scatter-gather)이 성립하는 query 모드를 기본으로 하고,
ROUTER_MODE env 로 조정 가능하게 둔다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): 라우터가 조용히 틀린 답을 반환했다.
  SELECT count(*) FROM t;  -- 실제 4행 저장
   count
  -------
       3   ← shard-0 부분집계
       1   ← shard-1 부분집계
  (2 rows)

근본원인: 재결합 구현(internal/router 의 MergeAggregate / DetectAggregates)은 있었으나
*실제 배포되는 라우터*(cmd/pg-router 의 scatter 경로)가 이를 호출하지 않았다 —
scattermode.go 는 샤드 응답을 UNION ALL 로 이어붙이기만 했다(주석에도 "재집계 필요(후속)").

수정:
- cmd/pg-router/scattermode.go: 병합부에 mergeScatterRows() 결선
  · 집계 → router.DetectAggregates → MergeAggregatePartials (COUNT/SUM/MIN/MAX 그룹당 1행)
  · ORDER BY → 전역 재정렬 (각 샤드는 이미 정렬돼 오므로 flatten 후 재정렬 = 전역 순서)
  · AVG·표현식·SELECT * 는 감지가 거부 → concat 으로 안전 degrade (틀린 재결합 금지)
  · 와이어 DataRow/RowDescription 디코드·인코드 헬퍼 추가
- queryOf(): 후행 세미콜론 트림 — psql 이 "...;" 로 보내면 DetectAggregates 가 다중문으로
  보고 재결합을 통째로 거부한다(라이브에서 ORDER BY 만 고쳐지고 count 가 계속 깨지던 원인)
- internal/router: 익스포트 래퍼 MergeAggregatePartials / SortRowsByCol (와이어 라우터 결선용)
- toInt64/toFloat: 문자열 파싱 추가 — PG 텍스트 프로토콜은 값이 전부 문자열이라
  미파싱 시 부분 count 합산이 0 이 된다

테스트 4건 추가: 집계 재merge(3+1→4) / 전역 ORDER BY / 세미콜론 트림 / AVG·비집계 degrade

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): tenant_id int 스키마에서 라우터가 모든 INSERT 를
`cannot scatter a keyless write query` 로 거부. 읽기는 조용히 scatter 로 새어나갔다.

근본원인: whereEqTok / insertValueTok 이 키 리터럴을 tokStr(문자열)만 인정 →
숫자 리터럴이 키로 안 잡힘. 정수 키(user_id / tenant_id)는 가장 흔한 샤딩 키인데
문자열 키(D-set)로만 테스트해서 드러나지 않았다.

수정: isKeyLiteral() = tokStr | tokNum. 해시는 키 문자열 기준이므로 토큰 텍스트를 그대로 사용.
테스트: INSERT/SELECT/UPDATE/DELETE 정수키 + 문자열키 회귀.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…opy 타임아웃 설정화 (B-15)

B-13 (라이브 4노드): tenant_id int 스키마에서 라우터가 모든 INSERT 를 거부.
  근본원인 2겹 — ① parser 의 키 리터럴이 문자열 전용(선행 커밋에서 수정)
  ② **배포된 라우터의 기본 extractor 가 regex** 였고 regex 도 따옴표 문자열만 매칭.
  parser 만 고쳐선 라이브가 안 고쳐졌다.
  수정: matchColumnEquals/matchInsertColumn 에 숫자 리터럴 그룹 추가 +
        pg-router 기본 extractor 를 auto(parser 우선 + regex fallback)로
        (PGROUTER_EXTRACTOR 로 조정 가능).

B-15 (라이브 4노드): offline reshard 의 InitialCopy 가 `context deadline exceeded` 로 실패.
  근본원인: reshard-copy 의 복사 타임아웃이 **60s 하드코딩**(CDC 모드만 15분).
  orders 10만행 복사가 60s 를 초과 → ShardSplitJob Failed.
  수정: 기본 15m + RESHARD_COPY_TIMEOUT env 로 조정 가능.

테스트: regex 추출기 숫자키(where/insert) + 문자열 회귀.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): InitialCopy Job 이 재시도되면 target 에 같은 행이 다시
INSERT 되어 중복 상태가 되고, 이후 인덱스 복제가 실패했다:
  router: create index on orders: pq: could not create unique index "orders_pkey" (23505)
  (copied 3000/103000 before error)

Job 은 backoffLimit 만큼 재시도되므로(타임아웃·네트워크 등 어떤 이유로든) 첫 시도가 일부
행을 남기고 죽으면 재시도가 그대로 깨진다 — 즉 InitialCopy 가 사실상 1회성이었다.

수정: CopyShardRange 가 복사 시작 전에 target 테이블을 TRUNCATE 한다. target 은 이 작업이
만든 빈 shard 이고 source 는 read-only 이므로 안전하며(주석의 rollback 정의도 target
truncate), 이로써 복사가 재시도 가능해진다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TIMEOUT passthrough

B-17 (4노드 라이브 실측 2026-07-14): 복사 처리량이 **분당 약 2,800행**에 그쳐
orders 10만행 이동에 36분+ 가 걸렸다 — InitialCopy 타임아웃을 반복 초과해 실질적으로
큰 샤드는 분할이 불가능했다.

근본원인: CopyShardRange 가 행마다 autocommit Exec 를 날렸다(매 행 왕복 + fsync).

수정:
- 한 트랜잭션 안에서 prepared statement 로 INSERT 하고 1000행마다 커밋(중간 커밋으로
  긴 트랜잭션의 WAL/락 부담도 회피).
- operator 가 RESHARD_COPY_TIMEOUT 을 copy Job 에 passthrough (B-15 의 env 노브가
  Job 에 전달되지 않아 실제로는 조정 불가였다).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): shard-1 하나를 분할했는데 **분할과 무관한 shard-0 이 삭제**됐다.

  - applyRouting 이 `sr.Spec.Ranges = flattenTargetRanges(ssj.Spec.Targets)` 로 ShardRange 의
    **전체 ranges 를 target 것으로 대체**했다 → shard-0 의 0x00000000~0x7fffffff 소실.
  - 그 결과 PostgresCluster.status.shards 에서 shard-0 이 사라지고 STS 가 0/0 으로 축소,
    PVC/PV(reclaimPolicy=Delete)가 GC 되어 **shard-0 의 6,000행이 비가역 소실**됐다
    (복구 후 빈 DB: `ERROR: relation "orders" does not exist`).
  - Cleanup Job 도 이미 사라진 source 에 접속하려다 실패 → ShardSplitJob Failed.

split 은 *부분 갱신*이어야 한다. mergeSplitRanges() 가 기존 ranges 에서 source shard 의
range 만 제거하고 target ranges 를 더한다 — 다른 shard 의 range 는 그대로 보존.

테스트: TestMergeSplitRanges_PreservesUnrelatedShards (shard-0 보존 + source 치환 + target 반영).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): split 이 Completed 로 끝나고 데이터도 정확히 이동했는데
라우터가 새 샤드(shard-1a/1b)에 접속하지 못했다 — `connection to server was lost`.

근본원인: reshard target STS 는 instance manager 없이 PG 만 띄우므로
`postgres.keiailab.io/instance-status` annotation 을 *영원히* 발행하지 않는다. 그래서
aggregateShardStatus 가 Primary 를 nil 로 두고, PostgresCluster.status.shards[].primary
가 계속 비어 라우터(PGROUTER_BACKEND=status)가 백엔드를 해석할 수 없었다.
  status.shards: shard-0|ready=true|ep=... / shard-1a|ready=|ep= / shard-1b|ready=|ep=

수정: annotation 이 없는 Pod 라도 reshard-target 라벨을 가지고 Kubernetes readiness 를
만족하면 primary 로 인정한다. target 은 단일 인스턴스라 primary 가 구조적으로 보장된다
(replica 없음). endpoint 는 target headless Service 기준 FQDN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4노드 라이브 실측(2026-07-14): status.shards[shard-1a].replicas 에 **copy Job Pod 들이**
들어가 있었다:
  replicas: [dc-sharded-rsd-copy-shard-1a-km4d8, dc-sharded-rsd-shard-1a-0,
             dc-sharded-rsd-copy-shard-1a-qr8n7, ...]

reshard 의 copy/delete Job Pod 도 대상 shard 를 가리키는 reshard-target 라벨을 달고 있어
podMatchesNamedShardIdentity 가 이들을 shard 멤버로 인정한 탓이다. 이들은 DB 인스턴스가
아니라 일회성 작업 Pod 이므로 primary 선출·replica 목록을 오염시킨다(B-19 의 status 공백과
연쇄).

수정: batch.kubernetes.io/job-name 라벨을 가진 Pod 은 shard 멤버에서 제외.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants