운영 작업

DB 지연 진단

연결 포화와 느린 문장과 잠금 대기를 구분한다

무엇을 하게 되나

연결 예산과 실제 연결을 비교하고 느린 문장·잠금 대기를 구분한다.

준비물

PostgreSQL 운영자 연결 권한과 로그 조회 권한이 필요하다. DB 연결 계약의 역할별 예산과 timeout을 먼저 확인한다. 관리 DB의 재시작과 확장 생성은 별도 운영 승인 범위다.

단계

1. 실제 연결과 timeout

역할마다 열린 연결 수는 DB에서 본다. Compose는 status와 project label로 고른 DB 컨테이너에서 docker exec -it <this-installation-postgres-container-id> psql -U <configured-user> -d <configured-database>로 들어가고, Helm은 운영자의 DB 클라이언트로 들어간다.

SELECT application_name, state, count(*)
  FROM pg_stat_activity
 WHERE application_name LIKE 'werft:%'
 GROUP BY 1, 2 ORDER BY 1, 2;

세션 설정은 그 연결에만 걸리므로 운영자 psql의 SHOW는 서버 기본값을 보인다. 플랫폼 연결의 값은 api 컨테이너(Helm은 api Pod) 안에서 같은 풀로 읽는다.

API_CONTAINER='<this-installation-api-container-id>'
docker exec -w /app/apps/control-host "$API_CONTAINER" bun -e '
import { createLogger } from "@werft-ai/observability";
import { createApiPool } from "./src/api/pool.ts";
const pool = createApiPool(process["env"]["DATABASE_URL"], createLogger());
const { rows } = await pool.query(
  "SELECT current_setting($1) AS statement_timeout, current_setting($2) AS idle_in_transaction, current_setting($3) AS application_name",
  ["statement_timeout", "idle_in_transaction_session_timeout", "application_name"],
);
console.log(JSON.stringify(rows[0]));
await pool.end();'
# {"statement_timeout":"10s","idle_in_transaction":"30s","application_name":"werft:api"}

API_CONTAINER는 이번 설치의 status와 project label로 확인한다. Helm은 docker exec -w /app/apps/control-host "$API_CONTAINER" 대신 kubectl -n werft exec deploy/werft-api -- sh -c 'cd /app/apps/control-host && bun -e …'로 같은 스크립트를 돌린다. release 이름이 werft가 아니면 namespace와 deployment 이름을 그 설치의 것으로 바꾼다.

api의 pool 대기 시간이 길면 실제 연결 수와 예산을 비교한다. 운영자 psql에서 SHOW한 값은 플랫폼 연결 설정을 증명하지 않는다.

2. 느린 문장과 잠금

Compose

infra/compose.core.yml의 postgres가 다음을 켜고 뜬다.

  • log_min_duration_statement=500ms: 500ms를 넘긴 문장을 본문과 시간으로 로그에 남긴다. POSTGRES_LOG_MIN_DURATION_STATEMENT로 바꾸고 -1이면 끈다.
  • log_lock_waits=on: deadlock_timeout(1초)보다 오래 기다린 잠금을, 그 잠금을 쥔 PID와 함께 남긴다.
  • shared_preload_libraries=pg_stat_statements: 문장별 누적 통계를 모은다. 보려면 처음 한 번 CREATE EXTENSION IF NOT EXISTS pg_stat_statements;를 실행한다.
DB_CONTAINER='<this-installation-postgres-container-id>'
docker logs "$DB_CONTAINER" 2>&1 | grep -E 'duration:|still waiting'
SELECT calls, round(mean_exec_time::numeric, 1) AS mean_ms,
       round(total_exec_time::numeric) AS total_ms, left(query, 120)
  FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;

이 설정을 바꾸면 postgres 컨테이너가 다시 만들어진다. 데이터는 postgres-data volume에 남는다.

Helm (운영자 DB)

chart는 DB를 만들지 않으므로 서버 설정도 바꾸지 않는다. 운영자가 DB 파라미터에 다음을 건다. RDS·Aurora는 parameter group에서 바꾸고, shared_preload_libraries는 인스턴스를 다시 시작해야 적용된다.

파라미터값
log_min_duration_statement500 (ms)
log_lock_waitson
shared_preload_libraries기존 값에 pg_stat_statements 추가

그다음 플랫폼 DB에서 CREATE EXTENSION IF NOT EXISTS pg_stat_statements;를 한 번 실행한다. 확장을 만들 권한(RDS는 rds_superuser)이 필요하다. migration은 확장을 만들지 않는다. 권한이 없는 설치에서도 migrate가 실패하지 않게 하기 위해서다.

로그는 DB가 남기는 곳(RDS는 CloudWatch Logs의 postgresql 로그)에서 duration:과 still waiting으로 찾는다. kind 확인 환경(deploy/kind/deps.yaml)의 Postgres는 이 표의 값으로 뜬다.

확인

역할별 연결 수, duration 로그, still waiting 로그, pg_stat_statements의 총 시간·평균 시간을 함께 확인한다. 운영자 클라이언트의 열린 BEGIN이 오래된 잠금을 잡지 않는지도 확인한다.

자주 막히는 곳

관리 DB의 확장을 만들 권한이 없으면 DB 관리자에게 해당 작업을 요청한다. shared_preload_libraries는 재시작이 필요할 수 있다. metrics의 read-only options를 거절하는 pooler는 지표 계약을 확인한다.

되돌리기

이번에 바꾼 로그 파라미터와 수집 간격을 이전 값으로 되돌린다. 통계 확장이나 운영 데이터를 진단이 끝났다는 이유로 삭제하지 않는다.

다음에 읽을 곳

DB 연결 계약, 업그레이드 호환성.

이 페이지