로그의 타임스탬프 절반은 맞고 절반은 UTC라는 신고가 들어오면 어디부터 볼까요. Percona Community에 올라온 사례의 답이 흥미로웠습니다. 원인이 PostgreSQL 안이 아니었습니다.
배경은 Oracle에서 Patroni로 관리하는 PostgreSQL 18 클러스터로 마이그레이션한 직후입니다. 개발자들이 “타임스탬프가 어떤 건 맞고 어떤 건 UTC로 나온다"고 알렸습니다.
계층이 넷이었다
문제를 이해하려면 timezone 값이 어디에 저장되는지 짚어야 합니다. 이 환경에서는 네 곳이었습니다.
| 계층 | 처음 | 문제 상황 | 최종 |
|---|---|---|---|
| VM (OS) | UTC | UTC | UTC |
| PostgreSQL | UTC (기본값) | Africa/Lagos | Africa/Lagos |
| Patroni 설정 | 없음 | 재시작 후 UTC | Africa/Lagos |
| PgBouncer | UTC (캐시) | UTC (낡은 캐시) | 갱신됨 |
세 번째 행과 네 번째 행이 사건의 두 단계입니다.
1단계: ALTER SYSTEM이 살아남지 못한다
처음 시도한 방법이 이것이었습니다.
ALTER SYSTEM SET TIMEZONE = 'Africa/Lagos';
SELECT pg_reload_conf();
이 명령이 어디에 쓰이는지 컨테이너로 확인했습니다.
$ psql -Atc "show timezone"
Etc/UTC
$ psql -c "alter system set timezone = 'Asia/Seoul'"
$ psql -Atc "select pg_reload_conf()"
t
$ cat $PGDATA/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
timezone = 'Asia/Seoul'
새 세션에서는 바로 반영됩니다.
$ psql -Atc "show timezone; select now();"
Asia/Seoul
2026-08-24 19:02:52.265584+09
여기까지는 정상입니다. 문제는 값이 postgresql.auto.conf에 있다는 것입니다. Patroni는 클러스터 설정을 분산 설정 저장소에서 관리하고, 노드를 재시작할 때 자기가 가진 설정으로 구성 파일을 다시 씁니다. ALTER SYSTEM이 남긴 값은 그 과정에서 사라집니다. Patroni 노드가 재시작되자 timezone이 UTC로 되돌아갔습니다.
Patroni 환경에서 올바른 방법은 Patroni 쪽에 넣는 것입니다.
patronictl edit-config
여기서 설정한 값은 노드 재시작에도 유지됩니다. Patroni를 쓰는 클러스터에서 ALTER SYSTEM을 쓰지 않는다는 원칙이 이 사례의 첫 교훈입니다.
2단계: PgBouncer가 기억하고 있었다
Patroni 설정을 Africa/Lagos로 바로잡은 뒤에도 일부 세션이 UTC로 나왔습니다. 여기가 진짜 원인입니다.
PgBouncer는 세션 파라미터를 캐시합니다. 클라이언트가 연결할 때 넘기는 startup parameter를 기억해 두고, 풀에서 서버 연결을 꺼내 줄 때 그 값을 맞춰 줍니다. 그런데 Patroni 설정 변경으로 데이터베이스의 timezone이 바뀐 경우에는 이 내부 캐시를 제대로 무효화하지 않습니다. ALTER DATABASE로 바꿨을 때와 경로가 다릅니다.
그래서 낡은 UTC 기준으로 만들어진 세션들이 풀에서 계속 재활용됐습니다. 새로 만들어진 연결은 Africa/Lagos, 재활용된 연결은 UTC입니다. 로그에 두 값이 섞인 이유입니다.
flowchart TD
A[애플리케이션] --> B[PgBouncer 풀]
B --> C[낡은 캐시 세션
UTC]
B --> D[새 세션
Africa/Lagos]
C --> E[PostgreSQL]
D --> E
E --> F[로그에 두 값 혼재]
startup parameter가 서버 설정을 이긴다
캐시가 왜 이렇게 강한지 궁금해서 직접 확인했습니다. 서버 설정은 Asia/Seoul인 상태에서, 클라이언트가 연결 시점에 timezone을 지정해 봤습니다.
$ psql -Atc "show timezone"
Asia/Seoul
$ PGOPTIONS="-c timezone=UTC" psql -Atc "show timezone"
UTC
같은 서버, 같은 순간인데 값이 다릅니다. 클라이언트가 연결할 때 넘긴 값이 서버의 설정을 덮어씁니다. 이게 정상 동작입니다. 세션 단위 설정이 서버 기본값보다 우선하니까요.
PgBouncer의 캐시가 문제가 되는 지점이 여기입니다. 서버 설정을 고쳐도 pooler가 예전 값을 세션에 계속 넣어 주면, 서버 쪽 변경은 그 세션에 닿지 않습니다. show timezone으로 확인해도 클라이언트가 보는 값은 pooler가 정한 값입니다.
어떻게 고쳤나
PgBouncer 파드를 재시작해 캐시를 비웠습니다. 이후 새 연결은 모두 Africa/Lagos를 반영했습니다.
원문이 정리한 교훈 셋입니다. Patroni에서 timezone을 바꾼 뒤에는 PgBouncer를 재시작하거나 재연결시킵니다. ALTER SYSTEM SET TIMEZONE이 아니라 patronictl edit-config를 씁니다. 그리고 Kubernetes에서는 PgBouncer를 독립 파드가 아니라 sidecar로 배치합니다.
세 번째 항목에 이 사례의 구조적 원인이 들어 있습니다. PgBouncer가 독립 파드로 떠 있었기 때문에 Patroni 노드 재시작과 PgBouncer 재시작이 서로 무관했습니다. sidecar였다면 파드 재시작이 둘을 함께 갈아 줬을 것입니다. 연결 풀이 DB 인스턴스보다 오래 사는 배치에서는 이런 종류의 불일치가 반복됩니다.
진단할 때 볼 지점
같은 증상을 만나면 확인 순서를 이렇게 잡을 만합니다.
-- 지금 이 세션이 보는 값과 그 출처
select name, setting, source, sourcefile
from pg_settings where name = 'TimeZone';
-- 접속 중인 세션별로 실제 적용된 값
select pid, application_name, backend_start
from pg_stat_activity where backend_type = 'client backend';
pg_settings의 source 컬럼이 핵심입니다. 값이 configuration file이면 서버 설정에서, client면 클라이언트가 startup parameter로 넘긴 값입니다. client로 나오면서 값이 기대와 다르면 pooler를 봐야 합니다.
pooler 쪽에서는 SHOW SERVERS와 SHOW POOLS로 서버 연결이 언제 만들어졌는지 확인합니다. 설정을 바꾼 시각보다 오래된 연결이 남아 있으면 그게 문제의 세션입니다. RECONNECT 명령이나 파드 재시작으로 정리합니다.
남는 생각
이 사례가 좋은 이유는 원인이 PostgreSQL 밖에 있었다는 점입니다. show timezone을 아무리 확인해도, 그 값을 정한 주체가 pooler라면 서버 설정을 보는 것으로는 안 풀립니다.
Oracle에서 넘어온 직후라는 배경도 한몫했다고 봅니다. Oracle에는 연결 풀을 이런 식으로 앞에 두는 관례가 덜하고, 세션 파라미터가 계층별로 덮어써지는 구조에 익숙하지 않으면 의심 대상에 pooler가 안 들어옵니다. 저도 이 글을 읽기 전까지 PgBouncer가 startup parameter를 그렇게 오래 붙들고 있는 줄은 몰랐어요.
참고
- The curious case of timezone inconsistencies between PgBouncer and Patroni Cluster (Percona Community, 2026-08-20)
- Patroni 동적 설정 문서
- PgBouncer 설정 문서