Redis单/多线程?
转载来源:CSDN
redis真的是单线程的吗?
- redis的单线程,主要是指redis的网络IO和键值对读写是由一个线程来完成的
- 但是redis的其他功能,比如说持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
- 所以,严格来说,redis并不是单线程
那为什么网上会有“redis是单线程”的说法呢?
总的来讲,redis版本有两个重要节点:
- Redisv4.0:引入多线程处理异步任务
- Redisv6.0:在网络模型中实现多线程IO
所以说:
- 网络上说的redis是单线程,通常是指在redis6.0之前,其核心网络模型使用的是单线程
- redis在4.0版本的时候就已经引入了多线程来做一些异步操作,此举主要针对那些非常耗时的命令,通过将这些命令的执行异步化,避免阻塞单线程的事件循环(增加了一些的非阻塞命令如 UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC)
- redis v6.0版本的时候引入了多线程IO,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程
多线程
redis6.0为何要引入虽然 Redis 在较新的版本中引入了多线程,不过是在部分命令上引入的,其中包括非阻塞的删除操作,在整体的架构设计上,主处理程序还是单线程模型的;由此看来,我们今天想要分析的两个问题可以简化成:
- 为什么 Redis 一开始选择单线程模型(单线程的好处)?
- 为什么 Redis 在 6.0 之后加入了多线程(在某些情况下,单线程出现了缺点,多线程可以解决)?为什么 Redis 服务增加了多个非阻塞的删除操作,例如:UNLINK、FLUSHALL ASYNC 和 FLUSHDB ASYNC?
redis的单线程模式
从redis的V4.0,redis的核心网络模型是单Reactor模型来处理网络请求,如下:
单Reactor模型所有事件的处理都在单个线程内完成。
- 使用单线程模型能够带来更好的可维护性,方便开发和调试
- 多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。单线程模式下,可以方便地进行调试和测试。
- 使用单线程模型也能并发的处理客户端的请求;
- FD是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态。使用 I/O 多路复用机制同时监听多个文件描述符的可读和可写状态。你可以理解为具有了多线程的特点。
- 一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会很快。也就是说在单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内存处理中得到忽略
- Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU;(决定性因素)
- 多线程能够充分利用CPU的资源,但是对于redis来说,由于是基于内存操作的,所以速度很快,能达到在一秒内处理10万个用户请求。如果一秒10万还不能满足需求,我们可以使用redis分片的技术来交给不同的redis服务器。这样做避免了在同一个redis服务中引入了大量的多线程操作。
- 而且redis大部分操作都是基于内存的,除非是进行AOF备份,否则基本上不会涉及到任何IO操作,数据的读写由于只发生在内存中,所以处理速度是非常快的;用多线程模型处理全部的外部请求可能不是一个好的方案。
上述三个原因中的最后一个是最终使用单线程模型的决定性因素,其他的两个原因都是使用单线程模型额外带来的好处,下面是详细说明
可维护性
- 多线程可以充分利用多核CPU,在高并发场景下,能够减少因I/O等待带来的CPU损耗,带来很好的性能表现。 * 不过多线程却是一把双刃剑,带来好处的同时,还会带来代码维护困难,线上问题难于定位和调试,死锁等问题。多线程模型中代码的执行过程不再是串行的,多个线程同时访问的共享变量如果处理不当也会带来诡异的问题。
可维护性对于一个项目来说非常重要,如果代码难以调试和测试,问题也经常难以复现,这对于任何一个项目来说都会严重地影响项目的可维护性。
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,代码的执行过程不再是串行的,多个线程同时访问的变量如果没有谨慎处理就会带来诡异的问题。
多线程模型会带来的潜在问题:竞争条件 (race condition) —— 如果计算机中的两个进程(线程同理)同时尝试修改一个共享内存的内容,在没有并发控制的情况下,最终的结果依赖于两个进程的执行顺序和时机,如果发生了并发访问冲突,最后的结果就会是不正确的。
- 引入了多线程,我们就必须要同时引入并发控制来保证在多个线程同时访问数据时程序行为的正确性,这就需要工程师额外维护并发控制的相关代码。
- 比如,我们会需要在可能被并发读写的变量上增加互斥锁,在访问这些变量或者内存之前也需要先对获取互斥锁,一旦忘记获取锁或者忘记释放锁就可能会导致各种诡异的问题,管理相关的并发控制机制也需要付出额外的研发成本和负担。
- 多线程虽然会帮助我们更充分地利用 CPU 资源,但是线程切换会带来额外的开销:
- 线程是由CPU调度的,CPU的一个核在一个时间片内只能同时执行一个线程
- 在CPU由线程A切换到线程B的过程中会发生一系列的操作,主要过程包括保存线程A的执行现场,然后载入线程B的执行现场,这个过程就是“线程上下文切换”。其中涉及线程相关指令的保存和恢复。
- 频繁的切换线程的上下文可能会导致性能急剧下降,这可能会导致我们不仅没有提升请求处理的平均速度,反而进行了负优化,所以这也是为什么 Redis 对于使用多线程技术非常谨慎。
在Linux系统中可以使用vmstat命令来查看上下文切换的次数,下面是vmstat查看上下文切换次数的示例:
性能瓶颈
多线程技术能够帮助我们充分利用CPU的计算资源来并发的执行不同的任务,但是CPU的资源往往不是redis服务器的性能瓶颈----哪怕我们在一个普通的linux服务器上启动redis服务,它也能在1s内处理1,000,000个用户请求
如果这种吞吐量不能满足我们的需求,更推荐的做法是使用分片的方式将不同的请求交给不同的 Redis 服务器来处理,而不是在同一个 Redis 服务中引入大量的多线程操作。
为什么说redis的性能瓶颈不在CPU?
- 首先,redis绝大部分操作都是基于内存的,而且是纯kv操作,所以命令执行速度非常快。我们可以大概理解成,redis中的数据存储在一张大hashmap中,hashmap的优势就是查找和写入的时间复杂度都是O(1)
- Redis内部采用这种结构存储数据,就奠定了Redis高性能的基础
- 根据Redis官网描述,在理想情况下Redis每秒可以提交一百万次请求,每次请求提交所需的时间在纳秒的时间量级。既然每次的Redis操作都这么快,单线程就可以完全搞定了,那还何必要用多线程呢!
简单总结一下:
- Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的;
AOF 是 Redis 的一种持久化机制,它会在每次收到来自客户端的写请求时,将其记录到日志中,每次 Redis 服务器启动时都会重放 AOF 日志构建原始的数据集,保证数据的持久性。
- 整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。
并发处理
redis的瓶颈并不在于CPU,而是内存和网络
- 内存瓶颈很好理解,redis作为缓存使用时很多场景需要缓存大量的数据,所以需要大量内存空间,这可以通过集群分片去解决,例如Redis自身的无中心集群分片方案以及Codis这种基于代理的集群分片方案。
- 对于网络瓶颈,redis在网络IO模型上才有用了多路复用技术,来减少网络瓶颈带来的影响
使用单线程模型也并不意味着程序不能并发的处理任务,Redis 虽然使用单线程模型处理用户的请求,但是它却使用 I/O 多路复用机制并发处理来自客户端的多个连接,同时等待多个连接发送的请求。
- socket:
- Socket可以理解成,在两个应用程序进行网络通信时,分别在两个应用程序中的通信端点。
- 通信时,一个应用程序将数据写入socket,然后通过网卡把数据发送到另外一个应用程序的socket中
- 我们平常所说的HTTP和TCP协议的远程通信,底层都是基于Socket实现的。5种网络IO模型也都要基于Socket实现网络通信。
- 阻塞与非阻塞
- 所谓阻塞,就是发出一个请求不能立即返回响应,要等所有的逻辑全部处理完成才能返回响应
- 非阻塞反之,发出一个请求立即返回应答,不用等处理完所有逻辑
- 内核空间和用户空间
- 在Linux中,应用程序稳定性远远比不上操作系统程序,为了保证操作系统的稳定性,Linux区分了内核空间和用户空间
- 可以这样理解,内核空间运行操作系统程序和驱动程序,用户空间运行应用程序
- Linux以这种方式隔离了操作系统程序和应用程序,避免了应用程序影响到操作系统自身的稳定性。
- 这也是Linux系统超时稳定的主要原因
- 所有的系统资源操作都在内核空间进行,比如读写磁盘文件、内存分配和回收、网络接口调用等
- 所以在一次网络IO读取过程中,数据并不是直接从网卡读取到用户空间中的应用程序缓冲区,而是先从网卡拷贝到内核空间缓冲区,然后再从内核拷贝到用户空间中的应用程序缓冲区
- 对于网络IO写入过程,过程则相反,先将数据从用户空间中的应用程序缓冲区拷贝到内核缓冲区,再从内核缓冲区把数据通过网卡发送出去。
- 多路复用I/O模型
- 多路复用I/O模型,建立在多路事件分离函数select、poll、epoll之上
- 以redis采用的epoll为例,在发起read请求前,先更新epoll的socket监控列表,然后等待epoll函数返回(此过程是阻塞的,所以说多路复用IO本质上也是阻塞IO模型)
- 当某个socket有数据返回1时,epoll函数返回。
- 此时用户线程才正式发起read请求,读取并处理数据
- 由于等待socket数据到达过程非常耗时,所以这种方式解决了阻塞IO模型一个socket连接就需要一个线程的问题,也不存在非阻塞IO模型忙轮询带来的CPU性能损耗的问题
使用IO多路复用技术能够极大地减少系统的开销,系统不再需要额外的创建和维护进程和线程来监听来自客户端的大量连接,减少了服务器的开发成本和维护成本
为什么要引入多线程
为什么要引入多线程(回答一)
随着硬件成本的降低,生产线上的物理机器配置越来越高了。而高配机器运行redis会有一些问题:
- 对Redis 单进程/单线程模型无法榨干机器硬件能力。对开发来说,榨干机器硬件能力可是很爽的事情。有人说可以在单机上起多个Redis进程,这可以部分解决问题,会带来运维上的开销。
- 某些操作严重耗时会拖累整个进程
- 长时间运行后导致内存碎片降低服务质量
- 由于单线程,数据备份与同步做的不够优雅
为什么要引入多线程(回答二)
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,redis的网络IO瓶颈已经越来越明显了:redis的网络IO读写占据了大部分的CPU时间(换句话说,读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间)
要提升redis的性能有两个方向:
- 优化网络IO模块
- 提高机器内存读写的速度
后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络IO的优化又可以分为两个方向
- 零拷贝技术和DPDK技术
- 利用多核优势
零拷贝技术有其局限性,无法完全适配redis这一复杂的网络IO模型。而DPDK技术通过旁路网卡IO绕过内核协议栈的方式又太过于复杂以及需要内核甚至硬件的支持,所以我们只能从后者下手啦
引入多线程的好处
- 可以充分利用服务器的CPU资源,目前单线程只能利用一个核
- 多线程任务可以分摊redis同步IO读写负荷
下面我们详细解释一下多路复用I/O模型。为了能更充分理解,我们先了解几个基本概念。
vmstat 1 表示每秒统计一次, 其中cs列就是指上下文切换的数目. 一般情况下, 空闲系统的上下文切换每秒在1500以下。
Redis 在最新的几个版本中加入了一些可以被其他线程异步处理的删除操作,也就是我们在上面提到的 UNLINK、FLUSHALL ASYNC 和 FLUSHDB ASYNC,我们为什么会需要这些删除操作,而它们为什么需要通过多线程的方式异步处理?
我们可以在redis宏使用DELY命令来删除key的对应的值,如果待删除的键值对占用了较小的内存空间,那么哪怕是同步的删除这写键值对也不会消耗太多的时间
但是对于 Redis 中的一些超大键值对,几十 MB 或者几百 MB 的数据并不能在几毫秒的时间内处理完,Redis 可能会需要在释放内存空间上消耗较多的时间,这些操作就会阻塞待处理的任务,影响 Redis 服务处理请求的可用性。
总结
- Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;
- 而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
然而释放内存空间的工作其实可以由后台线程异步进行处理,这也就是 UNLINK 命令的实现原理,它只会将键从元数据中删除,真正的删除操作会在后台异步执行。