안녕하세요. SRE팀에 근무하고 있는 박형규입니다.

저희는 Kubernetes 환경에서 동작하는 서비스의 증가와 최근 k8s 환경에서 대규모 서비스 오픈을 진행 했으며, 이에 대비하여 어떻게 마이크로 서비스에서 가시성을 확보할지, 또 문제가 생겼을 경우 어떻게 쉽게 문제를 확인하고 추적 할지에 대해 고민하게 되었습니다.

그 결과, OpenTelemetry와 SigNoz 조합을 활용한 Observability 환경을 구축하게 되었으며, 그 경험을 공유하고자 합니다.


시작하게 된 배경



기존 모니터링 환경 문제점

  • 모니터링 툴이 분산되어 있어 유지보수가 어렵고 관리 포인트가 많음
  • 문제 추적이 복잡하고 시간이 많이 소요됨 ⏳
  • 각기 다른 툴을 별도로 학습하고 운영해야 하는 부담이 있음

대부분의 회사가 위와 같은 모습일 거라고 생각합니다.
이런 환경이 점점 많아지면 유지보수가 어려워지고 관리 포인트가 증가할 뿐만아니라 복잡성까지 가중 되어 퇴근하기가 더욱 어려워집니다.
물론 각자 특화된 모습이 있겠지만 모두를 관리하는 입장과 이용하는 입장 양면에서, 좀 더 편하고 쉽게 문제 추적 및 모니터링 할 수 있었으면 좋겠다에서 시작했습니다.

Why OpenTelemetry?🤔


OpenTelemetry의 Architecture

Observability의 핵심인 Metric, Log, Trace를 통합 해주는데 이것은 프로세스와 아키텍쳐를 단순화 해주면서 셋 간의 상호연관 분석을 가능하게 해줍니다.




Telemetry 측정과 수집에 대해 표준화로 여러 Visualize Backend 도구들과 통합될 수 있도록 합니다. 그리고 여전히 OpenTelemetry는 CNCF 프로젝트 중 Kubernetes 다음으로 2위로 달리고 있습니다.
이렇게 표준화 및 다양한 관찰 도구 사용이 가능해짐으로 인해 Vendor Lock-In 이슈가 없다는 점과 저렴한 운영 비용 또한 장점이 됩니다.
실제로 DataDog, NewRelic, Dynatrace 등 운영 비용은 상당하기 때문에 비용 압력으로 관찰 대상을 포기하기 쉽습니다.


OpenTelemetry가 분산 추적을 가능하게 하는 개념은 바로 Context Propagation을 통해 진행 됩니다. 해당 기능을 통해 Signal들이 어디에서 생성되었는지에 관계 없이 서로 연관지을 수 있습니다.
이는 Tracing에만 국한되지 않고 시스템 전반에 걸쳐 서비스 간의 인과관계를 나타내는 정보를 구성 할 수 있게 해줍니다. Context Propagation을 이해 하려면 두 가지 핵심 Context와 Propagation 개념을 알아야 합니다.

Context
컨텍스트는 한 신호를 다른 신호와 연관 지을 수 있도록, 송신 서비스와 수신 서비스(또는 실행 단위)에 필요한 정보를 담고 있는 객체입니다.
예를 들어, 서비스 A가 서비스 B를 호출할 때, A의 스팬(span) ID가 컨텍스트 안에 포함되어 있으면, B에서 생성되는 다음 스팬은 A의 스팬을 부모 스팬으로 사용하게 됩니다.
또한 컨텍스트에 포함된 트레이스 ID(trace ID) 는 B에서 생성되는 스팬에도 동일하게 사용되어, A의 스팬과 B의 스팬이 같은 트레이스(trace) 내에 있다는 것을 의미합니다.

Propagation
전파(Propagation)는 Context를 서비스나 프로세스 간에 이동시키는 메커니즘입니다.
Context 객체를 직렬화 또는 역직렬화하여 필요한 정보를 다른 서비스로 전달합니다.

전파(Propagation)는 일반적으로 자동화된 계측 라이브러리(instrumentation library) 가 처리하며, 사용자가 명시적으로 다룰 필요는 없습니다.
하지만 수동으로 컨텍스트를 전파해야 할 경우에는 Propagators API 를 사용할 수 있습니다.

OpenTelemetry는 몇 가지 공식 전파자(propagator)를 제공합니다.
기본 전파자는 W3C의 TraceContext 명세에서 정의된 HTTP 헤더를 사용합니다.


OpenTelemetry Collector

Otel Collector는 벤더 중립적인 방식으로 Telemetry 데이터를 Receive, Process, Exporter 기능을 제공합니다. Otel Collector 하나로 다른 Agent나 Collector를 운영하거나 유지보수 할 필요가 없어집니다.
또한 해당 Collector는 확장성이 뛰어나고, Jaeger, Prometheus, Fluent Bit 등과 같은 오픈소스 가시성 데이터 포맷을 사용하여 하나 이상의 오픈소스 또는 상용 백엔드로 데이터를 전송 할 수 있습니다.


OpenTelemetry Collector Pipeline


파이프라인은 Collector 내에서 수신 -> 처리 -> 내보내기까지의 Flow를 정의 합니다. 해당 파이프라인은 M,L,T 모두 처리 할 수 있습니다.
각 파이프라인은 구성에 따라 특정 데이터 유형을 처리하도록 정의되며, 파이프라인에 포함된 수신기(Receivers), 처리기(Processors), 내보내기(Exporters) 모두 해당 데이터 유형을 지원해야 합니다.
그렇지 않으면 Collector가 설정을 로드할 때 pipeline.ErrSignalNotSupported 예외가 발생합니다.


Receivers
수신기는 일반적으로 네트워크 포트를 리슨하며 텔레메트리 데이터를 수신합니다. 또한 스크래퍼처럼 데이터를 능동적으로 수집할 수도 있습니다.
보통 하나의 수신기는 하나의 파이프라인에만 데이터를 전송하도록 구성되지만, 동일한 수신기를 여러 파이프라인에 연결하여 동일한 수신 데이터를 여러 파이프라인으로 전송할 수도 있습니다.
그리고 Receiver는 공식으로 지원하는 Receiver 뿐 아니라, Community의 Custom Receiver도 지원 합니다.

Note
공식 Receiver : https://opentelemetry.io/docs/platforms/kubernetes/collector/components/
Community Custom Receiver : https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: localhost:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp]
    traces/2:
      receivers: [otlp]
      processors: [transform]
      exporters: [otlp]

위의 구성에서는 otlp 수신기가 수신한 데이터를 traces 파이프라인과 traces/2 파이프라인 모두에 전송합니다.
구성에서 traces/2와 같이 type[/name] 형식의 복합 키 이름을 사용할 수 있습니다.

Collector가 이 구성을 로드하면, 수신기 하나가 생성되고 fan-out consumer를 통해 데이터를 두 파이프라인으로 분기합니다.


Processor
들어온 데이터들을 어떻게 처리 할지 정의 할 수 있으며, 거기에는 데이터를 변환하거나 수정하는 부분도 포함됩니다.
프로세서 안에는 여러 작업을 포함 시킬 수 있습니다. 예를 들면 batch, memory limiter, filter, transform 등 또한 여러 파이프라인에서 사용 할 수 있습니다.

예를 들어 batch 프로세서를 여러 파이프라인에서 사용하는 경우:

processors:
  batch:

service:
  pipelines:
    traces:
      processors: [batch]
    logs:
      processors: [batch]

각 파이프라인은 batch 프로세서의 자신만의 인스턴스를 가지지만, 구성은 동일하게 적용됩니다.
또한 여러 프로세서를 같이 사용할 때 순서도 매우 중요합니다. 처리 순서에 따라 데이터 흐름, 성능, 필터링 및 리소스 사용에 직접적인 영향을 미치기 때문입니다.

      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, filter/ottl, metricspan, batch]
          exporters: [clickhousetraces, metadataexporter]

저희는 memory_limiter, filter, add metric, batch 등을 포함시켰습니다.
간단하게 역할을 정리 해드리면

Processor 역할 및 특징
memory_limiter Collector 프로세스가 사용하는 메모리를 제한하고 과다 사용시(spike) 지연 및 용량제한 등 안정성 확보
filter 특정 조건에 맞는 트레이스/메트릭/로그를 포함하거나 제외하는 역할
metricspan 생성된 Span에 Metric을 연결시키는 역할
batch 데이터를 일정량 모아서 한꺼번에 내보내서 네트워크 효율향상 및 레이턴시 감소

프로세서 순서
memory_limiter로 가장 먼저 앞에 두어서 Collector 메모리 사용량을 모니터링 및 제한 —>
filter를 통해 필요없는 데이터를 조기에 제거하여 후단 처리량 감소 —>
생성된 Span에 메트릭데이터 연결 —>
batch를 마지막에 두어 남은 데이터를 묶어서 네트워크나 Exporter에 효율적으로 전송
상황별 예외사항이 있을 수 있으며 각 환경에 맞게 적절하게 순서나 프로세서를 추가하여 운영하면 좋을 것 같습니다.

Note
공식 및 Community에서 제공하는 Processor list : https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor


Exporter
Exporters는 일반적으로 수신한 데이터를 네트워크의 목적지로 전달합니다. 그러나 debug exporter처럼 데이터를 로컬 로그 등 다른 위치로 보낼 수도 있습니다.
동일한 타입의 exporter를 여러 개 정의하여 각각 다른 목적지로 전송할 수 있습니다

    exporters:
      otlphttp/backend:
        endpoint: "http://BACKEND로 보낼 주소"
      kafka/goodman:
        brokers:
          - IP:PORT
          - IP:PORT
          - IP:PORT
        topic: goodboy
        protocol_version: 2.0.0
        encoding: otlp_json
        auth:
          sasl:
            mechanism: SCRAM-SHA-512
            username: log
            password: "logs"
      elasticsearch/logs:
        endpoints:
          - IP:PORT
          - IP:PORT
          - IP:PORT
        logs_index: "logs-ingress_nginx.access-default"
        tls:
          ca_file: "/etc/certs/ca.crt"
        auth:
          authenticator: basicauth/es
    service:
      pipelines:
        logs:
          receivers: [zipkin]
          processors: [memory_limiter]
          exporters: [otlphttp/backend, kafka/goodman, elasticsearch/logs]


이런식으로 exporter를 여러개 정의하여 여러 곳으로 보낼 수 있습니다.
Backend가 OTLP를 지원할 경우 기본 OTLP Exporter를 직접 보낼 수 있지만, Elasticsearch와 같이 OTLP를 지원하지 않는 경우 관련 Exporter를 사용해야 합니다.

Note
공식 및 Community에서 제공하는 Exporter list : https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter

이렇게 OpenTelemetry Collector의 대표 3가지 구성요소에 구성하였습니다.

저희는 우선적으로 Kubernetes 환경에서의 Observability 확보를 진행하였으며 k8s 환경에서는 Auto-Instrumentation을 지원하고 있기 때문에 쉽게 반영할 수 있었습니다.
Auto-Instrumentation은 Deployment에 Inject가 정의되면 Pod가 Initialize할 때 Init Container를 동작 시킴으로서 관련 agent를 설치시키고 그런 후 Application 파드가 올라오는 형태입니다.
이렇게 진행되기 위해서는 Instrumentation CR이 설치되어야 합니다. Auto-Instrumentation 적용 순서 입니다.

  • 해당 k8s cluster에 CertManager, OpenTelemetry Operator 설치
  • Instrumentation Manifest 적용
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: {이름}
spec:
  exporter:
    endpoint: http://collector로 보낼주소 # 데이터 EndPoint
  propagators:                                   # Context Progation에 대한 정의
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio        # MLT에 대한 Sampling 비율 (0.0 ~ 1) (1이면 100%로 수집한다)
    argument: '1'
  java:                                   # 각 언어에 맞게 선언
    env:
      - name: OTEL_SERVICE_NAME                   # 서비스명 지정(커밋 ID가 붙지 않도록) 합니다.. UI에서 서비스명으로 filter 걸어서 보기 좋습니다.
        value: {서비스명}                         # 예 saramin-prod
      - name: OTEL_TRACES_EXPORTER                # console로 하면 standardout으로 전달되는 모습을 로그로 볼 수 있습니다(트러블슈팅시 용이) -> 수집된게 확인되면 console은 다시 빼는게 좋습니다.
        value: console,otlp                       # otlp가 실제로 backend로 전달하는 설정
      - name: OTEL_METRICS_EXPORTER
        value: console,otlp
      - name: OTEL_LOGS_EXPORTER
        value: console,otlp
      - name: OTEL_EXPORTER_OTLP_PROTOCOL
        value: http/protobuf
      - name: OTEL_INSTRUMENTATION_COMMON_DEFAULT_ENABLED  # 모든 계측을 활성화 (많은 데이터가 들어가게 되므로 사용하시다 익숙해지면 제외할 필요가 있습니다.)
        value: "true"
  • Deployment에 Inject 추가
apiVersion: v1
items:
- apiVersion: apps/v1
  kind: Deployment
  metadata:
    annotations:                       # 여기가 아닙니다.
    labels:
      app: saramin-java
      app.kubernetes.io/instance: otel
    name: saramin-node-webapp
    namespace: otel
  spec:
    minReadySeconds: 10
    progressDeadlineSeconds: 120
    replicas: 1
    revisionHistoryLimit: 2
    selector:
      matchLabels:
        app: saramin-java
    strategy:
      rollingUpdate:
        maxSurge: 25%
        maxUnavailable: 25%
      type: RollingUpdate
    template:
      metadata:         # Template.metadata.annotations에 라인추가 
        annotations:    # 각 언어에 맞는 inject를 아래처럼 true로 해줍니다.
          instrumentation.opentelemetry.io/inject-java: "true"
        creationTimestamp: null

Note
.NET: instrumentation.opentelemetry.io/inject-dotnet: “true”
Deno: instrumentation.opentelemetry.io/inject-sdk: “true”
Go: instrumentation.opentelemetry.io/inject-go: “true”
Java: instrumentation.opentelemetry.io/inject-java: “true”
Node.js: instrumentation.opentelemetry.io/inject-nodejs: “true”
Python: instrumentation.opentelemetry.io/inject-python: “true”

아래 형태로도 사용 가능합니다.

“true” - 현재 네임스페이스에서 기본 이름으로 Instrumentation 리소스를 주입합니다.
“my-instrumentation” - 현재 네임스페이스에 “my-instrumentation”이라는 이름의 Instrumentation CR 인스턴스를 주입합니다.
“my-other-namespace/my-instrumentation” - 다른 네임스페이스 “my-other-namespace”에서 “my-instrumentation”이라는 이름의 Instrumentation CR 인스턴스를 주입합니다.
“false” - 주입하지 않습니다.

자 이렇게 OpenTelemetry가 구성하였습니다. 그런데 M,L,T를 Exporter로 어디로 보내는걸로 구성했을까요?
OpenSource Observability 하면 떠오르는게 LGTM이 유명할 것으로 알고 있습니다.


위와 같은 아키텍쳐로 구성하여 파일럿 해보았지만, 이 마저도 복잡성을 증가 시키거나 유지보수 하는데 많은 시간을 할애 할 수 있을 것 같다고 판단 했습니다.
또한 각각에 OpenSource가 독립된 Helm Chart이며 각 구성요소를 새로 배워야 한다는 점입니다.

그래서 저희는 SigNoz라는 OpenSource를 이용하여 Observability Backend로 구성하였습니다.


SigNoz 하나로 통합 관리가 가능해졌고,
무엇보다도 운영 편의성과 유지보수 효율성이 크게 개선되었습니다.
SigNoz는 Signal + Noise의 합성어라고 합니다. 수많은 신호로 부터 오는 노이즈를 줄이겠다라는 의미로 만들어진 것 같습니다.

  • Metric, Log, Trace를 단일 도구로 수집 및 시각화 가능
  • OpenTelemetry 기반으로 쉽게 연동
  • ClickHouse로 대용량 데이터 처리
  • 직관적 UI로 사용 편리
  • 활발한 개발과 커뮤니티 활동 👍


단일 도구로 MLT 모두 수집 및 모니터링 할 수 있는 도구 입니다. 여러 거대 벤더사(Datadog, NewRelic 등) 대체제로 떠오르고 있습니다.
SigNoz Collector로 데이터를 받고 Exporter로 ClickHouse로 전달하여 해당 데이터를 SigNoz Binary를 통해 확인하는 구조입니다.

또한 여러 경쟁사들을 조사했습니다.


일단 출시일에 비해 가장 활발하게 Release 되고 있고 다른것에 비해 Contributor수도 많다고 생각했습니다.

마무리

현재 여러 모니터링 툴이 있습니다.
Sentry, Prometheus, Elastic APM, ElasticSearch, NewRelic 등등…
각각의 모니터링 툴마다 특성과 개성이 있기에 모두 안 볼수는 없겠지만,
OpenTelemetry + SigNoz 조합으로 점진적으로 좁혀지지 않을까 싶습니다.
앞으로도 Log 및 Trace 보관 전략과 OpenTelemetry Collector, ClickHouse 등 고민사항과
고도화 할 내용들이 남았지만 목표 달성을 위해 팀원들과 함께 진행하고 있습니다.
구축기가 많이 부족하지만 긴 글 읽어주셔서 감사합니다.