從 Google Kubernetes Engine 叢集連線至 Redis 執行個體



gcloud container clusters describe

您只能從與 Redis 執行個體使用相同授權網路的 Google Kubernetes Engine 叢集連線至 Redis 執行個體。

設定

如果您已安裝 Google Cloud CLI 並建立 Redis 執行個體,可以略過這些步驟。

  1. 安裝 gcloud CLI 並初始化:

    gcloud init
    
  2. 按照快速入門指南中的說明建立 Redis 執行個體。請記下 Redis 執行個體的區域、IP 位址和通訊埠。

準備 GKE 叢集

  1. 如果您尚未建立 GKE 叢集,請使用下列 Google Cloud CLI 適用的指令建立一個叢集:

    1. gcloud 中,指定這個應用程式範例的專案。

      gcloud config set project PROJECT_ID

    2. gcloud 中設定 Compute Engine 區域設定變數。

      gcloud config set compute/zone ZONE

    3. 建立名為 visitcount-cluster 的 GKE 叢集。

      gcloud container clusters create visitcount-cluster --num-nodes=3 --enable-ip-alias

  2. 如果不是使用 gcloud 建立叢集,請使用下列指令擷取叢集憑證:

    gcloud container clusters get-credentials CLUSTER_NAME --zone CLUSTER_ZONE --project PROJECT_ID
    1. CLUSTER_NAME 是 GKE 叢集的名稱。
    2. CLUSTER_ZONE 是叢集所在的區域。
    3. PROJECT_ID 是叢集和 Redis 執行個體所在的專案。
  3. 如果您的叢集為 1.8 以上版本,並且已啟用 IP 別名,請略過這個步驟。如果您的叢集為 1.7 以下版本,或是您的叢集為 1.8 以上版本,但未啟用 IP 別名,在嘗試連線到執行個體之前,請先按照這些解決方法的步驟操作。

    1. 執行這些指令,並將 RESERVED_IP_RANGE 取代為執行個體的保留 IP 範圍:

      git clone https://github.com/bowei/k8s-custom-iptables.git
      cd k8s-custom-iptables/
      TARGETS="RESERVED_IP_RANGE" ./install.sh
      cd ..
      
    2. 如果您不知道執行個體的保留 IP 範圍,請使用主控台 (進階選項) 或下列指令查看:

      gcloud redis instances describe INSTANCE_ID --region=REGION
      

    如要進一步瞭解 IP 別名,包含如何建立啟用這項設定的叢集,請參閱 IP 別名說明文件

應用程式範例

本 HTTP 伺服器應用程式範例會從 Google Kubernetes Engine 叢集建立與 Redis 執行個體的連線。

複製所選程式設計語言的存放區,然後前往包含程式碼範例的資料夾:

Go

git clone https://github.com/GoogleCloudPlatform/golang-samples
cd golang-samples/memorystore/redis

Java

git clone https://github.com/GoogleCloudPlatform/java-docs-samples
cd java-docs-samples/memorystore/redis

Node.js

git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples
cd nodejs-docs-samples/memorystore/redis

Python

git clone https://github.com/GoogleCloudPlatform/python-docs-samples
cd python-docs-samples/memorystore/redis

這個範例應用程式會在每次存取 / 端點時,遞增 Redis 計數器。

Go

這個應用程式會使用 github.com/gomodule/redigo/redis 用戶端。執行下列指令即可安裝:

go get github.com/gomodule/redigo/redis@latest

應用程式範例:


// Package main is a basic app that connects to a managed Redis instance.
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gomodule/redigo/redis"
)

var redisPool *redis.Pool

func incrementHandler(w http.ResponseWriter, r *http.Request) {
	conn := redisPool.Get()
	defer conn.Close()

	counter, err := redis.Int(conn.Do("INCR", "visits"))
	if err != nil {
		http.Error(w, "Error incrementing visitor counter", http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "Visitor number: %d", counter)
}

func main() {
	redisHost := os.Getenv("REDISHOST")
	redisPort := os.Getenv("REDISPORT")
	redisAddr := fmt.Sprintf("%s:%s", redisHost, redisPort)

	const maxConnections = 10
	redisPool = &redis.Pool{
		MaxIdle: maxConnections,
		Dial:    func() (redis.Conn, error) { return redis.Dial("tcp", redisAddr) },
	}

	http.HandleFunc("/", incrementHandler)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

Java

本應用程式以 Jetty 3.1 Servlet 為基礎。

此方法使用 Jedis 程式庫:

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>5.1.0</version>
</dependency>

AppServletContextListener 類別用於建立長期有效的 Redis 連線集區:


package com.example.redis;

import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@WebListener
public class AppServletContextListener implements ServletContextListener {

  private Properties config = new Properties();

  private JedisPool createJedisPool() throws IOException {
    String host;
    Integer port;
    config.load(
        Thread.currentThread()
            .getContextClassLoader()
            .getResourceAsStream("application.properties"));
    host = config.getProperty("redis.host");
    port = Integer.valueOf(config.getProperty("redis.port", "6379"));

    JedisPoolConfig poolConfig = new JedisPoolConfig();
    // Default : 8, consider how many concurrent connections into Redis you will need under load
    poolConfig.setMaxTotal(128);

    return new JedisPool(poolConfig, host, port);
  }

  @Override
  public void contextDestroyed(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool != null) {
      jedisPool.destroy();
      event.getServletContext().setAttribute("jedisPool", null);
    }
  }

  // Run this before web application is started
  @Override
  public void contextInitialized(ServletContextEvent event) {
    JedisPool jedisPool = (JedisPool) event.getServletContext().getAttribute("jedisPool");
    if (jedisPool == null) {
      try {
        jedisPool = createJedisPool();
        event.getServletContext().setAttribute("jedisPool", jedisPool);
      } catch (IOException e) {
        // handle exception
      }
    }
  }
}

VisitCounterServlet 類別是會遞增 Redis 計數器的網頁 Servlet:


package com.example.redis;

import java.io.IOException;
import java.net.SocketException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@WebServlet(name = "Track visits", value = "")
public class VisitCounterServlet extends HttpServlet {

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    try {
      JedisPool jedisPool = (JedisPool) req.getServletContext().getAttribute("jedisPool");

      if (jedisPool == null) {
        throw new SocketException("Error connecting to Jedis pool");
      }
      Long visits;

      try (Jedis jedis = jedisPool.getResource()) {
        visits = jedis.incr("visits");
      }

      resp.setStatus(HttpServletResponse.SC_OK);
      resp.getWriter().println("Visitor counter: " + String.valueOf(visits));
    } catch (Exception e) {
      resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }
}

Node.js

這個應用程式會使用 redis 模組。以下是 package.json 檔案範例:

{
  "name": "memorystore-redis",
  "description": "An example of using Memorystore(Redis) with Node.js",
  "version": "0.0.1",
  "private": true,
  "license": "Apache Version 2.0",
  "author": "Google Inc.",
  "engines": {
    "node": ">=16.0.0"
  },
  "dependencies": {
    "redis": "^4.0.0"
  }
}

應用程式程式碼範例:

'use strict';
const http = require('http');
const redis = require('redis');

const REDISHOST = process.env.REDISHOST || 'localhost';
const REDISPORT = process.env.REDISPORT || 6379;

const client = redis.createClient({
  url: `redis://${REDISHOST}:${REDISPORT}`,
});

client.on('error', err => console.error('ERR:REDIS:', err));

client
  .connect()
  .then(() => {
    console.log('Connected to Redis');
    http
      .createServer(async (req, res) => {
        try {
          const reply = await client.incr('visits');
          res.writeHead(200, {'Content-Type': 'text/plain'});
          res.end(`Visitor number: ${reply}\n`);
        } catch (err) {
          console.error(err);
          res.statusCode = 500;
          res.end(err.message);
        }
      })
      .listen(8080, () => {
        console.log('Server listening on port 8080');
      });
  })
  .catch(err => {
    console.error('Failed to connect to Redis:', err);
    throw err;
  });

Python

這個應用程式使用 Flask 提供網頁服務,並使用 redis-py 套件與 Redis 執行個體通訊。

Flask==3.1.3; python_version >= '3.9'
gunicorn==23.0.0
redis==6.0.0
Werkzeug==3.1.8; python_version >= '3.9'

應用程式程式碼範例:

import logging
import os

from flask import Flask
import redis

app = Flask(__name__)

redis_host = os.environ.get("REDISHOST", "localhost")
redis_port = int(os.environ.get("REDISPORT", 6379))
redis_client = redis.StrictRedis(host=redis_host, port=redis_port)


@app.route("/")
def index():
    value = redis_client.incr("counter", 1)
    return f"Visitor number: {value}"


@app.errorhandler(500)
def server_error(e):
    logging.exception("An error occurred during a request.")
    return (
        """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(
            e
        ),
        500,
    )


if __name__ == "__main__":
    # This is used when running locally. Gunicorn is used to run the
    # application on Google App Engine and Cloud Run.
    # See entrypoint in app.yaml or Dockerfile.
    app.run(host="127.0.0.1", port=8080, debug=True)

建構容器映像檔

使用 Cloud Build 建構容器映像檔,並推送至 Container Registry:

cp gke_deployment/Dockerfile .
gcloud artifacts repositories create --location REPO_REGION --repository-format=docker REPO_ID
gcloud builds submit --tag REPO_REGION-docker.pkg.dev/PROJECT_ID/REPO_ID/visit-counter:v1

將應用程式部署至 Google Kubernetes Engine

為避免對 Redis 執行個體 IP 進行硬式編碼,您可以建立 redishost ConfigMap

export REDISHOST_IP=REDISHOST_IP
kubectl create configmap redishost --from-literal=REDISHOST=${REDISHOST_IP}

使用下列指令驗證設定:

kubectl get configmaps redishost -o yaml

更新 gke_deployment/visit-counter.yaml,將 <REPO_REGION><PROJECT_ID><REPO_ID> 替換為在「建構容器映像檔」中建立的容器映像檔值。這個檔案包含部署和服務的設定。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: visit-counter
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "<REPO_REGION>-docker.pkg.dev/<PROJECT_ID>/<REPO_ID>/visit-counter:v1"
        env:
        - name: REDISHOST
          valueFrom:
            configMapKeyRef:
              name: redishost
              key: REDISHOST
        ports:
        - name: http
          containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: visit-counter
spec:
  type: LoadBalancer
  selector:
    app: visit-counter
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP

將設定套用到叢集:

kubectl apply -f gke_deployment/visit-counter.yaml

執行下列指令,找出這個範例應用程式的外部 IP 位址:

kubectl get service visit-counter

使用瀏覽器確認應用程式是否託管在外部 IP 位址,或使用 curl 或瀏覽器傳送 GET 要求:

curl http://EXTERNAL_IP

移除 Redis 執行個體的 IP 表格項目

如果您已按照「準備 GKE 叢集」一節的步驟三操作,則已將 Redis 執行個體的保留 IP 範圍安裝至 GKE 執行個體的 IP 表。如要從 GKE 執行個體的 IP 表中移除這個 Redis IP 範圍項目,請透過 k8s-custom-iptables/ 目錄執行下列指令:

./uninstall.sh