- 출처 / 참고: Microsoft Learn (Troubleshoot TempDB contention in SQL Server), Microsoft SQL Server Internals (TempDB Allocation Architecture)
명령어:
sqlcmd,top,pidstat,grep,systemctl
키워드: TempDB Contention, PAGELATCH_UP, PAGELATCH_EX, PFS, GAM, SGAM, High CPU, Trace Flag 1118, Multiple Data Files
사용처: 임시 테이블 및 테이블 변수 과다 사용 시 SQL Server CPU 점유율 100% 폭증 및 쿼리 병목(Latch Wait) 해결
실행예제
SQL Server 인스턴스에서 CPU 사용량이 비정상적으로 치솟고 쿼리 응답이 지연될 때, OS 레벨의 프로세스 CPU 사용률을 확인하고 데이터베이스 내부에서 TempDB 특화 대기 유형인 PAGELATCH_UP, PAGELATCH_EX 발생 여부 및 경합이 일어나는 시스템 페이지를 점검합니다.
# 1. OS 관점: sqlservr 프로세스의 CPU 점유율 및 스레드별 부하 확인
$ top -b -n 1 -p $(pgrep sqlservr | head -n 1)
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 1420 mssql 20 0 18.520g 12.150g 32450 R 785.4 38.2 42:15.80 sqlservr
# -> 다중 코어 환경에서 CPU 점유율이 수백 %에 도달한 상태 확인
# 2. 현재 SQL Server 전체에서 가장 높은 대기 시간을 기록 중인 Wait Type 확인
$ sqlcmd -S localhost -U sa -P 'MyStrongPassword123!' -Q "
SELECT TOP 5
wait_type,
waiting_tasks_count,
wait_time_ms,
ROUND(wait_time_ms * 1.0 / NULLIF(waiting_tasks_count, 0), 2) AS avg_wait_ms
FROM sys.dm_os_wait_stats
WHERE wait_type LIKE 'PAGELATCH_%'
ORDER BY wait_time_ms DESC;
"
# [출력 예시]
# wait_type waiting_tasks_count wait_time_ms avg_wait_ms
# ---------------- -------------------- -------------------- ------------
# PAGELATCH_UP 1542100 45214050 29.32
# PAGELATCH_EX 421050 12540120 29.78
# -> PAGELATCH_UP/EX 대기 시간이 압도적이면 TempDB 할당 페이지 경합 가능성 매우 높음
# 3. 실시간으로 TempDB(database_id = 2) 할당 페이지에서 대기 중인 세션 추적
$ sqlcmd -S localhost -U sa -P 'MyStrongPassword123!' -Q "
SELECT
session_id,
wait_type,
wait_duration_ms,
resource_description
FROM sys.dm_os_waiting_tasks
WHERE wait_type LIKE 'PAGELATCH_%'
AND resource_description LIKE '2:%';
"
# [출력 예시]
# session_id wait_type wait_duration_ms resource_description
# ---------- ------------- ---------------- --------------------
# 78 PAGELATCH_UP 45 2:1:1
# 84 PAGELATCH_UP 52 2:1:3
# -> resource_description이 '2:1:1'(PFS), '2:1:2'(GAM), '2:1:3'(SGAM)을 가리키면 전형적인 메타데이터 할당 경합임
# 4. 현재 TempDB 데이터파일 개수 및 크기/자동확장 균등 여부 확인
$ sqlcmd -S localhost -U sa -P 'MyStrongPassword123!' -Q "
SELECT
file_id,
name,
physical_name,
size * 8 / 1024 AS size_mb,
growth * 8 / 1024 AS growth_mb,
is_percent_growth
FROM sys.master_files
WHERE database_id = 2 AND type = 0;
"
스크립트
SQL Server의 CPU 코어 수 대비 TempDB 데이터파일 개수를 비교 검증하고, 현재 활성화된 세션 중 TempDB 시스템 페이지(PFS, GAM, SGAM) 경합에 묶여 있는 세션 및 원인 쿼리를 실시간 감지하는 진단 쉘 스크립트입니다.
#!/bin/bash
# diagnose_tempdb_contention.sh - Diagnose SQL Server TempDB Allocation Contention & High CPU
SQL_HOST=${1:-"localhost"}
SQL_USER=${2:-"sa"}
SQL_PASS=${3:-"MyStrongPassword123!"}
echo "=========================================="
echo " [SQL Server TempDB 경합 및 CPU 고부하 진단]"
echo " 대상 호스트: ${SQL_HOST}"
echo "=========================================="
if ! command -v sqlcmd &>/dev/null; then
echo " [오류] sqlcmd 명령어를 찾을 수 없습니다. mssql-tools 설치 여부를 확인하세요."
exit 1
fi
# 1. CPU 논리 코어 수와 TempDB 데이터파일 수 대조
echo -e "\n[1] CPU 코어 수 대비 TempDB 데이터파일 구성 점검:"
CORE_COUNT=$(nproc 2>/dev/null || echo 4)
TEMPDB_FILE_COUNT=$(sqlcmd -S "$SQL_HOST" -U "$SQL_USER" -P "$SQL_PASS" -h -1 -W -Q "
SET NOCOUNT ON;
SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2 AND type = 0;
" 2>/dev/null)
TEMPDB_FILE_COUNT=$(echo "$TEMPDB_FILE_COUNT" | tr -d '[:space:]')
echo " - 시스템 논리 CPU 코어 수: ${CORE_COUNT}개"
echo " - TempDB 데이터파일 개수 : ${TEMPDB_FILE_COUNT}개"
if [ "${TEMPDB_FILE_COUNT:-0}" -eq 1 ]; then
echo " [치명적 경고] TempDB 데이터파일이 1개뿐입니다!"
echo " -> 대량 동시 요청 시 PFS/SGAM 단일 페이지 병목으로 심각한 CPU 스핀 및 래치 경합 유발."
elif [ "${TEMPDB_FILE_COUNT:-0}" -lt "$CORE_COUNT" ] && [ "${CORE_COUNT}" -le 8 ]; then
echo " [주의] 데이터파일 개수가 논리 코어 수(${CORE_COUNT}개)보다 적습니다. 코어 수에 맞추어 증설을 권장합니다."
else
echo " [정상] 데이터파일 개수가 적정 수준으로 분산 구성되어 있습니다."
fi
# 2. 파일별 크기 및 자동확장(Growth) 동일성 점검
echo -e "\n[2] TempDB 파일 크기 및 증가폭 균등 여부 점검:"
FILE_CHECK=$(sqlcmd -S "$SQL_HOST" -U "$SQL_USER" -P "$SQL_PASS" -W -Q "
SET NOCOUNT ON;
SELECT
name,
size * 8 / 1024 AS size_mb,
growth * 8 / 1024 AS growth_mb,
CASE WHEN is_percent_growth = 1 THEN 'PERCENT' ELSE 'MB' END AS growth_type
FROM sys.master_files
WHERE database_id = 2 AND type = 0;
" 2>/dev/null)
echo "$FILE_CHECK" | sed 's/^/ /'
# 3. 실시간 TempDB 할당 페이지 경합(PFS/GAM/SGAM) 발생 여부
echo -e "\n[3] 실시간 TempDB 할당 페이지(PFS/GAM/SGAM) 대기 세션 추적:"
WAITING_SESSIONS=$(sqlcmd -S "$SQL_HOST" -U "$SQL_USER" -P "$SQL_PASS" -W -Q "
SET NOCOUNT ON;
SELECT
wt.session_id,
wt.wait_type,
wt.wait_duration_ms,
wt.resource_description,
SUBSTRING(st.text, (r.statement_start_offset/2)+1,
((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE r.statement_end_offset END - r.statement_start_offset)/2) + 1) AS current_query
FROM sys.dm_os_waiting_tasks wt
JOIN sys.dm_exec_requests r ON wt.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE wt.wait_type LIKE 'PAGELATCH_%'
AND wt.resource_description LIKE '2:%';
" 2>/dev/null)
if echo "$WAITING_SESSIONS" | grep -q "PAGELATCH"; then
echo " [위험] 현재 TempDB 페이지 래치 경합에 걸린 세션이 포착되었습니다!"
echo "$WAITING_SESSIONS" | head -n 15 | sed 's/^/ /'
else
echo " [정상] 현재 시점에 TempDB 시스템 페이지 경합은 감지되지 않았습니다."
fi
echo -e "\n=========================================="
echo " [진단 완료]"
echo "=========================================="
해설
SQL Server에서 쿼리 자체의 연산량이나 인덱스 누락이 없음에도 불구하고 CPU 점유율이 90~100%까지 치솟는 장애가 발생할 때, 가장 먼저 의심해야 할 병목 지점 중 하나가 바로 TempDB 할당 경합(TempDB Allocation Contention)입니다.
1. TempDB 할당 경합의 동작 원리
임시 테이블(#table), 테이블 변수(@table), 정렬(ORDER BY), 해시 조인(HASH JOIN) 연산 시 SQL Server는 메모리 또는 디스크의 TempDB 공간을 동적으로 할당받아 사용합니다. 이때 데이터베이스 내부 공간을 관리하는 특수 시스템 페이지를 반복 참조합니다.
- PFS (Page Free Space): 각 페이지의 여유 공간 비율을 기록 (약 8,000페이지(64MB)마다 1페이지씩 존재, 첫 페이지:
2:1:1). - GAM (Global Allocation Map): 어떤 익스텐트(Extent)가 할당되었는지 추적 (약 64,000익스텐트(4GB)마다 1페이지, 첫 페이지:
2:1:2). - SGAM (Shared Global Allocation Map): 혼합 익스텐트(Mixed Extent)의 여유 상태 추적 (첫 페이지:
2:1:3).
수백 개의 동시 접속 세션이 임시 객체를 빈번하게 생성/삭제하면, 모든 세션이 단 하나의 시스템 페이지(2:1:1 또는 2:1:3)를 변경하기 위해 메모리 상에서 배타적/갱신 래치(PAGELATCH_UP, PAGELATCH_EX)를 획득하려고 경쟁합니다. 이 과정에서 CPU 코어들은 락을 얻기 위해 초당 수만 회 이상 스핀(Spin)을 돌게 되며, 이는 시스템 전반의 CPU 100% 포화 현상으로 직결됩니다.
2. 단계별 튜닝 및 해결 방법
1) TempDB 데이터파일 다중 분할 구성 (가장 중요)
단일 데이터파일(tempdb.mdf) 구조에서는 모든 스레드가 동일한 단일 PFS/SGAM 페이지만을 바라봅니다. 데이터파일을 여러 개로 쪼개면 파일마다 독립적인 시스템 페이지가 생성되어 스레드들의 경합이 물리적으로 분산됩니다.
- 권장 분할 공식 (Microsoft 가이드라인):
- 논리 코어 수 $\le 8$: 논리 코어 수와 동일한 개수의 데이터파일 생성 (예: 4코어면 4개, 8코어면 8개).
- 논리 코어 수 $> 8$: 8개의 데이터파일로 시작한 후, 여전히 경합이 관측되면 4개 단위로 추가 증설 (보통 8~16개 유지).
-- 예시: 4개의 2GB 크기 데이터파일로 균등 분할
ALTER DATABASE tempdb ADD FILE (NAME = N'tempdev02', FILENAME = N'/var/opt/mssql/data/tempdb_mssql_2.ndf', SIZE = 2048MB, FILEGROWTH = 256MB);
ALTER DATABASE tempdb ADD FILE (NAME = N'tempdev03', FILENAME = N'/var/opt/mssql/data/tempdb_mssql_3.ndf', SIZE = 2048MB, FILEGROWTH = 256MB);
ALTER DATABASE tempdb ADD FILE (NAME = N'tempdev04', FILENAME = N'/var/opt/mssql/data/tempdb_mssql_4.ndf', SIZE = 2048MB, FILEGROWTH = 256MB);
2) 모든 TempDB 데이터파일의 초기 크기 및 자동 증가폭을 완전히 동일하게 설정
SQL Server는 파일 크기가 동일할 때 균등 라운드 로빈(Proportional Fill) 방식으로 데이터를 골고루 분산 할당합니다. 파일 중 하나라도 크기나 증가폭이 다르면 특정 파일로 할당이 쏠리며 다시 경합이 발생합니다.
3) 단일 익스텐트 강제 할당 (Trace Flag 1118 및 1117)
- SQL Server 2016 이상: 기본적으로 혼합 익스텐트(Mixed Extent)를 사용하지 않고 전용 단일 익스텐트(Uniform Extent)를 사용하도록 엔진이 개선되어 있어 별도의 트레이스 플래그 없이도 SGAM 경합이 대폭 감소했습니다.
- SQL Server 2014 이하 (레거시 환경):
- 시작 파라미터에
-T1118(Uniform Extent 강제) 및-T1117(모든 파일 동시 자동 증가)을 반드시 등록해야 합니다.
- 시작 파라미터에
4) 쿼리 레벨 최적화: 임시 객체 남발 방지
- 불필요한 임시 테이블 생성(
CREATE TABLE #TMP) 남발을 줄이고, 가능한 경우 공통 테이블 표현식(CTE)이나 파생 테이블(Derived Table)로 대체합니다. - 빈번한 DDL 호출을 피하기 위해 임시 테이블 재사용 또는 테이블 변수 크기 최적화를 검토합니다.
주의사항
- 데이터파일 개수를 무조건 CPU 코어 수 이상으로 늘리지 말 것:
- 32코어 또는 64코어 머신이라고 해서 TempDB 파일을 32개, 64개로 무작정 늘리면 파일 간 체크포인트 부하와 메타데이터 관리 오버헤드가 급증하여 오히려 I/O 성능이 저하될 수 있습니다. 8개부터 시작하여 대기 현상을 모니터링하면서 점진적으로 늘려야 합니다.
- 로그 파일(
templog.ldf)은 1개만 유지:- 데이터파일(
.ndf)은 여러 개로 분할하여 병렬 할당 이점을 얻지만, 트랜잭션 로그 파일은 구조상 순차 쓰기(Sequential Write)로 동작하므로 파일을 여러 개 만들어도 성능 분산 효과가 전혀 없습니다. 로그 파일은 반드시 단 1개만 유지해야 합니다.
- 데이터파일(
- 고속 스토리지(NVMe SSD) 분리 배치:
- TempDB는 인메모리 성격의 임시 I/O가 매우 빈번한 특수 데이터베이스이므로, 일반 사용자 데이터베이스 파일과 물리 디스크 드라이브를 분리하여 독립된 NVMe SSD 파티션에 격리 배치해야 스토리지 큐 병목을 방지할 수 있습니다.