Zettelkasten

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)였다. 더 큰 인스턴스를 쓰던 시절에도 같은 모양으로 줄었는데, 여유가 커서 재시작 전에 드러나지 않았을 뿐이다.

진단 순서

  1. 커넥션 곡선과 여유 메모리 곡선을 겹쳐 본다. 커넥션이 평평한데 메모리만 직선으로 줄면 커넥션당 메모리(스레드 스택 등)가 원인이 아니다.
  2. 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 안쪽이었다.
  3. 상위 항목에 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. 행 수로 확정한다.
    SELECT COUNT(*) host_rows, SUM(CURRENT_CONNECTIONS > 0) active_hosts
    FROM performance_schema.hosts;
    
    프라이머리는 4,362행 중 지금 접속 중인 게 20개였고, 호스트·계정 관련 P_S 메모리가 1,427 MiB였다. 호스트 하나에 약 0.33 MiB다. 27.6일 동안 하루 약 158개씩 새 호스트가 쌓였으니 하루 약 52 MiB로, 여유 메모리 감소분의 약 80%를 설명한다.

왜 쌓이는가 (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만 돌려도 증가는 막을 수 있다. 다만 이미 잡힌 메모리는 재시작 전까지 계속 묶여 있다.

관련 노트

참고