분산 추적을 사용하여 OpenCensus 및 Cloud Trace로 마이크로서비스 지연 관찰

이 가이드에서는 OpenCensus 및 Cloud Trace를 사용하여 마이크로서비스 앱의 trace 정보를 캡처하는 방법을 보여줍니다. 분산 추적은 요청이 복잡한 마이크로서비스 아키텍처를 통과하는 방식 및 각 단계와 관련된 지연을 이해하는 데 도움이 되는 시스템입니다. 이러한 특성은 관측 가능성의 핵심 기능입니다. 앱은 마이크로서비스로 전환되고 단일 사용자 트랜잭션과 관련된 구성요소 및 엔드포인트의 수는 증가하고 있습니다. 따라서 사용자 서비스를 안정적으로 운영하기 위해서는 여전히 관찰 기능이 중요합니다.

Kubernetes에서 실행되는 서비스의 경우 Istio와 같은 서비스 메시를 사용하면 전용 계측 장비 없이 서비스 간 트래픽을 분산 추적할 수 있습니다. 그러나 trace에 대한 더 폭넓은 제어를 원하거나, trace 정보에서 앱 내부를 캡처해야 하거나, Kubernetes에서 실행되지 않는 코드를 추적해야 하는 경우도 있습니다. OpenCensus는 분산 마이크로서비스 앱의 계측을 통해 다양한 언어, 플랫폼, 환경에서 trace 및 측정항목을 수집하는 오픈소스 라이브러리입니다.

이 가이드는 분산 추적의 기본 사항을 이해하고 적용하여 서비스 관찰 가능성을 개선하고자 하는 개발자, SRE, DevOps 엔지니어를 대상으로 합니다.

이 가이드는 다음 사항에 익숙하다고 가정합니다.

State of DevOps 보고서에는 소프트웨어 제공 성능을 향상시키는 기능이 나와 있습니다. 이 가이드에서는 다음 기능을 설명합니다.

목표

  • GKE 클러스터를 만들고 샘플 앱을 배포합니다.
  • OpenCensus 계측 코드를 검토합니다.
  • 계측에서 생성된 trace 및 로그를 검토합니다.

참조 아키텍처

다음 다이어그램은이 가이드에서 배포하는 아키텍처를 보여줍니다.

2개의 GKE 클러스터로 구성된 가이드용 아키텍처

앞의 다이어그램에서 나와 있듯이 2개의 GKE 클러스터를 만들고 각 클러스터에 앱을 배포합니다. 사용자 트래픽은 프런트엔드 클러스터의 프런트엔드 앱으로 전송됩니다. 프런트엔드 클러스터의 프런트엔드 pod는 백엔드 클러스터의 백엔드 pod와 통신합니다. 백엔드 pod는 외부 API 엔드포인트를 호출합니다.

완전 관리형 지속적 통합, 전송, 배포 플랫폼인 Cloud Build를 사용하여 샘플 코드에서 컨테이너 이미지를 만들고 Container Registry에 저장합니다. GKE 클러스터는 배포 시 Container Registry에서 이미지를 가져옵니다.

샘플 앱

이 가이드의 샘플 앱은 Go로 작성된 2개의 마이크로서비스로 구성됩니다.

프런트엔드 서비스는 / URL에서 HTTP 요청을 수락하고 백엔드 서비스를 호출합니다. 백엔드 서비스의 주소는 BACKEND 환경 변수로 정의됩니다. 변수의 값은 사용자가 만드는 ConfigMap 객체에 설정됩니다.

백엔드 서비스는 /URL에서 HTTP 요청을 수락하고 DESTINATION_URL 환경 변수로 정의된 외부 URL로 아웃바운드 호출을 수행합니다. 변수의 값은 ConfigMap 객체를 통해 설정됩니다. 외부 호출이 완료되면 backend 서비스는 호출자에게 HTTP 상태 호출(예: 200,)을 반환합니다.

trace, 범위, 컨텍스트 이해

분산 추적의 개념은 Google이 게시한 Dapper 연구 논문에 가장 잘 설명되어 있습니다. 논문의 다음 다이어그램은 trace의 5개 스팬을 보여줍니다.

trace에 5개의 스팬이 있는 분산 추적

다이어그램은 2개의 백엔드 요청을 수행하는 단일 프런트엔드 요청을 보여줍니다. 두 번째 백엔드 호출을 완료하려면 두 번의 도우미 호출이 필요합니다. 각 호출에는 스팬 ID와 상위 스팬의 ID 라벨이 표시됩니다.

trace는 분산 시스템이 사용자 요청에 응답하는 방법을 설명하는 정보의 총합입니다. trace는 스팬으로 구성되며 각 스팬은 사용자 요청 처리와 관련된 특정 요청 및 응답 쌍을 나타냅니다. 상위 스팬은 최종 사용자 관점에서 경험하는 지연을 설명합니다. 각 하위 스팬은 분산 시스템의 특정 서비스가 어떻게 호출되고 응답했는지를 각각에 대해 캡처된 지연 정보와 함께 설명합니다.

분산 시스템의 추적에서 문제는 다양한 백엔드 서비스에 대한 후속 요청이 수행될 때 원래 프런트엔드 요청에 대한 정보가 자동으로 또는 본질적으로 전달되지 않는다는 것입니다. Go에서 컨텍스트(클러스터 IP 주소와 사용자 인증 정보)로 요청을 수행할 수 있으며, 이는 HTTP 헤더에 추가 정보가 로드됨을 의미합니다. 컨텍스트의 개념은 OpenCensus로 확장되어 각 스팬 컨텍스트를 포함합니다. 이 경우 각 후속 요청에서 상위 스팬에 대한 정보를 포함할 수 있습니다. 그런 다음 하위 스팬을 추가해서 전체 trace를 구성하면 사용자 요청이 어떻게 시스템을 통과하고 최종적으로 사용자에게 다시 돌아오는지를 확인할 수 있습니다.

컨텍스트는 Go에 한정되지 않습니다. OpenCensus가 SpanContext 기능을 지원하는 언어에 대한 자세한 내용은 OpenCensus 기능 매트릭스를 참조하세요.

비용

이 가이드에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

이 가이드를 마치면 만든 리소스를 삭제하여 비용이 계속 청구되지 않게 할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google 계정으로 로그인합니다.

    아직 계정이 없으면 새 계정을 등록하세요.

  2. Cloud Console의 프로젝트 선택기 페이지에서 Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기 페이지로 이동

  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다. 프로젝트에 결제가 사용 설정되어 있는지 확인하는 방법을 알아보세요.

  4. GKE, Cloud Trace, Cloud Build, Cloud Storage, and Container Registry API를 사용 설정합니다.

    API 사용 설정

환경 설정

이 섹션에서는 이 가이드 전반에서 사용하는 도구로 환경을 설정합니다. Cloud Shell에서 이 가이드의 모든 터미널 명령어를 실행합니다.

  1. Cloud Console에서 Cloud Shell을 활성화합니다.

    Cloud Shell 활성화

    Cloud Console 하단에 Cloud Shell 세션이 시작되고 명령줄 프롬프트가 표시됩니다. Cloud Shell은 gcloud 명령줄 도구가 포함되고 Cloud SDK가 사전 설치된 셸 환경으로, 현재 프로젝트의 값이 이미 설정되어 있습니다. 세션이 초기화되는 데 몇 초 정도 걸릴 수 있습니다.

  2. 환경 변수를 설정합니다.
    export PROJECT_ID=$(gcloud config list --format 'value(core.project)' 2>/dev/null)
    
  3. 다음 Git 저장소를 클론하여 이 가이드에 필요한 파일을 다운로드합니다.
    git clone https://github.com/GoogleCloudPlatform/gke-observability-tutorials.git
    cd $HOME/gke-observability-tutorials/gke-opencensus-stackdriver/go/http
    WORKDIR=$(pwd)
    

    이 가이드와 관련된 모든 작업을 수행하는 $WORKDIR 저장소 폴더를 만듭니다. 가이드를 마치면 이 폴더를 삭제할 수 있습니다.

도구 설치

  1. Cloud Shell에서 kubectxkubens를 설치합니다.

    git clone https://github.com/ahmetb/kubectx $WORKDIR/kubectx
    export PATH=$PATH:$WORKDIR/kubectx
    

    이 두 도구를 사용하여 여러 Kubernetes 클러스터, 컨텍스트, 네임스페이스 관련 작업을 수행할 수 있습니다.

  2. Cloud Shell에서 오픈소스 부하 생성 도구인 Apache Bench를 설치합니다.

    sudo apt-get install apache2-utils
    

GKE 클러스터 만들기

이 섹션에서는 샘플 앱을 배포하는 2개의 GKE 클러스터를 만듭니다. GKE 클러스터는 기본적으로 Cloud Trace API에 대해 쓰기 전용 액세스 권한으로 생성됩니다. 따라서 클러스터를 만들 때 액세스 권한을 정의할 필요가 없습니다.

  1. Cloud Shell에서 클러스터를 만듭니다.

    gcloud container clusters create backend-cluster \
        --zone=us-west1-a --enable-stackdriver-kubernetes \
        --verbosity=none --async
    gcloud container clusters create frontend-cluster \
        --zone=us-west1-a --enable-stackdriver-kubernetes \
        --verbosity=none
    

    이 가이드에서 클러스터는 us-west1-a 영역에 위치합니다. 자세한 내용은 위치 및 리전을 참조하세요.

  2. 클러스터 사용자 인증 정보를 가져와 로컬에 저장합니다.

    gcloud container clusters get-credentials backend-cluster --zone=us-west1-a
    gcloud container clusters get-credentials frontend-cluster --zone=us-west1-a
    
  3. 클러스터의 컨텍스트 이름을 변경하여 이 가이드의 뒷부분에서 쉽게 액세스할 수 있도록 합니다.

    kubectx backend=gke_${PROJECT_ID}_us-west1-a_backend-cluster
    kubectx frontend=gke_${PROJECT_ID}_us-west1-a_frontend-cluster
    

OpenCensus 계측 검토

다음 섹션에서는 샘플 앱의 코드를 검토하여 컨텍스트 전파를 사용해서 여러 요청의 스팬을 단일 상위 trace에 추가하는 방법을 알아 봅니다.

프런트엔드 코드 검토

  • 프런트엔드 서비스 코드에는 3개의 가져오기가 있습니다.

    import (
    	"context"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"time"
    
    	"contrib.go.opencensus.io/exporter/stackdriver"
    	"go.opencensus.io/trace"
    
    	"go.opencensus.io/plugin/ochttp"
    	"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
    
    	"github.com/gorilla/mux"
    )
    
    • go.opencensus.io/plugin/ochttpgo.opencensus.io/plugin/ochttp/propagation/tracecontext 가져오기에는 여러 요청에 걸쳐 컨텍스트를 전파하는 ochttp 플러그인이 포함됩니다. 컨텍스트는 후속 스팬이 추가되는 trace에 대한 정보를 전달합니다.
    • contrib.go.opencensus.io/exporter/stackdriver 가져오기는 Trace로 trace를 내보냅니다. OpenCensus가 지원하는 백엔드 목록은 Exporters를 참조하세요.
    • github.com/gorilla/mux 가져오기는 샘플 앱에서 요청 처리에 사용하는 라이브러리입니다.
  • main() 함수는 Trace에 trace 내보내기를 설정하고 mux 라우터를 사용하여 / URL에 대한 요청을 처리합니다.

    func main() {
    	// set up Stackdriver exporter
    	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: projectID, Location: location})
    	if err != nil {
    		log.Fatal(err)
    	}
    	trace.RegisterExporter(exporter)
    	trace.ApplyConfig(trace.Config{
    		DefaultSampler: trace.AlwaysSample(),
    	})
    
    	// handle root request
    	r := mux.NewRouter()
    	r.HandleFunc("/", mainHandler)
    	var handler http.Handler = r
    	handler = &ochttp.Handler{
    		Handler:     handler,
    		Propagation: &tracecontext.HTTPFormat{}}
    
    	log.Fatal(http.ListenAndServe(":8081", handler))
    }
    
    • 핸들러는 HTTP 전파를 사용하여 요청에 컨텍스트를 추가합니다.
    • 이 가이드에서는 샘플링이 AlwaysSample로 설정됩니다. 기본적으로 OpenCensus는 사전에 정해진 속도로 trace를 샘플링하며, 이 매개변수를 사용하여 이 속도를 제어할 수 있습니다.
  • mainHandler() 함수를 검토합니다.

    func mainHandler(w http.ResponseWriter, r *http.Request) {
    	// create root span
    	ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
    	defer rootspan.End()
    
    	// create child span for backend call
    	ctx, childspan := trace.StartSpan(ctx, "call to backend")
    	defer childspan.End()
    
    	// create request for backend call
    	req, err := http.NewRequest("GET", backendAddr, nil)
    	if err != nil {
    		log.Fatalf("%v", err)
    	}
    
    	childCtx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond)
    	defer cancel()
    	req = req.WithContext(childCtx)
    
    	// add span context to backend call and make request
    	format := &tracecontext.HTTPFormat{}
    	format.SpanContextToRequest(childspan.SpanContext(), req)
    	//format.SpanContextToRequest(rootspan.SpanContext(), req)
    	client := http.DefaultClient
    	res, err := client.Do(req)
    	if err != nil {
    		log.Fatalf("%v", err)
    	}
    
    	fmt.Printf("%v\n", res.StatusCode)
    }
    
    • 이 함수는 전체적인 요청 지연을 캡처하기 위해 루트 스팬을 만듭니다. 후속 스팬을 루트 스팬에 추가합니다.

      // create root span
        ctx, rootspan := trace.StartSpan(context.Background(), "incoming call")
        defer rootspan.End()
      
    • 함수는 backend 호출 시간을 측정하기 위해 하위 스팬을 만듭니다.

      // create child span for backend call
        _, childspan := trace.StartSpan(ctx, "call to backend")
        defer childspan.End()
      
    • 이 함수는 백엔드에 대한 요청을 만듭니다.

      // create request for backend call
        req, err := http.NewRequest("GET", backendAddr, nil)
        if err != nil {
          log.Fatalf("%v", err)
        }
        childCtx, cancel := context.WithTimeout(req.Context(), 1000*time.Millisecond)
        defer cancel()
        req = req.WithContext(childCtx)
      
    • 이 함수는 해당 요청에 스팬 컨텍스트를 추가합니다.

      // add span context to backend call and make request
        format := &tracecontext.HTTPFormat{}
        format.SpanContextToRequest(rootspan.SpanContext(), req)
      

백엔드 코드 검토

  • 백엔드 코드의 main() 함수는 프런트엔드 서비스의 main() 함수와 동일합니다. 즉, Trace 내보내기를 설정하고 mux 라우터를 사용하여 mainHandler() 함수로 / URL에 대한 요청을 처리합니다.

    func main() {
    	// set up Stackdriver exporter
    	exporter, err := stackdriver.NewExporter(stackdriver.Options{ProjectID: projectID, Location: location})
    	if err != nil {
    		log.Fatal(err)
    	}
    	trace.RegisterExporter(exporter)
    	trace.ApplyConfig(trace.Config{
    		DefaultSampler: trace.AlwaysSample(),
    	})
    
    	// handle incoming request
    	r := mux.NewRouter()
    	r.HandleFunc("/", mainHandler)
    	var handler http.Handler = r
    	// handler = &logHandler{log: log, next: handler}
    
    	handler = &ochttp.Handler{
    		Handler:     handler,
    		Propagation: &tracecontext.HTTPFormat{}}
    
    	log.Fatal(http.ListenAndServe(":8080", handler))
    }
    
  • mainHandler() 함수는 수신 요청을 사용하여 Go에서 제공한 HTTP 컨텍스트와 OpenCensus에서 제공한 trace 컨텍스트를 모두 가져옵니다. 이 함수는 하위 스팬을 만들고 trace 컨텍스트에 추가합니다. 마지막으로 callRemoteEndpoint() 함수를 호출하여 지연을 캡처해야하는 또 다른 호출을 수행합니다.

    func mainHandler(w http.ResponseWriter, r *http.Request) {
    	// get context from incoming request
    	ctx := r.Context()
    	// get span context from incoming request
    	HTTPFormat := &tracecontext.HTTPFormat{}
    	if spanContext, ok := HTTPFormat.SpanContextFromRequest(r); ok {
    		_, span := trace.StartSpanWithRemoteParent(ctx, "call remote endpoint", spanContext)
    		defer span.End()
    		returnCode := callRemoteEndpoint()
    		fmt.Fprintf(w, returnCode)
    	}
    }
    
  • callRemoteEndpoint() 함수는 HTTP 라이브러리를 사용하여 원격 엔드포인트 호출을 수행합니다. 이 호출에 대한 추적은 상위 함수에서 처리되므로 trace 컨텍스트를 사용하거나 추가 하위 스팬을 만들 필요가 없습니다.

    // make an outbound call
    func callRemoteEndpoint() string {
    	resp, err := http.Get(destURL)
    	if err != nil {
    		log.Fatal("could not fetch remote endpoint")
    	}
    	defer resp.Body.Close()
    	body, err := ioutil.ReadAll(resp.Body)
    	if err != nil {
    		log.Fatal("could not read response from Google")
    		log.Fatal(body)
    	}
    
    	return strconv.Itoa(resp.StatusCode)
    }
    

앱 배포

이 섹션에서는 Cloud Build를 사용하여 백엔드 및 프런트엔드 서비스의 컨테이너 이미지를 빌드하고 이를 GKE 클러스터에 배포합니다.

백엔드 서비스 빌드

  1. Cloud Shell에서 backend 디렉터리로 변경합니다.

    cd $WORKDIR/backend/
    
  2. 빌드를 제출합니다.

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/backend:latest
    
  3. 컨테이너 이미지가 성공적으로 생성되었고Container Registry에서 사용할 수 있는지 확인합니다.

    gcloud container images list
    

    출력이 다음과 비슷하면 컨테이너 이미지가 성공적으로 생성된 것입니다. 여기서 project-id는 클라우드 프로젝트 ID입니다.

    NAME
    gcr.io/project-id/backend
    

백엔드 서비스 배포

  1. Cloud Shell에서 kubectx 컨텍스트를 backend 클러스터로 설정합니다.

    kubectx backend
    
  2. configmap 파일을 배포합니다.

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < backend-configmap.yaml | kubectl apply -f -
    
  3. backend 배포 파일을 만듭니다.

    envsubst < backend-deployment.yaml | kubectl apply -f -
    
  4. pod가 실행 중인지 확인합니다.

    kubectl get pods
    

    출력에서 StatusRunning으로 표시됩니다.

    NAME                       READY   STATUS    RESTARTS   AGE
    backend-645859d95b-7mx95   1/1     Running   0          52s
    backend-645859d95b-qfdnc   1/1     Running   0          52s
    backend-645859d95b-zsj5m   1/1     Running   0          52s
    
  5. 부하 분산기를 사용하여 backend 배포를 노출합니다.

    kubectl expose deployment backend --type=LoadBalancer
    
  6. backend 서비스의 IP 주소를 가져옵니다.

    kubectl get services backend
    

    출력은 다음과 비슷합니다.

    NAME      TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
    backend   LoadBalancer   10.11.247.58   34.83.88.143   8080:30714/TCP   70s
    

    서비스의 EXTERNAL-IP 필드가 <pending>에서 IP 주소로 변경될 때까지 이 명령어를 반복합니다.

  7. 이전 단계의 IP 주소를 변수로 캡처합니다.

    export BACKEND_IP=$(kubectl get svc backend -ojson | jq -r '.status.loadBalancer.ingress[].ip')
    

프런트엔드 서비스 빌드

  1. Cloud Shell에서 frontend 디렉터리로 변경합니다.

    cd $WORKDIR/frontend/
    
  2. 빌드를 제출합니다.

    gcloud builds submit . --tag=gcr.io/$PROJECT_ID/frontend:latest
    
  3. 컨테이너 이미지가 성공적으로 생성되었고Container Registry에서 사용할 수 있는지 확인합니다.

    gcloud container images list
    

    출력이 다음과 비슷한 경우 컨테이너 이미지가 성공적으로 생성된 것입니다.

    NAME
    gcr.io/qwiklabs-gcp-47a7dcba55b334f7/backend
    gcr.io/qwiklabs-gcp-47a7dcba55b334f7/frontend
    

프런트엔드 서비스 배포

  1. Cloud Shell에서 kubectx 컨텍스트를 백엔드 클러스터로 설정합니다.

    kubectx frontend
    
  2. configmap 파일을 배포합니다.

    export PROJECT_ID=$(gcloud info --format='value(config.project)')
    envsubst < frontend-configmap.yaml | kubectl apply -f -
    
  3. frontend 배포 파일을 만듭니다.

    envsubst < frontend-deployment.yaml | kubectl apply -f -
    
  4. pod가 실행 중인지 확인합니다.

    kubectl get pods
    

    출력에서 StatusRunning으로 표시됩니다.

    NAME                        READY   STATUS    RESTARTS   AGE
    frontend-747b445499-v7x2w   1/1     Running   0          57s
    frontend-747b445499-vwtmg   1/1     Running   0          57s
    frontend-747b445499-w47pf   1/1     Running   0          57s
    
  5. 부하 분산기를 사용하여 frontend 배포를 노출합니다.

    kubectl expose deployment frontend --type=LoadBalancer
    
  6. frontend 서비스의 IP 주소를 가져옵니다.

    kubectl get services frontend
    

    출력은 다음과 비슷합니다.

    NAME       TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    frontend   LoadBalancer   10.27.241.93   34.83.111.232   8081:31382/TCP   70s
    

    서비스의 EXTERNAL-IP 필드가 <pending>에서 IP 주소로 변경될 때까지 이 명령어를 반복합니다.

  7. 이전 단계의 IP 주소를 변수로 캡처합니다.

    export FRONTEND_IP=$(kubectl get svc frontend -ojson | jq -r '.status.loadBalancer.ingress[].ip')
    

앱 로드 및 trace 검토

이 섹션에서는 Apache Bench 유틸리티를 사용하여 앱에 대한 요청을 만들고 Trace에서 결과 trace를 검토합니다.

  1. Cloud Shell에서 Apache Bench를 사용하여 3개의 동시 스레드가 있는 요청을 1000개 생성합니다.

    ab -c 3 -n 1000 http://${FRONTEND_IP}:8081/
    
  2. Cloud Console에서 trace 목록 페이지로 이동합니다.

    trace 목록 페이지로 이동

  3. 타임라인을 검토하려면 incoming call 라벨이 있는 URI 중 하나를 클릭합니다.

    trace의 분산형 그래프

    이 trace에는 다음 이름을 가진 3개의 스팬이 포함됩니다.

    • incoming call 스팬은 클라이언트에서 관찰된 엔드 투 엔드 지연을 캡처합니다.
    • call to backend 스팬은 프런트엔드에서 관찰된 백엔드 호출의 지연을 캡처합니다.
    • call remote endpoint 스팬은 백엔드에서 관찰된 외부 엔드 포인트 호출의 지연을 캡처합니다.

    스팬의 막대 그래프

삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 가이드에서 만든 Cloud 프로젝트를 삭제하는 것입니다. 또는 개별 리소스를 삭제할 수 있습니다.

프로젝트 삭제

  1. Cloud Console에서 리소스 관리 페이지로 이동합니다.

    리소스 관리 페이지로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제 를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 다음 종료를 클릭하여 프로젝트를 삭제합니다.

다음 단계