Go로 앱의 성능 향상을 위한 계측 (1부: trace)

1. 소개

505827108874614d.png

최종 업데이트: 2022년 7월 15일

애플리케이션의 관측 가능성

관측 가능성 및 OpenTelemetry

관찰 가능성은 시스템의 속성을 설명하는 데 사용되는 용어입니다. 관측 가능성을 갖춘 시스템을 사용하면 팀이 시스템을 수시로 디버깅할 수 있습니다. 이러한 맥락에서 관측 가능성의 세 가지 요소인 로그, 측정항목, 트레이스는 시스템이 관측 가능성을 획득하기 위한 기본적인 계측입니다.

OpenTelemetry는 관측 가능성에 필요한 원격 분석 데이터 (로그, 측정항목, trace)의 계측 및 내보내기를 가속화하는 사양, 라이브러리, 에이전트 집합입니다. OpenTelemetry는 CNCF의 개방형 표준 및 커뮤니티 주도 프로젝트입니다. 개발자는 프로젝트와 생태계에서 제공하는 라이브러리를 활용하여 공급업체 중립적인 방식으로 여러 아키텍처에 대해 애플리케이션을 계측할 수 있습니다.

또한 관측 가능성의 세 가지 요소 외에도 연속 프로파일링은 관측 가능성의 또 다른 핵심 구성요소이며 업계에서 사용자층을 확장하고 있습니다. Cloud Profiler는 이러한 도구 중 하나이며 애플리케이션 호출 스택에서 성능 측정항목을 드릴다운할 수 있는 간편한 인터페이스를 제공합니다.

이 Codelab은 시리즈의 1부로, OpenTelemetry 및 Cloud Trace를 사용하여 마이크로서비스에서 분산 트레이스를 계측하는 방법을 다룹니다. 2부에서는 Cloud Profiler를 사용한 지속적인 프로파일링에 대해 설명합니다.

분산 트레이스

로그, 측정항목, 트레이스 중에서 트레이스는 시스템의 프로세스 중 특정 부분의 지연 시간을 알려주는 원격 분석입니다. 특히 마이크로서비스 시대에는 분산 추적이 전체 분산 시스템에서 지연 시간 병목 현상을 찾는 강력한 동력입니다.

분산 트레이스를 분석할 때 추적 데이터 시각화는 전체 시스템 지연 시간을 한눈에 파악하는 데 중요합니다. 분산 트레이스에서는 여러 스팬이 포함된 트레이스 형식으로 시스템 진입점에 대한 단일 요청을 처리하는 일련의 호출을 처리합니다.

Span은 시작 시간과 중지 시간을 기록하여 분산 시스템에서 실행된 개별 작업 단위를 나타냅니다. 스팬은 종종 서로 계층적 관계가 있습니다. 아래 그림에서 모든 작은 스팬은 큰 /messages 스팬의 하위 스팬이며 시스템을 통한 작업 경로를 보여주는 하나의 트레이스로 조합됩니다.

trace

Google Cloud Trace는 분산 추적 백엔드의 옵션 중 하나이며 Google Cloud의 다른 제품과 잘 통합되어 있습니다.

빌드할 항목

이 Codelab에서는 Google Kubernetes Engine 클러스터에서 실행되는 '셰익스피어 애플리케이션' (Shakesapp)이라는 서비스에서 트레이스 정보를 계측합니다. Shakesapp의 아키텍처는 다음과 같습니다.

44e243182ced442f.png

  • Loadgen이 HTTP로 클라이언트에 쿼리 문자열을 전송합니다.
  • 클라이언트가 gRPC에서 로드젠에서 서버로 쿼리를 전달합니다.
  • 서버는 클라이언트의 쿼리를 수락하고, Google Cloud Storage에서 텍스트 형식의 모든 셰익스피어 작품을 가져와, 쿼리가 포함된 줄을 검색하고, 클라이언트와 일치하는 줄 번호를 반환합니다.

요청 전체에서 트레이스 정보를 계측합니다. 그런 다음 서버에 프로파일러 에이전트를 삽입하고 병목 현상을 조사합니다.

학습할 내용

  • Go 프로젝트에서 OpenTelemetry Trace 라이브러리를 시작하는 방법
  • 라이브러리로 스팬을 만드는 방법
  • 앱 구성요소 간에 전선을 통해 스팬 컨텍스트를 전파하는 방법
  • Cloud Trace로 trace 데이터를 전송하는 방법
  • Cloud Trace에서 트레이스를 분석하는 방법

이 Codelab에서는 마이크로서비스를 계측하는 방법을 설명합니다. 이해하기 쉽게 이 예시에는 3가지 구성요소 (부하 생성기, 클라이언트, 서버)만 포함되어 있지만 이 Codelab에 설명된 동일한 프로세스를 더 복잡하고 대규모 시스템에 적용할 수 있습니다.

필요한 항목

  • Go에 관한 기본 지식
  • Kubernetes에 대한 기본 지식

2. 설정 및 요구사항

자습형 환경 설정

아직 Google 계정(Gmail 또는 Google Apps)이 없으면 계정을 만들어야 합니다. Google Cloud Platform Console(console.cloud.google.com)에 로그인하고 새 프로젝트를 만듭니다.

프로젝트가 이미 있으면 Console 왼쪽 위에서 프로젝트 선택 풀다운 메뉴를 클릭합니다.

7a32e5469db69e9.png

그리고 표시된 대화상자에서 '새 프로젝트' 버튼을 클릭하여 새 프로젝트를 만듭니다.

7136b3ee36ebaf89.png

아직 프로젝트가 없으면 첫 번째 프로젝트를 만들기 위해 다음과 비슷한 대화상자가 표시됩니다.

870a3cbd6541ee86.png

이후의 프로젝트 만들기 대화상자에서 새 프로젝트의 세부정보를 입력할 수 있습니다.

affdc444517ba805.png

모든 Google Cloud 프로젝트에서 고유한 이름인 프로젝트 ID를 기억하세요(위의 이름은 이미 사용되었으므로 사용할 수 없습니다). 이 이름은 나중에 이 Codelab에서 PROJECT_ID로 참조됩니다.

그런 다음 Google Cloud 리소스를 사용하고 Cloud Trace API를 사용 설정하기 위해서는 아직 완료하지 않은 경우 Developers Console에서 결제를 사용 설정해야 합니다.

15d0ef27a8fbab27.png

이 codelab을 실행하는 과정에는 많은 비용이 들지 않지만 더 많은 리소스를 사용하려고 하거나 실행 중일 경우 비용이 더 들 수 있습니다(이 문서 마지막의 '삭제' 섹션 참조). Google Cloud Trace, Google Kubernetes Engine, Google Artifact Registry의 가격은 공식 문서에 명시되어 있습니다.

Google Cloud Platform 신규 사용자는 $300 상당의 무료 체험판을 사용할 수 있으므로, 이 Codelab을 완전히 무료로 사용할 수 있습니다.

Google Cloud Shell 설정

Google Cloud 및 Google Cloud Trace를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

이 Debian 기반 가상 머신에는 필요한 모든 개발 도구가 로드되어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 즉, 이 Codelab에 필요한 것은 브라우저뿐입니다(Chromebook에서도 작동 가능).

Cloud Console에서 Cloud Shell을 활성화하려면 Cloud Shell 활성화 gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A를 클릭합니다. 환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다.

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 2017-06-14 at 10.13.43 PM.png

Cloud Shell에 연결되면 사용자 인증이 이미 완료되었고 프로젝트가 내 PROJECT_ID에 설정되어 있음을 확인할 수 있습니다.

gcloud auth list

명령어 결과

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

명령어 결과

[core]
project = <PROJECT_ID>

어떤 이유로든 프로젝트가 설정되지 않았으면 다음 명령어를 실행하면 됩니다.

gcloud config set project <PROJECT_ID>

PROJECT_ID를 찾고 계신가요? 설정 단계에서 사용한 ID를 확인하거나 Cloud Console 대시보드에서 확인하세요.

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

또한 Cloud Shell은 기본적으로 이후 명령어를 실행할 때 유용할 수 있는 몇 가지 환경 변수를 설정합니다.

echo $GOOGLE_CLOUD_PROJECT

명령어 결과

<PROJECT_ID>

마지막으로 기본 영역 및 프로젝트 구성을 설정합니다.

gcloud config set compute/zone us-central1-f

다양한 영역을 선택할 수 있습니다. 자세한 내용은 리전 및 영역을 참조하세요.

Go 언어 설정

이 Codelab에서는 모든 소스 코드에 Go를 사용합니다. Cloud Shell에서 다음 명령어를 실행하고 Go 버전이 1.17 이상인지 확인합니다.

go version

명령어 결과

go version go1.18.3 linux/amd64

Google Kubernetes 클러스터 설정

이 Codelab에서는 Google Kubernetes Engine (GKE)에서 마이크로서비스 클러스터를 실행합니다. 이 Codelab의 절차는 다음과 같습니다.

  1. Cloud Shell에 기준 프로젝트 다운로드
  2. 컨테이너에 마이크로서비스 빌드
  3. Google Artifact Registry (GAR)에 컨테이너 업로드
  4. GKE에 컨테이너 배포
  5. 트레이스 계측을 위해 서비스의 소스 코드 수정
  6. 2단계로 이동

Kubernetes Engine 사용 설정

먼저 GKE에서 Shakesapp이 실행되는 Kubernetes 클러스터를 설정했으므로 GKE를 사용 설정해야 합니다. 'Kubernetes Engine' 메뉴로 이동하여 사용 설정 버튼을 누릅니다.

548cfd95bc6d344d.png

이제 Kubernetes 클러스터를 만들 준비가 되었습니다.

Kubernetes 클러스터 만들기

Cloud Shell에서 다음 명령어를 실행하여 Kubernetes 클러스터를 만듭니다. Artifact Registry 저장소 생성에 사용할 영역 아래에 영역 값이 있는지 확인합니다. 저장소 리전이 영역을 포함하지 않는 경우 영역 값 us-central1-f를 변경합니다.

gcloud container clusters create otel-trace-codelab2 \
--zone us-central1-f \
--release-channel rapid \
--preemptible \
--enable-autoscaling \
--max-nodes 8 \
--no-enable-ip-alias \
--scopes cloud-platform

명령어 결과

Note: Your Pod address range (`--cluster-ipv4-cidr`) can accommodate at most 1008 node(s).
Creating cluster otel-trace-codelab2 in us-central1-f... Cluster is being health-checked (master is healthy)...done.     
Created [https://container.googleapis.com/v1/projects/development-215403/zones/us-central1-f/clusters/otel-trace-codelab2].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-f/otel-trace-codelab2?project=development-215403
kubeconfig entry generated for otel-trace-codelab2.
NAME: otel-trace-codelab2
LOCATION: us-central1-f
MASTER_VERSION: 1.23.6-gke.1501
MASTER_IP: 104.154.76.89
MACHINE_TYPE: e2-medium
NODE_VERSION: 1.23.6-gke.1501
NUM_NODES: 3
STATUS: RUNNING

Artifact Registry 및 skaffold 설정

이제 배포할 준비가 된 Kubernetes 클러스터가 있습니다. 다음으로 컨테이너를 푸시하고 배포할 컨테이너 저장소를 준비합니다. 이 단계에서는 Artifact Registry (GAR)를 설정하고 이를 사용하도록 skaffold를 설정해야 합니다.

Artifact Registry 설정

'Artifact Registry'(아티팩트 레지스트리) 메뉴로 이동하여 ENABLE(사용 설정) 버튼을 누릅니다.

45e384b87f7cf0db.png

잠시 후 GAR의 저장소 브라우저가 표시됩니다. '저장소 만들기' 버튼을 클릭하고 저장소 이름을 입력합니다.

d6a70f4cb4ebcbe3.png

이 Codelab에서는 새 저장소의 이름을 trace-codelab로 지정합니다. 아티팩트 형식은 'Docker'이고 위치 유형은 '리전'입니다. Google Compute Engine 기본 영역으로 설정한 영역에 가까운 리전을 선택합니다. 예를 들어 이 예에서는 위에서 'us-central1-f'를 선택했으므로 여기서는 'us-central1 (아이오와)'를 선택합니다. 그런 다음 '만들기' 버튼을 클릭합니다.

9c2d1ce65258ef70.png

이제 저장소 브라우저에 'trace-codelab'이 표시됩니다.

7a3c1f47346bea15.png

나중에 다시 여기로 돌아와 레지스트리 경로를 확인할 예정입니다.

Skaffold 설정

Skaffold는 Kubernetes에서 실행되는 마이크로서비스를 빌드할 때 유용한 도구입니다. Skaffold는 소수의 명령어로 애플리케이션 컨테이너의 빌드, 푸시, 배포 워크플로를 처리합니다. Skaffold는 기본적으로 Docker Registry를 컨테이너 레지스트리로 사용하므로 컨테이너를 푸시할 때 GAR를 인식하도록 Skaffold를 구성해야 합니다.

Cloud Shell을 다시 열고 skaffold가 설치되어 있는지 확인합니다. Cloud Shell은 기본적으로 skaffold를 환경에 설치합니다. 다음 명령어를 실행하고 skaffold 버전을 확인합니다.

skaffold version

명령어 결과

v1.38.0

이제 skaffold에서 사용할 기본 저장소를 등록할 수 있습니다. 레지스트리 경로를 가져오려면 Artifact Registry 대시보드로 이동하여 이전 단계에서 설정한 저장소의 이름을 클릭합니다.

7a3c1f47346bea15.png

그러면 페이지 상단에 탐색경로 트레일이 표시됩니다. e157b1359c3edc06.png 아이콘을 클릭하여 레지스트리 경로를 클립보드에 복사합니다.

e0f2ae2144880b8b.png

복사 버튼을 클릭하면 브라우저 하단에 다음과 같은 메시지가 포함된 대화상자가 표시됩니다.

'us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab'이(가) 복사되었습니다.

Cloud Shell로 돌아갑니다. 방금 대시보드에서 복사한 값을 사용하여 skaffold config set default-repo 명령어를 실행합니다.

skaffold config set default-repo us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab

명령어 결과

set value default-repo to us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab for context gke_stackdriver-sandbox-3438851889_us-central1-b_stackdriver-sandbox

또한 레지스트리를 Docker 구성으로 구성해야 합니다. 다음 명령어를 실행합니다.

gcloud auth configure-docker us-central1-docker.pkg.dev --quiet

명령어 결과

{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud",
    "us-central1-docker.pkg.dev": "gcloud"
  }
}
Adding credentials for: us-central1-docker.pkg.dev

이제 GKE에서 Kubernetes 컨테이너를 설정하는 다음 단계로 진행할 수 있습니다.

요약

이 단계에서는 Codelab 환경을 설정합니다.

  • Cloud Shell 설정
  • 컨테이너 레지스트리의 Artifact Registry 저장소를 만들었습니다.
  • 컨테이너 레지스트리를 사용하도록 skaffold 설정
  • Codelab 마이크로서비스가 실행되는 Kubernetes 클러스터 만들기

다음 단계

다음 단계에서는 마이크로서비스를 빌드, 푸시, 클러스터에 배포합니다.

3. 마이크로서비스 빌드, 푸시, 배포

Codelab 자료 다운로드

이전 단계에서 이 Codelab의 모든 기본 요건을 설정했습니다. 이제 이를 기반으로 전체 마이크로서비스를 실행할 수 있습니다. Codelab 자료는 GitHub에서 호스팅되므로 다음 git 명령어를 사용하여 Cloud Shell 환경에 다운로드합니다.

cd ~
git clone https://github.com/ymotongpoo/opentelemetry-trace-codelab-go.git
cd opentelemetry-trace-codelab-go

프로젝트의 디렉터리 구조는 다음과 같습니다.

.
├── README.md
├── step0
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step1
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step2
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step3
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step4
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
├── step5
│   ├── manifests
│   ├── proto
│   ├── skaffold.yaml
│   └── src
└── step6
    ├── manifests
    ├── proto
    ├── skaffold.yaml
    └── src
  • 매니페스트: Kubernetes 매니페스트 파일
  • proto: 클라이언트와 서버 간의 통신을 위한 proto 정의
  • src: 각 서비스의 소스 코드 디렉터리
  • skaffold.yaml: skaffold의 구성 파일

이 Codelab에서는 step0 폴더에 있는 소스 코드를 업데이트합니다. 다음 단계의 답변을 확인하려면 step[1-6] 폴더의 소스 코드를 참고해도 됩니다. (1부는 0~4단계를 다루고 2부는 5~6단계를 다룹니다.)

skaffold 명령어 실행

이제 방금 만든 Kubernetes 클러스터에 전체 콘텐츠를 빌드, 푸시, 배포할 준비가 되었습니다. 여러 단계가 포함된 것처럼 보이지만 실제로는 skaffold가 모든 작업을 자동으로 처리합니다. 다음 명령어를 사용하여 시도해 보겠습니다.

cd step0
skaffold dev

명령어를 실행하면 docker build의 로그 출력이 표시되고 레지스트리에 푸시되었는지 확인할 수 있습니다.

명령어 결과

...
---> Running in c39b3ea8692b
 ---> 90932a583ab6
Successfully built 90932a583ab6
Successfully tagged us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step1
The push refers to repository [us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice]
cc8f5a05df4a: Preparing
5bf719419ee2: Preparing
2901929ad341: Preparing
88d9943798ba: Preparing
b0fdf826a39a: Preparing
3c9c1e0b1647: Preparing
f3427ce9393d: Preparing
14a1ca976738: Preparing
f3427ce9393d: Waiting
14a1ca976738: Waiting
3c9c1e0b1647: Waiting
b0fdf826a39a: Layer already exists
88d9943798ba: Layer already exists
f3427ce9393d: Layer already exists
3c9c1e0b1647: Layer already exists
14a1ca976738: Layer already exists
2901929ad341: Pushed
5bf719419ee2: Pushed
cc8f5a05df4a: Pushed
step1: digest: sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe size: 2001

모든 서비스 컨테이너를 푸시하면 Kubernetes 배포가 자동으로 시작됩니다.

명령어 결과

sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8 size: 1997
Tags used in deployment:
 - serverservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/serverservice:step4@sha256:8acdbe3a453001f120fb22c11c4f6d64c2451347732f4f271d746c2e4d193bbe
 - clientservice -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/clientservice:step4@sha256:b71fce0a96cea08075dc20758ae561cf78c83ff656b04d211ffa00cedb77edf8
 - loadgen -> us-central1-docker.pkg.dev/psychic-order-307806/trace-codelab/loadgen:step4@sha256:eea2e5bc8463ecf886f958a86906cab896e9e2e380a0eb143deaeaca40f7888a
Starting deploy...
 - deployment.apps/clientservice created
 - service/clientservice created
 - deployment.apps/loadgen created
 - deployment.apps/serverservice created
 - service/serverservice created

배포 후 각 컨테이너의 stdout에 내보낸 실제 애플리케이션 로그가 다음과 같이 표시됩니다.

명령어 결과

[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:15 {"match_count":3040}
[loadgen] 2022/07/14 06:33:15 query 'love': matched 3040
[client] 2022/07/14 06:33:16 {"match_count":3040}
[loadgen] 2022/07/14 06:33:16 query 'love': matched 3040
[client] 2022/07/14 06:33:19 {"match_count":463}
[loadgen] 2022/07/14 06:33:19 query 'tear': matched 463
[loadgen] 2022/07/14 06:33:20 query 'world': matched 728
[client] 2022/07/14 06:33:20 {"match_count":728}
[client] 2022/07/14 06:33:22 {"match_count":463}
[loadgen] 2022/07/14 06:33:22 query 'tear': matched 463

이 시점에서 서버의 메시지가 표시되어야 합니다. 이제 서비스의 분산 추적을 위해 OpenTelemetry로 애플리케이션을 계측할 준비가 되었습니다.

서비스 계측을 시작하기 전에 Ctrl-C를 사용하여 클러스터를 종료하세요.

명령어 결과

...
[client] 2022/07/14 06:34:57 {"match_count":1}
[loadgen] 2022/07/14 06:34:57 query 'what's past is prologue': matched 1
^CCleaning up...
 - W0714 06:34:58.464305   28078 gcp.go:120] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead.
 - To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

요약

이 단계에서는 환경에서 Codelab 자료를 준비하고 skaffold가 예상대로 실행되는지 확인했습니다.

다음 단계

다음 단계에서는 트레이스 정보를 계측하도록 loadgen 서비스의 소스 코드를 수정합니다.

4. HTTP 계측

트레이스 계측 및 전파의 개념

소스 코드를 수정하기 전에 간단한 다이어그램으로 분산 트레이스가 작동하는 방식을 간단히 설명하겠습니다.

6be42e353b9bfd1d.png

이 예에서는 코드를 계측하여 트레이스 및 스팬 정보를 Cloud Trace로 내보내고 loadgen 서비스의 요청에서 서버 서비스로 트레이스 컨텍스트를 전파합니다.

Cloud Trace에서 동일한 trace ID를 가진 모든 스팬을 하나의 trace로 조합하려면 애플리케이션에서 trace ID 및 스팬 ID와 같은 trace 메타데이터를 전송해야 합니다. 또한 애플리케이션은 다운스트림 서비스를 요청할 때 트레이스 컨텍스트 (상위 스팬의 트레이스 ID와 스팬 ID 조합)를 전파해야 다루고 있는 트레이스 컨텍스트를 인식할 수 있습니다.

OpenTelemetry를 사용하면 다음 작업을 할 수 있습니다.

  • 고유한 Trace ID 및 Span ID를 생성합니다.
  • Trace ID 및 Span ID를 백엔드로 내보냅니다.
  • 다른 서비스에 트레이스 컨텍스트를 전파합니다.
  • 트레이스 분석에 도움이 되는 추가 메타데이터를 삽입합니다.

OpenTelemetry 추적의 구성요소

b01f7bb90188db0d.png

OpenTelemetry로 애플리케이션 트레이스를 계측하는 프로세스는 다음과 같습니다.

  1. 내보내기 도구 만들기
  2. 1에서 내보내기를 바인딩하는 TracerProvider를 만들고 전역으로 설정합니다.
  3. 전파 메서드를 설정하도록 TextMapPropagaror 설정
  4. TracerProvider에서 Tracer 가져오기
  5. Tracer에서 Span 생성

지금은 각 구성요소의 세부 속성을 이해할 필요는 없지만 가장 중요한 것은 다음과 같습니다.

  • 여기의 내보내기는 TracerProvider에 연결할 수 있습니다.
  • TracerProvider는 트레이스 샘플링 및 내보내기에 관한 모든 구성을 보유합니다.
  • 모든 트레이스가 Tracer 객체에 번들로 제공됨

이를 이해했으므로 이제 실제 코딩 작업으로 넘어가겠습니다.

계기 첫 번째 스팬

부하 생성기 서비스 계측

Cloud Shell 오른쪽 상단에 있는 776a11bfb2122549.png 버튼을 눌러 Cloud Shell 편집기를 엽니다. 왼쪽 창의 탐색기에서 step0/src/loadgen/main.go를 열고 main 함수를 찾습니다.

step0/src/loadgen/main.go

func main() {
        ...
        for range t.C {
                log.Printf("simulating client requests, round %d", i)
                if err := run(numWorkers, numConcurrency); err != nil {
                        log.Printf("aborted round with error: %v", err)
                }
                log.Printf("simulated %d requests", numWorkers)
                if numRounds != 0 && i > numRounds {
                        break
                }
                i++
        }
}

main 함수에서 run 함수를 호출하는 루프를 볼 수 있습니다. 현재 구현에서는 이 섹션에 함수 호출의 시작과 끝을 기록하는 로그 행이 2개 있습니다. 이제 Span 정보를 계측하여 함수 호출의 지연 시간을 추적해 보겠습니다.

먼저 이전 섹션에서 설명한 대로 OpenTelemetry의 전체 구성을 설정합니다. 다음과 같이 OpenTelemetry 패키지를 추가합니다.

step0/src/loadgen/main.go

import (
        "context" // step1. add packages
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
        // step1. end add packages
)

가독성을 위해 initTracer라는 설정 함수를 만들고 main 함수에서 호출합니다.

step0/src/loadgen/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

OpenTelemetry를 설정하는 절차는 이전 섹션에 설명되어 있습니다. 이 구현에서는 모든 트레이스 정보를 구조화된 형식으로 stdout으로 내보내는 stdout 내보내기를 사용합니다.

그런 다음 기본 함수에서 호출합니다. initTracer()를 호출하고 애플리케이션을 닫을 때 TracerProvider.Shutdown()를 호출해야 합니다.

step0/src/loadgen/main.go

func main() {
        // step1. setup OpenTelemetry
        tp, err := initTracer()
        if err != nil {
                log.Fatalf("failed to initialize TracerProvider: %v", err)
        }
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step1. end setup

        log.Printf("starting worder with %d workers in %d concurrency", numWorkers, numConcurrency)
        log.Printf("number of rounds: %d (0 is inifinite)", numRounds)
        ...

설정을 완료하면 고유한 트레이스 ID와 스팬 ID가 있는 스팬을 만들어야 합니다. OpenTelemetry는 이를 위한 편리한 라이브러리를 제공합니다. 계측 HTTP 클라이언트에 새 패키지를 추가합니다.

step0/src/loadgen/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net/http"
        "net/http/httptrace" // step1. add packages
        "net/url"
        "time"
        // step1. add packages
        "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        // step1. end add packages
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
        "go.opentelemetry.io/otel/trace"
)

부하 생성기가 runQuery 함수에서 net/http를 사용하여 HTTP에서 클라이언트 서비스를 호출하므로 net/http에 contrib 패키지를 사용하고 httptraceotelhttp 패키지 확장으로 계측을 사용 설정합니다.

먼저 계측된 클라이언트를 통해 HTTP 요청을 호출하는 패키지 전역 변수 httpClient를 추가합니다.

step0/src/loadgen/main.go

var httpClient = http.Client{
        Transport: otelhttp.NewTransport(http.DefaultTransport)
}

그런 다음 runQuery 함수에 계측을 추가하여 OpenTelemetry와 커스텀 HTTP 클라이언트의 자동 생성 스팬을 사용하여 커스텀 스팬을 만듭니다. 다음 단계를 따르세요.

  1. otel.Tracer()를 사용하여 전 세계 TracerProvider에서 트레이서 가져오기
  2. Tracer.Start() 메서드로 루트 스팬 만들기
  3. 임의의 시점에 루트 스팬 종료 (이 경우 runQuery 함수의 끝)

step0/src/loadgen/main.go

        reqURL.RawQuery = v.Encode()
        // step1. replace http.Get() with custom client call
        // resp, err := http.Get(reqURL.String())

        // step1. instrument trace
        ctx := context.Background()
        tr := otel.Tracer("loadgen")
        ctx, span := tr.Start(ctx, "query.request", trace.WithAttributes(
                semconv.TelemetrySDKLanguageGo,
                semconv.ServiceNameKey.String("loadgen.runQuery"),
                attribute.Key("query").String(s),
        ))
        defer span.End()
        ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
        req, err := http.NewRequestWithContext(ctx, "GET", reqURL.String(), nil)
        if err != nil {
                return -1, fmt.Errorf("error creating HTTP request object: %v", err)
        }
        resp, err := httpClient.Do(req)
        // step1. end instrumentation
        if err != nil {
                return -1, fmt.Errorf("error sending request to %v: %v", reqURL.String(), err)
        }

이제 loadgen (HTTP 클라이언트 애플리케이션)의 계측이 완료되었습니다. go mod 명령어로 go.modgo.sum를 업데이트해야 합니다.

go mod tidy

계기판 고객 서비스

이전 섹션에서는 아래 그림에서 빨간색 직사각형으로 묶인 부분을 계측했습니다. 부하 생성기 서비스에서 스팬 정보를 계측했습니다. 이제 부하 생성기 서비스와 마찬가지로 클라이언트 서비스를 계측해야 합니다. 부하 생성기 서비스와의 차이점은 클라이언트 서비스가 HTTP 헤더의 부하 생성기 서비스에서 전파된 Trace ID 정보를 추출하고 이 ID를 사용하여 스팬을 생성해야 한다는 점입니다.

bcaccd06691269f8.png

Cloud Shell 편집기를 열고 부하 생성기 서비스에서와 같이 필요한 패키지를 추가합니다.

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step1. add new import
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        // step1. end new import
)

OpenTelemetry를 다시 설정해야 합니다. loadgen에서 initTracer 함수를 복사하여 붙여넣고 클라이언트 서비스의 main 함수에서도 호출하면 됩니다.

step0/src/client/main.go

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

이제 스팬을 계측할 차례입니다. 클라이언트 서비스는 loadgen 서비스의 HTTP 요청을 수락해야 하므로 핸들러를 계측해야 합니다. 클라이언트 서비스의 HTTP 서버는 net/http로 구현되며 loadgen에서와 같이 otelhttp 패키지를 사용할 수 있습니다.

먼저 핸들러 등록을 otelhttp 핸들러로 바꿉니다. main 함수에서 HTTP 핸들러가 http.HandleFunc()에 등록된 줄을 찾습니다.

step0/src/client/main.go

        // step1. change handler to intercept OpenTelemetry related headers
        // http.HandleFunc("/", svc.handler)
        otelHandler := otelhttp.NewHandler(http.HandlerFunc(svc.handler), "client.handler")
        http.Handle("/", otelHandler)
        // step1. end intercepter setting
        http.HandleFunc("/_genki", svc.health)

그런 다음 핸들러 내에서 실제 스팬을 계측합니다. func (*clientService) handler()를 찾아 trace.SpanFromContext()로 스팬 계측을 추가합니다.

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        ctx := r.Context()
        ctx, cancel := context.WithCancel(ctx)
        defer cancel()
        // step1. instrument trace
        span := trace.SpanFromContext(ctx)
        defer span.End()
        // step1. end instrument
        ...

이 계측을 사용하면 handler 메서드의 시작 부분부터 끝 부분까지 스팬을 가져올 수 있습니다. 스팬을 쉽게 분석할 수 있도록 일치하는 개수를 저장하는 추가 속성을 쿼리에 추가합니다. 로그 줄 바로 앞에 다음 코드를 추가합니다.

step0/src/client/main.go

func (cs *clientService) handler(w http.ResponseWriter, r *http.Request) {
        ...
        // step1. add span specific attribute
        span.SetAttributes(attribute.Key("matched").Int64(resp.MatchCount))
        // step1. end adding attribute
        log.Println(string(ret))
        ...

위의 모든 계측을 통해 loadgen과 클라이언트 간의 트레이스 계측을 완료했습니다. 어떻게 작동하는지 살펴보겠습니다. skaffold를 사용하여 코드를 다시 실행합니다.

skaffold dev

GKE 클러스터에서 서비스를 실행하는 데 시간이 지나면 다음과 같은 대량의 로그 메시지가 표시됩니다.

명령어 결과

[loadgen] {
[loadgen]       "Name": "query.request",
[loadgen]       "SpanContext": {
[loadgen]               "TraceID": "cfa22247a542beeb55a3434392d46b89",
[loadgen]               "SpanID": "18b06404b10c418b",
[loadgen]               "TraceFlags": "01",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "Parent": {
[loadgen]               "TraceID": "00000000000000000000000000000000",
[loadgen]               "SpanID": "0000000000000000",
[loadgen]               "TraceFlags": "00",
[loadgen]               "TraceState": "",
[loadgen]               "Remote": false
[loadgen]       },
[loadgen]       "SpanKind": 1,
[loadgen]       "StartTime": "2022-07-14T13:13:36.686751087Z",
[loadgen]       "EndTime": "2022-07-14T13:14:31.849601964Z",
[loadgen]       "Attributes": [
[loadgen]               {
[loadgen]                       "Key": "telemetry.sdk.language",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "go"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "loadgen.runQuery"
[loadgen]                       }
[loadgen]               },
[loadgen]               {
[loadgen]                       "Key": "query",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "faith"
[loadgen]                       }
[loadgen]               }
[loadgen]       ],
[loadgen]       "Events": null,
[loadgen]       "Links": null,
[loadgen]       "Status": {
[loadgen]               "Code": "Unset",
[loadgen]               "Description": ""
[loadgen]       },
[loadgen]       "DroppedAttributes": 0,
[loadgen]       "DroppedEvents": 0,
[loadgen]       "DroppedLinks": 0,
[loadgen]       "ChildSpanCount": 5,
[loadgen]       "Resource": [
[loadgen]               {
[loadgen]                       "Key": "service.name",
[loadgen]                       "Value": {
[loadgen]                               "Type": "STRING",
[loadgen]                               "Value": "unknown_service:loadgen"
...

stdout 내보내기 도구는 이러한 메시지를 내보냅니다. loadgen의 모든 스팬의 상위 요소에는 TraceID: 00000000000000000000000000000000가 있습니다. 루트 스팬, 즉 트레이스의 첫 번째 스팬이기 때문입니다. 또한 삽입 속성 "query"에는 클라이언트 서비스에 전달되는 쿼리 문자열이 있습니다.

요약

이 단계에서는 HTTP로 통신하는 부하 생성기 서비스와 클라이언트 서비스를 계측하고 서비스 간에 Trace 컨텍스트를 성공적으로 전파하고 두 서비스에서 모두 스팬 정보를 stdout으로 내보낼 수 있는지 확인했습니다.

다음 단계

다음 단계에서는 클라이언트 서비스와 서버 서비스를 계측하여 gRPC를 통해 Trace 컨텍스트를 전파하는 방법을 확인합니다.

5. gRPC 계측

이전 단계에서는 이 마이크로서비스에서 요청의 전반부를 계측했습니다. 이 단계에서는 클라이언트 서비스와 서버 서비스 간의 gRPC 통신을 계측해 봅니다. (아래 사진에서 녹색 및 보라색 직사각형)

75310d8e0e3b1a30.png

gRPC 클라이언트의 사전 빌드 계측

OpenTelemetry의 생태계는 개발자가 애플리케이션을 계측하는 데 도움이 되는 여러 편리한 라이브러리를 제공합니다. 이전 단계에서는 net/http 패키지에 빌드 전 계측을 사용했습니다. 이 단계에서는 gRPC를 통해 Trace Context를 전파하려고 하므로 라이브러리를 사용합니다.

먼저 사전 빌드된 gRPC 패키지인 otelgrpc를 가져옵니다.

step0/src/client/main.go

import (
        "context"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "net/http"
        "net/url"
        "os"
        "time"

        "opentelemetry-trace-codelab-go/client/shakesapp"
        // step2. add prebuilt gRPC package (otelgrpc) 
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "go.opentelemetry.io/otel/trace"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

이번에는 클라이언트 서비스가 서버 서비스에 대한 gRPC 클라이언트이므로 gRPC 클라이언트를 계측해야 합니다. mustConnGRPC 함수를 찾아 클라이언트가 서버에 요청할 때마다 새 스팬을 계측하는 gRPC 인터셉터를 추가합니다.

step0/src/client/main.go

// Helper function for gRPC connections: Dial and create client once, reuse.
func mustConnGRPC(ctx context.Context, conn **grpc.ClientConn, addr string) {
        var err error
        // step2. add gRPC interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        *conn, err = grpc.DialContext(ctx, addr,
                grpc.WithTransportCredentials(insecure.NewCredentials()),
                grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(interceptorOpt)),
                grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(interceptorOpt)),
                grpc.WithTimeout(time.Second*3),
        )
        // step2: end adding interceptor
        if err != nil {
                panic(fmt.Sprintf("Error %s grpc: failed to connect %s", err, addr))
        }
}

이전 섹션에서 이미 OpenTelemetry를 설정했으므로 이 작업을 할 필요가 없습니다.

gRPC 서버용 사전 빌드된 계측

gRPC 클라이언트에서와 마찬가지로 gRPC 서버의 사전 빌드된 계측을 호출합니다. 가져오기 섹션에 다음과 같이 새 패키지를 추가합니다.

step0/src/server/main.go

import (
        "context"
        "fmt"
        "io/ioutil"
        "log"
        "net"
        "os"
        "regexp"
        "strings"

        "opentelemetry-trace-codelab-go/server/shakesapp"

        "cloud.google.com/go/storage"
        // step2. add OpenTelemetry packages including otelgrpc
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
        "go.opentelemetry.io/otel"
        stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
        "go.opentelemetry.io/otel/propagation"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        "google.golang.org/api/iterator"
        "google.golang.org/api/option"
        "google.golang.org/grpc"
        healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

서버를 계측하는 것은 이번이 처음이므로 loadgen 및 클라이언트 서비스에서와 마찬가지로 먼저 OpenTelemetry를 설정해야 합니다.

step0/src/server/main.go

// step2. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // create a stdout exporter to show collected spans out to stdout.
        exporter, err := stdout.New(stdout.WithPrettyPrint())
        if err != nil {
                return nil, err
        }
        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

func main() {
        ...

        // step2. setup OpenTelemetry
        tp, err := initTracer()
        if err != nil {
                log.Fatalf("failed to initialize TracerProvider: %v", err)
        }
        defer func() {
                if err := tp.Shutdown(context.Background()); err != nil {
                        log.Fatalf("error shutting down TracerProvider: %v", err)
                }
        }()
        // step2. end setup
        ...

다음으로 서버 인터셉터를 추가해야 합니다. main 함수에서 grpc.NewServer()가 호출되는 위치를 찾아 함수에 인터셉터를 추가합니다.

step0/src/server/main.go

func main() {
        ...
        svc := NewServerService()
        // step2: add interceptor
        interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider())
        srv := grpc.NewServer(
                grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(interceptorOpt)),
                grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(interceptorOpt)),
        )
        // step2: end adding interceptor
        shakesapp.RegisterShakespeareServiceServer(srv, svc)
        ...

마이크로서비스를 실행하고 트레이스를 확인합니다.

그런 다음 skaffold 명령어로 수정된 코드를 실행합니다.

skaffold dev

이제 다시 stdout에 여러 스팬 정보가 표시됩니다.

명령어 결과

...
[server] {
[server]        "Name": "shakesapp.ShakespeareService/GetMatchCount",
[server]        "SpanContext": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "96030dbad0061b3f",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": false
[server]        },
[server]        "Parent": {
[server]                "TraceID": "89b472f213a400cf975e0a0041649667",
[server]                "SpanID": "cd90cc3859b73890",
[server]                "TraceFlags": "01",
[server]                "TraceState": "",
[server]                "Remote": true
[server]        },
[server]        "SpanKind": 2,
[server]        "StartTime": "2022-07-14T14:05:55.74822525Z",
[server]        "EndTime": "2022-07-14T14:06:03.449258891Z",
[server]        "Attributes": [
...
[server]        ],
[server]        "Events": [
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:05:55.748235489Z"
[server]                },
[server]                {
[server]                        "Name": "message",
[server]                        "Attributes": [
...
[server]                        ],
[server]                        "DroppedAttributeCount": 0,
[server]                        "Time": "2022-07-14T14:06:03.449255889Z"
[server]                }
[server]        ],
[server]        "Links": null,
[server]        "Status": {
[server]                "Code": "Unset",
[server]                "Description": ""
[server]        },
[server]        "DroppedAttributes": 0,
[server]        "DroppedEvents": 0,
[server]        "DroppedLinks": 0,
[server]        "ChildSpanCount": 0,
[server]        "Resource": [
[server]                {
...
[server]        ],
[server]        "InstrumentationLibrary": {
[server]                "Name": "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc",
[server]                "Version": "semver:0.33.0",
[server]                "SchemaURL": ""
[server]        }
[server] }
...

스팬 이름을 삽입하지 않았으며 trace.Start() 또는 span.SpanFromContext()를 사용하여 스팬을 수동으로 만들었습니다. 하지만 gRPC 인터셉터가 스팬을 생성했기 때문에 여전히 많은 수의 스팬이 생성됩니다.

요약

이 단계에서는 OpenTelemetry 생태계 라이브러리의 지원을 받아 gRPC 기반 통신을 계측했습니다.

다음 단계

다음 단계에서는 Cloud Trace로 트레이스를 시각화하고 수집된 스팬을 분석하는 방법을 알아봅니다.

6. Cloud Trace로 트레이스 시각화

OpenTelemetry를 사용하여 전체 시스템의 트레이스를 계측했습니다. 지금까지 HTTP 및 gRPC 서비스를 계측하는 방법을 알아봤습니다. 계측하는 방법은 배웠지만 분석하는 방법은 아직 배운 적이 없습니다. 이 섹션에서는 stdout 내보내기 도구를 Cloud Trace 내보내기 도구로 대체하고 트레이스를 분석하는 방법을 알아봅니다.

Cloud Trace 내보내기 도구 사용

OpenTelemetry의 강력한 특징 중 하나는 플러그인 가능성입니다. 계측에서 수집한 모든 스팬을 시각화하려면 stdout 내보내기 도구를 Cloud Trace 내보내기 도구로 바꾸기만 하면 됩니다.

각 서비스의 main.go 파일을 열고 initTracer() 함수를 찾습니다. stdout 내보내기를 생성하는 줄을 삭제하고 대신 Cloud Trace 내보내기 도구를 만듭니다.

step0/src/loadgen/main.go

import (
        ...
        // step3. add OpenTelemetry for Cloud Trace package
        cloudtrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
)

// step1. add OpenTelemetry initialization function
func initTracer() (*sdktrace.TracerProvider, error) {
        // step3. replace stdout exporter with Cloud Trace exporter
        // cloudtrace.New() finds the credentials to Cloud Trace automatically following the
        // rules defined by golang.org/x/oauth2/google.findDefaultCredentailsWithParams.
        // https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentialsWithParams
        exporter, err := cloudtrace.New()
        // step3. end replacing exporter
        if err != nil {
                return nil, err
        }

        // for the demonstration, we use AlwaysSmaple sampler to take all spans.
        // do not use this option in production.
        tp := sdktrace.NewTracerProvider(
                sdktrace.WithSampler(sdktrace.AlwaysSample()),
                sdktrace.WithBatcher(exporter),
        )
        otel.SetTracerProvider(tp)
        otel.SetTextMapPropagator(propagation.TraceContext{})
        return tp, nil
}

클라이언트 서비스와 서버 서비스에서도 동일한 함수를 수정해야 합니다.

마이크로서비스를 실행하고 트레이스를 확인합니다.

수정한 후에는 skaffold 명령어로 평소와 같이 클러스터를 실행하면 됩니다.

skaffold dev

이제 내보내기 도구를 Cloud Trace 도구로 대체했기 때문에 stdout에 구조화된 로그 형식의 스팬 정보가 많이 표시되지 않습니다.

명령어 결과

[loadgen] 2022/07/14 15:01:07 simulated 20 requests
[loadgen] 2022/07/14 15:01:07 simulating client requests, round 37
[loadgen] 2022/07/14 15:01:14 query 'sweet': matched 958
[client] 2022/07/14 15:01:14 {"match_count":958}
[client] 2022/07/14 15:01:14 {"match_count":3040}
[loadgen] 2022/07/14 15:01:14 query 'love': matched 3040
[client] 2022/07/14 15:01:15 {"match_count":349}
[loadgen] 2022/07/14 15:01:15 query 'hello': matched 349
[client] 2022/07/14 15:01:15 {"match_count":484}
[loadgen] 2022/07/14 15:01:15 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:15 query 'insolence': matched 14
[client] 2022/07/14 15:01:15 {"match_count":14}
[client] 2022/07/14 15:01:21 {"match_count":484}
[loadgen] 2022/07/14 15:01:21 query 'faith': matched 484
[client] 2022/07/14 15:01:21 {"match_count":728}
[loadgen] 2022/07/14 15:01:21 query 'world': matched 728
[client] 2022/07/14 15:01:22 {"match_count":484}
[loadgen] 2022/07/14 15:01:22 query 'faith': matched 484
[loadgen] 2022/07/14 15:01:22 query 'hello': matched 349
[client] 2022/07/14 15:01:22 {"match_count":349}
[client] 2022/07/14 15:01:23 {"match_count":1036}
[loadgen] 2022/07/14 15:01:23 query 'friend': matched 1036
[loadgen] 2022/07/14 15:01:28 query 'tear': matched 463
...

이제 모든 스팬이 Cloud Trace로 올바르게 전송되는지 확인해 보겠습니다. Cloud 콘솔에 액세스하고 'Trace 목록'으로 이동합니다. 검색창에서 쉽게 액세스할 수 있습니다. 또는 왼쪽 창에서 메뉴를 클릭할 수 있습니다. 8b3f8411bd737e06.png

그러면 지연 시간 그래프에 파란색 점이 많이 분포되어 있는 것을 볼 수 있습니다. 각 스팟은 단일 트레이스를 나타냅니다.

3ecf131423fc4c40.png

그중 하나를 클릭하면 트레이스 내에서 세부정보를 확인할 수 있습니다. 4fd10960c6648a03.png

이렇게 간단하게 살펴보기만 해도 많은 유용한 정보를 얻을 수 있습니다. 예를 들어 폭포식 그래프에서 지연 시간의 원인은 대부분 shakesapp.ShakespeareService/GetMatchCount라는 스팬 때문임을 알 수 있습니다. (위 이미지의 1 참고) 요약 표에서 확인할 수 있습니다. 가장 오른쪽 열에는 각 기간의 길이가 표시됩니다. 또한 이 트레이스는 '친구' 쿼리에 대한 트레이스입니다. (위 이미지의 2 참고)

이러한 간단한 분석을 통해 GetMatchCount 메서드 내에서 더 세분화된 스팬을 알아야 한다는 것을 알 수 있습니다. 시각화는 stdout 정보에 비해 강력합니다. Cloud Trace 세부정보에 대해 자세히 알아보려면 공식 문서를 참고하세요.

요약

이 단계에서는 stdout 내보내기 도구를 Cloud Trace 내보내기 도구로 대체하고 Cloud Trace에서 트레이스를 시각화했습니다. 또한 트레이스 분석을 시작하는 방법도 알아봤습니다.

다음 단계

다음 단계에서는 서버 서비스의 소스 코드를 수정하여 GetMatchCount에 하위 스팬을 추가합니다.

7. 더 나은 분석을 위해 하위 스팬 추가

이전 단계에서 loadgen에서 관찰된 왕복 시간의 원인은 대부분 서버 서비스의 gRPC 핸들러인 GetMatchCount 메서드 내 프로세스라는 것을 확인했습니다. 하지만 핸들러 외에는 계측하지 않았기 때문에 폭포식 그래프에서 추가 통계를 찾을 수 없습니다. 이는 마이크로서비스를 계측할 때 흔히 발생하는 경우입니다.

3b63a1e471dddb8c.png

이 섹션에서는 서버가 Google Cloud Storage를 호출하는 하위 스팬을 계측합니다. 일부 외부 네트워크 I/O가 프로세스에서 시간이 오래 걸리는 경우가 많으며 호출이 원인인지 확인하는 것이 중요하기 때문입니다.

서버에서 하위 스팬 계측

서버에서 main.go를 열고 readFiles 함수를 찾습니다. 이 함수는 셰익스피어 작품의 모든 텍스트 파일을 가져오기 위해 Google Cloud Storage에 대한 요청을 호출합니다. 이 함수에서는 클라이언트 서비스의 HTTP 서버 계측에서와 같이 하위 스팬을 만들 수 있습니다.

step0/src/server/main.go

func readFiles(ctx context.Context, bucketName, prefix string) ([]string, error) {
        type resp struct {
                s   string
                err error
        }

        // step4: add an extra span
        span := trace.SpanFromContext(ctx)
        span.SetName("server.readFiles")
        span.SetAttributes(attribute.Key("bucketname").String(bucketName))
        defer span.End()
        // step4: end add span
        ...

새 스팬을 추가하는 방법은 여기까지입니다. 앱을 실행하여 결과를 확인해 보겠습니다.

마이크로서비스를 실행하고 트레이스를 확인합니다.

수정한 후에는 skaffold 명령어로 평소와 같이 클러스터를 실행하면 됩니다.

skaffold dev

트레이스 목록에서 query.request이라는 트레이스를 하나 선택합니다. shakesapp.ShakespeareService/GetMatchCount 아래에 새 스팬을 제외하고 유사한 트레이스 폭포식 그래프가 표시됩니다. (아래 빨간색 직사각형으로 묶인 범위)

3d4a891aa30d7a32.png

이 그래프에서 알 수 있는 것은 Google Cloud Storage에 대한 외부 호출이 상당한 지연 시간을 차지하지만 여전히 다른 요소가 지연 시간의 대부분을 차지한다는 것입니다.

trace 폭포식 그래프를 몇 번만 살펴봐도 많은 유용한 정보를 얻을 수 있습니다. 애플리케이션에서 성능 세부정보를 더 얻으려면 어떻게 해야 하나요? 여기에서 프로파일러가 사용됩니다. 하지만 지금은 이 Codelab을 종료하고 모든 프로파일러 튜토리얼을 2부로 위임하겠습니다.

요약

이 단계에서는 서버 서비스에 다른 스팬을 계측하고 시스템 지연 시간에 관한 추가 통계를 얻었습니다.

8. 축하합니다

OpenTelemetry로 분산 추적을 만들고 Google Cloud Trace에서 마이크로서비스 전반의 요청 지연 시간을 확인했습니다.

연장 연습문제는 다음 주제를 직접 시도해 보세요.

  • 현재 구현은 상태 점검에서 생성된 모든 스팬을 전송합니다. (grpc.health.v1.Health/Check) Cloud Trace에서 이러한 스팬을 제외하려면 어떻게 해야 하나요? 힌트는 여기에서 확인하세요.
  • 이벤트 로그를 스팬과 연결하고 Google Cloud Trace 및 Google Cloud Logging에서 작동하는 방식을 확인합니다. 힌트는 여기에서 확인하세요.
  • 일부 서비스를 다른 언어로 된 서비스로 대체하고 해당 언어의 OpenTelemetry로 계측해 봅니다.

또한 프로파일러에 대해 자세히 알아보려면 2단계로 이동하세요. 이 경우 아래의 정리 섹션을 건너뛸 수 있습니다.

정리

이 Codelab을 마친 후에는 Kubernetes 클러스터를 중지하고 프로젝트를 삭제하여 Google Kubernetes Engine, Google Cloud Trace, Google Artifact Registry에 예상치 못한 요금이 청구되지 않도록 하세요.

먼저 클러스터를 삭제합니다. skaffold dev로 클러스터를 실행하는 경우 Ctrl-C를 누르기만 하면 됩니다. skaffold run로 클러스터를 실행하는 경우 다음 명령어를 실행합니다.

skaffold delete

명령어 결과

Cleaning up...
 - deployment.apps "clientservice" deleted
 - service "clientservice" deleted
 - deployment.apps "loadgen" deleted
 - deployment.apps "serverservice" deleted
 - service "serverservice" deleted

클러스터를 삭제한 후 메뉴 창에서 'IAM 및 관리자' > '설정'을 선택한 다음 '종료' 버튼을 클릭합니다.

45aa37b7d5e1ddd1.png

그런 다음 대화상자의 양식에 프로젝트 ID (프로젝트 이름 아님)를 입력하고 종료를 확인합니다.