Memorystore 是 Google Cloud 在 2018 年推出的托管 Redis 服务,让用户一键生成 Redis 实例,必要的时候再一键 scale,省去了维护 Redis 的烦恼。

本文在 k8s 中部署一个简单的小程序访问 Memorystore 数据库,获取 counter 值,并开启一个 http server 对外提供这个值。

准备

GCP 提供了一个命令行工具 gcloud,几乎所有的 web 操作都有对应的 CLI,非常方便。不同操作系统对应的安装包可以在这里下载

我的笔记本就叫它 “local host”,安装好 gcloud 之后,以下所有的操作都在 local 进行,命令执行的结果直接部署到 cloud 中。

现在开始前期准备工作。首先,在 GCP web 界面一键创建 Memorystore,之后我们能在 MemoryStore 的 Instances 里面看到这个实例,它的 IP 地址是 10.0.16.3 端口 6379。

很显然,10.0.16.3 这个 IP 是无法直接访问的,而如果你在相同的 GCP Project 里面创建了一个 VM instance,GCP 会自动创建一条路由,让你的 VM 可以 telnet 10.0.16.3 6379。

然后,创建一个 k8s 集群,这一步也同样可以在 web 界面里做,如果要用 GCP 提供的 gcloud 命令行的话如下:

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

至此准备工作全部完成。

一个 App

然后写一个小程序,读 Redis 的值返回给 http client,代码如下

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.NewPool(func() (redis.Conn, error) {
                return redis.Dial("tcp", redisAddr)
        }, maxConnections)

        http.HandleFunc("/", incrementHandler)
        log.Fatal(http.ListenAndServe(":8080", nil))
}

这段代码有以下几个功能:

  • 调用 redigo 库,这样几行代码就能操作 Redis 数据库,非常方便
  • 开启了一个 http server,当外部 client 访问 http server 时,把 Redis 中取到的 counter 值返回给 client。

部署 app 到 kubernetes

我们知道,k8s 中运行的程序必须是容器化的,因此需要把上面这段代码生成二进制并打包成镜像。

打包 docker image 的方式有很多种,可以本地编译成二进制以后再打包;也可以在build image 的时候编译程序。

下面给出一个 Dockerfile,这是 google cloud 文档给出的一个示例。这个 image 在构建的过程中把程序源码也打包加入镜像了,我认为这样增加了 image 体积并不是很好。不过这是另外的话题了,不在本文讨论范围。

FROM golang:1.8-alpine

RUN apk update && apk add git

RUN go get github.com/gomodule/redigo/redis

ADD . /go/src/visit-counter
RUN go install visit-counter

ENV REDISHOST redis
ENV REDISPORT 6379

ENTRYPOINT /go/bin/visit-counter

EXPOSE 8080

有了这个 Dockerfile 以后,开始在本地 local host 打包 image

export PROJECT_ID="$(gcloud config get-value project -q)"
docker build -t gcr.io/${PROJECT_ID}/visit-counter:v1 .
gcloud docker -- push gcr.io/${PROJECT_ID}/visit-counter:v1

注意: 生成的镜像是 gcr.io 加上你的 GCP 上这个 project 的 ID,因为最后要用 gcloud 命令把这个镜像 push 到 GCP “Container Register” 中去。

k8s 部署 gcr.io 镜像

经过上面几步后,GCP “Container Register” 一栏中就会有这个 image 了,接下来就是通过 kubectl 命令让 kubernetes 自动去拉取镜像然后在集群中部署。

在 local host 安装了 gcloud 工具后,可以通过 gcloud 安装 kubectl 插件,这样就可以在本地使用 kubectl 命令控制 cloud 中的 k8s 集群。

k8s Deployment

先是 Deployment,然后再创建 Service。 因为我们 app 源码中获取 redis IP 是从环境变量读取的,因此在部署到 k8s 时也需要设置这个 ENV。

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

kubectl get configmaps redishost -o yaml

下面是正式的 Deployment 文件,除了基本的 kind,spec字段以外,还有 env 字段。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: visit-counter
  labels:
    app: visit-counter
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
      - name: visit-counter
        image: "gcr.io/<PROJECT_ID>/visit-counter:v1"
        env:
        - name: REDISHOST
          valueFrom:
            configMapKeyRef:
              name: redishost
              key: REDISHOST
        ports:
        - name: http
          containerPort: 8080

最后一键部署 kubectl apply -f vc-deploy.yaml

k8s Service

deployment 的意思是让 k8s 运行这个容器,但是外界无法访问 http server 服务,因此还要让 k8s 提供 Service

apiVersion: v1
kind: Service
metadata:
  name: visit-counter
spec:
  type: LoadBalancer
  selector:
    app: visit-counter
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP

注意其中的 port/targetPort 字段,有必要解释一下。

kubectl edit deploy visit-counter 可以看到更详细的 Deployment 信息。

kubectl edit service visit-counter 有更详细的 Service 信息。

  ports:
  - nodePort: 30553
    port: 80
    protocol: TCP
    targetPort: 8080

可以看到实际更加详细的还有 nodePort 。

  • port 指 k8s 集群中服务之间可以互相访问的端口。
  • targetPort 指的 POD 上实际提供 service 的端口。
  • nodePort 指用户可以通过 kube-proxy 访问到的端口。

所以在回过头看上面的配置信息:

  1. port 80, 集群内部服务直接通过这个端口互相访问。
  2. targetPort 8080,POD 上的端口。
  3. nodePort 30553,kube-proxy 可以访问的端口。

稍等几分钟再执行 kubectl get service visit-counter

NAME            TYPE           CLUSTER-IP   EXTERNAL-IP     PORT(S)        AGE
kubernetes      ClusterIP      10.0.0.1     <none>          443/TCP        23h
visit-counter   LoadBalancer   10.0.15.95   35.xx.xxx.xx    80:30553/TCP   22h

从 EXTERNAL-IP 字段得知外界可访问的 IP,于是可以 curl http://EXTERNAL-IP

上文刚刚提到,外部通过kube-proxy 可以访问的是 30553 端口,内部服务之间才能用 80 端口,可为什么这里直接访问 80 就行了呢?

这是因为实际访问的是 LoadBalancer 的 80 端口。关于这一点,以及 kube-proxy 的作用,下一篇再讲。

(完)

参考资料