샘플링 쿼리 프로파일러

샘플링 쿼리 프로파일러 (Sampling query profiler)

ClickHouse는 쿼리 실행을 분석할 수 있는 샘플링 프로파일러를 실행해요. 프로파일러를 사용해 쿼리 실행 중 가장 자주 사용되는 소스 코드 루틴을 찾을 수 있고, CPU 시간과 대기 시간을 포함한 wall-clock 시간을 추적할 수 있습니다.

출처: 문서

본문

ClickHouse는 쿼리 실행을 분석할 수 있는 샘플링 프로파일러를 실행합니다. 프로파일러를 사용해 쿼리 실행 중 가장 자주 사용되는 소스 코드 루틴을 찾을 수 있어요. 대기 시간을 포함한 CPU 시간과 wall-clock 시간을 추적할 수 있습니다.

쿼리 프로파일러는 ClickHouse Cloud에서 자동으로 활성화됩니다.

다음 예시 쿼리는 프로파일링된 쿼리에 대해 함수 이름과 소스 위치가 해석된 가장 빈번한 스택 트레이스를 찾습니다. 기본적으로 프로파일러는 수집 시점에 스택 트레이스를 심볼화(symbolize)하고 결과를 system.trace_logsymbolslines 컬럼에 저장합니다. 따라서 아래 예시는 그 컬럼들을 직접 읽으며 introspection 함수가 필요하지 않습니다. 심볼화는 trace_log 서버 설정 섹션의 symbolize 설정(기본 활성화)에 의해 제어되며 ELF 플랫폼(예: Linux)과 macOS에서 지원됩니다. FreeBSD에서는 symbolslines 컬럼이 항상 비어 있습니다. symbols의 함수 이름은 바이너리의 심볼 테이블에서 나오며 기본적으로 사용 가능합니다. lines의 소스 위치는 best-effort입니다. 디버그 정보(macOS에서는 바이너리 옆 .dSYM 번들)가 필요하며, ELF 플랫폼에서는 메인 ClickHouse 바이너리 안의 프레임만 해석되므로 해석할 수 없는 프레임(예: 공유 라이브러리)의 항목은 비어 있게 됩니다. 심볼화가 비활성화되면 addressToSymbol, demangle, addressToLine introspection 함수를 사용해 trace 컬럼의 원시 주소를 대신 해석하세요. 이 함수들은 심볼화와 같은 플랫폼(ELF 플랫폼, 예: Linux, 그리고 macOS)에서 사용 가능합니다. FreeBSD에서는 둘 다 컴파일되지 않으므로 trace의 주소는 서버 외부에서 해석해야 합니다.

프로파일링하려는 쿼리의 ID로 query_id 값을 바꾸세요.

  • ClickHouse Cloud
  • 자체 관리 (Self-managed)

ClickHouse Cloud에서 쿼리 ID는 쿼리 결과 테이블 위의 막대 맨 오른쪽(테이블/차트 토글 옆)에 있는 **"..."**를 클릭해 얻을 수 있습니다. 그러면 문맥 메뉴가 열리며 **"Copy query ID"**를 클릭할 수 있습니다.

클러스터의 모든 노드에서 선택하려면 clusterAllReplicas(default, system.trace_log)를 사용하세요:

SELECT
    count(),
    arrayStringConcat(arrayMap((symbol, line) -> concat(symbol, '\n    ', line), any(symbols), any(lines)), '\n') AS sym
FROM clusterAllReplicas(default, system.trace_log)
WHERE query_id = '<query_id>' AND trace_type = 'CPU' AND event_date = today()
GROUP BY trace
ORDER BY count() DESC
LIMIT 10
SELECT
    count(),
    arrayStringConcat(arrayMap((symbol, line) -> concat(symbol, '\n    ', line), any(symbols), any(lines)), '\n') AS sym
FROM system.trace_log
WHERE query_id = '<query_id>' AND trace_type = 'CPU' AND event_date = today()
GROUP BY trace
ORDER BY count() DESC
LIMIT 10

자체 관리 배포에서 쿼리 프로파일러 사용하기 (Using the query profiler in self-managed deployments)

자체 관리 배포에서 쿼리 프로파일러를 사용하려면 다음 단계를 따르세요:

1

디버그 정보와 함께 ClickHouse 설치하기

clickhouse-common-static-dbg 패키지를 설치합니다:

  1. "Debian 리포지토리 설정" 단계의 지침을 따릅니다
  2. sudo apt-get install clickhouse-server clickhouse-client clickhouse-common-static-dbg를 실행해 디버그 정보가 포함된 ClickHouse 컴파일 바이너리 파일을 설치합니다
  3. sudo service clickhouse-server start를 실행해 서버를 시작합니다
  4. clickhouse-client를 실행합니다. clickhouse-common-static-dbg의 디버그 심볼은 서버가 자동으로 가져오므로, 활성화를 위해 특별히 할 일이 없습니다

2

서버 설정 확인하기

서버 설정 파일trace_log 섹션이 설정되어 있는지 확인합니다. 기본적으로 활성화되어 있습니다:

<!-- Trace log. Stores stack traces collected by query profilers.
     See query_profiler_real_time_period_ns and query_profiler_cpu_time_period_ns settings. -->
<trace_log>
    <database>system</database>
    <table>trace_log</table>

    <partition_by>toYYYYMM(event_date)</partition_by>
    <flush_interval_milliseconds>7500</flush_interval_milliseconds>
    <max_size_rows>1048576</max_size_rows>
    <reserved_size_rows>8192</reserved_size_rows>
    <buffer_size_rows_flush_threshold>524288</buffer_size_rows_flush_threshold>
    <!-- Indication whether logs should be dumped to the disk in case of a crash -->
    <flush_on_crash>false</flush_on_crash>
    <symbolize>true</symbolize>
</trace_log>

이 섹션은 프로파일러 동작 결과를 포함하는 trace_log 시스템 테이블을 구성합니다.

symbolize 옵션(기본 활성화)은 ClickHouse가 수집 시점에 각 스택 프레임을 해석하고, demangle된 함수 이름과 소스 위치를 symbolslines 컬럼에 저장하게 만듭니다.

symbols의 함수 이름은 심볼 테이블에서 나오며 기본적으로 사용 가능하지만, lines의 소스 위치는 디버그 정보(macOS에서는 .dSYM 번들)가 필요하며 ELF 플랫폼에서는 메인 ClickHouse 바이너리 안의 프레임에 대해서만 해석됩니다. 해석되지 않은 프레임은 빈 lines 항목을 가집니다.

trace 컬럼의 원시 주소는 재시작과 업그레이드에 걸쳐 사전 심볼화된 컬럼보다 덜 안정적이라는 점을 기억하세요.

FreeBSD를 제외한 ELF 플랫폼에서 메인 ClickHouse 바이너리의 프레임은 물리적 파일 오프셋으로 저장되므로, 바이너리가 변경되지 않는 한 재시작 후에도 해석할 수 있습니다. macOS와 FreeBSD에서는 런타임 가상 주소로 저장되어 재시작 후 유효하지 않을 수 있습니다.

메인 바이너리 외부의 프레임(예: 공유 라이브러리)은 항상 재시작 후 유효하지 않을 수 있는 런타임 가상 주소로 저장되며, 바이너리 업그레이드 후에는 코드 레이아웃이 바뀌므로 어떤 원시 주소든 해석할 수 없게 됩니다.

ClickHouse는 재시작 시 테이블을 정리하지 않으므로 오래된 원시 주소가 남을 수 있습니다.

반면에 사전 심볼화된 symbolslines 컬럼은 재시작과 업그레이드에 걸쳐 유효하게 유지되므로, 과거 데이터를 분석할 때는 이 컬럼들을 선호하세요.

3

프로파일 타이머 구성하기

query_profiler_cpu_time_period_ns 또는 query_profiler_real_time_period_ns 설정을 구성합니다.

두 설정은 동시에 사용할 수 있습니다. 이러한 설정으로 프로파일러 타이머를 구성할 수 있어요.

세션 설정이므로 서버 전체, 개별 사용자나 사용자 프로필, 대화형 세션, 개별 쿼리별로 서로 다른 샘플링 빈도를 얻을 수 있습니다.

기본 샘플링 빈도는 초당 1개 샘플이며, CPU와 real 타이머가 모두 활성화되어 있습니다. 이 빈도는 서버 성능에 영향을 주지 않으면서 ClickHouse 클러스터에 대한 충분한 정보를 수집하게 해 줍니다.

개별 쿼리를 각각 프로파일링해야 한다면 더 높은 샘플링 빈도를 사용하세요.

4

trace_log 시스템 테이블 분석하기

어떤 쿼리에 대한 프로파일을 얻으려면 trace_log 테이블의 데이터를 집계해야 합니다. 개별 함수별로 또는 전체 스택 트레이스별로 데이터를 집계할 수 있습니다.

심볼화가 활성화되어 있으면(기본값), demangle된 함수 이름과 소스 위치가 이미 symbolslines 컬럼에 있으므로 추가 설정이 필요 없습니다. 심볼화는 FreeBSD에서 지원되지 않으며, 그곳에서는 이 컬럼들이 항상 비어 있습니다. 디버그 정보가 없거나 메인 ClickHouse 바이너리 밖에 있는 프레임은 lines 항목이 비어 있을 수 있습니다( 참고).

심볼화가 비활성화되어 있거나 trace 컬럼의 원시 주소를 즉석에서 해석하고 싶다면(예: 인라인 프레임을 펼치려면), allow_introspection_functions 설정으로 introspection 함수를 허용하세요:

SET allow_introspection_functions=1

보안상의 이유로 introspection 함수는 기본적으로 비활성화되어 있습니다.

ClickHouse 코드에서 함수 이름과 위치를 얻으려면 addressToLine, addressToLineWithInlines, addressToSymbol, demangle introspection 함수를 사용하세요. 심볼화와 마찬가지로 이 함수들은 ELF 플랫폼(예: Linux)과 macOS에서 사용할 수 있지만 FreeBSD에서는 사용할 수 없습니다.

trace_log 정보를 시각화해야 한다면 flamegraphspeedscope를 시도해 보세요.

flameGraph 함수로 플레임 그래프 만들기 (Building flame graphs with the flameGraph function)

ClickHouse는 trace_log에 저장된 스택 트레이스에서 직접 플레임 그래프를 만드는 flameGraph 집계 함수를 제공합니다.

출력은 flamegraph.pl과 호환되는 형식의 문자열 배열입니다.

구문:

flameGraph(traces, [size = 1], [ptr = 0])

인자:

  • traces — 스택트레이스. Array(UInt64).
  • size — 메모리 프로파일링을 위한 할당 크기. Int64.
  • ptr — 할당 주소. UInt64.

ptr이 0이 아닐 때, flameGraph는 같은 크기와 포인터를 가진 할당(size > 0)과 해제(size < 0)를 매핑합니다. 해제되지 않은 할당만 표시됩니다. 매칭되지 않는 해제는 무시됩니다.

CPU 플레임 그래프 (CPU flame graph)

아래 쿼리들은 flamegraph.pl이 설치되어 있어야 합니다. 다음을 실행해 설치할 수 있습니다:

git clone https://github.com/brendangregg/FlameGraph
# Then use it as:
# ~/FlameGraph/flamegraph.pl

아래 쿼리에서 flamegraph.pl을 머신에서 flamegraph.pl이 위치한 경로로 바꾸세요

SET query_profiler_cpu_time_period_ns = 10000000;

쿼리를 실행한 다음 플레임 그래프를 만듭니다:

clickhouse client --allow_introspection_functions=1 \
    -q "SELECT arrayJoin(flameGraph(arrayReverse(trace)))
        FROM system.trace_log
        WHERE trace_type = 'CPU' AND query_id = '<query_id>'" \
    | flamegraph.pl > flame_cpu.svg

메모리 플레임 그래프 — 모든 할당 (Memory flame graph — all allocations)

SET memory_profiler_sample_probability = 1, max_untracked_memory = 1;

쿼리를 실행한 다음 플레임 그래프를 만듭니다:

clickhouse client --allow_introspection_functions=1 \
    -q "SELECT arrayJoin(flameGraph(trace, size))
        FROM system.trace_log
        WHERE trace_type = 'MemorySample' AND query_id = '<query_id>'" \
    | flamegraph.pl --countname=bytes --color=mem > flame_mem.svg

메모리 플레임 그래프 — 해제되지 않은 할당 (Memory flame graph — unfreed allocations)

이 변형은 포인터로 할당을 해제와 매칭하고 쿼리 동안 해제되지 않은 메모리만 표시합니다.

SET memory_profiler_sample_probability = 1, max_untracked_memory = 1,
    use_uncompressed_cache = 1,
    merge_tree_max_rows_to_use_cache = 100000000000,
    merge_tree_max_bytes_to_use_cache = 1000000000000;

플레임 그래프를 만들기 위해 다음 쿼리를 실행합니다:

clickhouse client --allow_introspection_functions=1 \
    -q "SELECT arrayJoin(flameGraph(trace, size, ptr))
        FROM system.trace_log
        WHERE trace_type = 'MemorySample' AND query_id = '<query_id>'" \
    | flamegraph.pl --countname=bytes --color=mem > flame_mem_unfreed.svg

메모리 플레임 그래프 — 특정 시점의 활성 할당 (Memory flame graph — active allocations at a point in time)

이 접근 방식은 최대 메모리 사용량을 찾고 그 순간에 무엇이 할당되었는지 시각화하게 해 줍니다.

SET memory_profiler_sample_probability = 1, max_untracked_memory = 1;
시간에 따른 메모리 사용량 찾기 (Find memory usage over time)
SELECT
    event_time,
    formatReadableSize(max(s)) AS m
FROM (
    SELECT
        event_time,
        sum(size) OVER (ORDER BY event_time) AS s
    FROM system.trace_log
    WHERE query_id = '<query_id>' AND trace_type = 'MemorySample'
)
GROUP BY event_time
ORDER BY event_time;
최대 메모리 사용량의 시점 찾기 (Find the time point with maximum memory usage)
SELECT
    argMax(event_time, s),
    max(s)
FROM (
    SELECT
        event_time,
        sum(size) OVER (ORDER BY event_time) AS s
    FROM system.trace_log
    WHERE query_id = '<query_id>' AND trace_type = 'MemorySample'
);
그 시점의 활성 할당 플레임 그래프 만들기 (Build a flame graph of active allocations at that time point)
clickhouse client --allow_introspection_functions=1 \
    -q "SELECT arrayJoin(flameGraph(trace, size, ptr))
        FROM (
            SELECT * FROM system.trace_log
            WHERE trace_type = 'MemorySample'
              AND query_id = '<query_id>'
              AND event_time <= '<time_point>'
            ORDER BY event_time
        )" \
    | flamegraph.pl --countname=bytes --color=mem > flame_mem_time_point_pos.svg
그 시점 이후의 해제 플레임 그래프 만들기 (Build a flame graph of deallocations after that time point)

나중에 무엇이 해제되었는지 이해하기 위한 것입니다:

clickhouse client --allow_introspection_functions=1 \
    -q "SELECT arrayJoin(flameGraph(trace, -size, ptr))
        FROM (
            SELECT * FROM system.trace_log
            WHERE trace_type = 'MemorySample'
              AND query_id = '<query_id>'
              AND event_time > '<time_point>'
            ORDER BY event_time DESC
        )" \
    | flamegraph.pl --countname=bytes --color=mem > flame_mem_time_point_neg.svg

예시 (Example)

아래 코드 조각은:

  • trace_log 데이터를 쿼리 식별자와 현재 날짜로 필터링합니다.
  • 사전 심볼화된 symbolslines 컬럼을 읽어 다음의 보고서를 만듭니다:
    • 심볼 이름과 대응하는 소스 코드 함수 이름.
    • 이 함수들의 소스 코드 위치.
  • 원시 스택 트레이스(trace 컬럼)로 집계하고, 심볼화된 컬럼은 표시용으로만 사용하므로, 서로 다른 스택 트레이스가 best-effort 심볼화로 절대 병합되지 않습니다.
SELECT
    count(),
    arrayStringConcat(arrayMap((symbol, line) -> concat(symbol, '\n    ', line), any(symbols), any(lines)), '\n') AS sym
FROM system.trace_log
WHERE (query_id = '<query_id>') AND (event_date = today())
GROUP BY trace
ORDER BY count() DESC
LIMIT 10

더 알아보기 (Learn more)