第8章:Redis 网络通信模块源码分析
对齐原书第 8 章:Redis server/cli 网络链、ae/epoll、读写事件、threaded I/O、client 生命周期、缓冲、定时器、钩子与 RESP。
学习目标
- 能沿 Redis 6.0 源码解释 initServer、ae/epoll、acceptTcpHandler、createClient、readQueryFromClient、processInputBuffer、writeToClient 与 freeClient 调用链
- 能分析 client 的 query/reply buffers、pending lists、Redis 6.0 多线程网络 I/O ownership,以及 timer/beforeSleep/afterSleep 对事件循环的调度位置
- 能推导 redis-cli/hiredis 到 RESP inline/multibulk parser、请求应答、pipeline 与断开清理的完整数据流,并用 GDB/命令复现关键断点
机制总览
Redis 网络事件从 fd 到命令执行
- 1
接收事件
event loop 把可读 fd 分派给连接读处理器。
- 2
解析执行
输入缓冲允许半包与多命令,完整命令才进入执行。
- 3
发送回复
回复先进入输出缓冲,需要时注册可写事件并处理短写。
章级决策实验
Redis 网络事件从 fd 到命令执行
沿事件循环检查监听、读缓冲、命令解析、回复队列和可写事件。
选择推理阶段
当前阶段 · 接收事件
event loop 把可读 fd 分派给连接读处理器。
可核验证据
事件表、fd 状态与调用路径。
源码阅读必须把函数名放回事件循环与数据结构;否则看到的是局部实现,解释不了吞吐和延迟。
失效—证据矩阵
Redis 网络事件从 fd 到命令执行
接收事件
典型失效
只看 accept/read 函数,不确认事件注册和触发模式。
核验证据
事件表、fd 状态与调用路径。
解析执行
典型失效
把一次 read 当一条命令,忽略协议增量解析。
核验证据
query buffer 游标、RESP frame 与命令 trace。
发送回复
典型失效
大回复或慢客户端让缓冲无限增长。
核验证据
output buffer、client limit 与 writable 注册变化。
为什么源码分析要按事件链,而不是按文件顺序
从 networking.c 第一行读到最后一行会混在 client 创建、RESP parser、reply buffer、replication 和 threaded I/O 之间。更可靠的路径是跟随一次连接:初始化 listen fd → accept → create client → read bytes → parse argv → execute command → build/write reply → disconnect。
本章以原书对应的 Redis 6.0 源码为基线;后续 Redis 版本已经继续演化 connection abstraction 与 I/O threading,函数位置和 owner 边界必须按 tag 核对,不能把当前 unstable 分支反推到书中版本。
8.1 编译、启动与调试 Redis 6.0
这一节对应原书的 Redis 源码编译与启动和调试 Redis 环境:先固定版本与 artifact,再启动独立实例并设置可重复断点。
克隆官方仓库后 checkout 书中版本,清理缓存、构建带符号 binary,再用独立数据目录和非生产 port 启动。保留 exact commit、compiler flags 和 config,GDB breakpoint 才能与源码一一对应。
git clone https://github.com/redis/redis.git
cd redis
git checkout 6.0
make distclean
make CFLAGS='-O0 -g3 -fno-omit-frame-pointer'
mkdir -p /tmp/redis-cse
gdb --args src/redis-server --port 6380 --dir /tmp/redis-cse --save ''通信示例可用 redis-cli -p 6380 SET course cse,同时在 GDB 对 acceptTcpHandler、readQueryFromClient、processInputBuffer、writeToClient、freeClient 设断点。术语中 client 是 server-side connection/session state,不等同于外部 redis-cli process。
8.2 server 端监听 fd、accept 与 ae/epoll
server.c:initServer 创建 server.el = aeCreateEventLoop(...),listenToPort 建立 TCP listen fds,aeCreateTimeEvent(..., serverCron, ...) 安排时间事件,再把每个 listen fd 以 AE_READABLE 注册到 acceptTcpHandler。
Linux backend 在 ae_epoll.c 中由 aeApiCreate 建立 epoll fd,aeApiAddEvent 把 AE mask 映射为 EPOLLIN/EPOLLOUT 并调用 epoll_ctl,aeApiPoll/epoll_wait 产生 ready events,aeProcessEvents 调用 file handlers。
先预测:listen fd readable 后,真正创建 client 的函数位于 server.c 还是 networking.c;再沿图切换 startup/accept/read/write,核对每一步的文件和 state。
沿事件切换 Redis server 调用链
break initServer
break acceptTcpHandler
break acceptCommonHandler
break createClient
commands 4
silent
printf "create client conn=%p\n", conn
continue
end8.2.1 接受连接与 createClient
acceptTcpHandler 循环 accept TCP connection,交给 acceptCommonHandler 做 maxclients/protected-mode 等检查,再调用 createClient(conn)。createClient 将 connection 设 nonblocking/TCP_NODELAY/keepalive,注册 readQueryFromClient,初始化 client id、database、querybuf、argv/parser state、static reply buffer、reply list、flags、timestamps,并 link 到 global clients/index。
acceptTcpHandler
-> acceptCommonHandler
-> createClient(conn)
-> connNonBlock / connEnableTcpNoDelay / connKeepAlive
-> connSetReadHandler(conn, readQueryFromClient)
-> linkClient(c)监听 fd、accepted connection 和 client 分属 OS endpoint、connection abstraction 和 Redis protocol state 三层。调试时不要把 client id 当 fd;fd 可复用,client id/对象 lifetime 才能关联事件。
8.2.2 readQueryFromClient 到 command execution
readable 时 connection layer 调 readQueryFromClient(conn):取得 private client,扩展/读取 querybuf,更新 interaction/stat,然后直接 processInputBuffer(c),或在启用 threaded reads 时通过 postponeClientRead 放入 pending list。
processInputBuffer 根据 querybuf 首字节选择 PROTO_REQ_MULTIBULK 或 PROTO_REQ_INLINE;分别调用 processMultibulkBuffer / processInlineBuffer。完整 argv 形成后,processCommandAndResetClient 进入 command lookup/execution,再 reset argv/parser state,继续解析 pipeline 剩余 bytes。
readQueryFromClient
-> connRead -> c->querybuf
-> processInputBuffer
-> processInlineBuffer | processMultibulkBuffer
-> processCommandAndResetClient
-> processCommand -> c->cmd->procparser 不完整时保留 multibulklen/bulklen 与 qb_pos 等待下一次 read;pipeline 则在同一个 processInputBuffer loop 连续执行多条已完整命令。
8.2.3 可写事件、static buf 与 reply list
command 使用 addReply* 构造 RESP output。小回复优先进入 client 内嵌 static buf/bufpos;放不下或已有 list 时进入 reply linked list,并维护 reply_bytes/output-buffer limits。
clientInstallWriteHandler 先标记 CLIENT_PENDING_WRITE 并加入 server.clients_pending_write。beforeSleep 阶段的 handleClientsWithPendingWrites 尝试在回 event loop 前直接 flush,减少一次 writable event;若仍有剩余,才注册 sendReplyToClient。它调用 writeToClient 按 sentlen 推进 partial writes,完成后移除 write handler。
addReply* -> prepareClientToWrite -> clientInstallWriteHandler
-> clients_pending_write
-> handleClientsWithPendingWrites[UsingThreads]
-> writeToClient
-> remaining ? connSetWriteHandler(sendReplyToClient) : clear8.2.4 Redis 6.0 多线程网络 I/O
Redis 6.0 的 initThreadedIO 创建 I/O threads 与 per-thread lists,startThreadedIO/stopThreadedIO 控制运行。pending clients 按 batch 分配给 io_threads_list,workers 执行 socket read 或 write;主线程等待完成并继续 parser、command execution 与事件循环协调。
切换 pending read/parse/execute/write 阶段
break initThreadedIO
break handleClientsWithPendingReadsUsingThreads
break handleClientsWithPendingWritesUsingThreads
break IOThreadMain
thread apply all btCLIENT_PENDING_READ/CLIENT_PENDING_WRITE 防止同一 client 重复入队。分析 data race 要先确认当前 io_threads_op、client 所在 list 和主线程是否处在等待 barrier,而不是从“有多个线程”直接得出 keyspace 并行结论。
8.2.5 client 管理与断开流程
正常/异常断开最终进入 freeClient,高风险 callback 中可先 freeClientAsync 加入 async-free queue,由 freeClientsInAsyncFreeQueue 在安全阶段释放。unlinkClient 从 global clients/index 移除,清理 pending read/write list,注销 connection handlers并关闭 connection;随后释放 querybuf、argv、reply、pubsub/watch/transaction 等关联 state。
protocol/socket/output-limit error
-> freeClientAsync(c)
-> server.clients_to_close
-> freeClientsInAsyncFreeQueue
-> freeClient
-> unlinkClient
-> free query/reply/argv/session state8.2.6 收发缓冲区、限制与慢客户端
原书把 Redis 收发缓冲区、Redis 定时器和钩子函数连续讲解,因为 buffer drain 与周期/睡眠钩子的批处理顺序共同决定 event-loop latency。
querybuf 有协议最大/峰值管理,reply buffer 有 client type-specific hard/soft limits;超限可异步关闭。static buf 减少小回复 allocation,reply list 支持大/分段输出,sentlen 记录当前 block progress。
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60慢 client 不读取时,pending reply 增长;Redis 必须在 command-generated data 与 network drain 之间执行 backpressure/close policy。只观察 kernel Send-Q 不够,还要看 client reply_bytes、bufpos、flags 和 limit timestamp。
8.2.7 serverCron、time events 与 hooks
initServer 用 aeCreateTimeEvent 注册 serverCron。它周期更新 cached time、统计、clients/database housekeeping、persistence/replication 等;下一次 interval 可动态调整。它不是每连接 timeout wheel,而是 ae time-event 驱动的服务器周期任务。
aeSetBeforeSleepProc(server.el, beforeSleep) 与 aeSetAfterSleepProc(..., afterSleep) 安装钩子。beforeSleep 中会处理 pending writes/threaded I/O、cluster/module/blocked clients 等,再进入 poll;afterSleep 处理 poll 返回后的时钟/统计等。源码版本变化较快,分析 hook 应列出具体 tag 的调用清单。
break serverCron
break beforeSleep
break afterSleep
commands
silent
printf "clients=%lu pending_write=%lu\n", server.clients->len, server.clients_pending_write->len
continue
end8.3 redis-cli 端的网络通信模型
这一端到端路径同时覆盖 redis-cli 网络通信、Redis 通信协议、请求命令格式、应答命令格式、流水线和协议解析。
Redis 6.0 redis-cli.c 使用 hiredis redisContext 建立 TCP/Unix connection。交互/参数经过 cliSendCommand,使用 redisAppendCommandArgv 编码并追加 output;cliReadReply 调 redisGetReply 读取/解析 reply。pipeline 模式先 append 多条,再按顺序 get replies。
redis-cli main
-> cliConnect -> redisConnect / redisConnectUnix
-> cliSendCommand
-> redisAppendCommandArgv
-> cliReadReply -> redisGetReplyclient pipeline 提高 throughput 是减少 request/response 往返等待,不是让 server command response 乱序。connection 断开时,已写入但未确认的命令是否执行可能不确定,重试写操作需要业务 idempotency。
8.4 RESP 请求、应答、pipeline 与 inline command
RESP2 command 通常编码为 array of bulk strings。例如 SET key value:
*3\r\n
$3\r\nSET\r\n
$3\r\nkey\r\n
$5\r\nvalue\r\n回复前缀包括 simple string +、error -、integer :、bulk string $、array *;长度和 CRLF 定义 framing。inline command 是为简单 human input 保留的行协议,binary-safe client 应使用 multibulk RESP。
切换单命令、pipeline、partial 与 inline input
Redis 对协议数据的解析逻辑受 PROTO_MAX_QUERYBUF_LEN、bulk length、argc 等限制保护。fuzz 时覆盖 negative/overflow length、missing CRLF、huge argc、partial bulk 和 mixed pipeline。
本章回顾:一条 Redis 请求的源码路径
initServer创建 ae loop/listeners/time event/hooks,ae_epoll 把 AE masks 映射到 epoll。acceptTcpHandler → acceptCommonHandler → createClient建 client,并注册readQueryFromClient。- querybuf 经过 inline/multibulk incremental parser,完整 argv 进入 command execution。
addReply*只构建 reply buffer;pending-write batch、threaded I/O 或 writable handler 最终 flush。- Redis 6.0 I/O threads 主要并行 socket reads/writes,command/keyspace execution owner 仍要按主线程边界理解。
- freeClientAsync/freeClient/unlinkClient、serverCron 和 sleep hooks 共同闭合 lifetime/scheduling。
练习
问题 1:执行 SET 后在 addReplyStatus 看到 +OK,但客户端迟迟没收到。下一步应沿哪些 state/function 排查?
问题 2:为什么看到 IOThreadMain 正在 read/write 不能证明 Redis 6.0 多线程并行执行 SET/GET?怎样用断点证明 owner?
问题 3:一条 RESP bulk request 被两次 read 拆开,后面又紧跟两条 pipeline commands。parser 如何保证不丢不重?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Redis network event chain
- Redis debug baseline
- ae event loop
- Redis client object
- querybuf parser state
- Redis reply buffer chain
- Redis threaded I/O
- asynchronous client free
- client output buffer limit
- event-loop sleep hooks
- redisContext
- Redis pipelining