教學課程:本機 Cloud Run 服務疑難排解


本教學課程說明服務開發人員如何使用 Google Cloud Observability 工具來發現問題,並透過本機開發工作流程進行調查,以排解 Cloud Run 服務故障問題。

這份逐步操作的「個案研究」是疑難排解指南的輔助資料,其中使用了會在部署時導致執行階段錯誤的範例專案,您可以排解問題來找出並修正問題。

目標

  • 撰寫、建構及部署至 Cloud Run 的服務
  • 使用錯誤回報和 Cloud Logging 找出錯誤
  • 從 Container Registry 擷取容器映像檔,進行根本原因分析
  • 修正「正式版」服務,然後改善服務,以減輕日後的問題

費用

在本文件中,您會使用 Google Cloud的下列計費元件:

您可以使用 Pricing Calculator 根據預測用量產生預估費用。 新 Google Cloud 使用者可能符合申請免費試用的資格。

事前準備

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  5. Make sure that billing is enabled for your Google Cloud project.

  6. 啟用 Cloud Run Admin API
  7. 安裝並初始化 gcloud CLI
  8. 更新元件:
    gcloud components update
  9. 按照操作說明在本機安裝 Docker
  10. 必要的角色

    如要取得完成本教學課程所需的權限,請要求管理員為您授予專案的下列 IAM 角色:

    如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和機構的存取權」。

    您或許還可透過自訂角色或其他預先定義的角色取得必要權限。

設定 gcloud 預設值

如要針對 Cloud Run 服務設定 gcloud 的預設值:

  1. 設定您的預設專案:

    gcloud config set project PROJECT_ID

    PROJECT_ID 改為您為本教學課程建立的專案名稱。

  2. 為所選地區設定 gcloud:

    gcloud config set run/region REGION

    REGION 改為您所選擇的支援 Cloud Run 地區

Cloud Run 位置

Cloud Run 具有「地區性」,這表示執行 Cloud Run 服務的基礎架構位於特定地區,並由 Google 代管,可為該地區內所有區域提供備援功能。

選擇 Cloud Run 服務的執行地區時,請將延遲時間、可用性或耐用性需求做為主要考量。一般而言,您可以選擇最靠近使用者的地區,但您應考量 Cloud Run 服務所使用的其他 Google Cloud產品位置。使用分散在不同位置的 Google Cloud 產品,可能會影響服務的延遲時間和費用。

Cloud Run 可在下列地區使用:

採用級別 1 定價

採用級別 2 定價

如果您已建立 Cloud Run 服務,即可在 Google Cloud 控制台的 Cloud Run 資訊主頁中查看地區。

彙整程式碼

逐步建構新的 Cloud Run 問候服務。提醒您,這項服務會刻意為排解問題練習建立執行階段錯誤。

  1. 建立新專案:

    Node.js

    定義服務套件、初始依附元件和一些常見作業,建立 Node.js 專案。

    1. 建立新的 hello-service 目錄:

      mkdir hello-service
      cd hello-service
      
    2. 產生 package.json 檔案,建立新的 Node.js 專案:

      npm init --yes
      npm install express@4
      
    3. 在編輯器中開啟新的 package.json 檔案,並設定 start 指令碼來執行 node index.js。完成後,檔案會如下所示:

      {
        "name": "hello-broken",
        "description": "Broken Cloud Run service for troubleshooting practice",
        "version": "1.0.0",
        "private": true,
        "main": "index.js",
        "scripts": {
          "start": "node index.js",
          "test": "echo \"Error: no test specified\" && exit 0",
          "system-test": "NAME=Cloud c8 mocha -p -j 2 test/system.test.js --timeout=360000 --exit"
        },
        "engines": {
          "node": ">=16.0.0"
        },
        "author": "Google LLC",
        "license": "Apache-2.0",
        "dependencies": {
          "express": "^4.17.1"
        },
        "devDependencies": {
          "c8": "^10.0.0",
          "google-auth-library": "^9.0.0",
          "got": "^11.5.0",
          "mocha": "^10.0.0"
        }
      }
      

    如果您在完成本教學課程後,仍想繼續改良這項服務,建議您填入說明、作者資訊,並評估授權。詳情請參閱 package.json 說明文件

    Python

    1. 建立新的 hello-service 目錄:

      mkdir hello-service
      cd hello-service
      
    2. 建立 requirements.txt 檔案,並將依附元件複製到其中:

      Flask==3.0.3
      pytest==8.2.0; python_version > "3.0"
      # pin pytest to 4.6.11 for Python2.
      pytest==4.6.11; python_version < "3.0"
      gunicorn==23.0.0
      Werkzeug==3.0.3
      

    Go

    1. 建立新的 hello-service 目錄:

      mkdir hello-service
      cd hello-service
      
    2. 初始化新的 go 模組,建立 Go 專案:

      go mod init example.com/hello-service
      

    您可以視需要更新特定名稱:如果程式碼已發布至可透過網際網路存取的程式碼存放區,則應更新名稱。

    Java

    1. 建立新的 Maven 專案:

      mvn archetype:generate \
        -DgroupId=com.example.cloudrun \
        -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)
    }
    

    Java

    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:20-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 dependencies.
    # if you need a deterministic and repeatable build create a
    # package-lock.json file and use npm ci:
    # RUN npm ci --omit=dev
    # if you need to include development dependencies during development
    # of your application, use:
    # RUN npm install --dev
    
    RUN npm install --omit=dev
    
    # 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 official Go image to create a binary.
    # This is based on Debian and sets the GOPATH to /go.
    # https://hub.docker.com/_/golang
    FROM golang:1.23-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"]
    

    Java

    這個範例使用 Jib 建構 Docker 映像檔,並使用常見的 Java 工具。Jib 可在不需要 Dockerfile 或安裝 Docker 的情況下,最佳化容器建構作業。進一步瞭解如何使用 Jib 建構 Java 容器

    <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,然後將容器映像檔部署到 Cloud Run。

如要推送程式碼:

  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、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 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、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 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、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔儲存在 Container Registry 中,日後如有需要,可以重複使用。

    Java

    1. 使用 gcloud 憑證輔助程式授權 Docker 推送至 Container Registry。
      gcloud auth configure-docker
    2. 使用 Jib Maven 外掛程式建構容器,並將容器推送至 Container Registry。
      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 是容器映像檔名稱和 Cloud Run 服務名稱。請注意,容器映像檔是部署到您之前在「設定 gcloud」中設定的服務和地區。

    在出現「allow unauthenticated」(允許未經驗證) 提示時,回覆 y 或「是」。如要進一步瞭解以 IAM 為基礎的驗證,請參閱「管理存取權」一文。

    請等待部署完成,這可能需要半分鐘的時間。成功完成後,指令列會顯示服務網址。

立即體驗

請試用服務,確認您已成功部署服務。要求應以 HTTP 500 或 503 錯誤失敗 (5xx 伺服器錯誤類別的成員)。本教學課程將逐步說明如何排解這項錯誤回應問題。

系統會自動指派可瀏覽的網址給這項服務。

  1. 使用網路瀏覽器前往以下網址:

    1. 開啟網路瀏覽器

    2. 找出先前部署指令的輸出內容中所列的服務網址。

      如果部署指令未提供網址,則表示發生錯誤。查看錯誤訊息並採取相應行動:如果沒有可操作的指引,請參閱疑難排解指南,並嘗試重新執行部署指令。

    3. 複製網址並貼到瀏覽器的網址列中,然後按下 Enter 鍵,即可前往該網址。

  2. 查看 HTTP 500 或 HTTP 503 錯誤。

    如果您收到 HTTP 403 錯誤,表示您可能已在部署提示中拒絕 allow unauthenticated invocations。如要修正這個問題,請授予服務未經驗證的存取權:

    gcloud run services add-iam-policy-binding hello-service \
      --member="allUsers" \
      --role="roles/run.invoker"
    

詳情請參閱「允許公開 (未經驗證) 存取權」。

調查問題

請注意,上方「試用」一節中發生的 HTTP 5xx 錯誤,是實際執行時發生的錯誤。本教學課程將逐步說明處理這類問題的正式程序。雖然實際工作環境的錯誤解決程序差異很大,但本教學課程會提供特定的步驟順序,說明如何應用實用的工具和技巧。

如要調查這個問題,您需要完成以下階段:

  • 收集更多有關回報錯誤的詳細資料,以便進一步調查並設定緩解策略。
  • 決定是否要推送修復版本,或回復至已知正常運作的版本,以減輕對使用者的影響。
  • 重現錯誤,確認已收集正確的詳細資料,且錯誤並非一次性的故障
  • 針對錯誤執行根本原因分析,找出造成這項錯誤的程式碼、設定或程序

在調查開始時,您會收到網址、時間戳記和「Internal Server Error」訊息。

收集更多詳細資料

收集更多有關問題的資訊,瞭解發生的情況並決定後續步驟。

使用 Google Cloud Observability 工具收集更多詳細資料:

  1. 使用錯誤回報控制台,這個控制台提供資訊主頁,可針對有已辨識堆疊追蹤的錯誤提供詳細資料和重複發生情形追蹤。

    前往「錯誤回報」控制台

    錯誤清單的螢幕截圖,包括「Resolution Status」、「Occurrences」、「Error」和「Seen in」欄。
    已記錄的錯誤清單。系統會依修訂版本、服務和平台,將錯誤訊息分門別類。
  2. 按一下錯誤即可查看堆疊追蹤詳細資料,並注意在錯誤發生前所做的函式呼叫。

    單一剖析堆疊追蹤的螢幕截圖,展示這項錯誤的常見設定檔。
    錯誤詳細資料頁面中的「堆疊追蹤範例」會顯示單一錯誤例項。您可以查看個別事件。
  3. 使用 Cloud Logging 查看導致問題的作業順序,包括因缺少已辨識的錯誤堆疊追蹤而未包含在 Error Reporting 主控台中的錯誤訊息:

    前往 Cloud Logging 主控台

    在第一個下拉式方塊中,依序選取「Cloud Run 修訂版本」>「hello-service」。這會篩選出由服務產生的記錄項目。

進一步瞭解如何在 Cloud Run 中查看記錄檔

復原至正常版本

如果這是已知可運作的既有服務,Cloud Run 上會有該服務的先前修訂版本。本教學課程使用的是沒有舊版的全新服務,因此您無法進行回溯。

不過,如果您有可回溯至先前版本的服務,請按照「查看修訂版本詳細資料」中的步驟,擷取建立服務新工作部署作業所需的容器名稱和設定詳細資料。

重現錯誤

根據先前取得的詳細資料,確認問題在測試條件下持續發生。

再次測試相同的 HTTP 要求,看看是否會回報相同的錯誤和詳細資料。錯誤詳細資料可能需要一段時間才會顯示。

由於本教學課程中的範例服務為唯讀,且不會觸發任何複雜的副作用,因此在實際工作環境中重現錯誤是安全的。不過,許多實際服務並非如此:您可能需要在測試環境中重現錯誤,或將此步驟限制為本機調查。

重現錯誤可為後續工作建立背景資訊。舉例來說,如果開發人員無法重現錯誤,可能需要對服務進行額外的檢測,才能進一步調查。

執行根本原因分析

根本原因分析是有效疑難排解的重要步驟,可確保您修正的是問題,而非症狀。

在本教學課程的先前步驟中,您已在 Cloud Run 上重現問題,確認服務在 Cloud Run 上代管時,問題仍會發生。現在請在本機重現問題,判斷問題是否只發生在程式碼中,或是只發生在實際工作代管服務中。

  1. 如果您尚未在本機使用 Docker CLI 搭配 Container Registry,請使用 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. 在本機上執行應用程式,確認問題並非 Cloud Run 特有:

    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 是容器映像檔 tag,這是容器映像檔 sha256 雜湊 ID 的易讀標籤。如果本機沒有可用的映像檔,Docker 會嘗試從遠端登錄檔擷取映像檔。

    在瀏覽器中開啟 http://localhost:9000。查看終端機輸出內容,找出與 {ops_name} 相符的錯誤訊息。

    如果無法在本機重現問題,則可能是 Cloud Run 環境特有的情況。如要瞭解應調查的具體領域,請參閱 Cloud Run 疑難排解指南

    在這種情況下,本機會重現錯誤。

既然已確認錯誤持續發生,且是由服務程式碼而非代管平台造成,現在是時候仔細調查程式碼了。

為了本教學課程的目的,我們可以假設容器內的程式碼與本機系統中的程式碼相同。

請再次查看錯誤報表的堆疊追蹤,並與程式碼交叉參照,找出錯誤的特定行。

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")
}

Java

在記錄中顯示的堆疊追蹤中,找出錯誤訊息來源所在的 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!」

  • 修改執行中的 Cloud Run 服務環境,加入這個變數:

    1. 執行服務更新指令來新增環境變數:

      gcloud run services update hello-service \
        --set-env-vars NAME=Override
      
    2. 請稍候幾秒,Cloud Run 會根據先前的修訂版本建立新修訂版本,並新增新的環境變數。

  • 確認服務現已修正:

    1. 在瀏覽器中前往 Cloud Run 服務網址。
    2. 請查看頁面上顯示的「Hello Override!」。
    3. 確認 Cloud Logging 或 Error Reporting 中沒有顯示任何非預期的訊息或錯誤。

改善日後疑難排解的速度

在這個實際工作環境問題中,錯誤與作業設定有關。我們已進行程式碼變更,以便在日後盡量減少此問題的影響。

  • 改善錯誤記錄,加入更多具體詳細資料。
  • 請讓服務改為使用安全的預設值,而非傳回錯誤。如果使用預設值代表變更為正常功能,請使用警告訊息進行監控。

讓我們逐步移除 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")
    }

    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";
    }

  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)
    }

    Java

    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 .

    Java

    mvn compile jib:build

    確認 NAME 環境變數仍可正常運作:

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

    確認服務在沒有 NAME 變數的情況下能正常運作:

    PORT=8080 && docker run --rm -e PORT=$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 範例服務。

Terraform 疑難排解

如有 Terraform 相關疑難排解問題,請參閱「Terraform 政策驗證疑難排解」或洽詢 Terraform 支援團隊

清除所用資源

如果您是為了這個教學課程建立新專案,請刪除專案。如果您使用現有專案,且希望保留該專案而不採用本教學課程中的變更,請刪除為教學課程建立的資源

刪除專案

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

如要刪除專案:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

刪除教學課程資源

  1. 刪除您在本教學課程中部署的 Cloud Run 服務:

    gcloud run services delete SERVICE-NAME

    其中 SERVICE-NAME 是您選擇的服務名稱。

    您也可以從 Google Cloud 控制台刪除 Cloud Run 服務。

  2. 移除您在教學課程設定期間新增的 gcloud 預設區域設定:

     gcloud config unset run/region
    
  3. 移除專案設定:

     gcloud config unset project
    
  4. 刪除本教學課程中建立的其他 Google Cloud 資源:

後續步驟