Redis深入系列-线程IO模型1

一.Redis 到底有多快?

 Redis是基于内存的采用单进程和单线程模型的KV数据库,官方提供的压测数据可以达到100000+的QPS,这个不比采用单进程多线程的同样基于内存的KV数据库Memcached差;

官网给出的基准程序测试:https://redis.io/topics/benchmarks 

二.Redis 单线程为什么还能这么快?

  • 纯内存访问,所有数据都在内存中,所有的运算都是内存级别的运算,内存响应时间的时间为纳秒级别,这是redis达到万级每秒的基础。
  • 采用单线程,避免了不必要的上下文切换和竞争条件;不存在多线程导致的切换而消耗CPU,不用考虑各种锁的问题,不存在加锁和释放锁的的操作,没有因为可能出现的死锁而导致的性能消耗。(线程是需要内存开销的,1个线程可能需要2M 存放栈,1000个线程就需要2G的内存)
  • redis单线程如何处理那么多并发客户端连接?回答:非阻塞IO,使用I/O多路复用技术(linux下可以使用epoll/poll/select)。多路复用是指使用一个线程来检查多个文件符FD(socket,文件,管道)的就绪状态;比如select和poll函数,传入多个文件描述符,一个文件符就绪,则返回,否则阻塞直到超时。

      多路复用使是指:一个线程处理多个IO流。select/epoll机制,redis只运行在单线程的情况,允许内核中同时监听多个socket和已连接的Socket;

          

                                     

    这样在处理1000个连接的时候,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。                                           

 

                       

 

 

 

        说明:至选择哪种多路复用技术,在ae.c里有预处理的控制,也就是说,这些文件最后只有一个能够被编译。优先选择epoll或者kqueue(FREEBSD和Mac OSX可用)可用其次是select。服务器运行时主要关注两大类型的事件:文件事件和时间事件。文件事件指的是socket文件描述符FD的读写就绪情况,

 

 三  redis是单线程吗?

redis单线指的是,接收客户端请求->解析请求->进行数据读写操作->发送数据给客户端 这个过程是由一个线程(主线程)来完成的;

但是redis程序并不是单线程的,redis在启动的时候,会启动后台线程的;

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
  • redis 4.0版本以后,新增了一个新的后台线程,用来异步释放redis内存,也就是lazyfree线程。例如执行 unlink key/flushdb aysnc / flushall async 会把这些删除操作交给后台线程来执行,好处是不会导致redis的主线程卡顿。因此,当我们要删除一个大 key 的时候,可以不使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。

之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。

 

 关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:

  • BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
  • BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
  • BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;

 

四 redis单线程模式是怎么样得?

 

浅蓝色的部分是一个事件循环,是由主线程负责的,可以看到网络IO和命令处理都是单线程的;redis初始化的时候会做下面这几件事情:

1 调用epoll_create()创建一个epoll对象和调用socket()创建一个服务端的socket

2 调用bind()绑定端口和调用listen()监听该socket

3 将epoll_ctl将listen socket加入到epoll,同时注册连接事件;

初始化后主线程进入到一个事件循环函数,主要做的事件如下:

1 首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

2 接着,调用 epoll_wait 函数等待事件的到来:

  • 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
  • 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
  • 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

 

以上就是 Redis 单线模式的工作方式,如果你想看源码解析,可以参考这一篇:为什么单线程的 Redis 如何做到每秒数万 QPS ?(opens new window) 

高性能redis网络原理总结:

redis服务器端只需要单线程可达到非常高得处理能力,每秒可达到上万qps得高处理能力,网络原理其实是对linux提供得多路复用机制epoll得一个较为完美得应用而已;

reids得源码中,核心逻辑其实就是两个:

1 initServer启动服务

2 另外一个aeMain事件循环;

//file: src/server.c
int main(int argc, char **argv) {
    ......
    // 启动初始化
    initServer();
    // 运行事件处理循环,一直到服务器关闭为止
    aeMain(server.el);
}

initServer中干了三件重要得事情:

1 创建一个epoll对象

2 对配置得监听端口进行listen

3 把listen到的socket让epoll管理起来

在aeMain方法中是一个无休止的循环,每一次循环中,要做的事情如下图:

 

 

通过epoll_wait发现listen socket以及其他连接得可读、可写事件;

若发现listen socket上有新连接到达得时候,则接受新连接,并追加到epoll中进行管理

若发现socket上有命令请求到达,则读取和处理命令,把命令得结果写入到缓存中,加入到任务队列;

每一次进入epoll_wait前都调用beforesleep来将写任务队列中得数据实际进行发送;

如果beforesleep在将结果写回给客户端得时候,如果由于内核socket发送缓冲区过小而导致不能一次性发送完毕得时候,也会注册一个写事件处理器,等到epoll_wait发现对应得socket可写得时候,再执行write写处理;

 

 

 

五 redis6.0之前为什么使用单线程?

官方文档给出的解释:https://redis.io/docs/getting-started/faq/

 

How can Redis use multiple CPUs or cores?

It's not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, when using pipelining a Redis instance running on an average Linux system can deliver 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

However, to maximize CPU usage you can start multiple instances of Redis in the same box and treat them as different servers. At some point a single box may not be enough anyway, so if you want to use multiple CPUs you can start thinking of some way to shard earlier.

You can find more information about using multiple Redis instances in the Partitioning page.

As of version 4.0, Redis has started implementing threaded actions. For now this is limited to deleting objects in the background and blocking commands implemented via Redis modules. For subsequent releases, the plan is to make Redis more and more threaded.

 翻译:

CPU 成为 Redis 瓶颈的情况并不常见,因为通常 Redis 要么受内存限制,要么受网络限制。例如,当使用管道时,
在普通 Linux 系统上运行的 Redis 实例每秒可以传送 100 万个请求,因此,如果您的应用程序主要使用 O(N) 或 O(log(N)) 命令,则几乎不会使用很多CPU。
但是,为了最大限度地提高 CPU 使用率,您可以在同一个机器中启动多个 Redis 实例,并将它们视为不同的服务器。
在某些时候,单个盒子可能还不够,所以如果你想使用多个 CPU,你可以开始考虑一些更早的分片方法。
您可以在分区页面中找到有关使用多个 Redis 实例的更多信息。
从 4.0 版本开始,Redis 已经开始实现线程操作。目前,这仅限于在后台删除对象以及阻止通过 Redis 模块实现的命令。对于后续版本,计划是让 Redis 变得越来越线程化。

核心原因:CPU并不是制约redis性能表现的瓶颈所在。如果你想使用服务器多核CPU,可以一台服务器部署多个节点或者采用分片的集群模式;
使用单线程,可维护性高,多线程模型虽然在某些方面表现优异,但是引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了线程的切换,甚至加锁,解锁,死锁造成的性能损耗;

六 Redis 6.0 之后为什么引入了多线程?

在6.0之前redis主要程序工作(网络IO和执行命令)一直是单线程模型,但是在redis 6.0版本之后,也采用多个IO线程来处理网络请求,
这是因为随着网络硬件的性能提升,redis的性能瓶颈有时候会出现在网络IO处理上;
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理;但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。

   Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。

   redis 6.0版本支持 I/O多线程特性,默认情况下 I/O多线程只是针对发送响应数据(write client socket)并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理读请求,需要把redis.conf配置文件中的io-threads-do-reads 配置项设置为yes

 

//读请求也使用io多线程
io-threads-do-reads yes  

同时, Redis.conf 配置文件中提供了 IO 多线程个数的配置项。

// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
io-threads 4 

关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6线程数一定要小于机器核数,线程数并不是越大越好。

因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程这里的线程数不包括主线程):

  • Redis-server : Redis的主线程,主要负责执行命令;
  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务;
  • io_thd_1io_thd_2io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。

 

 

 

 参考文章:

【Redis 和 I/O 多路复用】: https://draveness.me/redis-io-multiplexing

【为什redis是单线程这么快】:https://blog.csdn.net/xlgen157387/article/details/79470556

【为什么单线程的 Redis 如何做到每秒数万 QPS 】https://mp.weixin.qq.com/s/oeOfsgF-9IOoT5eQt5ieyw

 

posted @ 2019-01-19 21:03  积淀  阅读(3683)  评论(0编辑  收藏  举报