基于Kubernetes的高可用Redis集群部署方案

Published: 21 Mar 2019 Category: docker

一、技术背景

1.2 名词解释

StatefulSet PV PVC

二、方案设计

三、方案实施

3.1 镜像准备

准备redis镜像:

Dockerfile:

FROM redis:4.0.1

COPY conf/redis.conf /redis/redis.conf

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone \
    && mkdir -p /usr/local/etc/redis \
    && chown redis:redis /usr/local/etc/redis 

ENTRYPOINT [ "bash", "-c" ]

redis.conf:

daemonize no
port 6379
logfile "/var/log/redis/redis-server.log"
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000

利用命令构建镜像vio/redis:1.0: docker build -t vio/redis:1.0 .

准备redis-trib镜像,用于管理集群,并提供VIP供上层服务调用

Dockerfile:

FROM alpine:latest

COPY redis-trib.4.0.1.rb /home
WORKDIR /home
RUN apk add --no-cache \
  ca-certificates \
  ruby \
  ruby-bundler \
  ruby-dev \
  ruby-rdoc \
  ruby-irb \
  haproxy \
  vim \
  drill \
  && gem install redis \
  && sed -i 's@yes_or_die "Can I set the above configuration?"@#yes_or_die "Can I set the above configuration?"@g' redis-trib.4.0.1.rb \
  && mv redis-trib.4.0.1.rb /usr/bin/redis-trib \
  && chmod 755 /usr/bin/redis-trib \
  && rm -rf /var/cache/apk/* \
  && rm -rf /var/lib/apt/lists/* 

ENTRYPOINT ["tail", "-f", "/dev/null"]

利用命令构建镜像vio/redis-manager:1.0: docker build -t vio/redis-manager:1.0 .

3.2 K8S服务启动

3.2.1 持久化准备工作

这部分后续再补充

3.2.2 创建StatefulSet服务

Headless service是StatefulSet实现稳定网络标识的基础,我们需要提前创建。

headless-service.yml

apiVersion: v1
kind: Service
metadata:
  name: redis-service
  namespace: dev
  labels:
    app: redis-app
spec:
  ports:
  - name: redis-port
    port: 6379
  clusterIP: None
  selector:
    app: redis-app
    appCluster: redis-cluster

创建服务:kubectl create -f headless-service.yml

查看服务:

$ kubectl -n dev get svc redis-service

可以看到,服务名称为redis-service,其CLUSTER-IP为None,表示这是一个“无头”服务。

3.2.3 创建configmap

可以直接将Redis的配置文件转化为Configmap,这是一种更方便的配置读取方式。配置文件redis.conf如下:

appendonly yes
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
dir /var/lib/redis
port 6379

创建名为redis-conf的Configmap: kubectl -n dev create configmap redis-conf --from-file=redis.conf

查看: kubectl -n dev get configmap kubectl -n dev describe cm redis-conf

删除:kubectl -n dev delete cm redis-conf

3.2.4 创建Redis集群节点

创建好Headless service后,就可以利用StatefulSet创建Redis集群节点。

先创建redis-node.yml文件:

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: redis-app
spec:
  serviceName: "redis-service"
  replicas: 6
  template:
    metadata:
      labels:
        app: redis-app
        appCluster: redis-cluster
    spec:
      terminationGracePeriodSeconds: 10
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - redis
              topologyKey: kubernetes.io/hostname
      containers:
      - name: redis
        image: "vio/redis:1.0"
        command:
          - "redis-server"
        args:
          - "/etc/redis/redis.conf"
          - "--protected-mode"
          - "no"
        resources:
          requests:
            cpu: "100m"
            memory: "100Mi"
        ports:
            - name: redis
              containerPort: 6379
              protocol: "TCP"
            - name: cluster
              containerPort: 16379
              protocol: "TCP"
        volumeMounts:
          - name: "redis-conf"
            mountPath: "/etc/redis"
          - name: redis-volume
            mountPath: "/var/lib/redis"
      volumes:
      - name: "redis-conf"
        configMap:
          name: "redis-conf"
          items:
            - key: "redis.conf"
              path: "redis.conf"
      - name: redis-volume
        hostPath:
          path: /root/redis/volumes

创建:kubectl apply -f redis-node.yaml

查看:kubectl -n dev get pods

查看:kubectl -n dev logs redis-app-1

删除:kubectl delete -f redis-node.yaml

如上,总共创建了6个Redis节点(Pod),其中3个将用于master,另外3个分别作为master的slave;Redis的配置通过volume将之前生成的redis-conf这个Configmap,挂载到了容器的/etc/redis/redis.conf。

这里有一个关键概念——Affinity,请参考官方文档详细了解。其中,podAntiAffinity表示反亲和性,其决定了某个pod不可以和哪些Pod部署在同一拓扑域,可以用于将一个服务的POD分散在不同的主机或者拓扑域中,提高服务本身的稳定性。

而PreferredDuringSchedulingIgnoredDuringExecution 则表示,在调度期间尽量满足亲和性或者反亲和性规则,如果不能满足规则,POD也有可能被调度到对应的主机上。在之后的运行过程中,系统不会再检查这些规则是否满足。

另外,根据StatefulSet的规则,我们生成的Redis的6个Pod的hostname会被依次命名为$(statefulset名称)-$(序号)

$ kubectl -n dev get pods -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP            NODE             NOMINATED NODE
redis-app-0   1/1     Running   0          28s   10.244.0.90   acg-vio-dev-01   <none>
redis-app-1   1/1     Running   0          24s   10.244.0.91   acg-vio-dev-01   <none>
redis-app-2   1/1     Running   0          22s   10.244.0.92   acg-vio-dev-01   <none>
redis-app-3   1/1     Running   0          20s   10.244.0.93   acg-vio-dev-01   <none>
redis-app-4   1/1     Running   0          17s   10.244.0.94   acg-vio-dev-01   <none>
redis-app-5   1/1     Running   0          14s   10.244.0.95   acg-vio-dev-01   <none>

这些Pods在部署时是以{0..N-1}的顺序依次创建的。注意,直到redis-app-0状态启动后达到Running状态之后,redis-app-1 才开始启动。 同时,每个Pod都会得到集群内的一个DNS域名,格式为$(podname).$(service name).$(namespace).svc.cluster.local,即:

redis-app-0.redis-service.default.svc.cluster.local
redis-app-1.redis-service.default.svc.cluster.local

在K8S集群内部,这些Pod就可以利用该域名互相通信。

3.2.4.1 异常处理: ImagePullBackOff

kubectl -n dev get pods -o wide 查看到ImagePullBackOff错误,可能是其他Node节点没有该镜像。

解决:1. 限制部署的节点,在spec配置下添加nodeName: node-1配置。

  1. 或者,在其他Node节点增加镜像。

https://blog.csdn.net/tiger435/article/details/73650147

3.2.4.2 异常处理: nodes.conf is already used by a different Redis Cluster node

因为6个redis节点共享了一个数据盘,导致系统生成的nodes.conf冲突。

解决:1. 创建足够多的PV

  1. 删除共享盘(搭建一个不可靠的redis集群) 然后把redis.conf(configmap)改成如下内容:

    appendonly yes cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 port 6379

3.3 初始化Redis集群

创建好6个Redis Pod后,利用常用的Redis-tribe工具进行集群的初始化。

headless-manager-service.yml

apiVersion: v1
kind: Service
metadata:
  name: redis-manager-service
  namespace: dev
  labels:
    app: redis-manager
spec:
  ports:
  - name: redis-port
    port: 6379
  clusterIP: None
  selector:
    app: redis-manager
    appCluster: redis-cluster

创建服务:kubectl create -f headless-manager-service.yml

查看服务:kubectl -n dev get services

创建容器,redis-manager.statefulset.yml 文件内容:

apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: redis-manager
  namespace: dev
spec:
  serviceName: "redis-manager-service"
  replicas: 1
  template:
    metadata:
      labels:
        app: redis-manager
        appCluster: redis-cluster
    spec:
      restartPolicy: Always
      nodeName: "acg-vio-dev-01"
      terminationGracePeriodSeconds: 10
      containers:
      - name: redis-trib
        image: "vio/redis-trib:1.0"
        command:
          - "tail"
        args:
          - "-f"
          - "/dev/null"
        ports:
            - name: redis
              containerPort: 6379
              protocol: "TCP"
            - name: cluster
              containerPort: 16379
              protocol: "TCP"

启动:kubectl apply -f redis-manager.statefulset.yml

查看:kubectl -n dev get pods -o wide

查看:kubectl -n dev logs redis-app-1

删除:kubectl delete -f redis-manager.statefulset.yml

进入容器:kubectl -n dev exec -it redis-manager-0 sh

3.3.1 原文

创建只有Master节点的集群:

redis-trib.py create \
  `dig +short redis-app-0.redis-service.default.svc.cluster.local`:6379 \
  `dig +short redis-app-1.redis-service.default.svc.cluster.local`:6379 \
  `dig +short redis-app-2.redis-service.default.svc.cluster.local`:6379

如上,命令dig +short redis-app-0.redis-service.default.svc.cluster.local用于将Pod的域名转化为IP,这是因为redis-trib不支持域名来创建集群。 其次,为每个Master添加Slave:

redis-trib.py replicate \
  --master-addr `dig +short redis-app-0.redis-service.default.svc.cluster.local`:6379 \
  --slave-addr `dig +short redis-app-3.redis-service.default.svc.cluster.local`:6379

redis-trib.py replicate \
  --master-addr `dig +short redis-app-1.redis-service.default.svc.cluster.local`:6379 \
  --slave-addr `dig +short redis-app-4.redis-service.default.svc.cluster.local`:6379

redis-trib.py replicate \
  --master-addr `dig +short redis-app-2.redis-service.default.svc.cluster.local`:6379 \
  --slave-addr `dig +short redis-app-5.redis-service.default.svc.cluster.local`:6379

存在的问题:

  1. dig(alpine换成了drill)域名获取不到ip,应该是没有安装kubedns的原因。解决办法就是通过脚本来完成集群创建。

  2. WARNING: redis-trib.rb is not longer available! 原因是redis-trib.rb版本过高,解决: 从对应版本(redis3.2.0即可)的源码压缩包中src文件夹下找到对应的redis-trib.rb文件使用。 下载redis源码版压缩包:http://download.redis.io/releases/

3.3.2 绕路走

创建脚本create_cluster.sh 在容器外执行脚本:

kubectl

后续只要连接redis-manager的6379即可享受redis服务。

3.3.3 回到主线

至此,我们的Redis集群就真正创建完毕了,连到任意一个Redis Pod中检验一下:

$ kubectl exec -it redis-app-2 /bin/bash
[Docker]$/usr/local/bin/redis-cli -c
127.0.0.1:6379> cluster nodes

注意一定要加-c参数,可以连任意个机器。

3.4 创建用于访问Service

前面我们创建了用于实现StatefulSet的Headless Service,但该Service没有Cluster Ip,因此不能用于外界访问。所以,我们还需要创建一个Service,专用于为Redis集群提供访问和负载均衡(redis-access-service.yml):

apiVersion: v1
kind: Service
metadata:
  name: redis-access-service
  namespace: dev
  labels:
    app: redis-app
spec:
  ports:
  - name: redis-port
    protocol: "TCP"
    port: 6379
    targetPort: 6379
  selector:
    app: redis-app
    appCluster: redis-cluster

如上,该Service名称为 redis-access-service,在K8S集群中暴露6379端口,并且会对labels name为app: redis或appCluster: redis-cluster的pod进行负载均衡。

创建后查看:

$ kubectl -n dev get svc redis-access-service -o wide

3.5 测试主从切换

在K8S上搭建完好Redis集群后,我们最关心的就是其原有的高可用机制是否正常。这里,我们可以任意挑选一个Master的Pod来测试集群的主从切换机制,如redis-app-2:

$ kubectl -n dev get pods redis-app-2 -o wide
NAME          READY     STATUS    RESTARTS   AGE       IP                NODE
redis-app-2   1/1       Running   0          2h        192.168.169.198   k8s-node2

进入redis-app-2查看:

[root@k8s-node1 redis]#  kubectl exec -it redis-app-2 /bin/bash
root@redis-app-2:/data# /usr/local/bin/redis-cli -c
127.0.0.1:6379> role
1) "master"
2) (integer) 8666
3) 1) 1) "192.168.169.201"
      2) "6379"
      3) "8666"
127.0.0.1:6379>

如上可以看到,其为master,slave为192.168.169.201即redis-app-5。 接着,我们手动删除redis-app-2:

[root@k8s-node1 redis]# kubectl delete pods redis-app-2
pod "redis-app-2" deleted

[root@k8s-node1 redis]# kubectl get pods redis-app-2 -o wide
NAME          READY     STATUS    RESTARTS   AGE       IP                NODE
redis-app-2   1/1       Running   0          20s       192.168.169.210   k8s-node2

如上,IP改变为192.168.169.210。我们再进入redis-app-2内部查看:

[root@k8s-node1 redis]# kubectl exec -it redis-app-2 /bin/bash
root@redis-app-2:/data# /usr/local/bin/redis-cli -c
127.0.0.1:6379> role
1) "slave"
2) "192.168.169.201"
3) (integer) 6379
4) "connected"
5) (integer) 8960
127.0.0.1:6379>

如上,redis-app-2变成了slave,从属于它之前的从节点192.168.169.201即redis-app-5。

REF

从零开始搭建Kubernetes redis集群