콘텐츠로 이동

트러블슈팅 — 격자 이행이 245초 걸려 dev 배포가 죽은 이유 (MSG-347)

요약

EPSG:5179 전환 머지 직후 첫 dev 배포가 헬스체크 150초를 넘겨 실패했다. 예외도 프로세스 종료도 없었고, Flyway V28의 좌표 변환이 245초를 잡아먹는 중이었다. 원인은 ST_Transform에 proj4 정의 문자열을 인자로 넘긴 것 — PostGIS가 호출마다 변환 경로를 새로 만든다(0.846ms). SRID로 넘기면 좌표계 캐시를 타서 0.008ms, 106배. 부산물 2개: 벤치마크를 count(*)로 감싸 아무것도 안 재고 있었고, 대체로 넣은 proj4text 비교 검사는 PostGIS가 그 컬럼을 안 봐서 무효였다(둘 다 실증 후 교체).

이 노트로 답할 수 있는 질문

  • 배포가 실패했는데 에러 로그가 없으면 어디를 보나?
  • ST_Transform에 좌표계를 문자열로 넘기는 것과 SRID로 넘기는 것은 왜 다른가?
  • 서버에 못 붙는 상태에서 마이그레이션 성능 문제를 어떻게 재현하나?
  • spatial_ref_sys.proj4text를 비교하면 좌표계 정합성을 보장할 수 있나?
  • 이미 적용된 Flyway V 파일을 고쳐야 할 때 절차는?
  • dev 스키마를 비우면 어떤 데이터가 자동으로 돌아오고 어떤 게 안 돌아오나?

요약

  • 증상: CD(run 31256846159)가 헬스체크 타임아웃으로 실패. 앱 로그가 12:18:18에서 끊긴 뒤 2분 19초 침묵. 예외 없음, systemd 종료 라인 없음 — 프로세스는 살아 있는데 막혀 있었다.
  • 위치 특정: Flyway가 남긴 마지막 NOTICE를 V28 구문 순서와 대조. 100행 DROP은 찍혔고 120행 DROP은 안 찍혔다 → 그 사이 CREATE TEMP TABLE mission_grid_map. 행마다 좌표를 변환하는 자리다.
  • 원인: ST_Transform(geom, from_text, to_text) 3-인자 오버로드는 호출마다 PROJ 파이프라인을 새로 만든다. SRID 오버로드는 DB 좌표계 캐시를 쓴다.
  • 수정: ST_Transform(point, 5179) 로 교체 + 이행 전 두 경로 좌표 대조 검사 신설. PR #135 (flyway-rewrite 라벨), develop 머지.

측정

로컬 Docker PostGIS 16-3.4에서 psql \timing 단일 실행. 부하테스트가 아니다 — RPS·p95는 측정하지 않았다.

대상 미션 격자 매핑 구문 V28 전체 출처
수정 전 (로컬 재현, 미션격자 10만) 166.8초 2분 53초 psql \timing
수정 후 (로컬 재현, 동일 조건) 2.5초 4.3초 psql \timing
수정 전 (dev 실제, 미션격자 48,134) 245.3초 flyway_schema_history
수정 후 (dev 실제, 빈 DB) 1.13초 flyway_schema_history

호출당 변환 비용은 문자열 0.846ms · SRID 0.008ms로 106배다.

발견

1. 서버 없이 재현하는 방법

EC2 접근 수단이 없는 상태에서, 로컬 DB에 dev와 같은 조건(구 체계 격자 10만 + 미션격자 10만)을 만들고 V28을 트랜잭션 안에서 통째로 실행한 뒤 롤백했다. 서버를 건드리지 않고 이행 시간만 잰다. 수정 전 버전이 그 한 구문만으로 150초를 넘기는 것을 확인해 "추정"이 "재현"이 됐다.

2. count(*)로 감싼 벤치마크는 아무것도 안 잰다

조사 초반 "2만 건 19ms니 변환은 안 느리다"는 결론이 나왔는데 측정이 틀렸다. SELECT count(*) FROM (SELECT ST_Transform(...) ...) 는 행 수만 세면 되므로 플래너가 안쪽 변환을 실행하지 않는다. count(ST_X(...)) 로 강제 평가시키니 같은 쿼리가 16,912ms — 890배 차이였다. 앞의 숫자는 2만 행을 만드는 시간만 잰 것이다.

3. proj4text 비교 검사는 무효였다

SRID로 바꾸면서 "DB 내장 정의가 계약 문자열과 다를 위험"을 막으려 spatial_ref_sys.proj4text 를 계약 문자열과 비교하는 사전 검사를 넣었다. 리뷰 지적을 받고 실증했더니, proj4text를 longlat 정의로 바꿔놓고 ST_Transform(g, 5179)를 불러도 미터 좌표가 그대로 나왔다. auth_name='EPSG' 가 있으면 PostGIS는 그 컬럼이 아니라 PROJ 내장 EPSG 데이터베이스로 파이프라인을 만든다. 검사가 실제로 쓰이는 경로를 안 보고 있었다.

교체안: 서비스 범위를 0.5도 격자로 덮는 221점에 두 경로를 실제로 적용해 좌표를 대조하고, 최대 차이가 1e-6m를 넘으면 중단한다. 현재 차이 0m, 비용 0.3초. 계약 문자열의 lon_0을 127.5→125로 틀리게 두니 234km 차이를 잡고 중단하는 것까지 확인했다.

4. 포맷 검증은 값의 세대를 못 가린다

dev 복구 과정에서 드러났다. courses-seed.json 이 격자 ID를 직접 담는데 8월 3일 파일이라 구 체계 값이고, 포맷은 멀쩡해서 리더의 정규형 검증을 그대로 통과한다. 재시딩하면 존재하지 않는 셀에 조용히 들어간다. 축제·팝업 파일은 위경도를 담아 시더가 GridEncoder.encode 로 그 자리에서 계산하므로 이 문제가 없다.

dev 복구에서 나온 판정

이미 적용된 V 파일을 고쳤으므로 기존 환경의 체크섬이 안 맞는다. prod는 flyway_schema_history 자체가 없어(V28 미적용) 무관했고, dev는 deploy.md 절차대로 스키마를 비우고 V1부터 재적용했다.

"새 V29로 전진 수정"은 이 경우 성립하지 않는다. 느린 구문이 V28 본문 안에 있어서, 데이터가 있는 DB는 V28을 통과하는 단계에서 멈춘다 — V29는 그 뒤라 도달하지 못한다.

스키마를 비우기 전에 무엇이 자동으로 돌아오는지 확인해야 한다. 이 세션에서 "미션은 기동 시 시더가 다시 넣는다"고 안내했다가 틀렸다. 미션 시더 3종은 기본값 false이고 "앱 1회 기동할 때만" 돌리는 설계이며, 상시 켜져 있는 건 zone 시더뿐이다.

데이터 복구 경로 결과
zones 기동 시 자동 UPSERT 48
축제(EVENT) 원천 재시딩 (위경도 → 새 격자 계산) 237 (유효 244 − 중복 7)
팝업(POPUP) 원천 재시딩 (종료분 제외가 설계) 157
코스(COURSE) 백업에서 이관 (id 재발급, 제목 매핑) 148
mission_grids 재시딩 31,914 + 코스 989 32,903

팝업이 백업의 345건에서 157건으로 준 것은 손실이 아니다. 원천 2,593건 중 종료일이 오늘 이후인 건이 정확히 157건이고, 백업의 345건은 7월부터 여러 번 시딩하며 그때그때 살아 있던 팝업이 쌓인 값이다.

리뷰에서 기각한 제안

제안 판정 근거
새 V29로 전진 수정 기각 느린 구문이 V28 본문 — V29에 도달하지 못한다
ST_SetSRID(point, 4326) 가드 추가 다르게 채택 가드 대신 ST_SetSRID 를 제거. 4326이 아닌 입력이 오면 조용히 오역하는 대신 올바르게 변환되거나 에러가 난다
IMMUTABLESTABLE 기각 PostGIS 3.4 카탈로그가 st_transform 네 오버로드를 전부 provolatile='i' 로 선언한다
btrim 비교 강화 쟁점 소멸 문자열 비교 자체가 사라졌다

시사점

  • 벤치마크는 결과값을 실제로 쓰는 형태로 짜야 한다. 아니면 플래너가 측정 대상을 건너뛴다.
  • 안전장치는 "무엇을 막으려는가"가 아니라 "실제 실행 경로가 그것을 보는가"로 검증한다. 사보타주(일부러 틀린 값을 넣고 반응을 보는 것)가 가장 싼 확인 방법이다.
  • 에러 없는 배포 실패는 로그의 침묵 구간과 마지막 출력 지점을 코드 순서와 대조해 위치를 좁힌다.
  • 좌표계는 값이 조금 어긋나도 사후 발견이 사실상 불가능하다. 검사는 이행 전에 세운다.

남은 일

  • courses-seed.json 원천 재산출 — 여전히 구 격자 ID. 코스를 재시딩할 일이 생기면 그때 새 격자로 다시 만들어야 한다(미루기로 한 결정 그대로).
  • 미션 시나리오 이행 회귀 테스트(병합 · seq MIN · target_count 클램프) 자동 커버리지 없음.
  • .claude/docs/infrastructure.md 가 DB를 RDS로 적고 있으나 실제 dev·prod는 EC2 위 postgis/postgis:16-3.4-alpine 컨테이너다.