点击上方“咸鱼学Python”,选择“加为星标”html
第一时间关注Python技术干货!
node
来源:rrd.me/gteACpython
嘿,我是咸鱼,以前给你们推荐过关于 redis 的很多干货,此次再一块儿学习一下 Redis 的性能分析。linux
Python | Python学习之Redis交互详解
ios
硬核!16000 字 Redis 面试知识点总结,建议收藏!
程序员
在一些网络服务的系统中,Redis 的性能,多是比 MySQL 等硬盘数据库的性能更重要的课题。好比微博,把热点微博[1],最新的用户关系,都存储在 Redis 中,大量的查询击中 Redis,而不走 MySQL。github
那么,针对 Redis 服务,咱们能作哪些性能优化呢?或者说,应该避免哪些性能浪费呢?web
Redis 性能的基本面
在讨论优化以前,咱们须要知道,Redis 服务自己就有一些特性,好比单线程运行。除非修改 Redis 的源代码,否则这些特性,就是咱们思考性能优化的基本面。面试
那么,有哪些 Redis 基本特性须要咱们考虑呢?Redis 的项目介绍中归纳了它特性:
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported.
首先,Redis 使用操做系统提供的虚拟内存来存储数据。并且,这个操做系统通常就是指 Unix。Windows 上也能运行 Redis,可是须要特殊处理。若是你的操做系统使用交换空间,那么 Redis 的数据可能会被实际保存在硬盘上。
其次,Redis 支持持久化,能够把数据保存在硬盘上。不少时候,咱们也确实有必要进行持久化来实现备份,数据恢复等需求。但持久化不会凭空发生,它也会占用一部分资源。
第三,Redis 是用 key-value 的方式来读写的,而 value 中又能够是不少不一样种类的数据;更进一步,一个数据类型的底层还有被存储为不一样的结构。不一样的存储结构决定了数据增删改查的复杂度以及性能开销。
最后,在上面的介绍中没有提到的是,Redis 大多数时候是单线程运行[2]的(single-threaded),即同一时间只占用一个 CPU,只能有一个指令在运行,并行读写是不存在的。不少操做带来的延迟问题,均可以在这里找到答案。
关于最后这个特性,为何 Redis 是单线程的,却能有很好的性能(根据 Amdahl’s Law,优化耗时占比大的过程,才更有意义),两句话归纳是:Redis 利用了多路 I/O 复用机制[3],处理客户端请求时,不会阻塞主线程;Redis 单纯执行(大多数指令)一个指令不到 1 微秒[4],如此,单核 CPU 一秒就能处理 1 百万个指令(大概对应着几十万个请求吧),用不着实现多线程(网络才是瓶颈[5])。
优化网络延时
Redis 的官方博客在几个地方都说,性能瓶颈更多是网络[6],那么咱们如何优化网络上的延时呢?
首先,若是大家使用单机部署(应用服务和 Redis 在同一台机器上)的话,使用 Unix 进程间通信来请求 Redis 服务,速度比 localhost 局域网(学名 loopback)更快。官方文档[7]是这么说的,想想,理论上也应该是这样的。
但不少公司的业务规模不是单机部署能支撑的,因此仍是得用 TCP。
Redis 客户端和服务器的通信通常使用 TCP 长连接。若是客户端发送请求后须要等待 Redis 返回结果再发送下一个指令,客户端和 Redis 的多个请求就构成下面的关系:
(备注:若是不是你要发送的 key 特别长,一个 TCP 包彻底能放下 Redis 指令,因此只画了一个 push 包)
这样这两次请求中,客户端都须要经历一段网络传输时间。
但若是有可能,彻底可使用 multi-key 类的指令来合并请求,好比两个 GET key
能够用 MGET key1 key2
合并。这样在实际通信中,请求数也减小了,延时天然获得好转。
若是不能用 multi-key 指令来合并,好比一个 SET
,一个 GET
没法合并。怎么办?
Redis 中有至少这样两个方法能合并多个指令到一个 request 中,一个是 MULTI/EXEC
,一个是 script。前者原本是构建 Redis 事务的方法,但确实能够合并多个指令为一个 request,它到通信过程以下。至于 script,最好利用缓存脚本的 sha1 hash key 来调起脚本,这样通信量更小。
这样确实更能减小网络传输时间,不是么?但如此以来,就必需要求这个 transaction / script 中涉及的 key 在同一个 node 上,因此要酌情考虑。
若是上面的方法咱们都考虑过了,仍是没有办法合并多个请求,咱们还能够考虑合并多个 responses。好比把 2 个回复信息合并:
这样,理论上能够省去 1 次回复所用的网络传输时间。这就是 pipeline 作的事情。举个 ruby 客户端使用 pipeline 的例子:
require 'redis'
@redis = Redis.new()
@redis.pipelined do
@redis.get 'key1'
@redis.set 'key2' 'some value'
end
# => [1, 2]
听说,有些语言的客户端,甚至默认就使用 pipeline 来优化延时问题,好比 node_redis。
另外,不是任意多个回复信息均可以放进一个 TCP 包中,若是请求数太多,回复的数据很长(好比 get 一个长字符串),TCP 仍是会分包传输,但使用 pipeline,依然能够减小传输次数。
pipeline 和上面的其余方法都不同的是,它不具备原子性。因此在 cluster 状态下的集群上,实现 pipeline 比那些原子性的方法更有可能。
小结一下:
-
使用 unix 进程间通讯,若是单机部署 -
使用 multi-key 指令合并多个指令,减小请求数,若是有可能的话 -
使用 transaction、script 合并 requests 以及 responses -
使用 pipeline 合并 response
警戒执行时间长的操做
在大数据量的状况下,有些操做的执行时间会相对长,好比 KEYS *
,LRANGE mylist 0 -1
,以及其余算法复杂度为 O(n) 的指令。由于 Redis 只用一个线程来作数据查询,若是这些指令耗时很长,就会阻塞 Redis,形成大量延时。
尽管官方文档中说 KEYS *
的查询挺快的,(在普通笔记本上)扫描 1 百万个 key,只需 40 毫秒(参见:https://redis.io/commands/keys),但几十 ms 对于一个性能要求很高的系统来讲,已经不短了,更况且若是有几亿个 key(一台机器彻底可能存几亿个 key,好比一个 key 100字节,1 亿个 key 只有 10GB),时间更长。
因此,尽可能不要在生产环境的代码使用这些执行很慢的指令,这一点 Redis 的做者在博客[8]中也提到了。另外,运维同窗查询 Redis 的时候也尽可能不要用。甚至,Redis Essential 这本书建议利用 rename-command KEYS ''
来禁止使用这个耗时的指令。
除了这些耗时的指令,Redis 中 transaction,script,由于能够合并多个 commands 为一个具备原子性的执行过程,因此也可能占用 Redis 很长时间,须要注意。
若是你想找出生产环境使用的「慢指令」,那么能够利用 SLOWLOG GET count
来查看最近的 count 个执行时间很长的指令。至于多长算长,能够经过在 redis.conf 中设置 slowlog-log-slower-than 来定义。
除此以外,在不少地方都没有提到的一个可能的慢指令是 DEL
,但 redis.conf 文件的注释[9]中却是说了。长话短说就是 DEL 一个大的 object 时候,回收相应的内存可能会须要很长时间(甚至几秒),因此,建议用 DEL 的异步版本:UNLINK
。后者会启动一个新的 thread 来删除目标 key,而不阻塞原来的线程。
更进一步,当一个 key 过时以后,Redis 通常也须要同步的把它删除。其中一种删除 keys 的方式是,每秒 10 次的检查一次有设置过时时间的 keys,这些 keys 存储在一个全局的 struct 中,能够用 server.db->expires
访问。检查的方式是:
-
从中随机取出 20 个 keys -
把过时的删掉。 -
若是刚刚 20 个 keys 中,有 25% 以上(也就是 5 个以上)都是过时的,Redis 认为,过时的 keys 还挺多的,继续重复步骤 1,直到知足退出条件:某次取出的 keys 中没有那么多过去的 keys。
这里对于性能的影响是,若是真的有不少的 keys 在同一时间过时,那么 Redis 真的会一直循环执行删除,占用主线程。
对此,Redis 做者的建议[10]是警戒 EXPIREAT
这个指令,由于它更容易产生 keys 同时过时的现象。我还见到过一些建议是给 keys 的过时时间设置一个随机波动量。最后,redis.conf 中也给出了一个方法,把 keys 的过时删除操做变为异步的,即,在 redis.conf 中设置 lazyfree-lazy-expire yes
。
优化数据结构、使用正确的算法
一种数据类型(好比 string,list)进行增删改查的效率是由其底层的存储结构决定的。
咱们在使用一种数据类型时,能够适当关注一下它底层的存储结构及其算法,避免使用复杂度过高的方法。举两个例子:
-
ZADD
的时间复杂度是 O(log(N)),这比其余数据类型增长一个新元素的操做更复杂,因此要当心使用。 -
若 Hash 类型的值的 fields 数量有限,它颇有可能采用 ziplist 这种结构作存储,而 ziplist 的查询效率可能没有同等字段数量的 hashtable 效率高,在必要时,能够调整 Redis 的存储结构。
除了时间性能上的考虑,有时候咱们还须要节省存储空间。好比上面提到的 ziplist 结构,就比 hashtable 结构节省存储空间(Redis Essentials 的做者分别在 hashtable 和 ziplist 结构的 Hash 中插入 500 个 fields,每一个 field 和 value 都是一个 15 位左右的字符串,结果是 hashtable 结构使用的空间是 ziplist 的 4 倍。)。但节省空间的数据结构,其算法的复杂度可能很高。因此,这里就须要在具体问题面前作出权衡。欢迎关注公众号:朱小厮的博客,回复:1024,能够领取redis专属资料。
如何作出更好的权衡?我以为得深挖 Redis 的存储结构才能让本身安心。这方面的内容咱们下次再说。
以上这三点都是编程层面的考虑,写程序时应该注意啊。下面这几点,也会影响 Redis 的性能,但解决起来,就不仅是靠代码层面的调整了,还须要架构和运维上的考虑。
考虑操做系统和硬件是否影响性能
Redis 运行的外部环境,也就是操做系统和硬件显然也会影响 Redis 的性能。在官方文档中,就给出了一些例子:
-
CPU:Intel 多种 CPU 都比 AMD 皓龙系列好 -
虚拟化:实体机比虚拟机好,主要是由于部分虚拟机上,硬盘不是本地硬盘,监控软件致使 fork 指令的速度慢(持久化时会用到 fork),尤为是用 Xen 来作虚拟化时。 -
内存管理:在 linux 操做系统中,为了让 translation lookaside buffer,即 TLB,可以管理更多内存空间(TLB 只能缓存有限个 page),操做系统把一些 memory page 变得更大,好比 2MB 或者 1GB,而不是一般的 4096 字节,这些大的内存页叫作 huge pages。同时,为了方便程序员使用这些大的内存 page,操做系统中实现了一个 transparent huge pages(THP)机制,使得大内存页对他们来讲是透明的,能够像使用正常的内存 page 同样使用他们。但这种机制并非数据库所须要的,多是由于 THP 会把内存空间变得紧凑而连续吧,就像 mongodb 的文档 [11]中明确说的,数据库须要的是稀疏的内存空间,因此请禁掉 THP 功能。Redis 也不例外,但 Redis 官方博客上给出的理由是:使用大内存 page 会使 bgsave 时,fork 的速度变慢;若是 fork 以后,这些内存 page 在原进程中被修改了,他们就须要被复制(即 copy on write),这样的复制会消耗大量的内存(毕竟,人家是 huge pages,复制一份消耗成本很大)。因此,请禁止掉操做系统中的 transparent huge pages 功能。 -
交换空间:当一些内存 page 被存储在交换空间文件上,而 Redis 又要请求那些数据,那么操做系统会阻塞 Redis 进程,而后把想要的 page,从交换空间中拿出来,放进内存。这其中涉及整个进程的阻塞,因此可能会形成延时问题,一个解决方法是禁止使用交换空间(Redis Essentials 中如是建议,若是内存空间不足,请用别的方法处理)。
考虑持久化带来的开销
Redis 的一项重要功能就是持久化,也就是把数据复制到硬盘上。基于持久化,才有了 Redis 的数据恢复等功能。
但维护这个持久化的功能,也是有性能开销的。
首先说,RDB 全量持久化。
这种持久化方式把 Redis 中的全量数据打包成 rdb 文件放在硬盘上。可是执行 RDB 持久化过程的是原进程 fork 出来一个子进程,而 fork 这个系统调用是须要时间的,根据Redis Lab 6 年前作的实验[12],在一台新型的 AWS EC2 m1.small^13 上,fork 一个内存占用 1GB 的 Redis 进程,须要 700+ 毫秒,而这段时间,redis 是没法处理请求的。
虽然如今的机器应该都会比那个时候好,可是 fork 的开销也应该考虑吧。为此,要使用合理的 RDB 持久化的时间间隔,不要太频繁。
接下来,咱们看另一种持久化方式:AOF 增量持久化。
这种持久化方式会把你发到 redis server 的指令以文本的形式保存下来(格式遵循 redis protocol),这个过程当中,会调用两个系统调用,一个是 write(2)
,同步完成,一个是 fsync(2)
,异步完成。
这两部均可能是延时问题的缘由:
-
write 可能会由于输出的 buffer 满了,或者 kernal 正在把 buffer 中的数据同步到硬盘,就被阻塞了。 -
fsync 的做用是确保 write 写入到 aof 文件的数据落到了硬盘上,在一个 7200 转/分的硬盘上可能要延时 20 毫秒左右,消耗仍是挺大的。更重要的是,在 fsync 进行的时候,write 可能会被阻塞。
其中,write 的阻塞貌似只能接受,由于没有更好的方法把数据写到一个文件中了。但对于 fsync,Redis 容许三种配置,选用哪一种取决于你对备份及时性和性能的平衡:
-
always:当把 appendfsync 设置为 always,fsync 会和客户端的指令同步执行,所以最可能形成延时问题,但备份及时性最好。 -
everysec:每秒钟异步执行一次 fsync,此时 redis 的性能表现会更好,可是 fsync 依然可能阻塞 write,算是一个折中选择。 -
no:redis 不会主动出发 fsync (并非永远不 fsync,那是不太可能的),而由 kernel 决定什么时候 fsync
使用分布式架构 —— 读写分离、数据分片
以上,咱们都是基于单台,或者单个 Redis 服务进行优化。下面,咱们考虑当网站的规模变大时,利用分布式架构来保障 Redis 性能的问题。
首先说,哪些状况下不得不(或者最好)使用分布式架构:
-
数据量很大,单台服务器内存不可能装得下,好比 1 个 T 这种量级 -
须要服务高可用 -
单台的请求压力过大
解决这些问题能够采用数据分片或者主从分离,或者二者都用(即,在分片用的 cluster 节点上,也设置主从结构)。
这样的架构,能够为性能提高加入新的切入点:
-
把慢速的指令发到某些从库中执行 -
把持久化功能放在一个不多使用的从库上 -
把某些大 list 分片
其中前两条都是根据 Redis 单线程的特性,用其余进程(甚至机器)作性能补充的方法。
固然,使用分布式架构,也可能对性能有影响,好比请求须要被转发,数据须要被不断复制分发。(待查)
后话
其实还有不少东西也影响 Redis 的性能,好比 active rehashing(keys 主表的再哈希,每秒 10 次,关掉它能够提高一点点性能),可是这篇博客已经写的很长了。并且,更重要不是收集已经被别人提出的问题,而后记忆解决方案;而是掌握 Redis 的基本原理,以不变应万变的方式决绝新出现的问题。
参考资料
热点微博: https://www.infoq.cn/article/weibo-relation-service-with-redis
[2]单线程运行: https://redis.io/topics/latency#single-threaded-nature-of-redis
[3]多路 I/O 复用机制: https://redis.io/topics/clients#how-client-connections-are-accepted
[4]1 微秒: https://redis.io/topics/latency#redis-latency-problems-troubleshooting
[5]网络才是瓶颈: https://redis.io/topics/benchmarks#factors-impacting-redis-performance
[6]网络: https://redis.io/topics/latency#latency-induced-by-network-and-communication
[7]官方文档: https://redis.io/topics/benchmarks#factors-impacting-redis-performance
[8]博客: https://redis.io/topics/latency#i39ve-little-time-give-me-the-checklist
[9]注释: https://github.com/antirez/redis/blob/5.0/redis.conf#L669
[10]建议: https://redis.io/topics/latency#latency-generated-by-expires
[11]mongodb 的文档: https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
[12]实验: 参见:https://redis.io/topics/latency#fork-time-in-different-systems
Love & Share
[ 完 ]
朕已阅
本文分享自微信公众号 - 咸鱼学Python(xianyuxuepython)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。