Redis 数据库的运维

作为纯内存缓存,Memcached拥有非常出色的读写性能,但也存在一个较为严重的缺点:无法持久化。

这意味着,一旦Memcached服务重启(更常见的是掉电),之前所有的缓存就会丢失。若线上的流量很大,这种重启很容易诱发"缓存雪崩",从而导致系统故障。

Redis的出现很好的解决了这个问题,它是一款高性能的内存的数据库,既不仅数据的支持持久化、也内置了许多数据结构,方便实现各种需求。在一些场景下1,可以直接用Redis取代Memcached + MySQL的组合。

本节将讨论Redis运维相关的问题。

Redis单服务器的运维

Redis同时支持单服务器、高可用、集群等三种方案。

我们先来看一下单服务器方案。

顾名思义,单服务器模式下,只启动一个Redis服务进程,若服务挂掉则Redis不可用。可见,这种方案并不保证高可用。

与之前的部署类似,我们同样将Redis部署在Kubernetes集群上,首先是创建Volume挂载点

sudo mkdir /data/redis
sudo chmod -R 777 /data/redis

接着,我们看一下部署文件:

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
  selector:
    app: redis
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-ct
        image: redis:3.2-alpine
        ports:
        - containerPort: 6379
          hostPort: 6379
        volumeMounts:
        - mountPath: "/data"
          name: volume
      volumes:
      - name: volume
        hostPath:
          path: /data/redis/

简要说明下:

  • 这里使用Redis官方的Docker镜像
  • 与MySQL类似,考虑到持久化后的数据量可能较大,我们将Pod绑定到minikube机器上,以固定存储。

应用servie,稍等一会,成功:

kubectl apply -f kubectl describe pod redis-798659bc79-vdht7
service "redis" created
deployment.apps "redis" created

我们尝试连接一下,首先获取Pod的ContainerId:

kubectl get pods
NAME                                                READY     STATUS    RESTARTS   AGE
redis-798659bc79-vdht7                              1/1       Running   0          4m
kubectl describe pod redis-798659bc79-vdht7
...
    Container ID:   docker://090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598
...

尝试登录,并登录Redis服务器:

minikube ssh
$ docker exec -i -t 090c2a7a004200aa6f0c4f3779e3823c401f03ad4f23985fdc08c38f86d6c598 /bin/sh
/data # echo "info" | redis-cli 
# Server
redis_version:3.2.12
redis_git_sha1:00000000
....

通过上面的操作,可以成功登录Redis服务器,并获取了版本信息。

至此,Redis的单服务器模式配置完成。

Redis高可用(Sentinel)集群的运维

在上面的Redis单服务器模式下,存在单点故障,假如这个Redis进程挂掉了,则Redis就无法提供服务了。

为了解决可用性,Redis内置了两种高可用方案,较为经典的是Sentinel集群。 Sentinel集群采用主备模式:

  • 支持多个Redis服务组,不同服务组通过唯一的master_name标识。
  • 组内一个主Redis节点提供服务,若干从Redis节点定期从主Redis节点同步数据。但从节点只作为热备,不提供服务。
  • 当某个组的主节点挂掉后,Sentinel服务会检测到主节点故障,并进行主备切换。
  • 客户端先连接Sentinel,根据master_name获取组内主Redis节点的IP和端口信息,再连接。

Sentinel集群架构

如果你对Sentinel的架构细节感兴趣,可以阅读官方文档

首先,我们来部署一组Redis服务的主节点:

apiVersion: v1
kind: Service
metadata:
  name: redis-lmsia-test1-master
spec:
  ports:
  - port: 6379
  selector:
    app: redis-lmsia-test1-master
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-lmaia-test1-deployment
spec:
  selector:
    matchLabels:
      app: redis-lmsia-test1-master
  template:
    metadata:
      labels:
        app: redis-lmsia-test1-master
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-sentinel-ct
        image: coder4/redis-sentinel-k8s:4.0.10
        ports:
        - containerPort: 6379
        env:
        - name: "MASTER"
          value: "true"
        - name: "MASTER_NAME"
          value: "lmsia_test1"

如上所示:

  • 我们使用了自定制的镜像redis-sentinel-k8s,其原理可以查看项目主页docker-redis-sentinel-k8s
  • MASTER=true,开启主节点模式
  • MASTER_NAME=lmsia_test1,Redis服务的组名叫lmsia_test1
  • 服务组名是redis-lmsia-test1-master,这个很重要,slave节点和sentinel会根据这个来定位master节点。

接着,我们启动lmsia_test1这个服务组一个从节点:

apiVersion: v1
kind: Service
metadata:
  name: redis-lmsia-test1-slave
spec:
  ports:
  - port: 6379
  selector:
    app: redis-lmsia-test1-slave
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-lmaia-test1-deployment
spec:
  selector:
    matchLabels:
      app: redis-lmsia-test1-slave
  template:
    metadata:
      labels:
        app: redis-lmsia-test1-slave
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-sentinel-ct
        image: coder4/redis-sentinel-k8s:4.0.10
        ports:
        - containerPort: 6379
        env:
        - name: "SLAVE"
          value: "true"
        - name: "MASTER_NAME"
          value: "lmsia_test1"

如上,组内从节点的启动方式和主节点基本一致,有几个需要特别注意的:

  • MASTER_NAME需要和主节点保持一致,即lmsia_test1
  • SLAVE=true,开启从节点模式。

我们先来启动这一组主从服务:

kubectl apply -f ./redis-lmsia-test1-master-service.yaml
kubectl apply -f ./redis-lmsia-test1-slave-service.yaml

我们分别登录Redis,看看他们的组状态,首先是master,身份是主节点,并可以看到从节点的IP:

redis-cli>info replication
info replication
# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.8,port=6379,state=online,offset=168,lag=1
master_replid:9b7dfe0b5f8d538d0f7b81d4095c239f1da72553
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:168
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:168

然后slave,状态是从节点,可以看到主节点的IP:

# Replication
role:slave
master_host:10.105.12.178
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:266
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9b7dfe0b5f8d538d0f7b81d4095c239f1da72553
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:266
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:266

接着,我们来启动Sentinel服务:

apiVersion: v1
kind: Service
metadata:
  name: redis-sentinel
spec:
  ports:
  - port: 26379
  selector:
    app: redis-sentinel
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-sentinel-deployment
spec:
  selector:
    matchLabels:
      app: redis-sentinel
  replicas: 3
  template:
    metadata:
      labels:
        app: redis-sentinel
    spec:
      nodeSelector:
        kubernetes.io/hostname: minikube
      restartPolicy: Always
      hostname: redis
      containers:
      - name: redis-sentinel-ct
        image: coder4/redis-sentinel-k8s:4.0.10
        ports:
        - containerPort: 26379
        env:
        - name: "SENTINEL"
          value: "true"
        - name: "MASTER_NAME_LIST"
          value: "lmsia_test1"

如上,我们部署了3个节点的Sentinel服务:

  • 使用我们定制的镜像redis-sentinel-k8s,其原理可以查看项目主页docker-redis-sentinel-k8s
  • 打开26379端口,这是默认Sentinel的默认端口
  • 环境变量SENTINEL表明以Sentinel模式启动
  • 环境变量MASTER_NAME_LIST,列出了所有要监听的组名即master_name,用空格分割开。

我们尝试连接任意一台sentinel来获取主结点的信息:

redis-cli -h localhost -p 26379
> SENTINEL get-master-addr-by-name lmsia_test1
1) "10.105.12.178"
2) "6379"

组内主Redis服务获取成功。

至此,我们已经完成了Redis的Sentinel部署方式。

小结

在本节中,我们从讨论了Redis的优点,以及单服务的运维方式。

接着,我们讨论了一种高可用Redis运维方案:Sentinel集群。这种方案可以保证Redis服务的高可用。但该方案也有明显的缺点:主备模式决定了资源的利用率只有50%,造成了一定的浪费。

拓展阅读

  1. 在小结中,我们提到了Sentinel模式会造成一定的资源浪费。可以采用Redis Cluster的部署模式,在保证高可用的同时,资源利用率。
  2. 为了保证高性能,Redis采用异步持久话的方式,分为rdb和aof两种,需要根据实际情况,选择适合的一种甚至混合方案。具体可参见文档(https://redis.io/topics/persistence)
  3. 若采用aof方式,积累较多修改后,重启Redis会非常慢,可以定期进行aof rewrite压缩aof日志。

^1^ . 若要维持较高性能,建议保留足够的内存以存储全部数据。

下一节:在上一章中,我们讨论了Redis服务的运维,包括单机运行和Sentinel运行。

在本小节中,我们讨论如何在Spring Boot中集成Redis。

Spring Boot内置了Redis的接入方式,spring-data-redis,这种方案在Jedis客户端的基础上尽心过了简单的封装。若只使用Redis的KV存储特性,该方案可以满足要求。但对于Redis的高级特性(如SortedSet、SETNX等),则需要手动调用底层Jedis客户端的API,使用方式较为晦涩且容易出错。

为此,我们推荐使用Redisson作为接入客户端,它提供了简单易用的封装,可以用最小的编程代价来发挥Redis的最大功能。