performance_schema hosts 행은 접속이 끊겨도 남아 컨테이너 환경에서 MySQL 메모리가 매일 는다
·수정 1회
요약
- ECS Fargate 태스크는 새로 뜰 때마다 다른 IP로 DB에 붙는다. performance_schema는 접속했던 호스트마다
hosts·accounts행과 호스트별 집계 행을 만들고, 접속이 끊겨도 지우지 않으며, 잡은 메모리를 서버가 도는 동안 돌려주지 않는다. 그래서 트래픽이 그대로여도 MySQL 메모리가 날마다 일정하게 는다. - 증가를 멈추려면 인스턴스마다
TRUNCATE TABLE performance_schema.accounts와performance_schema.hosts를 실행한다. 이미 잡힌 메모리까지 되찾으려면performance_schema_hosts_size·performance_schema_accounts_size를 고정값으로 넣고 재시작한다. 두 변수는 운영 중에 바꿀 수 없다.
본문
관찰된 증상 (RDS MySQL 8.4, 32 GiB 인스턴스, 버퍼풀 23 GiB)
CloudWatch FreeableMemory 일최저가 27일 동안 3.43 GiB에서 1.77 GiB로, 하루 약 65 MiB씩 직선으로 줄었다. 같은 기간 DatabaseConnections는 일평균 약 350, 최대 약 650으로 변화가 없었다. 리플리카도 같은 모양으로 절반 속도(하루 약 30 MiB)였다. 더 큰 인스턴스를 쓰던 시절에도 같은 모양으로 줄었는데, 여유가 커서 재시작 전에 드러나지 않았을 뿐이다.
진단 순서
- 커넥션 곡선과 여유 메모리 곡선을 겹쳐 본다. 커넥션이 평평한데 메모리만 직선으로 줄면 커넥션당 메모리(스레드 스택 등)가 원인이 아니다.
- mysqld 안쪽에서 늘었는지 본다.
전체에서SELECT * FROM sys.memory_global_total; SELECT event_name, ROUND(current_number_of_bytes_used/1048576,1) cur_mib FROM performance_schema.memory_summary_global_by_event_name ORDER BY current_number_of_bytes_used DESC LIMIT 15;memory/innodb/buf_buf_pool을 빼면 버퍼풀 밖 사용량이다. 이번에는 재시작 직후 2.0~2.4 GiB이던 것이 27일 뒤 3.42 GiB가 되어, 여유 메모리 감소분의 대부분이 mysqld 안쪽이었다. - 상위 항목에 by_host판과 by_account판이 같은 크기로 짝지어 나오면 호스트 수에 비례해 커지는 테이블이다. 이번에는
events_errors_summary_by_host_by_error와events_errors_summary_by_account_by_error가 243/243 MiB,events_statements_summary_by_host_by_event_name과 by_account판이 203/203 MiB였다. - 행 수로 확정한다.
프라이머리는 4,362행 중 지금 접속 중인 게 20개였고, 호스트·계정 관련 P_S 메모리가 1,427 MiB였다. 호스트 하나에 약 0.33 MiB다. 27.6일 동안 하루 약 158개씩 새 호스트가 쌓였으니 하루 약 52 MiB로, 여유 메모리 감소분의 약 80%를 설명한다.SELECT COUNT(*) host_rows, SUM(CURRENT_CONNECTIONS > 0) active_hosts FROM performance_schema.hosts;
왜 쌓이는가 (MySQL 8.4 문서)
- 클라이언트가 끊기면
CURRENT_CONNECTIONS만 1 줄고 행은 남는다. 행은TRUNCATE TABLE로만 지워진다. performance_schema_hosts_size·performance_schema_accounts_size의 기본값 -1은 autoscale이다. 처음엔 비어 있다가 데이터가 들어오는 대로 메모리를 잡고, 상한이 없다.- Performance Schema는 서버가 도는 동안 메모리를 해제하지 않고 재활용만 한다.
- Fargate 태스크는 저마다 ENI와 사설 IP를 받는다. 배포나 스케일링으로 태스크가 바뀌면 MySQL 입장에서는 새 호스트다.
- 호스트가 하나 생기면 집계 테이블마다 그 호스트 몫의 행이 오류 코드별·문장 종류별·대기 이벤트별·메모리 항목별로 생긴다. 그래서 호스트 하나가 0.33 MiB나 된다(관찰됨).
조치 1: TRUNCATE로 재시작 없이 증가를 멈춘다
TRUNCATE TABLE performance_schema.accounts;
TRUNCATE TABLE performance_schema.hosts;
- 접속이 없는 행만 지우고, 접속 중인 행은 남긴 채 누적 접속 수만 현재 값으로 초기화한다.
hosts를 비우면 by_host·by_account·by_thread 집계 테이블도 함께 비워진다. 서비스 테이블은 건드리지 않는다. - DROP 권한이 필요하다.
read_only=1인 리플리카에서도 된다. 문서상 read_only는 Performance Schema 테이블의 UPDATE·TRUNCATE를 막지 않는다. - 프라이머리에서 실행해도 리플리카의 행은 줄지 않았다(관찰됨). 인스턴스마다 따로 실행해야 한다.
- 프라이머리 결과는 4,362행에서 25행이었다. 빈자리 약 4,300개를 새 호스트가 재활용하므로, 지금 속도라면 약 27일 동안은 메모리를 더 잡지 않는다(추정).
TRUNCATE 뒤 메모리 수치가 그대로라서 실패로 읽으면 안 되는 이유: current_number_of_bytes_used는 채워진 행이 아니라 이미 잡혀 있는 메모리를 센다. 이번에도 1,427 MiB는 그대로였고 행 수만 줄었다(관찰됨). 성공 여부는 행 수로 판단한다.
조치 2: 크기를 고정하고 재시작해 잡힌 메모리까지 되찾는다
performance_schema_hosts_size·performance_schema_accounts_size에 고정값(예: 256)을 넣는다. 0으로 두면 해당 통계를 아예 모으지 않는다.- 두 변수는 Dynamic: No이고, RDS 파라미터 그룹에서도 static이다.
ApplyMethod=pending-reboot로 넣으면 다음 재부팅 때 반영된다. 재시작하면 쌓였던 1.4 GiB가 돌아오고, 이후에는 256개 × 0.33 MiB, 약 84 MiB 넘게 늘지 않는다. - 테이블이 다 차면 새 호스트 행이 추가되지 않고
Performance_schema_hosts_lost·Performance_schema_accounts_lost가 올라간다. performance_schema_users_size는 계정 이름 기준이라 IP가 바뀌어도 늘지 않으므로 건드릴 필요가 없다.
주기적으로 TRUNCATE만 돌려도 증가는 막을 수 있다. 다만 이미 잡힌 메모리는 재시작 전까지 계속 묶여 있다.
관련 노트
- RDS 다운사이징 전에 buffer pool을 점진적으로 낮춰 핫셋이 작은 pool에 맞는지 실측한다
- pymysql 파싱 CPU의 원인 쿼리는 performance_schema digest의 SUM_ROWS_SENT 랭킹으로 특정한다
- OPTIMIZE TABLE은 테이블을 통째로 리빌드해 DELETE로 남은 미반환 공간을 OS로 회수한다
참고
- https://dev.mysql.com/doc/refman/8.4/en/performance-schema-memory-model.html
- https://dev.mysql.com/doc/refman/8.4/en/performance-schema-connection-tables.html
- https://dev.mysql.com/doc/refman/8.4/en/performance-schema-system-variables.html
- https://dev.mysql.com/doc/refman/8.4/en/performance-schema-status-variables.html
- https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_read_only
- https://dev.mysql.com/doc/refman/8.4/en/truncate-table.html
- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-task-networking.html
함께 읽기 좋은 글
- pymysql 파싱 CPU의 원인 쿼리는 performance_schema digest의 SUM_ROWS_SENT 랭킹으로 특정한다
- LEFT JOIN 오른쪽 테이블 조건을 WHERE에 두면 INNER JOIN으로 퇴화한다
- semi-join과 anti-join은 매칭 여부만 보므로 INNER JOIN과 달리 행이 불어나지 않는다
- range_optimizer_max_mem_size를 넘으면 옵티마이저가 range 접근을 포기하고 풀 스캔으로 빠진다
- SSCursor는 결과를 서버에 두고 행 단위로 흘려보내 클라이언트 메모리 대신 커넥션 점유를 택한다