로컬 문제 해결


이 튜토리얼에서는 Stackdriver 도구를 사용해 검색하고 로컬 개발 워크플로를 통해 조사하여 Knative serving 서비스의 문제를 해결하는 방법을 보여줍니다.

문제 해결 가이드의 단계별 '우수사례' 컴패니언은 배포 시 런타임 오류가 발생하는 샘플 프로젝트를 사용하여 문제를 찾고 해결합니다.

목표

  • Knative serving에 대한 서비스 작성, 빌드, 배포
  • Cloud Logging을 사용하여 오류 식별
  • 근본 원인 분석을 위해 Container Registry에서 컨테이너 이미지 검색
  • '프로덕션' 서비스 수정 후 향후 문제 해결을 위한 서비스 개선

비용

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

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

시작하기 전에

코드 구성

새 Knative serving greeter 서비스를 단계별로 빌드합니다. 이 서비스는 문제해결 연습을 위해 런타임 오류를 일으킵니다.

  1. 새 프로젝트를 만듭니다.

    Node.js

    서비스 패키지, 초기 종속 항목, 일부 공통 작업을 정의하여 Node.js 프로젝트를 만듭니다.

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. package.json 파일을 생성합니다.

      npm init --yes
      npm install --save express@4
      
    3. 편집기에서 새 package.json 파일을 열고 node index.js를 실행하도록 start 스크립트를 구성합니다. 완료되면 파일이 다음과 같이 표시됩니다.

      {
        "name": "hello-service",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
            "start": "node index.js",
            "test": "echo \"Error: no test specified\" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
            "express": "^4.17.1"
        }
      }

    중간 튜토리얼을 넘어서 이 서비스를 계속 발전시키려면 설명, 저자 입력과 라이선스 평가를 고려하세요. 자세한 내용은 package.json 문서를 참조하세요.

    Python

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. requirements.txt 파일을 만들고 여기에 종속 항목을 복사합니다.

      Flask==3.0.0
      pytest==7.0.1; python_version > "3.0"
      # pin pytest to 4.6.11 for Python2.
      pytest==7.0.1; python_version < "3.0"
      gunicorn==20.1.0
      Werkzeug==3.0.1
      

    Go

    1. hello-service 디렉터리를 만듭니다.

      mkdir hello-service
      cd hello-service
      
    2. go 모듈을 초기화하여 Go 프로젝트를 만듭니다.

      go mod init <var>my-domain</var>.com/hello-service
      

    특정 이름은 원하는 대로 업데이트할 수 있습니다. 코드가 웹에 도달할 수 있는 코드 저장소에 게시된 경우 이름을 업데이트해야 합니다.

    Java

    1. Maven 프로젝트를 만듭니다.

      mvn archetype:generate \
        -DgroupId=com.example \
        -DartifactId=hello-service \
        -DarchetypeArtifactId=maven-archetype-quickstart \
        -DinteractiveMode=false
      
    2. 종속 항목을 pom.xml 종속 항목 목록(<dependencies> 요소 사이)에 복사합니다.

      <dependency>
        <groupId>com.sparkjava</groupId>
        <artifactId>spark-core</artifactId>
        <version>2.9.4</version>
      </dependency>
      <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.12</version>
      </dependency>
      <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.12</version>
      </dependency>
      
    3. 빌드 설정을 pom.xml(<dependencies> 요소 아래)에 복사합니다.

      <build>
        <plugins>
          <plugin>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>jib-maven-plugin</artifactId>
            <version>3.4.0</version>
            <configuration>
              <to>
                <image>gcr.io/PROJECT_ID/hello-service</image>
              </to>
            </configuration>
          </plugin>
        </plugins>
      </build>
      

  2. 새로 추가되는 요청을 처리할 HTTP 서비스를 만듭니다.

    Node.js

    const express = require('express');
    const app = express();
    
    app.get('/', (req, res) => {
      console.log('hello: received request.');
    
      const {NAME} = process.env;
      if (!NAME) {
        // Plain error logs do not appear in Stackdriver Error Reporting.
        console.error('Environment validation failed.');
        console.error(new Error('Missing required server parameter'));
        return res.status(500).send('Internal Server Error');
      }
      res.send(`Hello ${NAME}!`);
    });
    const port = parseInt(process.env.PORT) || 8080;
    app.listen(port, () => {
      console.log(`hello: listening on port ${port}`);
    });

    Python

    import json
    import os
    
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/", methods=["GET"])
    def index():
        """Example route for testing local troubleshooting.
    
        This route may raise an HTTP 5XX error due to missing environment variable.
        """
        print("hello: received request.")
    
        NAME = os.getenv("NAME")
    
        if not NAME:
            print("Environment validation failed.")
            raise Exception("Missing required service parameter.")
    
        return f"Hello {NAME}"
    
    if __name__ == "__main__":
        PORT = int(os.getenv("PORT")) if os.getenv("PORT") else 8080
    
        # This is used when running locally. Gunicorn is used to run the
        # application on Cloud Run. See entrypoint in Dockerfile.
        app.run(host="127.0.0.1", port=PORT, debug=True)

    Go

    
    // Sample hello demonstrates a difficult to troubleshoot service.
    package main
    
    import (
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    func main() {
    	log.Print("hello: service started")
    
    	http.HandleFunc("/", helloHandler)
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    		log.Printf("Defaulting to port %s", port)
    	}
    
    	log.Printf("Listening on port %s", port)
    	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
    }
    
    func helloHandler(w http.ResponseWriter, r *http.Request) {
    	log.Print("hello: received request")
    
    	name := os.Getenv("NAME")
    	if name == "" {
    		log.Printf("Missing required server parameter")
    		// The panic stack trace appears in Cloud Error Reporting.
    		panic("Missing required server parameter")
    	}
    
    	fmt.Fprintf(w, "Hello %s!\n", name)
    }
    

    자바

    import static spark.Spark.get;
    import static spark.Spark.port;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class App {
    
      private static final Logger logger = LoggerFactory.getLogger(App.class);
    
      public static void main(String[] args) {
        int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
        port(port);
    
        get(
            "/",
            (req, res) -> {
              logger.info("Hello: received request.");
              String name = System.getenv("NAME");
              if (name == null) {
                // Standard error logs do not appear in Stackdriver Error Reporting.
                System.err.println("Environment validation failed.");
                String msg = "Missing required server parameter";
                logger.error(msg, new Exception(msg));
                res.status(500);
                return "Internal Server Error";
              }
              res.status(200);
              return String.format("Hello %s!", name);
            });
      }
    }

  3. Dockerfile을 만들어 서비스를 배포하는 데 사용되는 컨테이너 이미지를 정의합니다.

    Node.js

    
    # Use the official lightweight Node.js image.
    # https://hub.docker.com/_/node
    FROM node:18-slim
    
    # Create and change to the app directory.
    WORKDIR /usr/src/app
    
    # Copy application dependency manifests to the container image.
    # A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
    # Copying this first prevents re-running npm install on every code change.
    COPY package*.json ./
    
    # Install production dependencies.
    # If you add a package-lock.json, speed your build by switching to 'npm ci'.
    # RUN npm ci --only=production
    RUN npm install --only=production
    
    # Copy local code to the container image.
    COPY . ./
    
    # Run the web service on container startup.
    CMD [ "npm", "start" ]
    

    Python

    
    # Use the official Python image.
    # https://hub.docker.com/_/python
    FROM python:3.11
    
    # Allow statements and log messages to immediately appear in the Cloud Run logs
    ENV PYTHONUNBUFFERED True
    
    # Copy application dependency manifests to the container image.
    # Copying this separately prevents re-running pip install on every code change.
    COPY requirements.txt ./
    
    # Install production dependencies.
    RUN pip install -r requirements.txt
    
    # Copy local code to the container image.
    ENV APP_HOME /app
    WORKDIR $APP_HOME
    COPY . ./
    
    # Run the web service on container startup.
    # Use gunicorn webserver with one worker process and 8 threads.
    # For environments with multiple CPU cores, increase the number of workers
    # to be equal to the cores available.
    # Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
    CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
    

    Go

    
    # Use the offical golang image to create a binary.
    # This is based on Debian and sets the GOPATH to /go.
    # https://hub.docker.com/_/golang
    FROM golang:1.21-bookworm as builder
    
    # Create and change to the app directory.
    WORKDIR /app
    
    # Retrieve application dependencies.
    # This allows the container build to reuse cached dependencies.
    # Expecting to copy go.mod and if present go.sum.
    COPY go.* ./
    RUN go mod download
    
    # Copy local code to the container image.
    COPY . ./
    
    # Build the binary.
    RUN go build -v -o server
    
    # Use the official Debian slim image for a lean production container.
    # https://hub.docker.com/_/debian
    # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
    FROM debian:bookworm-slim
    RUN set -x && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
        ca-certificates && \
        rm -rf /var/lib/apt/lists/*
    
    # Copy the binary to the production image from the builder stage.
    COPY --from=builder /app/server /server
    
    # Run the web service on container startup.
    CMD ["/server"]
    

    자바

    이 샘플은 Jib를 사용해서 일반적인 자바 도구로 Docker 이미지를 빌드합니다. Jib는 Dockerfile을 사용하거나 Docker를 설치할 필요 없이 컨테이너 빌드를 최적화합니다. Jib로 자바 컨테이너 빌드에 대해 자세히 알아보세요.

    <plugin>
      <groupId>com.google.cloud.tools</groupId>
      <artifactId>jib-maven-plugin</artifactId>
      <version>3.4.0</version>
      <configuration>
        <to>
          <image>gcr.io/PROJECT_ID/hello-service</image>
        </to>
      </configuration>
    </plugin>
    

코드 제공

코드 제공은 Cloud Build로 컨테이너 이미지를 빌드하고, Container Registry에 컨테이너 이미지를 업로드하고, 컨테이너 이미지를 Knative serving에 배포하는 세 단계로 구성됩니다.

코드를 제공하려면 다음 안내를 따르세요.

  1. 컨테이너를 빌드하고 Container Registry에 게시합니다.

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Java

    mvn compile jib:build -Dimage=gcr.io/PROJECT_ID/hello-service

    여기서 PROJECT_ID는 Google Cloud 프로젝트 ID입니다. gcloud config get-value project로 현재 프로젝트 ID를 확인할 수 있습니다.

    성공하면 BUILD SUCCESS 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

  2. 다음 명령어를 실행하여 앱을 배포합니다.

    gcloud run deploy hello-service --image gcr.io/PROJECT_ID/hello-service

    PROJECT_ID를 Google Cloud 프로젝트 ID로 바꿉니다. hello-service는 컨테이너 이미지 이름이자 Knative serving 서비스의 이름입니다. 컨테이너 이미지는 이전에 gcloud 설정에서 구성한 서비스 및 클러스터에 배포됩니다.

    배포가 완료될 때까지 기다립니다. 이 작업은 30초 정도 걸릴 수 있습니다. 성공하면 명령줄에 서비스 URL이 표시됩니다.

사용해 보기

성공적으로 배포되었는지 확인하기 위해 서비스를 시도합니다. 요청이 HTTP 500 또는 503 오류(5xx 서버 오류 중 일부)로 실패해야 합니다. 이 튜토리얼에서는 이 오류 응답의 문제 해결 방법을 단계별로 살펴봅니다.

클러스터가 라우팅 가능한 기본 도메인으로 구성된 경우 위 단계를 건너뛰고 URL을 웹브라우저에 복사합니다.

자동 TLS 인증서도메인 매핑을 사용하지 않으면 해당 서비스로 탐색 가능한 URL이 제공되지 않습니다.

대신 제공된 URL과 서비스 인그레스 게이트웨이의 IP 주소를 사용하여 서비스에 요청할 수 있는 curl 명령어를 만듭니다.

  1. 부하 분산기의 외부 IP를 가져오려면 다음 명령어를 실행합니다.

    kubectl get svc istio-ingressgateway -n ASM-INGRESS-NAMESPACE
    

    ASM-INGRESS-NAMESPACE를 Anthos Service Mesh 인그레스가 있는 네임스페이스로 바꿉니다. 기본 구성을 사용하여 Anthos Service Mesh를 설치한 경우 istio-system을 지정합니다.

    결과 출력은 다음과 유사합니다.

    NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP  PORT(S)
    istio-ingressgateway   LoadBalancer   XX.XX.XXX.XX   pending      80:32380/TCP,443:32390/TCP,32400:32400/TCP
    

    여기서 EXTERNAL-IP 값은 부하 분산기의 외부 IP 주소입니다.

  2. URL에서 이 GATEWAY_IP 주소를 사용하여 curl 명령어를 실행합니다.

     curl -G -H "Host: SERVICE-DOMAIN" https://EXTERNAL-IP/

    SERVICE-DOMAIN을 해당 서비스에 할당된 기본 도메인으로 바꿉니다. 이를 위해서는 기본 URL을 가져오고 http:// 프로토콜을 삭제하면 됩니다.

  3. HTTP 500 또는 HTTP 503 오류 메시지를 참조하세요.

문제 조사

사용해 보기에서 위에 표시된 HTTP 5xx 오류가 프로덕션 런타임 오류로 발생한 것을 시각적으로 표시합니다. 이 튜토리얼에서는 이를 처리하기 위한 공식 프로세스를 단계별로 살펴봅니다. 프로덕션 오류 해결 프로세스가 매우 다양하지만 이 튜토리얼에서는 일련의 단계에 따라 유용한 도구와 기법을 적용하는 방법을 보여줍니다.

여기에서는 문제 조사를 위해 다음과 같은 단계를 수행합니다.

  • 추가 조사 지원 및 완화 전략 설정을 위해 보고된 오류에 대한 추가 세부정보를 수집합니다.
  • 수정 사항을 푸시할지 아니면 알려진 정상 버전으로 롤백할지 결정하여 사용자에 대한 영향을 완화시킵니다.
  • 오류를 재현하여 올바른 세부정보가 수집되었으며 오류가 일회성 결함이 아님을 확인합니다.
  • 버그에 대해 근본 원인 분석을 수행하여 오류를 일으킨 코드, 구성, 프로세스를 찾습니다.

조사를 시작할 때는 URL, 타임스탬프, '내부 서버 오류' 메시지로 시작합니다.

추가 세부정보 수집

문제에 대한 추가 정보를 수집하여 발생한 결과를 파악하고 수행할 다음 단계를 결정합니다.

사용 가능한 도구를 사용하여 세부정보를 수집합니다.

  1. 자세한 내용은 로그를 확인하세요.

  2. Cloud Logging을 사용하여 오류 메시지를 포함하여 문제를 일으키는 작업 순서를 검토합니다.

정상 버전으로 롤백

정상 작동하는 버전이 있다면 서비스를 롤백하여 해당 버전을 사용할 수 있습니다. 예를 들어 이 튜토리얼에서 배포한 새 hello-service 서비스에는 하나의 버전만 포함되므로 롤백을 수행할 수 없습니다.

버전을 찾고 서비스를 롤백하려면 다음 안내를 따르세요.

  1. 서비스의 모든 버전 나열

  2. 모든 트래픽을 정상 버전으로 마이그레이션

오류 재현

이전에 가져온 세부정보를 사용하여 테스트 조건에서 문제가 일관적으로 발생하는지 확인합니다.

사용해 보기로 동일한 HTTP 요청을 보내고 동일한 오류와 세부정보가 보고되는지 확인합니다. 오류 세부정보가 표시되는 데 다소 시간이 걸릴 수 있습니다.

이 튜토리얼의 샘플 서비스가 읽기 전용이고 복잡한 부작용을 일으키지 않기 때문에 프로덕션에서 오류를 재현해도 안전합니다. 하지만 많은 실제 서비스의 경우에는 이와 달리, 테스트 환경에서 오류를 재현하거나 지역 조사로만 이 단계를 제한해야 할 수 있습니다.

오류를 재현하면 추가 작업을 위한 컨텍스트가 설정됩니다. 예를 들어 개발자가 오류를 재현할 수 없는 경우 추가 조사를 위해 서비스의 추가 도구화가 필요할 수 있습니다.

근본 원인 분석 수행

근본 원인 분석은 효과적인 문제 해결에서 증상 대신 문제 자체를 해결할 수 있게 해주는 중요한 단계입니다.

이 튜토리얼의 앞 부분에서는 Knative serving에서 문제를 재현하여 서비스가 Knative serving에서 호스팅될 때 문제가 발생하는지 확인했습니다. 이제는 문제를 로컬로 재현하여 문제가 해당 코드로 격리되는지 또는 프로덕션 호스팅에서만 발생하는지 확인합니다.

  1. 지금까지 Container Registry에서 Docker CLI를 로컬로 사용하지 않았으면 gcloud에서 인증합니다.

    gcloud auth configure-docker

    다른 접근 방식은 Container Registry 인증 방법을 참조하세요.

  2. 가장 최근에 사용한 컨테이너 이미지 이름을 사용할 수 없으면 서비스 설명에 가장 최근에 배포된 컨테이너 이미지의 정보가 포함됩니다.

    gcloud run services describe hello-service

    spec 객체에서 컨테이너 이미지 이름을 찾습니다. 명령어에 대상을 더 많이 지정하면 이를 직접 검색할 수 있습니다.

    gcloud run services describe hello-service \
       --format="value(spec.template.spec.containers.image)"

    이 명령어는 gcr.io/PROJECT_ID/hello-service와 같은 컨테이너 이미지 이름을 보여줍니다.

  3. Container Registry에서 환경으로 컨테이너 이미지를 가져옵니다. 이 단계에서는 컨테이너 이미지를 다운로드하는 데 몇 분 정도 걸릴 수 있습니다.

    docker pull gcr.io/PROJECT_ID/hello-service

    나중에 동일한 명령어를 사용해서 이 이름을 다시 사용하는 컨테이너 이미지 업데이트를 검색할 수 있습니다. 이 단계를 건너뛰면 아래 docker run 명령어가 로컬 머신에 없는 경우 컨테이너 이미지를 가져옵니다.

  4. 로컬로 실행하여 문제가 Knative serving에 고유한 문제가 아닌지 확인합니다.

    PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
       gcr.io/PROJECT_ID/hello-service

    위 명령어의 각 요소들에 대한 설명은 다음과 같습니다.

    • PORT 환경 변수는 서비스에서 컨테이너 내부에서 리슨할 포트를 결정하는 데 사용됩니다.
    • run 명령어는 Dockerfile 또는 상위 컨테이너 이미지에 정의된 진입점 명령어로 컨테이너를 시작합니다.
    • --rm 플래그는 종료 시 컨테이너 인스턴스를 삭제합니다.
    • -e 플래그는 환경 변수에 값을 할당합니다. -e PORT=$PORT는 로컬 시스템에서 변수 이름이 동일한 컨테이너로 PORT 변수를 전달합니다.
    • -p 플래그는 localhost 포트 9000에서 사용 가능한 서비스로 컨테이너를 게시합니다. localhost:9000에 대한 요청은 포트 8080의 컨테이너로 라우팅됩니다. 즉, 사용 중인 포트 번호에 대한 서비스 출력이 서비스 액세스 방법과 일치하지 않습니다.
    • 마지막 인수 gcr.io/PROJECT_ID/hello-service는 컨테이너 이미지의 최신 버전을 가리키는 저장소 경로입니다. 로컬에서 사용할 수 없는 경우 docker는 원격 레지스트리에서 이미지를 검색합니다.

    브라우저에서 http://localhost:9000을 엽니다. 터미널 출력에서 Google Cloud Observability의 오류 메시지와 일치하는 오류 메시지를 확인하세요.

    로컬에서 문제를 재현할 수 없는 경우 Knative serving 환경에서만 발생할 수 있습니다. 조사할 특정 영역은 Knative serving 문제 해결 가이드를 참조하세요.

    여기에서는 오류가 로컬로 재현됩니다.

이제 오류가 영구적으로 확인되었으며 호스팅 플랫폼 대신 서비스 코드로 인해 발생했으므로 코드를 더 자세히 조사해야 합니다.

이 튜토리얼에서는 컨테이너 내부의 코드와 로컬 시스템에 있는 코드가 동일하다고 간주해도 안전합니다.

Node.js

index.js 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.
const {NAME} = process.env;
if (!NAME) {
  // Plain error logs do not appear in Stackdriver Error Reporting.
  console.error('Environment validation failed.');
  console.error(new Error('Missing required server parameter'));
  return res.status(500).send('Internal Server Error');
}

Python

main.py 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.
NAME = os.getenv("NAME")

if not NAME:
    print("Environment validation failed.")
    raise Exception("Missing required service parameter.")

Go

main.go 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.

name := os.Getenv("NAME")
if name == "" {
	log.Printf("Missing required server parameter")
	// The panic stack trace appears in Cloud Error Reporting.
	panic("Missing required server parameter")
}

자바

App.java 파일에서 로그에 표시된 스택 트레이스 행 번호 주위에서 오류 메시지 소스를 찾습니다.

String name = System.getenv("NAME");
if (name == null) {
  // Standard error logs do not appear in Stackdriver Error Reporting.
  System.err.println("Environment validation failed.");
  String msg = "Missing required server parameter";
  logger.error(msg, new Exception(msg));
  res.status(500);
  return "Internal Server Error";
}

이 코드를 검사할 때 NAME 환경 변수가 설정되어 있지 않으면 다음 작업이 수행됩니다.

  • Google Cloud Observability에 오류가 기록됩니다.
  • HTTP 오류 응답이 전송됩니다.

이 문제는 누락된 변수로 인해 발생하지만 근본 원인은 더 구체적입니다. 환경 변수에 하드 종속성을 추가하는 코드 변경에 배포 스크립트 및 런타임 요구사항 문서에 대한 변경사항이 포함되지 않았습니다.

근본 원인 해결

이제 코드를 수집하고 잠재적 근본 원인을 파악했으므로 이를 해결하기 위한 단계를 수행할 수 있습니다.

  • NAME 환경 변수를 사용해서 서비스가 로컬로 작동하는지 확인합니다.

    1. 환경 변수를 추가하여 컨테이너를 로컬로 실행합니다.

      PORT=8080 && docker run --rm -e PORT=$PORT -p 9000:$PORT \
       -e NAME="Local World!" \
       gcr.io/PROJECT_ID/hello-service
    2. 브라우저를 http://localhost:9000으로 이동합니다.

    3. 페이지에 'Hello Local World!'가 표시되는지 확인합니다.

  • 다음 변수를 포함하도록 실행 중인 Knative serving 서비스 환경을 수정합니다.

    1. --update-env-vars 매개변수로 서비스 업데이트 명령어를 실행하여 환경 변수를 추가합니다.

      gcloud run services update hello-service \
        --update-env-vars NAME=Override
      
    2. Knative serving이 새로 추가된 환경 변수를 사용해서 이전 버전을 기준으로 새 버전을 만드는 동안 잠시 기다립니다.

  • 이제 서비스가 수정되었는지 확인합니다.

    1. 브라우저에서 Knative serving 서비스 URL로 이동합니다.
    2. "Hello Override!"가 페이지에 표시되는지 확인합니다.
    3. Cloud Logging에 예상치 못한 메시지나 오류가 표시되지 않는지 확인합니다.

이후 문제해결 속도 개선

이 샘플 프로덕션 문제에서 오류는 운영 구성과 관련되어 있습니다. 이후에 이 문제의 영향을 최소화하는 코드 변경사항이 있습니다.

  • 보다 구체적인 세부정보를 포함하도록 오류 로그를 개선합니다.
  • 오류를 반환하는 대신 서비스를 안전한 기본값으로 대체합니다. 기본값을 사용할 때 정상 기능이 변경될 경우, 모니터링 목적으로 경고 메시지를 사용합니다.

이제 하드 종속 항목으로서 NAME 환경 변수 삭제 단계를 수행합니다.

  1. 기존 NAME 처리 코드를 삭제합니다.

    Node.js

    const {NAME} = process.env;
    if (!NAME) {
      // Plain error logs do not appear in Stackdriver Error Reporting.
      console.error('Environment validation failed.');
      console.error(new Error('Missing required server parameter'));
      return res.status(500).send('Internal Server Error');
    }

    Python

    NAME = os.getenv("NAME")
    
    if not NAME:
        print("Environment validation failed.")
        raise Exception("Missing required service parameter.")

    Go

    name := os.Getenv("NAME")
    if name == "" {
    	log.Printf("Missing required server parameter")
    	// The panic stack trace appears in Cloud Error Reporting.
    	panic("Missing required server parameter")
    }

    자바

    String name = System.getenv("NAME");
    if (name == null) {
      // Standard error logs do not appear in Stackdriver Error Reporting.
      System.err.println("Environment validation failed.");
      String msg = "Missing required server parameter";
      logger.error(msg, new Exception(msg));
      res.status(500);
      return "Internal Server Error";
    }

  2. 대체 값을 설정하는 새 코드를 추가합니다.

    Node.js

    const NAME = process.env.NAME || 'World';
    if (!process.env.NAME) {
      console.log(
        JSON.stringify({
          severity: 'WARNING',
          message: `NAME not set, default to '${NAME}'`,
        })
      );
    }

    Python

    NAME = os.getenv("NAME")
    
    if not NAME:
        NAME = "World"
        error_message = {
            "severity": "WARNING",
            "message": f"NAME not set, default to {NAME}",
        }
        print(json.dumps(error_message))

    Go

    name := os.Getenv("NAME")
    if name == "" {
    	name = "World"
    	log.Printf("warning: NAME not set, default to %s", name)
    }

    자바

    String name = System.getenv().getOrDefault("NAME", "World");
    if (System.getenv("NAME") == null) {
      logger.warn(String.format("NAME not set, default to %s", name));
    }

  3. 영향을 받는 구성 사례를 통해 컨테이너를 다시 빌드하고 실행하여 로컬로 테스트합니다.

    Node.js

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    Python

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    Go

    docker build --tag gcr.io/PROJECT_ID/hello-service .

    자바

    mvn compile jib:build

    NAME 환경 변수가 계속 작동하는지 확인합니다.

    PORT=8080 && docker run --rm -e $PORT -p 9000:$PORT \
     -e NAME="Robust World" \
     gcr.io/PROJECT_ID/hello-service

    NAME 변수 없이 서비스가 작동하는지 확인합니다.

    PORT=8080 && docker run --rm -e $PORT -p 9000:$PORT \
     gcr.io/PROJECT_ID/hello-service

    서비스가 결과를 반환하지 않으면 첫 번째 단계의 코드 삭제로 응답 작성에 사용되는 것과 같은 추가 행이 삭제되지 않았는지 확인합니다.

  4. 코드 배포 섹션을 다시 확인하여 이를 배포합니다.

    서비스에 배포할 때마다 새 버전이 생성되고 준비가 되면 자동으로 트래픽 제공이 시작됩니다.

    앞에서 설정한 환경 변수를 삭제하려면 다음 안내를 따르세요.

    gcloud run services update hello-service --clear-env-vars

기본값에 대해 새 기능을 서비스의 자동화된 테스트 범위에 추가합니다.

로그에서 다른 문제 찾기

이 서비스의 로그 뷰어에 다른 문제가 표시될 수 있습니다. 예를 들어 지원되지 않는 시스템 호출은 로그에 '컨테이너 샌드박스 제한'으로 표시됩니다.

예를 들어 Node.js 서비스로 인해 다음과 같은 로그 메시지가 표시될 수 있습니다.

Container Sandbox Limitation: Unsupported syscall statx(0xffffff9c,0x3e1ba8e86d88,0x0,0xfff,0x3e1ba8e86970,0x3e1ba8e86a90). Please, refer to https://gvisor.dev/c/linux/amd64/statx for more information.

이 경우 지원 부족은 hello-service 샘플 서비스에 영향을 주지 않습니다.

삭제

이 튜토리얼에서 만든 리소스를 삭제하면 비용이 발생하지 않습니다.

튜토리얼 리소스 삭제

  1. 이 튜토리얼에서 배포한 Knative serving 서비스를 삭제합니다.

    gcloud run services delete SERVICE-NAME

    여기서 SERVICE-NAME은 선택한 서비스 이름입니다.

    Google Cloud 콘솔에서 Knative serving 서비스를 삭제할 수도 있습니다.

    Knative serving으로 이동

  2. 튜토리얼 설정 중에 추가한 gcloud 기본 구성을 삭제합니다.

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. 프로젝트 구성을 삭제합니다.

     gcloud config unset project
    
  4. 이 튜토리얼에서 만든 다른 Google Cloud 리소스를 삭제합니다.

다음 단계