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


本教學課程說明服務開發人員如何使用 Google Cloud Observability 工具進行探索,以及使用本機開發工作流程進行調查,排解 Cloud Run 服務中斷的問題。

這份逐步「案例研究」是疑難排解指南的輔助文件,會使用範例專案,說明如何排解部署時發生的執行階段錯誤,找出並修正問題。

目標

  • 撰寫、建構服務,並將服務部署到 Cloud Run
  • 使用 Error Reporting 和 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. Verify 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. Verify 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 檔案,並設定要執行 node index.jsstart 指令碼。完成後,檔案會如下所示:

      {
        "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,透過常見的 Java 工具建構 Docker 映像檔。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. 使用 Error Reporting 控制台,這個控制台會提供詳細資料和資訊主頁,並追蹤含有可辨識堆疊追蹤的錯誤是否重複發生。

    前往 Error Reporting 控制台

    錯誤清單的螢幕截圖,包括「解決狀態」、「發生次數」、「錯誤」和「顯示於」欄。
    記錄的錯誤清單。系統會依據修訂版本、服務和平台,將錯誤依訊息分組。
  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 標記會將容器發布為服務,並在通訊埠 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

將預設值的新功能新增至服務的自動化測試涵蓋範圍。

在記錄中尋找其他問題

您可能會在這項服務的記錄檢視器中看到其他問題。舉例來說,記錄檔會將不支援的系統呼叫顯示為「Container Sandbox Limitation」。

舉例來說,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 資源:

後續步驟