在現代軟體開發中,微服務架構已成為主流,但如何有效地除錯和確保其可靠性仍然是一大挑戰。本文將探討如何在 Kubernetes 環境下使用 kubectl 進行微服務除錯,並介紹各種可靠性策略,例如負載平衡、複製與冗餘、防禦性程式設計和測試、資料保護,以及超時、重試機制和熔斷器等進階技術。這些策略能幫助開發者構建更具容錯能力和還原力的微服務應用程式,確保服務在各種情況下都能穩定執行。

生產環境中的 Debugging

在生產環境中,debugging 可能更加具有挑戰性,因為我們需要在不影響使用者經驗的情況下進行除錯。這可以透過使用監控工具、日誌分析和 A/B 測試來實作。

內容解密:

在上面的內容中,我們介紹瞭如何 debug 微服務,從問題定位到解決。首先,我們需要檢查日誌和監控資料來定位問題。然後,我們可以使用 kubectl 命令來更新 pod 的組態或程式碼。最後,我們需要反思一下如何防止類別似的問題在未來發生。

圖表翻譯:

下面是一個簡單的流程圖,展示了 debug 微服務的步驟:

  flowchart TD
    A[問題發生] --> B[檢查日誌]
    B --> C[檢查監控資料]
    C --> D[定位問題]
    D --> E[解決問題]
    E --> F[反思和改進]

這個流程圖展示了 debug 微服務的基本步驟,從問題發生到反思和改進。

11.3 微服務除錯

在 Kubernetes 中,除錯微服務是一項重要的工作。為了確保微服務的正常執行,我們需要使用各種工具和技術來診斷和解決問題。在本文中,我們將介紹如何使用 kubectl 命令來除錯微服務。

使用 kubectl 除錯

kubectl 是 Kubernetes 的命令列工具,提供了許多功能來幫助我們除錯微服務。以下是幾個常用的 kubectl 命令:

  • kubectl get <resource-type> <resource-name>: 顯示指定資源的詳細訊息。
  • kubectl describe <resource-type> <resource-name>: 顯示指定資源的詳細訊息,包括其組態和狀態。
  • kubectl logs <pod-name>: 顯示指定 Pod 的日誌訊息。
  • kubectl exec --stdin --tty <pod-name> -- <command>: 在指定 Pod 中執行指定命令。

進入 Pod

如果我們需要進一步診斷微服務的問題,我們可以使用 kubectl exec 命令進入 Pod 中。例如:

kubectl exec --stdin --tty metadata-55bb6bdf58-7pjn2 -- bash

這個命令會開啟一個新的終端機,讓我們可以在 Pod 中執行命令。

連線埠轉發

如果我們需要從本地機器存取微服務,我們可以使用 kubectl port-forward 命令進行連線埠轉發。例如:

kubectl port-forward metadata-55bb6bdf58-7pjn2 6000:80

這個命令會將本地機器的 6000 連線埠轉發到 Pod 中的 80 連線埠。

檢查環境變數

環境變數是微服務組態的重要部分。如果我們需要檢查環境變數,我們可以使用 kubectl describe 命令。例如:

kubectl describe pod metadata-55bb6bdf58-7pjn2

這個命令會顯示 Pod 的詳細訊息,包括其環境變數。

檢查服務名稱和連線埠號

如果微服務之間的通訊出現問題,我們需要檢查服務名稱和連線埠號是否正確。例如:

kubectl get services

這個命令會顯示所有服務的詳細訊息,包括其名稱和連線埠號。

11.4 可靠性和還原力

無論如何,我們都無法避免問題的發生,但我們可以採取許多方法來處理這些問題,以維持服務的正常運作。在生產環境中,我們對應用程式有著一定的可靠性期望,並且有許多策略可以用來構建強健可靠的系統。本文概述了一些實踐和技術,以幫助我們構建容錯系統,可以快速從故障中還原。

11.4.1 練習防禦性程式設計

第一步是以防禦性程式設計的心態來編寫程式碼。當我們這樣做時,我們預計會發生錯誤,即使我們無法預料到它們會是什麼。我們應該始終預期以下情況:

  • 我們會收到程式碼的壞輸入。
  • 我們的程式碼中包含尚未出現的錯誤。
  • 我們的程式碼將來會增加錯誤。
  • 我們依賴的東西(例如 RabbitMQ)不是 100% 可靠的,偶爾會有自己的問題。 當我們採用防禦性心態時,我們會自動開始尋找方法,使程式碼在意外情況下表現得更好。容錯性從編碼級別開始。它從每個微服務開始。

11.4.2 練習防禦性測試

正如您可能知道的,測試在構建強韌可靠的系統中發揮著巨大的作用。我們在第 9 章中涵蓋了測試,因此我想在這裡說的是,測試“正常”程式碼路徑是不夠的。我們還應該測試軟體是否能夠處理錯誤。這是防禦性程式設計的下一步。我們應該練習防禦性測試,並撰寫積極攻擊我們程式碼的測試。這有助於我們找出可能需要更多關注的脆弱程式碼。我們需要確保程式碼可以優雅地還原,報告錯誤並處理意外情況。

11.4.3 保護資料

所有應用程式都處理使用者資料,我們必須採取必要的步驟來保護資料,以防發生故障。當發生意外故障時,我們需要確信最重要的資料沒有被損壞或丟失。錯誤會發生,但丟失資料不應該發生。並非所有資料都是一樣重要的。系統內生成的資料(可以重新生成)比從客戶端捕捉的資料 menos 重要。儘管所有資料都很重要,但我們必須投資最多來保護源資料。 保護資料的第一步是備份,這應該是自動化的。大多數雲供應商提供了啟用此功能的設施。 注意:別忘了練習從備份中還原!如果我們無法還原備份,則備份完全無用。

11.4.4 複製和冗餘

處理微服務故障的最佳方法是透過複製和冗餘。我們通常至少需要三個例項,每個微服務都在負載平衡器後面執行,如圖 11.12 所示。負載平衡器是一種服務,可以將傳入請求分佈在多個微服務之間,以便“負載”均勻地分佈在它們之間。

  graph LR
    A[Load Balancer] -->|Request|> B[Microservice 1]
    A -->|Request|> C[Microservice 2]
    A -->|Request|> D[Microservice 3]

圖表翻譯:

上述 Mermaid 圖表展示了一個負載平衡器如何將傳入請求分佈在多個微服務例項之間。這種方法可以確保即使一個或多個微服務例項發生故障,系統仍然可以正常運作。負載平衡器會自動將請求重新導向可用的微服務例項,從而確保高用性和容錯性。

負載平衡器的重要性

在微服務架構中,負載平衡器扮演著至關重要的角色。它可以將傳入的請求分配到多個微服務例項中,從而實作負載平衡和容錯移轉。這樣,即使某個微服務例項發生故障,負載平衡器也可以將請求重新導向到其他可用的例項,確保系統的高用性。

負載平衡器的工作原理

負載平衡器的工作原理是將傳入的請求分配到多個微服務例項中。每個傳入的請求只會被送到其中一個例項,從而避免單點故障。當某個微服務例項發生故障時,負載平衡器會立即將傳入的請求重新導向到其他可用的例項。

容錯移轉和復原

玄貓實作了微服務的冗餘機制,如果某個微服務例項發生故障,玄貓會立即重啟該例項,並將其工作負載轉移到其他可用的例項。這樣,可以維持系統的連續性和高用性。

日誌記錄和調查

雖然我們的系統可以處理故障,但並不意味著我們應該容忍故障。所有故障都應該被記錄和調查,以便找到並修復故障的原因。玄貓使用除錯過程來查詢和修復故障的原因。

內容解密:

負載平衡器的實作可以使用以下程式碼:

import random

class LoadBalancer:
    def __init__(self, instances):
        self.instances = instances

    def get_instance(self):
        available_instances = [instance for instance in self.instances if instance.is_available()]
        if available_instances:
            return random.choice(available_instances)
        else:
            return None

class Instance:
    def __init__(self, name):
        self.name = name
        self.available = True

    def is_available(self):
        return self.available

    def set_available(self, available):
        self.available = available

圖表翻譯:

下圖示範了負載平衡器的工作原理:

  flowchart TD
    A[傳入請求] --> B[負載平衡器]
    B --> C[微服務例項1]
    B --> D[微服務例項2]
    B --> E[微服務例項3]
    C --> F[處理請求]
    D --> F
    E --> F

在這個圖表中,傳入請求被送到負載平衡器,然後被分配到其中一個微服務例項。每個例項都可以處理請求,如果某個例項發生故障,負載平衡器會將請求重新導向到其他可用的例項。

實作 Kubernetes 中的複製

在我們為 FlixTube 佈署的微服務中,每個服務只有一個例項。這對於學習或開發微服務應用程式的初始階段是可以接受的,但它並不夠容錯。

幸運的是,Kubernetes 提供了一種簡單的方法來建立複製品。只需修改已經編寫的 Kubernetes 佈署 YAML 程式碼中的欄位值即可實作。這是基礎設施即程式碼的強大功能之一。

修改複製數量

我們可以輕鬆地透過修改 YAML 程式碼中的 replicas 欄位來更改複製數量。以下是對 gateway 微服務的 YAML 程式碼進行修改的示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      #...

在這個例子中,我們將 replicas 欄位的值從 1 改為 3。這意味著當我們應用這個變化時,Kubernetes 將為 gateway 微服務建立 3 個複製品。

實作容錯隔離和優雅退化

微服務的一個主要優點是它們可以實作容錯隔離。這意味著當一個微服務出現問題時,它不會影響其他微服務。然而,我們需要小心地實作這種隔離,以確保問題不會傳播到整個應用程式中。

為了實作容錯隔離,我們可以使用超時、重試、斷路器和 bulkhead 等機制。以下是對這些機制的簡要介紹:

  • 超時: 設定超時時間,以防止請求無限等待。
  • 重試: 對失敗的請求進行重試,以確保請求成功。
  • 斷路器: 當服務不可用時,斷路器可以防止請求被送到該服務。
  • bulkhead: bulkhead 是一種設計模式,用於隔離故障,以防止它們傳播到整個應用程式中。

實作容錯隔離的示例

假設我們有一個 video-upload 微服務,它出現了問題,無法正常工作。在這種情況下,我們需要確保問題不會傳播到整個應用程式中。

以下是實作容錯隔離的示例:

  graph LR
    A[Gateway] -->|請求|> B[Video Upload]
    B -->|錯誤|> A
    A -->|錯誤|> C[使用者]

在這個例子中,當 video-upload 微服務出現問題時,Gateway 會攔截錯誤,並傳回錯誤訊息給使用者。這樣,可以防止問題傳播到整個應用程式中。

圖表翻譯:

上述 Mermaid 圖表展示了當 video-upload 微服務出現問題時,Gateway 如何攔截錯誤,並傳回錯誤訊息給使用者。這是實作容錯隔離的一種方式,可以防止問題傳播到整個應用程式中。

內容解密:

在上述 YAML 程式碼中,replicas 欄位的值被設定為 3,這意味著 Kubernetes 將為 gateway 微服務建立 3 個複製品。這可以提高應用程式的可用性和容錯性。

圖表翻譯:

上述 Mermaid 圖表展示了當 video-upload 微服務出現問題時,Gateway 如何攔截錯誤,並傳回錯誤訊息給使用者。這是實作容錯隔離的一種方式,可以防止問題傳播到整個應用程式中。

設定Axios的超時時間

根據Axios的檔案,預設的超時時間是無限大!這意味著,Axios的請求可以永遠不會被中止。因此,我們需要為Axios的請求設定超時時間。

設定預設超時時間

我們可以設定預設的超時時間為所有請求。這樣就不需要為每個請求重複設定超時時間。

const axios = require("axios");
axios.defaults.timeout = 2500; // 設定預設超時時間為2.5秒

重試機制

我們知道,HTTP請求有時會失敗。由於我們不能控制外部服務,也不能看到他們的程式碼,所以很難確定他們的可靠性。即使是最可靠的服務,也可能會有間歇性的失敗。

一個簡單的解決方案是重試操作多次,希望它能夠在後續的嘗試中成功。這種情況如圖11.14所示。在這個例子中,FlixTube的影片儲存微服務請求從Azure Storage中檢索影片。有時,這些請求會因為間歇性的連線錯誤而失敗。在圖11.14中,兩個連續的請求都因為間歇性的連線錯誤而失敗,但第三個請求成功了。

假設網路是可靠的,是分散式計算中的謬誤之一,我們必須採取措施來減輕請求失敗的影響。在JavaScript中實作重試機制並不困難。在清單11.4中,您可以看到我在多個專案中使用的一個重試函式的實作。重試函式包裹其他非同步操作,例如HTTP請求,以便它們可以被多次嘗試。

清單11.3:設定Axios的預設超時時間

設定預設超時時間為2,500毫秒或2.5秒

圖11.14:重試機制

Azure Storage

影片儲存

第一次請求

第三次請求

初始請求因為間歇性的連線問題而失敗。 最後,請求成功了。

時間

第二次請求

外部檔案儲存:

我們無法控制

這個服務的可用性。

這個微服務正在

向外部服務傳送請求。

實作重試函式

function retry(fn, retries = 3, delay = 500) {
  return new Promise((resolve, reject) => {
    fn()
     .then(resolve)
     .catch((err) => {
        if (retries > 0) {
          setTimeout(() => {
            retry(fn, retries - 1, delay).then(resolve).catch(reject);
          }, delay);
        } else {
          reject(err);
        }
      });
  });
}

這個重試函式可以包裹其他非同步操作,例如HTTP請求,以便它們可以被多次嘗試。

重試機制:確保微服務的健全性

在微服務架構中,各個服務之間的通訊可能會因為網路問題、伺服器過載或其他臨時性錯誤而失敗。為了確保系統的健全性和可靠性,實作重試機制是非常重要的。這個機制允許系統在遇到錯誤時自動重試操作,直到成功或達到最大重試次數。

重試函式的實作

以下是JavaScript中的一個重試函式實作範例。這個函式接受三個引數:要重試的操作、最大重試次數和每次重試之間的等待時間。

async function sleep(timeMS) {
  return new Promise((resolve, reject) => {
    setTimeout(() => { resolve(); }, timeMS);
  });
}

async function retry(operation, maxAttempts, waitTimeMS) {
  let lastError;
  while (maxAttempts-- > 0) {
    try {
      const result = await operation();
      return result;
    } catch (err) {
      lastError = err;
      if (maxAttempts >= 1) {
        await sleep(waitTimeMS);
      }
    }
  }
  throw lastError;
}

使用重試函式

要使用這個重試函式,可以將它包裝在一個HTTP請求函式中。例如,以下程式碼展示瞭如何對一個HTTP GET請求進行重試:

const fetchWithRetry = async (url, maxAttempts, waitTimeMS) => {
  const operation = async () => {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  };
  return retry(operation, maxAttempts, waitTimeMS);
};

// 範例用法
fetchWithRetry('https://example.com/api/data', 3, 5000)
 .then(data => console.log(data))
 .catch(error => console.error('Failed to fetch data:', error));

重試機制的優點

  1. 提高系統可靠性:透過自動重試失敗的操作,可以提高系統的整體可靠性和可用性。
  2. 減少手動干預:自動重試機制可以減少人工干預的需要,從而節省時間和資源。
  3. 增強使用者經驗:快速重試和還原可以提供更好的使用者經驗,因為使用者不需要等待很長時間或者手動重新整理頁面。

非同步操作中的暫停與重試機制

在進行非同步操作時,瞭解如何暫停執行和重試失敗的操作是非常重要的。這使我們能夠更好地控制程式的流程,確保應用程式的穩定性和可靠性。

暫停機制

暫停機制允許我們在特定的時間間隔後執行某個動作。這通常是透過使用計時器或延遲函式來實作的。例如,在JavaScript中,我們可以使用setTimeout函式來設定一個延遲,當延遲時間到期後,就會執行指定的回呼函式。

function pause(duration) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, duration);
  });
}

在上面的例子中,pause函式傳回一個Promise,當延遲時間到期後,這個Promise就會被解決。這使我們能夠使用await關鍵字來暫停程式的執行,直到延遲時間到期。

重試機制

重試機制則允許我們對於失敗的非同步操作進行多次嘗試,直到操作成功為止。這通常是透過使用迴圈和條件判斷來實作的。

function retry(operation, maxAttempts) {
  let attempts = 0;
  while (attempts < maxAttempts) {
    try {
      return await operation();
    } catch (error) {
      attempts++;
      if (attempts >= maxAttempts) {
        throw error;
      }
    }
  }
}

在上面的例子中,retry函式接受一個非同步操作和最大嘗試次數為引數。它會對於這個操作進行多次嘗試,直到操作成功或達到最大嘗試次數。如果所有嘗試都失敗了,就會丟擲最後一次錯誤。

結合暫停和重試

我們可以結合暫停和重試機制來實作更強大的非同步操作控制。例如,我們可以在每次重試之前增加一個暫停,以避免過度的重試嘗試。

async function retryWithPause(operation, maxAttempts, pauseDuration) {
  let attempts = 0;
  while (attempts < maxAttempts) {
    try {
      return await operation();
    } catch (error) {
      attempts++;
      if (attempts >= maxAttempts) {
        throw error;
      }
      await pause(pauseDuration);
    }
  }
}

在上面的例子中,retryWithPause函式結合了重試和暫停機制。它會對於指定的非同步操作進行多次嘗試,如果操作失敗了,就會暫停一段時間後再次嘗試,直到操作成功或達到最大嘗試次數。

圖表翻譯:

  flowchart TD
    A[開始] --> B[執行操作]
    B --> C[操作成功]
    C --> D[結束]
    B --> E[操作失敗]
    E --> F[暫停]
    F --> G[重試]
    G --> B
    E --> H[達到最大嘗試次數]
    H --> I[丟擲錯誤]
    I --> D

這個流程圖描述了結合暫停和重試機制的非同步操作流程。當操作失敗時,程式會暫停一段時間後再次嘗試,直到操作成功或達到最大嘗試次數。

11.4.7 進階容錯技術

為了進一步提高應用程式的可靠性和還原力,還有許多更先進的技術可以使用。這些技術可以幫助應用程式更好地應對故障和錯誤。

工作佇列(Job Queue)

工作佇列是一種微服務架構中常用的元件,與 RabbitMQ 中的訊息佇列不同,它是一個更高層次的抽象。工作佇列用於管理計算密集型任務,例如影片轉碼、圖片處理等。

在 FlixTube 的例子中,當使用者上傳影片時,可以將生成縮圖或轉碼影片為較低解析度的任務增加到工作佇列中。這樣可以在不阻塞主執行緒的情況下進行計算密集型任務。

工作佇列的優點是可以記錄任務序列到資料函式庫中,使得應用程式可以在故障後還原任務處理。另外,工作佇列還可以控制任務執行的效能,例如將任務分散到較長的時間間隔內執行,或是在非高峰時段執行任務。

熔斷器(Circuit Breaker)

熔斷器是一種更先進的超時機制,它可以人工智慧地判斷當前系統是否出現問題,並根據情況決定是否允許請求透過。

在正常情況下,熔斷器的狀態為 “On”,允許 HTTP 請求透過。但如果請求失敗,熔斷器會切換到 “Off” 狀態,並立即拒絕新請求。

熔斷器的工作原理是當它發現上游系統出現故障時,就不會再向其傳送請求,而是直接傳回錯誤。這樣可以避免因為上游系統故障導致的連鎖故障。

實作重試機制

以下是實作重試機制的一個簡單示例:

const maxAttempts = 3;
const retryDelay = 5;

async function retryOperation(operation) {
  let attempts = 0;
  while (attempts < maxAttempts) {
    try {
      const result = await operation();
      return result;
    } catch (error) {
      attempts++;
      if (attempts >= maxAttempts) {
        throw error;
      }
      await new Promise(resolve => setTimeout(resolve, retryDelay));
    }
  }
}

這個示例實作了一個簡單的重試機制,當操作失敗時會等待一段時間後再次嘗試,直到最大嘗試次數為止。

內容解密:

上述程式碼實作了一個簡單的重試機制,當操作失敗時會等待一段時間後再次嘗試,直到最大嘗試次數為止。這個機制可以用於任何可能失敗的操作,例如網路請求或資料函式庫查詢。

圖表翻譯:

以下是工作佇列和熔斷器的工作原理圖表:

  flowchart TD
    A[工作佇列] --> B[任務增加]
    B --> C[任務執行]
    C --> D[任務完成]
    D --> E[結果傳回]
    
    F[熔斷器] --> G[請求接收]
    G --> H[請求處理]
    H --> I[請求完成]
    I --> J[結果傳回]

這個圖表展示了工作佇列和熔斷器的工作原理,包括任務增加、執行、完成和結果傳回。

微服務的工作原理和可靠性技術

在微服務架構中,各個服務之間的溝通和工作流程是非常重要的。為了確保系統的可靠性和效率,需要使用一些特定的技術和設計模式。

從系統架構的視角來看,建構可靠的微服務應用程式需要多層次的防禦機制。本文深入探討了從程式碼層面的防禦性程式設計和測試,到系統層面的複製、負載平衡、重試機制、熔斷器以及工作佇列等一系列關鍵技術。分析顯示,雖然複製和負載平衡能提升系統的可用性,但更關鍵的是如何優雅地處理錯誤和故障。透過重試機制和熔斷器模式,可以有效隔離故障,避免單點失效導致的雪崩效應。此外,工作佇列的應用則能提升系統處理非同步任務的效率和可靠性,尤其對於計算密集型任務,能有效降低系統負載並提升使用者經驗。展望未來,隨著Serverless技術的興起,微服務的佈署和管理將更加便捷,而容錯機制也將更加精細化和自動化。玄貓認為,對於追求高可靠性和高用性的線上應用,深入理解並實踐這些容錯技術至關重要,才能在複雜的生產環境中保持穩定執行。