Lesson Learned

운영 DB에 쿼리 타임아웃을 걸어야 하는 이유

J's Note 2026. 8. 31. 16:19

파라미터 값 하나가 비어 있었고, DB는 엿새에 걸쳐 조용히 죽어갔습니다.
쿼리 두 개가 끝나지 않은 채 디스크를 갉아먹는 동안 어떤 로그에도, 어떤 알림에도 흔적이 없었어요.
RDS MariaDB 스토리지 고갈 장애를 추적한 과정과, 그 끝에서 내린 결론을 정리했습니다.

결론부터 말하겠습니다

운영 데이터베이스에는 statement timeout이 걸려 있어야 한다.

MariaDB라면 max_statement_time,

MySQL이라면 max_execution_time,

PostgreSQL이라면 statement_timeout입니다.

 

이름은 달라도 역할은 같아요. 지정한 시간을 넘긴 쿼리를 강제로 끊는 값이죠.

 

저는 얼마 전까지 이 값을 필수라고 생각하지 않았습니다. 정상적인 배치가 끊길 수 있으니 함부로 걸면 안 되는 값, 필요해지면 그때 거는 값 정도로 여겼어요. 그런데 이 값 하나가 비어 있어서 DB가 엿새에 걸쳐 죽어가는 걸 처음부터 끝까지 추적하고 나서 생각이 완전히 바뀌었습니다.

 

왜 그렇게 됐는지, 사건을 따라가면서 말씀드리죠.

장애 당일 - 알림은 사후 통보였다

밤 9시가 조금 넘어 critical 알림이 왔습니다. 저희가 운영하는 백엔드, RDS MariaDB 10.11(db.m7g.xlarge, 4 vCPU)의 여유 스토리지 부족 알림이었어요.

접속해보니 이미 응답이 없었습니다. 대시보드에서 DatabaseConnections가 0으로 떨어져 있고 CPUUtilization도 1.7%까지 내려가 있더라고요. RDS가 인스턴스를 STORAGE_FULL 상태로 전환하면서 신규 접속을 차단한 상태였습니다.[1]

 

스토리지를 증설해 서비스를 살렸고, 그동안 홈페이지 응답 시간은 0.02초에서 최대 13.9초까지 올라가 있었고요.

복구하고 나서 알림 룰의 State history부터 열어봤습니다.

시각 (KST) 전이 FreeStorageSpace
21:03:20 Normal → Pending 2.26 GiB
21:08:20 Pending → Alerting 0.00 GiB
23:51:20 Alerting → Normal 131.56 GiB


DB가 멈춘 건 21시 5분이고 알림이 Alerting으로 넘어간 건 21시 8분입니다.

 

알림이 장애를 알려준 게 아니라 장애가 끝난 뒤에 울린 거예요. 임계값은 10GiB였는데, 나중에 계산해보니 이 환경에서 그건 세 시간짜리 여유였고, 마지막 45GiB는 10분 만에 사라졌습니다. 임계값을 다섯 배로 올렸어도 결과는 같았겠죠.

단순 용량 부족이 아니라는 뜻이죠. 원인을 찾으러 들어갔습니다.

추적 1 - 트래픽 때문이 아니었다

가장 먼저 의심할 건 뭘까요. 사용량 증가죠. 대시보드의 스토리지 패널을 열고 시간 범위를 일주일로 늘려봤습니다.

처음엔 그냥 우하향 그래프구나 했는데, 자세히 보니 좀 이상했어요. 자로 그은 것처럼 직선입니다. 보통 이런 그래프는 낮에 가파르고 새벽에 완만해지면서 계단처럼 울퉁불퉁하거든요. 패널을 테이블 뷰로 바꿔서 시간별 값을 확인했습니다.

08-25 03:00   210.27 GiB
08-25 04:00   207.83 GiB     -2.45
...
08-26 14:00   100.25 GiB     -3.16
08-26 15:00    97.09 GiB     -3.16
08-26 16:00    93.90 GiB     -3.19

새벽 3시와 낮 2시의 감소량이 같습니다. 시간당 3.2GiB에서 소수점 둘째 자리까지 흔들리지 않아요. 홈페이지는 낮과 새벽의 트래픽 차이가 큰데, 감소 속도는 시간대와 무관했죠. 사용량이 아니라 시간에만 비례해서 뭔가가 쌓이고 있다는 뜻이죠.

 

 

그래프에서 두 가지가 더 보였습니다. 08-21 오전 9시까지는 385.88GiB에서 선이 완전히 평평합니다. 그러다 10시부터 기울기가 생기고, 08-24 오후 4시에 기울기가 한 번 더 꺾여 두 배가 돼요. 계단이 두 번 생긴 거죠. 기억해두세요. 이 두 시각이 나중에 다시 나옵니다.

추적 2 - 백업도, 덤프도 아니었다

고갈 직전 구간으로 범위를 좁히고 해상도를 1분까지 낮췄습니다.

시각 ReadThr WriteThr NetTx NetRx QueueDepth
20:57 0.01 0.94 0.17 0.01 0.12
20:58 63.47 50.69 0.35 0.01 13.29
20:59 192.02 191.21 0.24 0.01 42.58
21:00 182.65 194.84 0.08 0.02 42.70

마지막 몇 분간 읽기가 192MiB/s까지 치솟았습니다. mysqldump 같은 게 테이블을 통째로 읽어간 그림이 떠오르죠. 그런데 네트워크 지표를 같이 띄워보니 아니었습니다. 읽기 선은 천장을 치는데 네트워크 송신 선은 바닥에 붙어 있었어요. 폭증 구간에 송신이 오히려 0.08MiB/s로 줄어듭니다.

 

192MiB/s로 읽었는데 밖으로 나간 게 없다. 그러면 외부 덤프는 아니죠. 그리고 ReadThroughputWriteThroughput이 192 대 191로 거의 1대 1이에요. 인스턴스가 자기 데이터를 읽어서 자기 디스크에 다시 쓰고 있었습니다. DiskQueueDepth 42는 I/O가 완전히 포화됐다는 뜻이고요.

 

테이블 재구축, 대량 복사, 디스크로 넘친 임시 테이블. 이 셋 중 하나로 좁혀졌습니다. 이제 그 시각에 무슨 쿼리가 돌았는지 보면 되죠.

추적 3 - 로그가 전부 비어 있었다

CloudWatch Logs의 RDS 로그가 export되도록 설정해두었고, 이부분을 확인해봤습니다.

/aws/rds/instance/<db>/audit
/aws/rds/instance/<db>/error
/aws/rds/instance/<db>/slowquery

audit 로그까지 있으니 그 시각의 쿼리가 그대로 나오겠다 싶었죠. Logs Insights로 사고 구간을 돌렸습니다.

fields @timestamp, @message | sort @timestamp asc | limit 400
20260827 11:50:02,ip-...,<app_user>,10.x.x.x,113421,4160707413,QUERY,<db>,
  'UPDATE BOARD_BBS SET HIT = HIT + 1 WHERE BOARD_SEQ = ...',0
20260827 11:50:59,ip-...,rdsadmin,localhost,35390,4161247441,QUERY,mysql,
  'SET STATEMENT max_statement_time=60 FOR INSERT INTO mysql.rds_heartbeat2 ...',0

게시글 조회수 UPDATE, 다운로드 카운트 UPDATE, RDS 관리 계정의 heartbeat. 분당 10~30건 수준이고 192MiB/s를 만들 만한 건 하나도 없었고요.

 

slow query 로그를 봤습니다. 비어 있었어요. 5분씩 걸린 쿼리가 있었다면 반드시 남았어야 하는데요. error 로그도 봤습니다. 역시 비어 있었습니다. DB가 죽었는데 에러 한 줄이 없어요.

 

지표는 분명히 뭔가가 디스크를 갈아먹고 있다고 말하는데, 로그는 아무 일도 없었다고 말하는 상황. 솔직히 여기서 좀 막혔습니다.

끝나지 않은 쿼리는 로그에 남지 않는다

한참 보다가 질문을 바꿨습니다. 무엇이 있었나가 아니라 왜 없는가로요.

audit 로그와 slow query 로그의 공통점이 하나 있죠. 둘 다 쿼리가 끝난 시점에 한 줄을 씁니다. 실행 시간, 검사한 행 수, 반환 코드 같은 걸 남기려면 쿼리가 끝나야 알 수 있으니까요. 위에서 본 audit 라인 맨 끝의 0도 반환 코드입니다. 그 값을 적으려면 쿼리가 종료돼야 하죠.

 

그러면 답이 나오죠. 끝나지 않은 쿼리는 어느 쪽에도 남지 않습니다.

저는 완료된 쿼리 목록에서 범인을 찾고 있었는데, 범인은 애초에 그 목록에 들어갈 수 없는 종류였던 거예요.

 

이 지점이 이 글의 주장과 바로 닿아 있습니다. 타임아웃이 없는 환경에서 폭주하는 쿼리는 실패하지 않습니다. 그냥 계속 실행됩니다. 그리고 실패하지 않는 한 에러 로그도, slow query 로그도, 애플리케이션 예외도 없어요. 개발자 입장에서는 저장 버튼을 눌렀는데 응답이 안 와서 새로고침을 한 번 했을 뿐이고, 그 뒤로는 아무 이상 신호가 없는 거죠.

 

실패는 시끄럽고, 실행 중은 조용합니다. 타임아웃은 성능 튜닝 수단이기 전에, 조용한 폭주를 시끄러운 실패로 바꿔주는 장치더라고요.

세션 두 개가 엿새를 살아 있었다

쿼리가 끝나지 않고 계속 돌고 있었다면 세션 점유로 나타나야겠죠. CPU 패널을 일주일로 늘리고 WriteThroughput을 겹쳐 봤어요. 두 선이 똑같은 모양으로 두 번 계단을 밟고 있었습니다.

구간 CPU WriteThroughput
08-20 (정상) 1.1% 0.013 MiB/s
08-21 10:00 ~ 26.0% 0.52 MiB/s
08-24 16:00 ~ 50.9% 0.93 MiB/s
재시작 후 2.6% 0.028 MiB/s

베이스라인 1.1%에서 +24.9%p씩 정확히 두 번 올라갑니다. 100 / 24.9 ≈ 4.0. 4 vCPU 인스턴스에서 코어 하나를 온전히 점유하는 무언가가 두 번 생긴 거예요. 쓰기 0.93MiB/s를 환산하면 시간당 3.27GiB로, 여유 공간 감소 속도와 일치하죠.

 

그러다 지표 목록에서 DBLoad를 발견했습니다. Performance Insights가 이미 켜져 있었던 겁니다. PI를 켜면 DBLoad, DBLoadCPU, DBLoadNonCPU가 CloudWatch에도 게시되거든요.[2] 저는 그때까지 PI를 켜야 한다고 생각하고 있었는데, 지표 목록만 봤어도 됐을 일이었어요.

 

구간 DBLoad (AAS) DBLoadCPU DBLoadNonCPU
08-20 0.00 0.00 0.00
08-21 10:00 ~ 1.00 0.02 0.98
08-24 16:00 ~ 2.00 0.03 1.97
재시작 후 0.06 0.00 0.04

AAS는 활성 세션의 평균 개수인데, PI가 1초 간격으로 샘플링해서 만들거든요.[3]

이 값이 소수점까지 정확히 1.00, 정확히 2.00으로 고정됐다는 건 모든 샘플에서 활성 세션이 정확히 하나, 둘씩 잡혔다는 뜻이에요. 짧은 쿼리들이 우연히 그렇게 나올 확률은 없죠. 끝나지 않는 세션이 08-21에 하나, 08-24에 하나 더 생겨서 엿새를 살아 있었던 겁니다. 스토리지 그래프의 계단 두 개와 시각이 정확히 일치하고요.

 

DBLoadNonCPU가 1.97이라는 것도 중요합니다. 부하의 98%가 연산이 아니라 I/O 대기였어요. CPU를 태우는 무한 루프가 아니라 디스크에 뭔가를 계속 쓰면서 도는 작업이었다는 뜻이죠.

 

Performance Insights 콘솔에서 시간 범위를 사고 직전 두 시간으로 지정하고 Top SQL을 열었습니다.

 

항목
Top SQL 1위 Load by waits (AAS) 1.99
나머지 24개 쿼리 전부 0.01 미만
1위 쿼리 Calls/sec 비어 있음
지배적 대기 이벤트 wait/io/table/sql/handler

단일 쿼리가 부하 2.00을 혼자 채우고 있었습니다. 같은 쿼리가 두 번 걸린 채 세션 하나씩을 물고 있었던 거죠. Calls/sec가 비어 있는 게 되게 결정적이었어요. 두 시간이 넘도록 한 번도 완료된 적이 없다는 의미입니다.

범인은 순환 참조를 만난 이중 재귀

그 쿼리는 CMS의 사이트 메뉴 트리를 펼치는 재귀 CTE였습니다.

WITH RECURSIVE MENU_CTE AS (
  -- 시작점
  SELECT ..., FN_MENU_ALL_NAME(...), FN_MENUCODE_ALL_PATH(...)
    FROM MENU_INFO
   WHERE PARENT_CODE = ?
  UNION ALL
  -- 재귀
  SELECT ..., FN_MENU_ALL_NAME(...), FN_MENUCODE_ALL_PATH(...)
    FROM MENU_INFO B
    JOIN MENU_CTE C ON B.PARENT_CODE = C.MENU_CODE
)
SELECT * FROM MENU_CTE ORDER BY MENU_SORT

재귀가 이중으로 걸려 있어요. 바깥의 WITH RECURSIVEPARENT_CODE를 따라 트리를 내려가고, 그렇게 나온 행마다 경로 문자열을 만드는 함수 두 개를 또 호출하죠. 이름을 보면 이 함수들도 상위 메뉴를 거슬러 올라가는 구조고요.

 

재귀 CTE는 앵커 결과를 내부 임시 테이블에 넣고, 재귀 부분을 조인해 나온 새 행을 다시 임시 테이블에 추가하는 식으로 돌다가 새 행이 없으면 종료됩니다.[4] 그런데 메뉴 데이터에 순환 참조가 있으면, 그러니까 A의 부모가 B이고 B의 부모가 다시 A인 상황이면 같은 노드가 계속 새 행으로 생성됩니다. UNION ALL이라 중복 제거도 없고, LEVEL과 경로 값이 매번 달라지니 서로 다른 행 취급이거든요. 종료 조건에 영영 도달하지 못합니다.

 

그동안 중간 결과는 임시 테이블에 계속 쌓이고, 메모리 한계를 넘으면 디스크로 내려가고요. 디스크에 임시 데이터가 무한히 누적되는 구조예요.

 

관측된 것들이 전부 설명되죠. 세션이 안 끝나는 것(AAS 고정), 부하의 98%가 I/O 대기인 것(임시 테이블 읽기·쓰기), 쓰기가 시간에만 비례하는 것(반복 속도가 일정하니 누적도 일정), 네트워크 송신이 없는 것(결과를 반환하지 못하고 누적만 함), 어느 로그에도 없는 것(완료 시점이 안 옴), 그리고 재시작으로 전부 사라진 것(세션 종료와 함께 임시 테이블 회수)까지요.

 

참고로 Top SQL 2위에 거의 똑같이 생긴 재귀 CTE가 있었는데 부하는 0.01 미만이었습니다. 조직 계층을 도는 쿼리였어요. 같은 패턴이 정상 데이터에서 동작할 때의 모습인 거죠. 문제는 코드 패턴이 아니라 데이터에 있었던 거죠.

만약 타임아웃이 있었다면

여기서 처음의 주장으로 돌아가겠습니다. 이 환경에 max_statement_time이 30초로 걸려 있었다고 해보죠.

 

08-21 오전 10시, 누군가 관리자 화면에서 메뉴를 편집합니다. 저장을 누르자 메뉴 트리를 다시 그리는 재귀 CTE가 돌기 시작하는데, 데이터 어딘가에 순환 참조가 생긴 뒤라 쿼리가 끝나지 않아요. 30초 뒤 쿼리가 강제 종료되죠. 화면에는 에러가 뜨고, 애플리케이션 로그에 예외가 남고, 사용자는 "메뉴 저장하면 30초 뒤에 에러 나요"라고 제보합니다. 개발자가 그날 순환 참조를 찾습니다. 버그 리포트 한 건짜리 사건입니다.

 

실제로는 어땠느냐. 쿼리는 죽지 않았고, 그래서 에러도 예외도 제보도 없었습니다. 사용자는 응답이 없길래 새로고침을 했을 테고, 커넥션 풀은 응답 없는 커넥션을 버리고 새로 연결했을 테니 화면은 결국 떴을 거예요. 그 뒤로 엿새 동안 아무 신호가 없다가, 무려 500GB 가까운 디스크가 전부 소진되고 나서야 전면 장애로 나타났습니다. 서비스 중단 세 시간, 복구 대응, 스토리지 증설 비용, 그리고 이 글을 쓰게 만든 추적까지요.

 

같은 버그입니다. 타임아웃이 있느냐 없느냐가 버그 리포트와 전면 장애를 갈랐어요.

하나 되게 재미있는 걸 audit 로그에서 봤습니다. RDS가 자기 관리용으로 돌리는 heartbeat 쿼리요.

SET STATEMENT max_statement_time=60 FOR
  INSERT INTO mysql.rds_heartbeat2 ...

AWS는 자기네 내부 쿼리에 전부 타임아웃을 걸어놨더라고요.[5] 관리 주체가 자기 쿼리에는 걸어둔 안전장치를, 정작 그 위에서 도는 서비스 쿼리는 무제한으로 달리고 있었던 셈이죠.

"정상 쿼리가 끊기면 어떡하나"에 대해

이 값을 안 거는 이유는 대부분 하나입니다. 정상적인 장기 작업이 끊길까 봐요. 저도 그래서 미뤄왔고, 실제로 무시할 수 없는 걱정이죠.

그런데 이번에 워크로드를 실측해보니 생각과 좀 달랐습니다. audit 로그 기준으로 이 서비스의 정상 쿼리는 전부 밀리초 단위였어요. 가장 무거운 축인 검색 프로시저도 1초 안에 끝났고요. 웹 애플리케이션 트래픽만 받는 DB라면 대부분 비슷할 거예요. 30초짜리 타임아웃에 걸리는 정상 쿼리가 있다면, 그건 타임아웃 문제가 아니라 그 쿼리의 문제일 가능성이 높습니다.

 

그래도 일괄 적용이 좀 부담스러우면 단계가 있습니다.

  • 전역은 넉넉하게, 계정별로 좁게. MariaDB는 유저 단위로도 이 값을 다르게 줄 수 있거든요. 웹 애플리케이션 계정은 짧게, 배치 계정은 길게 주면 돼요.
  • 문장 단위 예외. 정말 오래 걸려야 하는 작업은 SET STATEMENT max_statement_time=3600 FOR ...로 그 문장만 풀어줍니다. AWS가 heartbeat에 쓰는 방식 그대로요.
  • 동적 파라미터라 재시작이 필요 없습니다. RDS 파라미터 그룹에서 바로 적용되고, 문제가 생기면 바로 되돌릴 수 있어요.

값을 정하는 기준은 결국에는 워크로드 실측입니다. slow query 로그에서 정상 상태의 최대 실행 시간을 확인하고, 거기에 여유를 곱해서 잡으면 되고요. 저희는 정상 최대치의 수십 배를 줘도 이번 같은 폭주는 몇 초, 늦어도 몇 분 안에 끊을 수 있었어요.

타임아웃 하나로는 부족한 것들

그래서 타임아웃만 걸면 되느냐. 그건 아닙니다. 이번에 같이 정리한 것들이 있어요.

 

cte_max_recursion_depth. 재귀 CTE의 반복 상한이고 MariaDB 기본값이 1000입니다. 메뉴 계층이 수십 단계를 넘을 일은 없으니 훨씬 낮췄습니다. 타임아웃이 시간의 상한이라면 이건 반복의 상한이죠. 겹치지 않는 안전장치가 하나 더 생기는 겁니다.

 

innodb_temp_data_file_path 상한. 임시 테이블스페이스가 무한히 자라지 못하게 막습니다.

 

innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:20G

이걸 걸어두면 같은 일이 벌어져도 인스턴스 전체가 멈추는 대신 해당 쿼리만 실패합니다.[6] 한 번 커진 임시 테이블스페이스는 재시작 전까지 반환되지 않는 파일이라, 상한 없이 두면 이번처럼 조용히 전체를 잠식해요.

 

알림 재설계.

번 사고에서 가장 먼저 나타난 신호는 스토리지가 아니라 CPU와 AAS였습니다. 08-21 오전 10시에 CPU가 1.1%에서 26%로 뛴 순간을 잡았다면 엿새를 벌었어요. 그런데 CPU 알림 임계값이 90%라 50.9%도 안 걸렸습니다. 평소가 1%인 DB에서 26%는 26배 증가인데도요. 그래서 스토리지 알림은 잔여량에서 잔여 시간 기준으로, CPU는 절대값이 아니라 평소 대비 급변으로 바꾸고, DBLoad가 일정 수준으로 지속되는 걸 잡는 룰을 추가했습니다. noDataStateOK에서 Alerting으로 바꿨어요. 인스턴스가 죽어 지표가 끊기면 알림이 저절로 꺼지는, 가장 심각한 순간에 가장 조용해지는 설정이었거든요.

 

데이터 정합성.

순환 참조 자체를 찾아 고치는 건 애플리케이션과 데이터 영역이라 개발 쪽에 넘겼습니다. 재귀 CTE로 직접 찾을 수 있어요.

WITH RECURSIVE walk AS (
  -- 시작점: 모든 노드
  SELECT MENU_CODE, PARENT_CODE, ..., 1 AS depth, CAST(MENU_CODE AS CHAR(2000)) AS path
    FROM MENU_INFO
  UNION ALL
  -- 재귀: 부모 방향으로 거슬러 올라가며 경로 누적
  SELECT w.MENU_CODE, m.PARENT_CODE, ..., w.depth + 1, CONCAT(w.path, ',', m.MENU_CODE)
    FROM walk w
    JOIN MENU_INFO m ON m.MENU_CODE = w.PARENT_CODE
   WHERE w.depth < 50
     AND FIND_IN_SET(m.MENU_CODE, w.path) = 0   -- 이미 지나온 노드면 멈춘다
)
SELECT MENU_CODE, depth, path FROM walk WHERE depth >= 50;

FIND_IN_SET 조건이 방어막입니다. 이게 없으면 진단 쿼리 자체가 같은 함정에 빠져요.

 

아직 정리 안 된 것들

솔직히 아직 확인 못 한 것들도 있습니다.

하나는 순환 참조의 실물이에요. 구조적으로는 순환이 있어야 모든 관측이 설명되는데, 실제로 어느 메뉴 코드가 순환하는지는 위 쿼리를 돌려봐야 압니다. 개발 쪽 회신을 기다리는 중이죠.

 

또 하나는 타임아웃 값 자체입니다. 너무 짧으면 정상 배치가 끊기고, 길면 이번 같은 상황에서 의미가 없어요. 결국에는 고객마다 워크로드가 달라서 일괄 기준을 못 만들고 계정 단위로 하나씩 실측해서 잡고 있는데, 이건 사례가 더 쌓여야 기준이 나올 것 같습니다.

정리하면

말이 길어졌는데, 하고 싶었던 얘기는 처음의 한 문장입니다.

운영 데이터베이스에는 statement timeout이 걸려 있어야 한다.

 

이유를 세 줄로 줄이면 이렇습니다.

 

첫째, 타임아웃이 없으면 폭주는 실패하지 않고 그냥 계속 실행됩니다. 그리고 실행 중인 쿼리는 audit 로그에도 slow query 로그에도 남지 않아요. 완료 기반 로그는 끝난 것만 기록하니까요. 조용한 폭주를 시끄러운 실패로 바꾸는 게 타임아웃의 진짜 역할입니다.

 

둘째, 결국 같은 버그라도 타임아웃 유무가 결과의 크기를 가릅니다. 이번 건은 타임아웃이 있었다면 저장 버튼 에러, 버그 리포트 한 건이었을 일이에요. 없어서 엿새 뒤 전면 장애가 됐습니다.

 

셋째, 걱정만큼 위험하지 않습니다. 계정별 차등, 문장 단위 예외, 무중단 적용까지 다 가능해요. AWS도 자기 내부 쿼리에는 전부 걸어두고요.

 

결국에는 실패를 빨리, 시끄럽게 만드는 것들이 시스템을 지키더라고요. 에러가 안 나는 것과 문제가 없는 것은 다릅니다. 이번 사고에서 에러는 단 한 줄도 없었어요. 그게 문제였습니다.


주석

[1] RDS는 인스턴스의 여유 스토리지가 고갈되면 상태를 storage-full로 전환하고 신규 연결과 쓰기를 차단합니다. 이 상태에서는 로그 파일 기록도 실패할 수 있어, 정작 사고 순간의 진단 정보가 남지 않는 경우가 생깁니다.

 

[2] Performance Insights를 활성화하면 DBLoad, DBLoadCPU, DBLoadNonCPUAWS/RDS 네임스페이스로 게시되어 일반 CloudWatch 지표처럼 조회하거나 알림을 걸 수 있습니다. SQL 단위 분해는 PI 콘솔이 필요하지만, 인스턴스 전체 부하 추이는 평소 쓰는 대시보드에 패널 하나 추가하는 것으로 볼 수 있습니다.

 

[3] Average Active Sessions. 특정 구간에 데이터베이스에서 활성 상태였던 세션의 평균 개수입니다. PI가 1초 간격으로 세션 상태를 샘플링해 산출합니다. AAS가 1이면 평균적으로 항상 한 개의 세션이 무언가를 실행 중이라는 뜻이고, vCPU 수와 비교하면 부하 수준을 가늠할 수 있습니다.

 

[4] 재귀 CTE는 앵커(비재귀) 부분의 결과를 내부 임시 테이블에 채운 뒤, 그 결과를 재귀 부분에 조인해 새로 생성된 행을 다시 임시 테이블에 추가하는 방식으로 반복합니다. 새 행이 나오지 않으면 종료됩니다. UNION ALL을 쓰면 중복 제거가 없으므로, 순환 참조가 있고 컬럼 값이 매번 달라지면 종료 조건에 도달하지 못합니다.

 

[5] MariaDB의 max_statement_time은 전역, 세션, 계정, 그리고 SET STATEMENT ... FOR 형태로 문장 단위까지 지정할 수 있습니다. 초 단위 실수값을 받으며, 지정 시간을 넘긴 statement는 중단됩니다. SELECT만이 아니라 모든 statement에 적용된다는 점이 MySQL의 max_execution_time(SELECT 한정)과 다릅니다.

 

[6] innodb_temp_data_file_path는 InnoDB 임시 테이블스페이스 파일의 경로, 초기 크기, 확장 방식, 최대 크기를 지정합니다. max 값을 지정하면 임시 테이블스페이스가 그 크기를 넘어 확장되지 않고 해당 쿼리가 에러로 종료됩니다. 이 파일은 서버 재시작 시 초기 크기로 다시 생성되므로, 한 번 커진 공간은 재시작 전까지 반환되지 않습니다. 정적 파라미터라 적용에는 재시작이 필요합니다.

참고

이 글의 수치와 타임라인은 실제 장애 대응 과정에서 확인한 것입니다. 고객사 식별 정보와 내부 리소스 이름은 익명화했습니다.