performance_schema_show_processlist — SHOW PROCESSLIST 내부 동작
한 줄 요약: MySQL 8.0.22에 도입된 이 옵션은
SHOW PROCESSLIST의 데이터 출처를 레거시 스레드 매니저에서performance_schema.processlist로 전환하는 스위치임. 8.0.35부터 deprecated 처리되었으며, 변수가 제거된 이후에는 PFS 구현이 기본값이 됨.
변수 개요
performance_schema_show_processlist는 SHOW PROCESSLIST 명령의 내부 구현 경로를 선택하는 전역 동적 변수임. MySQL 8.0.22 이전까지는 C++ 스레드 매니저를 직접 순회하는 단 하나의 구현만 존재했음. 8.0.22에서 performance_schema.processlist 테이블을 읽는 PFS 경로가 추가되면서 이 변수가 두 경로 사이의 스위치가 되었고, 8.0.35부터는 변수 자체가 deprecated되어 향후 PFS 경로만 남을 예정임.
| 항목 | 내용 |
|---|---|
| 도입 | MySQL 8.0.22 (Release Notes) |
| Deprecated | MySQL 8.0.35 (8.4.x에서도 동일) |
| 기본값 | OFF (레거시 경로) |
| 적용 범위 | 글로벌, 동적 변경 가능 |
결론
두 경로 비교
| 구분 | 레거시 경로 (OFF, 기본값) | PFS 경로 (ON) |
|---|---|---|
| 데이터 출처 | Global_THD_manager 직접 순회 | performance_schema.processlist 쿼리 |
| 잠금 | 목록 복사 시 파티셔닝된 뮤텍스 + per-thread 뮤텍스 | 없음 (lock-free) |
SHOW PROCESSLIST INFO | 100자 | 100자 |
SHOW FULL PROCESSLIST INFO | max_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.cc의 PT_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_copy는 LOCK_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:118의 build_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가 된 이유는 크게 세 가지임.
- 모니터링 표준의 이전:
performance_schema와sys스키마 직접 쿼리가 공식 표준이 되어 중간 단계 스위치 변수의 필요성이 낮아짐 - 불완전한 하위 호환성:
SHOW FULL PROCESSLIST임에도 INFO가 1024바이트로 잘리는 동작이 혼란을 야기함 - 코드 복잡성 감소: 두 경로를 분기하는 코드를 제거해 유지보수 부담 경감
참고
- MySQL 8.4.8 소스 코드
sql/sql_show.cc—Sql_cmd_show_processlist::execute_inner(),mysqld_list_processes()sql/sql_show_processlist.cc—build_processlist_query()sql/parse_tree_nodes.cc—PT_show_processlist::make_cmd()sql/sys_vars.cc—Sys_pfs_processlist변수 선언sql/mysqld_thd_manager.cc—do_for_all_thd_copy(),NUM_PARTITIONS = 8storage/perfschema/pfs_instr.h—pfs_thread::m_processlist_infostorage/perfschema/pfs_column_types.h—COL_INFO_CHAR_SIZE = 1024
- MySQL 8.0.22 Release Notes —
performance_schema_show_processlist도입