gRPC C++에서 기본 OpenTelemetry 플러그인 설정

1. 소개

이 Codelab에서는 gRPC를 사용하여 C++로 작성된 경로 매핑 애플리케이션의 기반을 형성하는 클라이언트와 서버를 만듭니다.

튜토리얼을 마치면 gRPC OpenTelemetry 플러그인으로 계측된 간단한 gRPC HelloWorld 애플리케이션이 있으며 내보낸 관측 가능성 측정항목을 Prometheus에서 볼 수 있습니다.

학습할 내용

  • 기존 gRPC C++ 애플리케이션용 OpenTelemetry 플러그인을 설정하는 방법
  • 로컬 Prometheus 인스턴스 실행
  • Prometheus로 측정항목 내보내기
  • Prometheus 대시보드에서 측정항목 보기

2. 시작하기 전에

필요한 항목

  • git
  • curl
  • build-essential
  • clang
  • bazel를 사용하여 이 Codelab에서 예시 빌드

다음 기본 요소를 설치합니다.

sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install -y git curl build-essential clang

bazelbazelisk을 통해 설치할 수 있습니다. 최신 버전은 여기에서 확인할 수 있습니다.

간단하게 설정하는 방법은 다음과 같이 경로에 bazel 바이너리로 설치하는 것입니다.

sudo cp bazelisk-linux-amd64 /usr/local/bin/bazel
sudo chmod a+x /usr/local/bin/bazel

또는 CMake를 사용할 수도 있습니다. CMake 사용에 관한 안내는 여기에서 확인할 수 있습니다.

코드 가져오기

학습을 간소화하기 위해 이 Codelab에서는 시작하는 데 도움이 되는 사전 빌드된 소스 코드 스캐폴드를 제공합니다. 다음 단계에서는 애플리케이션에서 gRPC OpenTelemetry 플러그인을 계측하는 방법을 설명합니다.

grpc-codelabs

이 Codelab의 스캐폴드 소스 코드는 이 GitHub 디렉터리에서 확인할 수 있습니다. 코드를 직접 구현하지 않으려면 completed 디렉터리에서 완료된 소스 코드를 확인하세요.

먼저 grpc codelab 저장소를 클론하고 grpc-cpp-opentelemetry 폴더로 cd합니다.

git clone https://github.com/grpc-ecosystem/grpc-codelabs.git
cd grpc-codelabs/codelabs/grpc-cpp-opentelemetry/

또는 codelab 디렉터리만 포함된 .zip 파일을 다운로드하고 직접 압축을 해제할 수 있습니다.

bazel을 사용하여 gRPC 라이브러리를 빌드합니다.

bazel build start_here/...

3. OpenTelemetry 플러그인 등록

gRPC OpenTelemetry 플러그인을 추가하려면 gRPC 애플리케이션이 필요합니다. 이 Codelab에서는 gRPC OpenTelemetry 플러그인으로 계측할 간단한 gRPC HelloWorld 클라이언트와 서버를 사용합니다.

첫 번째 단계는 클라이언트에 Prometheus 내보내기 도구로 구성된 OpenTelemetry 플러그인을 등록하는 것입니다. 원하는 편집기로 codelabs/grpc-cpp-opentelemetry/start_here/greeter_callback_client.cc를 열고 main()을 다음과 같이 변환합니다.

int main(int argc, char **argv) {
  absl::ParseCommandLine(argc, argv);

  // Codelab Solution: Register a global gRPC OpenTelemetry plugin configured
  // with a prometheus exporter.
  opentelemetry::exporter::metrics::PrometheusExporterOptions opts;
  opts.url = absl::GetFlag(FLAGS_prometheus_endpoint);
  auto prometheus_exporter =
      opentelemetry::exporter::metrics::PrometheusExporterFactory::Create(opts);
  auto meter_provider =
      std::make_shared<opentelemetry::sdk::metrics::MeterProvider>();

  // The default histogram boundaries are not granular enough for RPCs. Override
  // the "grpc.client.attempt.duration" view as recommended by
  // https://github.com/grpc/proposal/blob/master/A66-otel-stats.md.
  AddLatencyView(meter_provider.get(), "grpc.client.attempt.duration", "s");
  meter_provider->AddMetricReader(std::move(prometheus_exporter));
  auto status = grpc::OpenTelemetryPluginBuilder()
                    .SetMeterProvider(std::move(meter_provider))
                    .BuildAndRegisterGlobal();
  if (!status.ok()) {
    std::cerr << "Failed to register gRPC OpenTelemetry Plugin: "
              << status.ToString() << std::endl;
    return static_cast<int>(status.code());
  }

  // Continuously send RPCs.
  RunClient(absl::GetFlag(FLAGS_target));

  return 0;
}

다음 단계는 서버에 OpenTelemetry 플러그인을 추가하는 것입니다. codelabs/grpc-cpp-opentelemetry/start_here/greeter_callback_server.cc를 열고 main을 다음과 같이 변환합니다.

int main(int argc, char** argv) {
  absl::ParseCommandLine(argc, argv);

  // Register a global gRPC OpenTelemetry plugin configured with a prometheus
  // exporter.
  opentelemetry::exporter::metrics::PrometheusExporterOptions opts;
  opts.url = absl::GetFlag(FLAGS_prometheus_endpoint);
  auto prometheus_exporter =
      opentelemetry::exporter::metrics::PrometheusExporterFactory::Create(opts);
  auto meter_provider =
      std::make_shared<opentelemetry::sdk::metrics::MeterProvider>();

  // The default histogram boundaries are not granular enough for RPCs. Override
  // the "grpc.server.call.duration" view as recommended by
  // https://github.com/grpc/proposal/blob/master/A66-otel-stats.md.
  AddLatencyView(meter_provider.get(), "grpc.server.call.duration", "s");
  meter_provider->AddMetricReader(std::move(prometheus_exporter));
  auto status = grpc::OpenTelemetryPluginBuilder()
                    .SetMeterProvider(std::move(meter_provider))
                    .BuildAndRegisterGlobal();
  if (!status.ok()) {
    std::cerr << "Failed to register gRPC OpenTelemetry Plugin: "
              << status.ToString() << std::endl;
    return static_cast<int>(status.code());
  }
  RunServer(absl::GetFlag(FLAGS_port));
  return 0;
}

필수 헤더 파일과 빌드 종속 항목은 편의를 위해 이미 추가되었습니다.

#include "opentelemetry/exporters/prometheus/exporter_factory.h"
#include "opentelemetry/exporters/prometheus/exporter_options.h"
#include "opentelemetry/sdk/metrics/meter_provider.h"

#include <grpcpp/ext/otel_plugin.h>

빌드 종속 항목도 BUILD 파일에 이미 추가되어 있습니다.

cc_binary(
    name = "greeter_callback_client",
    srcs = ["greeter_callback_client.cc"],
    defines = ["BAZEL_BUILD"],
    deps = [
        "//util:util",
        "@com_github_grpc_grpc//:grpc++",
        "@com_github_grpc_grpc//:grpcpp_otel_plugin",
        "@com_google_absl//absl/flags:flag",
        "@com_google_absl//absl/flags:parse",
        "@io_opentelemetry_cpp//exporters/prometheus:prometheus_exporter",
        "@io_opentelemetry_cpp//sdk/src/metrics",
    ],
)

4. 예 실행 및 측정항목 보기

서버를 실행하려면 다음을 실행하세요.

bazel run start_here:greeter_callback_server

설정이 완료되면 서버에 다음 출력이 표시됩니다.

Server listening on 0.0.0.0:50051

서버가 실행되는 동안 다른 터미널에서 클라이언트를 실행합니다.

bazel run start_here:greeter_callback_client

성공적인 실행은 다음과 같습니다.

Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world
Greeter received: Hello world

Prometheus를 사용하여 측정항목을 내보내도록 gRPC OpenTelemetry 플러그인을 설정했기 때문입니다. 이러한 측정항목은 서버의 경우 localhost:9464, 클라이언트의 경우 localhost:9465에서 확인할 수 있습니다.

클라이언트 측정항목을 보려면 다음 단계를 따르세요.

curl localhost:9465/metrics

결과는 다음과 같은 형식입니다.

# HELP exposer_transferred_bytes_total Transferred bytes to metrics services
# TYPE exposer_transferred_bytes_total counter
exposer_transferred_bytes_total 0
# HELP exposer_scrapes_total Number of times metrics were scraped
# TYPE exposer_scrapes_total counter
exposer_scrapes_total 0
# HELP exposer_request_latencies Latencies of serving scrape requests, in microseconds
# TYPE exposer_request_latencies summary
exposer_request_latencies_count 0
exposer_request_latencies_sum 0
exposer_request_latencies{quantile="0.5"} Nan
exposer_request_latencies{quantile="0.9"} Nan
exposer_request_latencies{quantile="0.99"} Nan
# HELP target Target metadata
# TYPE target gauge
target_info{otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",service_name="unknown_service",telemetry_sdk_version="1.13.0",telemetry_sdk_name="opentelemetry",telemetry_sdk_language="cpp"} 1 1721958543107
# HELP grpc_client_attempt_rcvd_total_compressed_message_size_bytes Compressed message bytes received per call attempt
# TYPE grpc_client_attempt_rcvd_total_compressed_message_size_bytes histogram
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_count{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev"} 96 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_sum{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev"} 1248 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="0"} 0 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="5"} 0 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="10"} 0 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="25"} 96 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="50"} 96 1721958543107
grpc_client_attempt_rcvd_total_compressed_message_size_bytes_bucket{grpc_method="helloworld.Greeter/SayHello",grpc_status="OK",grpc_target="dns:///localhost:50051",otel_scope_name="grpc-c++",otel_scope_version="1.67.0-dev",le="75"} 96 1721958543107

마찬가지로 서버 측 측정항목의 경우 다음을 충족해야 합니다.

curl localhost:9464/metrics

5. Prometheus에서 측정항목 보기

여기서는 Prometheus를 사용하여 측정항목을 내보내는 gRPC 예시 클라이언트와 서버를 스크랩하는 Prometheus 인스턴스를 설정합니다.

플랫폼에 맞는 Prometheus의 최신 버전을 다운로드한 다음 압축을 풀고 실행합니다.

tar xvfz prometheus-*.tar.gz
cd prometheus-*

다음과 같이 Prometheus 구성 파일을 만듭니다.

cat > grpc_otel_cpp_prometheus.yml <<EOF
scrape_configs:
  - job_name: "prometheus"
    scrape_interval: 5s
    static_configs:
      - targets: ["localhost:9090"]
  - job_name: "grpc-otel-cpp"
    scrape_interval: 5s
    static_configs:
      - targets: ["localhost:9464", "localhost:9465"]
EOF

새 구성으로 prometheus를 시작합니다.

./prometheus --config.file=grpc_otel_cpp_prometheus.yml

이렇게 하면 클라이언트 및 서버 Codelab 프로세스의 측정항목이 5초마다 스크랩되도록 구성됩니다.

http://localhost:9090/graph로 이동하여 측정항목을 확인합니다. 예를 들어 다음 쿼리는

histogram_quantile(0.5, rate(grpc_client_attempt_duration_seconds_bucket[1m]))

분위수 계산의 시간 창으로 1분을 사용하여 시도 지연 시간의 중앙값을 보여주는 그래프가 표시됩니다.

질문 비율 -

increase(grpc_client_attempt_duration_seconds_bucket[1m])

6. (선택사항) 사용자 연습

Prometheus 대시보드에서 QPS가 낮음을 확인할 수 있습니다. QPS를 제한하는 의심스러운 코드를 예시에서 식별할 수 있는지 확인합니다.

열성적인 사용자를 위해 클라이언트 코드는 특정 시점에 하나의 대기 중인 RPC만 갖도록 제한됩니다. 이 값을 수정하여 클라이언트가 이전 RPC가 완료될 때까지 기다리지 않고 더 많은 RPC를 전송할 수 있습니다. (이 문제의 해결 방법은 제공되지 않았습니다.)