Chapter 8. Scaling Redis (Beyond a Single Instance)
精彩的总在最后,终于来到这一章了!
本章讲述通过多实例来进行水平扩展,重要的概念有:persistence, replication, partitioning。
Persistence
内存是临时的, 易失性存储, 为防止数据丢失, Redis提供了两种persistence的方法:Redis Database (RDB) 和 Append-only File (AOF),两种方法可以单独使用也可结合使用。
由于persistence是Redis概念中最容易误解的部分,在2012年3月26日,Salvatore Sanfilippo发表了一片非常精彩的博文,参见:
Redis persistence demystified
我看了这篇文章后,觉得写得还是挺深的,其实我更推荐另一篇:http://redis.io/topics/persistence,这里有AOF和RDB的比较。
RDB (Redis Database)
.rdb二进制文件是Redis实例中数据的一个时间点快照(snapshot或savepoint),格式被优化用来读和写,格式与内存中的表示形式非常相似。
另外,RDB可以使用LZF进行压缩,LZF是一种非常高效的压缩算法,压缩时只需很小的内存,尽管压缩效率并非最佳,但对于Redis已经很好了。同时,单个RDB文件可以完整的恢复Redis实例。
RDB可以很好的用于灾备和备份,你可以根据需要,每月,每周,每天或每小时来生产RDB文件。然后可以很简单的进行恢复(恢复到任何时间点?)。
SAVE命令可以生成RDB,但应避免使用,因为在生成快照的过程中会阻塞Redis服务器。推荐使用BGSAVE(后台SAVE),效果和SAVE一样,但是通过一个子进程运行,不会阻塞Redis。
为了避免对性能的影响,BGSAVE利用redis-server fork出的一个子进程来来做所有的persistence I/O操作,而父进程不做任何I/O操作。
在fork的过程中,如果Redis的数据发生变化,子进程需要利用copy-on-write的技术将变化量存入内存中,改变越大,需要的内存越多。不过最多就和主进程一样了。按照以前存储的概念,通常变化量是20%。
从使用copy-on-write技术也可看出,SAVE/BGSAVE生成的是一个时间点快照。
通常,保存RDB的频率不要低于30秒。
快照可以禁止,通过删除配置文件的save语句,然后重启Redis,或通过CONFIG SET设置
RDB并不能100%保证恢复成功,如果在两个时间点备份间, Redis发生故障,最新的数据就会丢失。
RDB的另一个不足是产生快照会影响性能,有时会阻塞服务从毫秒到几秒。
以下是配置文件中和RDB有关的指令:
stop-writes-on-bgsave-error
rdbcompression: 启用LZF压缩
rdbchecksum: 缺省是yes
dbfilename: RDB文件名,缺省为dump.rdb
save: 产生快照的频率。具体说明如下(缺省配置)
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
缺省情况下,SAVE/BGSAVE产生的RDB文件会覆盖已有的文件。验证如下:
[redis@tt12c redis-3.0.7]$ ll dump.rdb
-rw-rw-r-- 1 redis redis 473 Apr 27 22:45 dump.rdb
[redis@tt12c redis-3.0.7]$ redis-cli save
OK
[redis@tt12c redis-3.0.7]$ ll dump.rdb
-rw-rw-r-- 1 redis redis 473 May 6 14:03 dump.rdb
[redis@tt12c redis-3.0.7]$ cksum dump.rdb
855035226 473 dump.rdb
[redis@tt12c redis-3.0.7]$ redis-cli save
OK
[redis@tt12c redis-3.0.7]$ cksum dump.rdb
855035226 473 dump.rdb
[redis@tt12c redis-3.0.7]$ ll dump.rdb
-rw-rw-r-- 1 redis redis 473 May 6 14:04 dump.rdb
同时可以看到,此文件和内存大小无关,和数据大小有关,验证如下:
[redis@tt12c redis-3.0.7]$ redis-cli zadd redis-essentials 10 good
(integer) 1
[redis@tt12c redis-3.0.7]$ redis-cli save
OK
[redis@tt12c redis-3.0.7]$ strings dump.rdb
REDIS0006
article:12345:votes
books
Clean Code
article:10001:votes
article:10001:headline0For Millennials, the End of the TV Viewing Party
article:12345:headline1Google Wants to Turn Your Clothes Into a Computer
my_key#Hello World using Node.js and Redis
article:60056:votes
first
First Key value
redis-essentials
good
city
beijing
article:60056:headline@AAlicia Vikander, Who Portrayed Denmark's Queen, Is Screen Royalty second Second Key value
AOF (Append-only File)
AOF启用后,每次Redis的数据发生改变时,都会讲产生修改的命令最佳到AOF文件尾部。因此恢复时只需将所有的命令按序执行即可。Redis认为只有AOF才是真正的“fully durable approach”。
AOF记录的是命令,是一个可直接读的文本文件。而RDB是二进制文件。即使AOF发生损坏。也可以通过一个工具:redis-check-aof 进行修复。
配置文件中与AOF相关的指令包括:
appendonly: 是否启动AOF,缺省为No
appendfilename: AOF文件名
appendfsync: fsync通知OS调用fsync()将缓存中数据写盘。本参数指定是否执行fsync和fsync的频率,缺省是1秒1次。也可设置为No或Always
no-appendfsync-on-rewrite
auto-aof-rewrite-percentage
auto-aof-rewrite-min-size
aof-load-truncated
dir
RDB versus AOF
当数据库较大时,RDB恢复比AOF快,因无需重新执行指令。
RDB和AOF可以结合一起使用。
Redis在启动时会加载RDB或AOF文件,如果这两个文件都存在,则只会用AOF,因为其可保证完整恢复。
以下是一些Redis持久化的建议:
* 如果应用无需持久化,则禁止RDB和AOF
* 如果应用可接受部分数据丢失,使用RDB
* 如果应用需要完整的恢复,同时使用RDB和AOF
还是看一下redis.conf的说法吧:
############################## APPEND ONLY MODE ###############################
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.
这里也可以看到,即使是AOF,使用每秒fsync一次,还是可能导致1秒数据的丢失。毕竟persistence不是复制。如果使用appendfsync always,对性能影响又比较大。
可以用redis-server –appendonly yes 启动server,然后在client中执行一些写命令,如set,然后就可以看到AOF文件了。
Replication
复制发生在master和slave(replica)之间,当master改变时,会传递变化到一个或多个slave,最终slave会和master一致。
Redis 2.8开始支持异步复制,slave会定期的确认master传递过来的变化。Redis的复制支持1:N以及级联。
有3中方式使Redis Server成为slave,分别是redis.conf, 启动命令选项,redis-cli命令,都是采用订阅master的方式(SLAVEOF IP PORT)
来看一个例子,这例子设计的非常好。3个instance,1个master对应两个replica。
第1个终端:
$ redis-server --port 5555
第2个终端:
$ redis-server --port 6666 --slaveof 127.0.0.1 5555
第3个终端:
$ redis-server --port 7777 --slaveof 127.0.0.1 5555
At this point, there is a master with two replicas running.
第4个终端:
$ redis-cli -p 5555 SET testkey testvalue
OK
$ redis-cli -p 6666 GET testkey
"testvalue"
$ redis-cli -p 7777 GET testkey
"testvalue"
复制广泛的用于提高扩展性,这样所有的replica可以处理读,而master只需处理写。数据冗余是另一个使用replica的理由。
也可以把持久化的工作在replica端来做,以减轻master的I/O压力,在这种场景,master必须禁止掉persistence,而且它不能自动重启,否则会将一个空的Redis复制到对方,导致所有数据丢失。
Replica缺省是只读的。可以改成可写,但不推荐。
通过增加replica的数量同时保证响应在指定时间内,可以保证数据一致性,指令为min-slaves-to-write和min-slaves-max-lag—they。
在master失效时,由于replica包含最近的数据,可以升级为master。
不幸的是,当Redis单实例运行时,不支持自动的failover,所有连接到老的master的replica和client都需要重新配置。 自动failover的功能即Redis Sentinel会在下一章介绍。
SLAVEOF NO ONE命令将slave变为master。
来看一个例子,还是一个master对应2个slave,如master失效,其中一个slave升级为master:
关键的命令如下:
$ redis-cli -p 5555 DEBUG SEGFAULT // 使master crash
$ redis-cli -p 6666 SLAVEOF NO ONE // promote slave
$ redis-cli -p 7777 SLAVEOF 127.0.0.1 6666 // change slave relationship
# 验证
$ redis-cli -p 6666 SET newkey newvalue
OK
$ redis-cli -p 7777 GET newkey
"newvalue"
所有的客户端需要重新连接到127.0.0.1:6666.
Partitioning
Partitioning(分区) 将数据分布到不同的主机上。有两种形式: horizontal partitioning 和 vertical partitioning。分区可以提供更好的性能,可用性和可维护性。
Redis最初设计可以很好的运行在单机上,没有考虑数据分布,后来才有了Redis Cluster的概念。
随时间推移,单机已无法满足数据量,性能的需求。replica可以减轻master的压力,但这还不够,不同的需求需要不同的解决方法,例如
* 单机内存无法容纳所有数据
* 单机带宽无法满足吞吐量需求
We will show some solutions to the problems listed in the preceding list using the concept of partitioning. We also will show some client-level implementations of partitioning (in single-instance mode, Redis does not support partitioning or clustering), and later, we will see how it can be done with a proxy or a query routing system. Redis Cluster will be explained in detail in Chapter 9, Redis Cluster and Redis Sentinel (Collective Intelligence).
水平分区(也称为sharding)表示将数据key分布到不同的实例。垂直分区表示将value分布到不同的实例。水平分区对于Redis最常用。
Range partitioning
范围分区是根据key所处的范围(区间)来分布数据。例如userID为1-1000的在发送到某一实例。或者根据邮箱,姓名的首字母来做分区。
此分区方式可能会导致数据分布不均衡,从而使负载不均衡。
另一个不足是不能很好的适应变更,例如增加了Redis实例后,相应的分区策略需要改变,数据需要移动。
来看一个用首字母分区的例子, 其实分区是通过编程实现的,Redis并没有自带的分区命令。关键代码如下:
var possibleValues = '0123456789abcdefghijklmnopqrstuvwxyz';
var rangeSize = possibleValues.length / this.clients.length; <- Redis 实例个数
Hash partitioning
Hash通过计算key的hash值,然后用余数来确定分区,Hash分区没有范围分区数据不均匀的缺点。常用hash算法有 MD5 和 SHA1
示意代码如下:
var index = hashFunction(redisKey) % redisHosts.length;
var host = redisHosts[index];
实例的个数建议是素数(质数)以避免hash冲突
同范围分区一样,增加实例的个数,会导致应用的修改,数据需要重新分布。
Presharding
为了避免前面两种方法添加实例带来的影响,可以预先将实例的个数设大些,然后让实例运行于不同的端口,之后实例的总数不允许改变。这个方法可行,因为Redis是单线程,一个机器的资源通常使用不完。
之后,如果要扩容,并非靠增加实例,而是替换为更大的服务器。
例如,我们有三台主机,每主机5个实例。
var redisHosts = [
'server1:6379',
'server1:6380',
'server1:6381',
'server1:6382',
'server1:6383',
'server2:6379',
'server2:6380',
'server2:6381',
'server2:6382',
'server2:6383',
'server3:6379',
'server3:6380',
'server3:6381',
'server3:6382',
'server3:6383',
];
若server3能力不够,可以用更大的服务器server4替代。步骤如下:
1. 在server4上启动5个实例,每个实例都是server3实例的slave
2. 数据同步后,在redisHosts中将server3替换为server4
3. 停止连接到server3的应用,将server4升级为master
4. 重启之前停掉的应用
5. 关闭server3
如果不能接受应用中断,可以将slave设置为可写,然后让应用连接新的server4上的实例,然后再升级成master
presharding不能很好的应对灾难的情况,特别是新的server不能尽快替代旧server的时候。
另一个不足是你必须管理和监控较多的实例,但目前有没有合适的工具来做这件事。这时我们可以使用接下来介绍的consistent hashing方法。
Consistent hashing
前述的hash算法的一大弊病就是当server增加和减少(例如服务器故障)时,几乎所有的key都需要remapping(迁移)。我们解释一下原因:
key 映射到 server采用公式: h(key) mod n,h表示hash算法,例如有100台主机,如果增加1台,h(key) mod 100 和 h(key) mod 101的可能性非常小,这时,几乎所有的数据都需要remapping,这对应用几乎是灾难。
Consistent hashing的目的就是为了尽量减少key的移动,实现的效果是当添加节点时,需要remapping的key最多只有total keys/total servers。
算法的关键是不仅对key做hash,也对server做hash,详见 Stanford的CS168课程:Introduction and Consistent Hashing,这篇讲义非常好,浅显易懂。
Tagging
SDIFF, SINTER, SUNION集合操作都要求key存储在同一实例上,而tagging的作用即可保证相同的key保存在相同的实例上,原理就是为key添加prefix和/或suffix,然后基于prefix和/或suffix做hash确定server。
Redis惯例是将prefix和suffix用{}括起,例如,以下的key的hash值相同:
{prefix}user:1{suffix}
{prefix}user:2{suffix}
{prefix}user:3{suffix}
Data store versus cache
partition是扩展Redis的重要方法,我们建议使用consistent hashing作为最佳的分区机制,因为当server数目变化时,可以最少的remapping。
Redis作为data store时,key必须总是映射到相同的Redis实例,这时可以使用tagging技术,不过这也意味着Redis server的列表不能改变,否则key就会map到其它的server。解决方法之一是通过复制产生副本,然后再通过Redis服务器做路由,后面的Redis Cluster会涉及到。
Redis作为cache时,建议使用consistent hashing来减少cache miss(其实和key remapping是一个意思,反复提cache miss是因为consistent hashing最开始就是为解决web cache的扩展产生的,如Akamai)。
Implementations of Redis partitioning
分区可以在不同的层次实现,
client: 即应用层,就像前面的例子
proxy layer:额外的一层,处理所有的Redis请求和为应用做分区。就像后面会介绍的twemproxy程序。
query route: 对于应用不可见,属于data store的一部分,接收到请求的Redis实例会将请求路由到相应的实例,例如Redis Cluster、
在使用分区时,有些命令就没有意义了,例如多key的处理,因为这时key可能分布在不同的实例中。
Automatic sharding with twemproxy
本节介绍一个Twitter在2012年发布的工具twemproxy,又称为nutcracker。
twemproxy是一个轻型的工具,可以作为Redis的proxy,并实现hash,presharding和consistent hashing等算法,还有其它一些功能,包括方便的水平扩展Redis等。 它被用于Pinterest, Tumblr, Twitter, Vine, Wikimedia, Digg, Snapchat的生产系统中。
twemproxy的简介可以参见官网:
twemproxy (pronounced “two-em-proxy”), aka nutcracker is a fast and lightweight proxy for memcached and redis protocol. It was built primarily to reduce the number of connections to the caching servers on the backend. This, together with protocol pipelining and sharding enables you to horizontally scale your distributed caching architecture.
本节演示如何编译,配置和使用twemproxy连接3个Redis实例,并且做读写分布。3个实例运行在一台主机上,当然也可以运行在Amazon EC2上。
启动3个Redis实例,在后台运行:
$ redis-server --port 6666 --daemonize yes
$ redis-server --port 7777 --daemonize yes
$ redis-server --port 8888 --daemonize yes
启动3个客户端连接到3个实例,每个客户端都运行monitor命令
$ redis-cli -p 6666 monitor
$ redis-cli -p 7777 monitor
$ redis-cli -p 8888 monitor
接下来从 https://github.com/twitter/twemproxy 下载和安装twemproxy
$ tar -zxf nutcracker-0.4.1.tar.gz
$ cd nutcracker-0.4.1
$ ./configure
$ make
运行以下命令验证安装是否成功
[redis@tt12c nutcracker-0.4.1]$ ./src/nutcracker --help
This is nutcracker-0.4.1
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]
Options:
-h, --help : this help
-V, --version : show version and exit
-t, --test-conf : test configuration for syntax errors and exit
-d, --daemonize : run as a daemon
-D, --describe-stats : print stats description and exit
-v, --verbose=N : set logging level (default: 5, min: 0, max: 11)
-o, --output=S : set logging file (default: stderr)
-c, --conf-file=S : set configuration file (default: conf/nutcracker.yml)
-s, --stats-port=N : set stats monitoring port (default: 22222)
-a, --stats-addr=S : set stats monitoring ip (default: 0.0.0.0)
-i, --stats-interval=N : set stats aggregation interval in msec (default: 30000 msec)
-p, --pid-file=S : set pid file (default: off)
-m, --mbuf-size=N : set size of mbuf chunk in bytes (default: 16384 bytes)
接下来创建配置文件,twemproxy配置文件的格式是YAML(“YAML Ain’t Markup Language”),配置文件中含twemproxy需要管理的Redis实例信息,以及hash模式和分布式模式的设置。
这个配置文件以及存放在第八章的目录中了:
[redis@tt12c chapter 8]$ cat twemproxy_redis.yml
my_cluster:
listen: 127.0.0.1:22121 <- twemproxy cluster的地址,Redis客户端需要连接到这个地址。
hash: md5 <- hash模式
distribution: ketama <- 分布式模式, ketama就是consistent hashing
auto_eject_hosts: true
redis: true <- 使用Redis还是Memcached
servers: <- 服务器列表,格式为IP:PORT:WEIGHT NAME
- 127.0.0.1:6666:1 server1
- 127.0.0.1:7777:1 server2
- 127.0.0.1:8888:1 server3
如果将nutcracker部署到生产环境,推荐阅读:https://github.com/twitter/twemproxy/blob/master/notes/recommendation.md
运行nutcracker:
$ ./src/nutcracker -c twemproxy_redis.yml
创建一个应用,在Chapter 8下面,twemproxy.js。此应用连接的是twemproxy(按端口号),然后写26个子母,这些子母既是key也是value
cat twemproxy.js
var redis = require('redis');
var options = { // 1
"no_ready_check": true,
};
var client = redis.createClient(22121, 'localhost', options); // 2
var alphabet = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
]; // 3
alphabet.forEach(function(letter) { // 4
client.set(letter, letter); // 5
});
client.quit(); // 6
执行此文件:node twemproxy.js
然后通过输出,验证了twemproxy可以自动做sharding。 (可以做一个添加server的实验,试着改一下yml配置文件,然后让客户端通过proxy来get)
为了验证consistent partition,我们又增加了一个Redis实例在9999端运行,然后修改配置文件,重启twemproxy。然后看key remapping的效果
$ redis-cli -p 6666 flushall
$ redis-cli -p 7777 flushall
$ redis-cli -p 8888 flushall
$ redis-server --port 9999 --daemonize yes
# 修改twemproxy的配置文件如下
$ cat twemproxy_redis.yml
my_cluster:
listen: 127.0.0.1:22121
hash: md5
distribution: ketama
auto_eject_hosts: true
redis: true
servers:
- 127.0.0.1:6666:1 server1
- 127.0.0.1:7777:1 server2
- 127.0.0.1:8888:1 server3
- 127.0.0.1:9999:1 server4
# 重启twemproxy
kill -9 pid_of_nutcracker
$ nutcracker -c twemproxy_redis.yml
$ node twemproxy.js
[redis@tt12c chapter 8]$ for i in 6666 7777 8888 9999 ; do echo keys in server $i; redis-cli -p $i keys \*; done
keys in server 6666
1) "N"
2) "V"
3) "T"
4) "J"
5) "L"
6) "I"
keys in server 7777
1) "U"
2) "P"
3) "K"
4) "E"
5) "A"
6) "C"
7) "B"
8) "F"
9) "W"
10) "Y"
11) "O"
12) "Q"
keys in server 8888
1) "Z"
2) "X"
3) "R"
4) "S"
keys in server 9999
1) "G" <- 以前在 8888
2) "M" <- 以前在 8888
3) "D" <- 以前在 7777
4) "H" <- 以前在 6666
对比上一个实验的结果,发现只有4个做了remapping,比例为4/26,效果还是不错的。
又追加了一个实验,验证当架构发生变化时,twemproxy只做sharding,不会做key的remapping。
此实验在第一个实验的基础上继续做,即key自动sharding到3个Redis实例,然后增加一个实例,重启nutcracker。用客户端连接nutcracker去读取key:G,M,D, H。
[redis@tt12c chapter 8]$ cat remapping.js
var redis = require('redis');
var options = {
"no_ready_check": true,
};
var client = redis.createClient(22121, 'localhost', options);
client.get("A", redis.print);
client.get("G", redis.print);
client.get("M", redis.print);
client.get("D", redis.print);
client.get("H", redis.print);
client.quit();
运行此程序,发现取A可以,去G,M,D,H则返回空。原因是根据hash模式,GMDH需要到新的Redis实例上去取,而nutcracker不会自动将key迁移,需要人工来做
[redis@tt12c chapter 8]$ node reshard.js
Reply: A
Reply: null
Reply: null
Reply: null
Reply: null
Other architectures that use twemproxy
前述架构中,twemproxy是单点故障。为此可以添加多个twemproxy,每一个twemproxy都可以连接到后端所有Redis服务器。然后在twemproxy之前架设负载均衡器。
Summary
本章介绍了持久化(AOF和RDB), 复制(异步,主从模式,支持一对多,级联),分区方法(hash,presharding, consistent hashing)。同时介绍了一个自动sharding的工具:twemproxy。