데이터베이스 연구실.log

PostgreSQL 지표 수집

PostgreSQL 패널별 통계와 함수, psycopg 부하 발생기 및 긴 SQL의 누적 통계 반영 지연

pg_terminate_backend악성 SQLpg_stat_databasepg_stat_activityblks_hitblks_readquery_startwait_event_typepg_ls_logdirpg_read_filepg_stat_user_tablesload.pyrun_monitor.bat

#핵심 요약

실습 미니 PG 게이지는 Oracle판과 같은 구조로, 통계 뷰를 3초마다 조회해 화면에 그림

초당 값(Transactions/s, Rows Read/s, Disk Blocks Read/s)은 pg_stat_database 누적값의 3초 전 값과의 차이를 경과 초로 나눠 구함

PostgreSQL은 pg_stat_database 숫자를 SQL이 끝날 때 반영함
몇 분째 도는 긴 SQL은 끝날 때까지 그래프에 보이지 않으므로, 긴 SQL은 경보와 세션 표로 잡음

실습 부하 결과: 평상시 초당 트랜잭션 약 590, 풀스캔 부하에서 행 읽기 약 2,900만/s, 디스크 블록 읽기 약 29만/s, 캐시 적중률 45%

#개념과 동작 원리

패널 읽는 뷰, 함수 계산과 읽는 법
KPI 카드 pg_stat_database 누적값 3초 전 값과의 차이 ÷ 경과 초
Connections pg_stat_activity ÷ max_connections 최대 접속의 몇 %인가
Buffer Cache Hit blks_hit ÷ (blks_hit + blks_read) shared_buffers에서 바로 찾은 비율, 90% 미만이면 빨강
Running SQL Elapsed Time pg_stat_activity.query_start 3초마다 실행 중인 SQL을 점으로, 10초 넘으면 분홍
Server Log pg_ls_logdir(), pg_read_file() 최신 로그 파일 끝 64KB에서 ERROR, FATAL, deadlock 줄
Wait Event Type pg_stat_activity.wait_event_type 비어 있으면 CPU, IO는 디스크, Lock은 락 대기
Database Size / Seq Scan Tables pg_database_size() / pg_stat_user_tables 풀스캔으로 많이 읽힌 테이블 순위

패널 SQL 본문과 수집 함수 이름은 실습 텍스트에 없음
전용 계정의 권한 구성은 모니터링 연결과 계정에 있음

#필수 명령어 또는 SQL

#실행 파일

PC (도구 폴더의 run_monitor.bat)현재 세션 설정만 변경함
set MON_HOST=<HOST>
set MON_PORT=5432
set MON_DB=postgres
set MON_USER=<MONITOR_USER>
set MON_PASSWORD=<PASSWORD>
set MON_WATCH_DB=<LOAD_USER>
python -m streamlit run app.py --server.port 8501

결과 해석: MON_* 환경 변수가 사이드바 기본값이 되고, 비밀번호까지 들어 있어 띄우면 바로 연결됨
실습 확인 결과는 사이드바 입력 없이 PostgreSQL 15.19 연결임
Anaconda를 찾아 켜는 방법은 Oracle과 PostgreSQL 연결에서 다룸

#부하 발생기(load.py 일부)

PC (도구 폴더의 load.py)객체 또는 설정을 변경함
ARGS = dict(host=os.getenv("PG_HOST", "<HOST>"), port=int(os.getenv("PG_PORT", "5432")),
            dbname="<LOAD_USER>", user="<LOAD_USER>", password=os.getenv("PG_PASSWORD", "<PASSWORD>"))
stop = threading.Event()

def oltp():
    """인덱스로 1건 조회 + 1건 입력·커밋 반복"""
    with psycopg.connect(application_name="load_oltp", autocommit=True, **ARGS) as c:
        while not stop.is_set():
            c.execute("select amount from big_sales where id = %s", (random.randint(1, 2_000_000),)).fetchone()
            c.execute("insert into commit_log(id) values (%s)", (random.randint(1, 1000),))
            time.sleep(0.01)

def full_scan():
    """인덱스 없는 조건으로 200만 건 풀스캔"""
    with psycopg.connect(application_name="load_scan", autocommit=True, **ARGS) as c:
        while not stop.is_set():
            c.execute("select count(*), avg(amount) from big_sales where filler like %s",
                      (f"%{random.randint(0, 9)}%",)).fetchone()

def main():
    heavy = len(sys.argv) > 1 and sys.argv[1] == "heavy"
    workers = [threading.Thread(target=oltp, daemon=True) for _ in range(4)]
    if heavy:
        workers += [threading.Thread(target=full_scan, daemon=True) for _ in range(2)]
    for w in workers:
        w.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        stop.set()

결과 해석:

읽는 법: 스레드 하나가 연결 하나를 열고, threading.Event로 모든 스레드를 한 번에 멈춤
autocommit=True라 INSERT마다 커밋됨
%s는 psycopg의 바인드 자리표시라 값을 SQL 문자열에 직접 붙이지 않음

Anaconda Prompt (도구 폴더)객체 또는 설정을 변경함
python load.py
python load.py heavy

결과 해석: 평상시는 세션 4개, heavy는 풀스캔 세션 2개가 더해져 6개임
창에는 시작 한 줄만 나오고 결과는 대시보드에서 봄
Ctrl+C로 멈춤

부하 실습 대시보드 결과
평상시 초당 트랜잭션 약 590, 행 읽기 수천/s
풀스캔 행 읽기 약 2,900만/s, 디스크 블록 읽기 약 29만/s, 캐시 적중률 45%(빨강), 대기는 CPU와 IO

#다른 세션이 보이는지(check_monitor.py 핵심)

PC (Anaconda Prompt)조회만 함
rows = mon.execute("""select pid, usename, datname, application_name, state, left(query, 40)
                        from pg_stat_activity
                       where backend_type = 'client backend'
                       order by pid""").fetchall()
name, size = mon.execute("select name, size from pg_ls_logdir() order by modification desc limit 1").fetchone()

결과 해석: 부하 계정 세션의 QUERY가 실제 SQL로 보이고 최근 로그 파일 이름과 크기가 나오면 됨
로그 파일 이름은 요일별이라 날마다 다름

#악성 SQL 5종 탐지와 세션 종료

악성 판정 기본값은 실행 10초 이상, 트랜잭션 방치 30초 이상, 다른 세션 블로킹이며, 대시보드가 경보 → SQL 전문, 실행계획 → 진단 → pg_terminate_backend 명령까지 보여 주는지 다섯 시나리오로 확인함

psql (loadtest, 악성 SQL 창)객체 또는 설정을 변경함
\timing on
-- 1 카테시안 곱
select count(*) from big_sales a, big_sales b where a.amount < 50000;
-- 2 상관 서브쿼리 반복
select count(*) from big_sales s
 where s.amount > (select avg(x.amount) from big_sales x where x.cust_id = s.cust_id);
-- 3 인덱스 컬럼 가공(DO 블록 안 1000번 반복)
do $$ declare n int; begin
  for i in 1..1000 loop
    select count(*) into n from big_sales where to_char(sale_date, 'YYYYMMDD') = '20240101';
  end loop; end $$;
-- 4 락 걸고 방치: 창 A에서 begin; update hot_row set cnt = cnt + 1 where id = 1; (커밋 안 함)
--   창 B·C에서 같은 update 실행 → 대기
-- 5 PL/pgSQL 무한 루프
do $$ declare x bigint := 0; begin loop x := x + 1; end loop; end $$;
psql (postgres, 종료 창: cd /tmp; sudo -u postgres psql)인스턴스 또는 서비스 중단이 필요함
select pid, usename, state, wait_event_type, wait_event from pg_stat_activity where pid = <PID>;
select pg_terminate_backend(<PID>);
select pg_sleep(1);
select pid from pg_stat_activity where pid = <PID>;

결과 해석:

시나리오 실행계획, 진단 실습 기록
1 카테시안 곱 조인 조건 없는 Nested Loop(빨강), Seq Scan 종료까지 약 1분 38초
2 상관 서브쿼리 Aggregate(SubPlan 1)와 Seq Scan 두 번, 대기 IO: DataFileRead 약 1분 36초
3 인덱스 컬럼 가공 DO 블록 안 SELECT를 꺼내 만든 계획, to_char 조건 약 1분 17초
4 락 방치 보유 세션만 경보, 대기 2개는 피해 세션 표시, 실행계획 대신 기다리는 세션 목록 보유 세션 idle in transaction, 끊은 뒤 hot_row.cnt가 2
5 무한 루프 실행계획 없음, 대기 이벤트도 없이 CPU만 씀 약 1분 21초

종료 전에 pg_stat_activity로 PID가 정말 loadtest의 악성 세션인지 확인하고, 끊은 뒤 1초 쉬었다가 (0개 행)으로 사라졌는지 봄

끊긴 쪽 psql에는 FATAL: terminating connection due to administrator command가 찍히고 psql이 다시 접속함
idle in transaction 세션은 끊겨도 바로 모르고 다음 문장을 보낼 때 알게 됨

락 시나리오의 cnt = 2는 끊긴 창 A의 UPDATE가 롤백되고 창 B, C의 UPDATE만 반영됐다는 뜻임
실무에서는 끊기 전에 그 변경을 버려도 되는지 업무 담당자에게 확인함

같은 행을 기다리는 세션은 A ← B ← C처럼 줄을 서고 pg_blocking_pids()는 바로 앞 세션만 알려 주므로, 수집기는 줄 맨 앞까지 따라가 원인 세션에 대기 수를 몰아 줌

monitor는 읽기 전용이라 UPDATE, DELETE는 EXPLAIN할 수 없고, DO 블록은 통째로 EXPLAIN할 수 없어 블록 안 첫 SELECT에서 into를 빼고 계획을 만듦

pg_cancel_backend(PID)는 실행 중인 SQL만 취소하고 세션은 남기며, pg_terminate_backend(PID)는 세션을 끊고 열린 트랜잭션을 롤백함

psql 창을 여러 개 띄우기 어려우면 PC에서 python bad_sql.py 1~`5`로 같은 상황을 만듦(4번은 보유 1 + 대기 2)

#Oracle판과 PostgreSQL판 비교

항목 Oracle 19c PostgreSQL 15
접속 라이브러리 python-oracledb psycopg 3
모니터링 계정 권한 SELECT_CATALOG_ROLE, SELECT ANY DICTIONARY pg_monitor, pg_read_all_data
세션, 대기 V$SESSION(event, wait_class) pg_stat_activity(wait_event_type, wait_event)
블로킹 V$SESSION.BLOCKING_SESSION pg_blocking_pids(pid), 줄 선 대기자는 바로 앞 세션을 가리킴
초당 지표 V$SYSSTAT, V$OSSTAT(CPU, 메모리) pg_stat_database(OS CPU, 메모리는 SQL로 못 봄)
SQL 통계 V$SQLSTATS pg_stat_statements는 재시작 필요해 실행 중 SQL을 3초마다 관측
에러 로그 V$DIAG_ALERT_EXT pg_ls_logdir()와 pg_read_file()
실행계획 V$SQL_PLAN(실제 커서 계획) EXPLAIN (FORMAT JSON)으로 다시 만듦, SELECT만
세션 종료 ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE SELECT pg_terminate_backend(pid)

도구 파일은 collector.py(패널 SQL, 수집기, 악성 판정, 종료 명령), app.py(화면), run_monitor.bat, load.py, bad_sql.py, sql/01_create_monitor_user.sql, sql/02_create_load_schema.sql 등으로 나뉨

검증 중 big_sales에 id 기본 키를 추가해 평상시 부하(id로 1건 조회)가 풀 스캔이 되지 않게 고쳤음

#사용 조건과 주의사항

실습에서는 캐시 적중률이 평상시에도 90% 근처인 이유를, 큰 테이블(341MB)이 shared_buffers(128MB)보다 커서 임의의 id를 찾을 때도 디스크에서 읽기 때문이라고 설명하고, 운영 DB라면 shared_buffers 증설을 검토할 신호라고 적었음
캐시 적중률만으로 운영 DB의 메모리 증설을 판단할 수 없음

메모장으로 bat 파일을 만들 때 줄바꿈이 LF면 cmd가 한글 줄을 잘못 나눠 명령으로 실행함
CRLF로 저장하고 맨 위에 chcp 65001을 둠

악성 SQL 탐지의 Oracle판은 문제 SQL 탐지와 세션 진단, PostgreSQL 세션 종료 일반은 세션 종료와 영향에 있음

SEO_HKabsumveritas@gmail.comBuilt with Claude Code & Codex

마지막 갱신 개인 학습 기록이며 운영 환경 적용 전 공식 문서 확인 필요

두 글자 이상 입력, 위아래 화살표로 이동하고 Enter로 열기