(KO) CUBRID 모니터링 — Perfmon 카운터, 통계 집계, 서브시스템별 모니터
목차
학술적 배경
섹션 제목: “학술적 배경”운영 중인 데이터베이스 엔진은 외부에서 보면 블랙박스다. 수백 개의 워커 스레드, 수십 개의 서브시스템, 분당 수십억 건의 작은 연산이 그 안에서 돌아간다. 무엇이 건강하고, 어디서 시간이 소모되고, 무엇이 병목인지 파악하려면 엔진이 하는 일을 세고 재는 것 외에 방법이 없다. 모니터링 서브시스템은 엔진의 계측 표면이다. 모든 서브시스템이 자신의 이벤트를 여기 쌓고, DBA와 statdump, SHOW 명령, 자동 튜너가 집계값을 다시 꺼내 간다.
이 표면의 모양을 결정하는 교과서적 아이디어 세 가지가 있다.
카운터 기반 모니터링. Hellerstein–Stonebraker의 Anatomy of a Database System (Red Book 4장)과 Petrov의 Database Internals (13장)은 엔진을 각 서브시스템이 이름 붙은 숫자 카운터의 고정 목록을 소유하는 그래프로 묘사한다. 목록은 정적이다. 바이너리에 포함된다. 카운터는 *오래 산다. 서버 시작 시점부터 누적된다. 읽기는 *싸다. 배열을 memcpy하면 그만이다. 계측의 비용, 즉 lock_object나 pgbuf_fix, qexec_execute_mainblock 안에서 발생하는 증가 연산은 모니터링을 켜도 측정 대상 워크로드가 흔들리지 않을 만큼 작아야 한다. 이것이 하이젠베르크 관찰 효과 문제다.
스레드별 vs 서버 전체 집계. 모든 스레드가 공유 카운터 하나를 증가시키는 가장 단순한 구현은 코어 사이에서 캐시 라인이 핑퐁하게 만들어, 실제 작업보다 계측 비용이 커진다. 교과서적 해결책은 두 갈래다. (a) 단일 전역 변수에 원자적 증가를 가하거나 (CAS 한 번), (b) 스레드별 샤드를 두고 읽는 쪽이 게으르게 집계한다 (쓰기는 대기 없음, 읽기는 스레드 수에 비례). 대부분의 엔진은 이 둘을 섞는다. 저빈도 카운터는 원자적 방식, 고빈도 카운터는 샤드 방식. 카운터의 존재 자체가 아니라 이 혼합 비율이 아키텍처 결정이다.
샘플링 vs 상시 계측. 상시 카운터는 정확한 합계를 제공하되 지속적인 오버헤드가 따른다. 샘플링 방식(PostgreSQL의 pg_stat_statements, Oracle의 ASH, MySQL의 events_* 링 버퍼)은 정확성 대신 이벤트별 메타데이터를 얻는다. 카운터는 얼마나 많이 일어났는가에 답하고, 샘플은 누가 했는가에 답한다. CUBRID의 모니터 서브시스템은 순수하게 카운터 진영에 속한다. 샘플링 방식의 관찰 가능성은 SQL 추적, 브로커 SQL 로그, 서버 진단 등 다른 곳에 자리한다.
src/monitor/와 src/base/perf_monitor.{h,c}의 구조들은 CUBRID이 각 축 위에서 내린 선택을 그대로 표현한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”PostgreSQL — pg_stat_*과 통계 수집기. 각 백엔드가 백엔드별 공유 메모리에 이벤트별 카운터를 기록하고, 백그라운드 수집기가 그것을 스냅샷으로 집계해 pg_stat_* 뷰가 일반 heap 스캔 방식으로 읽는다. PG 15부터 수집기가 직접 공유 메모리 쓰기와 동적 스냅샷 읽기 경로로 교체되었지만, 쓰기 측 카운터와 읽기 측 뷰의 분리라는 아키텍처 구도는 유지된다.
MySQL — Performance Schema + INFORMATION_SCHEMA + SHOW. 전용 스토리지 엔진(ha_perfschema)이 링 버퍼 이벤트 테이블과 집계 테이블을 관리한다. INFORMATION_SCHEMA는 ANSI 정렬 뷰를 제공하고, SHOW STATUS/SHOW ENGINE INNODB STATUS는 같은 백엔드 위의 문법적 편의 장치다. 계측 비용은 setup_consumers/setup_instruments로 옵트인 방식이다.
Oracle — X$ 고정 테이블 위의 V$. 커널이 내부 C 배열을 X$ 고정 테이블로 공개하고, V$/GV$가 그 위의 카탈로그 뷰다. AWR/ASH가 보존을 위해 V$를 실제 테이블로 스냅샷한다.
SQL Server — DMV. sys.dm_exec_*, sys.dm_os_wait_stats 등이 Oracle의 V$/X$와 구조적으로 동일하다. 카탈로그 뷰가 행을 요청 시점에 계산하는 내부 테이블 소스로 해소된다.
CUBRID은 PostgreSQL보다 MySQL/Oracle에 가깝다. 카운터를 별도 수집기 프로세스에 넘기지 않고 프로세스 로컬 UINT64 배열(pstat_Global.global_stats)에 직접 쓰고, 같은 배열을 SHOW 패밀리로 노출한다. 더 최근에 추가된 cubmonitor C++ 템플릿 라이브러리는 PostgreSQL의 백엔드별 샤딩(트랜잭션별 시트)과 구조적으로 유사한 등록 모델을 전역 카운터 위에 얹는다. 두 반쪽 모두 하나의 바이너리 안에 있다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”CUBRID의 모니터링 표면은 단일 서브시스템이 아니다. 두 개의 협력하는 계층과 자체 비카운터 상태를 유지하는 서브시스템별 모니터들로 이뤄진다.
flowchart LR
HOT["핫 패스
(pgbuf_fix, lock_object,
btree_find, qexec_∗)"]
PERF["perf_monitor.{h,c}
pstat_Global.global_stats[]
PSTAT_METADATA pstat_Metadata[]"]
CUB["cubmonitor (C++ 템플릿)
monitor::register_statistics
counter/timer/max 통계"]
TX["transaction_sheet_manager
트랜잭션별 시트 (≤ MAX_SHEETS)"]
OVFP["서브시스템별 모니터
ovfp_threshold_mgr
(per-vacuum-worker 연결 리스트)"]
HOT -->|perfmon_inc/perfmon_add_at_offset| PERF
HOT -->|stat.collect / autotimer| CUB
CUB -.->|전역 확장| TX
HOT -->|add_read_pages_count| OVFP
SHOW["SHOW STATS / statdump
SHOW EXEC STATISTICS
sysprm dump"]
SHOW -->|perfmon_get_stats /
perfmon_calc_diff_stats| PERF
SHOW -.->|monitor::fetch_*_statistics| CUB
SHOW -->|ovfp_threshold_mgr::dump| OVFP
C 언어의 perf_monitor 배열이 더 크고 오래된 표면이다. SHOW EXEC STATISTICS, cubrid statdump, 추적 기능이 아직도 여기서 읽는다. cubmonitor C++ 라이브러리는 더 풍부한 조합(카운터 + 타이머 + 최대값 + 평균을 하나로, 트랜잭션별 시트 선택 지원)을 위해 나중에 추가되었다. 현재는 lockfree_hashmap 계측과 일부 호출 지점을 담당한다. ovfp_threshold_mgr 같은 서브시스템별 모니터는 세 번째 패턴이다. 상태가 평면 카운터 배열에 담기에는 너무 풍부한 구조(BTID, OID, 카운트, 최대 페이지, 타임스탬프의 정렬된 연결 리스트)를 가진 모듈들이 여기 속한다.
세 계층은 CBRD-26177에 기록된 규율을 공유한다. 커넥션 워커 풀의 핫 패스에는 perfmon 증가 연산을 두지 않는다. 카운터 증가는 공유될 가능성이 있는 캐시 라인에 쓰기를 수행한다. 스레드별 워커 풀은 지연 시간에 민감한 요청 디스패치 경로에서 이런 공유 쓰기를 없애기 위해 재설계된 것이다. 재설계에서 살아남은 카운터는 저빈도 이벤트를 뒷받침하는 원자적 카운터뿐이고, 나머지는 스레드별 샤드로 이동하거나 제거되었다.
cubmonitor::statistic — 기본 구성 요소
섹션 제목: “cubmonitor::statistic — 기본 구성 요소”C++ 계층의 모든 카운터는 monitor_statistic.hpp의 한 템플릿 계층 구조를 세 개의 직교 축으로 조합해 만든다.
- 표현 타입 — 카운터/합계에는
amount_rep = std::uint64_t, 비율에는floating_rep = double, 타이머에는time_rep = duration(std::chrono::high_resolution_clock::duration). - 수집 의미론 —
accumulator_statistic은 더하고,gauge_statistic은 덮어쓰고,max_statistic은 가장 큰 값을 유지하고,min_statistic은 가장 작은 값을 유지한다. - 동기화 —
_atomic_변형은 저장소를std::atomic으로 감싸고fetch_add/compare_exchange_strong을 사용한다. 비원자적 변형은 단일 쓰기를 가정한다.
template <class Rep>class accumulator_statistic : public primitive<Rep>{ public: void collect (const Rep &value); };
template <class Rep>class accumulator_atomic_statistic : public atomic_primitive<Rep>{ public: void collect (const Rep &value); };
// time_rep는 std::atomic 안에 직접 넣을 수 없다 — duration은 사소하게 원자적이지 않다template <>class atomic_primitive<time_rep>{ // ... condensed ... std::atomic<time_rep::rep> m_value; // duration이 아닌 count를 저장};명명 규칙은 값타입_수집타입[_atomic]_statistic 형태다. 가장 흔한 타이머 변형은 time_accumulator_atomic_statistic이다. monitor_statistic.hpp 하단의 별칭들이 모든 합법적 조합을 열거한다.
fetch 계약은 통일되어 있다. 모든 기본 요소는 fetch(statistic_value *destination, fetch_mode mode)를 노출해 통계당 uint64_t 하나를 쓴다. statistic_value 표현은 단형이다. floating_rep와 time_rep는 비트 캐스트/기간 캐스트로 uint64_t 슬롯에 들어가므로, 단일 읽기 측 버퍼가 모든 것을 담는다. 시간은 마이크로초를 거친다.
// statistic_value_cast — monitor_statistic.cppstatistic_valuestatistic_value_cast (const time_rep &rep){ // nanoseconds to microseconds return static_cast<statistic_value> (std::chrono::duration_cast<std::chrono::microseconds> (rep).count ());}// note: time_rep_cast (statistic_value_cast (stat)) != stat (lossy round-trip)손실 있는 왕복 변환은 의도적이다. 마이크로초로도 대시보드가 소비하기에 충분하고, 나노초를 유지하려면 64비트 동적 범위가 절반으로 줄거나 더 넓은 익스포트 ABI가 필요하다.
최대/최소 통계는 누적기 위의 얇은 계층이다. 원자적 변형은 표준 CAS 루프를 사용하고 저장값을 numeric_limits::min()/::max()로 초기화하므로(그리고 time_rep::min()/::max() 특수화도 있다), 첫 번째 collect가 항상 이긴다.
그룹 통계 — timer_statistic, counter_timer_statistic, counter_timer_max_statistic
섹션 제목: “그룹 통계 — timer_statistic, counter_timer_statistic, counter_timer_max_statistic”대부분의 호출 지점은 하나의 그룹을 원한다. 횟수, 총 시간, 최대 지연, 파생 평균이 함께 있는 그룹이다. monitor_collect.hpp가 기본 요소들을 조합한다.
// counter_timer_statistic — monitor_collect.hpptemplate <class A = amount_accumulator_statistic, class T = time_accumulator_statistic>class counter_timer_statistic{ public: class autotimer { /* RAII: 생성자에서 리셋, 소멸자에서 time_and_increment */ }; void time_and_increment (const time_rep &d, const amount_rep &a = 1); void time_and_increment (const amount_rep &a = 1); // 내부 타이머 사용 void register_to_monitor (monitor &mon, const char *basename) const; private: timer m_timer; A m_amount_statistic; T m_time_statistic;};두 패턴이 반복된다. 내부 타이머 + autotimer — 각 그룹 통계가 clock_type::now()를 생성 시 스냅샷하는 전용 cubmonitor::timer를 가진다. autotimer RAII 클래스는 생성 시 리셋하고 소멸 시 증가시켜, 범위 계측은 { counter_timer_stat::autotimer at(my_stat); /* 작업 */ } 형태로 쓴다. 그룹 등록 — register_to_monitor는 get_statistics_count() + 1개의 슬롯을 등록한다. +1은 fetch 시점에 total/count로 계산되는 파생 평균이다. 이름은 build_name_vector로 Num_, Total_time_, Max_time_, Avg_time_ 접두사로 붙는다. C perf_monitor의 PSTAT_COUNTER_TIMER_VALUE 행이 쓰는 접두사와 같다. 이 일치가 statdump 출력에서 두 계층이 시각적으로 동일하게 보이는 이유다.
// counter_timer_max_statistic::register_to_monitor — monitor_collect.hppauto fetch_func = [&] (statistic_value * destination, fetch_mode mode){ this->fetch (destination, mode); destination[get_statistics_count ()] = statistic_value_cast (this->get_average_time (mode));};mon.register_statistics (stat_count, fetch_func, names);람다가 this를 캡처해 평균을 네 번째 슬롯으로 fetch 시점에 합성한다. 평균 카운터는 어디에도 저장되지 않는다. 마이크로초 캐스트의 한 가지 이유가 여기 있다. fetch 시점에 total / count가 나노초 정수보다 마이크로초 정수에서 수치적으로 더 안정적이다.
monitor — 중앙 레지스트리
섹션 제목: “monitor — 중앙 레지스트리”monitor_registration.hpp가 레지스트리를 선언한다. 클래스 전체가 등록 벡터다. 각 등록은 (개수, fetch_함수, 이름들)의 세 쌍이다. fetch 함수는 std::function<void (statistic_value *, fetch_mode)>이므로, 하나의 등록이 내부 임의 상태에서 여러 슬롯을 채울 수 있다. register_to_monitor가 만드는 람다가 정확히 그런 함수다.
// monitor — monitor_registration.hppclass monitor{ public: using fetch_function = std::function<void (statistic_value *, fetch_mode)>;
void register_statistics (std::size_t statistics_count, const fetch_function &fetch_f, const std::vector<std::string> &names);
statistic_value *allocate_statistics_buffer (void) const; void fetch_global_statistics (statistic_value *destination) const; void fetch_transaction_statistics (statistic_value *destination) const; void fetch_statistics (statistic_value *destination, fetch_mode mode) const;
private: struct registration { std::size_t m_statistics_count; fetch_function m_fetch_func; };
std::size_t m_total_statistics_count; std::vector<std::string> m_all_names; std::vector<registration> m_registrations;};
monitor &get_global_monitor (void);fetch 루프는 가능한 한 단순하다. 등록들을 순서대로 걷고, 각 fetch 함수를 호출하고, 목적지 포인터를 등록의 m_statistics_count만큼 전진시킨다. fetch 주변에 동기화는 없다. 일관성은 쓰는 쪽의 문제다. _atomic_ 통계가 제공하는 것이 바로 그 일관성이다. 등록들을 가로지르는 스냅샷 일관성은 명시적으로 보장하지 않는다.
// monitor::fetch_statistics — monitor_registration.cppvoidmonitor::fetch_statistics (statistic_value *destination, fetch_mode mode) const{ statistic_value *stats_iterp = destination; for (auto it : m_registrations) { it.m_fetch_func (stats_iterp, mode); stats_iterp += it.m_statistics_count; }}전역 모니터는 파일 범위 static monitor Monitor로, get_global_monitor()가 반환한다. 생명주기 관리는 없다. 모니터는 서버 시작부터 종료까지 살고, m_registrations 벡터는 서브시스템들이 등록을 수행함에 따라 단조롭게 늘어난다.
등록 모델을 한 가지 주의할 점이 있다. register_statistics(count, fetch_f, names) 시그니처는 서브시스템이 커스텀 집합을 직접 등록할 때도 쓰이고, 그룹 통계의 register_to_monitor 도우미로 간접적으로도 쓰인다. fetch_f는 통계 인스턴스를 참조로 캡처하는 람다다. 따라서 통계 인스턴스는 전역 모니터보다 오래 살아야 한다. 실제로 레지스트리에 들어가는 C++ 통계는 모두 서버 수명 동안 살아 있는 정적 변수나 멤버 변수고, 등록은 초기화 중 한 번 수행된다. unregister_statistics는 없다.
트랜잭션별 시트 — transaction_statistic과 transaction_sheet_manager
섹션 제목: “트랜잭션별 시트 — transaction_statistic과 transaction_sheet_manager”C++ 계층에서 가장 독특한 부분은 트랜잭션별 시트 모델이다. 단일 트랜잭션을 모든 카운터에 스레드별 샤딩 비용을 지불하지 않고 독립적으로 측정할 수 있다.
// transaction_statistic — monitor_transaction.hpptemplate <class S>class transaction_statistic{ public: using statistic_type = S; void fetch (statistic_value *destination, fetch_mode mode = FETCH_GLOBAL) const; void collect (const typename statistic_type::rep &value); // 전역 + 시트(열린 경우) private: void extend (std::size_t to); // 동기화된 크기 조정 statistic_type m_global_stat; statistic_type *m_sheet_stats; std::size_t m_sheet_stats_count; std::mutex m_extend_mutex;};transaction_statistic<S>는 임의의 통계 S를 감싸고 시트별 사본의 동적으로 늘어나는 배열을 추가한다. collect()는 항상 전역을 증가시킨다. 호출 트랜잭션에 열린 시트가 있으면 시트 로컬 사본도 추가로 증가시�킨다. 배열은 m_extend_mutex 아래에서 필요에 따라 늘어난다. 관찰하지 않는 트랜잭션은 아무 비용도 치르지 않는다.
시트 식별자는 정적 클래스인 transaction_sheet_manager에서 온다.
// transaction_sheet_manager — monitor_transaction.hppclass transaction_sheet_manager{ public: static const transaction_sheet INVALID_TRANSACTION_SHEET = std::numeric_limits<std::size_t>::max (); static const std::size_t MAX_SHEETS = 1024; static bool start_watch (void); static void end_watch (bool end_all = false); static transaction_sheet get_sheet (void); private: static std::size_t s_current_sheet_count; static unsigned s_sheet_start_count[MAX_SHEETS]; static transaction_sheet *s_transaction_sheets; static std::mutex s_sheets_mutex;};두 개의 상태가 협력한다. s_transaction_sheets[i]는 i번째 트랜잭션을 그 시트 슬롯(또는 INVALID)에 매핑한다. s_sheet_start_count[k]는 시트 k에 대한 중첩된 start_watch 호출 횟수를 세므로, 감시는 재진입 가능하고 end_watch는 카운트가 0으로 내려갈 때만 슬롯을 해제한다.
start_watch는 s_sheets_mutex를 잡고, logtb_get_current_tran_index()로 호출 트랜잭션을 조회하고, 그 시트 카운트를 올리거나 s_sheet_start_count[]를 스캔해 카운트-0 슬롯을 찾는다. MAX_SHEETS = 1024가 모두 사용 중이면 false를 반환한다. 시트는 획득이 필요한 자원이다.
아키텍처적 핵심은 get_sheet의 빠른 경로다.
// transaction_sheet_manager::get_sheet — monitor_transaction.cppif (s_current_sheet_count == 0) return INVALID_TRANSACTION_SHEET; // 시트 없음; 조기 반환start_watch를 한 번도 호출한 트랜잭션이 없는 일반적인 상황에서, transaction_statistic::collect는 전역 카운터 증가 하나와 비교 연산 하나만 치른다. 아무도 관찰하지 않으면 collect당 추가 비용은 사실상 없다.
절충점이 있다. 시트는 재사용되며 재사용된 시트는 이전 사용자의 값을 그대로 이어받는다. 헤더 텍스트에 나와 있듯이, 트랜잭션을 올바르게 검사하는 방법은 시작 시점에 스냅샷 하나, 끝 시점에 스냅샷 하나를 fetch하고 차이를 구하는 것이다. 스냅샷 차분은 소비자가 해결해야 할 문제다. PostgreSQL의 pg_stat_statements_reset() + 델타와 같은 의미론이다.
sequenceDiagram participant TX as 트랜잭션 (tx_idx) participant TSM as transaction_sheet_manager participant TS as transaction_statistic<S> participant G as 전역 통계 S TX->>TSM: start_watch () TSM-->>TX: (시트 k 할당, 카운트 1) TX->>TS: collect(value) TS->>G: m_global_stat.collect(value) TS->>TSM: get_sheet () TSM-->>TS: k TS->>TS: k >= m_sheet_stats_count이면 extend TS->>TS: m_sheet_stats[k].collect(value) Note over TX,TSM: 측정 구간 종료 TX->>TS: fetch(buf, FETCH_TRANSACTION_SHEET) TS->>TSM: get_sheet () TSM-->>TS: k TS-->>TX: m_sheet_stats[k] 값 (스냅샷) TX->>TSM: end_watch () TSM-->>TSM: --s_sheet_start_count[k]; 0이면 슬롯 해제
fetch 경로도 읽기 측을 그대로 반영한다. monitor::fetch_transaction_statistics는 먼저 현재 트랜잭션에 시트가 있는지 확인한다.
// monitor::fetch_transaction_statistics — monitor_registration.cppvoidmonitor::fetch_transaction_statistics (statistic_value *destination) const{ if (transaction_sheet_manager::get_sheet () == transaction_sheet_manager::INVALID_TRANSACTION_SHEET) { // 트랜잭션 시트 없음, fetch할 것 없음 return; } fetch_statistics (destination, FETCH_TRANSACTION_SHEET);}시트가 없으면 목적지 버퍼를 건드리지 않는다. 시트가 있으면 FETCH_GLOBAL 대신 FETCH_TRANSACTION_SHEET로 같은 fetch_statistics 순회를 수행한다. 각 transaction_statistic은 m_global_stat 대신 m_sheet_stats[sheet] 슬롯을 읽는다. transaction_statistic으로 감싸지지 않은 통계(일반 기본 요소, 일반 누적기)는 트랜잭션 모드를 무시하고 0을 쓴다. 반환할 시트별 사본이 없기 때문이다.
핫 패스 규율 — 처리량이 요구하는 곳에서 원자성 없애기
섹션 제목: “핫 패스 규율 — 처리량이 요구하는 곳에서 원자성 없애기”C perf_monitor.c 배열은 더 오래되고 더 광범위하다. PSTAT_* 아래의 모든 카운터가 여기 있다. 쓰기는 복합 카운터를 위해 perfmon_inc, perfmon_add_stat, perfmon_add_stat_at_offset을 거친다. 원자적 변형(ATOMIC_INC_64)은 항상 안전하다. 비원자적 변형은 경합이 구조적으로 드물거나, 모든 페이지 fix에서 lock cmpxchg를 치르는 대신 약간의 수치 왜곡을 받아들이는 곳에 쓰인다.
CBRD-26177 지시사항 — 워커 풀 재설계에 반영된 — 은 커넥션 워커 핫 패스에 perfmon을 쓰지 않는다 는 것이다. 카운터 증가는 결국 코어 간에 가시적으로 되어야 하는 쓰기를 수행하고, 그 가시성 확보 비용이 증가 자체보다 크다. 재설계는 계측을 느린 작업 실행 경로나 읽기 시점에 순회하는 스레드별 샤드로 옮겼다.
C++ cubmonitor 계층은 다른 탈출구를 제공한다. 등록 모델이 호출 트랜잭션이 관찰 중인지 알기 때문에, 활성 쓰기 횟수가 스레드 수가 아니라 열린 시트 수에 의해 결정된다. s_current_sheet_count == 0이면 모든 collect는 경합 없는 전역 증가 하나와 상수 시간 검사 하나다. 시트 하나가 열리면 쓰기는 두 번이다(전역 + 시트 로컬).
세 가지 패턴이 반복된다.
쓰기 처리량이 높고 읽기가 드문 곳에서 원자적 방식. lockfree_hashmap은 cubmonitor::atomic_counter_timer_stat을 쓴다. 동시 삽입자는 fetch_add를 치르고 SHOW STATS는 load를 한다. 해시맵 연산 빈도는 주변 비즈니스 로직에 의해 결정되므로 이 비용은 감수할 만하다.
구조적으로 단일 쓰기인 곳에서 비원자적 방식. 로그 플러셔, 페이지 플러셔, vacuum master 같은 데몬 통계는 한 스레드가 쓰고 여럿이 읽는다. 비원자적 accumulator_statistic이 정확하다. 쓰기 경합이 없으며, 64비트 읽기가 찢어지더라도 x86-64에서는 정렬된 64비트 쓰기가 원자적이므로 문제없고, 다른 아키텍처에서도 근사값으로 허용된다.
쓰는 쪽이 엔진이 아닌 사용자인 곳에서 트랜잭션별 시트. SHOW EXEC STATISTICS는 호출 세션의 카운터를 보고한다. 세션은 접속 시 시트를 열고, 엔진은 전역과 시트 모두를 증가시키고, 접속 해제가 감시를 끝낸다. 시트 기계장치는 서브시스템마다 세션별 카운터를 다시 구현하지 않고 이 구도를 표현한다.
flowchart LR W1["워커 1 일반 accumulator_statistic (동기화 없음; 단일 쓰기)"] W2["워커 2 amount_accumulator_atomic_statistic (이벤트마다 fetch_add)"] W3["워커 3 transaction_statistic<accum_atomic> collect → 전역 원자적 + 선택적 시트"] W1 -->|쓰기| G1["m_value (uint64_t)"] W2 -->|fetch_add| G2["m_value (atomic uint64_t)"] W3 -->|원자적 add| G3["m_global_stat.m_value"] W3 -.->|시트가 열려 있으면| S1["m_sheet_stats[k].m_value"] R["fetch (FETCH_GLOBAL)"] R --> G1 R --> G2 R --> G3 RT["fetch (FETCH_TRANSACTION_SHEET) (get_sheet () != INVALID인 경우에만)"] RT -.-> S1
리셋과 스냅샷 의미론
섹션 제목: “리셋과 스냅샷 의미론”monitor::reset은 없다. C++ 계층은 통계 리셋을 지원하지 않는다. 리셋과 가장 가까운 경로들은 다음과 같다.
transaction_statistic::extend는 새로 늘어난 시트 슬롯을 위해 새 통계 인스턴스(new statistic_type[to])를 할당한다. 새것은 0에서 시작한다.- 재사용된 시트에 대한
transaction_sheet_manager::start_watch는 시트의 카운터를 초기화하지 않는다. 시작 카운트만 올린다. 호출자가 시작 시점 스냅샷과 종료 시점 스냅샷을 찍고 스스로 차분을 구해야 한다. 헤더 텍스트가 정확히 그렇게 요구한다. - C
perf_monitor.c에는 인메모리 배열을 비우는 리셋 경로(perfmon_get_stats_and_clear)가 있다. 클라이언트 측의RESET STATISTICS패밀리와 세션별 통계 블록이 이를 호출한다.
스냅샷 원자성 문제 — SHOW STATS가 일관된 카운터 집합을 볼 수 있는가. 는 MySQL/Oracle과 같은 방식으로 답한다. 없다. 각 카운터는 독립적으로 읽힌다. fetch 루프는 등록들을 순차로 걷고, 두 읽기 사이에 쓰기가 발생할 수 있다. 소비자(statdump, 대시보드)는 장기간 누적된 집계를 보고 있으므로, 카운터당 이벤트 하나의 경합은 노이즈 범위 안에 있다.
서브시스템별 모니터 — ovfp_threshold_mgr
섹션 제목: “서브시스템별 모니터 — ovfp_threshold_mgr”모든 모니터링 관심사가 카운터에 들어맞지는 않는다. monitor_vacuum_ovfp_threshold.{cpp,hpp} 뒤의 vacuum 오버플로우 페이지 임계치 추적기가 전형적인 예다. 이 모듈이 답하는 질문은 이것이다. 어떤 (클래스, 인덱스) 쌍이 설정된 수의 오버플로우 페이지 읽기를 초과했으며, 그것이 언제 일어났는가? 답은 레코드의 정렬된 목록이지 카운터가 아니다. 따라서 이 모듈은 모니터에 등록하는 대신 자체 자료구조를 소유한다.
헤더는 서버 전용이다.
#if !defined (SERVER_MODE)#error Belongs to server module#endif
class ovfp_monitor_lock final{#define LOCK_FREE_OWNER_ID (-1)#define LOCK_ALL_OWNER_ID (VACUUM_MAX_WORKER_COUNT)#define LOCK_ITEMS_SIZE (VACUUM_MAX_WORKER_COUNT) private: std::mutex m_ovfp_monitor_mutex; int m_lock_arr[LOCK_ITEMS_SIZE]; public: void lock (int lock_index, int owner_id); void unlock (int lock_index, int owner_id);};세 개의 클래스가 구현을 층층이 쌓는다. ovfp_threshold는 워커별로 (BTID, OID, 최근 페이지, 최대 페이지, 최근 시각, 최대 시각, 히트 수) 레코드의 연결 리스트다. ovfp_printer는 여기에 정렬과 덤프 시점의 마스터 병합 경로를 더한다. ovfp_threshold_mgr는 vacuum 워커별 배열을 펼쳐내는 단일 코디네이터다.
// ovfp_threshold_mgr — monitor_vacuum_ovfp_threshold.hppclass ovfp_threshold_mgr{ private: ovfp_monitor_lock m_ovfp_lock; ovfp_threshold m_ovfp_threshold[VACUUM_MAX_WORKER_COUNT];
UINT64 m_over_secs; char m_since_time[32]; int m_threshold_pages; public: void init (); void add_read_pages_count (THREAD_ENTRY *thread_p, int worker_idx, BTID *btid, int npages); void dump (THREAD_ENTRY *thread_p, FILE *outfp); inline int get_threshold_page_cnt () const { return m_threshold_pages; }};단일 전역 인스턴스는 vacuum.c에 있다.
// g_ovfp_threshold_mgr — query/vacuum.cclass ovfp_threshold_mgr g_ovfp_threshold_mgr;vacuum 워커는 스레드별 페이지 카운트가 임계치를 넘을 때마다 오버플로우 페이지를 읽을 때 여기 호출한다.
if (thread_p->read_ovfl_pages_count >= g_ovfp_threshold_mgr.get_threshold_page_cnt ()) { g_ovfp_threshold_mgr.add_read_pages_count (thread_p, worker->idx, btid_int.sys_btid, thread_p->read_ovfl_pages_count); }세 가지 설계 선택이 핵심이다. 워커별 분할 — m_ovfp_threshold[VACUUM_MAX_WORKER_COUNT]가 각 워커에게 자체 목록과 락 슬롯을 제공해, add_info가 형제 워커에 의해 블록되지 않는다. 덤프 시점 붕괴 — dump는 LOCK_ALL_OWNER_ID로 모든 워커를 잠그고, 엔트리를 단일 ovfp_printer로 접어 중복 (BTID, OID) 튜플을 hit_cnt 합산과 max(read_pages[MAX_POS]) 취득으로 병합하고, 최근 시각 순으로 정렬해 출력한다. hold-and-sleep 뮤텍스 — ovfp_monitor_lock::lock은 std::mutex 위에 손으로 만든 토큰 락이다.
// ovfp_monitor_lock::lock — monitor_vacuum_ovfp_threshold.cppvoid ovfp_monitor_lock::lock (int lock_index, int owner_id){ m_ovfp_monitor_mutex.lock(); while (m_lock_arr[lock_index] != LOCK_FREE_OWNER_ID) { m_ovfp_monitor_mutex.unlock(); usleep (1); m_ovfp_monitor_mutex.lock(); } m_lock_arr[lock_index] = owner_id; m_ovfp_monitor_mutex.unlock();}전역 뮤텍스 아래에서 정수 소유권 토큰을 지키고 경합 시 양보한다. 이론적으로는 최적이 아니지만 실제로는 무해하다. 경합은 워커-i 핫 패스와 덤프 시 전체 워커 잠금 사이에서만 발생하고, 덤프는 대역 외에서 실행된다.
사용자가 보는 표면은 고정 형식의 덤프다. m_since_time(init이 설정)과 설정된 m_threshold_pages(PRM_ID_VACUUM_OVFP_CHECK_THRESHOLD)가 헤더를 이루고, m_over_secs(PRM_ID_VACUUM_OVFP_CHECK_DURATION)보다 오래된 레코드는 덤프 시점에 check_over_duration_times로 제거된다.
SHOW 통합
섹션 제목: “SHOW 통합”SQL로의 연결은 SHOW 패밀리로 이뤄진다. 디스패치 아키텍처는 cubrid-show-commands.md에 있다. 모니터링을 건드리는 SHOW 타입들은 S_SHOWSTMT_SCAN을 쓰고 PSTAT 슬롯에서 읽는다. SHOWSTMT_PAGE_BUFFER_STATUS는 PSTAT_PB_DIRTY_CNT/PSTAT_PB_LRU*_CNT/PSTAT_PB_VICT_CAND를 들여다보고, SHOWSTMT_TRAN_TABLES는 트랜잭션 디스크립터 테이블을 걷고, SHOWSTMT_THREADS는 워커 풀 상태와 스레드당 pstat_Global 엿보기를 읽는다. SHOW EXEC STATISTICS는 C++ 모니터가 아닌 C 배열을 perfmon_get_stats + perfmon_calc_diff_stats로 세션 범위의 델타를 계산한다. 두 계층은 현재 출력을 합치지 않는다. 아키텍처적 의도는 C++ 계층이 시간이 지나면서 C 계층을 흡수하는 것이지만, 마이그레이션은 점진적으로 진행 중이다.
pstat_Metadata와 C 측 표면
섹션 제목: “pstat_Metadata와 C 측 표면”C 카탈로그는 PERF_STAT_ID로 인덱스된 단일 정적 배열 PSTAT_METADATA pstat_Metadata[]다.
// pstat_metadata — base/perf_monitor.hstruct pstat_metadata{ PERF_STAT_ID psid; const char *stat_name; PSTAT_VALUE_TYPE valtype; // ACCUMULATE_SINGLE / PEEK_SINGLE / // COUNTER_TIMER / COMPUTED_RATIO / COMPLEX int start_offset; // 시작 시 계산 int n_vals; PSTAT_DUMP_IN_FILE_FUNC f_dump_in_file; PSTAT_DUMP_IN_BUFFER_FUNC f_dump_in_buffer; PSTAT_LOAD_FUNC f_load;};start_offset/n_vals는 perfmon_initialize에서 채워져 카운터들이 하나의 연속된 UINT64 버퍼(pstat_Global.global_stats)에 살게 된다. PSTAT_COUNTER_TIMER_VALUE 행은 PSTAT_COUNTER_TIMER_*_VALUE 매크로가 배치한 네 개의 슬롯(횟수, 합계, 최대값, 파생 평균)을 차지한다. Num_*/Total_time_*/Max_time_*/Avg_time_* 명명이 cubmonitor의 register_to_monitor와 일치한다. 이 시각적 연속성이 statdump 출력에서 두 계층을 구별할 수 없게 만든다.
PSTAT_COMPLEX_VALUE 행은 손으로 만든 덩어리다. PSTAT_PBX_FIX_COUNTERS는 PERF_PAGE_FIX_STAT_OFFSET으로 인덱스된 모듈 × 페이지_타입 × 페이지_모드 × 래치 × 조건 차원의 평면 배열로, 커스텀 덤프/로드 콜백을 가진다. 단일 PSTAT 행이 enum을 부풀리지 않고 수백 개의 차원을 노출한다. 페이지 버퍼 핫 패스는 perfmon_add_stat_at_offset(thread_p, psid, offset, amount)로 pstat_Global.global_stats[start_offset + offset]에 쓴다. CBRD-26177 지시사항은 커넥션 워커 디스패치 루프에서 이런 호출을 금지한다. 요청이 작업 실행 스레드에 올라온 뒤라면 fix당 계측은 괜찮다.
flowchart TB
subgraph "C 측 (perf_monitor)"
META["pstat_Metadata[PSTAT_∗]
{이름, 값타입, 오프셋, 슬롯 수}"]
GLOBAL["pstat_Global.global_stats[]
연속 UINT64 버퍼"]
META -->|기술| GLOBAL
end
subgraph "C++ 측 (cubmonitor)"
REG["m_registrations[]
fetch_function + 개수 + 이름들"]
STATS["accumulator/gauge/max/min
기본 요소 + transaction_statistic"]
REG -.->|fetch_function 호출| STATS
end
subgraph "서브시스템별"
OVFP2["ovfp_threshold_mgr
m_ovfp_threshold[VACUUM_MAX_WORKER_COUNT]"]
end
HOT2["핫 패스
pgbuf_fix, lock_object,
btree_∗, qexec_∗"]
HOT2 -->|perfmon_add_stat[_at_offset]| GLOBAL
HOT2 -->|stat.collect / autotimer| STATS
HOT2 -->|add_read_pages_count| OVFP2
CONS["소비자
SHOW STATS / EXEC STATISTICS
statdump / sysprm
추적 기능
ovfp 덤프"]
CONS -->|perfmon_get_stats / calc_diff_stats| GLOBAL
CONS -.->|monitor::fetch_*| REG
CONS -->|ovfp_threshold_mgr::dump| OVFP2
소스 코드 가이드
섹션 제목: “소스 코드 가이드”아래 심볼들이 기준 닻이다. 줄 번호는 이 문서의 updated: 날짜 기준 힌트다.
정의와 캐스트 (monitor_definition.hpp, monitor_statistic.{hpp,cpp}): cubmonitor::statistic_value(64비트 슬롯 타입), clock_type/time_point/duration(chrono 별칭), FETCH_GLOBAL/FETCH_TRANSACTION_SHEET를 가진 fetch_mode, amount_rep/floating_rep/time_rep, statistic_value_cast/amount_rep_cast/floating_rep_cast/time_rep_cast(시간 왕복 변환은 의도적으로 손실 있음).
기본 통계 (monitor_statistic.hpp): primitive<Rep>(단일 값), atomic_primitive<Rep>(원자적 대응), atomic_primitive<time_rep>(특수화 — duration은 사소하게 원자적이지 않음). 하위 클래스: accumulator_statistic/accumulator_atomic_statistic(더하기), gauge_statistic/gauge_atomic_statistic(덮어쓰기), max_statistic/max_atomic_statistic(CAS 루프), min_statistic/min_atomic_statistic. 별칭들이 (표현 × 종류 × 원자?) 모든 조합을 열거한다.
그룹 통계 (monitor_collect.hpp): timer(RAII 클록 스냅샷), timer_statistic<T>, counter_timer_statistic<A,T>, counter_timer_max_statistic<A,T,M>, autotimer RAII 내부 클래스, register_to_monitor, build_name_vector. 별칭: counter_timer_stat, atomic_counter_timer_stat, timer_stat, transaction_timer_stat, transaction_atomic_timer_stat.
레지스트리 (monitor_registration.{hpp,cpp}): monitor, fetch_function, registration, register_statistics(count, fetch_f, names)와 템플릿 오버로드, add_registration, allocate_statistics_buffer, fetch_global_statistics, fetch_transaction_statistics, fetch_statistics, get_statistics_count/get_registered_count/get_statistic_name, get_global_monitor.
트랜잭션별 시트 (monitor_transaction.{hpp,cpp}): transaction_sheet(size_t), INVALID_TRANSACTION_SHEET/MAX_SHEETS = 1024를 가진 transaction_sheet_manager, start_watch/end_watch/get_sheet/static_init, 내부 상태 s_current_sheet_count/s_sheet_start_count[]/s_transaction_sheets[]/s_sheets_mutex. collect/extend를 가진 transaction_statistic<S> 래퍼.
Vacuum 오버플로우 페이지 임계치 (monitor_vacuum_ovfp_threshold.{hpp,cpp}): ovfp_monitor_lock(워커별 토큰 뮤텍스), INDEX_OVFP_INFO(BTID,OID당 레코드), ovfp_threshold(add/find/alloc_info_mem/free_info_mem/check_over_duration_times/set_worker_idx), ovfp_printer(sort/add_info 병합), ovfp_threshold_mgr(init/add_read_pages_count/dump/get_classoid/get_class_name_index_name/time_to_string/print), query/vacuum.c에 선언된 전역 g_ovfp_threshold_mgr.
C 측 perfmon (교차 참조): PERF_STAT_ID enum, pstat_Metadata[], PSTAT_METADATA struct, PSTAT_VALUE_TYPE enum, pstat_Global.global_stats[]. 쓰기 경로 perfmon_inc/perfmon_add_stat/perfmon_add_stat_at_offset/perfmon_add_at_offset; 읽기 경로 perfmon_get_stats/perfmon_calc_diff_stats/perfmon_calc_diff_stats_for_trace. PSTAT_COMPLEX_VALUE 콜백으로 f_{load,dump_in_file,dump_in_buffer}_Num_data_page_fix_ext.
위치 힌트 (이 개정판 기준)
섹션 제목: “위치 힌트 (이 개정판 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
statistic_value, FETCH_GLOBAL/FETCH_TRANSACTION_SHEET | src/monitor/monitor_definition.hpp | 33, 42–43 |
primitive<Rep>, atomic_primitive<Rep>, atomic_primitive<time_rep> | src/monitor/monitor_statistic.hpp | 127, 169, 220 |
accumulator_statistic::collect, max_atomic_statistic::collect | src/monitor/monitor_statistic.hpp | 494, 537 |
statistic_value_cast(time_rep), accumulator_atomic_statistic<time_rep>::collect | src/monitor/monitor_statistic.cpp | 67, 169 |
timer, timer_statistic<T>, counter_timer_statistic<A,T>, counter_timer_max_statistic<A,T,M> | src/monitor/monitor_collect.hpp | 37, 80, 132, 194 |
counter_timer_statistic::register_to_monitor, counter_timer_max_statistic::register_to_monitor | src/monitor/monitor_collect.hpp | 388, 519 |
build_name_vector (기반) | src/monitor/monitor_collect.cpp | 26 |
monitor 클래스 | src/monitor/monitor_registration.hpp | 65 |
monitor::fetch_statistics, fetch_transaction_statistics, register_statistics, get_global_monitor | src/monitor/monitor_registration.cpp | 92, 109, 138, 159 |
transaction_sheet_manager, transaction_statistic<S>, transaction_statistic::extend, ::collect | src/monitor/monitor_transaction.hpp | 54, 99, 152, 257 |
transaction_sheet_manager::start_watch/end_watch/get_sheet | src/monitor/monitor_transaction.cpp | 71, 128, 179 |
ovfp_monitor_lock, ovfp_threshold, ovfp_threshold_mgr | src/monitor/monitor_vacuum_ovfp_threshold.hpp | 35, 75, 112 |
ovfp_monitor_lock::lock, ovfp_threshold::add/find/check_over_duration_times | src/monitor/monitor_vacuum_ovfp_threshold.cpp | 50, 99, 169, 263 |
ovfp_printer::sort/add_info, ovfp_threshold_mgr::init/add_read_pages_count/dump | src/monitor/monitor_vacuum_ovfp_threshold.cpp | 303, 351, 570, 580, 596 |
g_ovfp_threshold_mgr 전역 인스턴스 | src/query/vacuum.c | 663 |
pstat_metadata, PSTAT_VALUE_TYPE, PSTAT_COUNTER_TIMER_*_VALUE 매크로 | src/base/perf_monitor.h | 724, ~700, 140–143 |
perfmon_get_stats, perfmon_calc_diff_stats, perfmon_add_stat_at_offset | src/base/perf_monitor.c | 792, 1404, 3481 |
ct_stat_type = atomic_counter_timer_stat | src/base/lockfree_hashmap.hpp | 114 |
교차 검증 메모
섹션 제목: “교차 검증 메모”두 계층이 공존하며 부분적으로 겹친다. cubmonitor(C++ 템플릿)와 perf_monitor(C 배열)는 같은 바이너리 안에 있는 별개의 카운터 시스템이다. 명명 규칙은 공유되지만(Num_*, Total_time_*, Max_time_*, Avg_time_*) 저장소는 독립적이다. SHOW EXEC STATISTICS는 C 배열을 읽는다. C++ monitor::fetch_global_statistics는 접근 가능하지만 소수의 서브시스템(lockfree_hashmap 및 몇 가지 호출 지점)만이 데이터를 채운다. 어떤 배열을 말하는지 독자가 명확히 해야 한다.
atomic_primitive<floating_rep>는 게이트로 막혀 있다. 헤더 가드 MONITOR_ENABLE_ATOMIC_FLOATING_REP가 원자적 부동소수점 통계를 컴파일 타임 스위치 뒤에 둔다. std::atomic<double>::fetch_add는 C++20이고 프로젝트는 아직 C++17을 쓴다. CAS 루프는 완결성을 위해 구현되어 있지만 컴파일되지 않는다.
스냅샷 일관성은 명시적으로 약하다. monitor::fetch_statistics는 전역 락 없이 등록들을 걷는다. 원자적 통계는 카운터별 일관성을 제공한다. 카운터 간 일관성은 없다. 의도된 소비자는 델타 소비자다(t1에 읽고, t2에 읽고, 뺀다).
시트는 재사용되며 값을 이어받는다. transaction_sheet_manager::start_watch는 시작 카운트가 0에 도달한 시트 슬롯을 재사용하지만, 재사용된 시트의 transaction_statistic 슬롯은 초기화되지 않는다. 새 감시자는 시작 시점에 자체 스냅샷을 찍어야 한다.
transaction_statistic::extend는 매니저당이 아니라 인스턴스당이다. 각 transaction_statistic<S>는 자체 m_sheet_stats[]와 m_extend_mutex를 가진다. 처음으로 시트 번호 k를 만나면 배열이 k+1로 늘어난다. 첫 번째 접근은 할당 비용을 치르고 이후 collect는 증가만 한다.
ovfp_monitor_lock은 hold-and-sleep이지 스핀이 아니다. usleep(1)은 바쁜 대기 대신 양보한다. 경합이 구조적으로 드물기 때문에(워커-i 핫 패스 대 덤프 시 전체 워커 잠금 사이에서만 발생) 여기서는 괜찮다. 커넥션 워커 핫 패스에는 맞지 않는다.
모니터 수명은 서버 수명이다. C++ 모니터에는 unregister_statistics가 없다. 등록된 통계는 this를 참조로 캡처하며 전역 모니터보다 오래 살아야 한다. 이 수명 계약은 타입 시스템이 강제하지 않는다.
time_rep_cast(statistic_value_cast(stat)) != stat이다. 시간 값은 마이크로초로 내보내고 나노초로 다시 들여오므로 서브마이크로초 정밀도를 잃는다. monitor_statistic.cpp에 문서화되어 있다.
C perf_monitor는 자체 차원을 가진다. PSTAT_PBX_FIX_COUNTERS 같은 PSTAT_COMPLEX_VALUE 행은 5차원 공간(모듈 × 페이지_타입 × 페이지_모드 × 래치 × 조건)을 하나의 슬롯 범위로 펼친다. C++ 모니터에는 동등한 것이 없다. 다차원 통계는 별개의 등록을 여럿 두거나 커스텀 fetch 람다가 필요하다.
CBRD-26177 지시사항은 커밋 이력에 산다. 커넥션 워커 핫 패스에 perfmon 없음 규칙은 워커 풀 디스패치 루프 안에 perfmon_inc/perfmon_add_* 호출이 없다는 사실로 나타난다. 그 제약이 의도적이라는 사실을 모르면, 나중에 계측을 추가하는 기여자가 이를 모를 수 있다.
열린 질문들
섹션 제목: “열린 질문들”C perf_monitor 배열이 cubmonitor로 마이그레이션될 것인가? C++ 설계가 구조적으로 더 깔끔하다. 타입 안전하고, 조합 가능하고, 시트도 갖춘다. 그러나 C 배열에는 pstat_Metadata 행이 있는 수백 개의 카운터가 있다. 점진적 마이그레이션은 SHOW STATS 출력을 분열시킬 위험이 있고, 일괄 전환은 몇 달 치 작업이다. 현재의 두 계층 상태가 영구적인지 과도기적인지 소스에 확정된 것이 없다.
transaction_statistic이 선제적으로 확장되어야 하는가? 동적 크기 조정은 맞지만 각 시트 번호가 처음 나타날 때 핫 패스에 할당을 발생시킨다. MAX_SHEETS = 1024로 미리 할당하면 서버 시작 메모리가 늘지만 크기 조정을 없앨 수 있다. SHOW EXEC STATISTICS가 세션당 항상 켜지는 방향으로 간다면 재검토할 가치가 있다.
MAX_SHEETS = 1024가 올바른 상한인가? 헤더 주석은 줄이는 것을 고려할 만하다고 적어 두었다. 1024는 동시 감시자에게 넉넉하지만 통계당 메모리를 1024 * sizeof(S)로 제한한다. NUM_NORMAL_TRANS에 묶인 설정 가능한 상한이 더 나은 기본값일 수 있다.
ovfp_threshold_mgr가 락프리 리스트를 써야 하는가? 워커별 락이 워커 간 경합을 피한다. usleep은 드문 충돌에 필요 이상으로 무겁다. 워커당 락프리 연결 리스트가 더 빠르겠지만 경합 빈도가 구조적으로 낮다. 덤프 시점에만 발생하기 때문이다.
상시 카운터 위에 샘플링을 더해야 하는가? cubmonitor는 순수하게 카운터 형태다. CUBRID의 샘플링 방식 관찰 가능성은 SQL 추적과 브로커 SQL 로그에 모니터링 서브시스템 밖에 있다. 샘플링을 같은 우산 아래 가져올지는 미결이다.
C++ 모니터가 unregister_statistics를 지원해야 하는가? 오늘의 람다-참조-캡처 모델은 동적 등록을 금지한다. 미래 서브시스템이 더 동적이 된다면(로드 가능 플러그인, 데이터베이스별 계측), 이 모델이 부채가 된다.
src/monitor/ 아래의 주요 헤더와 구현:
monitor_definition.hpp—statistic_value,clock_type,fetch_mode, 기초 타입 정의.monitor_statistic.hpp/monitor_statistic.cpp— 기본 및 원자적 통계, 누적기/게이지/최대/최소, 타입 캐스트.monitor_collect.hpp/monitor_collect.cpp— 그룹 통계(타이머, 카운터+타이머, 카운터+타이머+최대값),register_to_monitor,build_name_vector.monitor_registration.hpp/monitor_registration.cpp—monitor레지스트리 클래스와 파일 범위 전역 인스턴스.monitor_transaction.hpp/monitor_transaction.cpp—transaction_sheet_manager,transaction_statistic<S>.monitor_vacuum_ovfp_threshold.hpp/monitor_vacuum_ovfp_threshold.cpp— vacuum 워커별 오버플로우 페이지 임계치 추적기.
엔진 내부 교차 참조:
src/base/perf_monitor.h/src/base/perf_monitor.c— 오래된 C 카운터 배열,PSTAT_*enum,pstat_Metadata[],perfmon_*쓰기/읽기 API.src/base/perf.cpp/src/base/perf_monitor_trackers.cpp— perf 유틸리티와 트래커.src/base/lockfree_hashmap.hpp—cubmonitor::atomic_counter_timer_stat소비 예시.src/query/vacuum.c—g_ovfp_threshold_mgr를 선언하고 vacuum 오버플로우 페이지 읽기 경로에서add_read_pages_count를 호출.src/base/system_parameter.{c,h}—PRM_ID_VACUUM_OVFP_CHECK_DURATION,PRM_ID_VACUUM_OVFP_CHECK_THRESHOLD정의.
이 지식 베이스의 관련 문서:
cubrid-show-commands.md— SHOW STATS, SHOW EXEC STATISTICS, SHOW THREADS, SHOW PAGE BUFFER STATUS가S_SHOWSTMT_SCAN으로 카운터 배열에 도달하는 방식.cubrid-vacuum.md—g_ovfp_threshold_mgr가 보완하는 vacuum 서브시스템 아키텍처.cubrid-thread-worker-pool.md— 어떤 카운터가 어디서 살아남았는지를 결정한 커넥션 워커 핫 패스에 perfmon 없음 지시사항을 낳은 워커 풀 재설계.
JIRA 참조:
- CBRD-26177 — 커넥션 워커 핫 패스에서 perfmon 증가를 제거한 지시사항. 재설계된 워커 풀 아래 공유 카운터 캐시 라인 경합이 이유다.