上一篇文章我们对redis的基本类型及相应的应用场景做了了解,那你知道redis 为什么这么快吗?redis 宕机了怎么办?redis 的持久化方式有哪些?如果你是云服务厂商,你怎么优化你的云版的redis呢?
本文将对redis 的相关原理进行分析。
线程 IO 模型
redis 是单线程程序的,除了 Redis 之外,Node.js 也是单线程,Nginx 也是单线程,但是它们都是服务器高性能的典范。
redis 单线程也这么快,也要归功于非阻塞IO和 多路复用器。
非阻塞IO
能读多少读多少,取决于内核为套接字分配的读缓冲区内部的数据字节数。
能写多少写多少,取决于内核为套接字分配的写缓存区空闲空间的字节数。
事件轮询(多路复用)
非阻塞 IO 有个问题,那就是线程要读数据,结果读了一部分就返回了,线程如何知道
何时才应该继续读。也就是当数据到来时,线程如何得到通知。写也是一样,如果缓冲区满
了,写不完,剩下的数据何时才应该继续写,线程也应该得到通知。
最简单的事件轮询API,如下图所示。select函数,它是操作系统提供的。在描述符特别多的情况下,性能特别差。现代的操作系统多路复用改为:epoll(linux) kqueue(freeBsd 和 macosx)。
目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,pselect,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select 和epoll的区别
select
首先是垮平台的,select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
epoll
是2.6 内核提出的,是 select 和poll 的增强版,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
它的原理其实就是epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,
一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
它的有点优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
单线程如何处理相应请求?
通过 指令队列,相应队列,和定时任务的设计来相应请求。
指令队列
客户端指令通过队列来排队进行顺序处理,先到先服务
响应队列
等队列里面有数据了再把客户端fd 放进去,避免浪费cpu
定时任务
服务器处理要响应 IO 事件外,还要处理其它事情。比如定时任务就是非常重要的一件事。如果线程阻塞在 select 系统调用上,定时任务将无法得到准时调度。
那 Redis 是如何解决这个问题的呢?
Redis 的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期,Redis 都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调用的 timeout 参数。因为 Redis 知道未来 timeout 时间内,没有其它定时任务需要处理,所以可以安心睡眠 timeout 的时间。
Nginx 和 Node 的事件处理原理和 Redis 也是类似的。
通信协议
Redis 的请求也相当于一次rpc 的调用,rpc中,通信协议的设计是非常重要的。,Redis 的作者认为数据库系统的瓶颈一般不在于网络流量,而是数据库自身内部逻辑处理上。所以即使 Redis 使用了浪费流量的文本协议,依然可以取得极高的访问性能。Redis
将所有数据都放在内存,用一个单线程对外提供服务,单个节点在跑满一个 CPU 核心的情况下可以达到了 10w/s 的超高 QPS。
RESP(Redis Serialization Protocol)
RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简
单,解析性能极好。Redis 协议里有大量冗余的回车换行符,但是这不影响它成为互联网技术领域非常受欢
迎的一个文本协议。有很多开源项目使用 RESP 作为它的通讯协议。在技术领域性能并不总
是一切,还有简单性、易理解性和易实现性,这些都需要进行适当权衡。
关于持久化
一款优秀的中间件都会考虑到持久化,Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制
Redis 的持久化机制有两种,第一种是RDB(快照)(Redis DataBase),第二种是 AOF(Append Only File)日志。
RDB原理:
redis 使用操作系统的多进程(cow copy on write【写时复制】)机制来实现快照的持久化;手动命令save,bgsave;配置文件设置.
fork 多进程
调用glibc 的函数 fork产生一个子进程,快照持久化交个子进程处理.子进程刚刚产生时和父进程共享代码段和数据段,随着父进程修改操作持续进行,more 共享页面被分离出来,创建进程变快;内存增长,不会超过原来的2倍.另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面子进程的数据没有变化,可以安心的遍历数据,进行序列化写磁盘。
AOF原理
AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。随着操作的进行,指令文本会越来越大。需要对AOF 重写,进行日志瘦身,redis 提供bgrewriteaof 指令对AOF 进行瘦身,原理是开辟一个子进程对内存进行遍历,序列化一个新的aof+这段时间的增量。Linux 的glibc 提供了 fsync(int fd) 强制从内核缓存刷到磁盘,fsync 很慢,一般生产环境每隔一秒执行一次,可配置。
redis 是先执行指令再写日志,leveldb\hbase 相反。
关于redis 运维的一些经验,不要在主节点进行持久化,要在从节点进行持久化,因为持久化是一个很耗时的IO操作。
关于混合持久化
为什么要进行混合持久化?首先fork 动作需要copy 页表,大内存场景下阻塞server,百ms 或秒级 latency spike(延迟尖峰),比较适合每天定时的 全备场景。部分云厂商的方案比如:redis+rocksDB。从以下几个方面来分析。
数据写入
写入Redis 内存,并记录增量,后台线程异步应用增量到RocksDB
数据可靠性
RocksDB 包含全量数据,彻底避免Fork 调用,启动时,从RocksDB 加载全量索引信息到内存
数据读取
数据在redis 内存,直接读取,数据不在Redis 内存,整体换入Redis 再读取,针对简单命令,可以直接从Rocks DB 读取。
关于RcoksDB,RcoksDB可嵌入的,持久型的key-value 存储,
RocksDB项目起源于Facebook的一个实验项目,该项目旨在开发一个与快速存储器(尤其是闪存)存储数据性能相当的数据库软件,以应对高负载服务。这是一个c++库,可用于存储键和值,可以是任意大小的字节流。它支持原子读和写。RocksDB具有高度灵活的配置功能,可以通过配置使其运行在各种各样的生产环境,包括纯内存,Flash,硬盘或HDFS。它支持各种压缩算法,并提供了便捷的生产环境维护和调试工具。
数据结构编码
每个key对应一个RocksDB metakey,存储key 的元数据。所有的metakey 相邻存储,启动加载效率高,复杂key的每个元素在RocksDB 里面对应一个Datakey,元素在RocksDB 相邻存储,访问效率高。简化String 类型,metakey 与Datakey 合并优化IO。
数据交换模型(SWAP MODE)
内存数据换出:后台定期检查是否超哥内存使用阀值,根据访问评率、大小等选择key 预备淘汰。
磁盘数据换入:某个key 被频繁读取或有o(n) 的读取操作,整个key 换入的RocksDB,加速访问;key 写入时,数据不再内存,整体换入内存;默认后台多线程异步换入。
主备同步&迁移
全量同步:使用RocksDB checkoutpoint 代替RDB,避免fork 系统调用,备接收到checkoutpoint 数据只需加载meta key
增量同步:从 checkoutpoint 对应aof binlog 位点继续增量同步,增量同步断开,直接从断开位置继续同步,无需触发全量同步。
一些其他的知识
管道
技术的本质:客户端提供,pipline 包含多条指令;客户端改变管道中的指令列表的读写顺序,可以大幅节省IO 时间。自带压力测试工具 redis-beanchmark。普通 set 5w qps
小对象压缩
如果redis 使用内存不超过4G,使用32bit 编译。