存档

作者存档

分布式redis架构设计简介

2020年5月18日 没有评论

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

线上服务qps 2万/s,即每分钟12万次请求,吞吐量是相当大的,存储是线上服务稳定核心,用好redis十分重要。业务上redis作为主要存储,深入了解他有助于更好的使用,出现一些问题可以有深入一些的理解以及方便和存储维护方沟通。了解了存储才能用合理应用,对业务进行合理规划,在出现问题时也能从容进行应对。

redis架构设计特点:

1、redis由非阻塞单线程模式实现。编程简洁不易出bug,而且qps不低,可以说是极高,另一个无阻塞单线程是nginx,所以说单线程不一定性能差,关键是要看怎样合理使用,设计架构。非阻塞io各个操作系统下

2、命令式设计,客户端与服务端交互采用命令式进行交互,客户端发送命令到服务端,服务端接收命令后根据命令类型优先级进行相应解析处理。

3、单线程设计,大的数据请求会阻塞后续请求,以致后续整体tp99都会降低使用时需要注意。

4、超时数据会单独存储,采用定时过期以及访问时过期两种方式。定时过期是随机随段进行过期,非全部遍历进行过期处理。访问时过期,访问数据时,入数据过期进行过期处理。

5、hash数据结构,hash是kv存储基石,时间复杂度o(n),高效数据结构,快速读写都是通过hash数据机构作为基础实现。

6、当redis进行扩容或者缩容时,rehash渐进的对原有数据ht[0]生成到新的ht[1]面,当复制完成后。ht[0]销毁,ht[1]变为ht[0],完成扩容或者缩容过程。

redis分布式相关:

1、codis集群方案通过codis-proxy代理来管理redis分片,通过代理方式能实现无需客户端更新的升级扩容,以及有问题节点替换处理。通过zookeeper来存储集群元信息。

2、redis3.0 提供redis cluster集群方案,官方集群方案采用p2p形式。每个节点存储其他节点信息,当数据请求不在节点时,节点返回数据所在节点,通过两次请求完成数据获取。架构好处是非中心架构,通过grossip协议来同步获取集群信息。缺点是集群分片节点过多(比如分片多于600),会导致集群节点间通信过多导致集群性能下降。

3、单机不能处理超越内存数据量问题,分布式集群方式解决。分布式有cluster和集中存储元数据两种方式。redis分布式一个重要概念是slot,由槽组成分片,多个分片构成集群。

4、集群监控,内存、连接数、超时时间,这是管理平台设计时需要考虑的,通过配置平台实现配置下发以及集群状态管理。

5、存储系统,当下设计重点热点处理,自动扩容。实际使用时避免单点,热升级,热点分裂。当访问量或数据减少后为了节省资源,增加资源利用率要进行所容。

redis使用时需要注意和避免的问题:

1、限制key长度特别是在lrange命令下,数据条数过多会阻塞redis访问。导致其他命令阻塞导致tp99上升。

2、限制单个value大小,业务使用存储一个值有几兆大,访问频率低没有问题,当新上key key访问次数加大,相应接口tp99由毫秒到几秒。

3、单个key避免读写热电,写热点包含单个key长度写的特别大,再有就是进行访问计数器计算计算写的特别频繁也是热点。

4、读的分片热点可以通过动态扩容解决,但单个key热点只能增加slave个数并且客户端增加随机访问master以及slave来分散访问热点,再有就是热key通过本地缓存来避免redis热点数据。会导致tp99升高,双11压测线上服务全线性能不行,是热点key。

5、一次redis整个集群吞吐量下降,mGet了太多数据,618前扩容成为压垮redis的最后一根稻草,过多key拉取会阻塞redis集群。业务涉及要尽量避免过多mGet批量获取。

https://mp.weixin.qq.com/s/YkygrSjCUJOSOAktdS81aQ

分类: redis头条 标签:

如何优雅的分析 Redis 里存了啥?

2020年5月18日 没有评论

Redis 是互联网产品开发中不可缺少的常备武器,它性能高、数据结构丰富、简单易用,但同时也是因为太容易用了,我们的开发同学不管什么数据、不管这数据有多大、不管数据有多少通通塞进去,最后导致的问题就是 Redis 内存使用持续上升,但是又不知道里面的数据是不是有用,是否可以拆分和清理。

为了更好地使用 Redis,除了对 Redis 做一些使用规范,还需要对线上使用的 Redis 有充分的了解。那么问题来了:一个 Redis 的实例用了那么大的内存,里边到底存了啥?都有哪些 key?每个 key 占用了多少空间?

雪球当前有几十个 Redis 集群,近千个 Redis 实例,5T 的内存数据,我们也想要分析业务是否有误用,以提高资源利用率。当然,曾经我们也深深地被这个问题所困扰,今天我就来和大家分享下我是如何解决这个问题的,希望能给各位一些启发。

那有没有什么办法让我们安全高效的看到 Redis 内存消耗的详细报表呢?办法总比问题多,有需求就有解决方案。雪球 SRE 团队针对这个问题实现了一个 Redis 数据可视化平台 RDR (Redis Data Reveal)。RDR 可以非常方便的对 Reids 的内存进行分析,了解一个 Redis 实例里都有哪些 key,哪类 key 占用的空间是多少,最耗内存的 key 有哪些,占比如何,非常直观。

设计思路

我们先梳理下,有什么办法可以拿到 Redis 的所有数据。从我的角度看,大概有以下几种方法,我们分析一下个字的优缺点:

1. 先通过 keys * 命令,拿到所有的 key,然后根据 key 再获取所有的内容。

  • 优点:可以不使用 Redis 机器的硬盘,直接网络传输
  • 缺点:如果 key 数量特别多,keys 命令可能会导致 Redis 卡住影响业务;需要对 Redis 请求非常多次,资源消耗多;遍历数据太慢

2. 开启 aof,通过 aof 文件获取到所有数据。

  • 优点:无需影响 Redis 服务,完全离线操作,足够安全;
  • 缺点:有一些 Redis 实例写入频繁,不适合开启 aof,普适性不强;aof 文件有可能特别大,传输、解析起来太慢,效率低。

3. 使用 bgsave,获取 rdb 文件,解析后获取数据。

  • 优点:机制成熟,可靠性好;文件相对小,传输、解析效率高;
  • 缺点:bgsave 虽然会 fork 子进程,但还是有可能导致主进程卡住一段时间,对业务有产生影响的风险;

以上几种方式我们评估之后,决定采用低峰期在从节点做 bgsave 获取 rdb 文件,相对安全可靠,也可以覆盖所有业务的 Redis 集群。也就是说每个实例每天在低峰期自动生成一个 rdb 文件,即使报表数据有一天的延迟也是可以接受的。

拿到了 rdb 文件就相当于拿到了 Redis 实例的所有数据,接下来就是生成报表的过程了:

  1. 解析 rdb 文件,获取到 Key 和 Value 的内容;
  2. 根据相对应的数据结构及内容,估算内存消耗等;
  3. 统计并生成报表;

逻辑很简单,所以设计思路很清晰。数据流图如下:

如何优雅的分析 Redis 里存了啥?

我们再看下具体该如何实现,首先是语言选型,雪球 SRE 自研的组件基本都是用 Go 语言做的后端,所以语言选型没什么纠结,直接用 Go。然后就是刚刚说的那几个流程。

1. 解析 RDB

按照 Redis 的协议来做就可以了,这个在 GitHub 上搜索 parse rdb 就可以找到许多解析 rdb 文件的库,拿过来使用即可。我们使用了 https://github.com/cupcake/rdb 。

2. 估算内存消耗

一条记录会有哪些内存使用呢?

我们知道 Redis 的实现里面有一些基础的数据结构,就是用这些结构来实现了对外暴露的各种数据类型:比如 sds、dict、intset、zipmap、adlist、ziplist、quicklist、skiplist 等等。只要根据这条记录的数据类型,找出使用了哪些数据结构,再计算出这些基础数据结构的内存消耗,再加上数据的内存使用,以及一些额外开销比如过期时间等,就可以估算出一条记录到底使用了多少内存。

但是由于 Redis 做了非常多的优化,同样的一种数据类型,在不同场景下使用的数据结构有可能是不同的。比如 List ,比较老版本的 Redis,会根据 list 元素的数量来决定来使用哪种结构,较短的时候使用 adlist,长之后使用 ziplist,数值可以通过 list-max-ziplist-entries 来配置。

3.2 版本以后全都使用了 quicklist。而不同结构对于内存的使用其实是有区别的,我们计算的时候也没办法拿到具体的配置,所以都按默认配置来计算,最后得出的值是一个估算的值,不过也基本可以反应使用情况了。如果大家对于 Redis 使用的各种数据结构感兴趣,想了解其设计及适用场景,可以多搜索一下相关的资料以及阅读 Redis 源码。

举个计算内存使用的例子:

假如我们通过解析 rdb,获取到了一个 key 为 hello,value 为 world,类型为 string ,ttl 为 1440 的一条记录,它的内存使用是这样的:

  • 一个 dictEntry 的消耗,Redis db 就是一个大 dict,每对 kv 都是其中的一个 entry ;
  • 一 个 robj 的消耗,robj 是为了在同一个 dict 内能够存储不同类型的 value,而使用的一个通用的数据结构,全名是 Redis Object;
  • 存储 key 的 sds 消耗,sds 是 Redis 中存储字符串使用的数据结构;
  • 存储过期时间消耗;
  • 存储 value 的 sds 消耗;

前四项基本是存储任何一个 key 都需要消耗的,最后一项根据 value 的数据结构不同而不同。

  • 一个 dictEntry 有 2 个指针,一个 int64 的内存消耗;
  • 一个 robj 有 1 指针,一个 int,以及几个使用位域的字段共消耗 4 字节;
  • 过期时间也是存储为一个 dictEntry,时间戳为 int64;
  • 存储 sds 需要存储 header 以及字符串长度 +1 的空间,header 长度根据字符串长度不同也会有所不同;

我们根据以上信息可以算出,向操作系统申请这些内存,真正需要多少内存。由于 Redis 支持多种 malloc 算法,我们就按 jemalloc 的分配方式算,这里也是可能存在误差的点。

所以最后 key 为 hello 的这条记录在 64 位操作系统上一共会消耗 92 字节。

如何优雅的分析 Redis 里存了啥?

其他类型的计算也大致是同样的思路,只不过根据不同的数据结构需要计算不同的内存消耗,计算的时候要记得考虑内存对齐的情况。还有由于 zset 的算法涉及到了随机生成层数,我们也使用同样的算法来随机,但是算出来的值肯定不是精确的,也是一个误差点。

3. 统计计数

终于可以拿到任何一个 key 的内存使用了,哪些是最有意义最有价值的数据呢?

  • top N,毫无疑问最大的前 N 个 key 一定是要关注的;
  • 不同数据类型的 key 数量元素数量分布以及内存使用情况;
  • 按照前缀分类,统一的前缀一般意味着某个特定的业务在使用,计算各个分类的 key 数量及内存使用情况;

这几个需求实现起来也都很容易:

  1. 维护一个小顶堆来存储前 N 个最大的即可,最后取出堆中的数据即可;
  2. 计数即可;
  3. 一般都会有特定的分隔符,比如 :|._ 等字符,按照这些字符切出公共前缀再统计,同时把所有的数字都替换为 0,便于分类;

4. 报表数据

如何优雅的分析 Redis 里存了啥?

如何优雅的分析 Redis 里存了啥?

如何优雅的分析 Redis 里存了啥?

可以每天打开个网页就可以看到某个 Redis 实例的内存使用的详细情况,是件非常幸福的事情,Redis 的内存使用再也不是黑盒。

这个系统上线一年以来对我们优化 Redis 资源使用、提高效率、节约成本提供了非常重要的数据支撑,而且在内部完全自动化,开发同学自己就可以看到当前 Redis 的使用情况是否符合预期,对于保障业务稳定也起到了非常重要的作用。这也是雪球的工程师团队一贯的做法,SRE 提供高效的工具,开发工程师可以自己运维自己的业务系统,可以极大的提高生产效率。

这个项目参考了 redis-rdb-tool 这个开源项目,但是性能上比它高效几倍,为了回馈社区,也希望有机会帮到大家,所以我们决定开源出来。

雪球的内部系统根据自己的特殊场景做了自动化获取 rdb 文件并备份的逻辑,开源出来的版本去除了定制化,只保留了获取到 rdb 之后的分析逻辑以及页面。

项目地址为 https://github.com/xueqiu/rdr 。https://www.infoq.cn/article/analysis-redis/?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global

分类: redis头条 标签:

redis-cli,Redis命令行工具

2020年3月22日 没有评论

redis-cli是Redis命令行工具,是一个命令行客户端程序,可以将命令直接发送到Redis,并直接从终端读取服务器返回的应答。

它有两种主要模式:

交互模式:其中存在一个REPL(Read Eval Print Loop),用户可以在其中键入命令并获得答复;

参数模式:将命令作为redis-cli的参数发送,并打印执行结果在标准输出上。

redis-cli 可以使用一些选项来启动程序,以使其进入特殊模式,以完成更复杂的任务。

例如,

模拟从服务器并打印从主服务器接收的复制流,

检查Redis的延迟并打印统计数据,

显示延迟样本和频率以及其他许多东西的ASCII图。

本文将介绍redis-cli使用的方方面面,从最简单的内容开始入手,由浅入深。

*命令行用法

直接运行命令并将返回结果打印在标准输出上:

$ redis-cli incr mycounter
(integer) 7

该命令的返回值是“ 7”。Redis有多种返回值类型(可以是字符串,数组,整数,NULL,错误等),返回值中括号部分内容表示返回值的类型。

当将redis-cli的输出用作另一个命令的输入时,或者当我们要将其重定向到文件中时,这个返回致类型并不是我们想要的。

实际上,redis-cli仅当它检测到标准输出是tty(基本上是终端)时,才会显示其他信息,提高可读性。其它情况下,它将自动启用原始输出模式,不带返回值类型。如以下示例所示:

$ redis-cli incr mycounter > /tmp/output.txt
$ cat /tmp/output.txt
8

(integer)由于CLI检测到输出不再写入终端,因此从输出中省略了类型。可以使用--raw选项在终端上强制进行原始输出:

$ redis-cli --raw incr mycounter
9

同样,可以使用来在写入文件或通过管道将其传递给其他命令时强制使可读的输出--no-raw

*主机,端口,密码和数据库

默认情况下redis-cli通过127.0.0.1端口6379连接到服务器。要指定其他主机名或IP地址,请使用-h。设置其他端口,请使用-p

$ redis-cli -h redis15.localnet.org -p 6390 ping
PONG

如果您的实例受密码保护,则该-a <password>选项将执行身份验证,从而无需使用AUTH命令来明确地进行以下操作

$ redis-cli -a myUnguessablePazzzzzword123 ping
PONG

或者,可以通过设置REDISCLI_AUTH环境变量提供密码。

最后,默认的数据库编号为零可以使用-n <dbnum>选折特定编号的数据库:

$ redis-cli flushall
OK
$ redis-cli -n 1 incr a
(integer) 1
$ redis-cli -n 1 incr a
(integer) 2
$ redis-cli -n 2 incr a
(integer) 1

也可以使用-u <uri>

$ redis-cli -u redis://p%40ssw0rd@redis-16379.hosted.com:16379/0 ping
PONG

*SSL / TLS

默认情况下,redis-cli使用明文TCP连接来连接到Redis。可以使用--tls选项启用S​​SL / TLS ,使用--cacert
--cacertdir配置受信任的根证书捆绑包或目录。

如果目标服务器需要使用客户端证书进行身份验证,则可以使用--cert--key
来指定证书和相应的私钥

*从其他程序获取输入

stdin读取的输入做为redis-cli的最后一个参数。例如,读取文件/etc/services做为foo变量的值,使用-x
选项:

$ redis-cli -x set foo < /etc/services
OK
$ redis-cli getrange foo 0 50
"#\n# Network services, Internet style\n#\n# Note that "

上面的例子中,没指定SET命令的最后一个参数但使用了-x选项,并将文件重定向到CLI的标准输入。因此,输入被读取,并用作命令的最后一个参数。

另一种方法是把redis-cli要执行的一系列写入文本文件:

$ cat /tmp/commands.txt
set foo 100
incr foo
append foo xxx
get foo
$ cat /tmp/commands.txt | redis-cli
OK
(integer) 101
(integer) 6
"101xxx"

依次执行commands.txt中的所有命令,就好像它们是由用户交互键入的一样。如果需要,可以在文件内用引号,以便可以在其中包含带空格或换行符的单个参数或其他特殊字符:

$ cat /tmp/commands.txt
set foo "This is a single argument"
strlen foo
$ cat /tmp/commands.txt | redis-cli
OK
(integer) 25

*连续运行相同的命令

当我们要连续监视一些关键内容或INFO字段输出时,或者当我们要模拟一些重复的写事件时(例如每5秒将一个新项目推入一个列表)。连续运行相同命令的功能很重要。

此功能由两个选项控制:-r <count>-i <delay>

第一个参数是运行命令的次数,第二个参数配置命令调用之间的延迟(以秒为单位)(十进制数(如0.1,以表示100毫秒)。

默认情况下,间隔(或延迟)设置为0,因此命令会立刻执行:

$ redis-cli -r 5 incr foo
(integer) 1
(integer) 2
(integer) 3
(integer) 4
(integer) 5

要永久运行同一命令,请使用-1做为 count。例如,监视RSS内存随时间变化,可以使用如下命令:

$ redis-cli -r -1 -i 1 INFO | grep rss_human
used_memory_rss_human:1.38M
used_memory_rss_human:1.38M
used_memory_rss_human:1.38M
... a new line will be printed each second ...

*使用redis-cli插入大量数据

使用redis-cli插入大量数据会单独重点介绍,请参考
大量插入指南

*CSV输出

有时您可能需要使用redis-cli快速将数据从Redis导出到外部程序。这可以使用CSV(逗号分隔值)输出功能来完成:

$ redis-cli lpush mylist a b c d
(integer) 4
$ redis-cli --csv lrange mylist 0 -1
"d","c","b","a"

目前,不可能像这样导出整个数据库,而只能运行带有CSV输出的单个命令。

*运行Lua脚本

redis-cli与以交互方式将脚本键入外壳程序或作为参数输入相比,您也可以使用一种更舒适的方式来运行文件中的脚本:

$ cat /tmp/script.lua
return redis.call('set',KEYS[1],ARGV[1])
$ redis-cli --eval /tmp/script.lua foo , bar
OK

Redis EVAL命令将脚本使用的键列表以及其他非键参数作为不同的数组。使用EVAL时,需要将键的数量提供为数字。但是,redis-cli使用上述--eval选项,无需显式指定键的数量。相反,它使用以逗号分隔键和参数的约定。这就是为什么在上述调用中您将其foo , bar视为参数的原因。

因此foo将填充KEYS数组和barARGV数组。

--eval选项在编写简单脚本时很有用。对于更复杂的工作,使用Lua调试器绝对更舒适。可以混合使用两种方法,因为调试器还使用来自外部文件的执行脚本。

*互动模式

在交互模式下,用户在提示符下键入Redis命令。该命令将发送到服务器,进行处理,然后将回复解析并呈现为更简单的形式以供阅读。

运行CLI不需要任何特殊操作-无需任何参数即可启动它,即可处于交互模式下:

$ redis-cli
127.0.0.1:6379> ping
PONG

该字符串127.0.0.1:6379>是提示。它提醒您已连接到给定的Redis实例。

当您连接的服务器发生更改时,或者在数据库编号为零以外的数据库上运行时,提示也会更改:

127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> dbsize
(integer) 1
127.0.0.1:6379[2]> select 0
OK
127.0.0.1:6379> dbsize
(integer) 503

*处理连接和重新连接

connect通过指定我们要连接主机名端口,以交互方式使用该命令可以连接到其他实例

127.0.0.1:6379> connect metal 6379
metal:6379> ping
PONG

如您所见,提示会相应更改。如果用户尝试连接到无法访问的实例,则redis-cli进入断开连接模式,输入新命令时尝试重新连接:

127.0.0.1:6379> connect 127.0.0.1 9999
Could not connect to Redis at 127.0.0.1:9999: Connection refused
not connected> ping
Could not connect to Redis at 127.0.0.1:9999: Connection refused
not connected> ping
Could not connect to Redis at 127.0.0.1:9999: Connection refused

通常,在检测到断开连接之后,CLI总是尝试透明地重新连接:如果尝试失败,它将显示错误并进入断开连接状态。以下是断开连接和重新连接的示例:

127.0.0.1:6379> debug restart
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> ping
PONG
127.0.0.1:6379> (now we are connected again)

执行重新连接后,将redis-cli自动重新选择最后选择的数据库编号。但是,有关连接的所有其他状态都会丢失,例如如果处于事务状态中间,将会丢失事物状态:

$ redis-cli
127.0.0.1:6379> multi
OK
127.0.0.1:6379> ping
QUEUED

( here the server is manually restarted )

127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

在交互式模式下使用CLI进行测试时,通常这不是问题,但是您应该意识到这一限制。

*编辑,历史和自动补全

因为redis-cli使用
linenoise,所以它始终具有行编辑功能,而无需依赖libreadline或其他可选库。

您可以访问已执行命令的历史记录,以免反复按箭头键(上和下)来再次键入它们。历史记录在两次CLI重新启动之间保留在.rediscli_history,该文件位于用户主目录内。可以通过设置REDISCLI_HISTFILE环境变量来使用其他历史记录文件名,并通过将其设置为/dev/null来禁用它。

CLI也可以通过按TAB键来完成命令名称,如以下示例所示:

127.0.0.1:6379> Z<TAB>
127.0.0.1:6379> ZADD<TAB>
127.0.0.1:6379> ZCARD<TAB>

*N次执行相同的命令

通过在命令名称前加上数字前缀,可以多次运行同一命令:

127.0.0.1:6379> 5 incr mycounter
(integer) 1
(integer) 2
(integer) 3
(integer) 4
(integer) 5

*显示有关Redis命令的帮助

Redis有许多命令,有时在测试时,您可能不记得参数的确切顺序。redis-cli使用help命令为大多数Redis命令提供联机帮助。该命令可以两种形式使用:

  • help @<category>显示给定类别的所有命令。类别有:@generic@list@set@sorted_set@hash
    @pubsub@transactions@connection@server@scripting
    @hyperloglog
  • help <commandname> 显示特定命令的帮助。

例如,为了显示PFADD命令的帮助,请使用:

127.0.0.1:6379>help PFADD

PFADD key element [element …] summary: Adds the specified elements to the specified HyperLogLog. since: 2.8.9

请注意,help支持TAB补全。

*清除终端屏幕

在交互模式下,使用clear命令将清除终端的屏幕。

*特殊的操作模式

到目前为止,我们已经看到了两种主要的redis-cli使用模式

  • Redis命令的命令行执行。
  • 交互式“ REPL”用法。

下面是CLI执行与Redis相关的其他辅助任务

*连续统计模式

这可能是的鲜为人知的功能之一,并且对于实时监视Redis实例非常有用。要启用此模式,请使用--stat选项:

$ redis-cli --stat
------- data ------ --------------------- load -------------------- - child -
keys       mem      clients blocked requests            connections
506        1015.00K 1       0       24 (+0)             7
506        1015.00K 1       0       25 (+1)             7
506        3.40M    51      0       60461 (+60436)      57
506        3.40M    51      0       146425 (+85964)     107
507        3.40M    51      0       233844 (+87419)     157
507        3.40M    51      0       321715 (+87871)     207
508        3.40M    51      0       408642 (+86927)     257
508        3.40M    51      0       497038 (+88396)     257

在这种模式下,每秒钟都会打印一条新行,其中包含有用的信息以及与旧数据点之间的差异。您可以轻松地了解内存使用情况,连接的客户端等情况。

-i <interval>可修改更新频率,默认值为一秒钟。

*扫描大键

在这种特殊模式下,redis-cli充当键空间分析器。它会在数据集中扫描大键,但还会提供有关数据集所包含的数据类型的信息。该模式通过该--bigkeys选项启用,并产生非常详细的输出:

$ redis-cli --bigkeys

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest string found so far 'key-419' with 3 bytes
[05.14%] Biggest list   found so far 'mylist' with 100004 items
[35.77%] Biggest string found so far 'counter:__rand_int__' with 6 bytes
[73.91%] Biggest hash   found so far 'myobject' with 3 fields

-------- summary -------

Sampled 506 keys in the keyspace!
Total key length in bytes is 3452 (avg len 6.82)

Biggest string found 'counter:__rand_int__' has 6 bytes
Biggest   list found 'mylist' has 100004 items
Biggest   hash found 'myobject' has 3 fields

504 strings with 1403 bytes (99.60% of keys, avg size 2.78)
1 lists with 100004 items (00.20% of keys, avg size 100004.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
1 hashs with 3 fields (00.20% of keys, avg size 3.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

在输出的第一部分中,报告遇到的每个比先前的较大键(相同类型)大的新键。摘要部分提供有关Redis实例内部数据的常规统计信息。

该程序使用SCAN命令,因此可以在繁忙的服务器上执行命令而不会影响操作,但是-i可以使用选项来限制所请求的每100个键的指定秒数的扫描过程。例如,这-i 0.1将大大减慢程序的执行速度,但也将服务器上的负载减少到很小的程度。

请注意,该摘要还以更简洁的形式报告了每次找到的最大密钥。如果针对非常大的数据集运行,则初始输出只是尽快提供一些有趣的信息。

*非阻塞获取所有键列表

获取redis的key空间,但是不会像KEYS *那样阻塞Redis。--bigkeys选项一样,此模式使用SCAN命令,因此如果数据集正在更改,则可能会多次报告键,但是如果自迭代开始以来一直存在该键,则不会丢失任何键。

$ redis-cli --scan | head -10
key-419
key-71
key-236
key-50
key-38
key-458
key-453
key-499
key-446
key-371

head -10命令用于打印输出的前10行。

扫描符合给定模式的key,--pattern

$ redis-cli --scan --pattern '*-11*'
key-114
key-117
key-118
key-113
key-115
key-112
key-119
key-11
key-111
key-110
key-116

配合wc统计特定种类的key个数:

$ redis-cli --scan --pattern 'user:*' | wc -l
3829433

*发布/订阅模式

CLI只需使用PUBLISH命令就可以在Redis发布/订阅通道中发布消息因为PUBLISH命令与任何其他命令都非常相似,所以这是可以预期的订阅频道以接收消息是不同的-在这种情况下,我们需要阻止并等待消息,因此在中将其实现为一种特殊模式redis-cli与其他特殊模式不同,此模式不是通过使用特殊选项启用的,而只是通过使用SUBSCRIBEPSUBSCRIBE命令以交互或非交互模式启用的

$ redis-cli psubscribe '*'
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "*"
3) (integer) 1

阅读邮件消息显示,我们进入的Pub / Sub模式。当另一个客户端在某个通道中发布某些消息时(例如,您可以使用进行操作)redis-cli PUBLISH mychannel mymessage,发布/订阅模式下的CLI将显示以下内容:

1) "pmessage"
2) "*"
3) "mychannel"
4) "mymessage"

这对于调试发布/订阅问题非常有用。要退出发布/订阅模式,只需处理CTRL-C

*监视在Redis中执行的命令

与发布/订阅模式相似,一旦使用了MONITOR模式,便会自动进入监视模式。它将打印Redis实例收到的所有命令:

$ redis-cli monitor
OK
1460100081.165665 [0 127.0.0.1:51706] "set" "foo" "bar"
1460100083.053365 [0 127.0.0.1:51707] "get" "foo"

请注意,可以使用管道传递输出,因此您可以使用诸如grep之类的工具监视特定的模式。

*监控Redis实例的延迟

Redis通常用于延迟非常关键的环境中。延迟涉及应用程序中的多个移动部分,从客户端库到网络堆栈,再到Redis实例本身。

CLI具有多种功能,可用于研究Redis实例的延迟并了解延迟的最大值,平均值和分布。

基本延迟检查工具是--latency可选的。CLI使用此选项运行一个循环,在该循环中,将PING命令发送到Redis实例,并测量获得回复的时间。每秒发生100次,并且统计信息会在控制台中实时更新:

$ redis-cli --latency
min: 0, max: 1, avg: 0.19 (427 samples)

统计信息以毫秒为单位。通常,非常快的实例的平均延迟往往由于系统redis-cli
自身
的内核调度程序而导致的延迟而被高估了一点,因此,平均延迟为0.19以上可能很容易为0.01或更小。但是,这通常不是什么大问题,因为我们对几毫秒或更长时间的事件感兴趣。

有时,研究最大和平均延迟如何随时间变化是有用的。--latency-history选项用于此目的:它的工作方式与完全相同--latency,但是每15秒(默认情况下)从头开始一个新的采样会话:

$ redis-cli --latency-history
min: 0, max: 1, avg: 0.14 (1314 samples) -- 15.01 seconds range
min: 0, max: 1, avg: 0.18 (1299 samples) -- 15.00 seconds range
min: 0, max: 1, avg: 0.20 (113 samples)^C

您可以使用该-i <interval>选项更改采样会话的长度

最先进的延迟研究工具,但对于没有经验的用户来说,也更难解释,它是使用彩色终端显示延迟频谱的能力。您将看到一个彩色输出,指示不同百分比的样本,以及不同的ASCII字符,指示不同的等待时间数字。使用以下--latency-dist
选项
启用此模式

$ redis-cli --latency-dist
(output not displayed, requires a color terminal, try it!)

内部还有另一个非常不寻常的延迟工具redis-cli它不检查Redis实例的延迟,而是检查您所运行的计算机的延迟redis-cli您可能会问什么延迟?内核调度程序,虚拟化实例情况下的管理程序固有的延迟等等。

之所以称其为固有延迟,是因为它对程序员来说是不透明的。如果您的Redis实例具有严重的延迟,而不管可能是引起原因的所有显而易见的事情,那么值得检查一下,通过redis-cli直接在运行Redis服务器的系统中以这种特殊模式运行系统可以做的最好的事情

通过测量固有延迟,您知道这是基线,Redis无法超越您的系统。为了在此模式下运行CLI,请使用--intrinsic-latency <test-time>测试的时间以秒为单位,并指定redis-cli应检查多少秒才能检查当前正在运行的系统的延迟。

$ ./redis-cli --intrinsic-latency 5
Max latency so far: 1 microseconds.
Max latency so far: 7 microseconds.
Max latency so far: 9 microseconds.
Max latency so far: 11 microseconds.
Max latency so far: 13 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 34 microseconds.
Max latency so far: 82 microseconds.
Max latency so far: 586 microseconds.
Max latency so far: 739 microseconds.

65433042 total runs (avg latency: 0.0764 microseconds / 764.14 nanoseconds per run).
Worst run took 9671x longer than the average latency.

重要说明:此命令必须在要在其上运行Redis服务器的计算机上执行,而不是在其他主机上。它甚至不连接到Redis实例,而仅在本地执行测试。

在上述情况下,我的系统无法完成比最坏情况下的739微秒更好的延迟,因此我可以预期某些查询有时会在不到1毫秒的时间内运行。

*RDB文件的远程备份

在Redis复制的第一次同步过程中,主服务器和从服务器以RDB文件的形式交换整个数据集。redis-cli为了提供远程备份功能,可以利用此功能,该功能允许将RDB文件从任何Redis实例传输到运行的本地计算机
redis-cli要使用此模式,请使用以下--rdb <dest-filename>
选项
调用CLI

$ redis-cli --rdb /tmp/dump.rdb
SYNC sent to master, writing 13256 bytes to '/tmp/dump.rdb'
Transfer finished with success.

这是确保您的Redis实例具有灾难恢复RDB备份的简单但有效的方法。但是,在脚本或cron作业中使用此选项时,请确保检查命令的返回值。如果它不为零,则发生错误,如以下示例所示:

$ redis-cli --rdb /tmp/dump.rdb
SYNC with master failed: -ERR Can't SYNC while not connected with my master
$ echo $?
1

*从机模式

CLI的从模式是一项高级功能,对Redis开发人员和调试操作很有用。它允许检查主服务器在复制流中发送给其从服务器的内容,以便将写入传播到其副本。选项名称就是--slave它是这样工作的:

$ redis-cli --slave
SYNC with master, discarding 13256 bytes of bulk transfer...
SYNC done. Logging commands from master.
"PING"
"SELECT","0"
"set","foo","bar"
"PING"
"incr","mycounter"

该命令首先丢弃第一次同步的RDB文件,然后将接收到的每个命令记录为CSV格式。

如果您认为某些命令没有在从属服务器中正确复制,则这是检查正在发生的事情的好方法,同时也是改善缺陷报告的有用信息。

*执行LRU模拟

Redis通常用作具有LRU驱逐功能的缓存根据键的数量和为高速缓存分配的内存量(通过maxmemory指令指定),高速缓存的命中和未命中量将发生变化。有时,模拟命中率对于正确配置缓存非常有用。

CLI具有一种特殊的模式,它在请求模式中使用80-20%的幂定律分布来模拟GET和SET操作。这意味着20%的密钥将被请求80%的时间,这是缓存方案中的常见分布。

从理论上讲,考虑到请求的分布和Redis内存开销,应该可以使用数学公式来分析计算命中率。但是,Redis可以配置为具有不同的LRU设置(样本数),并且在Redis中近似的LRU的实现在不同版本之间会发生很大变化。同样,每个密钥的内存量可能会在版本之间变化。这就是构建该工具的原因:其主要目的是测试Redis LRU实施的质量,但现在也可用于测试给定版本在部署时考虑的设置下的行为。

为了使用此模式,您需要指定测试中的键数。您还需要配置一个适合maxmemory初次尝试的设置。

重要说明:maxmemory在Redis配置中配置设置至关重要:如果没有最大内存使用量的上限,则由于所有密钥都可以存储在内存中,因此命中率最终将为100%。或者,如果您指定的键太多而没有最大内存,则最终将使用所有计算机RAM。大多数情况下,还需要配置适当的
最大内存策略allkeys-lru

在以下示例中,我将内存限制配置为100MB,并使用1000万个键进行LRU模拟。

警告:该测试使用流水线操作,并且会对服务器造成压力,请勿将其用于生产实例。

$ ./redis-cli --lru-test 10000000
156000 Gets/sec | Hits: 4552 (2.92%) | Misses: 151448 (97.08%)
153750 Gets/sec | Hits: 12906 (8.39%) | Misses: 140844 (91.61%)
159250 Gets/sec | Hits: 21811 (13.70%) | Misses: 137439 (86.30%)
151000 Gets/sec | Hits: 27615 (18.29%) | Misses: 123385 (81.71%)
145000 Gets/sec | Hits: 32791 (22.61%) | Misses: 112209 (77.39%)
157750 Gets/sec | Hits: 42178 (26.74%) | Misses: 115572 (73.26%)
154500 Gets/sec | Hits: 47418 (30.69%) | Misses: 107082 (69.31%)
151250 Gets/sec | Hits: 51636 (34.14%) | Misses: 99614 (65.86%)

该程序每秒显示一次统计信息。如您所见,在最初的几秒钟内,缓存开始被填充。后来的未命中率稳定在很长一段时间内的实际数字中:

120750 Gets/sec | Hits: 48774 (40.39%) | Misses: 71976 (59.61%)
122500 Gets/sec | Hits: 49052 (40.04%) | Misses: 73448 (59.96%)
127000 Gets/sec | Hits: 50870 (40.06%) | Misses: 76130 (59.94%)
124250 Gets/sec | Hits: 50147 (40.36%) | Misses: 74103 (59.64%)

对于我们的用例来说,59%的错率可能是不可接受的。因此,我们知道100MB的内存不足。让我们尝试使用半GB。几分钟后,我们将看到输出稳定到以下数字:

140000 Gets/sec | Hits: 135376 (96.70%) | Misses: 4624 (3.30%)
141250 Gets/sec | Hits: 136523 (96.65%) | Misses: 4727 (3.35%)
140250 Gets/sec | Hits: 135457 (96.58%) | Misses: 4793 (3.42%)
140500 Gets/sec | Hits: 135947 (96.76%) | Misses: 4553 (3.24%)

因此,我们知道,有了500MB的存储空间,我们的密钥数量(1000万个)和分发数量(80-20种样式)已经足够了。

分类: 专题 标签:

Redis发布周期

2020年3月22日 没有评论

Redis是系统软件,并且是一种保存用户数据的系统软件,因此它是软件栈中最关键的部分。

因此,Redis的发布周期会尽力确保只有在达到足够高的稳定性时才发布稳定的发布,即使是以较慢的发布周期为代价。

给定版本的Redis可以处于三个不同的稳定性级别:

  • 不稳定
  • 开发
  • 冻结
  • 发布候选
  • 稳定

不稳定的树

Redis的不稳定版本始终位于Redis GitHub Repository中的unstable分支中。

这是大多数新功能都在开发中的源代码树,不被认为已准备好投入生产:它可能包含严重的bug,但尚未完全就绪,并且可能不稳定。

但是,我们努力确保即使不稳定的分支在大多数情况下在开发环境中也可用,而没有重大问题。

分叉,冻结,发布候选树

当开始计划新版本的Redis时,不稳定的分支(或有时是当前稳定的分支)将分支到具有目标发行版名称的新分支中。

例如,当Redis 2.6作为稳定版发布时,unstable分支分支到2.8分支中。

这个新分支可以处于三个不同的稳定性级别:开发,冻结和发布候选。

  • 开发:新功能和错误修复已提交到分支中,但并不是所有unstable合并到此的内容。仅合并可以在合理时间内稳定的功能。
  • 冻结:未添加任何新功能,除非几乎可以保证对源代码的零稳定性影响,并且出于某种原因,这是必须尽快交付的非常重要的功能。大型代码更改仅在需要它们才能修复错误时才允许。
  • 候选版本:仅针对此版本提交修复程序。

稳定的树

在某个时候,当给定的Redis版本处于“候选发布”状态足够长的时间时,我们观察到发出严重bug的频率开始下降,以至于在几周内我们没有任何严重的bug。报告。

发生这种情况时,版本将标记为稳定。

版本号

稳定版本遵循通常的major.minor.patch版本控制架构,并具有以下特殊规则:

  • 次要版本甚至在Redis的稳定版本中也是如此。
  • 未成年人在不稳定,发展,冻结,释放的候选人方面很奇怪。例如,不稳定的2.8.x版本将具有2.7.x形式的版本号。通常,不稳定版本的xyz将具有版本x.(y-1).z。
  • 随着Redis不稳定版本的进行,补丁程序级别会不时增加,因此在给定的时间,您可能会有2.7.2,然后是2.7.3,依此类推。但是,当达到发布候选状态时,补丁程序级别将从101开始。因此,例如2.7.101是2.8的第一个发布候选,2.7.105是发布候选5,依此类推。

支持

不支持旧版本,因为我们非常努力地使Redis API大部分向后兼容。升级到较新版本通常很简单。

例如,如果当前的稳定版本是2.6.x,则我们接受错误报告并提供对先前稳定版本(2.4.x)的支持,但不支持较早的版本(例如2.2.x)。

当2.8成为当前的稳定版本时,2.6.x将是受支持的最早版本。

分类: 专题 标签:

Redis 内存优化

2020年3月21日 没有评论

这个页面仍然在维护中。当你在使用redis的过程中遇到一些与内存相关的问题时,你需要关注下面的事情。

小的聚合类型数据的特殊编码处理

Redis2.2版本及以后,存储集合数据的时候会采用内存压缩技术,以使用更少的内存存储更多的数据。如Hashes,Lists,Sets和Sorted Sets,当这些集合中的所有数都小于一个给定的元素,并且集合中元素数量小于某个值时,存储的数据会被以一种非常节省内存的方式进行编码,使用这种编码理论上至少会节省10倍以上内存(平均节省5倍以上内存)。并且这种编码技术对用户和redis api透明。因为使用这种编码是用CPU换内存,所以我们提供了更改阈值的方法,只需在redis.conf里面进行修改即可.

hash-max-zipmap-entries 64 (2.6以上使用hash-max-ziplist-entries)
hash-max-zipmap-value 512  (2.6以上使用hash-max-ziplist-value)
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512

(集合中)如果某个值超过了配置文件中设置的最大值,redis将自动把把它(集合)转换为正常的散列表。这种操作对于比较小的数值是非常快的,但是,如果你为了使用这种编码技术而把配置进行了更改,你最好做一下基准测试(和正常的不采用编码做一下对比).

使用32位的redis

使用32位的redis,对于每一个key,将使用更少的内存,因为32位程序,指针占用的字节数更少。但是32的redis整个实例使用的内存将被限制在4G以下。使用make 32bit命令编译生成32位的redis。RDB和AOF文件是不区分32位和64位的(包括字节顺序),所以你可以使用64位的reidis恢复32位的RDB备份文件,相反亦然.

位级别和字级别的操作

Redis 2.2引入了位级别和字级别的操作: GETRANGE, SETRANGE, GETBITSETBIT.使用这些命令,你可以把redis的字符串当做一个随机读取的(字节)数组。例如你有一个应用,用来标志用户的ID是连续的整数,你可以使用一个位图标记用户的性别,使用1表示男性,0表示女性,或者其他的方式。这样的话,1亿个用户将仅使用12 M的内存。你可以使用同样的方法,使用 GETRANGESETRANGE 命令为每个用户存储一个字节的信息。这仅是一个例子,实际上你可以使用这些原始数据类型解决更多问题。

尽可能使用散列表(hashes)

小散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.

如果你想了解更多关于这方面的知识,请读下一段.

使用散列结构高效存储抽象的键值对

我知道这部分的标题很吓人,但是我将详细解释这部分内容.

一般而言,把一个模型(model)表示为key-value的形式存储在redis中非常容易,当然value必须为字符串,这样存储不仅比一般的key value存储高效,并且比memcached存储还高效.

让我们做个对比:一些key存储了一个对象的多个字段要比一个散列表存储对象的多个字段占用更多的内存。这怎么可能?从原理上讲,为了保证查找一个数据总是在一个常量时间内(O(1)),需要一个常量时间复杂度的数据结构,比如说散列表.

但是,通常情况下,散列表只包括极少的几个字段。当散列表非常小的时候,我们采用将数据encode为一个O(N)的数据结构,你可以认为这是一个带有长度属性的线性数组。只有当N是比较小的时候,才会采用这种encode,这样使用HGETHSET命令的复杂度仍然是O(1):当散列表包含的元素增长太多的时候,散列表将被转换为正常的散列表(极限值可以在redis.conf进行配置).

无论是从时间复杂度还是从常量时间的角度来看,采用这种encode理论上都不会有多大性能提升,但是,一个线性数组通常会被CPU的缓存更好的命中(线性数组有更好的局部性),从而提升了访问的速度.

既然散列表的字段及其对应的值并不是用redis objects表示,所以散列表的字段不能像普通的key一样设置过期时间。但是这毫不影响对散列表的使用,因为散列表本来就是这样设计的(我们相信简洁比多功能更重要,所以嵌入对象是不允许的,散列表字段设置单独的过期时间是不允许的).

所以散列表能高效利用内存。这非常有用,当你使用一个散列表存储一个对象或者抽象其他一类相关的字段为一个模型时。但是,如果我们有一个普通的key value业务需求怎么办?

假如我们想使用redis存储许多小对象,这些对象可以使用json字符串表示,也可能是HTML片段和简单的key->boolean键值对。概况的说,一切皆字符串,都可以使用string:string的形式表示.

我们假设要缓存的对象使用数字后缀进行编码,如:

  • object:102393
  • object:1234
  • object:5

我们可以这样做。每次SET的时候,把key分为两部分,第一部分当做一个key,第二部当做散列表字段。比如“object:1234”,分成两部分:

  • a Key named object:12
  • a Field named 34

我们使用除最后2个数字的部分作为key,最后2个数字做为散列表的字段。使用命令:

HSET object:12 34 somevalue

如你所见,每个散列表将(理论上)包含100个字段,这是CPU资源和内存资源之间的一个折中.

另一个需要你关注的是在这种模式下,无论缓存多少对象,每个散列表都会分配100个字段。因为我们的对象总是以数字结尾,而不是一个随机的字符串。从某些方面来说,这是一种隐性的预分片。

对于小数字怎么处理?比如object:2,我们采用object:作为key,所有剩下的数字作为一个字段。所以object:2和object:10都会被存储到key为object:的散列表中,但是一个使用2作为字段,一个使用10作为字段。

这种方式将节省多少内存?

我使用了下面的Ruby程序进行了测试:

require 'rubygems'
require 'redis'

UseOptimization = true

def hash_get_key_field(key)
    s = key.split(":")
    if s[1].length > 2
        {:key => s[0]+":"+s[1][0..-3], :field => s[1][-2..-1]}
    else
        {:key => s[0]+":", :field => s[1]}
    end
end

def hash_set(r,key,value)
    kf = hash_get_key_field(key)
    r.hset(kf[:key],kf[:field],value)
end

def hash_get(r,key,value)
    kf = hash_get_key_field(key)
    r.hget(kf[:key],kf[:field],value)
end

r = Redis.new
(0..100000).each{|id|
    key = "object:#{id}"
    if UseOptimization
        hash_set(r,key,"val")
    else
        r.set(key,"val")
    end
}

在redis2.2的64位版本上测试结果:

  • 当开启优化时使用内存1.7M
  • 当未开启优化时使用内存11M

从结果看出,这是一个数量级的优化,我认为这种优化使redis成为最出色的键值缓存。

特别提示: 要使上面的程序较好的工作,别忘记设置你的redis:

hash-max-zipmap-entries 256

相应的最大键值长度设置:

hash-max-zipmap-value 1024

每次散列表的元素数量或者值超过了阈值,散列将被扩展为一张真正的散列表进行存储,此时节约存储的优势就没有了.

或许你想问,你为什么不自动将这些key进行转化以提高内存利用率?有两个原因:第一是因为我们更倾向于让这些权衡明确,而且必须在很多事情之间权衡:CPU,内存,最大元素大小限制。第二是顶级的键空间支持很多有趣的特性,比如过期,LRU算法,所以这种做法并不是一种通用的方法.

Redis的一贯风格是用户必须理解它是如何运作的,必须能够做出最好的选择和权衡,并且清楚它精确的运行方式.

内存分配

为了存储用户数据,当设置了maxmemory后Redis会分配几乎和maxmemory一样大的内存(然而也有可能还会有其他方面的一些内存分配).

精确的值可以在配置文件中设置,或者在启动后通过 CONFIG SET 命令设置(see Using memory as an LRU cache for more info). Redis内存管理方面,你需要注意以下几点:

  • 当某些缓存被删除后Redis并不是总是立即将内存归还给操作系统。这并不是redis所特有的,而是函数malloc()的特性。例如你缓存了5G的数据,然后删除了2G数据,从操作系统看,redis可能仍然占用了5G的内存(这个内存叫RSS,后面会用到这个概念),即使redis已经明确声明只使用了3G的空间。这是因为redis使用的底层内存分配器不会这么简单的就把内存归还给操作系统,可能是因为已经删除的key和没有删除的key在同一个页面(page),这样就不能把完整的一页归还给操作系统.
  • 上面的一点意味着,你应该基于你可能会用到的 最大内存 来指定redis的最大内存。如果你的程序时不时的需要10G内存,即便在大多数情况是使用5G内存,你也需要指定最大内存为10G.
  • 内存分配器是智能的,可以复用用户已经释放的内存。所以当使用的内存从5G降低到3G时,你可以重新添加更多的key,而不需要再向操作系统申请内存。分配器将复用之前已经释放的2G内存.
  • 因为这些,当redis的peak内存非常高于平时的内存使用时,碎片所占可用内存的比例就会波动很大。当前使用的内存除以实际使用的物理内存(RSS)就是fragmentation;因为RSS就是peak memory,所以当大部分key被释放的时候,此时内存的mem_used / RSS就比较高.

如果 maxmemory 没有设置,redis就会一直向OS申请内存,直到OS的所有内存都被使用完。所以通常建议设置上redis的内存限制。或许你也想设置 maxmemory-policy 的值为 noeviction(在redis的某些老版本默认 不是这样)

设置了maxmemory后,当redis的内存达到内存限制后,再向redis发送写指令,会返回一个内存耗尽的错误。错误通常会触发一个应用程序错误,但是不会导致整台机器宕掉.

维护中

更多技巧后续推出.

分类: 专题 标签:

Redis 快速插入大量数据

2020年3月21日 没有评论

有些时候,Redis实例需要装载大量用户在短时间内产生的数据,数以百万计的keys需要被快速的创建。

我们称之为大量数据插入(mass insertion),本文档的目标就是提供如下信息:Redis如何尽可能快的处理数据。

使用Luke协议

使用正常模式的Redis 客户端执行大量数据插入不是一个好主意:因为一个个的插入会有大量的时间浪费在每一个命令往返时间上。使用管道(pipelining)是一种可行的办法,但是在大量插入数据的同时又需要执行其他新命令时,这时读取数据的同时需要确保请可能快的的写入数据。

只有一小部分的客户端支持非阻塞输入/输出(non-blocking I/O),并且并不是所有客户端能以最大限度的提高吞吐量的高效的方式来分析答复。

例如,如果我们需要生成一个10亿的`keyN -> ValueN’的大数据集,我们会创建一个如下的redis命令集的文件:

SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN

一旦创建了这个文件,其余的就是让Redis尽可能快的执行。在以前我们会用如下的netcat命令执行:

(cat data.txt; sleep 10) | nc localhost 6379 > /dev/null

然而这并不是一个非常可靠的方式,因为用netcat进行大规模插入时不能检查错误。从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

使用pipe mode模式的执行命令如下:

cat data.txt | redis-cli --pipe

这将产生类似如下的输出:

All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000

使用redis-cli将有效的确保错误输出到Redis实例的标准输出里面。

生成Redis协议

它会非常简单的生成和解析Redis协议,Redis协议文档请参考Redis协议说明。 但是为了生成大量数据插入的目标,你需要了解每一个细节协议,每个命令会用如下方式表示:

*<args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>

这里的<cr>是”\r”(或者是ASCII的13)、<lf>是”\n”(或者是ASCII的10)。

例如:命令SET key value协议格式如下:

*<args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>

或表示为引用字符串:

"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"

你需要将大量插入数据的命令按照上面的方式一个接一个的生成到文件。

下面是使用Ruby生成协议的参考:

def gen_redis_proto(*cmd)
    proto = ""
    proto << "*"+cmd.length.to_s+"\r\n"
    cmd.each{|arg|
        proto << "$"+arg.to_s.bytesize.to_s+"\r\n"
        proto << arg.to_s+"\r\n"
    }
    proto
end

puts gen_redis_proto("SET","mykey","Hello World!").inspect

针对上面的例子,使用下面代码可以很容易的生成需要的文件:

(0...1000).each{|n|
    STDOUT.write(gen_redis_proto("SET","Key#{n}","Value#{n}"))
}

我们可以直接用 redis-cli 的 pipe执行我们的第一个大量数据插入命令,过程如下:

$ ruby proto.rb | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000

pipe mode的工作原理是什么?

难点是保证redis-cli在pipe mode模式下执行和netcat一样快的同时,如何能理解服务器发送的最后一个回复。

这是通过以下方式获得:

  • redis-cli –pipe试着尽可能快的发送数据到服务器。
  • 读取数据的同时,解析它。
  • 一旦没有更多的数据输入,它就会发送一个特殊的ECHO命令,后面跟着20个随机的字符。我们相信可以通过匹配回复相同的20个字符是同一个命令的行为。
  • 一旦这个特殊命令发出,收到的答复就开始匹配这20个字符,当匹配时,就可以成功退出了。

同时,在分析回复的时候,我们会采用计数器的方法计数,以便在最后能够告诉我们大量插入数据的数据量。

分类: 专题 标签:

Redis键空间通知

2020年3月21日 没有评论
重要:键空间通知功能自2.8.0版本开始可用。

功能概述

键空间通知允许客户端订阅发布/订阅频道,以便以某种方式接收影响Redis数据集的事件。可能接收的事件示例如下:
  • 所有影响给定键的命令。
  • 所有接收LPUSH操作的键。
  • 所有在数据库0中到期的键。
事件使用Redis的普通发布/订阅层传递,因此实现了发布/订阅的客户端无需修改即可使用此功能。由于Redis的发布/订阅是fire and forget,因此如果你的应用要求可靠的事件通知,目前还不能使用这个功能,也就是说,如果你的发布/订阅客户端断开连接,并在稍后重连,那么所有在客户端断开期间发送的事件将会丢失。将来有计划允许更可靠的事件传递,但可能会在更一般的层面上解决,要么为发布/订阅本身带来可靠性,要么允许Lua脚本拦截发布/订阅的消息以执行推送等操作,就像往队列里推送事件一样。

事件类型

键空间通知的实现是为每一个影响Redis数据空间的操作发送两个不同类型的事件。例如,在数据库0中名为mykey的键上执行DEL操作,将触发两条消息的传递,完全等同于下面两个PUBLISH命令:
PUBLISH __keyspace@0__:mykey del
PUBLISH __keyevent@0__:del mykey
以上很容易看到,一个频道允许监听所有以键mykey为目标的所有事件,以及另一个频道允许获取有关所有DEL操作目标键的信息。第一种事件,在频道中使用keyspace前缀的被叫做键空间通知,第二种,使用keyevent前缀的,被叫做键事件通知。在以上例子中,为键mykey生成了一个del事件。 会发生什么:
  • 键空间频道接收到的消息是事件的名称。
  • 键事件频道接收到的消息是键的名称。
可以只启用其中一种通知,以便只传递我们感兴趣的事件子集。

配置

默认情况下,键空间事件通知是不启用的,因为虽然不太明智,但该功能会消耗一些CPU。可以使用redis.conf中的notify-keyspace-events或者使用CONFIG SET命令来开启通知。将参数设置为空字符串会禁用通知。 为了开启通知功能,使用了一个非空字符串,由多个字符组成,每一个字符都有其特殊的含义,具体参见下表:
K     键空间事件,以__keyspace@<db>__前缀发布。
E     键事件事件,以__keyevent@<db>__前缀发布。
g     通用命令(非类型特定),如DEL,EXPIRE,RENAME等等
$     字符串命令
l     列表命令
s     集合命令
h     哈希命令
z     有序集合命令
x     过期事件(每次键到期时生成的事件)
e     被驱逐的事件(当一个键由于达到最大内存而被驱逐时产生的事件)
A     g$lshzxe的别名,因此字符串AKE表示所有的事件。
字符串中应当至少存在K或者E,否则将不会传递事件,不管字符串中其余部分是什么。例如,要为列表开启键空间事件,则配置参数必须设置为Kl,以此类推。字符串KEA可以用于开启所有可能的事件。

不同的命令生成的事件

根据以下列表,不同的命令产生不同种类的事件。
  • DEL命令为每一个删除的key生成一个del事件。
  • RENAME生成两个事件,一个是为源key生成的rename_from事件,一个是为目标key生成的rename_to事件。
  • EXPIRE在给一个键设置有效期时,会生成一个expire事件,或者每当设置有效期导致键被删除时,生成expired事件(请查阅EXPIRE文档以获取更多信息)。
  • SORT会在使用STORE选项将结果存储到新键时,生成一个sortstore事件。如果结果列表为空,且使用了STORE选项,并且已经存在具有该名称的键时,那个键将被删除,因此在这种场景下会生成一个del事件。
  • SET以及所有其变种(SETEXSETNXGETSET)生成set事件。但是SETEX还会生成一个expire事件。
  • MSET为每一个key生成一个set事件。
  • SETRANGE生成一个setrange事件。
  • INCRDECRINCRBYDECRBY命令都生成incrby事件。
  • INCRBYFLOAT生成一个incrbyfloat事件。
  • APPEND生成一个append事件。
  • LPUSHLPUSHX生成一个lpush事件,即使在可变参数情况下也是如此。
  • RPUSHRPUSHX生成一个rpush事件,即使在可变参数情况下也是如此。
  • RPOP生成rpop事件。此外,如果键由于列表中的最后一个元素弹出而被删除,则会生成一个del事件。
  • LPOP生成lpop事件。此外,如果键由于列表中的最后一个元素弹出而被删除,则会生成一个del事件。
  • LINSERT生成一个linsert事件。
  • LSET生成一个lset事件。
  • LTRIM生成ltrim事件,此外,如果结果列表为空或者键被移除,将会生成一个del事件。
  • RPOPLPUSHBRPOPLPUSH生成rpop事件和lpush事件。这两种情况下,顺序都将得到保证(lpush事件将总是在rpop事件之后传递)。此外,如果结果列表长度为零且键被删除,则会生成一个del事件。
  • HSETHSETNX以及HMSET都生成一个hset事件。
  • HINCRBY生成一个hincrby事件。
  • HINCRBYFLOAT生成一个hincrbyfloat事件。
  • HDEL生成一个hdel事件,此外,如果结果哈希集为空或者键被移除,将生成一个del事件。
  • SADD生成一个sadd事件,即使在可变参数情况下也是如此。
  • SREM生成一个srem事件,此外,如果结果集合为空或者键被移除,将生成一个del事件。
  • SMOVE为每一个源key生成一个srem事件,以及为每一个目标key生成一个sadd事件。
  • SPOP生成一个spop事件,此外,如果结果集合为空或者键被移除,将生成一个del事件。
  • SINTERSTORESUNIONSTORESDIFFSTORE分别生成sinterstoresunionostoresdiffstore事件。在特殊情况下,结果集是空的,并且存储结果的键已经存在,因为删除了键,所以会生成del事件。
  • ZINCR生成一个zincr事件。
  • ZADD生成一个zadd事件,即使添加了多个元素。
  • ZREM生成一个zrem事件,即使删除了多个元素。当结果有序集合为空且生成了键,则会生成额外的del事件。
  • ZREMBYSCORE生成一个zrembyscore事件。当结果有序集合为空且生成了键,则会生成额外的del事件。
  • ZREMBYRANK生成一个zrembyrank事件。当结果有序集合为空且生成了键,则会生成额外的del事件。
  • ZINTERSTOREZUNIONSTORE分别生成zinterstorezunionstore事件。在特殊情况下,结果有序集合是空的,并且存储结果的键已经存在,因为删除了键,所以会生成del事件。
  • 每次一个拥有过期时间的键由于过期而从数据集中移除时,将生成一个expired事件。
  • 每次一个键由于maxmemory策略而被从数据集中驱逐,以便释放内存时,将生成一个evicted事件。
重要 所有命令仅在真正修改目标键时才生成事件。例如,使用SREM命令从集合中删除一个不存在的元素将不会改变键的值,因此不会生成任何事件。如果对某个命令如何生成事件有疑问,最简单的方法是自己观察:
$ redis-cli config set notify-keyspace-events KEA
$ redis-cli --csv psubscribe '__key*__:*'
Reading messages... (press Ctrl-C to quit)
"psubscribe","__key*__:*",1
此时,在另外一个终端使用redis-cli发送命令到Redis服务器,并观察生成的事件:
"pmessage","__key*__:*","__keyspace@0__:foo","set"
"pmessage","__key*__:*","__keyevent@0__:set","foo"
...

过期事件的时间安排

设置了生存时间的键由Redis以两种方式过期:
  • 当命令访问键时,发现键已过期。
  • 通过后台系统在后台逐步查找过期的键,以便能够收集那些从未被访问的键。
当通过以上系统之一访问键且发现键已经过期时,将生成expired事件。因此无法保证Redis服务器在键过期的那一刻同时生成expired事件。如果没有命令不断地访问键,并且有很多键都有关联的TTL,那么在键的生存时间降至零到生成expired事件之间,将会有明显的延迟。基本上,expired事件是在Redis服务器删除键的时候生成的,而不是在理论上生存时间达到零值时生成的。
分类: 专题 标签:

Redis分布式锁

2020年3月21日 没有评论

分布式锁在很多场景中是非常有用的原语, 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。

有很多分布式锁的库和描述怎么实现分布式锁管理器(DLM)的博客,但是每个库的实现方式都不太一样,很多库的实现方式为了简单降低了可靠性,而有的使用了稍微复杂的设计。

这个页面试图提供一个使用Redis实现分布式锁的规范算法。我们提出一种算法,叫Redlock,我们认为这种实现比普通的单实例实现更安全,我们希望redis社区能帮助分析一下这种实现方法,并给我们提供反馈。

实现细节

在我们开始描述算法之前,我们已经有一些可供参考的实现库.

安全和活性失效保障

按照我们的思路和设计方案,算法只需具备3个特性就可以实现一个最低保障的分布式锁。

  1. 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
  2. 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
  3. 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

为什么基于故障转移的实现还不够

为了更好的理解我们想要改进的方面,我们先分析一下当前大多数基于Redis的分布式锁现状和实现方法.

实现Redis分布式锁的最简单的方法就是在Redis中创建一个key,这个key有一个失效时间(TTL),以保证锁最终会被自动释放掉(这个对应特性2)。当客户端释放资源(解锁)的时候,会删除掉这个key。

从表面上看,似乎效果还不错,但是这里有一个问题:这个架构中存在一个严重的单点失败问题。如果Redis挂了怎么办?你可能会说,可以通过增加一个slave节点解决这个问题。但这通常是行不通的。这样做,我们不能实现资源的独享,因为Redis的主从同步通常是异步的。

在这种场景(主从结构)中存在明显的竞态:

  1. 客户端A从master获取到锁
  2. 在master将锁同步到slave之前,master宕掉了。
  3. slave节点被晋级为master节点
  4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

有时候程序就是这么巧,比如说正好一个节点挂掉的时候,多个客户端同时取到了锁。如果你可以接受这种小概率错误,那用这个基于复制的方案就完全没有问题。否则的话,我们建议你实现下面描述的解决方案。

单Redis实例实现分布式锁的正确方法

在尝试克服上述单实例设置的限制之前,让我们先讨论一下在这种简单情况下实现分布式锁的正确做法,实际上这是一种可行的方案,尽管存在竞态,结果仍然是可接受的,另外,这里讨论的单实例加锁方法也是分布式加锁算法的基础。

获取锁使用命令:

SET resource_name my_random_value NX PX 30000

这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。

value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

使用这种方式释放锁可以避免删除别的客户端获取成功的锁。举个例子:客户端A取得资源锁,但是紧接着被一个其他操作阻塞了,当客户端A运行完毕其他操作后要释放锁时,原来的锁早已超时并且被Redis自动释放,并且在这期间资源锁又被客户端B再次获取到。如果仅使用DEL命令将key删除,那么这种情况就会把客户端B的锁给删除掉。使用Lua脚本就不会存在这种情况,因为脚本仅会删除value等于客户端A的value的key(value相当于客户端的一个签名)。

这个随机字符串应该怎么设置?我认为它应该是从/dev/urandom产生的一个20字节随机数,但是我想你可以找到比这种方法代价更小的方法,只要这个数在你的任务中是唯一的就行。例如一种安全可行的方法是使用/dev/urandom作为RC4的种子和源产生一个伪随机流;一种更简单的方法是把以毫秒为单位的unix时间和客户端ID拼接起来,理论上不是完全安全,但是在多数情况下可以满足需求.

key的失效时间,被称作“锁定有效期”。它不仅是key自动失效时间,而且还是一个客户端持有锁多长时间后可以被另外一个客户端重新获得。

截至到目前,我们已经有较好的方法获取锁和释放锁。基于Redis单实例,假设这个单实例总是可用,这种方法已经足够安全。现在让我们扩展一下,假设Redis没有总是可用的保障。

Redlock算法

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

这个算法是异步的么?

算法基于这样一个假设:虽然多个进程之间没有时钟同步,但每个进程都以相同的时钟频率前进,时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近:每个计算机都有一个本地时钟,我们可以容忍多个计算机之间有较小的时钟漂移。

从这点来说,我们必须再次强调我们的互相排斥规则:只有在锁的有效时间(在步骤3计算的结果)范围内客户端能够做完它的工作,锁的安全性才能得到保证(锁的实际有效时间通常要比设置的短,因为计算机之间有时钟漂移的现象)。.

想要了解更多关于需要时钟漂移间隙的相似系统, 这里有一个非常有趣的参考: Leases: an efficient fault-tolerant mechanism for distributed file cache consistency.

失败时重试

当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁)。同样,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该同时(并发地)向所有Redis发送SET命令。

需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到(然而,如果已经存在网络分裂,客户端已经无法和Redis实例通信,此时就只能等待key的自动释放了,等于被惩罚了)。

释放锁

释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

安全争议

这个算法安全么?我们可以从不同的场景讨论一下。

让我们假设客户端从大多数Redis实例取到了锁。所有的实例都包含同样的key,并且key的有效时间也一样。然而,key肯定是在不同的时间被设置上的,所以key的失效时间也不是精确的相同。我们假设第一个设置的key时间是T1(开始向第一个server发送命令前时间),最后一个设置的key时间是T2(得到最后一台server的答复后的时间),我们可以确认,第一个server的key至少会存活 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。所有其他的key的存活时间,都会比这个key时间晚,所以可以肯定,所有key的失效时间至少是MIN_VALIDITY。

当大部分实例的key被设置后,其他的客户端将不能再取到锁,因为至少N/2+1个实例已经存在key。所以,如果一个锁被(客户端)获取后,客户端自己也不能再次申请到锁(违反互相排斥属性)。

然而我们也想确保,当多个客户端同时抢夺一个锁时不能两个都成功。

如果客户端在获取到大多数redis实例锁,使用的时间接近或者已经大于失效时间,客户端将认为锁是失效的锁,并且将释放掉已经获取到的锁,所以我们只需要在有效时间范围内获取到大部分锁这种情况。在上面已经讨论过有争议的地方,在MIN_VALIDITY时间内,将没有客户端再次取得锁。所以只有一种情况,多个客户端会在相同时间取得N/2+1实例的锁,那就是取得锁的时间大于失效时间(TTL time),这样取到的锁也是无效的.

如果你能提供关于现有的类似算法的一个正式证明(指出正确性),或者是发现这个算法的bug? 我们将非常感激.

活性争议

系统的活性安全基于三个主要特性:

  1. 锁的自动释放(因为key失效了):最终锁可以再次被使用.
  2. 客户端通常会将没有获取到的锁删除,或者锁被取到后,使用完后,客户端会主动(提前)释放锁,而不是等到锁失效另外的客户端才能取到锁。.
  3. 当客户端重试获取锁时,需要等待一段时间,这个时间必须大于从大多数Redis实例成功获取锁使用的时间,以最大限度地避免脑裂。.

然而,当网络出现问题时系统在失效时间(TTL)内就无法服务,这种情况下我们的程序就会为此付出代价。如果网络持续的有问题,可能就会出现死循环了。 这种情况发生在当客户端刚取到一个锁还没有来得及释放锁就被网络隔离.

如果网络一直没有恢复,这个算法会导致系统不可用.

性能,崩溃恢复和Redis同步

很多用户把Redis当做分布式锁服务器,使用获取锁和释放锁的响应时间,每秒钟可用执行多少次 acquire / release 操作作为性能指标。为了达到这一要求,增加Redis实例当然可用降低响应延迟(没有钱买硬件的”穷人”,也可以在网络方面做优化,使用非阻塞模型,一次发送所有的命令,然后异步的读取响应结果,假设客户端和redis服务器之间的RTT都差不多。

然而,如果我们想使用可以从备份中恢复的redis模式,有另外一种持久化情况你需要考虑,.

我们考虑这样一种场景,假设我们的redis没用使用备份。一个客户端获取到了3个实例的锁。此时,其中一个已经被客户端取到锁的redis实例被重启,在这个时间点,就可能出现3个节点没有设置锁,此时如果有另外一个客户端来设置锁,锁就可能被再次获取到,这样锁的互相排斥的特性就被破坏掉了。

如果我们启用了AOF持久化,情况会好很多。我们可用使用SHUTDOWN命令关闭然后再次重启。因为Redis到期是语义上实现的,所以当服务器关闭时,实际上还是经过了时间,所有(保持锁)需要的条件都没有受到影响. 没有受到影响的前提是redis优雅的关闭。停电了怎么办?如果redis是每秒执行一次fsync,那么很有可能在redis重启之后,key已经丢弃。理论上,如果我们想在Redis重启地任何情况下都保证锁的安全,我们必须开启fsync=always的配置。这反过来将完全破坏与传统上用于以安全的方式实现分布式锁的同一级别的CP系统的性能.

然而情况总比一开始想象的好一些。当一个redis节点重启后,只要它不参与到任意当前活动的锁,没有被当做“当前存活”节点被客户端重新获取到,算法的安全性仍然是有保障的。

为了达到这种效果,我们只需要将新的redis实例,在一个TTL时间内,对客户端不可用即可,在这个时间内,所有客户端锁将被失效或者自动释放.

使用延迟重启可以在不采用持久化策略的情况下达到同样的安全,然而这样做有时会让系统转化为彻底不可用。比如大部分的redis实例都崩溃了,系统在TTL时间内任何锁都将无法加锁成功。

使算法更加可靠:锁的扩展

如果你的工作可以拆分为许多小步骤,可以将有效时间设置的小一些,使用锁的一些扩展机制。在工作进行的过程中,当发现锁剩下的有效时间很短时,可以再次向redis的所有实例发送一个Lua脚本,让key的有效时间延长一点(前提还是key存在并且value是之前设置的value)。

客户端扩展TTL时必须像首次取得锁一样在大多数实例上扩展成功才算再次取到锁,并且是在有效时间内再次取到锁(算法和获取锁是非常相似的)。

这样做从技术上将并不会改变算法的正确性,所以扩展锁的过程中仍然需要达到获取到N/2+1个实例这个要求,否则活性特性之一就会失效。

想要得到帮助?

如果你正在做分布式系统,你的意见和分析非常重要。其他语言实现分布式锁的算法同样非常宝贵。

提前感谢各位!

Redlock 分析

  1. Martin Kleppmann 在这儿分析了Redlock. 我不赞同他的说法,并且对他做出了回复 我的回复在这儿.
分类: 专题 标签:

Redis 延迟监控

2019年7月26日 没有评论

每个Redis实例经常被用于每时每刻都要提供大量查询服务的场景,同时,对平均响应时间和最大响应延迟的要求都非常严格。

当Redis用作内存系统时,它以不同的方式与操作系统进行交互,例如,持久化数据到磁盘上。再者,Redis实现了丰富的命令集。大部分命令执行都很快,能在确定时间内或对数时间内完成(译者注;对数时间是时间复杂度的一种),另外有些命令则是复杂度为O(N)的命令,会导致延迟毛刺(latency spikes)。

最后,Redis是单线程的:以查看单核处理量的观点来看,单线程通常被认为是优点,并且能够提供延迟的概况,但同时,从延迟本身的观点来看,单线程也会带来挑战,因为单线程只能逐个处理任务,例如,对key过期时间的处理,不会影响到其他客户端。

综上所虑,Redis 2.8.13引入延迟监控(Latency Monitoring)的新特性,帮助用户检查和排除可能的延迟问题。延迟监控由以下概念部分组成:

  • 延迟钩子(Latency hooks):检测不同敏感度延迟的代码路径。
  • 以不同事件分隔的延迟毛刺的时间序列记录。
  • 报告引擎:从时间序列记录中提取原始数据。
  • 分析引擎:提供易懂的报表和按测量结果给出的提示。

事件和时间序列

不同监控代码路径有不同的名称,并称之为事件。例如,command是测量可能很慢的命令的执行延迟毛刺的事件,而fast-command则是监控时间复杂度为O(1)和O(log N)的命令的事件名称。其他事件则不太通用,主要监控Redis执行的特殊操作。例如,fork事件仅仅监控Redis执行系统调用fork(2)所耗的时间。

延迟毛刺是指运行时间比配置的延迟阀值更长的事件。每个监控事件会关联一个独立的时间序列。下面说明时间序列如何工作的:

  • 每次出现延迟毛刺,会记录合适的时间序列
  • 每个时间序列由160个元素组成
  • 每个元素是一个值对:包含检测到延迟毛刺出现时的unix时间戳和事件耗时的毫秒数
  • 相同事件在同一时刻出现多个延迟毛刺,将会被合并(取最大延迟),因此,即使给定事件被检测到出现连续延迟毛刺,例如,由于用户设定了非常低的延迟阀值,将只会保留180秒的历史记录。
  • 对于每一个元素记录最大的延迟时间。

如何启用延迟监控

某种用例出现高延迟,对另外的用例可能不会出现高延迟。当一些应用的所有查询的响应延迟都必须少于1毫秒时,而另一些应用的客户端有很小比例出现2秒的延迟,这种情况是可以接受的。

因此,启动延迟监控的第一步是以毫秒为单位设置延迟阀值(latency threshold)。仅当事件耗时超过指定的延迟阀值才会记录延迟毛刺。用户可根据需要来设置延迟阀值。例如,如果基于Redis的应用能接受的最大延迟是100毫秒,则延迟阀值应当设置为大于或等于100毫秒,以便记录所有阻塞Redis服务器的事件。

在生产服务器上,通过下面的命令可以在运行时启用延迟监控:

CONFIG SET latency-monitor-threshold 100

延迟监控默认是关闭状态,即使延迟监控处理几乎不耗时。然而,当延迟监控只需非常小的内存时,则没有必要为一个运行良好的Redis实例提高基线内存使用量(baseline memory usage)。

延迟命令(LATENCY)的信息报告

延迟监控子系统的用户界面是LATENCY命令。像其他Redis命令一样,LATENCY可以接收子命令来改变命令的行为。接下来的章节对各个子命令进行说明。

LATENCY LATEST

LATENCY LATEST命令会上报已记录事件的最后一次延迟。每个事件包含以下字段:

  • 事件名称
  • 事件出现延迟毛刺的Unix时间戳
  • 最后事件延迟(单位:毫秒)
  • 本事件的最大延迟(所有时间段)

最大事件延迟所指的所有时间段并不是指从Redis实例启动以来,因为事件数据有可能会用稍后看到的LATENCY RESET命令来重置。

下面是输出样例:

127.0.0.1:6379> debug sleep 1
OK
(1.00s)
127.0.0.1:6379> debug sleep .25
OK
127.0.0.1:6379> latency latest
1) 1) "command"
   2) (integer) 1405067976
   3) (integer) 251
   4) (integer) 1001

LATENCY HISTORY event-name

LATENCY HISTORY命令从时间序列中提取原始数据时是非常有用的,数据是“时间戳-延迟”值对的形式。本命令会返回给定事件的160个元素。应用程序可用提取的原始数据来实现性能监控和展示图表等功能。

输出样例:

127.0.0.1:6379> latency history command
1) 1) (integer) 1405067822
   2) (integer) 251
2) 1) (integer) 1405067941
   2) (integer) 1001

LATENCY RESET [event-name … event-name]

LATENCY RESET命令,若不带参数,则重置所有事件,丢弃当前记录的延迟毛刺事件并重置最大事件延迟的值。

通过指定事件名称作为参数,可以重置指定的事件。该命令在执行期间,会返回已被重置的事件时间序列的序号。

LATENCY GRAPH event-name

为指定事件生成字符画(ASCII-art style graph):

127.0.0.1:6379> latency reset command
(integer) 0
127.0.0.1:6379> debug sleep .1
OK
127.0.0.1:6379> debug sleep .2
OK
127.0.0.1:6379> debug sleep .3
OK
127.0.0.1:6379> debug sleep .5
OK
127.0.0.1:6379> debug sleep .4
OK
127.0.0.1:6379> latency graph command
command - high 500 ms, low 101 ms (all time high 500 ms)
--------------------------------------------------------------------------------
   #_
  _||
 _|||
_||||

11186
542ss
sss

字符画每列下面的垂直标签表示事件是多久之前发生的,可用秒、分钟、小时或天来表示时间单位。例如,”15s”表示上图中第1个图形化的事件发生在15秒钟之前。

图形对最小值和最大值的图例进行了规范化定义,即0表示最小值(即最低一行的下划线),最高一行的#表示最大值。

graph子命令非常有用,能快速判断指定事件的延迟趋势,不需要使用额外的工具,也不需要解析LATENCY HISTORY命令提供的原始数据。

LATENCY DOCTOR

LATENCY DOCTOR命令是最强大的延迟监控分析工具,能提供更多统计数据,如延迟毛刺间的平均时间间隔,中值偏差和易懂的事件分析。对某些事件,如”fork”事件,会提供如系统创建进程的速率等更多的信息。

如果想寻求延迟相关问题的帮助,应当把本命令的输出内容发送到Redis邮件列表。

输出样例:

127.0.0.1:6379> latency doctor

Dave,我在Redis实例中已发现延迟毛刺。
您不介意谈论一下,Dave,是吧?

1. command: 5次延迟毛刺(平均 300毫秒,中位偏差 120毫秒,持续时间 73.40秒)。最大事件延迟500毫秒。

我给您如下几点建议:

- 当前配置仅记录比您配置的延迟监控阀值低的事件。请执行命令:'CONFIG SET slowlog-log-slower-than 1000'。
- 检查已记录的日志,以便了解哪些命令的执行特别慢。请访问http://redis.cn/commands/slowlog.html获取更多信息。
- 对大对象进行删除、过期和淘汰操作(由于最大内存策略导致)都属于阻塞操作。如果经常对大对象进行删除、过期和淘汰操作,可尝试把大对象拆分成多个小对象。

LATENCY DOCTOR命令目前尚不完善(原意:有精神病行为),因此,我们建议谨慎使用该命令。

分类: 专题 标签:

Redis 信号处理

2019年7月26日 没有评论

本文档提供的信息是有关Redis是如何应对不同POSIX系统下产生的信号异常,比如SIGTERM,SIGSEGV等等。

本文档中的信息只适用于Redis2.6或更高版本

SIGTERM信号的处理

SIGTERM信号会让Redis安全的关闭。Redis收到信号时并不立即退出,而是开启一个定时任务,这个任务就类似执行一次SHUTDOWN命令的。 这个定时关闭任务会在当前执行命令终止后立即施行,因此通常有有0.1秒或更少时间延迟。

万一Server被一个耗时的LUA脚本阻塞,如果这个脚本可以被SCRIPT KILL命令终止,那么定时执行任务就会在脚本被终止后立即执行,否则直接执行。

这种情况下的Shutdown过程也会同时包含以下的操作:

  • 如果存在正在执行RDB文件保存或者AOF重写的子进程,子进程被终止。
  • 如果AOF功能是开启的,Redis会通过系统调用fsync将AOF缓冲区数据强制输出到硬盘。
  • 如果Redis配置了使用RDB文件进行持久化,那么此时就会进行同步保存。由于保存时同步的,那也就不需要额外的内存。
  • 如果Server是守护进程,PID文件会被移除。
  • 如果Unix域的Socket是可用的,它也会被移除。
  • Server退出,退出码为0.

万一RDB文件保存失败,Shutdown失败,Server则会继续运行以保证没有数据丢失。自从Redis2.6.11之后,Redis不会再次主动Shutdown,除非它接收到了另一个SIGTERM信号或者另外一个SHUTDOWN命令

SIGSEGV,SIGBUS,SIGFPF和SIGILL信号的处理

Redis接收到以下几种信号时会崩溃:

  • SIGSEGV
  • SIGBUS
  • SIGFPE
  • SIGILL

如果以上信号被捕获,Redis会终止所有正在进行的操作,并进行以下操作:

  • 包括调用栈信息,寄存器信息,以及clients信息会以bug报告的形式写入日志文件。
  • 自从Redis2.8(当前为开发版本)之后,Redis会在系统崩溃时进行一个快速的内存检测以保证系统的可靠性。
  • 如果Server是守护进程,PID文件会被移除。
  • 最后server会取消自己对当前所接收信号的信号处理器的注册,并重新把这个信号发给自己,这是为了保证一些默认的操作被执行,比如把Redis的核心Dump到文件系统。

子进程被终止时会发生什么

当一个正在进行AOF重写的子进程被信号终止,Redis会把它当成一个错误并丢弃这个AOF文件(可能是部分或者完全损坏)。AOF重写过程会在以后被重新触发。

当一个正在执行RDB文件保存的子进程被终止Redis会把它当做一个严重的错误,因为AOF重写只会导致AOF文件冗余,但是RDB文件保存失败会导致Redis不可用。

如果一个正在保存RDB文件的子进程被信号终止或者自身出现了错误(非0退出码),Redis会进入一种特殊的错误状态,不允许任何写操作。

  • Redis会继续回复所有的读请求。
  • Redis会回复给所有的写请求一个MISCONFIG错误。

此错误状态只需被清楚一次就可以进行成功创建数据库文件。

不触发错误状态的情况下终止RDB文件的保存

但是有时用户希望可以在不触发错误的情况下终止保存RDB文件的子进程。自从Redis2.6.10之后就可以使用信号SIGUSR1,这个信号会被特殊处理:它会像其他信号一样终止子进程,但是父进程不会检测到这个严重的错误,照常接收所有的用户写请求。

分类: 专题 标签: