performance_schema_show_processlist — SHOW PROCESSLIST 내부 동작

한 줄 요약: MySQL 8.0.22에 도입된 이 옵션은 SHOW PROCESSLIST의 데이터 출처를 레거시 스레드 매니저에서 performance_schema.processlist로 전환하는 스위치임. 8.0.35부터 deprecated 처리되었으며, 변수가 제거된 이후에는 PFS 구현이 기본값이 됨.


변수 개요

performance_schema_show_processlistSHOW PROCESSLIST 명령의 내부 구현 경로를 선택하는 전역 동적 변수임. MySQL 8.0.22 이전까지는 C++ 스레드 매니저를 직접 순회하는 단 하나의 구현만 존재했음. 8.0.22에서 performance_schema.processlist 테이블을 읽는 PFS 경로가 추가되면서 이 변수가 두 경로 사이의 스위치가 되었고, 8.0.35부터는 변수 자체가 deprecated되어 향후 PFS 경로만 남을 예정임.

항목내용
도입MySQL 8.0.22 (Release Notes)
DeprecatedMySQL 8.0.35 (8.4.x에서도 동일)
기본값OFF (레거시 경로)
적용 범위글로벌, 동적 변경 가능

결론

두 경로 비교

구분레거시 경로 (OFF, 기본값)PFS 경로 (ON)
데이터 출처Global_THD_manager 직접 순회performance_schema.processlist 쿼리
잠금목록 복사 시 파티셔닝된 뮤텍스 + per-thread 뮤텍스없음 (lock-free)
SHOW PROCESSLIST INFO100자100자
SHOW FULL PROCESSLIST INFOmax_allowed_packet까지1024바이트 (PFS 내부 버퍼 크기 제한)
현재 상태기본값이지만 레거시8.0.35부터 deprecated

커넥션 수백 개 이하 소규모 서버에서는 큰 차이가 없지만, 수천 개 이상을 처리하는 서버에서는 레거시 경로의 잠금 경합이 실질적인 부담이 됨.

권장 방법

1단계 — 현재 변수 상태 확인

SHOW VARIABLES LIKE 'performance_schema_show_processlist';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| performance_schema_show_processlist | OFF   |
+-------------------------------------+-------+

2단계 — performance_schema.processlist 직접 조회

변수를 건드리지 않고 performance_schema.processlist를 직접 쿼리하는 방식이 권장됨. 잠금 없이 읽히며 필터링·정렬도 자유롭게 가능함.

SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
FROM performance_schema.processlist
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC;
+----+-----------------+-----------+--------+---------+------+------------------------+-------------------+
| ID | USER            | HOST      | DB     | COMMAND | TIME | STATE                  | INFO              |
+----+-----------------+-----------+--------+---------+------+------------------------+-------------------+
|  5 | event_scheduler | localhost | NULL   | Daemon  |   16 | Waiting on empty queue | NULL              |
| 10 | root            | localhost | testdb | Query   |    1 | User sleep             | SELECT SLEEP(300) |
| 11 | root            | localhost | testdb | Query   |    1 | User sleep             | SELECT SLEEP(300) |
+----+-----------------+-----------+--------+---------+------+------------------------+-------------------+

sys.processlist는 추가 컬럼(락 대기 여부, 메모리 사용량 등)을 포함해 현업에서 더 편리하게 활용할 수 있음. sys.session은 현재 세션만 조회함.

3단계 — 1024바이트를 초과하는 쿼리 전문 확인

performance_schema.processlist의 INFO 컬럼은 pfs_column_types.h에 정의된 내부 버퍼(char m_processlist_info[1024]) 크기 제한으로 최대 1024바이트까지만 담김. 전문이 필요하면 events_statements_current를 함께 활용해야 함.

SELECT t.PROCESSLIST_ID, s.SQL_TEXT
FROM performance_schema.threads t
JOIN performance_schema.events_statements_current s
  ON t.THREAD_ID = s.THREAD_ID
WHERE t.PROCESSLIST_ID = 10;
+----------------+-------------------+
| PROCESSLIST_ID | SQL_TEXT          |
+----------------+-------------------+
|             10 | SELECT SLEEP(300) |
+----------------+-------------------+

내부 구현 분석

파싱부터 실행까지

SHOW PROCESSLIST 구문이 파싱되면 parse_tree_nodes.ccPT_show_processlist::make_cmd()에서 첫 번째 분기가 일어남.

// sql/parse_tree_nodes.cc:2879
bool use_pfs = pfs_processlist_enabled;  // 시스템 변수 값을 한 번만 읽음
m_sql_cmd.set_use_pfs(use_pfs);
if (use_pfs) {
    build_processlist_query(m_pos, thd, m_sql_cmd.verbose());
}

실행 시에는 Sql_cmd_show_processlist::execute_inner()에서 두 경로로 갈라짐.

// sql/sql_show.cc:591
bool Sql_cmd_show_processlist::execute_inner(THD *thd) {
  if (use_pfs()) {
    return Sql_cmd_show::execute_inner(thd);  // PFS 경로: SQL 플랜으로 실행
  } else {
    mysqld_list_processes(thd, ...);          // 레거시 경로: C++ 직접 순회
    return false;
  }
}

레거시 경로 (OFF, 기본값)

레거시 경로는 mysqld_list_processes()Global_THD_manager::do_for_all_thd_copy() 순으로 호출됨.

// sql/mysqld_thd_manager.cc:290
void Global_THD_manager::do_for_all_thd_copy(Do_THD_Impl *func) {
  for (int i = 0; i < NUM_PARTITIONS; i++) {
    MUTEX_LOCK(lock_remove, &LOCK_thd_remove[i]);
    mysql_mutex_lock(&LOCK_thd_list[i]);
 
    THD_array thd_list_copy(thd_list[i]);   // 스레드 목록을 복사
 
    mysql_mutex_unlock(&LOCK_thd_list[i]);  // 복사 직후 잠금 해제
 
    // 이후 순회는 잠금 없이 복사본으로 진행
    std::for_each(thd_list_copy.begin(), thd_list_copy.end(), doit);
  }
}

파티션 기반 Lock Striping

흔히 “글로벌 뮤텍스를 잡는다”고 표현하지만, 소스를 보면 더 정확한 그림이 나옴.

전체 스레드 목록은 NUM_PARTITIONS = 8개의 파티션으로 분산 관리됨(sql/mysqld_thd_manager.cc 상수). 각 파티션은 독립된 뮤텍스 쌍(LOCK_thd_list[i], LOCK_thd_remove[i])을 가지며, 스레드는 thread_id % 8 값에 따라 배정됨.

thd_list[0]  ← LOCK_thd_list[0], LOCK_thd_remove[0]
thd_list[1]  ← LOCK_thd_list[1], LOCK_thd_remove[1]
  ...
thd_list[7]  ← LOCK_thd_list[7], LOCK_thd_remove[7]

이 구조는 MySQL 8.0.0에서 WL#9250(커밋 58187639671, 2016-05-03)으로 도입됨. 커밋 메시지는 도입 목적을 명확히 밝히고 있음.

“This is done to remove the currently dominating mutex bottleneck for connect/disconnect performance.”

MySQL 5.7 이하에서는 LOCK_thd_list, LOCK_thd_remove 각각 단일 뮤텍스 하나가 전체 스레드 목록을 보호했음. 커넥션이 1,000개면 1,000개짜리 배열을 복사하는 동안 뮤텍스를 점유해야 했고, 그 사이 새로운 연결 추가/제거가 전부 차단됐음. connect/disconnect 성능에서 이 단일 뮤텍스가 **지배적인 병목(dominating bottleneck)**이었음.

버전구조
MySQL 5.7 이하단일 LOCK_thd_list + 단일 LOCK_thd_remove — 전체 스레드를 하나의 뮤텍스로 보호
MySQL 8.0 이상8개 파티션으로 분할 — lock striping (WL#9250)

파티션 도입 후에는 한 번에 전체의 1/8만 복사하고 잠금을 해제하므로 잠금 보유 시간이 약 1/8로 줄어들고, 서로 다른 파티션에 속한 스레드는 동시에 추가/제거가 가능해짐.

do_for_all_thd_copy의 설계 원칙은 다음과 같음.

  • LOCK_thd_list: 복사하는 순간에만 짧게 획득하고 즉시 해제 → 새 연결 추가(insert)는 허용
  • LOCK_thd_remove: 파티션 전체 스코프 동안 유지 → 순회 중 dangling pointer 방지, 제거(remove)는 차단

그럼에도 활성 세션이 많은 고부하 시스템에서는 목록 복사 단계의 잠금 경합과 수십~수백 개의 per-thread 뮤텍스(LOCK_thd_data)를 연속으로 획득하는 과정이 누적되면 실질적인 부담이 발생함.

INFO 컬럼 길이 제한

// sql/sql_show.cc:2987
size_t max_query_length =
    (verbose ? thd->variables.max_allowed_packet : PROCESS_LIST_WIDTH);
  • SHOW PROCESSLIST: PROCESS_LIST_WIDTH = 100자로 제한
  • SHOW FULL PROCESSLIST: max_allowed_packet까지 허용 (기본값 64MiB)

사용자 체감 문제

내부 구현의 한계는 실제 운영 환경에서 다음 네 가지 형태로 나타남.

1. 모니터링 도구가 서버를 느리게 만드는 역설

PMM, Zabbix 같은 모니터링 툴은 보통 1초 간격으로 SHOW PROCESSLIST를 실행함. 커넥션이 많을수록 각 호출마다 뮤텍스 획득 → 수천 개 스레드 복사 → 해제 사이클이 반복되어, 서버 상태를 확인하는 행위 자체가 성능을 저하시킴. 부하가 높을 때가 모니터링이 가장 필요한 시점인데, 바로 그때 오버헤드도 가장 큰 구조임.

2. 커넥션 폭주 시 장애 악화

커넥션이 급증하는 상황에서 DBA가 진단용으로 SHOW PROCESSLIST를 실행하면:

1) 커넥션 폭주 → DBA가 SHOW PROCESSLIST 실행
2) 뮤텍스 획득 → 수천 개 복사하는 동안 신규 커넥션 대기
3) 대기 중인 커넥션이 더 쌓임
4) 모니터링 툴도 동시에 SHOW PROCESSLIST 실행
5) 뮤텍스 경합 심화 → 장애 악화

진단 행위가 장애를 가속시키는 악순환이 발생함.

3. 커넥션 해제 지연

do_for_all_thd_copyLOCK_thd_remove를 파티션 전체 스코프 동안 보유함. SHOW PROCESSLIST 실행 중에 해당 파티션 소속 스레드가 disconnect를 시도하면 LOCK_thd_remove 획득을 대기해야 하므로, 커넥션을 닫았음에도 즉시 반환되지 않는 현상으로 나타날 수 있음.

4. 동시 실행 시 직렬화

여러 세션이나 모니터링 툴이 동시에 SHOW PROCESSLIST를 실행하면, 같은 파티션의 뮤텍스를 두고 순차 처리됨. 커넥션 수천 개 서버에서 툴 2~3개가 동시에 동작하면 각 호출의 응답 지연이 누적됨.

문제영향발생 조건
모니터링 오버헤드서버 전체 성능 저하모니터링 주기가 짧고 커넥션이 많을 때
장애 시 악순환진단이 장애를 악화커넥션 폭주 상황
커넥션 해제 지연커넥션 풀 고갈SHOW PROCESSLIST 실행 중 disconnect 시도
동시 실행 직렬화SHOW PROCESSLIST 응답 지연여러 세션/툴이 동시 실행

PFS 경로 (ON)

옵션을 ON으로 설정하면 SHOW PROCESSLIST는 실제로 다음 SQL 쿼리로 변환되어 실행됨.

-- SHOW PROCESSLIST 시
SELECT ID Id, USER User, HOST Host, DB db, COMMAND Command,
       TIME Time, STATE State, LEFT(INFO, 100) Info
FROM performance_schema.processlist;
 
-- SHOW FULL PROCESSLIST 시
SELECT ID Id, USER User, HOST Host, DB db, COMMAND Command,
       TIME Time, STATE State, LEFT(INFO, 1024) Info
FROM performance_schema.processlist;

이 쿼리 변환 로직은 sql/sql_show_processlist.cc:118build_processlist_query()에 구현되어 있음.

// sql/sql_show_processlist.cc:124
assert(PROCESS_LIST_WIDTH == 100);
if (verbose) {
    info_len = "1024";  // SHOW FULL PROCESSLIST
} else {
    info_len = "100";   // SHOW PROCESSLIST
}
// SELECT ..., LEFT(INFO, <info_len>) AS Info FROM performance_schema.processlist

스레드 목록을 직접 순회하지 않고 SQL 플랜으로 읽기 때문에 레거시 경로의 잠금 경합을 피할 수 있음. Performance Schema는 각 스레드가 자기 상태를 lock-free로 계측(instrumentation)해두고, 읽는 쪽은 그 계측 데이터를 읽기만 함.

INFO 1024바이트 제한의 진짜 이유

SHOW FULL PROCESSLIST에서 INFO가 1024바이트로 잘리는 이유는 PFS 내부 저장 구조 자체가 고정 크기 배열이기 때문임(storage/perfschema/pfs_instr.h, pfs_column_types.h 참조).

// storage/perfschema/pfs_instr.h:628
char m_processlist_info[COL_INFO_SIZE];  // = char[1024]
// storage/perfschema/pfs_column_types.h:69
#define COL_INFO_CHAR_SIZE 1024
#define COL_INFO_SIZE (COL_INFO_CHAR_SIZE * 1)

performance_schema.processlist의 INFO 컬럼은 스키마상 LONGTEXT로 선언되어 있지만, 실제 데이터가 담기는 PFS 스레드 구조체의 버퍼가 1024바이트 고정 배열임. 따라서 SELECT * FROM performance_schema.processlist로 직접 조회해도 INFO는 최대 1024바이트임. 이는 변수 ON/OFF와 무관한 PFS 구조의 근본 제약이며, 전문이 필요한 경우 결론 3단계의 events_statements_current 조회를 활용해야 함.


Deprecated — 8.0.35부터

이 변수는 MySQL 8.0.35부터 deprecated 처리됨. 실제 MySQL 8.4.8에서 변수를 변경하면 다음 경고가 발생함.

SET GLOBAL performance_schema_show_processlist = ON;
-- Warning (Code 4166): '@@performance_schema_show_processlist' is deprecated
-- and will be removed in a future release. When it is removed, SHOW PROCESSLIST
-- will always use the performance schema implementation.

sys_vars.cc에서 ON_UPDATE 핸들러로 구현되어 있음.

// sql/sys_vars.cc:546
static bool performance_schema_show_processlist_update(sys_var *, THD *thd,
                                                       enum_var_type) {
  push_warning_printf(thd, Sql_condition::SL_WARNING,
                      ER_WARN_DEPRECATED_WITH_NOTE,
                      ER_THD(thd, ER_WARN_DEPRECATED_WITH_NOTE),
                      "@@performance_schema_show_processlist",
                      "When it is removed, SHOW PROCESSLIST will always use"
                      " the performance schema implementation.");
  return false;
}

deprecated되는 것은 스위치 변수 자체이며, 변수가 제거된 이후에는 PFS 구현이 영구 기본값이 됨. 즉 레거시 경로를 완전히 제거하고 PFS 경로만 남긴다는 뜻임.

deprecated가 된 이유는 크게 세 가지임.

  1. 모니터링 표준의 이전: performance_schemasys 스키마 직접 쿼리가 공식 표준이 되어 중간 단계 스위치 변수의 필요성이 낮아짐
  2. 불완전한 하위 호환성: SHOW FULL PROCESSLIST임에도 INFO가 1024바이트로 잘리는 동작이 혼란을 야기함
  3. 코드 복잡성 감소: 두 경로를 분기하는 코드를 제거해 유지보수 부담 경감

참고

  • MySQL 8.4.8 소스 코드
    • sql/sql_show.ccSql_cmd_show_processlist::execute_inner(), mysqld_list_processes()
    • sql/sql_show_processlist.ccbuild_processlist_query()
    • sql/parse_tree_nodes.ccPT_show_processlist::make_cmd()
    • sql/sys_vars.ccSys_pfs_processlist 변수 선언
    • sql/mysqld_thd_manager.ccdo_for_all_thd_copy(), NUM_PARTITIONS = 8
    • storage/perfschema/pfs_instr.hpfs_thread::m_processlist_info
    • storage/perfschema/pfs_column_types.hCOL_INFO_CHAR_SIZE = 1024
  • MySQL 8.0.22 Release Notesperformance_schema_show_processlist 도입