Redis技术详解

一、Redis基本概念和安装使用

1、综述

Redis是一款内存数据库,所谓内存数据库是指它存储数据的主要介质是内存而非传统意义的磁盘,后者只用于辅助功能。Redis可以当作NoSQL数据库,缓存和消息代理来使用,目前各行业实践中使用Redis最多的场景还是把它当成缓存子系统,例如存储在线用户的登录情况,存储1小时内提交的订单情况等,缓存图片路径或者图片内容等等;其次较多的场景是作为消息代理来使用,例如DUBBO支持使用Redis进行事件订阅和通知。

Redis的发起者是Salvatore Sanfilippo,最初开发它的目的就是为了解决快速存储和查询社交网站上常见的好友关系数据。目前Vmware在资助着Redis项目的开发和维护,Redis最新的版本是3.X(本文写作时为version 3.2.5),其中内置支持了很多实用的数据存储结构,例如string, hashes, lists, set, sorted sets等,还提供了很多实用的高性能、高可靠特性,例如集合运算、LRU缓存内容管理、主从同步等等。

在本专题讨论Redis时,会首先花一些较少的篇幅介绍Redis的基本安装和使用,然后将深入Redis所支持的数据结构主要讲解Redis底层设计对这些数据结构的支撑,接下来会介绍Redis的主要配置优化事项,最后介绍Redis的集群搭建方式(基于3.X版本)和实施案例。

2、快速安装和基本配置说明

2-1、快速安装

Redis官方网站非常好记:https://redis.io/,安装也非常简单。请到官网下载最新发布的稳定版本:http://download.redis.io/releases/redis-3.2.5.tar.gz,解压后可直接make & make install。注意,类似Centos 6.X的老版本操作系统,在安装时可能会报一些错误,依次解决就行了:

make: cc: Command not found make:

# make install
> make: cc: Command not found make***

遇到这个错误,说明没有安装编译工具。yum安装 gcc 即可:

# yum install -y gcc

You need tcl 8.5 or newer in order

# make test
> error:You need tcl 8.5 or newer in order

这是因为相应软件的版本太老了,下载一个安装就行:

# wget http://downloads.sourceforge.net/tcl/tcl8.6.1-src.tar.gz
......
解压
......
# cd ./unix/
......
安装即可
......

安装过程中还可能有其它问题,按照报错解决就可以了

这些问题解决后,就可以安装Redis 3.2的版本了:

# make install
......
Hint: It's a good idea to run 'make test' ;)
......
INSTALL install
INSTALL install
INSTALL install
INSTALL install
INSTALL install
......

2-2、客户端连接测试

Redis的客户端组件可以和Spring框架无缝集成,还兼容多种开发语言:如PHP、Go、C#、python、C++、NodeJS等等。下面我们单纯使用Redis的Java客户端组件Jedis,完成一个简单的Redis服务端连接,并向服务器写入一个Key-Value值。我们使用的Jedis版本为2.8,您首先需要在Maven工程中引入这个组件:

......

redis.clients
jedis
2.8.0

......

以下是Java测试程序的主要代码片段:

......
Jedis redisClient = new Jedis("192.168.61.140", 6379);
// 写入一个K-V值
redisClient.set("key1", "value");
redisClient.close();
......

通过以上代码,我们向Redis服务器写入了一个K-V键值对,那么这个操作是否成功了呢?我们可以使用Redis自带的Client功能进行查看:

# redis-cli -h 192.168.61.140
192.168.61.140:6379> get key1
"value"

Redis自带的Client功能非常强大,包括写入、查询、监控、调试等,从以上的“get”命令结果来看Java客户端代码上的写入操作是成功了。但那只是最简单的写入操作,Redis支持的多种复杂数据结构以及他们的组织方式,我们将在后文进行详细讨论。

3、基本配置参数

Redis 3.2版本中,最重要的配置文件就是存在于安装目录中的名叫redis.conf的文件。其中的范本格式清楚说明了Redis 3.2版本中可以配置的主要信息和含义。主要包括几个方面:网络配置项、一般配置项、快照配置项、复制(高可用)配置项、集群配置项、安全配置项、资源限制配置项、LUA脚本配置、慢日志配置、监控配置、事件通知配置、数据结构高级配置等。

本节内容将首先讨论一些基础配置项,后续文章中在介绍Redis对各种数据结构支持时再介绍数据结构相应的配置项,介绍Redis数据快照原理时再介绍快照配置项,介绍Redis高可用方案和集群方案时再介绍相应的配置选项。

3-1、网络配置项和一般配置项

3-2、安全和资源限制配置项

requirepass:该选项指定客户端在完成连接后,在执行任何其它命令前必须使用的鉴权密码。这在技术团队需要拒绝一些不信任的客户端访问Redis服务的情况下,是比较有用的。

rename-command:在生产环境下,有一些Redis命令是非常危险的,例如FLUSHALL命令和FLUSHDB命令。所以Redis服务器为生产环境的指令安全性提供了一个重命名功能,通过这个功能我们可以将一些需要避免执行的指令变更成其它指令。例如以下设定可以将FLUSHALL指令变更成空字符:

......
rename-command FLUSHALL ""
rename-command FLUSHDB ""
......

这是再使用Redis的客户端执行以上命令,就会收到以下的提示:

......
// 开启了密码功能,并设置密码为123456
# redis-cli -h 192.168.61.140 -a 123456
192.168.61.140:6379> FLUSHDB
(error) ERR unknown command 'FLUSHDB'
192.168.61.140:6379> FLUSHALL
(error) ERR unknown command 'FLUSHALL'
......

3-3、日志和监控配置项

3-3-1、慢日志

Redis从很早的版本就提供了一个慢操作的日志记录功能,称为慢日志。当Redis中某一个内部操作所耗费的时间超过了设定的一个限制阀值,这个操作就会被慢日志功能记录在内存中。注意这里提到的是内部操作时间,并不包括可能的网络I/O操作时间。

通过Redis的客户端使用slowlog get、slowlog len、slowlog reset等命令可以查看当然Redis服务端记录的慢日志情况。Redis的配置信息中提供了两个参数供技术团队调整慢日志功能:slowlog-log-slower-than和slowlog-max-len。

slowlog-log-slower-than:这个参数就是给定慢日志的时间阀值,单位为微妙。也就是说1000000等于1秒。该参数的默认为10000。请注意,如果该参数的值设置为负数,将禁用慢日志功能;如果设置为0,将会记录每一条操作日志

slowlog-max-len:这个参数存储Redis服务端能够记录的慢日志总长度。这个数值没有上限,只是需要技术团队注意这会消耗宝贵的内存资源,实际上默认的128就是比较实用的设置值。

3-3-2、延迟监控功能

LATENCY监控器功能,是从Redis Version 2.8.3开始提供的一个重要的反映其延迟状态的监控功能。其工作任务是当技术人员设置了一个延迟操作的时间阀值后,凡是超过了这个阀值时间的操作都将被认为是执行缓慢的操作被记录下来。Redis客户端在登录后,可以使用诸如LATENCY LATEST、LATENCY HISTORY、LATENCY RESET、LATENCY GRAPH、LATENCY DOCTOR等命令控制/查询LATENCY监控器。LATENCY监控器功能更详细的介绍,可以参看Redis官方文档中的介绍:https://redis.io/topics/latency-monitor。

latency-monitor-threshold,这个参数项用来设置操作时间的阀值,如果关闭延迟监控功能的话,可以将这个值设置为0。另外客户端连接可以独立设置这个阀值,只需要在连接Redis服务端成功后,执行“CONFIG SET latency-monitor-threshold XXXX”命令即可。

二、Redis事件订阅和持久化存储

3-4、事件功能和配置项

Redis从2.X版本开始,就支持一种基于非持久化消息的、使用发布/订阅模式实现的事件通知机制。所谓基于非连接保持,是因为一旦消息订阅者由于各种异常情况而被迫断开连接,在其重新连接后,其离线期间的事件是无法被重新通知的(一些Redis资料中也称为即发即弃)。而其使用的发布/订阅模式,意味着其机制并不是由订阅者周期性的从Redis服务拉取事件通知,而是由Redis服务主动推送事件通知到符合条件的若干订阅者。

Redis中的事件功能可以提供两种不同的功能。一类是基于Channel的消息事件,这一类消息和Redis中存储的Keys没有太多关联,也就是说即使不在Redis中存储任何Keys信息,这类消息事件也可以独立使用。另一类消息事件可以对(也可以不对)Redis中存储的Keys信息的变化事件进行通知,可以用来向订阅者通知Redis中符合订阅条件的Keys的各种事件。Redis服务的事件功能在实际场景中虽然使用得不多,不过还是可以找到案例,例如服务治理框架DUBBO默认情况下使用Zookeeper作为各节点的服务协调装置,但可以通过更改DUBBO的配置,将Zookeeper更换为Redis。

3-4-1、publish和subscribe

我们先从比较简单的publish命令和subscribe命令开始介绍,因为这组命令所涉及到的Channel(通道)和Redis中存储的数据相对独立。publish命令由发送者使用,负责向指定的Channel发送消息;subscribe命令由订阅者使用,负责从指定的一个或者多个Channel中获取消息。

Redis技术详解

以下是publish命令和subscribe命令的使用示例:

// 该命令向指定的channel名字发送一条消息(字符串)
PUBLISH channel message
// 例如:向名叫FM955的频道发送一条消息,消息信息为“hello!”
PUBLISH FM955 "hello!"
// 再例如:向名叫FM900的频道发送一条消息,消息信息为“ doit!”
PUBLISH FM900 "doit!"
// 该命令可以开始向指定的一个或者多个channel订阅消息
SUBSCRIBE channel [channel ...]
// 例如:向名叫FM955的频道订阅消息
SUBSCRIBE FM955
// 再例如:向名叫FM955、FM900的两个频道订阅消息
SUBSCRIBE FM955 FM900

如果您使用需要使用publish命令和subscribe命令,您并不需要对Redis服务的配置信息做任何更改。以下示例将向读者展示两个命令的简单使用方式——前提是您的Redis服务已经启动好了:

由客户端A充当订阅者,在ChannelA和ChannelB两个通道上订阅消息

-- 我们使用的Redis服务地址为192.168.61.140,端口为默认值

[root@kp2 ~]# redis-cli -h 192.168.61.140
192.168.61.140:6379> SUBSCRIBE ChannelA ChannelB
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ChannelA"
3) (integer) 1
1) "subscribe"
2) "ChannelB"
3) (integer) 2

有客户端B从当订阅者,通过ChannelB发送消息给所有订阅者。

-- 连接到Redis服务器后,直接运行PUBLISH命令,发送信息

[root@kp1 ~]# redis-cli -h 192.168.61.140
192.168.61.140:6379> PUBLISH ChannelB "hello"
(integer) 1

以下是订阅者客户端A所受到的message信息:

......
-- 这时订阅者收到消息如下:
1) "message"
2) "ChannelB"
3) "hello"

从以上示例中可以看到,客户端A确实收到了客户端B所发送的消息信息,并且收到三行信息。这三行信息分别表示消息类型、消息通道和消息内容。注意,以上介绍的这组publish命令和subscribe命令的操作过程并没有对Redis服务中已存储的任何Keys信息产生影响。

3-4-2、模式订阅psubscribe

Redis中还支持一种模式订阅,它主要依靠psubscribe命令向技术人员提供订阅功能。模式订阅psubscribe最大的特点是,它除了可以通过Channel订阅消息以外,还可以配合配置命令来进行Keys信息变化的事件通知。

模式订阅psubscribe的Channel订阅和subscribe命令类似,这里给出一个命令格式,就不再多做介绍了(可参考上文对subscribe命令的介绍):

// 该命令可以开始向指定的一个或者多个channel订阅消息
// 具体使用示例可参见SUBSCRIBE命令
PSUBSCRIBE channel [channel ...]

模式订阅psubscribe对Keys变化事件的支持分为两种类型:keyspace(键空间通知)和keyevent(键事件通知),这两类事件都是依靠Key的变化触发的,而关键的区别在于事件描述的焦点,举例说明:

当Redis服务中0号数据库的MyKey键被删除时,键空间和键事件向模式订阅者分别发送的消息格式如下:

// 以下命令可订阅键空间通知
// 订阅0号数据库任何Key信息的变化
192.168.61.140:6379> psubscribe __keyspace@0__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyspace@0__:*"
3) (integer) 1
// 出现以上信息,说明订阅成功
// 当其他客户端执行 set mykey 123456 时,该订阅可收到以下信息
1) "pmessage"
2) "__keyspace@0__:*"
3) "__keyspace@0__:mykey"
4) "set"

以上收到的订阅信息,其描述可以概括为:“mykey的键空间发生了事件,事件为set”。这样的事件描述着重于key的名称,并且告诉客户端key的事件为set。我们再来看看订阅键事件通知时,发生同样事件所得到的订阅信息:

// 以下命令可订阅键事件通知
// 订阅0号数据库任何事件的变化
192.168.61.140:6379> psubscribe __keyevent@0__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:*"
3) (integer) 1
// 出现以上信息,说明订阅成功
// 当其他客户端执行 set mykey 123456 时,该订阅可收到以下信息
1) "pmessage"
2) "__keyevent@0__:*"
3) "__keyevent@0__:set"
4) "mykey"

以上收到的订阅信息中事件是主体,其信息可以概括为:“0号数据库发生了set事件,发生这个事件的key信息为mykey”。

3-4-3、模式订阅的配置

a、配置和通配符

要使用psubscribe命令进行键事件的订阅,就首先需要在Redis的主配置文件中对模式订阅进行设定。注意,如果您只是使用psubscribe命令通过Channel发送消息到订阅者,或者更单纯的使用publish命令和subscribe命令组合通过Channel发送和接收消息,就不需要进行这样的配置。

默认情况下Redis服务下的键空间通知和键事件通知都是关闭的。在redis.conf文件下,有专门的“EVENT NOTIFICATION”区域进行设定,设置的格式为:

......
notify-keyspace-events [通配符]
......

通配符的定义描述如下:

以下的几个实例说明了配置格式中通配符的用法:

// 监控任何数据格式的所有事件,包括键空间通知和键事件通知
notify-keyspace-events "AKE"
// 只监控字符串结构的所有事件,包括键空间通知和键事件通知
notify-keyspace-events "g$KExe"
// 只监控所有键事件通知
notify-keyspace-events "AE"
// 只监控Hash数据解构的键空间通知
notify-keyspace-events "ghKxe"
// 只监控Set数据结构的键事件通知
notify-keyspace-events "gsExe"

注意,在Redis主配置文件中进行事件通知的配置,其配置效果是全局化的。也就是说所有连接到Redis服务的客户端都会使用这样的Key事件通知逻辑。但如果单独需要为某一个客户端会话设置独立的Key事件通知逻辑,则可以在客户端成功连接Redis服务后,使用类似如下的命令进行设置:

......
192.168.61.140:6379> config set notify-keyspace-events KEA
OK

b、键事件订阅

完成键事件的配置后,就可以使用psubscribe命令在客户端订阅消息通知了。这个过程还是需要使用通配符参数,才能完成订阅指定。通配符格式如下所示:

psubscribe __[keyspace|keyevent]@__:[prefix]
// 例如:
// 订阅0号数据库中,所有的键变化事件,进行键空间通知
psubscribe __keyspace@0__:*
// 订阅0号数据库,所有的键变化事件,进行键空间通知和键事件通知
psubscribe __key*@0__:*

注意,就如上文所提到的那样,客户端能够进行键信息变化事件订阅的前提是Redis服务端或者这个客户端会话本身开启了相应配置。以下举例说明psubscribe命令中参数的使用方式:

// 注意,Redis服务上的配置信息如下
// notify-keyspace-events "gsExe"
// 即是说只允许监控Set结构的所有事件,并且之启用了键事件通知,没有启用键空间通知。
// 客户端使用以下命令开始订阅Key的变化事件
192.168.61.140:6379> psubscribe __key*@0__:*
// 以上命令订阅了0号数据库所有键信息的变化通知,包括键事件通知和键空间通知
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__key*@0__:*"
3) (integer) 1
// 接着,已连接到Redis服务上的另一个客户端执行了如下命令
// > sadd mysetkey rt
// 那么收到的消息通知为
1) "pmessage"
2) "__key*@0__:*"
3) "__keyevent@0__:sadd"
4) "mysetkey"

以上实例操作中有两个问题需要单独进行说明:

3-4-4、Redis订阅/发布功能的不足

Redis提供的订阅/发布功能并不完美,更不能和ActiveMQ/RabbitMQ提供的订阅/发布功能相提并论。

3-5、Redis持久化存储

从严格意义上说,Redis服务提供四种持久化存储方案:RDB、AOF、虚拟内存(VM)和DISKSTORE。虚拟内存(VM)方式,从Redis Version 2.4开始就被官方明确表示不再建议使用,Version 3.2版本中更找不到关于虚拟内存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo还专门写了一篇论文,来反思Redis对虚拟内存(VM)存储技术的支持问题。

至于DISKSTORE方式,是从Redis Version 2.8版本开始提出的一个存储设想,到目前为止Redis官方也没有在任何stable版本中明确建议使用这用方式。在Version 3.2版本中同样找不到对于这种存储方式的明确支持。从网络上能够收集到的各种资料来看,DISKSTORE方式和RDB方式还有这一些千丝万缕的联系,不过各位读者也知道,除了官方文档以外网络资料很多就是大抄。

最关键的是目前官方文档上能够看到的Redis对持久化存储的支持明确的就只有两种方案(https://redis.io/topics/persistence):RDB和AOF。所以本文也只会具体介绍这两种持久化存储方案的工作特定和配置要点。

3-5-1、RDB

RDB中文名为快照/内存快照,它的过程很好理解,就是Redis按照一定的时间周期将目前服务中的所有数据全部写入到磁盘中。但这个过程说起简单,实际上呢有很多细节需要被处理。Redis主配置文件的“REPLICATION”部分,放置了对这个过程的配置选项。在我们后续文章中讲解Redis支持的主从复制时,也可以看到RDB的影子。

Redis技术详解

上图反映了内存快照的大致过程,由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。

另一个问题是,在进行快照操作的这段时间,如果发生服务崩溃怎么办?很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。

以下是Redis中关于内存快照的主要配置信息:

# 周期性执行条件的设置格式为
save  
# 默认的设置为:
save 900 1
save 300 10
save 60 10000
# 以下设置方式为关闭RDB快照功能
save ""

以上三项默认信息设置代表的意义是:如果900秒内有1条Key信息发生变化,则进行快照;如果300秒内有10条Key信息发生变化,则进行快照;如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。

3-5-2、AOF

由于是周期性的同步,所以RDB存在的最大问题就是在Redis异常崩溃,需要从最近一次RDB文件恢复数据时,常常出现最近一批更新的数据丢失,而且根据快照的周期设置,这批数据的总量还可能比较大。另外,虽然使用专门的快照进程进行快照数据同步的方式,本身不会造成Redis服务出现卡顿。但如果需要快照的数据量特别大,操作系统基本上会将CPU资源用到快照操作上去,这可能间接造成包括Redis主进程在内的其它进程被挂起。所以,以一个较大的时间周期全部同步Redis数据状态的快照方式,在非常高并发的情况下并不是最好的解决方法。

虽然RDB快照基本上可以应付我们遇到的大多数业务场景,也可以满足至少80%业务系统设计时的预想性能压力,但为了尽可能解决RDB的工作缺陷,Redis还是提供了另一种数据持久化方式——AOF。AOF全称是Append Only File,从字面上理解就是“只进行增加的文件”。在本专题中,我们在介绍InnoDB的工作过程时,也介绍了类似“只进行增加的文件”,就是InnoDB中最关键的重做日志

在物理磁盘的操作无论是机械磁盘还是固态磁盘,使用顺序读写都将获得比随机读写好得多的I/O性能。所以我们可以看到无论是关系型数据库、NoSQL数据库还是之前的业务案例,无一例外都追求在物理磁盘上尽可能进行顺序读写操作。

AOF方式的核心设计思量可以总结为一句话:忠实记录Redis服务启动成功后的每一次影响数据状态的操作命令,以便在Redis服务异常崩溃的情况出现时,可以按照这些操作命令恢复数据状态。既然要记录每次影响数据状态的操作命令,就意味着AOF文件会越来越大!这是必然的。还好Redis为AOF提供了一种重写AOF文件的功能,保证了AOF文件中可以存储尽可能少的操作命令就能保证数据恢复到最新状态,这个功能被称为日志重写功能(请注意这可不是我们在讲解InnoDB时提到的重做日志)。

举个例子,操作人员在Redis中设置了一个K-V结构:mykey3 = yinwenjie,之后有删除了这个Key信息。那么AOF文件中记录的动作可能如下所示(AOF文件中的内容可以直接通过各种文本编辑工具直接查看):

# cat ./appendonly.aof
......
*3
$3
SET
$6
mykey3
$12
yinwenjie111
......
*2
$3
del
$6
mykey3
......

可以看到以上AOF文件中的内容,如实记录了这两个操作:设置Key和删除Key。但是这种日志记录过程对恢复Key的信息没有任何帮助,因为“mykey3”这个Key信息注定在最新的Redis内存中是不存在的。所以一旦我们运行“重写日志”命令(可以是设定的条件也可以直接运行“BGREWRITEAOF”命令),那么整理后的AOF文件的内容可能就是如下所示了:

# cat ./appendonly.aof
......
......

在AOF文件中对mykey3这个Key信息的操作过程记录消失了!这不但缩小了AOF文件还没有对数据恢复过程造成任何困扰。Redis主配置文件中关于AOF功能的设定可以在“APPEND ONLY MODE”部分找到:

appendfsync参数项可以设置三个值,分别是:always、everysec、no,默认的值为everysec。

always参数值,会使得AOF对数据的保存非常稳健。其设置意义是只要有一个写操作命令执行成功,就执行一次fsync函数调用。所以很显然always的设定值,就是三个选项值中处理效率最慢的。

no参数值,这个设置值表示Redis不会将执行成功的操作命令正真刷入AOF文件,而是完成操作系统级别的写操作后就认为AOF文件记录成功了,后续的I/O操作完全依赖于操作系统的设定,一般30秒会刷一次。

everysec参数值,这是默认的设置值,也是可以在数据稳健性和性能上平衡较好策略。它表示每秒钟都做一次fsync函数调用,正真做AOF文件的写入操作。

3-5-3、持久化存储的性能建议

关于持久化存储的性能建议,我们将结合后文介绍的Redis集群方案一起进行分析

三、Redis集群方案:高可用

1、概述

从本篇文章开始,我们将向读者介绍几种Redis的高可用高负载集群方案。除了介绍Redis 3.X版本中推荐的原生集群方案外,还会介绍使用第三方组件搭建Redis集群的方法。本文我们会首先介绍Redis的高可用集群方案。

2、Redis高可用方案

Redis提供的高可用方案和我们介绍过的很多软件的高可用方案类似,都是使用主从节点的思路。即是有一个Master节点在平时提供服务,另外一个或多个Slave节点在平时不提供服务(或只提供数据读取服务)。当Master节点由于某些原因停止服务后,再人工/自动完成Slave节点到Master节点的切换工作,以便整个Redis集群继续向外提供服务。既然是要进行角色切换,且要求这些节点针对外部调用者来说没有任何不同,最重要的就是Master节点和Slave节点的数据同步过程。数据同步最关键的设计思路是如何在数据一致性和同步性能上找到一个完美的平衡点。

同步复制的工作思路可以概括为:Master节点的任何数据变化都会立即同步到一个或多个Slave节点上。只要一个Slave节点同步失败(例如超时),都会认为整个数据写操作过程失败。这样的设计考虑侧重于保证各节点上的数据绝对一致,完全没有考虑对Master节点的响应性能,甚至会出现Master节点为了保证数据一致性而停止对后续写操作请求的响应。

异步复制的工作思路可以概括为:Master节点首先保证对外部请求的响应性能,它和Slave节点的数据同步一般由一个新的进程/线程独立完成。数据复制过程由Slave节点周期性发起或者由它一直驻留在Master节点的连接进行实时监控又或者由Master节点主动推送数据,再或者是同时使用多个异步复制过程。由于在Slave节点进行数据同步时,Master节点一直在处理新的数据写请求,所以Slave节点已完成同步的数据和Master上的实时数据一般会存在一些差异。例如MySQL原生支持的数据复制过程,就是一个异步过程。

很显然异步复制思路在对调用者的响应性能上,表现要比同步复制好得多。但如果由于异步复制而导致的节点间数据差异达到某种程度,就失去了数据同步的意义了。所以如何减少节点间的数据差异就成为异步复制过程中需要关注的要点。而后者的处理办法就有很多了,例如MySQL由第三方插件支持的半同步方式,又例如讲解ActiveMQ消息队列时提到的AutoAck和DUPS_OK_ACK,再例如我们下文介绍的Diskless Replication和Master写保护。

2-1、主从复制工作过程

Redis的主从复制功能除了支持一个Master节点对应多个Slave节点的同时进行复制外,还支持Slave节点向其它多个Slave节点进行复制。这样就使得架构师能够灵活组织业务缓存数据的传播,例如使用多个Slave作为数据读取服务的同时,专门使用一个Slave节点为流式分析工具服务。Redis的主从复制功能分为两种数据同步模式:全量数据同步和增量数据同步。

Redis技术详解

上图简要说明了Redis中Master节点到Slave节点的全量数据同步过程。当Slave节点给定的run_id和Master的run_id不一致时,或者Slave给定的上一次增量同步的offset的位置在Master的环形内存中无法定位时(后文会提到),Master就会对Slave发起全量同步操作。这时无论您是否在Master打开了RDB快照功能,它和Slave节点的每一次全量同步操作过程都会更新/创建Master上的RDB文件。在Slave连接到Master,并完成第一次全量数据同步后,接下来Master到Slave的数据同步过程一般就是增量同步形式了(也称为部分同步)。增量同步过程不再主要依赖RDB文件,Master会将新产生的数据变化操作存放在一个内存区域,这个内存区域采用环形构造。过程如下:

Redis技术详解

为什么在Master上新增的数据除了根据Master节点上RDB或者AOF的设置进行日志文件更新外,还会同时将数据变化写入一个环形内存结构,并以后者为依据进行Slave节点的增量更新呢?主要原因有以下几个:

由于网络环境的不稳定,网络抖动/延迟都可能造成Slave和Master暂时断开连接,这种情况要远远多于新的Slave连接到Master的情况。如果以上所有情况都使用全量更新,就会大大增加Master的负载压力——写RDB文件是有大量I/O过程的,虽然Linux Page Cahe特性会减少性能消耗。

另外在数据量达到一定规模的情况下,使用全量更新进行和Slave的第一次同步是一个不得已的选择——因为要尽快减少Slave节点和Master节点的数据差异。所以只能占用Master节点的资源和网络带宽资源。

使用内存记录数据增量操作,可以有效减少Master节点在这方面付出的I/O代价。而做成环形内存的原因,是为了保证在满足数据记录需求的情况下尽可能减少内存的占用量。这个环形内存的大小,可以通过repl-backlog-size参数进行设置。

Slave重连后会向Master发送之前接收到的Master run_id信息和上一次完成部分同步的offset的位置信息。如果Master能够确定这个run_id和自己的run_id一致且能够在环形内存中找到这个offset的位置,Master就会发送从offset的位置开始向Slave发送增量数据。那么连接正常的各个Slave节点如何接受新数据呢?连接正常的Slave节点将会在Master节点将数据写入环形内存后,主动接收到来自Master的数据复制信息。

2-2、基本Master/Slave配置

Redis提供的主从复制功能的配置信息,在Redis主配置文件的“REPLICATION”部分。以下是这个部分的主要参数项说明:

2-3、Master和Slave设置实例

讨论了Redis中主从复制的基本原理和Redis主配置文件中针对主从复制的设定选项意义后,我们来看一个实际设置过程。注意,由于这个过程非常简单所以我们会“非常快”。首先Master服务器不需要针对主从复制做任何的设置(这不包括对主从复制过程的配置优化)。所以我们就直接来看Slave节点的配置:

Slave节点上我们只需要做一件事情,就是打开slaveof选项:

......
# slaveof选项的设置,给定master节点的ip和port就可以了
# 192.168.61.140就是master节点
slaveof 192.168.61.140 6379
......

接着,我们马上就可以看看同步效果了。首先确保您的master节点使工作正常的,然后就可以启动Slave节点了:

......
5349:S 17 Dec 04:20:00.773 * Connecting to MASTER 192.168.61.140:6379
5349:S 17 Dec 04:20:00.773 * MASTER <-> SLAVE sync started
5349:S 17 Dec 04:20:00.774 * Non blocking connect for SYNC fired the event.
5349:S 17 Dec 04:20:00.775 * Master replied to PING, replication can continue...
5349:S 17 Dec 04:20:00.776 * Partial resynchronization not possible (no cached master)
5349:S 17 Dec 04:20:00.782 * Full resync from master: 976f0b31cbf6acd4fcc888301ea4639a7c591136:1
5349:S 17 Dec 04:20:00.864 * MASTER <-> SLAVE sync: receiving 119 bytes from master
5349:S 17 Dec 04:20:00.865 * MASTER <-> SLAVE sync: Flushing old data
5349:S 17 Dec 04:20:00.865 * MASTER <-> SLAVE sync: Loading DB in memory
5349:S 17 Dec 04:20:00.865 * MASTER <-> SLAVE sync: Finished with success
5349:S 17 Dec 04:20:01.068 * Background append only file rewriting started by pid 5352
5349:S 17 Dec 04:20:01.082 * AOF rewrite child asks to stop sending diffs.
5352:C 17 Dec 04:20:01.082 * Parent agreed to stop sending diffs. Finalizing AOF...
5352:C 17 Dec 04:20:01.082 * Concatenating 0.00 MB of AOF diff received from parent.
5352:C 17 Dec 04:20:01.082 * SYNC append only file rewrite performed
5352:C 17 Dec 04:20:01.082 * AOF rewrite: 6 MB of memory used by copy-on-write
5349:S 17 Dec 04:20:01.168 * Background AOF rewrite terminated with success
5349:S 17 Dec 04:20:01.168 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
5349:S 17 Dec 04:20:01.168 * Background AOF rewrite finished successfully
......

笔者在Slave节点上开启了定期的RDB快照和AOF日志功能,所以各位读者可以忽略那些日志信息,直接关注“Connecting to MASTER ….”和“MASTER <-> SLAVE …….”这些日志信息就好。

以下是Master节点上给出的日志信息

......
5614:M 17 Dec 04:20:00.789 * Slave 192.168.61.145:6379 asks for synchronization
5614:M 17 Dec 04:20:00.789 * Full resync requested by slave 192.168.61.145:6379
5614:M 17 Dec 04:20:00.789 * Starting BGSAVE for SYNC with target: disk
5614:M 17 Dec 04:20:00.791 * Background saving started by pid 5620
5620:C 17 Dec 04:20:00.814 * DB saved on disk
5620:C 17 Dec 04:20:00.815 * RDB: 6 MB of memory used by copy-on-write
5614:M 17 Dec 04:20:00.875 * Background saving terminated with success
5614:M 17 Dec 04:20:00.877 * Synchronization with slave 192.168.61.145:6379 succeeded
......

看来Master节点收到了Slave节点的连接信息,并完成了全量数据同步操作。

2-4、关闭RDB功能的说明

以上介绍的Master节点和Slave节点的设置是否特别简单?是的,实际上只需要打开了Slave节点上“REPLICATION”区域的slaveof选项就可以让Redis的主从复制功能运作起来。现在我们往回倒,回到上一篇文章的介绍。在上一篇文章介绍RDB快照功能的配置项时,文章提到了可以用以下方式关闭RDB快照功能:

# 以下为默认的设置为,注释掉即可
# save 900 1
# save 300 10
# save 60 10000
# 在设置以下选项,就可以关闭RDB功能
save ""

但是根据本文对Redis主从复制的介绍,我们可以发现Redis的RDB快照功能实际上是无法真正关闭的!以上所谓关闭RDB功能的设置,只是关闭了Redis服务在正常工作时定期快照的条件设定,但只要有Slave节点请求全量数据同步,Master节点就会强制做一次RDB快照。并且如果客户端主动发送BGSAVE命令,要求Redis服务进行RDB快照时,Redis也会被动执行RDB快照操作。

但是本文还是建议在组建Redis高可用集群时,关闭Master节点上的RDB功能。读者一定要清楚这样做的原因:这不是为了像个别网络资料说的那样真正关闭Redis的RDB快照功能,而是尽可能减少Master上主动进行RDB操作的次数,并将RDB快照工作转移到各个Slave节点完成。

3、Redis Sentinel

Redis服务提供了性能较高的主从复制功能,但是没有提供原生的Master——Slave的切换功能。也就是说如果您只是配置了Redis的主从复制功能,那么在Master节点出现故障时,您必须手动将一台Slave状态的节点切换为Master状态。当然这个问题在Redis Version 2.8 版本前是有标准解决方案的,那就是:Keepalived + Redis服务组成的高可用集群。

Redis技术详解

由Keepalived监控Redis高可用集群中Master节点的工作状态,并在异常情况下切换另一个节点接替工作。但是,这个方案是有一些问题了,其中之一就是所有的Slave节点在Standby状态时无法分担Master节点的任何性能压力——即使您设置了read-only等参数也不行,因为VIP根本不会把请求切过去。并且这种方式还不太方便监控Redis高可用集群中各个服务节点的实时状态。

从Version 2.8版本开始,Redis提供了一个原生的主从状态监控和切换的组件——Redis Sentinel。通过它技术人员不但可以完成Redis高可用集群的适时监控,还可以通过编程手段减轻集群中Master节点中读操作的压力。本节内容,我们向读者介绍这个Redis Sentinel的简单使用。

3-1、基本配置

由于Redis Sentinel是Redis原生支持的,以Redis Version 3.2为例,在下载安装后就可以直接使用命令“redis-sentinel”启动Sentinel了。Sentinel的主配置文件模板存放在Redis安装目录的下,默认名为“sentinel.conf”。以下命令可以启动Sentinel(启动Sentinel所依据的配置文件是一定要携带的参数):

# redis-sentinel ./sentinel.conf

Redis Sentinel本身也支持集群部署,而且为了在生产环境下避免Sentinel单点故障,所以也建议同时部署多个Sentinel节点。部署多个Sentinel还有一个原因,就是提高Master——Slave切换的准确性。以下的配置文件介绍会说明这一点。

下面我们介绍一些Sentinel主配置文件中的关键配置,注意Sentinel主配置文件也有类似Redis主配置文件提供的访问保护模式(protected-mode)、访问者权限控制(auth-pass)等,但是它们的意义基本上类似前文介绍过的,在Redis主配置文件中的相似内容,所以这里就不再赘述了。

sentinel monitor

这个属性是Redis Sentinel中的最主要设置元素,换句话说如果要开启Sentinel甚至可以只设置这个属性。它包括了四个参数:master-name,这个参数是一个英文名说明了Sentinel服务监听的Master节点的别名,如果一个Sentinel服务需要同时监控多个Master,这需要设置多个不同的master-name;ip和redis-port,指向sentinel需要监控的Redis集群最初的那个Master节点(为什么会是最初呢?后文会说明)的ip和端口;quorum,投票数量这个参数很重要,如果是Sentinel集群方式下,它设定“当quorum个Sentinel认为Master异常了,就判定该Master真的异常了”。单个Sentinel节点认为Master下线了被称为主管下线,而quorum个Sentinel节点都认为Master下线的情况被称为客观下线。

sentinel parallel-syncs

一旦原来的Master节点被认为客观下线了,Sentinel就会启动切换过程。大致来讲就是从当前所有Slave节点选择一个节点成为新的Master节点(这时在Redis中设定的slave-priority参数就会起作用了)。而其它的Slave其slaveof的Master信息将被sentinel切换到新的Master上。而一次同时并行切换多少个Slave到新的Master上就是这个参数决定的。如果整个Redis高可用集群的节点数量不多(没有超过6个),建议使用默认值就可以了。

主配置文件中被rewrite的参数内容:sentinel.conf文件中的配置内容会随着Sentinel的监控情况发生变化——由Sentinel程序动态写入到文件中。例如sentinel known-slave参数、sentinel current-epoch参数和sentinel leader-epoch参数。

注意,在Sentinel中您只需要配置最初的Master的监控位置,无需配置Master下任何Slave的位置,Sentinel会自己识别到这些Master直接的或者间接的Slave。

3-2、切换效果

介绍完配置后,我们来简单看一个Sentinel工作和切换的例子。这个例子中的有一个Master节点和一个Slave节点,当Master节点出现故障时,通过Sentinel监控到异常情况并自动完成Slave状态的切换。

首先请保证您的Master节点和Slave节点都是正常工作的,这个过程可以参见笔者之前文章的介绍:

Redis技术详解

这里就不再赘述Redis Master和Redis Slave的内容了,因为在本文第2节中已经详细介绍过。实际上您只需要打开Slave节点的主配置文件,并增加slaveof的配置信息,将其指向Master的IP和端口就可以了。以下是Sentinel节点主要更改的配置信息:

......
sentinel monitor mymaster 192.168.61.140 6379 1
......

由于在测试环境中我们只使用了一个Sentinel节点,所以设置sentinel monitor配置项中的quorum为1就可以了,代表有一个Sentinel节点认为Master不可用了,就开启故障转移过程。当然生产环境下不建议这样使用。

之后我们使用以下Sentinel的主要配置信息启动Sentinel:

# redis-sentinel ./sentinel.conf
......
8576:X 19 Dec 00:49:01.085 # Sentinel ID is 5a5eb7b97de060e7ad5f6aa20475a40b3d9fd3e1
8576:X 19 Dec 00:49:01.085 # +monitor master mymaster 192.168.61.140 6379 quorum 1
......

之后主动终止原来Master的运行过程(您可以直接使用kill命令,或者拔掉网线,又索性直接关机),来观察Slave节点和Sentinel节点的日志情况:

当断开原来的Master节点后,Slave节点将提示连接失效并开始重试。当Sentinel开始进入故障转移并完成后,Salve又会打印相应的过程信息:

......
8177:S 19 Dec 00:53:17.467 * Connecting to MASTER 192.168.61.140:6379
8177:S 19 Dec 00:53:17.468 * MASTER <-> SLAVE sync started
8177:S 19 Dec 00:53:17.468 # Error condition on socket for SYNC: Connection refused
......
8177:M 19 Dec 00:53:18.134 * Discarding previously cached master state.
8177:M 19 Dec 00:53:18.134 * MASTER MODE enabled (user request from 'id=3 addr=192.168.61.140:51827 fd=5 name=sentinel-5a5eb7b9-cmd age=258 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=r cmd=exec')
8177:M 19 Dec 00:53:18.138 # CONFIG REWRITE executed with success.
......

从以上Slave节点的内容可以看到,Slave被切换成了Master状态。那么Sentinel本身有哪些重要的日志信息呢?如下所示:

......
// 当前Sentinel节点确定原Master主观下线
8576:X 19 Dec 00:53:18.074 # +sdown master mymaster 192.168.61.140 6379
// 由于设置的quorum为1,所以一个Sentinel节点的主管下线就认为Master客观下线了
8576:X 19 Dec 00:53:18.074 # +odown master mymaster 192.168.61.140 6379 #quorum 1/1
// 第三代,每转移一次故障epoch的值+1,
// 不好意思,在书写测试实例前,本人已经自行测试了两次故障转移,所以这里看到的epoch为3
// 这个信息会自动写入到Sentinel节点的主配置文件中
8576:X 19 Dec 00:53:18.074 # +new-epoch 3
// 开始进行故障转移
8576:X 19 Dec 00:53:18.074 # +try-failover master mymaster 192.168.61.140 6379
// 选举出主导故障转移的Sentinel节点,因为不是所有Sentinel节点都会主导这个过程
8576:X 19 Dec 00:53:18.084 # +vote-for-leader 5a5eb7b97de060e7ad5f6aa20475a40b3d9fd3e1 3
8576:X 19 Dec 00:53:18.084 # +elected-leader master mymaster 192.168.61.140 6379
8576:X 19 Dec 00:53:18.084 # +failover-state-select-slave master mymaster 192.168.61.140 6379
// 选择提升哪一个slave作为新的master
8576:X 19 Dec 00:53:18.156 # +selected-slave slave 192.168.61.145:6379 192.168.61.145 6379 @ mymaster 192.168.61.140 6379
8576:X 19 Dec 00:53:18.156 * +failover-state-send-slaveof-noone slave 192.168.61.145:6379 192.168.61.145 6379 @ mymaster 192.168.61.140 6379
8576:X 19 Dec 00:53:18.211 * +failover-state-wait-promotion slave 192.168.61.145:6379 192.168.61.145 6379 @ mymaster 192.168.61.140 6379
// 提升原来的slave
8576:X 19 Dec 00:53:19.201 # +promoted-slave slave 192.168.61.145:6379 192.168.61.145 6379 @ mymaster 192.168.61.140 6379
// 试图重写所有salves节点的配置信息,并让它们指向新的master
8576:X 19 Dec 00:53:19.201 # +failover-state-reconf-slaves master mymaster 192.168.61.140 6379
// 故障转移结束
8576:X 19 Dec 00:53:19.250 # +failover-end master mymaster 192.168.61.140 6379
// 最终完成master节点的切换
8576:X 19 Dec 00:53:19.250 # +switch-master mymaster 192.168.61.140 6379 192.168.61.145 6379
// 注意原有的master节点会再显示一条作为主观下线,但是这次下线信息是以salve身份通知的
// 这是因为这次故障切换后,原来的master就算再上线,也只会作为Slave节点了
8576:X 19 Dec 00:53:19.251 * +slave slave 192.168.61.140:6379 192.168.61.140 6379 @ mymaster 192.168.61.145 6379
8576:X 19 Dec 00:53:49.305 # +sdown slave 192.168.61.140:6379 192.168.61.140 6379 @ mymaster 192.168.61.145 6379
......

通过Slave节点和Sentinel节点的日志可以看到,在经过了短暂的时间后Sentinel成功将唯一一个Slave节点转换成了Master节点,并继续向外部提供服务。

之后我们重新启动原有Master节点,看看会发生什么:

# 以下是原有Master启动后,在Sentinel显示的信息
......
8576:X 19 Dec 01:31:12.743 * +reboot slave 192.168.61.140:6379 192.168.61.140 6379 @ mymaster 192.168.61.145 6379
8576:X 19 Dec 01:31:12.805 # -sdown slave 192.168.61.140:6379 192.168.61.140 6379 @ mymaster 192.168.61.145 6379
......

一个非常重要的现象是,当原来的Master节点再次启动时,即使配置文件中没有设定slaveof信息,它也会在Sentinel的协调下称为Slave节点。这是因为任何一次Master到Slave的切换都是要付出代价的,其中除了状态本身的判断外,还有Sentinel自身协调和选举过程(选举哪一个Sentinel进行实质的切换动作),还有新的Master的选定问题,甚至包括Slave的slaveof目标变化过程中需要处理的数据一致性问题等等工作。所以最好的办法就是:只要能够保证Redis高可用集群持续工作,就不进行Master状态的切换。

3-3、Java客户端配合Sentinel的使用

通过Sentinel组建的高可用集群对比通过第三方软件组建的高可用集群而言,有其明显的优点。例如可以实时返回集群中每个Redis节点的状态,且各节点间更能保持最佳的数据一致性,另外还可以在必要的时候通过转移客户端读操作,减轻Master节点的工作压力。但是它也有一个很明显的缺点,就是由于整个集群可以向调用者开放多个Redis节点的地址,且Sentinel本身并不能充当路由器的作用,所以当Redis高可用集群进行状态切换时,客户端可能并不清楚原有的Master节点已经失效了。如下图所示:

Redis技术详解

还好的是,Java最常用的Redis客户端jedis提供了一组针对Sentinel的集群工具,让客户端可以在获取当前Redis高可用集群中的Master节点后,再在这个Master节点上完成数据读写操作。但另外一个读操作的负载问题还是没有被解决,所有的读操作也只会在Master节点完成。

Redis技术详解

我们来看看一些关键代码:

......
// 这是基本的连接配置
// 当让这些属性都可以根据您的实际情况进行更改
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(50);
poolConfig.setMinIdle(20);
poolConfig.setMaxWaitMillis(6 * 1000);
poolConfig.setTestOnBorrow(true);
// 以下是可用的多个Sentinel节点的ip和端口
Set jedisClusterNodes = new HashSet();
jedisClusterNodes.add("192.168.61.140:26379");
//如果有多个Sentinel一个一个添加进去
//jedisClusterNodes.add("192.168.61.139:26379");
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("mymaster", jedisClusterNodes , poolConfig);
// 开始插入信息
for(Integer index = 0 ; index < 10000 ; index++) {
// 获取最新的master信息
Jedis master = null;
try {
master = jedisSentinelPool.getResource();
} catch(JedisConnectionException e) {
// 如果出现异常,说明当前的maser断开了连接,那么等待一段时间后重试
LOGGER.info("master is loss , waiting for try again......");
synchronized (MasterSlaveApp.class) {
MasterSlaveApp.class.wait(5000);
}
index--;
continue;
}
// 开始正式插入
master.set(("key" + index).getBytes(), index.toString().getBytes());
LOGGER.info("write : " + "key" + index);
synchronized (MasterSlaveApp.class) {
// 停止0.5秒,以便观察现象
MasterSlaveApp.class.wait(500);
}
}
jedisSentinelPool.close();

在示例代码中Sentinel节点只有一个,存在于192.168.61.140:26379上。如果是生产环境建议不要对Sentinel进行单点部署,否则一旦Sentinel单点崩溃会造成整个Redis高可用集群在客户端无法进行Master节点的切换。在初始阶段192.168.61.140:6379是master节点,然后我们在程序执行过程中将原有的master节点关闭,这时上面的客户端代码片段可能的输出以下日志信息(部分):

......
14639 [main] INFO redis_test.test.MasterSlaveApp - write : key29
16144 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
22148 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
28151 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
34155 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
40159 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
十二月 20, 2016 4:12:22 下午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 192.168.61.145:6379
46163 [main] INFO redis_test.test.MasterSlaveApp - master is loss , waiting for try again......
51166 [main] INFO redis_test.test.MasterSlaveApp - write : key30
51670 [main] INFO redis_test.test.MasterSlaveApp - write : key31
......

四、Redis集群方案:高性能

1、概述

从这篇文章开始我们一起来讨论Redis中两种高性能集群方案,并且在讨论过程中将上一篇文章介绍的高可用集群方案结合进去。这两种高性能集群方案是:Twemproxy和Redis自带的Cluster方案。

2、Redis高性能集群:Twemproxy

2-1、Twemproxy概要

Twemproxy是一个Twitter开源的一个Redis/Memcache代理服务器,最早也是Twitter在使用。在Twitter决定开发Twemproxy时,互联网领域使用最广泛的缓存技术还是Memcache,那个时候Redis并没有提供原生的Cluster功能,甚至没有Bate版本。而Twemproxy(也称为nutcraker)恰恰是为了解决将多个独立的Redis节点组成集群,同时提供缓存服务的问题。

您可以在GitHub上下载Twemproxy(https://github.com/twitter/twemproxy)。这个地址也可以算作Twemproxy的官网了,其上对Twemproxy的特性进行了简要描述,包括:轻量级的快速访问代理、减少客户端对Redis服务的直连、支持多个Redis节点同时工作、支持数据分片、支持多种Hash算法、故障检查和故障节点自动排除、支持多种操作系统Linux, BSD, OS X 以及 Solaris…… 。

但是官网上并没有明确介绍Twemproxy的缺点,而且细心的朋友可以观察到官网上的源代码已经有相当时间没有更新了,这能说明什么问题呢?本文后续内容将进行说明。下图说明了Twemproxy的功能定位:

Redis技术详解

2-2、Twemproxy基本配置

Twemproxy的基本配置非常简单,直接解压安装后就可以运行。它的主要配置文件在conf目录下的“nutcracker.yml”文件。由于Twemproxy的安装太简单了,本文就不再进行描述了,这里给出安装命令就可以了(CentOS 6.X / 7.X):

// 先安装automake、libtool等第三方支持组件
# yum install -y automake libtool
// 然后解压下载的Twemproxy压缩包
// 进入解压的目录后运行(生成configure )
# autoreconf -fvi
// 正式开始安装
# ./configure --prefix=您的安装目录

安装后打开“nutcracker.yml”文件,这个文件实际上已经有内容了,相当于一个各种运行场景下的配置示例。各位读者可以将这些配置信息注释掉或者直接删除。这里我们给出配置文件中一些关键的配置属性,大家只需要知道这些配置属性的意义,就可以进行灵活操作了:

listen:这个参数用于设置Twemproxy监控的IP和端口信息,如果要监控本机的所有IP设备,则设置为0.0.0.0,例如0.0.0.0:22122。

hash:这个参数非常重要所以多说几句。Twemproxy可以使用一致性Hash算法,通过计算Key的Hash值定位这个Key对应的数据存储在下层Redis节点的哪个位置。注意不是计算Key的数据结构,而是Key对应数据的存储位置。Twemproxy支持多种Hash算法,包括:md5、crc16、fnv1_64、fnv1a_64、fnv1_32、fnv1a_32、hsieh、murmur、jenkins等。之前我们介绍过,考虑采用哪种Hash算法有两个重要指标:Hash算法的速度和Hash算法的碰撞率。例如之前号称破解了MD5算法的王小云教授就是依靠Hash碰撞完成的,但实际上这种方式算不算完全破解呢?行业内就有很多种观点了,这里笔者经验有限就不展开讨论了。但MD5算法的两个事实依然是存在的:MD5任然是不可逆的,同时现成的破解站点也是存在的:http://www.ttmd5.com/。另外两个事实是MD5算法的碰撞率确实是各种Hash算法中碰撞率非常低的,但它确实不是所有Hash算法中速度最快的。显然,这里我们更看重的Hash值的计算速度而不是碰撞率,产生Hash碰撞的两个Key其后果无非是被分配到同一个Redis节点进行存储而已。所以这里我们推荐设置两种Hash算法,murmur和fnv1a_64。

以下展示了一个完整的可以使用的Twemproxy代理的配置文件:

beta:
listen: 0.0.0.0:22122
hash: fnv1a_64
hash_tag: "{}"
distribution: ketama
auto_eject_hosts: false
timeout: 400
redis: true
servers:
- 192.168.61.140:6379:1 server1
- 192.168.61.145:6379:1 server2
#原有配置文件中的其它配置信息如果不使用则可以注释掉

以上配置信息中的各个属性已经在前文详细介绍,这里就不再赘述了。以下是Twemproxy的启动指令,记得要首先设置Linux下的环境变量:

# nutcracker -c ./nutcracker.yml
// 您还可以通过以下命令测试配置文件的正确性
# nutcracker -c ./nutcracker.yml -t
// 还有更多参数可选
Usage: nutcracker [-?hVdDt] [-v verbosity level] [-o output file]
[-c conf file] [-s stats port] [-a stats addr]
[-i stats interval] [-p pid file] [-m mbuf size]
// 关于这些参数更详细的使用说明,可以参考官方文档中的说明

2-3、LVS + Twemproxy + Keepalived + Redis + Redis Sentinel + Sentinel Agent

在生产环境下搭建Redis高性能集群,如果其中只使用一个Twemproxy节点,那肯定是不合理的。因为那样做会存在Twemproxy单节点故障问题,所以至少应该使用两个Twemproxy节点。又因为Twemproxy服务的工作相对独立,为了增加访问性能可以使用两个甚至多个Twemproxy节点同时提供服务,其上统一使用LVS服务进行负载分发。根据这样的描述,我们可以构建一种在生产环境下使用的Redis高性能集群方案:

Redis技术详解

上图中我们使用了两组Twemproxy节点,每一组都有两个Twemproxy节点在同一时间分别处于Active状态和Standby状态,在使用Keepalived组件进行状态监控和浮动IP切换。这四个Twemproxy节点的配合信息完全一样,保证了无论数据读写请求通过LVS到达哪一个Twemproxy节点,最终计算出来的目标Redis节点都是一样的。

但是以上方案还是有问题,就是单个Redis节点的高可用性无法保证。虽然在这样的Redis集群中,每一个活动的Redis节点在宕机后都可以被Twemproxy自动下线,造成的数据丢失情况也因为使用了一致性Hash算法而被限制到了一个可控制的范围。但是毕竟会丢失一部分数据,而且丢失的数据规模会和集群中Redis节点数量成反比关系。所以我们还需要在上一个集群方案的设计上再进行调整,加入我们在上一篇文章中介绍的Redis主从同步方案和Sentinel监控功能,形成第二种方案。

Redis技术详解

Twemproxy提供了一个配合使用的扩展组件:Redis_Twemproxy_Agent,它的作用是监控Sentinel中Master节点的情况,并且将最新的Master节点情况通知Twemproxy。这样一来当下层某组Redis高可用集群发生Master—Slave状态切换时,Twemproxy就会适时对其下层代理配置情况作出调整。

另外,上图中给出的第二种生产环境下的Redis集群方案,一共有5组独立运行的Redis高可用集群组,每组Redis高可用集群都有一个Master节点和至少一个Slave节点,它们之间使用Redis原生提供的数据复制功能保持数据同步。最后这些Redis高可用集群组通过一组Sentinel进行状态监控,而这组Sentinel也是同时拥有一个Master节点和两个Slave节点的高可用集群。

3、Redis高性能集群:Redis Cluster

3-1、Twemproxy的生产环境问题

可维护性上的问题:

LVS + Twemproxy + Keepalived + Redis + Sentinel + Sentinel Agent 的架构方案应该是笔者迄今为止介绍的层次最多,且每层组件最多的单一系统架构。Keepalived在LVS和Twemproxy都有使用,所以在不将Keepalived单独算作一层的情况下就是4层结构(这里说的层次都限于指本公司/机构的运维团队需要进行维护的系统组件)。而我们介绍过的Nginx集群方案是两层架构(LVS+Nginx),由于智能DNS路由一般是购买所以不参与计算;介绍过的ActiveMQ生产集群可以是三层架构(Zookeeper + ActiveMQ + LevelDB),也可以是两层架构(ActiveMQ + KahaBD/关系型数据库);介绍过的MySQL分库分表集群是三层架构(LVS + MyCAT + MySQL节点)。架构层次越多、每一层使用的组件越多,给运维团队带来的维护压力就越大,给生产环境带来的不稳定因素也越大。很显然从运维角度出发,为了解决单一功能而使用四层架构的情况是不太多见的——除非业务功能不能改变且系统架构层面又没有替代方案。

执行性能和设计思路问题:

Twemproxy并不是目前执行速度最快的Redis Proxy产品,例如豌豆荚在2014年开源的一款产品Codis就可以当做Twemproxy的替代方案。Codis对下层Redis节点的组织方式个人认为要优于Twemproxy,例如它将下层的Redis节点明确分为多个组,每个组中有一个Master和至少一个Slave节点,并且采用了类似随后要介绍的Redis Cluster那样的预分片方式(Slot),另外它还采用了ZK对各节点的工作状态进行协调。要知道Twemproxy虽然支持健康检查,也支持宕机节点的自动删除,但是Twemproxy并不支持数据转移。也就是说当某个Redis节点下线后,其上的数据也不会转移到其它节点上,而且Twemproxy中使用一致性Hash算法的基点或者取模运算所使用的基数也会发生变化。而如果引入的组的概念后,就可以减轻这个问题产生的风险,因为在一个组中的Master节点一旦出现问题,就会有Slave节点来接替它,而不会出现数据丢失问题。最后,根据豌豆荚自己的测试和广大网友自行测试的结果看,Codis对下层Redis节点的代理性能也要优于Twemproxy。

其它问题:

Redis的数据结构中,我们可以使用Set结构进行交并补运算。但是Twemproxy代理不支持这样的运算。另外Twemproxy也不对事务功能提供支持。

3-2、Redis原生的Cluster支持

可以说Twemproxy是早期Redis原生的Cluster没有成熟时的替代方案,而后Redis官方推荐的高性能集群方案还是基于其原生的Redis Cluster功能。Redis Cluster从Redis 3.0开始引入,实际上那个时候还是一个Bate版本,光放也不建议在生产环境下使用。但是到了目前最新的Version 3.2版本,Redis Cluster已经非常稳定了。

Redis技术详解

3-3、Redis Cluster基本配置示例

这里我们给出一个Redis Cluster的安装示例,首先介绍一下这个Redis Cluster的配置示例要达到的部署效果,这样才便于各位读者继续阅读。在这个示例场景中我们有两台物理机,每台物理机上启动了三个Redis节点,一共六个节点,并让它们按照Cluster模式工作起来。如下表所示:

Redis技术详解

请注意,在生产环境中笔者并不建议在一台物理机上/虚拟机上部署多个Redis节点,因为这样大大增加了多个Redis节点同时不可用的风险,但这是示例场景所以无所谓啦。

3-3-1、Redis安装和配置文件部署

由于有六个节点参与到集群中,所以我们需要准备六份不同的配置文件。读者可以将这6个文件存放到不同的文件夹下:

========== 192.168.61.145:6379 ==========
######### NETWORK #########
bind 192.168.61.145
port 6379
######### GENERAL #########
pidfile "/var/run/redis_6379.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.145_6379
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes
========== 192.168.61.145:6380 ==========
######### NETWORK #########
bind 192.168.61.145
port 6380
######### GENERAL #########
pidfile "/var/run/redis_6380.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.145_6380
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes
========== 192.168.61.145:6381 ==========
######### NETWORK #########
bind 192.168.61.145
port 6381
######### GENERAL #########
pidfile "/var/run/redis_6381.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.145_6381
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes
========== 192.168.61.140:6379 ==========
######### NETWORK #########
bind 192.168.61.140
port 6379
######### GENERAL #########
pidfile "/var/run/redis_6379.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.140_6379
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes
========== 192.168.61.140:6380 ==========
######### NETWORK #########
bind 192.168.61.140
port 6380
######### GENERAL #########
pidfile "/var/run/redis_6380.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.140_6380
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes
========== 192.168.61.140:6381 ==========
######### NETWORK #########
bind 192.168.61.140
port 6381
######### GENERAL #########
pidfile "/var/run/redis_6381.pid"
######### REDIS CLUSTER #########
cluster-enabled yes
cluster-config-file nodes.140_6381
cluster-node-timeout 15000
######### APPEND ONLY MODE #########
appendonly yes

以上只是列举了要参与Redis Cluster的六个节点中和本节内容相关的重点配置项,包括网络配置、一般性配置和集群部分的配置。其它的配置项可以根据读者所处技术环境的自行决定,例如是否开启主动SNAPSHOTTING的策略问题,因为Cluster中的Master都会有一个或者多个Slave节点,所以基本上一组高可用集群的数据不会同时丢失,而Master和Slave间的数据同步还是依靠Redis原生的主从同步方案完成的,所以Redis Master节点还是会做被动作SNAPSHOTTING动作。以下是六个节点的启动命令:

// 启动145上的三个redis节点
# redis-server ./redis.conf.145_6379 &
# redis-server ./redis.conf.145_6380 &
# redis-server ./redis.conf.145_6381 &
// 启动140上的三个redis节点
# redis-server ./redis.conf.140_6379 &
# redis-server ./redis.conf.140_6380 &
# redis-server ./redis.conf.140_6381 &

以上启动命令和您放置配置文件具体位置有关、和您是否设定了环境变量有关,还和您准备如何查看命令执行日志有关。所以具体执行参数肯定是有差异的。请注意,在第一次单独启动某个Redis节点时,您可能会看到类似以下的提示:

//============= redis.conf.145_6380节点
......
18449:M 29 Dec 18:32:52.036 # I have keys for unassigned slot 95. Taking responsibility for it.
18449:M 29 Dec 18:32:52.036 # I have keys for unassigned slot 219. Taking responsibility for it.
18449:M 29 Dec 18:32:52.038 # I have keys for unassigned slot 641. Taking responsibility for it.
......
//============= redis.conf.145_6381节点
......
9582:M 29 Dec 18:43:49.048 # I have keys for unassigned slot 95. Taking responsibility for it.
9582:M 29 Dec 18:43:49.048 # I have keys for unassigned slot 219. Taking responsibility for it.
9582:M 29 Dec 18:43:49.048 # I have keys for unassigned slot 641. Taking responsibility for it.
......

这是因为Redis启动时,会自动创建技术人员在cluster-config-file配置项设定的集群配置文件,例如nodes.140_6380、nodes.140_6381这些文件,并且会默认托管一些slots。但细心的读者可以发现,这六个节点独立启动时默认托管的slots信息都是一样的。这是因为这些节点还没有建立通讯机制,并不能协调slot的管理信息。而且这些cluster-config-file中都会默认自身节点是一个Master节点。

经过以上过程我们启动了六个节点,但是到目前为止这六个节点还是独立工作的并没有形成集群。这是因为各个cluster-config-file中并没有明确协调哪些节点将成为Master节点,哪些节点将成为Slave节点并且他们的主从映射关系,也没有协调任何和节点发现有关的信息,同样也没有协调各个节点的ID信息或者节点所映射的Master的ID信息,更没有协调各个节点分别负责的slot信息。那么以上这些协调动作都是通过下一个操作步骤。

Redis技术详解

(上图说明了各个Redis节点单独启动后的Redis Cluster状态)

3-3-2、Ruby安装和集群命令运行

Redis Cluster通过运行一个Ruby脚本进行初始化和启动,如果您的操作系统还没有安装Ruby,请进行安装(以下示例的安装命令适用于CentOS):

# yum install -y ruby rubygems
......
# gem install redis
Successfully installed redis-3.3.2
1 gem installed
Installing ri documentation for redis-3.3.2...
Installing RDoc documentation for redis-3.3.2...
......

在Redis的源文件目录的src目录中,有一个Ruby脚本文件“redis-trib.rb”,通过运行这个脚本文件可以完成Redis Cluster的初始化和启动操作。如果各位读者希望以后都能方便的运行这个脚本文件,可以先将这个脚本文件Copy到Redis的执行目录下:

# cp 你的源码路径/redis-trib.rb /usr/local/bin/redis-trib.rb
//或者
# cp 你的源码路径/redis-trib.rb /usr/redis/bin/redis-trib.rb
......

接下来就可以运行这个脚本了:

# redis-trib.rb create --replicas 1 192.168.61.140:6379 192.168.61.140:6380 192.168.61.140:6381 192.168.61.145:6379 192.168.61.145:6380 192.168.61.145:6381
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.61.145:6379
192.168.61.140:6379
192.168.61.145:6380
Adding replica 192.168.61.140:6380 to 192.168.61.145:6379
Adding replica 192.168.61.145:6381 to 192.168.61.140:6379
Adding replica 192.168.61.140:6381 to 192.168.61.145:6380
M: 1cf10fb6d7c0ad4d936b1c061a99d370bda07757 192.168.61.140:6379
slots:5461-10922 (5462 slots) master
S: 8749db7b6a5860be63f592e94388239a7467cbb1 192.168.61.140:6380
replicates 3ee2a9f173ccbee3a5a79b082af2910be7d22e57
S: 33f9ee49963a32220984122278105cdda7761517 192.168.61.140:6381
replicates 120bc340ed1b24ba8e07368cf18d433094644e6e
M: 3ee2a9f173ccbee3a5a79b082af2910be7d22e57 192.168.61.145:6379
slots:0-5460 (5461 slots) master
M: 120bc340ed1b24ba8e07368cf18d433094644e6e 192.168.61.145:6380
slots:10923-16383 (5461 slots) master
S: 0b107150f7c075fe7ba701b64a9f7bf9f7896ead 192.168.61.145:6381
replicates 1cf10fb6d7c0ad4d936b1c061a99d370bda07757
Can I set the above configuration? (type 'yes' to accept): yes

以上命令中,create参数代表创建一个新的新的Redis Cluster,然后我们后给出了一个replicas参数,这个参数代表集群中的每一个Master节点对应多少个Slave节点,这里给出的数值是1,就代表每一个Master节点会对应一个Slave节点。需要注意,这里并不需要明确指定哪些节点将成为Master节点,哪些节点将成为Slave节点,而redis-trib会参考replicas参数的值自行计算得出。在命令的最后我们还给出了参与这个新的Redis Cluster的所有Redis节点的信息。

redis-trib会根据以上这些参数预计一个可能的配置信息,特别是初始化的Master和Slave节点的预计情况、每个节点的ID编号以及每个Master节点负责的Slot。接着redis-trib会将这份报告呈现给技术人员,由后者最终确定是否执行初始化。输入“yes”,redis-trib就将按照这份计划执行Redis Cluster的创建工作了:

......
>>> Performing Cluster Check (using node 192.168.61.140:6379)
M: 1cf10fb6d7c0ad4d936b1c061a99d370bda07757 192.168.61.140:6379
slots:5461-10922 (5462 slots) master
1 additional replica(s)
S: 33f9ee49963a32220984122278105cdda7761517 192.168.61.140:6381
slots: (0 slots) slave
replicates 120bc340ed1b24ba8e07368cf18d433094644e6e
S: 0b107150f7c075fe7ba701b64a9f7bf9f7896ead 192.168.61.145:6381
slots: (0 slots) slave
replicates 1cf10fb6d7c0ad4d936b1c061a99d370bda07757
M: 3ee2a9f173ccbee3a5a79b082af2910be7d22e57 192.168.61.145:6379
slots:0-5460 (5461 slots) master
1 additional replica(s)
S: 8749db7b6a5860be63f592e94388239a7467cbb1 192.168.61.140:6380
slots: (0 slots) slave
replicates 3ee2a9f173ccbee3a5a79b082af2910be7d22e57
M: 120bc340ed1b24ba8e07368cf18d433094644e6e 192.168.61.145:6380
slots:10923-16383 (5461 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

到此为止6个节点的Redis Cluster就创建完成了,除了初始化创建Redis Cluster外,您还可以参考官方网站上的介绍完成Redis Cluster中的节点新增、删除或者其它操作:https://redis.io/topics/cluster-tutorial。

3-3-3、使用客户端进行连接

客户端进行集群环境的连接,就是一个更简单的工作了。实际上Redis的客户端并不需要连接到Redis Cluster中的所有节点,就可以完整操作Redis Cluster中的数据。这是因为每个Redis Cluster中的节点都清楚整个集群的全局情况,特别是Slot存在的位置。以下示例代码展示了如何通过Java代码连接到Redis Cluster:

......
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(10);
config.setMaxIdle(2);
// 这里添加集群节点。可以添加多个节点,但并不是需要添加Cluster的所有节点
HostAndPort node0 = new HostAndPort("192.168.61.140", 6379);
HostAndPort node1 = new HostAndPort("192.168.61.145", 6379);
Set nodes = new HashSet();
nodes.add(node0);
nodes.add(node1);
// 创建和连接到集群
JedisCluster jedisCluster = new JedisCluster(nodes, 5000, 10, config);
//==============================
// 做你要做的Redis操作吧,少年
//==============================
jedisCluster.close();
......

————————————————

版权声明:本文为CSDN博主「说好不能打脸」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/yinwenjie/article/details/53905637

展开阅读全文

页面更新:2024-05-12

标签:快照   节点   详解   客户端   命令   参数   事件   操作   功能   数据   技术   信息

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top