Memorystore for Redis 的最佳做法

本頁面提供指引,說明如何以最佳方式使用 Memorystore for Redis。本頁面也會指出應避免的潛在問題。

如需疑難排解情境清單,請參閱「疑難排解」。

高可用性和資料耐久性最佳做法

建議使用標準層做為高可用性 (HA) 的主要機制。標準級提供具備多個副本的高可用性執行個體,並在主要執行個體發生故障時,透過自動容錯移轉功能快速復原。此外,在標準級執行個體上啟用 RDB 快照,可提供額外保護,防範節點故障。

在某些情況下,您可能也想確保可以從快照備份還原資料。在這些情況下,備份和從 RDB 快照還原資料的功能,可提供額外的資料遺失防護。

如果您啟用 RDB 快照,且需要復原資料,Memorystore for Redis 可以從最新的快照執行這項操作。這些快照僅供內部系統復原,您無法存取。

耐用性限制和最佳做法

雖然 Standard 層級透過複製和自動容錯移轉提供高可用性,但高可用性無法保證資料的完整耐久性。即使在啟用高可用性的情況下,Memorystore for Redis 也無法在某些災難性事件 (例如區域暫時性故障) 期間,提供完整的資料耐久性。

為協助保護資料並提供基本層級的持續性,使用 Google Cloud 控制台建立 Memorystore for Redis 執行個體時,系統預設會啟用 RDB 快照。

對於正式版工作負載,請考慮下列最佳做法,進一步提升資料復原能力:

  • 災難復原:Memorystore for Redis 不支援跨區域複製。如果您的災難復原策略需要跨區域複製,才能真正實現區域韌性,建議使用 Memorystore for Valkey 執行個體。如要進一步瞭解如何跨區域設定及管理次要執行個體,請參閱 Memorystore for Valkey 說明文件中的「使用跨區域複製功能」。
  • 最高持久性:如果應用程式需要最高層級的資料持久性,建議使用設定跨區域複製、每日自動備份和僅附加檔案 (AOF) 持久性的 Memorystore for Valkey 執行個體。結合這兩項功能,可提供最完善的資料遺失防護。詳情請參閱 Memorystore for Valkey 說明文件中的「持久性總覽」。

匯出 RDB

匯出 RDB 備份時,請參閱下列指南:

需要大量資源的作業

對於標準級 Redis 執行個體,下列作業會在作業期間使用額外記憶體:

由於複製作業,版本升級、調度及手動容錯移轉會使用額外記憶體 (適用於 Standard 層級執行個體)。這些作業會遵循「標準層例項升級行為」一文所述的複製程序。

由於匯入和匯出作業會產生 Redis 分支程序,並複製與這些作業相關聯的寫入時複製資料管理作業,因此需要額外記憶體。

為減輕耗用大量資源的作業所帶來的缺點,建議您採取下列措施:

需要重試連線的作業和情境

下列作業和情境會中斷網路與 Redis 執行個體之間的網路連線:

這些作業會修改執行個體,因此需要暫時中斷連線。 執行這些作業前,請務必採用指數輪詢重試邏輯,確保應用程式會自動重新連線並繼續正常運作。

例行維護

Memorystore for Redis 執行個體會定期進行維護作業。詳情請參閱 Memorystore for Redis 維護政策。

請採用下列最佳做法,為例行維護做好準備:

記憶體管理

由於開放原始碼 Redis 會發生眾所周知的記憶體片段化問題,因此記憶體管理可能是一項挑戰。建議您降低執行個體的maxmemory 設定,以便在記憶體壓力過大時提供額外資源。

如要監控 Memorystore 執行個體的記憶體壓力,最佳方式是使用「系統記憶體用量比率」指標。如需 Memorystore for Redis 記憶體管理詳細指南,請參閱記憶體管理最佳做法。

管理閒置連線

如果連線未正確終止,一段時間後,您可能會發現連線至 Memorystore 執行個體的連線數增加。這可能會對效能造成負面影響,特別是使用傳輸中資料加密時,因為系統會根據容量層級設定連線數量上限。為減輕這類情況的影響,建議使用 timeout Redis 設定參數,設定閒置用戶端連線自動終止前的秒數。

資料存取透明化控管機制資源名稱

請勿在 Memorystore for Redis 資源名稱中儲存私密資料。資源名稱是指 Memorystore for Redis 執行個體名稱,以及執行個體中繼資料,例如標記。資源名稱中儲存的資料不一定會受到 Google Cloud 資料存取透明化控管機制保護,且可能與貴機構的資料存取透明化控管機制法規遵循規定衝突。

部分無伺服器環境需要無伺服器 VPC 存取連接器

部分無伺服器環境需要無伺服器 VPC 存取連接器,才能連線至 Memorystore for Redis。如要透過上述任一環境連線,請為專案設定無伺服器 VPC 存取連接器。

網路

建議使用私人服務存取 連線模式。Memorystore for Redis 使用兩種連線模式:私人服務存取權和直接對接。私人服務存取連線模式可簡化 IP 範圍管理作業,並允許您視需要使用 Shared VPC。

建立執行個體後,即無法變更連線模式。

詳情請參閱「網路」。

監控與警示

建議您使用監控和快訊,因為這些功能會提供 Redis 執行個體記憶體用量的主要信號。您也可以藉此深入瞭解 Redis 執行個體回應傳入快取要求的效率。

建議您設定下列預設快訊:

CPU 使用率最佳做法

不當使用耗費資源的 Redis 指令會導致延遲時間長、無回應或連線發生問題。標準層級執行個體可在災難復原期間提供高可用性,並依賴主要節點與副本節點之間的非同步複製功能。如果其中一個節點的指令處理作業耗用大量資源,導致 Redis 主執行緒遭到封鎖,複製作業可能會受到影響。如果問題持續發生,且發生位置中斷,則中斷位置中最近寫入的資料可能無法在其他位置使用。

建議使用 Cloud Monitoring 為「主執行緒 CPU 秒數」(redis.googleapis.com/stats/cpu_utilization_main_thread) 指標設定快訊,確保當副本指定為唯讀副本時,主要節點的 CPU 使用率不會超過 0.8 秒,每個副本節點的 CPU 使用率不會超過 0.5 秒。

如果 Redis 執行個體超出建議值,建議您將執行個體擴展至較高的容量層級,或按照疑難排解說明操作,避免 CPU 密集型作業。

如果執行個體的 CPU 使用率偏高,或是資源耗盡 (例如連線過多),執行個體可能會發生異常行為,且外部指標可能會遺失。

耗用大量資源的指令

強烈建議您避免使用耗用大量資源的 Redis 指令。使用這些指令可能會導致下列效能問題:

  • 高延遲和用戶端逾時
  • 導致記憶體用量增加的指令造成記憶體壓力
  • 節點複製和同步處理期間發生資料遺失,因為 Redis 主執行緒遭到封鎖
  • 健康狀態檢查、可觀測性和複製作業遭到延遲

下表列出耗用大量資源的 Redis 指令示例,並提供節省資源的替代方案。

類別 耗用大量資源的指令 資源效率替代方案
針對整個鍵空間執行 KEYS SCAN
針對可變長度鍵集執行 LRANGE 限制查詢所用範圍的大小。
ZRANGE 限制查詢所用範圍的大小。
HGETALL HSCAN
SMEMBERS SSCAN
封鎖指令碼執行作業 EVAL 確認指令碼不會無限期執行。
EVALSHA 確認指令碼不會無限期執行。
移除檔案和連結 DEL UNLINK
發布及訂閱 PUBLISH SPUBLISH
SUBSCRIBE SSUBSCRIBE

Redis 用戶端最佳做法

本節提供 Redis 用戶端的最佳使用方式。

偵測及處理沒有回應的連線

我們強烈建議您設定用戶端應用程式,偵測與 Memorystore for Redis 的連線是否沒有回應。系統偵測到連線沒有回應時,用戶端必須重設連線。如要建構彈性應用程式,建議您採用下列用戶端設定:

  • 設定 TCP 存留信號參數:設定 TCP keepalive time、TCP keepalive interval 和 TCP keepalive probes 參數,讓用戶端主動偵測並捨棄沒有回應的連線,即使連線處於閒置狀態也一樣。舉例來說,如果將 TCP keepalive time 參數設為 30 秒、TCP keepalive interval 設為 10 秒,且 TCP keepalive probes 設為 3,則用戶端會在 1 分鐘內重設沒有回應的閒置連線。
  • 設定 TCP 使用者逾時:在用戶端中設定這個逾時時間,即可重設有待處理要求的連線,並停止回應。舉例來說,如果將逾時時間設為 15 秒,用戶端就會在 15 秒後重設有待處理要求的無回應連線。

客戶專屬最佳做法

如果您水平擴展應用程式,您可能會在寫入指令時遇到 READONLY錯誤。Memorystore for Redis 使用非 Sentinel 部署作業和靜態端點,且用戶端不會預先收到動態角色資訊。如果您使用單一用戶端連線連至主要端點和副本端點,用戶端可能會不小心將寫入指令傳送至唯讀副本。備用資源無法處理寫入指令,因此會傳回 READONLY 錯誤。

為避免寫入作業錯誤路由,請勿使用單一連線進行讀取和寫入作業。請改為建立下列獨立的用戶端例項,將作業分開:

  • 主要用戶端:僅連線至主要端點
  • 副本用戶端:僅連線至副本端點

以下分頁說明如何在 Go、Java、Node.js 和 Python 中設定個別範本。

Go

package main

import (
  "context"
  "fmt"

  "github.com/redis/go-redis/v9"
)

func main() {
ctx := context.Background()

  // Initialize the primary client connecting only to the primary endpoint
  primaryClient := redis.NewClient(&redis.Options{
      Addr:     "PRIMARY_HOST:6379",
      Password: "YOUR_PASSWORD",
      DB:       0,
  })
  defer primaryClient.Close()

  // Initialize the replica client connecting only to the replica endpoint
  replicaClient := redis.NewClient(&redis.Options{
      Addr:     "REPLICA_HOST:6379",
      Password: "YOUR_PASSWORD",
      DB:       0,
  })
  defer replicaClient.Close()

  // Use the primary client for all mutating commands
  err := primaryClient.Set(ctx, "example_key", "example_value", 0).Err()
  if err != nil {
      fmt.Printf("Failed to write to primary: %v\n", err)
  }

  // Use the replica client for all read-only commands
  val, err := replicaClient.Get(ctx, "example_key").Result()
  if err != nil {
      fmt.Printf("Failed to read from replica: %v\n", err)
  } else {
      fmt.Printf("Successfully read value: %s\n", val)
  }
}

Java

@Bean
public RedisConnectionFactory primaryConnectionFactory() {
  RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("PRIMARY_HOST", 6379);
  config.setPassword(RedisPassword.of("YOUR_PASSWORD"));
  return new LettuceConnectionFactory(config);
}

@Bean
public RedisConnectionFactory replicaConnectionFactory() {
  RedisStaticMasterReplicaConfiguration config =
      new RedisStaticMasterReplicaConfiguration("PRIMARY_HOST", 6379);
  config.addNode("REPLICA_HOST", 6379);
  config.setPassword(RedisPassword.of("YOUR_PASSWORD"));

  LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
      .readFrom(ReadFrom.REPLICA_PREFERRED)
      .build();

  return new LettuceConnectionFactory(config, clientConfig);
}

@Bean
public RedisTemplate<String, Object> primaryRedisTemplate(
  @Qualifier("primaryConnectionFactory") RedisConnectionFactory factory) {

  RedisTemplate<String, Object> template = new RedisTemplate<>();
  template.setConnectionFactory(factory);
  return template;
}

@Bean
public RedisTemplate<String, Object> replicaRedisTemplate(
  @Qualifier("replicaConnectionFactory") RedisConnectionFactory factory) {

  RedisTemplate<String, Object> template = new RedisTemplate<>();
  template.setConnectionFactory(factory);
  return template;
}

Node.js

import { createClient } from 'redis';

async function main() {
// Initialize the primary client connecting only to the primary endpoint
const primaryClient = createClient({
  url: 'redis://PRIMARY_HOST:6379',
  password: 'YOUR_PASSWORD'
});

// Initialize the replica client connecting only to the replica endpoint
const replicaClient = createClient({
  url: 'redis://REPLICA_HOST:6379',
  password: 'YOUR_PASSWORD'
});

primaryClient.on('error', (err) => console.error('Primary Client Error', err));
replicaClient.on('error', (err) => console.error('Replica Client Error', err));

await primaryClient.connect();
await replicaClient.connect();

// Use the primary client for all mutating commands
try {
  await primaryClient.set('example_key', 'example_value');
  console.log('Successfully wrote to primary');
} catch (err) {
  console.error('Failed to write to primary:', err);
}

// Use the replica client for all read-only commands
try {
  const val = await replicaClient.get('example_key');
  console.log(`Successfully read value: ${val}`);
} catch (err) {
  console.error('Failed to read from replica:', err);
}

await primaryClient.disconnect();
await replicaClient.disconnect();
}

main();

Python

import redis

def main():
  # Initialize the primary client connecting only to the primary endpoint
  primary_client = redis.Redis(
      host='PRIMARY_HOST',
      port=6379,
      password='YOUR_PASSWORD',
      decode_responses=True
  )

  # Initialize the replica client connecting only to the replica endpoint
  replica_client = redis.Redis(
      host='REPLICA_HOST',
      port=6379,
      password='YOUR_PASSWORD',
      decode_responses=True
  )

  # Use the primary client for all mutating commands
  try:
      primary_client.set('example_key', 'example_value')
      print('Successfully wrote to primary')
  except redis.RedisError as e:
      print(f'Failed to write to primary: {e}')

  # Use the replica client for all read-only commands
  try:
      val = replica_client.get('example_key')
      print(f'Successfully read value: {val}')
  except redis.RedisError as e:
      print(f'Failed to read from replica: {e}')

if __name__ == '__main__':
  main()