Redis 源码剖析与实战
蒋德钧
中科院计算所副研究员
17747 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 47 讲
Redis 源码剖析与实战
15
15
1.0x
00:00/00:00
登录|注册

13 | Redis 6.0多IO线程的效率提高了吗?

你好,我是蒋德钧。
通过上节课的学习,我们知道 Redis server 启动后的进程会以单线程的方式,执行客户端请求解析和处理工作。但是,Redis server 也会通过 bioInit 函数启动三个后台线程,来处理后台任务。也就是说,Redis 不再让主线程执行一些耗时操作,比如同步写、删除等,而是交给后台线程异步完成,从而避免了对主线程的阻塞。
实际上,在 2020 年 5 月推出的 Redis 6.0 版本中,Redis 在执行模型中还进一步使用了多线程来处理 IO 任务,这样设计的目的,就是为了充分利用当前服务器的多核特性,使用多核运行多线程,让多线程帮助加速数据读取、命令解析以及数据写回的速度,提升 Redis 整体性能。
那么,这些多线程具体是在什么时候启动,又是通过什么方式来处理 IO 请求的呢?
今天这节课,我就来给你介绍下 Redis 6.0 实现的多 IO 线程机制。通过这部分内容的学习,你可以充分了解到 Redis 6.0 是如何通过多线程来提升 IO 请求处理效率的。这样你也就可以结合实际业务来评估,自己是否需要使用 Redis 6.0 了。
好,接下来,我们先来看下多 IO 线程的初始化。注意,因为我们之前课程中阅读的是 Redis 5.0.8 版本的代码,所以在开始学习今天的课程之前,你还需要下载Redis 6.0.15的源码,以便能查看到和多 IO 线程机制相关的代码。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Redis 6.0版本引入了多IO线程机制,旨在充分利用多核特性,提升Redis整体性能。文章详细介绍了多IO线程的初始化过程,包括设置IO线程的激活标志和对IO线程数量的判断,以及对相关数组的初始化和线程的创建。此外,文章还解释了IO线程的运行函数IOThreadMain的执行逻辑,包括从客户端列表中获取客户端并根据操作类型进行读写操作。另外,文章还介绍了Redis如何推迟客户端的读写操作,并将待处理的客户端添加到相应的列表中。通过本文,读者可以了解到Redis 6.0如何通过多线程来提升IO请求处理效率,以及IO线程的具体工作流程。文章还详细介绍了如何把待读客户端和待写客户端分配给IO线程执行的过程,包括判断IO线程激活状态、分配客户端给各个IO线程、主IO线程处理待读客户端、以及避免采用多线程的条件判断。这些内容为读者提供了全面了解Redis多IO线程机制的重要信息。文章还介绍了Redis如何推迟客户端读写操作的具体实现过程,包括判断推迟条件和相应的处理函数。同时,还提到了如何将推迟执行的客户端分配给多IO线程进行处理。这些内容为读者提供了深入了解Redis多IO线程机制的重要信息。Redis多IO线程机制使用startThreadedIO函数和stopThreadedIO函数,来设置IO线程激活标识io_threads_active为1和为0。此处,这两个函数还会对线程互斥锁数组进行解锁和加锁操作。这些操作的目的是确保在启动和停止IO线程时,对线程的激活状态进行同步控制,以保证线程操作的正确性。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Redis 源码剖析与实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(15)

  • 最新
  • 精选
  • Kaito
    1、Redis 6.0 之前,处理客户端请求是单线程,这种模型的缺点是,只能用到「单核」CPU。如果并发量很高,那么在读写客户端数据时,容易引发性能瓶颈,所以 Redis 6.0 引入了多 IO 线程解决这个问题 2、配置文件开启 io-threads N 后,Redis Server 启动时,会启动 N - 1 个 IO 线程(主线程也算一个 IO 线程),这些 IO 线程执行的逻辑是 networking.c 的 IOThreadMain 函数。但默认只开启多线程「写」client socket,如果要开启多线程「读」,还需配置 io-threads-do-reads = yes 3、Redis 在读取客户端请求时,判断如果开启了 IO 多线程,则把这个 client 放到 clients_pending_read 链表中(postponeClientRead 函数),之后主线程在处理每次事件循环之前,把链表数据轮询放到 IO 线程的链表(io_threads_list)中 4、同样地,在写回响应时,是把 client 放到 clients_pending_write 中(prepareClientToWrite 函数),执行事件循环之前把数据轮询放到 IO 线程的链表(io_threads_list)中 5、主线程把 client 分发到 IO 线程时,自己也会读写客户端 socket(主线程也要分担一部分读写操作),之后「等待」所有 IO 线程完成读写,再由主线程「串行」执行后续逻辑 6、每个 IO 线程,不停地从 io_threads_list 链表中取出 client,并根据指定类型读、写 client socket 7、IO 线程在处理读、写 client 时有些许差异,如果 write_client_pedding < io_threads * 2,则直接由「主线程」负责写,不再交给 IO 线程处理,从而节省 CPU 消耗 8、Redis 官方建议,服务器最少 4 核 CPU 才建议开启 IO 多线程,4 核 CPU 建议开 2-3 个 IO 线程,8 核 CPU 开 6 个 IO 线程,超过 8 个线程性能提升不大 9、Redis 官方表示,开启多 IO 线程后,性能可提升 1 倍。当然,如果 Redis 性能足够用,没必要开 IO 线程 课后题:为什么 startThreadedIO / stopThreadedIO 要执行加解锁? 既然涉及到加锁操作,必然是为了「互斥」从而控制某些逻辑。可以在代码中检索这个锁变量,看存在哪些逻辑对 io_threads_mutex 操作了加解锁。 跟踪代码可以看到,在 networking.c 的 IOThreadMain 函数,也对这个变量进行了加解锁操作,那就说明 startThreadedIO / stopThreadedIO 函数,可以控制 IOThreadMain 里逻辑的执行,IOThreadMain 代码如下。 void *IOThreadMain(void *myid) { ... while(1) { ... /* Give the main thread a chance to stop this thread. */ if (io_threads_pending[id] == 0) { pthread_mutex_lock(&io_threads_mutex[id]); pthread_mutex_unlock(&io_threads_mutex[id]); continue; } // 读写 client socket // ... } 这个函数正是 IO 多线程的主逻辑。 从注释可以看到,这是为了给主线程停止 IO 线程的的机会。也就是说,这里的目的是为了让主线程可以控制 IO 线程的开启 / 暂停。 因为每次 IO 线程在执行时必须先拿到锁,才能执行后面的逻辑,如果主线程执行了 stopThreadedIO,就会先拿到锁,那么 IOThreadMain 函数在执行时就会因为拿不到锁阻塞「等待」,这就达到了 stop IO 线程的目的。 同样地,调用 startThreadedIO 函数后,会释放锁,IO 线程就可以拿到锁,继续「恢复」执行。
    2021-08-24
    32
  • 曾轼麟
    一样首先回答老师的问题,你知道为什么这两个函数要执行解锁和加锁操作么? 答案:是为了方便主线程动态,灵活调整IO线程而设计的,当clients数量较少的时候可以方便直接停止IO线程。停止IO线程的阈值是,当等待写的client客户端数量小于IO线程数量的两倍,就会停止IO线程避免多线程带来不必要的开销 回归代码: 1、stopThreadedIO,startThreadedIO 和 stopThreadedIOIfNeeded这三个函数中有体现,其中在stopThreadedIOIfNeeded中会判断当前待写出客户端数量是否大于2倍IO线程数量,如果不是则会调用stopThreadedIO函数通过io_threads_mutex的方式停止所有IO线程(主线程除外,因为index是从1开始的)并且将io_threads_active设置为0,并且后续调用stopThreadedIOIfNeeded函数会返回0,在handleClientsWithPendingWritesUsingThreads函数中会直接调用handleClientsWithPendingWrites来使用单线程进行写出。 流程如下: 第一次:handleClientsWithPendingWritesUsingThreads -> stopThreadedIOIfNeeded -> stopThreadedIO -> 设置io_threads_active为0并lock住IO线程 第二次: handleClientsWithPendingWritesUsingThreads -> stopThreadedIOIfNeeded -> 直接返回1 -> handleClientsWithPendingWrites进行单线程处理 2、当待写出client的数量上来的时候,stopThreadedIOIfNeeded函数中判断,待写出client数量大于2倍IO线程数量,返回0,然后调用startThreadedIO激活IO线程 流程如下: handleClientsWithPendingWritesUsingThreads -> stopThreadedIOIfNeeded(发现不满足需要IO线程,返回0) -> startThreadedIO(激活IO线程) -> 设置io_threads_active为1 此外注意:IO线程一定是处理完了所有client之后,才会倍lock,在IOThreadMain有一个条件 if (getIOPendingCount(id) == 0) 总结: 本篇文章,老师带我们了解了IO线程的设计原理和多IO给Redis带了了性能上的提升,从代码中可以看出,IO线程的数量并不是随心所欲的设置的,应当结合Redis client的数量而定的,并且上限是128,此外IO线程,是和主线程共同协调运行的,最典型的就是主线程通过控制io_threads_op来协调IO线程是同步读取还是写入 建议: IO线程这块其实还涉及一个比较大的内容,就是RESP的协议编解码,IO线程虽然不涉及命令执行,但是会协助主线程进行协议编解码,而RESP协议的设计很巧妙,对粘包拆包等处理也是其一大亮点
    2021-08-31
    1
    6
  • 里咯破
    redis6刚出时用redis-benchmark 测试过,的确会有提升,get能有将近一倍,set根据数据量不同有20%~40%的提升.但是只是单机测试,没有考虑网络环境.
    2021-09-09
    1
  • 土豆种南城
    回答课后题:为什么 startThreadedIO / stopThreadedIO 要执行加解锁? 几个评论都提到这部分代码: /* Give the main thread a chance to stop this thread. */ if (io_threads_pending[id] == 0) { pthread_mutex_lock(&io_threads_mutex[id]); pthread_mutex_unlock(&io_threads_mutex[id]); continue; } 我觉得光看这里还不够,还应该结合这部分代码的前面一段来看: /* Wait for start */ for (int j = 0; j < 1000000; j++) { if (getIOPendingCount(id) != 0) break; } /* Give the main thread a chance to stop this thread. */ if (io_threads_pending[id] == 0) { pthread_mutex_lock(&io_threads_mutex[id]); pthread_mutex_unlock(&io_threads_mutex[id]); continue; } 总体逻辑是这样的,io子线程启动后直接一入一段“狂热”时间,子线程会积极响应主线程设置的任务。但是如果一段时间(一百万次循环)之后任务数量还是0会发生两种情况: 1. 主线程没打开多线程模式(没有调用过startThreadedIO),这情况可能是主线程本身就没收到任何请求,也可能是主线程觉得不需要子线程来处理(stopThreadedIOIfNeeded) 2. 主线程打开了多线程模式,但是还没来得及调用setIOPendingCount设置任务 1情况下pthread_mutex_lock会阻塞子线程,相当于子线程进入沉睡状态了 2情况下不会阻塞子线程,子线程进入下次循环,依然处于“狂热”状态,只要主线程调用setIOPendingCount就可以立即工作 综上,startThreadedIO的解锁操作相当于是“唤醒”了子线程在“狂热”状态未满足下进入的沉睡。stopThreadedIO能让子线程在一个阶段的“狂热”结束后进入沉睡
    2021-09-05
    1
  • 可怜大灰狼
    networking.c中IOThreadMain方法有如下一小段代码: /* Give the main thread a chance to stop this thread. */ if (getIOPendingCount(id) == 0) { pthread_mutex_lock(&io_threads_mutex[id]); pthread_mutex_unlock(&io_threads_mutex[id]); continue; } 就像代码里说的,给主线程暂停子线程的机会。 如果主线程没有在startThreadedIO做unlock和在stopThreadedIO做lock,主线程也无法暂停和开始子线程,进而会导致cpu资源浪费。
    2021-08-24
    1
  • 孤独患者
    假设按顺序先后收到a、b、c三个命令,分别被线程1、2、3成功解析,redis是怎么保证主线程执行命令也是按a、b、c这个顺序的呢?
    2024-01-11归属地:上海
  • 孤独患者
    多线程的话,能保证先到的命令先执行吗?虽然说执行命令还是在一个线程顺序进行,但是命令解析是在不同的线程,有没有可能后收到的命令,被先执行了?
    2024-01-11归属地:上海
  • Hubery
    老师,最近遇到个问题。压测的时候,本地机子是16核的,然后redis只会把一个核打满,其他核都是空闲的,花费的时间主要是在软中断上面。不知道啥原因
    2023-09-26归属地:广东
  • kobe
    你好,关于handleClientsWithPendingReadsUsingThreads和handleClientsWithPendingWritesUsingThreads两个方法的第四步,为什么前者是直接由主线程处理new buffers,包括解析和执行命令,而后者是注册个新的可写事件,交由事件驱动框架去处理?
    2023-08-31归属地:浙江
  • 水滴s
    判断所有多线程是否处理完读,这里不会造成CPU忙等待吗,为啥不使用锁条件变量实现呢? while(1) { unsigned long pending = 0; for (int j = 1; j < server.io_threads_num; j++) pending += io_threads_pending[j]; if (pending == 0) break; }
    2022-06-25
    1
收起评论
显示
设置
留言
15
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部