第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. 1

    接收事件

    event loop 把可读 fd 分派给连接读处理器。

  2. 2

    解析执行

    输入缓冲允许半包与多命令,完整命令才进入执行。

  3. 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 对 acceptTcpHandlerreadQueryFromClientprocessInputBufferwriteToClientfreeClient 设断点。术语中 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_ctlaeApiPoll/epoll_wait 产生 ready events,aeProcessEvents 调用 file handlers。

先预测:listen fd readable 后,真正创建 client 的函数位于 server.c 还是 networking.c;再沿图切换 startup/accept/read/write,核对每一步的文件和 state。

分步1 / 3

沿事件切换 Redis server 调用链

break initServer
break acceptTcpHandler
break acceptCommonHandler
break createClient
commands 4
  silent
  printf "create client conn=%p\n", conn
  continue
end

8.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_MULTIBULKPROTO_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->proc

parser 不完整时保留 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) : clear

8.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 与事件循环协调。

分步1 / 3

切换 pending read/parse/execute/write 阶段

break initThreadedIO
break handleClientsWithPendingReadsUsingThreads
break handleClientsWithPendingWritesUsingThreads
break IOThreadMain
thread apply all bt

CLIENT_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 state

8.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

initServeraeCreateTimeEvent 注册 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
end

8.3 redis-cli 端的网络通信模型

这一端到端路径同时覆盖 redis-cli 网络通信Redis 通信协议请求命令格式应答命令格式流水线协议解析

Redis 6.0 redis-cli.c 使用 hiredis redisContext 建立 TCP/Unix connection。交互/参数经过 cliSendCommand,使用 redisAppendCommandArgv 编码并追加 output;cliReadReplyredisGetReply 读取/解析 reply。pipeline 模式先 append 多条,再按顺序 get replies。

redis-cli main
  -> cliConnect -> redisConnect / redisConnectUnix
  -> cliSendCommand
    -> redisAppendCommandArgv
    -> cliReadReply -> redisGetReply

client 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。

分步1 / 3

切换单命令、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 请求的源码路径

  1. initServer 创建 ae loop/listeners/time event/hooks,ae_epoll 把 AE masks 映射到 epoll。
  2. acceptTcpHandler → acceptCommonHandler → createClient 建 client,并注册 readQueryFromClient
  3. querybuf 经过 inline/multibulk incremental parser,完整 argv 进入 command execution。
  4. addReply* 只构建 reply buffer;pending-write batch、threaded I/O 或 writable handler 最终 flush。
  5. Redis 6.0 I/O threads 主要并行 socket reads/writes,command/keyspace execution owner 仍要按主线程边界理解。
  6. 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

讨论

评论区加载中…