NGINX 튜토리얼: OpenTelemetry 트레이싱으로 마이크로서비스를 이해하는 방법

NGINX | 2023년 3월 28일

이 게시물은 다음의 개념을 실제로 적용하는 데 도움이 되는 네 가지 튜토리얼 중 하나입니다. 마이크로서비스 2023년 3월: 마이크로서비스 제공 시작하기:

마이크로서비스 아키텍처는 팀의 자율성을 높이고 확장 및 배포의 유연성을 향상하는 등 많은 이점을 제공합니다. 반면, 시스템 내 서비스가 많아질수록(마이크로서비스 애플리케이션에는 수십 개에서 수백 개의 서비스가 포함될 수 있음) 시스템의 전반적인 작동 방식을 명확하게 파악하기가 더 어려워집니다. 복잡한 소프트웨어 시스템을 개발하고 유지 관리하는 입장에서, 이러한 명확한 그림을 확보하는 것이 얼마나 중요한지 잘 알고 있습니다. 관측 가능성 도구를 사용하면 수많은 서비스와 이를 지원하는 인프라 전반에 걸쳐 이러한 그림을 구축할 수 있습니다.

이 튜토리얼에서는 마이크로서비스 앱의 관찰 가능성에서 매우 중요한 유형 중 하나인 트레이싱을 중점적으로 살펴봅니다. 시작하기 전에 관찰 가능성을 논할 때 흔히 사용하는 몇 가지 용어를 정의해 보겠습니다.

  • 관찰 가능성 – 복잡한 시스템(예: 마이크로서비스 앱)의 외부 출력(예: 추적, 로그, 메트릭)에 대한 정보만으로 해당 시스템의 내부 상태나 조건을 파악할 수 있는 능력입니다.
  • 모니터링 – 일정 기간 동안 대상의 진행 상황이나 상태를 관찰하고 확인하는 기능입니다. 예를 들어, 사용량이 가장 많은 시간대에 앱으로 유입되는 트래픽을 모니터링하고, 해당 정보를 활용하여 트래픽에 맞게 앱을 적절히 확장할 수 있습니다.
  • 텔레메트리 – 메트릭, 추적 및 로그를 수집하여 생성 지점에서 저장 및 분석을 위한 다른 시스템으로 전송하는 작업입니다. 이렇게 수집된 데이터 자체를 의미하기도 합니다.
  • 트레이싱/트레이스 – 요청이나 작업이 분산 시스템의 모든 노드를 거쳐 이동하는 과정을 기록한 것입니다.
  • 스팬 – 작업 및 관련 메타데이터를 추적 내에 기록한 것입니다. 추적은 여러 개의 중첩된 스팬으로 구성됩니다.
  • 이벤트 로깅/로그 – 메타데이터가 포함된 타임스탬프 형식의 텍스트 기록입니다.
  • 메트릭 – 런타임에 수집되는 측정값입니다. 예를 들어 특정 시점에 애플리케이션이 사용 중인 메모리의 양을 나타냅니다.

이러한 모든 개념을 활용하면 마이크로서비스의 성능을 파악할 수 있습니다. 추적은 요청이 발생할 때 서로 느슨하게 결합된 경우가 많은 여러 구성 요소에서 어떤 일이 일어나는지 전체적인 관점을 제공하므로, 관찰 가능성 전략에서 특히 유용합니다. 또한 성능 병목 현상을 식별하는 데도 매우 효과적입니다.

이 튜토리얼에서는 OpenTelemetry(OTel)의 추적 도구 키트를 사용합니다. OTel은 텔레메트리 데이터를 수집, 처리 및 내보내기 위한 오픈 소스 벤더 중립적 표준으로, 빠르게 인기를 얻고 있습니다. OTel에서 트레이스는 여러 서비스가 관여할 수 있는 데이터 흐름을 시간순으로 정렬된 일련의 ‘청크’로 나누어 다음 사항을 쉽게 파악할 수 있도록 합니다:

  • ‘청크’에서 발생한 모든 단계
  • 이 모든 단계에 걸린 시간
  • 각 단계에 대한 메타데이터

OTel이 익숙하지 않다면 OpenTelemetry란 무엇인가요?에서 이 표준에 대한 자세한 소개와 구현 시 고려 사항을 확인하세요.

튜토리얼 개요

이 튜토리얼에서는 OTel을 사용하여 마이크로서비스 앱의 작업을 추적하는 방법을 중점적으로 다룹니다. 이 튜토리얼의 네 가지 과제를 통해 시스템에서 요청을 추적하고 마이크로서비스에 관한 질문에 답하는 방법을 알아봅니다:

이러한 과제는 처음으로 트레이싱을 설정할 때 권장하는 프로세스를 보여줍니다. 단계는 다음과 같습니다:

  1. 시스템뿐만 아니라 계측하려는 특정 작업도 이해합니다.
  2. 실행 중인 시스템에서 무엇을 파악해야 하는지 결정합니다.
  3. 시스템을 ‘단순하게’ 계측합니다. 즉, 불필요한 정보를 걸러 내거나 사용자 지정 데이터 포인트를 수집하려 하지 말고 기본 구성을 사용한 다음, 계측을 통해 궁금한 점에 대한 답을 얻을 수 있는지 평가합니다.
  4. 보고되는 정보를 조정하여 해당 질문에 더 빠르게 답할 수 있도록 합니다.

참고: 이 튜토리얼의 목적은 프로덕션 환경에서 마이크로서비스를 배포하는 올바른 방법을 보여주는 것이 아니라, 텔레메트리의 몇 가지 핵심 개념을 설명하는 것입니다. 실제 ‘마이크로서비스’ 아키텍처를 사용하지만, 몇 가지 중요한 유의 사항이 있습니다:

  • 이 튜토리얼에서는 Kubernetes나 Nomad와 같은 컨테이너 오케스트레이션 프레임워크를 사용하지 않습니다. 특정 프레임워크의 세부 사항에 얽매이지 않고 마이크로서비스 개념을 학습할 수 있도록 하기 위해서입니다. 여기서 소개하는 패턴은 이러한 프레임워크 중 하나를 실행하는 시스템에도 적용할 수 있습니다.
  • 서비스는 소프트웨어 엔지니어링의 엄밀성보다는 이해하기 쉽도록 최적화되어 있습니다. 핵심은 코드의 세부 사항이 아니라 시스템에서 서비스가 수행하는 역할과 통신 패턴을 살펴보는 것입니다. 자세한 내용은 각 서비스의 README 파일을 참조하세요.

튜토리얼 아키텍처 및 텔레메트리 목표

아키텍처 및 사용자 흐름

이 다이어그램은 튜토리얼에서 사용되는 마이크로서비스와 기타 요소 간의 전반적인 아키텍처 및 데이터 흐름을 보여줍니다.

두 개의 마이크로서비스, NGINX, RabbitMQ로 구성된 메시징 시스템을 OpenTelemetry로 추적하는 튜토리얼의 토폴로지 다이어그램

두 마이크로서비스는 다음과 같습니다:

  • The messenger 서비스 – 메시지 저장 기능을 갖춘 간단한 채팅 API
  • The notifier 서비스 – 사용자의 환경 설정에 따라 사용자에게 알림을 보내는 이벤트를 트리거하는 리스너

지원 인프라는 다음 세 가지로 구성됩니다:

  • NGINX Open Source – messenger 서비스 및 전체 시스템으로 들어가는 진입점
  • RabbitMQ – 서비스 간 비동기 통신을 지원하는 널리 사용되는 오픈 소스 메시지 브로커
  • Jaeger – 시스템 내 구성 요소에서 생성되는 텔레메트리를 수집하고 시각화하는 오픈 소스 엔드투엔드 분산 추적 시스템

잠시 OTel은 제쳐 두고, 추적하려는 이벤트의 흐름, 즉 사용자가 새 채팅 메시지를 보내고 수신자가 이에 대한 알림을 받기까지 어떤 일이 일어나는지에 집중해 보겠습니다.

튜토리얼에서 사용하는 메시징 시스템의 정보 흐름을 보여주는 다이어그램

흐름은 다음과 같이 나뉩니다:

  1. 사용자가 messenger 서비스에 메시지를 보냅니다. NGINX 리버스 프록시가 메시지를 가로채 여러 messenger 서비스 인스턴스 중 하나로 전달합니다.
  2. The messenger 서비스가 새 메시지를 데이터베이스에 기록합니다.
  3. The messenger 서비스는 메시지가 전송되었음을 알리기 위해 chat_queue라는 RabbitMQ 메시지 큐에 이벤트를 발행합니다. 이 이벤트는 범용 이벤트이며 특정 대상을 지정하지 않습니다.
  4. 동시에:
    • 4a. messenger 서비스는 메시지가 성공적으로 전송되었음을 알리는 응답을 발신자에게 반환합니다.
    • 4b. notifier 서비스가 chat_queue의 새 이벤트를 감지하고 소비합니다.
  5. The notifier 서비스는 데이터베이스에서 새 메시지 수신자의 알림 설정을 확인합니다.
  6. The notifier 서비스는 수신자가 선호하는 방식으로 하나 이상의 알림을 전송합니다(이 튜토리얼에서 선택할 수 있는 방식은 SMS와 이메일입니다).

텔레메트리 목표

텔레메트리 계측을 설정할 때는 “모든 데이터를 전송하고 유용한 인사이트가 나오기를 기대한다”는 막연한 접근보다 구체적인 계측 목표를 먼저 세우는 것이 좋습니다. 이 튜토리얼에서는 다음과 같은 세 가지 핵심 텔레메트리 목표를 다룹니다:

  1. 새 메시지 흐름에서 요청이 거치는 모든 단계를 파악합니다
  2. 정상적인 조건에서 해당 흐름이 5초 이내에 처음부터 끝까지 실행된다는 확신을 갖습니다.
  3. See how long it takes the notifier 서비스가 messenger 서비스에서 전송한 이벤트를 처리하기 시작하는 데 걸리는 시간을 확인합니다(지연 시간이 지나치게 길다면 notifier 서비스가 이벤트 큐에서 이벤트를 읽는 데 문제가 있어 이벤트가 적체되고 있다는 의미일 수 있습니다).

이러한 목표는 시스템의 기술적 운영과 사용자 경험 모두와 관련이 있다는 점에 유의하세요.

튜토리얼 사전 요구 사항 및 설정

사전 요구 사항

자체 환경에서 이 튜토리얼을 완료하려면 다음이 필요합니다:

  • Linux/Unix 호환 환경 참고: 이 튜토리얼에서 NGINX 추적과 관련된 작업은 NGINX용 OpenTelemetry 모듈이 호환되지 않으므로 ARM 기반 프로세서에서 작동하지 않습니다. (여기에는 Linux aarch64 아키텍처와 M1 또는 M2 칩이 탑재된 Apple 컴퓨터가 포함됩니다.) messenger 및 notifier 서비스와 관련된 작업은 모든 아키텍처에서 작동합니다.
  • Linux 명령줄, JavaScript 및 bash에 대한 기본적인 이해(단, 모든 코드와 명령이 제공되고 설명되므로 관련 지식이 많지 않아도 학습을 완료할 수 있습니다)
  • Docker 및 Docker Compose
  • Node.js 19.x 이상
    • 버전 19.x에서 테스트했지만, 최신 버전의 Node.js에서도 작동할 것으로 예상합니다.
    • Node.js 설치에 대한 자세한 내용은 README의 messenger 서비스 리포지토리를 참조하세요. 또한 asdf를 설치하면 튜토리얼에서 사용한 것과 정확히 동일한 Node.js 버전을 사용할 수 있습니다.
  • curl(대부분의 시스템에 이미 설치되어 있음)
  • 다음에 나열된 기술: 아키텍처 및 사용자 흐름: messenger 및 notifier(다음 섹션에서 다운로드), NGINX Open Source, Jaeger, RabbitMQ.

참고: 이 튜토리얼에서는 messenger 및 notifier 서비스가 Node.js로 작성되었으므로 JavaScript SDK를 사용합니다. 또한 OTel에서 제공되는 정보의 유형을 파악할 수 있도록 OTel의 자동 계측 기능(자동 계측)도 설정합니다. 이 튜토리얼에서는 OTel Node.js SDK에 관해 알아야 할 모든 내용을 설명하지만, 자세한 내용은 OTel 문서를 참조하세요.

설정

  1. 터미널 세션을 시작합니다.
  2. 홈 디렉터리에 microservices-march 디렉터리를 만들고 이 튜토리얼에 사용할 GitHub 리포지토리를 해당 디렉터리에 복제합니다. (다른 디렉터리 이름을 사용하고 그에 맞게 지침을 조정해도 됩니다.) 참고: 명령을 터미널에 더 쉽게 복사하여 붙여넣을 수 있도록 이 튜토리얼에서는 Linux 명령줄 프롬프트를 생략합니다. 물결표(~)는 홈 디렉터리를 나타냅니다.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/messenger --branch mm23-metrics-start git clone https://github.com/microservices-march/notifier --branch mm23-metrics-start git clone https://github.com/microservices-march/platform --branch mm23-metrics-start

과제 1: 기본 OTel 계측 설정

이 챌린지에서는 start the messenger 서비스를 시작하고 OTel 자동 계측을 구성하여 텔레메트리를 콘솔로 전송합니다.

messenger 서비스 실행

  1. Change to the platform 저장소로 이동하여 Docker Compose를 시작합니다:
cd ~/microservices-march/platformdocker compose up -d --build
  1. 이렇게 하면 이후 과제에서 사용할 RabbitMQ와 Jaeger가 시작됩니다.
    • The ‑d 플래그는 컨테이너가 시작된 후 Docker Compose가 컨테이너에서 분리되도록 합니다(그렇지 않으면 컨테이너가 터미널에 연결된 상태로 유지됩니다).
    • The --build 플래그는 Docker Compose가 실행 시 모든 이미지를 다시 빌드하도록 합니다. 이를 통해 파일이 변경될 가능성이 있더라도 실행 중인 이미지를 최신 상태로 유지할 수 있습니다.
  2. Change to the app 디렉터리로 이동합니다. 이 디렉터리는 messenger 리포지토리에 있습니다. 그런 다음 Node.js를 설치합니다(원하는 경우 다른 방법을 사용해도 됩니다):
cd ~/microservices-march/messenger/appasdf install
  1. 종속 항목을 설치합니다:
npm install
  1. Start the PostgreSQL database for the messenger 서비스용 PostgreSQL 데이터베이스를 시작합니다:
docker compose up -d
  1. 데이터베이스 스키마와 테이블을 생성하고 일부 초기 데이터를 삽입합니다:
npm run refresh-db

콘솔로 전송되는 OTel 자동 계측 구성

OTel 자동 계측을 사용하면 추적을 설정하기 위해 messenger 코드베이스를 수정할 필요가 없습니다. 모든 추적 구성은 애플리케이션 코드 자체로 가져오는 대신, 런타임에 Node.js 프로세스로 가져오는 스크립트에 정의됩니다.

여기서는 messenger 서비스의 자동 계측을 구성하고, 트레이스의 가장 기본적인 전송 대상인 콘솔로 보내도록 설정합니다. 과제 2에서는 트레이스를 외부 수집기인 Jaeger로 보내도록 구성을 변경합니다.

  1. 계속해서 app 디렉터리에서 작업합니다. messenger 리포지토리의 핵심 OTel Node.js 패키지를 설치하세요:
npm install @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/auto-instrumentations-node@0.36.4
  1. 이러한 라이브러리는 다음 기능을 제공합니다: 참고: OTel의 JavaScript SDK는 매우 작은 단위로 세분화되어 있다는 특이점이 있습니다. 따라서 이 튜토리얼의 기본 예제를 따라 하는 데에도 몇 가지 패키지를 추가로 설치해야 합니다. 이 튜토리얼에서 다루는 범위를 넘어 계측 작업을 수행할 때 어떤 패키지가 필요한지 알아보려면 OTel의 훌륭한 시작 가이드를 살펴보고 OTel의 GitHub 저장소도 확인하세요.
    • @opentelemetry/sdk-node – OTel 데이터 생성 및 내보내기
    • @opentelemetry/auto-instrumentations-node – 가장 일반적인 모든 Node.js 계측을 기본 구성으로 자동 설정
  2. OTel 추적 설정 및 구성 코드를 담을 tracing.mjs라는 새 파일을 만듭니다:
touch tracing.mjs
  1. 선호하는 텍스트 편집기에서 tracing.mjs 파일을 열고 다음 코드를 추가합니다:
//1
import opentelemetry from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";

//2
const sdk = new opentelemetry.NodeSDK({
  traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
  instrumentations: [getNodeAutoInstrumentations()],
});

//3
sdk.start();
  1. 이 코드는 다음 작업을 수행합니다:
    1. OTel SDK에서 필요한 함수와 객체를 가져옵니다.
    2. NodeSDK의 새 인스턴스를 생성하고 다음과 같이 구성합니다:
      • 스팬을 콘솔로 전송합니다(ConsoleSpanExporter).
      • 자동 계측기를 기본 계측 세트로 사용합니다. 이 계측기는 가장 일반적인 자동 계측 라이브러리를 모두 로드합니다. 이 튜토리얼에서 관련된 라이브러리는 다음과 같습니다:
        • @opentelemetry/instrumentation-pg Postgres 데이터베이스 라이브러리용 (pg)
        • @opentelemetry/instrumentation-express는 Node.js Express 프레임워크용
        • @opentelemetry/instrumentation-amqplib RabbitMQ 라이브러리용 (amqplib)
    3. SDK를 시작합니다.
  2. 3단계에서 만든 자동 계측 스크립트를 가져와 messenger 서비스를 시작합니다.
node --import ./tracing.mjs index.mjs
  1. 잠시 후 콘솔(터미널)에 추적과 관련된 많은 출력이 표시되기 시작합니다:
...
{
  traceId: '9c1801593a9d3b773e5cbd314a8ea89c',
  parentId: undefined,
  traceState: undefined,
  name: 'fs statSync',
  id: '2ddf082c1d609fbe',
  kind: 0,
  timestamp: 1676076410782000,
  duration: 3,
  attributes: {},
  status: { code: 0 },
  events: [],
  links: []
}
...

참고: 챌린지 2에서 다시 사용할 수 있도록 터미널 세션을 열어 두세요.

과제 2: 모든 서비스에 대한 OTel 계측 및 트레이스 시각화 설정

트레이스를 확인하고 분석하는 데 사용할 수 있는 도구는 많지만, 이 튜토리얼에서는 Jaeger를 사용합니다. Jaeger는 스팬과 기타 트레이싱 데이터를 확인할 수 있는 웹 기반 사용자 인터페이스가 내장된 간단한 오픈 소스 엔드투엔드 분산 트레이싱 프레임워크입니다. platform 리포지토리에서 제공하는 인프라에는 Jaeger가 포함되어 있으므로(챌린지 1의 1단계에서 시작함), 복잡한 도구를 다루는 대신 데이터 분석에 집중할 수 있습니다.

브라우저에서 http://localhost:16686 엔드포인트를 통해 Jaeger에 액세스할 수 있지만, 지금 이 엔드포인트에 접속해도 시스템에 관한 정보는 아무것도 표시되지 않습니다. 현재 수집 중인 트레이스가 콘솔로 전송되고 있기 때문입니다! Jaeger에서 트레이스 데이터를 보려면 OpenTelemetry 프로토콜(OTLP) 형식을 사용하여 트레이스를 내보내야 합니다.

이 챌린지에서는 다음 항목에 대한 계측을 구성하여 핵심 사용자 흐름을 계측합니다:

외부 Collector로 전송되는 OTel 자동 계측 구성

다시 말씀드리면, OTel 자동 계측을 사용하면 추적을 설정하기 위해 messenger 코드베이스를 수정할 필요가 없습니다. 대신 모든 추적 구성은 런타임에 Node.js 프로세스로 가져오는 스크립트에 포함됩니다. 여기서는 messenger 서비스에서 생성된 추적의 전송 대상을 콘솔에서 외부 수집기(이 튜토리얼에서는 Jaeger)로 변경합니다.

  1. 챌린지 1에서 사용한 것과 동일한 터미널에서 계속 작업하면서, app 디렉터리로 이동한 후 messenger 리포지토리에 OTLP 익스포터 Node.js 패키지를 설치합니다:
npm install @opentelemetry/exporter-trace-otlp-http@0.36.0
  1. @opentelemetry/exporter-trace-otlp-http 라이브러리는 HTTP를 통해 추적 정보를 OTLP 형식으로 내보냅니다. OTel 외부 수집기로 텔레메트리를 전송할 때 사용됩니다.
  2. Open tracing.mjs 파일(챌린지 1에서 생성하고 편집한 파일)을 열고 다음과 같이 변경합니다. 참고: 이 튜토리얼에서는 편의상 수집기가 기본 위치인 http://localhost:4318/v1/traces에 있다고 가정합니다. 실제 시스템에서는 위치를 명시적으로 설정하는 것이 좋습니다.
    • 파일 상단의 import 문 모음에 다음 줄을 추가합니다:
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
  • OTel SDK에 제공하는 “exporter”를 챌린지 1에서 사용한 콘솔 exporter에서 HTTP를 통해 OTLP 호환 collector로 OTLP 데이터를 전송할 수 있는 exporter로 변경합니다. 다음을 교체하세요:
traceExporter:new opentelemetry.tracing.ConsoleSpanExporter(),
  • 사용:
traceExporter: new OTLPTraceExporter({ headers: {} }),
  1. Press Ctrl+c 키를 눌러 이 터미널에서 messenger 서비스를 중지합니다. 이 서비스는 콘솔로 전송되는 OTel 자동 계측 구성의 4단계에서 시작했습니다. 그런 다음 2단계에서 구성한 새 익스포터를 사용하도록 서비스를 다시 시작합니다:
^cnode --import ./tracing.mjs index.mjs
  1. 별도의 두 번째 터미널 세션을 시작합니다. (이후 지침에서는 이 터미널을 클라이언트 터미널이라고 하고, 1단계와 3단계에서 사용한 원래 터미널을 메신저 터미널이라고 합니다.) 약 10초간 기다린 후 messenger 서비스에 상태 확인 요청을 보냅니다(여러 트레이스를 확인하려면 이 작업을 몇 번 실행해도 됩니다).
curl -X GET http://localhost:4000/health
  1. 요청을 보내기 전에 10초 동안 기다리면 서비스가 시작될 때 자동 계측에서 생성되는 수많은 트레이스 뒤에 해당 트레이스가 기록되므로 더 쉽게 찾을 수 있습니다.
  2. 브라우저에서 Jaeger UI에 접속합니다: http://localhost:16686 OTLP 내보내기가 예상대로 작동하는지 확인합니다. 제목 표시줄에서 검색을 클릭하고 서비스 필드의 드롭다운 메뉴에서 이름이 unknown_service로 시작하는 서비스를 선택합니다. 트레이스 찾기 버튼을 클릭합니다:
  3. 창 오른쪽에서 트레이스를 클릭하면 해당 트레이스에 포함된 스팬 목록이 표시됩니다. 각 스팬은 트레이스의 일부로 실행된 작업을 설명하며, 경우에 따라 여러 서비스가 관련될 수 있습니다. 스크린샷의 jsonParser 스팬은 messenger 서비스의 요청 처리 코드 중 jsonParser 부분을 실행하는 데 걸린 시간을 보여 줍니다.
    올바른 서비스 이름이 표시되도록 자동 계측을 변경하기 전, unknown_service의 스팬 목록을 보여 주는 Jaeger GUI 스크린샷
  4. 5단계에서 언급했듯이 OTel SDK에서 내보낸 서비스 이름(unknown_service)은 의미가 없습니다. 이 문제를 해결하려면 messenger 터미널에서 Ctrl+c를 눌러 messenger 서비스를 중지합니다. 그런 다음 Node.js 패키지 두 개를 추가로 설치합니다:
^c npm install @opentelemetry/semantic-conventions@1.10.0 \ @opentelemetry/resources@1.10.0
  1. 이 두 라이브러리는 다음과 같은 기능을 제공합니다:
    • @opentelemetry/semantic-conventions – OTel 사양에 정의된 트레이스의 표준 속성을 정의합니다.
    • @opentelemetry/resources – OTel 데이터를 생성하는 소스를 나타내는 객체(리소스)를 정의합니다(이 튜토리얼에서는 messenger 서비스).
  2. 텍스트 편집기에서 tracing.mjs 파일을 열고 다음과 같이 변경합니다:
    • 파일 상단의 import 문 모음에 다음 줄을 추가합니다:
import { Resource } from "@opentelemetry/resources";import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
  • OTel 사양의 올바른 키 아래에 resource messenger를 생성하려면 마지막 import 문 뒤에 다음 줄을 추가합니다:
const resource = new Resource({  [SemanticResourceAttributes.SERVICE_NAME]: "messenger",
});
  • 검은색 줄 사이에 주황색으로 강조 표시된 줄을 추가하여 resource 객체를 NodeSDK 생성자에 전달합니다:
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [getNodeAutoInstrumentations()],
});
  1. Restart the messenger 서비스를 다시 시작합니다:
node --import ./tracing.mjs index.mjs
  1. 약 10초간 기다린 다음, 클라이언트 터미널(4단계에서 연 터미널)에서 서버로 상태 확인 요청을 다시 전송합니다(여러 트레이스를 확인하려면 명령을 몇 번 실행해도 됩니다):
curl -X GET http://localhost:4000/health
  1. 참고: 다음 섹션에서 다시 사용할 수 있도록 클라이언트 터미널을 열어 두고, 챌린지 3에서 다시 사용할 수 있도록 메신저 터미널도 열어 두세요.
  2. 브라우저의 Jaeger UI에 messenger라는 새 서비스가 표시되는지 확인합니다(표시되는 데 몇 초 정도 걸릴 수 있으며 Jaeger UI를 새로 고쳐야 할 수도 있습니다).
    심층 검사를 위해 사용할 수 있는 서비스 목록에 messenger가 표시된 Jaeger GUI 스크린샷
  3. Select messenger를 Service 드롭다운 메뉴에서 선택하고 Find Traces 버튼을 클릭하면 messenger 서비스에서 생성된 최근 트레이스를 모두 볼 수 있습니다(스크린샷에는 총 20개 중 가장 최근 트레이스 2개가 표시되어 있습니다).
    messenger 서비스의 가장 최근 트레이스 2개를 보여 주는 Jaeger GUI 스크린샷
  4. 추적을 클릭하면 해당 추적에 포함된 스팬이 표시됩니다. 각 스팬에는 messenger 서비스에서 생성되었음을 나타내는 태그가 올바르게 지정되어 있습니다.
    단일 messenger 스팬의 세부 정보를 보여 주는 Jaeger GUI 스크린샷

notifier 서비스의 OTel 자동 계측 구성

이제 notifier 서비스의 자동 계측을 시작하고 구성합니다. 기본적으로 앞의 두 섹션에서 messenger 서비스에 사용한 것과 동일한 명령을 실행하면 됩니다.

  1. 새 터미널 세션을 엽니다(이후 단계에서는 notifier 터미널이라고 함). notifier 리포지토리의 app 디렉터리로 이동한 후 Node.js를 설치합니다(원하는 경우 다른 방법을 사용해도 됨):
cd ~/microservices-march/notifier/appasdf install
  1. 종속성을 설치합니다:
npm install
  1. Start the PostgreSQL database for the notifier 서비스용 PostgreSQL 데이터베이스를 시작합니다:
docker compose up -d
  1. 데이터베이스 스키마와 테이블을 생성하고 일부 초기 데이터를 삽입합니다:
npm run refresh-db
  1. OTel Node.js 패키지를 설치합니다(패키지의 기능에 대한 설명은 콘솔로 전송되는 OTel 자동 계측 구성의 1단계와 3단계 참조):
npm install @opentelemetry/auto-instrumentations-node@0.36.4 \ @opentelemetry/exporter-trace-otlp-http@0.36.0 \ @opentelemetry/resources@1.10.0 \ @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/semantic-conventions@1.10.0
  1. 다음 이름으로 새 파일을 만듭니다. tracing.mjs:
touch tracing.mjs
  1. 선호하는 텍스트 편집기에서 tracing.mjs 파일을 열고 다음 스크립트를 추가하여 OTel SDK를 실행합니다:
import opentelemetry from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";

const resource = new Resource({
  [SemanticResourceAttributes.SERVICE_NAME]: "notifier",
});

const sdk = new opentelemetry.NodeSDK({
  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
  1. 참고: 이 스크립트는 messenger 서비스에 사용된 스크립트와 완전히 동일하지만, SemanticResourceAttributes.SERVICE_NAME 필드의 값이 notifier라는 점만 다릅니다.
  2. OTel 자동 계측을 사용하여 notifier 서비스를 시작합니다:
node --import ./tracing.mjs index.mjs
  1. 약 10초간 기다린 후 클라이언트 터미널에서 notifier 서비스로 상태 확인 요청을 보냅니다. 이 서비스는 포트 4000에서 수신 대기 중인 messenger 서비스와의 충돌을 방지하기 위해 포트 5000에서 수신 대기합니다:
curl http://localhost:5000/health
  1. 참고: 챌린지 3에서 다시 사용할 수 있도록 클라이언트 및 알림 프로그램 터미널을 열어 두세요.
  2. 브라우저의 Jaeger UI에 notifier라는 새 서비스가 표시되는지 확인합니다.
    스팬을 심층적으로 검사할 수 있는 서비스 목록에 notifier가 표시된 Jaeger GUI 스크린샷

NGINX의 OTel 계측 구성

NGINX에서는 OTel 자동 계측 방식 대신 추적을 수동으로 설정합니다. 현재 OTel을 사용해 NGINX를 계측하는 가장 일반적인 방법은 C로 작성된 모듈을 사용하는 것입니다. 타사 모듈은 NGINX 생태계의 중요한 부분이지만 설정하려면 다소 작업이 필요합니다. 이 튜토리얼에서는 해당 설정 과정을 안내합니다. 자세한 배경 정보는 블로그의 NGINX 및 NGINX Plus용 타사 동적 모듈 컴파일을 참조하세요.

  1. 새 터미널 세션(NGINX 터미널)을 시작하고 messenger 리포지토리의 루트 디렉터리로 이동한 다음, load-balancer라는 새 디렉터리와 Dockerfile, nginx.conf, opentelemetry_module.conf라는 새 파일을 만듭니다:
cd ~/microservices-march/messenger/mkdir load-balancer cd load-balancer touch Dockerfile touch nginx.conf touch opentelemetry_module.conf
  1. 선호하는 텍스트 편집기에서 Dockerfile을 열고 다음 내용을 추가합니다(주석에 각 줄의 역할이 설명되어 있지만, 모든 내용을 이해하지 않아도 Docker 컨테이너를 빌드하고 실행할 수 있습니다):
FROM --platform=amd64 nginx:1.23.1
# Replace the nginx.conf file with our own
COPY nginx.conf /etc/nginx/nginx.conf

# Define the version of the NGINX OTel module
ARG OPENTELEMETRY_CPP_VERSION=1.0.3

# Define the search path for shared libraries used when compiling and running NGINX
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/opentelemetry-webserver-sdk/sdk_lib/lib

# 1. Download the latest version of Consul template and the OTel C++ web server module, otel-webserver-module
ADD https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv${OPENTELEMETRY_CPP_VERSION}/opentelemetry-webserver-sdk-x64-linux.tgz /tmp

RUN apt-get update \
  && apt-get install -y --no-install-recommends dumb-init unzip \
# 2. Extract the module files
  && tar xvfz /tmp/opentelemetry-webserver-sdk-x64-linux.tgz -C /opt \
  && rm -rf /tmp/opentelemetry-webserver-sdk-x64-linux.tgz \
# 3. Install and add the 'load_module' directive at the top of the main NGINX configuration file
  && /opt/opentelemetry-webserver-sdk/install.sh \
  && echo "load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/1.23.1/ngx_http_opentelemetry_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf

# 4. Copy in the configuration file for the NGINX OTel module
COPY opentelemetry_module.conf /etc/nginx/conf.d/opentelemetry_module.conf

EXPOSE 8085

STOPSIGNAL SIGQUIT
  1. Open nginx.conf을 열고 다음 내용을 추가합니다:
events {}
http {
    include /etc/nginx/conf.d/opentelemetry_module.conf;

    upstream messenger {
        server localhost:4000;
    }

    server {
        listen 8085;

        location / {
            proxy_pass http://messenger;
        }
    }
}
  1. 이 매우 기본적인 NGINX 구성 파일은 NGINX에 다음과 같이 지시합니다. 참고: 이는 프로덕션 환경에서 NGINX를 리버스 프록시 및 로드 밸런서로 사용할 때의 실제 구성과 매우 유사합니다. 유일한 주요 차이점은 server 지시어의 인수가 upstream 블록에서 일반적으로 localhost가 아닌 도메인 이름이나 IP 주소라는 점입니다.
    • 다음과 같은 업스트림 그룹을 설정합니다: messenger 서비스 인스턴스 그룹을 나타내는 messenger service instances
    • 포트 8085에서 HTTP 요청 수신 대기
    • 다음으로 시작하는 경로에 대한 모든 수신 요청을 /(즉, 모든 수신 요청) messenger 업스트림으로 전달
  2. Open opentelemetry_module.conf 파일을 열고 다음 내용을 추가합니다:
NginxModuleEnabled ON;NginxModuleOtelSpanExporter otlp;
NginxModuleOtelExporterEndpoint localhost:4317;
NginxModuleServiceName messenger-lb;
NginxModuleServiceNamespace MicroservicesMarchDemoArchitecture;
NginxModuleServiceInstanceId DemoInstanceId;
NginxModuleResolveBackends ON;
NginxModuleTraceAsError ON;
  1. NGINX와 NGINX OTel 모듈이 포함된 Docker 이미지를 빌드합니다:
docker build -t messenger-lb .
  1. NGINX 리버스 프록시 및 로드 밸런서용 Docker 컨테이너를 시작합니다:
docker run --rm --name messenger-lb -p 8085:8085 --network="host" messenger-lb
  1. 클라이언트 터미널에서 NGINX 리버스 프록시 및 로드 밸런서를 통해 messenger 서비스에 상태 확인 요청을 전송합니다(이 요청은 기다리지 않고 바로 전송해도 됩니다):
curl http://localhost:8085/health
  1. 참고: 챌린지 3에서 다시 사용할 수 있도록 NGINX 및 클라이언트 터미널을 열어 두세요.
  2. 브라우저에서 새 messenger-lb 서비스가 이전에 시작한 서비스와 함께 Jaeger UI에 표시되는지 확인합니다. 브라우저에서 Jaeger UI를 새로고침해야 할 수도 있습니다.
    이제 messenger-lb가 포함된, 스팬을 심층적으로 검사할 수 있는 서비스 목록을 보여 주는 Jaeger GUI 스크린샷

과제 3: OTel 트레이스 읽는 법 배우기

In 아키텍처 및 사용자 흐름에서는 사용자 흐름의 각 단계를 설명했습니다. 다시 요약하면 다음과 같습니다.

  1. 사용자가 다른 사용자에게 메시지를 보내 대화를 시작합니다.
  2. NGINX 리버스 프록시가 메시지를 가로채 messenger 서비스로 전달합니다.
  3. The messenger 서비스는 메시지를 데이터베이스에 기록한 다음 RabbitMQ를 통해 이벤트를 전송합니다.
  4. The notifier 서비스는 해당 이벤트를 수신하고 수신자(두 번째 사용자)의 알림 설정을 조회한 후, 수신자가 선호하는 방식으로 알림을 보냅니다.

그리고 텔레메트리 구현의 목표는 다음과 같습니다:

  1. 새로운 메시지 흐름을 완료하기 위해 요청이 거치는 모든 단계를 파악합니다.
  2. 정상적인 조건에서 해당 흐름이 5초 이내에 처음부터 끝까지 실행된다는 확신을 갖습니다.
  3. See how long it takes the notifier 서비스가 messenger 서비스에서 전송한 이벤트를 처리하기 시작하는 데 걸리는 시간을 확인합니다.

이 챌린지에서는 OTel 계측에서 생성된 트레이스가 앞서 언급한 목표를 충족하는지 평가하는 방법을 알아봅니다. 먼저 시스템을 직접 실행해 보고 몇 가지 트레이스를 생성합니다. 그런 다음 메시지 흐름의 트레이스와 이 트레이스에서 NGINX, messenger 서비스, 그리고 notifier 서비스가 생성한 구간을 살펴봅니다.

트레이스 데이터 생성

클라이언트 터미널에서 대화를 설정하고 두 사용자 간에 메시지를 몇 개 주고받습니다:

curl -X POST \ -H "Content-Type: application/json" \ -d '{"participant_ids": [1, 2]}' \ 'http://localhost:8085/conversations' curl -X POST \ -H "User-Id: 1" \ -H "Content-Type: application/json" \ -d '{"content": "This is the first message"}' \ 'http://localhost:8085/conversations/1/messages' curl -X POST \ -H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'

다음과 같은 출력은 notifier 서비스에서 생성되며 notifier 터미널에 표시됩니다:

Received new_message: {"type":"new_message","channel_id":1,"user_id":1,"index":1,"participant_ids":[1,2]}Sending notification of new message via sms to 12027621401

Received new_message:  {"type":"new_message","channel_id":1,"user_id":2,"index":2,"participant_ids":[1,2]}

Sending notification of new message via email to the_hotstepper@kamo.ze

Sending notification of new message via sms to 19147379938

트레이스를 읽을 준비하기

브라우저에서 Jaeger UI를 열고 messenger-lb를 Service 드롭다운 메뉴에서 선택한 다음 Find Traces 버튼을 클릭합니다. 흐름의 맨 처음부터 시작되는 트레이스 목록이 표시됩니다. 다음 스크린샷과 같이 아무 트레이스나 클릭하면 세부 정보를 확인할 수 있습니다:

흐름의 전체 스팬 집합을 보여 주는 Jaeger GUI 스크린샷

여기저기를 클릭하며 잠시 살펴보세요. 그런 다음 계속 진행하기 전에, 추적 정보가 챌린지 3의 소개에 나열된 계측 목표를 달성하는 데 어떻게 도움이 되는지 생각해 보세요. 다음과 같은 질문을 고려할 수 있습니다:

  • 목표를 달성하는 데 도움이 되는 정보는 무엇인가요?
  • 어떤 정보가 누락되어 있나요?
  • 관련이 없는 정보는 무엇인가요?

트레이스의 NGINX(messenger-lb) 섹션 살펴보기

목표 1: 새 메시지 흐름에서 요청이 거치는 모든 단계 확인

먼저 상위 스팬 내에 11개의 하위 스팬이 있는 NGINX 스팬부터 살펴보겠습니다. 현재 NGINX 구성이 매우 단순하기 때문에 하위 스팬에는 특별히 흥미로운 내용이 없으며, NGINX 요청 처리 수명 주기의 각 단계에 걸린 시간만 보여 줍니다. 하지만 상위 스팬(맨 처음 스팬)에서는 몇 가지 흥미로운 정보를 확인할 수 있습니다:

트레이스의 NGINX(messenger-lb) 섹션에 있는 상위 스팬을 보여 주는 Jaeger GUI 스크린샷

  • Under 태그 아래에 다음 속성이 표시됩니다. 이 세 가지 정보를 종합하면 “POST 요청이 /conversations/1/messages에 전송되었고 응답은 201(정상적으로 생성됨)이었다”는 의미입니다. 이는 아키텍처 및 사용자 흐름의 1단계와 4a단계에 해당합니다).
    • http.method 필드 – POST (REST 용어로는 생성을 의미함)
    • http.status_code 필드 – 201 (성공적으로 생성되었음을 나타냄)
    • http.target 필드 – conversations/1/messages(메시지 엔드포인트)
  • Under 프로세스에서 webengine.name 필드는 요청 중 NGINX에서 처리되는 부분임을 나타냅니다.

또한 messenger와 notifier의 범위가 messenger-lb의 conversations/1 범위 내에 중첩되어 있으므로(트레이스 읽기 준비의 스크린샷 참조), NGINX 리버스 프록시를 통해 messenger 서비스로 전송된 요청이 흐름에서 예상되는 모든 구성 요소를 거쳤음을 알 수 있습니다.

이 정보를 통해 NGINX 리버스 프록시가 흐름의 일부였음을 확인할 수 있으므로 목표를 충족합니다.

목표 2: 흐름이 5초 이내에 실행되는지 확인

In the list of spans labeled messenger-lb 레이블이 지정된 스팬 목록에서 가장 최근 스팬(목록 맨 아래에 있음)을 확인하여 요청의 NGINX 처리 부분에 걸린 시간을 살펴봅니다. 스크린샷에서 스팬은 589마이크로초(µs)에 시작해 24µs 동안 지속되었으므로, 전체 리버스 프록시 작업에는 약 0.6밀리초(ms)인 613µs밖에 걸리지 않았습니다. (직접 튜토리얼을 실행하면 정확한 값은 당연히 달라집니다.)

트레이스의 NGINX(messenger-lb) 섹션에 있는 스팬을 보여 주는 Jaeger GUI 스크린샷

이와 같은 구성에서는 대부분의 경우 측정값이 다른 측정값과 비교할 때만 의미가 있으며, 시스템마다 달라집니다. 하지만 이 경우에는 이 작업의 소요 시간이 5초에 근접할 가능성이 전혀 없습니다.

이 정보를 통해 NGINX 작업에 5초에 가까운 시간조차 걸리지 않았음을 확인할 수 있으므로 목표를 달성할 수 있습니다. 처리 흐름에 매우 느린 작업이 있다면 이후 단계에서 발생하는 것임이 틀림없습니다.

목표 3: notifier 서비스가 messenger 서비스에서 전송한 이벤트를 읽는 데 걸리는 시간 확인

NGINX 리버스 프록시 계층에는 이에 관한 정보가 없으므로, messenger 스팬으로 넘어가면 됩니다.

트레이스의 messenger 섹션 살펴보기

목표 1: 새로운 메시지 흐름에서 요청이 거치는 모든 단계 확인

추적의 messenger 서비스 섹션에는 11개의 스팬이 더 있습니다. 여기서도 대부분의 하위 스팬은 Express 프레임워크가 요청을 처리할 때 사용하는 기본 단계에 관한 것으로, 그다지 흥미롭지 않습니다. 하지만 상위 스팬(맨 처음 스팬)에서는 몇 가지 흥미로운 정보를 다시 확인할 수 있습니다:

트레이스의 messenger 섹션에 있는 상위 스팬을 보여 주는 Jaeger GUI 스크린샷

Under 태그에서 다음 속성을 확인할 수 있습니다:

  • http.method 필드 – POST(다시 말해, REST 용어로는 생성을 의미함)
  • http.route 필드 – /conversations/:conversationId/messages(메시지 라우트)
  • http.target 필드 – /conversations/1/messages(메시지 엔드포인트)

이 정보는 messenger 서비스가 흐름의 일부였으며 호출된 엔드포인트가 새 메시지 엔드포인트였음을 보여 주므로 목표를 충족합니다.

목표 2: 흐름이 5초 이내에 실행되는지 확인

다음 스크린샷에서 볼 수 있듯이 추적의 messenger 부분은 1.28ms에 시작하여 36.28ms에 종료되었으며, 총 소요 시간은 35ms였습니다. 이 시간의 대부분은 JSON을 구문 분석하는 데(middleware - jsonParser) 사용되었고, 그보다 훨씬 더 많은 시간은 데이터베이스에 연결하는 데(pg-pool.connect 및 tcp.connect) 사용되었습니다.

메시지를 작성하는 과정에서 여러 SQL 쿼리도 실행된다는 점을 고려하면 이는 타당합니다. 따라서 해당 쿼리의 실행 시간을 포착할 수 있도록 자동 계측 구성을 확장하는 것이 좋습니다. (이 튜토리얼에서는 이러한 추가 계측을 다루지 않지만, 챌린지 4에서는 데이터베이스 쿼리를 감싸는 데 사용할 수 있는 스팬을 직접 생성합니다.)

트레이스의 메신저 섹션에 있는 스팬과 각 스팬의 소요 시간을 보여 주는 Jaeger GUI 스크린샷

이 정보는 messenger 작업에 5초 가까이 걸리지 않는다는 것을 보여 주므로 목표를 충족합니다. 흐름에 매우 느린 작업이 있다면 이후 단계에서 발생하는 것이 분명합니다.

목표 3: notifier 서비스가 messenger 서비스에서 디스패치한 이벤트를 읽는 데 걸리는 시간 확인

NGINX 스팬과 마찬가지로 messenger 스팬에는 이 정보가 포함되어 있지 않으므로 notifier 스팬으로 이동할 수 있습니다.

트레이스의 notifier 섹션 살펴보기

목표 1: 새로운 메시지 흐름에서 요청이 거치는 모든 단계 확인

추적의 notifier 섹션에는 두 개의 스팬만 포함되어 있습니다:

트레이스의 notifier 섹션에 있는 두 개의 스팬을 보여 주는 Jaeger GUI 스크린샷

  • The chat_queue process 스팬 – notifier 서비스가 chat_queue 메시지 큐의 이벤트를 처리했음을 확인합니다
  • The pg-pool.connect 스팬 – 이벤트를 처리한 후 notifier 서비스가 데이터베이스에 연결했음을 보여줍니다

이러한 스팬에서 제공되는 정보만으로는 모든 단계를 파악하려는 목표를 일부만 달성할 수 있습니다. notifier 서비스가 큐에서 이벤트를 소비하는 지점까지 도달했다는 것은 알 수 있지만, 다음 사항은 알 수 없습니다:

  • 이 서비스에서 전송한 메시지 알림이 messenger 서비스에서 발행한 이벤트에 해당하는지
  • 관련 메시지 알림이 메시지 수신자에게 올바르게 전송되었습니다

이는 notifier 서비스 흐름을 완전히 이해하려면 다음을 수행해야 함을 의미합니다:

  • 알림이 전송되는 과정을 보여 주는 스팬을 수동으로 계측합니다
  • Ensure that there’s an explicit connection between the event dispatched by the messenger 서비스에서 발송한 이벤트와 notifier 서비스에서 소비한 이벤트가 추적 ID를 통해 명시적으로 연결되도록 합니다.

목표 2: 흐름이 5초 이내에 실행되는지 확인

Looking at the overall timing of the notifier 서비스 스팬의 전체 소요 시간을 살펴보면, 요청이 흐름의 notifier 구간에서 30.77ms를 소비했음을 알 수 있습니다. 하지만 전체 흐름의 '종료'(수신자에게 알림 전송)를 나타내는 스팬이 없으므로, 이 흐름 구간의 총 소요 시간이나 작업이 완료되기까지 걸린 전체 시간을 확인할 수 없습니다.

목표 3: notifier 서비스가 messenger 서비스에서 전송한 이벤트를 읽는 데 걸리는 시간 확인

하지만 chat_queue process 스팬이 notifier 서비스에서 6.12ms에 시작되었으며, 이는 chat_queue send 스팬이 messenger 서비스에서 4.12ms에 시작된 지 2ms 후임을 확인할 수 있습니다.

메신저 서비스가 디스패치한 이벤트를 알림 서비스가 소비하는 모습을 보여 주는 Jaeger GUI 스크린샷

이 목표는 notifier가 messenger 서비스에서 이벤트를 디스패치한 지 2ms 후에 해당 이벤트를 소비했다는 사실을 알 수 있으므로 달성됩니다. 목표 2와 달리, 이 목표를 달성하기 위해 이벤트가 완전히 처리되었는지 또는 처리에 얼마나 걸렸는지 알 필요는 없습니다.

결론

현재 OTel 자동 계측에서 생성된 트레이스를 분석한 결과, 다음과 같은 점이 분명합니다:

  • 이러한 스팬 중 상당수는 현재 형태로는 유용하지 않습니다:
    • NGINX는 권한 확인 및 파일 제공과 같은 기능과 관련된 스팬을 생성하지만, 이러한 스팬은 사용자가 중요하게 여기는 역할인 리버스 프록시와는 관련이 없습니다. 그러나 현재 NGINX용 OTel 계측에서는 관련 없는 스팬을 제외할 수 없으므로 달리 조치할 수 있는 방법이 없습니다.
    • Node.js 서비스(messenger 및 notifier 서비스)의 스팬 중 일부는 목표와 관련이 있어 보입니다. JSON 파싱, request handler, 그리고 모든 데이터베이스 작업에 대한 스팬이 이에 해당합니다. 일부 미들웨어 스팬(예: expressInit 및 corsMiddleware)은 관련이 없어 보이므로 제거할 수 있습니다.
  • 다음 항목에 대한 중요 스팬이 누락되었습니다:
    • Notifications sent by the notifier 서비스에서 전송한 알림
    • RabbitMQ 이벤트 간의 명확한 매핑: messenger 서비스에서 발행한 이벤트와 notifier 서비스에서 처리한 이벤트

이는 기본 계측이 마지막 목표를 충족한다는 의미입니다:

  • See how long it takes the notifier 서비스가 messenger 서비스에서 전달한 이벤트 처리를 시작하는 데 걸리는 시간 확인

하지만 처음 두 가지 목표를 달성하기에는 정보가 충분하지 않습니다:

  • 새 메시지 흐름에서 요청이 거치는 모든 단계를 이해합니다
  • 정상적인 상황에서 흐름이 처음부터 끝까지 5초 이내에 실행된다는 확신을 갖습니다.

과제 4: 트레이스 판독값을 기반으로 계측 최적화

이 과제에서는 과제 3에서 수행한 트레이스 분석을 바탕으로 OTel 계측을 최적화합니다. 여기에는 불필요한 스팬 제거, 새 사용자 지정 스팬 생성, 그리고 확인 작업이 포함됩니다. 확인 작업에서는 알림 서비스가 소비하는 이벤트가 메신저 서비스에서 생성된 이벤트와 동일한지 검증합니다.

불필요한 스팬 제거

  1. 선호하는 텍스트 편집기에서 tracing.mjs 파일을 열고, app 디렉터리는 messenger 리포지토리에 있습니다. 그런 다음 파일 상단의 import 문 목록 끝에 다음 내용을 추가합니다:
const IGNORED_EXPRESS_SPANS = new Set([  "middleware - expressInit",
  "middleware - corsMiddleware",
]);
  1. 다음은 Jaeger UI의 아래 스크린샷에 표시된 스팬 목록을 바탕으로, 이 흐름에 유용한 정보를 제공하지 않아 트레이스에서 제외할 스팬 이름 집합을 정의합니다. 스크린샷에 나열된 다른 스팬도 필요하지 않다고 판단되면 IGNORED_EXPRESS_SPANS 목록에 추가할 수 있습니다.
    관련 정보를 제공하지 않아 추적에서 생략할 수 있는 메신저 서비스의 여러 스팬 목록을 보여 주는 Jaeger GUI 스크린샷
  2. 주황색으로 강조 표시된 줄을 변경하여 자동 계측 구성에 원하지 않는 스팬을 제외하는 필터를 추가합니다:
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [getNodeAutoInstrumentations()],
});
  1. 다음과 같이 변경합니다:
const sdk = new opentelemetry.NodeSDK({  resource,
  traceExporter: new OTLPTraceExporter({ headers: {} }),
  instrumentations: [ getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-express": { ignoreLayers: [ (name) => { return IGNORED_EXPRESS_SPANS.has(name); }, ], }, }), ],
});
  1. getNodeAutoInstrumentations 함수는 1단계에서 정의한 스팬 집합을 참조하여 @opentelemetry/instrumentation-express에서 생성된 트레이스에서 해당 스팬을 필터링합니다. 즉, return 문은 IGNORED_EXPRESS_SPANS에 속하는 스팬에 대해 true로 평가되며, ignoreLayers 문은 해당 스팬을 트레이스에서 제거합니다.
  2. messenger 터미널에서 Ctrl+c를 눌러 messenger 서비스를 중지합니다. 그런 다음 다시 시작합니다:
^cnode --import ./tracing.mjs index.mjs
  1. 약 10초 정도 기다린 다음, 클라이언트 터미널에서 새 메시지를 전송합니다:
curl -X POST \	-H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'
  1. Jaeger UI에서 messenger 스팬을 다시 확인합니다. 두 middleware 스팬인 expressInit 및 corsMiddleware가 더 이상 표시되지 않습니다(Challenge 3의 목표 2에 있는 트레이스의 messenger 섹션 살펴보기 스크린샷과 비교할 수 있습니다).
    계측 설정을 변경하여 두 스팬을 트레이스에서 필터링한 후, 해당 스팬이 더 이상 트레이스에 포함되지 않는 모습을 보여 주는 Jaeger GUI 스크린샷

사용자 지정 스팬 설정

이 섹션에서는 처음으로 애플리케이션 코드를 다룹니다. 자동 계측은 애플리케이션을 변경하지 않고도 상당히 많은 정보를 생성하지만, 일부 인사이트는 비즈니스 로직의 특정 부분을 계측해야만 얻을 수 있습니다.

새로 계측하는 메시지 흐름에서는 메시지 수신자에게 알림이 전송되는 과정을 추적하는 것이 그 예입니다.

  1. Open index.mjs 파일을 app 디렉터리의 notifier 리포지토리에서 엽니다. 이 파일에는 서비스의 모든 비즈니스 로직이 포함되어 있습니다. 파일 상단의 import 문 목록 끝에 다음 줄을 추가합니다:
import { trace } from "@opentelemetry/api";
  1. 다음 코드를 바꿉니다(파일의 약 91번째 줄):
for (let pref of preferences) {
  console.log(
    `Sending notification of new message via ${pref.address_type} to ${pref.address}`
  );
}
  1. 다음으로 바꿉니다:
const tracer = trace.getTracer("notifier"); // 1tracer.startActiveSpan( // 2
  "notification.send_all",
  {
    attributes: {
      user_id: msg.user_id,
    },
  },
  (parentSpan) => {
    for (let pref of preferences) {
      tracer.startActiveSpan(  // 3
        "notification.send",
        {
          attributes: { // 4
            notification_type: pref.address_type,
            user_id: pref.user_id,
          },
        },
        (span) => {
          console.log(
            `Sending notification of new message via ${pref.address_type} to ${pref.address}`
          );
          span.end(); // 5
        }
      );
    }
    parentSpan.end(); // 6
  }
);
  1. 새 코드는 다음과 같이 동작합니다. 상위 스팬을 두면 사용자에 대한 알림 기본 설정을 찾지 못하더라도 각 ‘알림 보내기’ 작업이 반드시 보고됩니다.
    1. OTel 추적과 상호 작용하는 전역 객체인 tracer를 가져옵니다.
    2. Starts a new parent span called notification.send_all이라는 새 부모 스팬을 시작하고, 메시지 발신자를 식별하도록 user_id 속성을 설정합니다.
    3. 수신자의 알림 설정을 열거하는 루프에 진입하고, notification.send라는 새 하위 스팬이 notification.send_all 아래에 생성됩니다. 각 알림마다 새 스팬이 생성됩니다.
    4. 하위 스팬에 추가 속성을 설정합니다:
      • notification_type – sms 또는 email 중 하나
      • user_id – 알림을 받는 사용자의 ID
    5. 각 하위 notification.send 스팬을 차례로 종료합니다.
    6. 상위 notification.send_all 스팬을 종료합니다.
  2. 알림 서비스 터미널에서 Ctrl+c를 눌러 notifier 서비스를 중지합니다. 그런 다음 다시 시작합니다:
^cnode --import ./tracing.mjs index.mjs
  1. 약 10초간 기다린 다음, 클라이언트 터미널에서 새 메시지를 전송합니다:
curl -X POST \	-H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'
  1. Jaeger UI에서 notifier 스팬을 다시 확인합니다. 상위 스팬 하나와 하위 스팬 두 개가 표시되며, 각 스팬에는 ‘알림 보내기’ 작업이 있습니다.
    알림 서비스 코드에 세 개의 새 스팬을 정의한 결과를 보여 주는 Jaeger GUI 스크린샷

이제 새로운 메시지 흐름에서 요청이 거치는 모든 단계를 확인할 수 있으므로 첫 번째와 두 번째 목표를 완전히 달성할 수 있습니다. 각 스팬의 타이밍을 통해 이러한 단계 사이에서 발생하는 지연을 파악할 수 있습니다.

메신저와 알림기가 동일한 이벤트를 처리하는지 확인

흐름을 완전히 파악하려면 한 가지를 더 확인해야 합니다. notifier 서비스에서 처리 중인 이벤트가 실제로 messenger 서비스에서 디스패치한 이벤트와 동일한가요?

두 트레이스를 연결하기 위해 명시적으로 변경할 필요는 없습니다. 하지만 자동 계측이 알아서 해 줄 것이라고 무턱대고 믿어서도 안 됩니다.

이를 염두에 두고, NGINX 서비스에서 시작된 트레이스가 notifier 서비스에서 처리되는 트레이스와 실제로 동일한지(동일한 트레이스 ID를 갖는지) 확인할 수 있도록 간단한 디버그 코드를 추가합니다.

  1. Open the index.mjs 파일을 app 디렉터리에서 messenger 리포지토리를 열고 다음과 같이 변경합니다:
    • 상단의 import 문 목록 끝에 다음 줄을 추가합니다:
import { trace } from "@opentelemetry/api";
  • 검은색으로 표시된 기존 줄 아래에 주황색으로 강조된 줄을 추가합니다:
async function createMessageInConversation(req, res) {
  const tracer = trace.getActiveSpan(); console.log("TRACE_ID: ", tracer.spanContext().traceId);
  • 새로 추가된 줄은 새 메시지 생성을 처리하는 TRACE_ID 함수 내부에서 messenger를 출력합니다.
  1. Open the index.mjs 파일을 app 디렉터리의 notifier 리포지토리에서 열고, 기존의 검은색 줄 아래에 주황색으로 강조 표시된 줄을 추가합니다:
export async function handleMessageConsume(channel, msg, handlers) {  console.log("RABBIT_MQ_MESSAGE: ", msg);
  1. 새로 추가된 줄은 notifier 서비스가 수신한 AMQP 이벤트의 전체 내용을 출력합니다.
  2. 다음 명령을 messenger 및 notifier 터미널에서 각각 실행하여 messenger 및 notifier 서비스를 중지한 후 다시 시작합니다:
^cnode --import ./tracing.mjs index.mjs
  1. 약 10초간 기다린 후 클라이언트 터미널에서 메시지를 다시 전송합니다:
curl -X POST \	-H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'
  1. Look at the logs for the messenger 및 notifier 서비스의 로그를 확인합니다. messenger 서비스 로그에는 메시지의 추적 ID를 보여 주는 다음과 같은 줄이 포함되어 있습니다(튜토리얼을 실행할 때 실제 ID는 다릅니다):
TRACE_ID:  29377a9b546c50be629c8e64409bbfb5
  1. 마찬가지로 notifier 서비스 로그는 다음과 같은 출력에 추적 ID를 표시합니다:
_spanContext: {
  traceId: '29377a9b546c50be629c8e64409bbfb5',
  spanId: 'a94e9462a39e6dbf',
  traceFlags: 1,
  traceState: undefined
},
  1. 콘솔의 추적 ID가 일치하지만, 마지막 단계로 Jaeger UI의 추적 ID와 비교할 수 있습니다. 해당 추적 ID 엔드포인트에서 UI를 여세요(사용자의 엔드포인트는 다르지만, 이 예시에서는 http://localhost:16686/trace/29377a9b546c50be629c8e64409bbfb5입니다). 그러면 전체 추적을 확인할 수 있습니다. Jaeger 추적에서 다음 사항을 확인할 수 있습니다:
    • The AMQP auto‑instrumentation in the messenger 서비스의 AMQP 자동 계측은 이벤트가 디스패치될 때 이 추적 ID를 메타데이터의 일부로 추가합니다.
    • The AMQP auto‑instrumentation in the notifier 서비스의 AMQP 자동 계측은 해당 메타데이터를 사용해 추적 컨텍스트를 적절히 설정합니다.

참고: 실제 프로덕션 시스템에서는 흐름이 예상대로 작동하는 것을 확인한 후 이 섹션에서 추가한 코드를 제거해야 합니다.

리소스 정리

이 튜토리얼을 진행하면서 몇 개의 컨테이너와 이미지를 만들었습니다. 다음 안내에 따라 이를 삭제하세요.

  • 실행 중인 Docker 컨테이너를 모두 제거하려면:
docker rm $(docker stop messenger-lb)
  • To remove the platform 서비스와 messenger 및 notifier 데이터베이스 서비스를 제거하려면:
cd ~/microservices-march/platform && docker compose down cd ~/microservices-march/notifier && docker compose down cd ~/microservices-march/messenger && docker compose down

다음 단계

축하합니다. 튜토리얼을 완료하셨습니다!

  • NGINX 리버스 프록시와 두 개의 Node.js 서비스 전반에 OTel 계측을 설정했습니다.
  • OTel 자동 계측에서 제공하는 데이터를 비판적인 시각으로 검토하고, OTel 실습 목표를 달성하는 데 필요한 누락된 텔레메트리 데이터를 추가했습니다:
    • 애플리케이션 코드를 직접 변경하지 않고도 메시징 시스템을 통과하는 특정 요청의 흐름을 적절히 파악할 수 있었습니다.
    • 정상적인 상황에서 해당 흐름이 처음부터 끝까지 5초 이내에 실행되는 것을 확인했습니다.

하지만 이상적인 추적 구성이 어떤 모습일지에 대해서는 아직 겉핥기 수준에 불과합니다! 프로덕션 환경에서는 각 데이터베이스 쿼리에 사용자 지정 스팬을 추가하고, 각 서비스의 컨테이너 ID와 같은 런타임 세부 정보를 설명하는 추가 메타데이터를 모든 스팬에 포함할 수 있습니다. 또한 나머지 두 가지 유형의 OTel 데이터(메트릭 및 로깅)를 구현하여 시스템 상태를 종합적으로 파악할 수도 있습니다.

마이크로서비스에 대해 계속 학습하려면 Microservices March 2023을 확인해 보세요. 유닛 4: 관측 가능성으로 마이크로서비스의 혼란과 복잡성 관리하기에서는 관측 가능성 데이터의 세 가지 주요 유형, 인프라와 애플리케이션 간 정렬의 중요성, 심층 데이터 분석을 시작하는 방법을 알아봅니다.


공유

관련 블로그 게시물

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
NGINX | 2023. 11. 30.

시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric

이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
NGINX | 2023. 03. 23.

F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법

앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
NGINX | 2021. 09. 27.

Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션

개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX | 2021. 07. 06.

진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법

NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX | 2021. 06. 30.

NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리

NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
NGINX | 2015. 03. 25.

Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유

Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.