土法炼钢兴趣小组的算法知识备份

【Redis / 缓存内核】事件循环与 I/O:ae · 客户端缓冲 · IO threads

文章导航

分类入口
databasestorage
标签入口
#redis#ae#event-loop#reactor#epoll#io-threads#networking#resp#single-threaded#redis-kernel

目录

「Redis 是单线程」在运维圈几乎成常识,却常在三个地方被误用:把 IO threads 当成「多线程 Redis」、把 后台子进程(RDB fork)算进请求路径、或忽略 beforeSleep 里 AOF fsync 与过期扫描对延迟的贡献。读 缓存服务器全景 后已知 Redis 在网络内存结构栈中的位置;本篇要钉住 一条 RESP 命令从 epoll 可读事件到 processCommand 再写回 socket 的源码路径。

本文是系列第 2 篇,回答:

  1. aeEventLoop 如何组织 文件事件时间事件serverCron)?
  2. 客户端 读缓冲 / 写缓冲networking.c 里如何与命令执行衔接?
  3. io-threads 配置到底改变了什么——哪些仍在主线程?

本文是「Redis / 缓存内核」系列第 2 篇(共 16 篇)。→ 系列目录

篇目 核心内容
第 1 篇 · 缓存服务器全景 四类 KV 生态位
第 2 篇 · 事件循环与 I/O ae、客户端缓冲、IO threads 边界
第 3 篇 · 对象模型与 SDS redisObject、encoding

版本锚定:Redis OSS 7.4 / 8.x;下文源码路径与函数名以 7.4.x tag 为准(如 7.4.1src/ae.csrc/networking.csrc/server.c)。

环境说明:本篇引用源码片段为仓库内核对过的摘录,非完整文件;不粘贴未在本机执行的 LATENCY DOCTOR 输出。


一、Reactor 谱系与 Redis 的 ae

1.1 模式来源

Reactor 模式(事件 demultiplex + 同步事件分发)在 Schmidt, Pattern-Oriented Software Architecture, Vol. 2 中形式化:单线程(或每线程一个 loop)在 select/poll/epoll 上阻塞,就绪 fd 回调 handler。Nginx、libevent、Node.js(libuv)与 Redis 均属这一族,差异在 多路复用 API 封装是否把业务突变放在 callback 里

Redis 自带 aesrc/ae.csrc/ae.h),注释写明源自 Jim Tcl 解释器事件循环的移植。编译期按性能顺序选择后端:ae_epoll.c / ae_kqueue.c / ae_evport.c / ae_select.c(见 ae.c 顶部 #include 链)。

1.2 与 libevent redis-lite 的分工

libevent redis-litebufferevent 在回调里直接 handle_cmd;生产 Redis 把 协议解析processCommand 突变 拆在 networking.c,并用 beforeSleep 批量处理 AOF、复制 ACK、客户端写队列——这是 toy 实现与生产路径的核心差距(对象层见第 3 篇)。


二、aeEventLoop:文件事件与时间事件

2.1 核心结构

ae.h 定义三类回调:

/* src/ae.h — 文件事件 mask(节选) */
#define AE_READABLE 1
#define AE_WRITABLE 2
#define AE_BARRIER 4   /* 与 WRITABLE 同用:同轮迭代先处理读再写,或反向 */

2.2 主循环 aeMain

/* src/ae.c — aeMain(节选) */
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

aeProcessEvents 在阻塞前调用 beforesleep(Redis 里绑定 beforeSleep),在 aeApiPoll 返回后调用 aftersleep,再依次触发就绪 fd 的 rfileProc / wfileProcAE_BARRIER 注释说明典型用途:先 fsync AOF,再向客户端写回复(与 beforeSleepflushAppendOnlyFile 顺序一致)。

2.3 时间事件:serverCron

initServer() 注册:

/* src/server.c — initServer(节选) */
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);

serverCron(同文件)按 server.hz 周期执行:更新 近似 LRU 时钟 server.lruclock、增量 rehash、过期采样触发、复制心跳、统计瞬时指标等。注意:cron 与命令执行同在主线程;长耗时 cron 任务会占用事件循环(具体任务见 serverCronrun_with_period 块)。

stateDiagram-v2
  direction LR
  [*] --> Poll: aeProcessEvents
  Poll --> BeforeSleep: aeApiPoll returned
  BeforeSleep --> Handlers: afterSleep + fd callbacks
  Handlers --> Poll: next iteration
  note right of BeforeSleep
    AOF flush
    IO thread fan-in
    activeExpireCycle fast
  end note

三、连接建立到命令执行的路径

3.1 监听与可读事件

新连接在 conn.c / networking.c 侧创建 client,为 socket 注册 readQueryFromClient 为可读 handler(经 aeCreateFileEvent)。默认路径下,可读事件 同步 调用 readQueryFromClient;开启 IO threads 读时,可读 handler 可能只把 client 挂入 clients_pending_readpostponeClientRead),真正读 socket 在 beforeSleephandleClientsWithPendingReadsUsingThreads 中完成。

3.2 读路径:readQueryFromClientprocessInputBuffer

readQueryFromClientnetworking.c)把字节追加到 c->querybuf(SDS),再调用 processInputBuffer

  1. 识别 RESP 内联或多 bulk(c->reqtype)。
  2. 解析完整命令到 c->argv / c->argc
  3. 调用 processCommandAndResetClientprocessCommand 执行命令突变。

关键分支(IO threads 读解析):

/* src/networking.c — processInputBuffer(节选) */
if (io_threads_op != IO_THREADS_OP_IDLE) {
    serverAssert(io_threads_op == IO_THREADS_OP_READ);
    c->flags |= CLIENT_PENDING_COMMAND;
    break;
}
/* 主线程路径 */
if (processCommandAndResetClient(c) == C_ERR)
    return C_ERR;

结论(A 级,源码直接支持):IO 线程可以 读 socket + 解析 RESP,但一旦遇到完整命令,只打 CLIENT_PENDING_COMMANDprocessCommand 仍在主线程(经 processPendingCommandAndInputBuffer)。

3.3 写路径:reply 与 clients_pending_write

命令处理通过 addReply* 把 RESP 回复追加到客户端输出结构;需要写 socket 时进入 clients_pending_writebeforeSleep 末尾调用 handleClientsWithPendingWritesUsingThreads:默认 IO threads 只并行 writeToClient;复制连接(CLIENT_TYPE_SLAVE)的写 强制留在主线程(注释:全局 replication buffer 线程安全)。

3.4 beforeSleep 在请求路径上的位置

beforeSleepserver.c)在 每轮 poll 之前 运行,与单次命令处理交织。与 I/O 相关的顺序(简化):

sequenceDiagram
  participant EL as aeEventLoop
  participant BS as beforeSleep
  participant Main as main thread
  participant IOT as IO threads

  EL->>BS: before poll
  BS->>IOT: handleClientsWithPendingReadsUsingThreads (optional)
  BS->>Main: processPendingCommand on fan-in
  BS->>BS: flushAppendOnlyFile / blocked clients
  BS->>IOT: handleClientsWithPendingWritesUsingThreads
  EL->>Main: readable/writable callbacks
  Main->>Main: readQueryFromClient / processCommand

因此 「单线程 Redis」 更精确的说法是:共享数据集上的命令执行与大部分 cron 逻辑在主线程;IO threads 与 BIO 线程(AOF fsync 等,bio.c)是辅助。RDB fork 发生在 BGSAVE 路径,不在每条 GET 的热路径上(第 9 篇)。


四、客户端缓冲与内存边界

每个 clientserver.h)至少包含:

字段 / 结构 作用
querybuf 累积 socket 读入的 RESP 字节;qb_pos 标记已消费偏移
reply / reply 链表 待发送的 RESP 回复;大回复可能引用 clientReplyBlock
flags CLIENT_PENDING_COMMANDCLIENT_PENDING_WRITECLIENT_BLOCKED

readQueryFromClient 会根据 client_max_querybuf_len 等配置拒绝过大输入;maxmemory 压力下还有 客户端连接级 evictionevictClients()beforeSleep 调用,networking.c / evict.c 协作)——这与第 8 篇 key 淘汰不同层。

Pipeline 行为:processInputBufferwhile 循环在 同一次可读事件 中连续解析多条命令,减少 poll 次数;这与 libevent redis-lite 提到的「批量回复」思想一致,但生产实现还要处理 阻塞命令CLIENT_BLOCKED 时停止消费 querybuf)、MULTI/EXECACL


五、IO threads:配置、fan-out/fan-in 与不变量

5.1 相关配置

配置 含义(官方文档 + 源码)
io-threads IO 线程总数;1 表示关闭(仅主线程)
io-threads-do-reads yes 是否让 IO 线程参与 读+解析;默认 often 仅写线程
io-threads-active 运行时是否已 startThreadedIO()INFO 可见)

initThreadedIOnetworking.c):io_threads_num == 1不创建 额外线程;否则 spawn IOThreadMain,主线程 id 为 0。

5.2 IOThreadMain 做什么

/* src/networking.c — IOThreadMain(节选) */
while ((ln = listNext(&li))) {
    client *c = listNodeValue(ln);
    if (io_threads_op == IO_THREADS_OP_WRITE) {
        writeToClient(c, 0);
    } else if (io_threads_op == IO_THREADS_OP_READ) {
        readQueryFromClient(c->conn);
    }
}

主线程通过 io_threads_opsetIOPendingCount 做 fan-out,busy-wait fan-in 直至 pending 归零(同文件注释:thread safety 靠主线程在 fan-in 前不碰队列)。

5.3 设计不变量(7.4/8.x)

  1. 命令突变processCommand 只在 io_threads_op == IO_THREADS_OP_IDLE 的主线程上下文执行(见 processInputBuffer 断言与 processPendingCommandAndInputBuffer)。
  2. 复制输出:replica 客户端写回放在 io_threads_list[0](主线程处理)。
  3. 动态启停:待写客户端过少时 stopThreadedIOIfNeeded 停线程,避免线程切换开销超过收益(阈值与 io_threads_num*2 相关)。

5.4 与 Memcached 多线程对比(预告第 14 篇)

Memcached worker 线程直接处理 get/set;Redis 刻意保持单线程突变 以回避细粒度锁与结构体并发安全。NSDI’13 的 Memcached 扩展讨论 多核线性扩展;Redis 7.x 选择 IO 并行 + 命令串行,是工程上不同的分叉点(争论见第 16 篇)。


六、开放问题:多线程 Redis 往哪走?

  1. IO 并行是否够? 当命令本身 CPU 重(大 key、复杂 SUNION、模块)或单实例 QPS 顶满 单核,IO threads 无法扩展 突变 吞吐——社区出现过多线程 fork 与 Redis 7+ 仍坚持主线程命令 的对比(选型见第 16 篇;Valkey 等 fork 的线程模型需单独读 release note,本篇不列未核对数字)。
  2. 读解析 offload 的代价io-threads-do-reads 增加 fan-out/fan-in 与 CLIENT_PENDING_COMMAND 状态机复杂度;短连接、小 payload 场景下是否默认开启,需结合 实际 RTT 与 pipeline 比例 评估(官方文档建议与 INFO 配合,无 universal 开关)。
  3. 与持久化 / 模块的交互beforeSleep 里 AOF、模块 GIL(moduleReleaseGIL / moduleAcquireGIL)与 IO threads 的时序约束,是 延迟尾刺 排查入口(第 15 篇 LATENCY DOCTOR)。

这些问题没有单一 peer-reviewed「最终答案」;可跟踪 Redis/Valkey release notes 与 OSDI/SOSP 系 multicore KV 论文(如早期 parallel KV 研究)作对照,但 生产结论必须以具体 tag 源码为准


七、常见误解

  1. 「开了 io-threads 就是多线程 Redis」。错。默认只并行 写 socket;命令仍在主线程。读线程还需 io-threads-do-reads yes,且解析后执行仍回主线程。
  2. 「单线程 = 只有一个 OS 线程」。错。除 IO threads 外,还有 BIO(AOF fsync)、jemalloc 后台线程(若启用)、模块自建线程等;它们 不直接并发修改 db dict。
  3. 「慢是因为 epoll 慢」。多数生产慢查询来自 命令复杂度、大 key、fork、AOF rewrite,而非 aeApiPoll 本身(第 15 篇分解)。
  4. 「可读 callback 里一定执行 processCommand」。IO 读路径下 callback 可能只 postpone;执行点在 beforeSleep fan-in 之后
  5. 「redis-lite 用了 libevent,所以 Redis 也依赖 libevent」。错。Redis 使用自研 ae,不依赖 libevent;redis-lite 是教学对照。

八、小结

三句话小结

  1. Redis 请求路径 = aeMain 驱动 epoll/kqueue + beforeSleep 批量 I/O 与 AOF + processInputBufferprocessCommand
  2. IO threads 仅 offload readQueryFromClient / writeToClientCLIENT_PENDING_COMMAND 保证突变回到主线程
  3. 「单线程 Redis」描述的是 数据面突变模型,不是 OS 线程数;理解 fan-out/fan-in 才能正确解读 INFO 里的 io_threads_active 与延迟 profile。

下一篇进入内存对象层:对象模型与 SDS——redisObject 的 type/encoding 如何承接本篇命令路径上的 lookupKey / setKey


参考资料

模式 / 论文

  1. Schmidt, Douglas C. et al., Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects, Wiley(Reactor 模式;B 级)。
  2. Nishtala, Rajesh et al., Scaling Memcache at Facebook, NSDI 2013(多线程纯缓存对照;B 级)。

源码(Redis OSS 7.4.x,A 级)

  1. src/ae.caeCreateEventLoopaeProcessEventsaeMain
  2. src/ae.h — 文件/时间事件 mask 与回调类型。
  3. src/server.cinitServerbeforeSleepafterSleepserverCronaeMain 入口。
  4. src/networking.creadQueryFromClientprocessInputBufferprocessCommandAndResetClientinitThreadedIOIOThreadMainhandleClientsWithPendingReadsUsingThreadshandleClientsWithPendingWritesUsingThreads
  5. src/server.hclient 结构、io_threads_numCLIENT_* 标志。

官方文档

  1. Redis Documentation, IO Threads / Latency(redis.io,7.4 / 8.x)。

站内

  1. 系列第 1 篇系列 index
  2. libevent redis-litelinux/redis-source(4.0 ae 情景)

返回 系列目录 | 上一篇:缓存服务器全景 | 下一篇:对象模型与 SDS

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .