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 访问到的端口。
所以在回过头看上面的配置信息:
- port 80, 集群内部服务直接通过这个端口互相访问。
- targetPort 8080,POD 上的端口。
- 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 的作用,下一篇再讲。
(完)
参考资料