OpenTelemetry 통합

OpenTelemetry 통합 (Integrating OpenTelemetry)

OpenTelemetry(OTel)를 사용해 로그와 트레이스를 수집하고 ClickHouse로 내보내는 방법을 살펴봐요.

출처: 문서

본문

모든 옵저버빌리티 솔루션은 로그와 트레이스를 수집하고 내보낼 수단이 필요해요. 이를 위해 ClickHouse는 OpenTelemetry(OTel) 프로젝트를 권장해요. "OpenTelemetry는 트레이스, 메트릭, 로그 같은 텔레메트리 데이터를 생성하고 관리하도록 설계된 옵저버빌리티 프레임워크이자 툴킷"이에요. ClickHouse나 Prometheus와 달리 OpenTelemetry는 옵저버빌리티 백엔드가 아니라 텔레메트리 데이터의 생성, 수집, 관리, 내보내기에 초점을 맞춰요. OpenTelemetry의 초기 목표는 언어별 SDK로 애플리케이션이나 시스템을 쉽게 계측하는 것이었지만, 지금은 OTel collector를 통한 로그 수집까지 확장되었어요. collector는 텔레메트리 데이터를 수신, 처리, 내보내는 에이전트 또는 프록시예요.

ClickHouse 관련 컴포넌트

OpenTelemetry는 많은 컴포넌트로 구성돼요. 데이터와 API 스펙, 표준화된 프로토콜, 필드/컬럼 명명 규칙을 제공하는 것 외에도, OTel은 ClickHouse로 옵저버빌리티 솔루션을 구축하는 데 근본적인 두 가지 기능을 제공해요:

  • OpenTelemetry Collector는 텔레메트리 데이터를 수신, 처리, 내보내는 프록시예요. ClickHouse 기반 솔루션은 배칭과 삽입 전에 이 컴포넌트를 로그 수집과 이벤트 처리에 모두 사용해요.
  • 스펙, API, 텔레메트리 데이터 내보내기를 구현하는 언어 SDK. 이 SDK들은 애플리케이션 코드 내에서 트레이스가 올바르게 기록되도록 하고, 구성 스팬을 생성하며, 메타데이터를 통해 서비스 간 컨텍스트가 전파되도록 보장해서 분산 트레이스를 구성하고 스팬을 상관시킬 수 있게 해줘요. 이 SDK들은 일반적인 라이브러리와 프레임워크를 자동으로 구현하는 생태계로 보완되어, 사용자가 코드를 변경하지 않고 즉시 사용 가능한 계측을 얻을 수 있어요.

ClickHouse 기반 옵저버빌리티 솔루션은 이 두 도구를 모두 활용해요.

배포판 (Distributions)

OpenTelemetry collector에는 여러 배포판이 있어요. ClickHouse 솔루션에 필요한 filelog receiver와 ClickHouse exporter는 OpenTelemetry Collector Contrib Distro에만 있어요. 이 배포판은 많은 컴포넌트를 포함해 다양한 구성을 실험할 수 있게 해줘요. 하지만 프로덕션에서 실행할 때는 collector를 환경에 필요한 컴포넌트만 포함하도록 제한하는 것이 권장돼요. 그 이유는:

  • collector 크기를 줄여 배포 시간을 단축
  • 공격 표면을 줄여 collector의 보안 개선

커스텀 collectorOpenTelemetry Collector Builder로 만들 수 있어요.

OTel로 데이터 수집하기

Collector 배포 역할

로그를 수집해 ClickHouse에 삽입하려면 OpenTelemetry Collector를 권장해요. OpenTelemetry Collector는 두 가지 주요 역할로 배포할 수 있어요:

  • 에이전트(Agent) - 에이전트 인스턴스는 에지(서버나 Kubernetes 노드)에서 데이터를 수집하거나, OpenTelemetry SDK로 계측된 애플리케이션에서 이벤트를 직접 받아요. 후자의 경우 에이전트 인스턴스는 애플리케이션과 함께 또는 같은 호스트에서 실행돼요(사이드카나 DaemonSet처럼). 에이전트는 데이터를 ClickHouse에 직접 보내거나 게이트웨이 인스턴스로 보낼 수 있어요. 전자를 Agent 배포 패턴이라 해요.
  • 게이트웨이(Gateway) - 게이트웨이 인스턴스는 독립 실행형 서비스(Kubernetes의 deployment 등)를 제공하며, 보통 클러스터/데이터 센터/리전별로 배포돼요. 이들은 단일 OTLP 엔드포인트를 통해 애플리케이션(또는 에이전트 역할의 다른 collector)에서 이벤트를 받아요. 보통 게이트웨이 인스턴스 집합을 배포하고, 기본 제공 로드 밸런서로 부하를 분산시켜요. 모든 에이전트와 애플리케이션이 이 단일 엔드포인트로 신호를 보내면 Gateway 배포 패턴이라 부르는 경우가 많아요.

아래에서는 이벤트를 ClickHouse로 직접 보내는 단순한 에이전트 collector를 가정할게요. 게이트웨이 사용과 적용 시기에 대한 자세한 내용은 게이트웨이로 확장하기를 참고하세요.

로그 수집

collector를 사용하는 주요 이점은 서비스가 데이터를 빠르게 오프로드할 수 있어서, 재시도, 배칭, 암호화, 민감 데이터 필터링 같은 추가 처리를 collector가 맡도록 할 수 있다는 거예요. Collector는 세 가지 주요 처리 단계에 receiver, processor, exporter라는 용어를 사용해요. Receiver는 데이터 수집에 사용되며 pull 또는 push 기반일 수 있어요. Processor는 메시지에 변환과 강화를 수행할 수 있게 해줘요. Exporter는 데이터를 다운스트림 서비스로 보내는 역할을 해요. 이 서비스는 이론적으로 다른 collector일 수 있지만, 아래 초기 논의에서는 모든 데이터가 ClickHouse로 직접 보내진다고 가정할게요. 사용자에게 전체 receiver, processor, exporter 집합에 익숙해지길 권장해요. collector는 로그 수집을 위한 두 가지 주요 receiver를 제공해요:

OTLP를 통해 - 이 경우 OpenTelemetry SDK에서 OTLP 프로토콜로 로그가 collector에 직접(푸시 방식으로) 보내져요. OpenTelemetry 데모는 이 접근 방식을 사용하며, 각 언어의 OTLP exporter가 로컬 collector 엔드포인트를 가정해요. 이 경우 collector는 OTLP receiver로 구성되어야 해요 — 위 데모의 구성을 참고하세요. 이 접근 방식의 장점은 로그 데이터에 자동으로 Trace Id가 포함되어, 나중에 특정 로그의 트레이스를 찾거나 그 반대도 가능하다는 거예요. 이 접근 방식은 사용자가 적절한 언어 SDK로 코드를 계측해야 해요.

Filelog receiver로 스크래핑 - 이 receiver는 디스크의 파일을 tail 하면서 로그 메시지를 구성해 ClickHouse로 보내요. 이 receiver는 멀티라인 메시지 감지, 로그 롤오버 처리, 재시작 강건성을 위한 체크포인팅, 구조 추출 같은 복잡한 작업을 처리해요. 이 receiver는 Docker와 Kubernetes 컨테이너 로그도 tail 할 수 있고, helm 차트로 배포 가능하며, 이 로그들에서 구조를 추출하고 pod 상세 정보로 강화할 수 있어요.

대부분의 배포는 위 receiver들의 조합을 사용할 거예요. collector 문서를 읽고 기본 개념과 구성 구조, 설치 방법에 익숙해지는 걸 권장해요.

팁: otelbin.io otelbin.io는 구성을 검증하고 시각화하는 데 유용해요.

구조화 vs 비구조화

로그는 구조화되거나 비구조화될 수 있어요. 구조화된 로그는 JSON 같은 데이터 포맷을 사용해 http 코드와 소스 IP 주소 같은 메타데이터 필드를 정의해요.

{
    "remote_addr":"54.36.149.41",
    "remote_user":"-","run_time":"0","time_local":"2019-01-22 00:26:14.000","request_type":"GET",
    "request_path":"\/filter\/27|13 ,27|  5 ,p53","request_protocol":"HTTP\/1.1",
    "status":"200",
    "size":"30577",
    "referer":"-",
    "user_agent":"Mozilla\/5.0 (compatible; AhrefsBot\/6.1; +http:\/\/ahrefs.com\/robot\/)"
}

비구조화 로그는 보통 정규식 패턴으로 추출할 수 있는 몇 가지 내재된 구조가 있지만, 로그를 순수하게 문자열로 표현해요.

54.36.149.41 - - [22/Jan/2019:03:56:14 +0330] "GET
/filter/27|13%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,27|%DA%A9%D9%85%D8%AA%D8%B1%20%D8%A7%D8%B2%205%20%D9%85%DA%AF%D8%A7%D9%BE%DB%8C%DA%A9%D8%B3%D9%84,p53 HTTP/1.1" 200 30577 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" "-"

가능하면 구조화된 로깅을 사용하고 JSON(즉 ndjson)으로 로그를 남기는 것을 권장해요. 이렇게 하면 나중에 필요한 로그 처리가 단순해져요. Collector processors로 ClickHouse에 보내기 전에 처리하거나, 머티어리얼라이즈드 뷰로 삽입 시점에 처리할 수 있어요. 구조화된 로그는 궁극적으로 후속 처리 리소스를 절약하고 ClickHouse 솔루션에 필요한 CPU를 줄여줘요.

예시

예시를 위해 각각 약 1000만 행의 구조화(JSON) 및 비구조화 로깅 데이터셋을 제공해요:

아래 예시는 구조화 데이터셋을 사용해요. 아래 예시를 재현하려면 이 파일을 다운로드하고 압축을 풀어 주세요. 다음은 filelog receiver로 디스크의 이 파일들을 읽고 결과 메시지를 stdout으로 출력하는 OTel Collector의 간단한 구성이에요. 로그가 구조화되어 있으므로 json_parser 연산자를 사용해요. access-structured.log 파일의 경로를 수정해 주세요.

ClickHouse에서 파싱 고려 아래 예시는 로그에서 타임스탬프를 추출해요. 이는 전체 로그 라인을 JSON 문자열로 변환해 결과를 LogAttributes에 넣는 json_parser 연산자를 사용해야 해요. 이는 계산 비용이 들 수 있으며 ClickHouse에서 더 효율적으로 수행할 수 있어요 — SQL로 구조 추출. regex_parser를 사용해 이를 달성하는 동등한 비구조화 예시는 여기에서 찾을 수 있어요.

config-structured-logs.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
processors:
  batch:
    timeout: 5s
    send_batch_size: 1
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [logging]

공식 지침에 따라 collector를 로컬에 설치할 수 있어요. 중요하게, 지침이 contrib 배포판(filelog receiver 포함)을 사용하도록 수정되었는지 확인해 주세요. 예를 들어 otelcol_0.102.1_darwin_arm64.tar.gz 대신 otelcol-contrib_0.102.1_darwin_arm64.tar.gz를 다운로드해요. 릴리스는 여기에서 찾을 수 있어요. 설치가 끝나면 OTel Collector를 다음 명령으로 실행할 수 있어요:

./otelcol-contrib --config config-logs.yaml

구조화 로그를 사용한다고 가정하면 메시지는 출력에서 다음 형태를 취해요:

LogRecord #98
ObservedTimestamp: 2024-06-19 13:21:16.414259 +0000 UTC
Timestamp: 2019-01-22 01:12:53 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str({"remote_addr":"66.249.66.195","remote_user":"-","run_time":"0","time_local":"2019-01-22 01:12:53.000","request_type":"GET","request_path":"\/product\/7564","request_protocol":"HTTP\/1.1","status":"301","size":"178","referer":"-","user_agent":"Mozilla\/5.0 (Linux; Android 6.0.1; Nexus 5X Build\/MMB29P) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/41.0.2272.96 Mobile Safari\/537.36 (compatible; Googlebot\/2.1; +http:\/\/www.google.com\/bot.html)"})
Attributes:
        -> remote_user: Str(-)
        -> request_protocol: Str(HTTP/1.1)
        -> time_local: Str(2019-01-22 01:12:53.000)
        -> user_agent: Str(Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html))
        -> log.file.name: Str(access.log)
        -> status: Str(301)
        -> size: Str(178)
        -> referer: Str(-)
        -> remote_addr: Str(66.249.66.195)
        -> request_type: Str(GET)
        -> request_path: Str(/product/7564)
        -> run_time: Str(0)
Trace ID:
Span ID:
Flags: 0

위는 OTel collector가 생성한 단일 로그 메시지를 나타내요. 이 같은 메시지를 이후 섹션에서 ClickHouse로 수집할게요. 로그 메시지의 전체 스키마와 다른 receiver를 사용할 때 나타날 수 있는 추가 컬럼은 여기에 유지되며, 이 스키마에 익숙해지기를 적극 권장해요. 핵심은 로그 라인 자체가 Body 필드의 문자열로 유지되지만, JSON은 json_parser 덕분에 Attributes 필드로 자동 추출된다는 거예요. 이 연산자는 타임스탬프를 적절한 Timestamp 컬럼으로 추출하는 데도 사용되었어요. OTel로 로그를 처리하는 권장 사항은 Processing을 참고하세요.

연산자(Operators) 연산자는 로그 처리의 가장 기본 단위예요. 각 연산자는 파일에서 줄을 읽거나 필드에서 JSON을 파싱하는 것 같은 단일 책임을 수행해요. 연산자는 파이프라인에서 함께 연결되어 원하는 결과를 얻어요.

위 메시지에는 TraceIDSpanID 필드가 없어요. 분산 트레이싱을 구현하는 경우처럼 존재한다면, 위에서 보여준 것과 같은 기법으로 JSON에서 추출할 수 있어요. 로컬 또는 Kubernetes 로그 파일을 수집해야 하는 사용자에게는 filelog receiver의 구성 옵션과 offsets멀티라인 로그 파싱 처리 방법에 익숙해지는 것을 권장해요.

Kubernetes 로그 수집

Kubernetes 로그 수집에는 OpenTelemetry 문서 가이드를 권장해요. 로그와 메트릭을 pod 메타데이터로 강화하려면 Kubernetes Attributes Processor를 권장해요. 이는 ResourceAttributes 컬럼에 저장되는 동적 메타데이터(예: 라벨)를 생성할 수 있어요. ClickHouse는 현재 이 컬럼에 Map(String, String) 타입을 사용해요. 이 타입의 처리와 최적화에 대한 자세한 내용은 Maps 사용Maps에서 추출을 참고하세요.

트레이스 수집

코드를 계측하고 트레이스를 수집하려는 사용자에게는 공식 OTel 문서를 따르길 권장해요. 이벤트를 ClickHouse로 전달하려면 적절한 receiver로 OTLP 프로토콜을 통해 트레이스 이벤트를 받는 OTel collector를 배포해야 해요. OpenTelemetry 데모는 각 지원 언어를 계측하고 이벤트를 collector로 보내는 예시를 제공해요. 이벤트를 stdout으로 출력하는 적절한 collector 구성 예시는 아래와 같아요.

예시

트레이스는 OTLP로 받아야 하므로 telemetrygen 도구를 사용해 트레이스 데이터를 생성해요. 설치 지침은 여기를 따라 해요. 다음 구성은 stdout으로 보내기 전에 OTLP receiver에서 트레이스 이벤트를 받아요. config-traces.xml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 1s
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging]

이 구성을 다음으로 실행해요:

./otelcol-contrib --config config-traces.yaml

telemetrygen으로 트레이스 이벤트를 collector로 보내요:

$GOBIN/telemetrygen traces --otlp-insecure --traces 300

그러면 아래 예시와 같은 트레이스 메시지가 stdout으로 출력돼요:

Span #86
        Trace ID        : 1bb5cdd2c9df5f0da320ca22045c60d9
        Parent ID       : ce129e5c2dd51378
        ID              : fbb14077b5e149a0
        Name            : okey-dokey-0
        Kind            : Server
        Start time      : 2024-06-19 18:03:41.603868 +0000 UTC
        End time        : 2024-06-19 18:03:41.603991 +0000 UTC
        Status code     : Unset
        Status message :
Attributes:
        -> net.peer.ip: Str(1.2.3.4)
        -> peer.service: Str(telemetrygen-client)

위는 OTel collector가 생성한 단일 트레이스 메시지를 나타내요. 이 같은 메시지를 이후 섹션에서 ClickHouse로 수집할게요. 트레이스 메시지의 전체 스키마는 여기에 유지되며, 이 스키마에 익숙해지기를 적극 권장해요.

처리 — 필터링, 변환, 강화

앞서 로그 이벤트의 타임스탬프 설정 예시에서 보여줬듯이, 이벤트 메시지를 필터링, 변환, 강화하고 싶을 거예요. 이는 OpenTelemetry의 여러 기능으로 달성할 수 있어요:

  • Processors - processors는 receivers가 수집한 데이터를 가져와 수정하거나 변환한 뒤 exporters로 보내요. processors는 collector 구성의 processors 섹션에 구성된 순서대로 적용돼요. 선택 사항이지만 최소 집합이 일반적으로 권장돼요. ClickHouse와 OTel collector를 사용할 때 processors를 다음으로 제한하는 것을 권장해요:
    • memory_limiter는 collector의 메모리 부족 상황을 방지하는 데 사용돼요. 리소스 추정에 대한 권장 사항은 Estimating Resources를 참고하세요.
    • 컨텍스트 기반 강화를 수행하는 모든 processor. 예를 들어 Kubernetes Attributes Processor는 스팬, 메트릭, 로그 리소스 속성을 k8s 메타데이터(예: 이벤트를 소스 pod id로 강화)로 자동 설정하게 해줘요.
    • 트레이스에 필요하다면 Tail 또는 head sampling.
    • 기본 필터링 - 연산자로 처리할 수 없으면 필요 없는 이벤트를 드랍(아래 참고).
    • Batching - ClickHouse와 작업할 때 데이터가 배치로 전송되도록 보장하는 데 필수적이에요. "ClickHouse로 내보내기"를 참고하세요.
  • Operators - 연산자는 receiver에서 사용 가능한 가장 기본적인 처리 단위를 제공해요. 기본 파싱이 지원되어 Severity와 Timestamp 같은 필드를 설정할 수 있어요. 여기서 JSON과 정규식 파싱, 이벤트 필터링, 기본 변환이 지원돼요. 이벤트 필터링은 여기서 수행하는 것을 권장해요.

연산자나 transform processors로 과도한 이벤트 처리를 하는 것은 피하는 것을 권장해요. 특히 JSON 파싱은 상당한 메모리와 CPU 오버헤드를 발생시킬 수 있어요. 몇 가지 예외(특히 컨텍스트 인지 강화, 예: k8s 메타데이터 추가)를 제외하면 모든 처리를 머티어리얼라이즈드 뷰와 컬럼으로 삽입 시점에 ClickHouse에서 수행할 수 있어요. 자세한 내용은 SQL로 구조 추출을 참고하세요. OTel collector로 처리를 한다면, 게이트웨이 인스턴스에서 변환을 수행하고 에이전트 인스턴스에서 작업을 최소화하는 것을 권장해요. 이렇게 하면 서버에서 실행되는 에지의 에이전트가 필요로 하는 리소스가 최소로 유지돼요. 보통 사용자가 필터링(불필요한 네트워크 사용 최소화), 타임스탬프 설정(연산자로), 그리고 컨텍스트가 필요한 강화만 에이전트에서 수행하는 것을 봐요. 예를 들어 게이트웨이 인스턴스가 다른 Kubernetes 클러스터에 있다면 k8s 강화는 에이전트에서 발생해야 해요.

예시

다음 구성은 비구조화 로그 파일 수집을 보여줘요. 로그 라인에서 구조를 추출하는 연산자(regex_parser)와 이벤트 필터링, 이벤트를 배치하고 메모리 사용을 제한하는 processor의 사용을 주목하세요. config-unstructured-logs-with-processor.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-unstructured.log
    start_at: beginning
    operators:
      - type: regex_parser
        regex: '^(?P<ip>[\d.]+)\s+-\s+-\s+\[(?P<timestamp>[^\]]+)\]\s+"(?P<method>[A-Z]+)\s+(?P<url>[^\s]+)\s+HTTP/[^\s]+"\s+(?P<status>\d+)\s+(?P<size>\d+)\s+"(?P<referrer>[^"]*)"\s+"(?P<user_agent>[^"]*)"'
        timestamp:
          parse_from: attributes.timestamp
          layout: '%d/%b/%Y:%H:%M:%S %z'
          #22/Jan/2019:03:56:14 +0330
processors:
  batch:
    timeout: 1s
    send_batch_size: 100
  memory_limiter:
    check_interval: 1s
    limit_mib: 2048
    spike_limit_mib: 256
exporters:
  logging:
    loglevel: debug
service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch, memory_limiter]
      exporters: [logging]
./otelcol-contrib --config config-unstructured-logs-with-processor.yaml

ClickHouse로 내보내기

Exporter는 하나 이상의 백엔드나 목적지로 데이터를 보내요. Exporter는 pull 또는 push 기반일 수 있어요. 이벤트를 ClickHouse로 보내려면 push 기반 ClickHouse exporter를 사용해야 해요.

OpenTelemetry Collector Contrib 사용 ClickHouse exporter는 OpenTelemetry Collector Contrib의 일부이며, core 배포판에는 없어요. contrib 배포판을 사용하거나 자체 collector를 빌드할 수 있어요.

전체 구성 파일은 아래와 같아요. clickhouse-config.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 5s
    send_batch_size: 10000
exporters:
  clickhouse:
    endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1
    # ttl: 72h
    traces_table_name: otel_traces
    logs_table_name: otel_logs
    create_schema: true
    timeout: 5s
    database: default
    sending_queue:
      queue_size: 1000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s

service:
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [clickhouse]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [clickhouse]

다음 핵심 설정에 유의하세요:

  • pipelines - 위 구성은 로그와 트레이스용으로 각각 하나씩, receivers, processors, exporters 집합으로 구성된 파이프라인 사용을 강조해요.
  • endpoint - ClickHouse와의 통신은 endpoint 매개변수로 구성돼요. 연결 문자열 tcp://localhost:9000?dial_timeout=10s&compress=lz4&async_insert=1은 TCP로 통신을 발생시켜요. 트래픽 전환 이유로 HTTP를 선호한다면 여기에 설명된 대로 이 연결 문자열을 수정해요. 이 연결 문자열 내에서 사용자 이름과 비밀번호를 지정할 수 있는 전체 연결 세부 사항은 여기에 설명되어 있어요.

중요: 위 연결 문자열은 압축(lz4)과 비동기 삽입을 모두 활성화해요. 둘 다 항상 활성화하는 것을 권장해요. 비동기 삽입에 대한 자세한 내용은 Batching을 참고하세요. 압축은 항상 지정해야 하며 이전 버전의 exporter에서는 기본적으로 활성화되지 않아요.

  • ttl - 이 값은 데이터가 유지되는 기간을 결정해요. 자세한 내용은 "Managing data"에 있어요. 72h 같은 시간 단위로 지정해야 해요. 아래 예시에서는 TTL을 비활성화해요. 데이터가 2019년 것이라 삽입 시 ClickHouse가 즉시 제거할 것이기 때문이에요.
  • traces_table_namelogs_table_name - 로그와 트레이스 테이블의 이름을 결정해요.
  • create_schema - 시작 시 기본 스키마로 테이블을 만들지 여부를 결정해요. 시작하기에서는 기본값 true예요. false로 설정하고 자체 스키마를 정의해야 해요.
  • database - 대상 데이터베이스.
  • retry_on_failure - 실패한 배치를 재시도할지 결정하는 설정.
  • batch - 배치 processor가 이벤트가 배치로 전송되도록 보장해요. timeout 5s로 최소 10,000 값을 권장해요(메모리가 허용하면 최대 100,000도 사용 가능). 둘 중 먼저 도달하는 것이 배치를 flush하도록 exporter에 시작해요. 이 값을 낮추면 지연 시간이 낮은 파이프라인이 되어 데이터를 더 빨리 조회할 수 있지만, ClickHouse에 더 많은 연결과 배치가 보내져요. 비동기 삽입을 사용하지 않는다면 ClickHouse에서 too many parts 문제를 일으킬 수 있으므로 권장하지 않아요. 반대로 비동기 삽입을 사용하면 조회 가능한 데이터 가용성도 비동기 삽입 설정에 따라 달라지지만, 커넥터에서 데이터는 더 일찍 flush돼요. 자세한 내용은 Batching을 참고하세요.
  • sending_queue - 전송 큐의 크기를 제어해요. 큐의 각 항목은 배치를 포함해요. ClickHouse에 도달할 수 없는데 이벤트가 계속 도착하는 경우처럼 이 큐를 초과하면 배치가 드랍돼요.

사용자가 구조화 로그 파일을 압축 해제했고 로컬 ClickHouse 인스턴스가 실행 중(기본 인증)이라면 이 구성을 다음 명령으로 실행할 수 있어요:

./otelcol-contrib --config clickhouse-config.yaml

이 collector에 트레이스 데이터를 보내려면 telemetrygen 도구로 다음 명령을 실행해요:

$GOBIN/telemetrygen traces --otlp-insecure --traces 300

실행 중에는 간단한 쿼리로 로그 이벤트가 있는지 확인해요:

SELECT *
FROM otel_logs
LIMIT 1
FORMAT Vertical
Row 1:
──────
Timestamp:              2019-01-22 06:46:14.000000000
TraceId:
SpanId:
TraceFlags:             0
SeverityText:
SeverityNumber:         0
ServiceName:
Body:                   {"remote_addr":"109.230.70.66","remote_user":"-","run_time":"0","time_local":"2019-01-22 06:46:14.000","request_type":"GET","request_path":"\/image\/61884\/productModel\/150x150","request_protocol":"HTTP\/1.1","status":"200","size":"1684","referer":"https:\/\/www.zanbil.ir\/filter\/p3%2Cb2","user_agent":"Mozilla\/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko\/20100101 Firefox\/64.0"}
ResourceSchemaUrl:
ResourceAttributes: {}
ScopeSchemaUrl:
ScopeName:
ScopeVersion:
ScopeAttributes:        {}
LogAttributes:          {'referer':'https://www.zanbil.ir/filter/p3%2Cb2','log.file.name':'access-structured.log','run_time':'0','remote_user':'-','request_protocol':'HTTP/1.1','size':'1684','user_agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0','remote_addr':'109.230.70.66','request_path':'/image/61884/productModel/150x150','status':'200','time_local':'2019-01-22 06:46:14.000','request_type':'GET'}

트레이스 이벤트도 마찬가지로 otel_traces 테이블을 확인할 수 있어요:

SELECT *
FROM otel_traces
LIMIT 1
FORMAT Vertical
Row 1:
──────
Timestamp:              2024-06-20 11:36:41.181398000
TraceId:                00bba81fbd38a242ebb0c81a8ab85d8f
SpanId:                 beef91a2c8685ace
ParentSpanId:
TraceState:
SpanName:               lets-go
SpanKind:               SPAN_KIND_CLIENT
ServiceName:            telemetrygen
ResourceAttributes: {'service.name':'telemetrygen'}
ScopeName:              telemetrygen
ScopeVersion:
SpanAttributes:         {'peer.service':'telemetrygen-server','net.peer.ip':'1.2.3.4'}
Duration:               123000
StatusCode:             STATUS_CODE_UNSET
StatusMessage:
Events.Timestamp:   []
Events.Name:            []
Events.Attributes:  []
Links.TraceId:          []
Links.SpanId:           []
Links.TraceState:   []
Links.Attributes:   []

기본 제공 스키마

ClickStack은 최적화된 기본 스키마를 제공해요 ClickStack은 로그, 트레이스, 메트릭용 기본 제공 스키마를 제공해요. 이 스키마는 최신 ClickHouse 기능(전문 및 맵 키 검색용 텍스트 인덱스, 직접 읽기 필터링용 머티어리얼라이즈드 컬럼과 ALIAS 배열, 블록 번호 행 조회)을 통합하고 로깅과 트레이스 워크로드에 강력한 기본 제공 성능을 제공하도록 벤치마킹되었어요. 여러분의 설계를 위한 참조점으로 사용하세요.

기본적으로 ClickHouse exporter는 로그와 트레이스 양쪽에 대상 로그 테이블을 만들어요. 이는 create_schema 설정으로 비활성화할 수 있어요. 또한 로그와 트레이스 테이블 이름을 기본값인 otel_logsotel_traces에서 위에서 언급한 설정으로 수정할 수 있어요.

아래 스키마에서는 TTL이 72h로 활성화되었다고 가정해요.

로그의 기본 스키마는 다음과 같아요(otelcol-contrib v0.102.1):

CREATE TABLE default.otel_logs
(
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` String CODEC(ZSTD(1)),
    `SpanId` String CODEC(ZSTD(1)),
    `TraceFlags` UInt32 CODEC(ZSTD(1)),
    `SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
    `SeverityNumber` Int32 CODEC(ZSTD(1)),
    `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    `ResourceSchemaUrl` String CODEC(ZSTD(1)),
    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `ScopeSchemaUrl` String CODEC(ZSTD(1)),
    `ScopeName` String CODEC(ZSTD(1)),
    `ScopeVersion` String CODEC(ZSTD(1)),
    `ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
    INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_scope_attr_key mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_scope_attr_value mapValues(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS ttl_only_drop_parts = 1

여기의 컬럼은 여기에 문서화된 OTel 공식 로그 스펙과 상관 관계가 있어요. 이 스키마에 대한 몇 가지 중요한 참고 사항:

  • 기본적으로 테이블은 PARTITION BY toDate(Timestamp)로 날짜별 파티셔닝돼요. 이는 만료된 데이터를 효율적으로 드롭할 수 있게 해줘요.
  • TTL은 TTL toDateTime(Timestamp) + toIntervalDay(3)으로 설정되며 collector 구성에 설정된 값과 일치해요. ttl_only_drop_parts=1은 포함된 모든 행이 만료되었을 때만 전체 파트가 드롭된다는 뜻이에요. 이는 비용이 드는 삭제를 수반하는 파트 내 행 드롭보다 더 효율적이에요. 항상 설정하는 것을 권장해요. 자세한 내용은 TTL 데이터 관리를 참고하세요.
  • 테이블은 클래식 MergeTree 엔진을 사용해요. 로그와 트레이스에 권장되며 변경할 필요가 없어요.
  • 테이블은 ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)로 정렬돼요. 이는 쿼리가 ServiceName, SeverityText, Timestamp, TraceId 필터에 최적화된다는 뜻이에요 — 목록의 앞쪽 컬럼이 뒤쪽보다 더 빨리 필터링돼요. 예를 들어 ServiceName으로 필터링하는 것이 TraceId로 필터링하는 것보다 훨씬 빠릅니다. 예상 접근 패턴에 따라 이 정렬을 수정해야 해요 — 기본 키 선택을 참고하세요.
  • 위 스키마는 컬럼에 ZSTD(1)을 적용해요. 이는 로그에 가장 좋은 압축을 제공해요. 더 나은 압축을 위해 ZSTD 압축 레벨을(기본 1 이상으로) 올릴 수 있지만 거의 이점이 없어요. 이 값을 올리면 삽입 시점(압축 중)에 더 큰 CPU 오버헤드가 발생하지만, 압축 해제(따라서 쿼리)는 비슷하게 유지돼야 해요. 자세한 내용은 여기를 참고하세요. Timestamp에는 디스크 크기를 줄이기 위한 추가 delta 인코딩이 적용돼요.
  • ResourceAttributes, LogAttributes, ScopeAttributes가 맵이라는 점에 유의하세요. 이들의 차이를 이해하는 것이 중요해요. 이 맵들에 접근하고 그 안의 키 접근을 최적화하는 방법은 "Using maps"을 참고하세요.
  • 여기의 대부분의 다른 타입, 예를 들어 ServiceName을 LowCardinality로 사용하는 것은 최적화된 거예요. 예시 로그에서 JSON인 Body는 String으로 저장된다는 점에 유의하세요.
  • 블룸 필터는 맵 키와 값, 그리고 Body 컬럼에 적용돼요. 이는 이 컬럼들에 접근하는 쿼리 시간을 개선하는 것을 목표로 하지만 일반적으로 필요하지 않아요. 보조/데이터 건너뛰기 인덱스를 참고하세요.
CREATE TABLE default.otel_traces
(
        `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
        `TraceId` String CODEC(ZSTD(1)),
        `SpanId` String CODEC(ZSTD(1)),
        `ParentSpanId` String CODEC(ZSTD(1)),
        `TraceState` String CODEC(ZSTD(1)),
        `SpanName` LowCardinality(String) CODEC(ZSTD(1)),
        `SpanKind` LowCardinality(String) CODEC(ZSTD(1)),
        `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
        `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
        `ScopeName` String CODEC(ZSTD(1)),
        `ScopeVersion` String CODEC(ZSTD(1)),
        `SpanAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
        `Duration` Int64 CODEC(ZSTD(1)),
        `StatusCode` LowCardinality(String) CODEC(ZSTD(1)),
        `StatusMessage` String CODEC(ZSTD(1)),
        `Events.Timestamp` Array(DateTime64(9)) CODEC(ZSTD(1)),
        `Events.Name` Array(LowCardinality(String)) CODEC(ZSTD(1)),
        `Events.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
        `Links.TraceId` Array(String) CODEC(ZSTD(1)),
        `Links.SpanId` Array(String) CODEC(ZSTD(1)),
        `Links.TraceState` Array(String) CODEC(ZSTD(1)),
        `Links.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
        INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
        INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_span_attr_key mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_span_attr_value mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
        INDEX idx_duration Duration TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId)
TTL toDateTime(Timestamp) + toIntervalDay(3)
SETTINGS ttl_only_drop_parts = 1

다시 말하지만 이는 여기에 문서화된 OTel 공식 트레이스 스펙에 해당하는 컬럼과 상관 관계가 있어요. 여기의 스키마는 위 로그 스키마와 많은 동일한 설정을 사용하며 스팬 특유의 추가 Link 컬럼이 있어요. 자동 스키마 생성을 비활성화하고 테이블을 수동으로 만들 것을 권장해요. 이렇게 하면 기본 및 보조 키를 수정하고, 쿼리 성능 최적화를 위한 추가 컬럼을 도입할 기회가 생겨요. 자세한 내용은 스키마 설계를 참고하세요.

삽입 최적화

높은 삽입 성능과 강력한 일관성 보장을 얻으려면 collector를 통해 옵저버빌리티 데이터를 ClickHouse에 삽입할 때 간단한 규칙을 지켜야 해요. OTel collector를 올바르게 구성하면 다음 규칙은 쉽게 따라 할 수 있어요. 이는 또한 사용자가 ClickHouse를 처음 사용할 때 겪는 일반적인 문제를 피하게 해줘요.

배칭 (Batching)

기본적으로 ClickHouse에 보내진 각 삽입은 삽입의 데이터와 함께 저장해야 할 다른 메타데이터를 포함하는 저장 파트를 즉시 생성해요. 따라서 각각 더 적은 데이터를 포함하는 더 많은 삽입을 보내는 것보다 각각 더 많은 데이터를 포함하는 더 적은 삽입을 보내는 것이 필요한 쓰기 수를 줄여요. 한 번에 최소 1,000행의 꽤 큰 배치로 데이터를 삽입하는 것을 권장해요. 자세한 내용은 여기. 기본적으로 ClickHouse로의 삽입은 동기적이며 동일하면 멱등적이에요. merge tree 엔진 계열의 테이블에 대해 ClickHouse는 기본적으로 삽입을 자동으로 중복 제거해요. 이는 다음과 같은 경우 삽입이 관대하다는 뜻이에요:

  • (1) 데이터를 받는 노드에 문제가 있으면 삽입 쿼리가 타임아웃(또는 더 구체적인 오류)되고 확인을 받지 못해요.
  • (2) 데이터가 노드에 기록됐지만 네트워크 중단으로 쿼리 발신자에게 확인이 반환될 수 없으면, 발신자는 타임아웃 또는 네트워크 오류를 받아요.

collector의 관점에서 (1)과 (2)는 구별하기 어려울 수 있어요. 하지만 두 경우 모두 확인되지 않은 삽입을 즉시 재시도할 수 있어요. 재시도한 삽입 쿼리가 같은 데이터를 같은 순서로 포함하는 한, ClickHouse는 (확인되지 않은) 원래 삽입이 성공했다면 재시도한 삽입을 자동으로 무시해요. 위 요구 사항을 충족하기 위해 이전 구성에서 보여준 batch processor를 사용하는 것을 권장해요. 이는 위 요구 사항을 충족하는 일관된 행 배치로 삽입이 전송되도록 보장해요. collector의 처리량(초당 이벤트)이 높고 각 삽입에서 최소 10,000개 이벤트를 보낼 수 있다면, 이것이 보통 파이프라인에서 필요한 유일한 배칭이에요. 메모리가 허용하면 최대 100,000까지 사용할 수 있어요. 이 경우 collector는 batch processor의 timeout에 도달하기 전에 배치를 flush해서 파이프라인의 종단간 지연이 낮게 유지되고 배치 크기가 일관되도록 해요.

비동기 삽입 사용

보통 collector의 처리량이 낮으면 사용자는 더 작은 배치를 보내야 하지만 여전히 최소 종단간 지연으로 데이터가 ClickHouse에 도달하기를 기대해요. 이 경우 batch processor의 timeout이 만료될 때 작은 배치가 전송돼요. 이는 문제를 일으킬 수 있으며 바로 이때 비동기 삽입이 필요해요. 이 경우는 보통 에이전트 역할의 collector가 ClickHouse에 직접 보내도록 구성됐을 때 발생해요. 게이트웨이는 애그리게이터 역할을 해 이 문제를 완화할 수 있어요 — 게이트웨이로 확장하기를 참고하세요. 큰 배치를 보장할 수 없다면 Asynchronous Inserts로 배칭을 ClickHouse에 위임할 수 있어요. 비동기 삽입에서는 데이터가 먼저 버퍼에 삽입된 다음 나중에 또는 비동기적으로 데이터베이스 저장소에 기록돼요. 비동기 삽입 활성화 시 ClickHouse ① 삽입 쿼리를 받으면 쿼리의 데이터가 ② 먼저 인메모리 버퍼에 즉시 기록돼요. ③ 다음 버퍼 flush가 발생하면 버퍼의 데이터가 기본 키 컬럼별로 정렬되어 파트로 데이터베이스 저장소에 기록돼요. 데이터가 데이터베이스 저장소로 flush되기 전에는 쿼리로 검색할 수 없다는 점을 유의하세요. 버퍼 flush는 구성 가능해요. collector에 비동기 삽입을 활성화하려면 연결 문자열에 async_insert=1을 추가해요. 전달 보장을 얻으려면 wait_for_async_insert=1(기본값)을 사용하는 것을 권장해요 — 자세한 내용은 여기. 비동기 삽입의 데이터는 ClickHouse 버퍼가 flush되면 삽입돼요. 이는 async_insert_max_data_size 또는 async_insert_busy_timeout에 도달하거나 async_insert_max_query_number에 도달한 후에 발생해요.

적응형 비동기 삽입 고려 소수의 에이전트를 사용하고 처리량이 낮지만 엄격한 종단간 지연 요구 사항이 있는 경우, 적응형 비동기 삽입이 유용할 수 있어요. 일반적으로 이는 ClickHouse에서 보이는 것 같은 고처리량 옵저버빌리티 사용 사례에는 적용되지 않아요.

마지막으로, 동기 삽입과 관련된 이전의 중복 제거 동작은 비동기 삽입을 사용할 때 기본적으로 활성화되지 않아요. 필요하다면 async_insert_deduplicate 설정을 참고하세요. 이 기능 구성에 대한 전체 세부 사항은 여기, 심층 분석은 여기에서 찾을 수 있어요.

배포 아키텍처

OTel collector를 ClickHouse와 사용할 때 여러 배포 아키텍처가 가능해요. 각각과 적용 시기를 설명할게요.

에이전트 전용 (Agents only)

에이전트 전용 아키텍처에서는 사용자가 OTel collector를 에이전트로 에지에 배포해요. 이들은 로컬 애플리케이션(사이드카 컨테이너로)에서 트레이스를 받고 서버와 Kubernetes 노드에서 로그를 수집해요. 이 모드에서 에이전트는 데이터를 ClickHouse에 직접 보내요. 이 아키텍처는 소규모에서 중간 규모 배포에 적합해요. 주요 장점은 추가 하드웨어가 필요 없고 ClickHouse 옵저버빌리티 솔루션의 전체 리소스 풋프린트를 최소로 유지하며, 애플리케이션과 collector 사이에 단순한 매핑이 있다는 거예요. 에이전트 수가 수백 개를 넘으면 게이트웨이 기반 아키텍처로의 마이그레이션을 고려해야 해요. 이 아키텍처는 확장을 어렵게 만드는 몇 가지 단점이 있어요:

  • 연결 확장 - 각 에이전트가 ClickHouse에 연결을 설정해요. ClickHouse가 수백(수천은 아니더라도) 개의 동시 삽입 연결을 유지할 수 있지만, 이는 궁극적으로 제한 요소가 되고 삽입을 덜 효율적으로 만들어요 — 즉 ClickHouse가 연결을 유지하는 데 더 많은 리소스가 사용돼요. 게이트웨이를 사용하면 연결 수를 최소화하고 삽입을 더 효율적으로 만들어요.
  • 에지에서 처리 - 이 아키텍처에서는 모든 변환이나 이벤트 처리를 에지 또는 ClickHouse에서 수행해야 해요. 이는 제한적일 뿐 아니라 복잡한 ClickHouse 머티어리얼라이즈드 뷰를 의미하거나, 중요한 서비스가 영향을 받고 리소스가 부족할 수 있는 에지로 상당한 계산을 밀어내는 것을 의미할 수 있어요.
  • 작은 배치와 지연 - 에이전트 collector는 개별적으로 이벤트를 거의 수집하지 못할 수 있어요. 이는 보통 전달 SLA를 충족하기 위해 설정된 간격으로 flush하도록 구성해야 한다는 뜻이에요. 이로 인해 collector가 작은 배치를 ClickHouse로 보낼 수 있어요. 단점이지만 비동기 삽입으로 완화할 수 있어요 — 삽입 최적화를 참고하세요.

게이트웨이로 확장

위 제한을 해결하기 위해 OTel collector를 게이트웨이 인스턴스로 배포할 수 있어요. 이들은 보통 데이터 센터 또는 리전별로 독립 실행형 서비스를 제공해요. 이들은 단일 OTLP 엔드포인트를 통해 애플리케이션(또는 에이전트 역할의 다른 collector)에서 이벤트를 받아요. 보통 게이트웨이 인스턴스 집합을 배포하고 기본 제공 로드 밸런서로 부하를 분산해요. 이 아키텍처의 목적은 계산 집약적 처리를 에이전트에서 오프로드해 리소스 사용을 최소화하는 거예요. 게이트웨이는 에이전트가 해야 할 변환 작업을 수행할 수 있어요. 또한 많은 에이전트의 이벤트를 집계함으로써 게이트웨이는 큰 배치가 ClickHouse로 전송되도록 보장해 효율적인 삽입을 허용해요. 에이전트가 추가되고 이벤트 처리량이 증가함에 따라 게이트웨이 collector는 쉽게 확장될 수 있어요. 예시 구조화 로그 파일을 소비하는 관련 에이전트 구성과 함께 게이트웨이 구성 예시는 아래와 같아요. 에이전트와 게이트웨이 사이의 통신에 OTLP를 사용한다는 점에 유의하세요. clickhouse-agent-config.yaml

receivers:
  filelog:
    include:
      - /opt/data/logs/access-structured.log
    start_at: beginning
    operators:
      - type: json_parser
        timestamp:
          parse_from: attributes.time_local
          layout: '%Y-%m-%d %H:%M:%S'
processors:
  batch:
    timeout: 5s
    send_batch_size: 10000
exporters:
  otlp:
    endpoint: localhost:4317
    tls:
      insecure: true # Set to false if you are using a secure connection
service:
  telemetry:
    metrics:
      address: 0.0.0.0:9888 # Modified as 2 collectors running on same host
  pipelines:
    logs:
      receivers: [filelog]
      processors: [batch]
      exporters: [otlp]

clickhouse-gateway-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
processors:
  batch:
    timeout: 5s
    send_batch_size: 10000
exporters:
  clickhouse:
    endpoint: tcp://localhost:9000?dial_timeout=10s&compress=lz4
    ttl: 96h
    traces_table_name: otel_traces
    logs_table_name: otel_logs
    create_schema: true
    timeout: 10s
    database: default
    sending_queue:
      queue_size: 10000
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [clickhouse]

이 구성들은 다음 명령으로 실행할 수 있어요.

./otelcol-contrib --config clickhouse-gateway-config.yaml
./otelcol-contrib --config clickhouse-agent-config.yaml

이 아키텍처의 주요 단점은 collector 집합을 관리하는 데 드는 비용과 오버헤드예요. 더 큰 게이트웨이 기반 아키텍처를 관련 학습과 함께 관리하는 예시는 이 블로그 포스트를 권장해요.

Kafka 추가

독자들은 위 아키텍처가 Kafka를 메시지 큐로 사용하지 않는 것을 알아차릴 수 있어요. Kafka 큐를 메시지 버퍼로 사용하는 것은 로깅 아키텍처에서 볼 수 있는 인기 있는 설계 패턴이며 ELK 스택이 유명하게 만든 것이에요. 몇 가지 이점을 제공해요. 주로 더 강력한 메시지 전달 보장을 돕고 백프레셔를 처리하는 데 도움을 줘요. 메시지는 수집 에이전트에서 Kafka로 보내져 디스크에 기록돼요. 이론적으로 클러스터형 Kafka 인스턴스는 메시지를 파싱하고 처리하는 것보다 데이터를 디스크에 선형으로 쓰는 것이 계산 오버헤드가 덜 들기 때문에 고처리량 메시지 버퍼를 제공해야 해요 — 예를 들어 Elastic에서는 토큰화와 인덱싱이 상당한 오버헤드를 발생시켜요. 에이전트에서 데이터를 옮기면 소스에서 로그 회전의 결과로 메시지를 잃을 위험도 줄어들어요. 마지막으로 일부 메시지 재생과 교차 리전 복제 기능을 제공해 몇몇 사용 사례에 매력적일 수 있어요. 하지만 ClickHouse는 매우 빠르게 데이터를 삽입할 수 있어요 — 적당한 하드웨어에서 초당 수백만 행. ClickHouse의 백프레셔는 드물어요. 종종 Kafka 큐를 활용하는 것은 더 많은 아키텍처 복잡성과 비용을 의미해요. 로그가 은행 거래나 다른 업무 중요 데이터와 같은 전달 보장을 필요로 하지 않는다는 원칙을 받아들일 수 있다면 Kafka의 복잡성을 피하는 것을 권장해요. 하지만 높은 전달 보장이나 데이터 재생 능력(잠재적으로 여러 소스로)이 필요하다면 Kafka는 유용한 아키텍처 추가가 될 수 있어요. 이 경우 OTel 에이전트는 Kafka exporter를 통해 데이터를 Kafka로 보내도록 구성할 수 있어요. 게이트웨이 인스턴스는 차례로 Kafka receiver로 메시지를 소비해요. 자세한 내용은 Confluent와 OTel 문서를 권장해요.

리소스 추정

OTel collector의 리소스 요구 사항은 이벤트 처리량, 메시지 크기, 수행되는 처리량에 따라 달라져요. OpenTelemetry 프로젝트는 리소스 요구 사항을 추정하는 데 사용할 수 있는 벤치마크를 유지 관리해요. 우리 경험에 따르면 3코어와 12GB RAM의 게이트웨이 인스턴스가 초당 약 6만 개 이벤트를 처리할 수 있어요. 이는 필드 이름 변경만 담당하고 정규식이 없는 최소 처리 파이프라인을 가정해요. 이벤트를 게이트웨이로 전달하고 이벤트에 타임스탬프만 설정하는 에이전트 인스턴스의 경우 예상 초당 로그 수에 따라 크기를 정하는 것을 권장해요. 다음은 시작점으로 사용할 수 있는 대략적인 숫자예요:

로깅 속도 collector 에이전트 리소스
1k/초 0.2CPU, 0.2GiB
5k/초 0.5 CPU, 0.5GiB
10k/초 1 CPU, 1GiB

더 알아보기 (Learn more)