Redis 网络模型与高 I/O 性能:从单线程事件循环到多线程 I/O

Redis 高性能网络模型深度解析:ae.c 事件循环源码、epoll/kqueue/select 多路复用对比、RESP2/RESP3 协议、Pipeline 批量优化、TCP backlog 调优、Redis 6.0+ IO 多线程、TLS 加密开销与网络基准测试

Redis 之所以能在单机缓存领域长期保持 10 万 QPS 以上的吞吐量,与其精心设计的网络模型密不可分。从经典的单线程事件循环到 Redis 6.0 引入的多线程 I/O,从 RESP 明文协议到 TLS 加密传输,每一步架构选择都蕴含着深刻的工程权衡。本文深入 Redis 底层源码与操作系统机制,带你理解 C10K(单机万级连接)甚至 C100K 场景下 Redis 如何做到高并发低延迟,以及在生产环境中如何调优网络参数、规避性能陷阱。


一、单线程事件循环:ae.c 的实现细节

Redis 的主线程处理所有客户端连接的读写请求,但"单线程"绝不等于"串行阻塞"。Redis 通过事件驱动(Event-Driven)模型,将 I/O 操作委托给操作系统内核的多路复用机制,主线程仅在事件就绪时执行用户空间的命令处理。

1.1 核心架构:一个线程 + 多路复用

+----------------------------------------------------+
|                   Redis 服务器主线程                 |
|                                                    |
|  +-------------+    +-------------+    +-------+  |
|  | 文件事件    | -> | 命令处理器  | -> | 回复   |  |
|  | (ae.c)      |    | (processFileEvents)        |  |
|  +-------------+    +-------------+    +-------+  |
|         ^                                  |       |
|         +------ epoll_wait / kqueue ------+       |
+----------------------------------------------------+
                        |
                        v
+----------------------------------------------------+
|           OS 内核:epoll / kqueue / devpoll          |
|      (监控所有 socket,告知哪些连接有数据可读)         |
+----------------------------------------------------+

这一系列机制的核心实现位于 src/ae.c(Antirez Event)模块。Redis 的事件驱动系统由三个核心组件构成:

  • aeEventLoop:主事件循环结构体,持有 apidata(多路复用后端私有数据)、文件事件数组、时间事件链表等
  • aeFileEvent:文件事件,表示一个套接字的可读/可写状态
  • aeTimeEvent:时间事件,用于 serverCron 等后台周期性任务

1.2 aeEventLoop 结构体

// ae.h 中的核心结构

typedef struct aeEventLoop {
    int maxfd;           // 当前注册的最大文件描述符
    int setsize;         // fds 数组大小(= maxclients + 32)
    long long timeEventNextId;
    time_t lastTime;     // 上次执行时间事件的时间
    aeFileEvent *events; // 文件事件数组(索引即 fd)
    aeFiredEvent *fired; // 就绪事件数组
    aeTimeEvent *timeEventHead; // 时间事件链表
    int stop;
    void *apidata;       // epoll 或 kqueue 的私有数据
    aeBeforeSleepProc *beforesleep;
    aeBeforeSleepProc *aftersleep;
} aeEventLoop;

Redis 将文件事件注册到操作系统内核的多路复用接口,然后进入主循环不断调用 aeProcessEvents

1.3 事件循环主流程

// ae.c - aeProcessEvents 简化逻辑

int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    int numevents;
    if (eventLoop->maxfd != -1) {
        // 计算最近的时间事件,作为阻塞等待的超时时间
        tvp = aeSearchNearestTimer(eventLoop);
    }
    // 1. 调用 epoll_wait / kqueue 等待就绪事件
    numevents = aeApiPoll(eventLoop, tvp);

    // 2. 处理就绪的文件事件
    for (j = 0; j < numevents; j++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
        int mask = eventLoop->fired[j].mask;
        int fd = eventLoop->fired[j].fd;
        int rfilemask = 0, wfilemask = 0;

        if (fe->mask & mask & AE_READABLE) {
            rfilemask |= AE_READABLE;
            fe->rfileProc(eventLoop, fd, fe->clientData, mask);
            processed++;
        }
        if (fe->mask & mask & AE_WRITABLE) {
            wfilemask |= AE_WRITABLE;
            if (!rfilemask || fe->wfileProc != fe->rfileProc) {
                fe->wfileProc(eventLoop, fd, fe->clientData, mask);
                processed++;
            }
        }
    }
    // 3. 处理时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed;
}

关键点:

  • 无余力等待aeApiPoll 的阻塞时间不会超过最近一个定时任务的触发时间,因此 Redis 不会像传统阻塞服务器那样"空等"。
  • 事件分离:每个 fd 注册独立的读处理函数(rfileProc)和写处理函数(wfileProc),回调处理实现解耦。
  • 单线程顺序执行epoll_wait 返回后,就绪事件在主线程中逐个串行处理。没有任何并发竞态,不需要锁,这是 Redis 高性能的核心设计哲学。

1.4 回调函数的注册

当新客户端连接时,Redis 在 acceptTcpHandler 中为新 fd 注册读事件:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    // accept 新连接
    cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
    if (cfd == ANET_ERR) return;
    // 创建客户端结构体并注册读事件
    acceptCommonHandler(connCreateAcceptedSocket(cfd), 0, cip);
}

// 后续在 networking.c 中注册到 event loop
void createClient(connection *conn) {
    client *c = zmalloc(sizeof(client));
    ...
    if (connSetReadHandler(conn, readQueryFromClient) == C_ERR) {
        freeClient(c);
        return;
    }
}

当客户端有数据到达时,内核通过 epoll_wait 通知 Redis,主线程调用 readQueryFromClient 读取请求、解析 RESP 协议、执行命令、将回复写入客户端输出缓冲区。如果输出缓冲区有数据等待发送,Redis 还会注册写事件,让 epoll_wait 在 socket 变得可写时触发 writeToClient 将数据发回。


二、I/O 多路复用:epoll vs kqueue vs select

单线程是否能支撑上万并发,完全取决于底层多路复用机制的效率。Redis 在编译期自动探测操作系统支持的多路复用接口,按优先级依次选择 epoll(Linux)、kqueue(BSD/macOS)、evport(Solaris)、select(兜底)。

2.1 四种机制的深度对比

特性selectpollepoll(Linux)kqueue(BSD/macOS)
时间复杂度O(N),每次遍历所有 fdO(N),遍历整个数组O(1),事件数量与 fd 总数无关O(1),仅返回就绪事件
fd 上限默认 1024(可修改 FD_SETSIZE 重编译)无硬编码上限(受内存限制)无上限(/proc/sys/fs/file-max)无上限
触发模式水平触发水平触发支持 ET(边缘触发)和 LT(水平触发)支持 EV_CLEAR(类似 ET)
内存拷贝每次调用拷贝 fd_set 到内核每次调用拷贝整个数组通过 epoll_ctl 注册后,epoll_wait 不再拷贝通过 kevent 注册后,仅返回就绪事件
数据结构设计位图(bitmap),fd 为索引数组,逐个遍历红黑树维护全部 fd,就绪列表维护就绪 fdkqueue 内核队列
Redis 使用场景不用于生产不用于生产Linux 生产环境macOS 开发、FreeBSD 生产

2.2 select 的性能瓶颈

// select 的核心问题:每次调用都要传递整个 fd_set
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
// ... 将所有需要监控的 fd 加入 readfds
select(maxfd + 1, &readfds, NULL, NULL, &timeout);
// 返回后需要遍历所有 fd,检查 FD_ISSET(fd, &readfds)

select 的两个致命缺陷:

  1. fd_set 大小固定为 1024,对于 C10K 场景根本不够。虽然可以修改 FD_SETSIZE 重编译内核和 glibc,但线性扫描的开销会急剧恶化。
  2. 每次调用需要重新传递 fd_set 到内核,内核返回后再全部拷贝回用户空间。假设监控 10000 个 fd,每次都要发生 10000/8 = 1250 字节的内存拷贝。

在 Redis 中,select 仅在极其老旧的系统上作为兜底方案,生产环境不应使用。

2.3 epoll 的设计优势

// epoll 的三步操作模式
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // ET 模式
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);  // 只注册一次

// 主循环中反复调用
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
for (int i = 0; i < nfds; i++) {
    handle_event(events[i].data.fd, events[i].events);
}

epoll 的关键改进:

  • 红黑树管理 fd:所有被监控的 fd 存储在内核红黑树中,epoll_ctl 增删改的复杂度为 O(log N)。
  • 就绪链表:当网络事件发生时,内核回调 ep_poll_callback 将 fd 加入就绪链表。epoll_wait 只需将链表中的事件拷贝到用户空间,复杂度 O(活跃事件数),与总 fd 数无关。
  • ET(边缘触发)vs LT(水平触发)
    • LT 模式下,只要 fd 可读,epoll_wait 每次返回都会报告该 fd。
    • ET 模式下,仅在 fd 状态从不可读变为可读时通知一次,要求用户空间必须一次性读尽数据。Redis 使用 LT 模式,因为处理单个客户端的读请求后可能还有其他逻辑需要处理,不需要在每个 fd 上死循环读取直到 EAGAIN。

2.4 kqueue:BSD 系的优等生

// kqueue 的 API 更为统一,将文件事件、信号、进程事件统一到一个队列
int kq = kqueue();
struct kevent changes[2], events[10];

// 注册事件
EV_SET(&changes[0], fd1, EVFILT_READ, EV_ADD, 0, 0, NULL);
EV_SET(&changes[1], fd2, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
kevent(kq, changes, 2, NULL, 0, NULL);

// 等待事件
int nev = kevent(kq, NULL, 0, events, 10, &timeout);

kqueue 的设计更加简洁优雅,且与定时器(EVFILT_TIMER)天然集成。macOS 上开发 Redis 时,ae_kqueue.c 会被自动编译。不过 macOS 不适合作为 Redis 生产服务器,性能测试环境感受即可。

2.5 查看你的 Redis 使用哪种多路复用

# 查看编译配置
redis-cli INFO server | grep multiplexing_api
# 输出示例:multiplexing_api:epoll

# 确认系统支持
$ cat /proc/sys/fs/file-max
9223372036854775807

# 调整单进程 fd 上限(Redis 并发连接数受限于此)
ulimit -n 65535

Redis 的 maxclients 默认 10000,但实际能支持的连接数还受限于操作系统的文件描述符上限。建议:

# /etc/security/limits.conf
redis soft nofile 65535
redis hard nofile 65535

# /etc/sysctl.conf
fs.file-max = 100000
net.core.somaxconn = 65535

三、RESP 协议:RESP2 与 RESP3

响应式协议(REdis Serialization Protocol)是 Redis 客户端与服务端之间的通信语言。RESP2 自 Redis 1.2 沿用至今,RESP3 在 Redis 6.0 引入,新增丰富的数据类型。

3.1 RESP2 协议格式

RESP2 是一个二进制安全的文本协议,基于 TCP 流传输,使用 \r\n(CRLF)作为分隔符。

类型前缀含义示例
+简单字符串(Simple String)+OK\r\n
-错误(Error)-ERR unknown command\r\n
:整数(Integer):1000\r\n
$批量字符串(Bulk String),二进制安全$6\r\nfoobar\r\n
*数组(Array),批量字符串的集合*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n

3.2 Wire-Level 示例:一次 GET 命令

# 客户端发送的原始字节(十六进制表示)
# *2\r\n$3\r\nGET\r\n$4\r\nname\r\n

2a 32 0d 0a        # *2\r\n    -> 2 个元素的数组
24 33 0d 0a        # $3\r\n    -> 第一个元素:长度为 3 的批量字符串
47 45 54 0d 0a     # GET\r\n   -> 字符串内容
24 34 0d 0a        # $4\r\n    -> 第二个元素:长度为 4 的批量字符串
6e 61 6d 65 0d 0a  # name\r\n  -> 字符串内容

服务端回复:

$5\r\nAlice\r\n

24 35 0d 0a        # $5\r\n    -> 长度为 5 的批量字符串
41 6c 69 63 65 0d 0a  # Alice\r\n

回复空值:

$-1\r\n            # nil / null(空键时返回)

3.3 RESP3 新增类型

RESP3(Redis 6.0+)解决了 RESP2 中"一切皆为字符串"的语义模糊问题,引入强类型系统:

类型前缀类型说明
_Null空值,取代 RESP2 的 $-1
#Boolean#t\r\n = true, #f\r\n = false
,Double浮点数 ,3.14\r\n
(Big Number大整数
!Bulk Error带长度的错误信息
=Verbatim String逐字字符串(带格式标注)
%Map%{count}\r\n + key-value 对
~Set~{count}\r\n + 元素数组(无序)
|Attribute元数据属性
>Push服务端主动推送(用于 Client Side Caching)

3.4 协议切换:HELLO

# 客户端请求切换到 RESP3
redis-cli HELLO 3
# 返回 Map 格式的服务端信息:
# 1# "server" => "redis"
# 2# "version" => "7.2.4"
# 3# "proto" => (integer) 3

# 回退 RESP2
redis-cli HELLO 2

RESP3 的价值在于语义精确:RESP2 中布尔值、数字、null 都是字符串表示,客户端需要猜测实际类型。RESP3 为 RedisJSON 等模块提供了原生的 Map/Set 支持,也为 客户端缓存 提供了 > Push 类型实现主动失效通知。

3.5 协议对性能的影响

RESP 是文本协议而非二进制协议,解析器需要逐字节扫描 \r\n。但 RESP 的设计足够紧凑,且解析不涉及复杂语法分析(没有巴科斯范式文法)。在 10 万 QPS 级别,协议解析的 CPU 开销只占命令处理总时间的 5-10%。真正的时间大头在:

  1. 网络 I/Oread() / write() 系统调用
  2. 命令执行:数据结构操作、内存分配
  3. 序列化/反序列化:特别是大 Key 的批量回复

这也引出了 Pipeline 和 IO 多线程优化的必要性。


四、Pipeline 与批量命令优化

Redis 的性能瓶颈往往不在服务端处理速度,而在网络往返(Round Trip Time, RTT)。在 1ms 跨机房网络中,即便服务端处理耗时 10 微秒,单次请求也需要 1ms 的总耗时(0.5ms 去 + 处理 + 0.5ms 回)。Pipeline 通过一次发送多条命令、一次读取所有回复,将多个 RTT 压缩为一个。

4.1 RTT 对吞吐量的影响

无 Pipeline:                有 Pipeline (n=100):
Client: SET k1 v1           Client: SET k1 v1
        | ======>                  SET k2 v2
        | <======                  ...
        | ======>                  SET k100 v100
        | <======                  | ========>
        | ...                      | <========
        | ======>                  
        | <======                 100 条命令,1 个 RTT
100 次 RTT

吞吐量: 1 / RTT (约 1k QPS)     吞吐量: 100 / RTT (约 100k QPS)

4.2 Pipeline 的使用方式

# redis-cli 的 Pipeline 模式(使用 --pipe 选项)
cat << 'EOF' | redis-cli --pipe
SET user:1001:name Alice
SET user:1001:age 28
HSET user:1001:profile city "Shanghai" country "CN"
EXPIRE user:1001:name 3600
EXPIRE user:1001:age 3600
EOF
# Python 使用 Pipeline
import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)
pipe = r.pipeline()

for i in range(1000):
    pipe.set(f"key:{i}", f"value:{i}")
    pipe.expire(f"key:{i}", 3600)

# 一次性发送 2000 条命令,只产生 1 次 RTT
results = pipe.execute()
print(f"成功执行 {len(results)} 条命令")
// Go 使用 Pipeline
package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    pipe := rdb.Pipeline()
    for i := 0; i < 1000; i++ {
        pipe.Set(ctx, fmt.Sprintf("key:%d", i), fmt.Sprintf("val:%d", i), 0)
    }

    cmds, err := pipe.Exec(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Printf("执行了 %d 条命令\n", len(cmds))
}

4.3 Pipeline vs MULTI 事务

特性PipelineMULTI/EXEC 事务
原子性非原子,命令可能被中间插⼊原子,事务内命令连续执行
回滚无(Redis 事务不支持回滚)
性能最高,仅减少 RTT中等,需加锁和 WATCH 乐观锁
适用场景大量独立命令的批量写入需要条件性执行的原子操作

注意:Pipeline 中的命令不是原子性的。如果中间某个命令失败,后续命令仍然执行。对于需要原子性的场景,使用 Lua 脚本或 MULTI/EXEC 事务

4.4 Pipeline 的最佳实践

# 1. 控制单次 Pipeline 的命令数量,避免输出缓冲区膨胀
# 一般建议 100-1000 条为一个 batch
BATCH_SIZE=500

# 2. 使用 Redis 2.6+ 的原生批量命令(比 Pipeline 更高效)
# MGET/MSET 的原子批量操作
MSET key1 val1 key2 val2 key3 val3
MGET key1 key2 key3

# HMSET/HMGET(现在推荐使用 HSET 的批量形式)
HSET user:1001 name Alice age 28 city Shanghai

# LPUSH/RPUSH 批量入队
LPUSH queue:tasks task1 task2 task3 task4 task5

# 3. 使用 UNLINK 替代 DEL 批量删除(异步释放内存)
UNLINK key1 key2 key3

4.5 Pipeline 与输出缓冲区

Pipeline 大批量发送命令时,服务端会在连接对应的 client->bufclient->reply 链表中累积回复。若 Pipeline 条数过多(如 10 万条),可能触发 Redis 的 client-output-buffer-limit,导致连接被强制断开。

# redis.conf 输出缓冲区限制
client-output-buffer-limit normal 0 0 0          # 普通客户端无限制
client-output-buffer-limit replica 256mb 64mb 60  # 副本客户端限制
client-output-buffer-limit pubsub 32mb 8mb 60     # 发布订阅客户端限制

建议:单次 Pipeline 命令数控制在 1000 以内,大批量写入时拆分成多个 Pipeline 批次,每批次之间留空行检查是否需要调整。


五、连接处理与 TCP Backlog 调优

高并发场景下,连接建立阶段是 Redis 常见的性能瓶颈之一。TCP 的三次握手、SYN Flood 攻击防护、accept 队列长度都直接影响并发能力。

5.1 TCP 连接建立流程

客户端: SYN
服务端: SYN + ACK    -> 进入 SYN_RECV,连接放入半连接队列
客户端: ACK
服务端: 连接移入 accept 队列,等待 accept()

Redis 主线程: accept() -> 创建 client 结构体 -> aeCreateFileEvent(读取)

5.2 关键内核参数调优

参数说明建议值
net.core.somaxconnaccept 队列最大长度65535
net.ipv4.tcp_max_syn_backlogSYN 半连接队列大小65535
net.ipv4.tcp_syncookiesSYN Cookie 防洪水攻击1(开启)
net.ipv4.tcp_tw_reuseTIME_WAIT 状态复用1
net.ipv4.tcp_tw_recycleTIME_WAIT 快速回收0(内核 4.12+ 已废弃)
net.ipv4.tcp_keepalive_timeTCP 保活探测间隔300
net.ipv4.tcp_keepalive_intvl保活探测重试间隔30
net.ipv4.tcp_keepalive_probes保活探测重试次数3
# 临时生效
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_syncookies=1

# 永久生效
# /etc/sysctl.d/99-redis.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

sysctl --system

5.3 Redis 层面配置

# redis.conf

# 最大客户端连接数(默认 10000)
maxclients 30000

# TCP backlog 长度(默认 511,但受限于 somaxconn)
tcp-backlog 65535

# TCP 保活探测间隔(秒)
tcp-keepalive 60

# 端口绑定
port 6379
bind 0.0.0.0

# 连接超时(秒)
timeout 0

tcp-backlog 设置为 65535 的同时,必须确保 net.core.somaxconn >= 65535。如果系统参数小于 Redis 配置,实际生效值会以系统参数为准,Redis 启动时会在日志中提示:

# WARNING: The TCP backlog setting of 65535 cannot be enforced because
# /proc/sys/net/core/somaxconn is set to the lower value of 128.

5.4 客户端连接池调优

频繁创建/关闭 Redis 连接会产生大量 TIME_WAIT 状态的套接字,消耗本地端口资源。应用层必须使用连接池:

// Jedis 连接池配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);          // 最大连接数
poolConfig.setMaxIdle(20);            // 最大空闲连接
poolConfig.setMinIdle(5);             // 最小空闲连接
poolConfig.setMaxWaitMillis(3000);    // 获取连接超时
poolConfig.setTestOnBorrow(false);    // 借用时检测(关闭可降低延迟)
poolConfig.setTestWhileIdle(true);    // 空闲时检测

JedisPool pool = new JedisPool(poolConfig, "localhost", 6379, 2000);
# Python redis-py 连接池
import redis

pool = redis.ConnectionPool(
    host='localhost',
    port=6379,
    max_connections=100,
    socket_keepalive=True,
    socket_keepalive_options={
        socket.TCP_KEEPIDLE: 60,
        socket.TCP_KEEPINTVL: 10,
        socket.TCP_KEEPCNT: 3,
    }
)
r = redis.Redis(connection_pool=pool)

六、Redis 6.0+ 多线程 I/O:io-threads

Redis 6.0 引入了一个被广泛期待的特性——多线程 I/O。理解这个设计的前提:Redis 的命令执行仍保持单线程,多线程仅用于网络 I/O 的读取和写入解析。

6.1 为什么不是"多线程 Redis"?

单线程执行命令是 Redis 数据一致性的基石。如果多个线程同时操作同一个 Hash 或 ZSet,就需要引入锁机制(如读写锁、细粒度锁),这会极大增加代码复杂度,且在高竞争下锁开销会吞噬多线程带来的收益。

因此 Redis 6.0 的设计是IO 线程 + 单线程命令处理器的混合模式:

+-------------------------------------------------------+
|                    Redis 主线程                         |
|  1. epoll_wait 等待就绪事件                             |
|  2. 将读就绪的 client 分发给 IO 线程队列                |
|  3. 等待所有 IO 线程完成读取和协议解析                    |
|  4. 主线程顺序执行所有 client 的命令                     |
|  5. 将写就绪的 client 分发给 IO 线程队列                  |
|  6. 等待 IO 线程完成回复写出                           |
+-------------------------------------------------------+
         |                      |
         v                      v
+------------+            +------------+
| IO Thread 1|            | IO Thread 2|
| read/parse |            | read/parse |
| write/reply|            | write/reply|
+------------+            +------------+
        ...                   ...
+------------+
| IO Thread N|
+------------+

6.2 IO 线程的工作内容

每个 IO 线程处理以下任务:

  1. 读取阶段:从 fd 读取网络数据到 client.querybuf
  2. 解析阶段:解析 RESP 协议,生成 client.argv / client.argc
  3. 写出阶段:将 client->buf / client->reply 链表的回复数据写入 fd

第一阶段和第三阶段是并行的(各个线程处理不同 client 的 I/O),命令执行阶段是串行的(主线程独占数据结构)。

6.3 启用 IO 多线程

# redis.conf - IO 多线程配置示例

# 启用 IO 多线程
io-threads-do-reads yes
io-threads 4

# 注意:主线程本身也是一个线程
# io-threads 4 表示 1 个主线程 + 3 个 IO 线程 = 4 个线程处理 I/O

配置建议:

场景io-threads说明
4 核以下2够用即可,避免线程调度浪费
4-8 核4推荐配置
8 核以上8上限不推荐超过 8,CPU 核心数的 1/2
纯写入负载关闭或不启用 reads若 write 比 read 多,io-threads 优化有限

注意:io-threads 是全局配置,不能针对单个客户端调整。Redis 7.0 引入了 CLIENT NO-EVICT 和更精细的控制,但 IO 线程数仍是全局的。

6.4 IO 线程的性能增益

根据 Redis 官方测试,在以下场景 IO 线程收益最明显:

  • 大流量写入:Pipeline 批量写入,每次 write 的数据量很大
  • 大量小请求:连接数 > 1000,每个请求的数据量小,但网络 I/O 密集
  • 长连接场景:短连接(频繁 connect/disconnect)下 IO 线程无显著收益

测试数据表明:8 核服务器、4 个 IO 线程、Pipeline=16 的场景下,吞吐量可从单线程的 80k QPS 提升到 240k QPS(约 3 倍)。但纯 GET 小键场景提升约 1.5 倍,因为命令执行本身已经是主线程瓶颈。

6.5 源码视角的 IO 线程分配

// networking.c - handleClientsWithPendingReadsUsingThreads

// 将待读取的客户端均匀分配给 IO 线程
int item_id = 0;
listRewind(server.clients_pending_read, &li);
while ((ln = listNext(&li))) {
    client *c = listNodeValue(ln);
    int target_id = item_id % server.io_threads_num;
    listAddNodeTail(io_threads_list[target_id], c);
    item_id++;
}

// 设置 IO 线程操作类型并开始
io_threads_op = IO_THREADS_OP_READ;
for (int j = 1; j < server.io_threads_num; j++) {
    int count = listLength(io_threads_list[j]);
    setIOPendingCount(j, count);
}

// 主线程也参与处理第 0 号列表
// ...

Redis 简单使用 item_id % io_threads_num 的轮询策略分配 client,没有基于 fd 亲缘性或 NUMA 感知的高级调度。这已足够,因为命令解析是无状态的,不需要考虑缓存一致性。


七、TLS 加密开销与配置

Redis 6.0 引入了对 TLS/SSL 的原生支持。启用 TLS 后,所有网络通信被加密,但这会带来 CPU 开销和延迟增长。

7.1 TLS 的握手开销

TLS 1.2 握手流程(完整握手):

Client: ClientHello
Server: ServerHello + Certificate + ServerKeyExchange
Client: ClientKeyExchange + ChangeCipherSpec + Finished
Server: ChangeCipherSpec + Finished
--- 2-RTT ---

TLS 1.3 握手流程:
Client: ClientHello + KeyShare
Server: ServerHello + EncryptedExtensions + Certificate + Finished
Client: Finished
--- 1-RTT ---

7.2 性能损耗对比

指标明文TLS 1.2TLS 1.3影响
吞吐量(QPS)100%85-90%90-95%加解密消耗 CPU
首次连接延迟RTTRTT + 2-RTTRTT + 1-RTTTLS 握手开销
CPU 占用基准+15-30%+10-20%对称/非对称加解密
内存占用基准略增略增TLS 上下文结构

长连接场景中,握手的单点开销被稀释。若应用使用连接池保持长连接,TLS 的总体性能损失可控制在 10% 以内。短连接场景(每次请求新建连接)则损失显著。

7.3 Redis TLS 配置

# redis.conf - TLS 最小配置

# 监听端口(若 port=0 则仅启用 TLS)
port 6379
tls-port 6380

# 证书配置
tls-cert-file /etc/redis/certs/redis.crt
tls-key-file /etc/redis/certs/redis.key
tls-ca-cert-file /etc/redis/certs/ca.crt

# 协议和密码套件
tls-protocols "TLSv1.2 TLSv1.3"
tls-ciphers DEFAULT:!MEDIUM

# 客户端证书验证(双向 TLS)
tls-auth-clients optional

# 集群/副本通信启用 TLS
tls-replication yes
tls-cluster yes

# TLS 会话缓存(减少握手次数)
tls-session-caching yes
tls-session-cache-size 2048
tls-session-cache-timeout 300

详细的 TLS 证书生成和客户端连接示例,可参考 Redis 生产安全指南 中"TLS/SSL 加密"一章。

7.4 TLS 性能优化建议

  1. 使用 TLS 1.3:1-RTT 握手,零往返恢复(0-RTT)在 Redis 场景下不适用(无重复会话语义),但仍比 TLS 1.2 快。
  2. 优先使用 AES-GCM:现代 CPU(Intel/AMD 2013+,ARMv8)支持 AES-NI 指令集,AES-GCM 加解密几乎零额外 CPU 开销。
  3. 保持连接复用:通过连接池避免频繁 TLS 握手。对于 go-redis、redis-py 等客户端,启用连接池即可自动复用。
  4. 硬件卸载:在极高吞吐场景(>50 万 QPS),可考虑支持 TLS offloading 的代理或 NIC 网卡(如 SmartNIC)。

7.5 对比明文流量抓包

# 明文 Redis 的 tcpdump 输出(可直接读取)
tcpdump -i eth0 port 6379 -A -n | grep "GET\|SET"
# 输出: .GET key1 (# 虽然是文本,但需要 RESP 解析)

# TLS Redis 的 tcpdump 输出(加密)
tcpdump -i eth0 port 6380 -A -n
# 输出: ...加密二进制乱码... 无法直接识别命令

对于安全合规要求高的场景(金融、政务、医疗),TLS 是不可选项。此时应将 Redis 部署在物理隔离的内网中,TLS 用于防止内部横向渗透,而不是公网暴露的直接防护。


八、网络基准测试:redis-benchmark

性能优化必须基于量化指标。Redis 自带的 redis-benchmark 工具是验证网络调优效果的利器。

8.1 基础用法

# 基本压力测试(10 万请求、50 并发连接)
redis-benchmark -h localhost -p 6379 -n 100000 -c 50

# 输出示例:
# ====== SET ======
# 100000 requests completed in 0.82 seconds
# 50 parallel clients
# 3 bytes payload
# keep alive: 1
# 99.99% <= 1 milliseconds
# 121951.22 requests per second
参数说明常用值
-h主机localhost
-p端口6379 / 6380
-n总请求数100000
-c并发连接数50 / 100 / 500
-d数据大小(字节)3 / 100 / 1024 / 4096
-t测试的命令集set,get,lrange,lpush
-PPipeline 批量数1 / 16 / 100
-kkeepalive1 (长连接) / 0 (短连接)
--csvCSV 格式输出用于对比分析
--tls启用 TLS测试 TLS 场景

8.2 Pipeline 性能对比

# 无 Pipeline
redis-benchmark -n 100000 -c 50 -t set,get -P 1
# ~80,000 requests per second

# Pipeline=16
redis-benchmark -n 100000 -c 50 -t set,get -P 16
# ~1,200,000 requests per second (提升 15 倍!)

# Pipeline=100
redis-benchmark -n 100000 -c 50 -t set,get -P 100
# ~2,500,000 requests per second

注意:极高的 Pipeline 值会导致服务端输出缓冲区膨胀,实测时客户端和服务端都需合理配置。

8.3 大 Key 场景测试

# 测试 1KB 数据的读写性能
redis-benchmark -n 100000 -c 50 -d 1024 -t set,get

# 测试 10KB 数据
redis-benchmark -n 10000 -c 50 -d 10240 -t set,get

# 测试 100KB 数据(注意网络带宽瓶颈)
redis-benchmark -n 10000 -c 10 -d 102400 -t set,get

8.4 TLS 性能对比测试

# 明文性能基准
redis-benchmark -h localhost -p 6379 -n 100000 -c 50 -t set,get --csv > plain.csv

# TLS 性能测试
redis-benchmark -h localhost -p 6380 -n 100000 -c 50 -t set,get \
  --tls \
  --cacert /etc/redis/certs/ca.crt \
  --cert /etc/redis/certs/client.crt \
  --key /etc/redis/certs/client.key \
  --csv > tls.csv

# 对比
diff plain.csv tls.csv

8.5 自定义脚本与 CPU 亲和力

# 绑定 Redis 到特定 CPU 核心,避免上下文切换
# redis-server 启动时
taskset -c 0 redis-server /etc/redis/redis.conf

# 绑定 benchmark 到另一颗核心
taskset -c 1 redis-benchmark -h localhost -n 1000000 -c 100 -t set,get -P 16

8.6 结果分析模板

测试完后重点观察这些指标:

# 综合吞吐量
redis-benchmark -n 1000000 -c 100 -P 16 -t set,get,lpush,lrange

# 关注长尾延迟:99th percentile 应 < 5ms
# 若 p99 显著高于 p95,说明存在偶发阻塞(如 AOF fsync、大 Key、内存碎片整理)

# INFO 命令查看服务端状态
redis-cli INFO stats
# keyspace_hits / (keyspace_hits + keyspace_misses) = 命中率
# total_commands_processed / uptime_in_seconds = 实际 QPS
# instantaneous_ops_per_sec = 瞬态 QPS

8.7 IO 线程效果验证

# 单线程基准
redis-cli CONFIG SET io-threads 1
redis-benchmark -n 500000 -c 100 -P 16 -t set,get > single_thread.txt

# 多线程基准
redis-cli CONFIG SET io-threads 4
redis-cli CONFIG SET io-threads-do-reads yes
redis-benchmark -n 500000 -c 100 -P 16 -t set,get > multi_thread.txt

# 对比差异
diff single_thread.txt multi_thread.txt

九、生产环境网络优化检查清单

以下是基于本文内容的 Redis 网络性能检查清单,可用于上线前或定期巡检:

9.1 操作系统层面

  • net.core.somaxconn >= 65535redis.conftcp-backlog 已同步
  • fs.file-max > maxclients 的 2 倍以上
  • ulimit -n 已设置为足够大的值(如 65535)
  • TCP 保活已配置(tcp-keepalive + 内核参数)
  • 禁用了 iptables conntrack 对 Redis 端口的追踪(高连接数下 conntrack 表会溢出)
  • 若使用 NUMA 架构,已将 Redis 绑定到单一 NUMA 节点的核心

9.2 Redis 配置层面

  • io-threads 设置为 CPU 核心数的 1/2(建议 2-8)
  • io-threads-do-reads 已在 I/O 密集型场景启用
  • maxclients 已根据实际并发调整,且不超过系统 fd 上限
  • client-output-buffer-limit 已根据 Pipeline 场景配置
  • 若启用 TLS,tls-session-caching yes 已配置以减少握手开销
  • timeout 按需设置(0 表示永不超时,长连接场景推荐)

9.3 应用层面

  • 客户端使用连接池,禁止每个请求新建连接
  • Pipeline 批量写入时,每次命令数控制在 500-1000 以内
  • 大批量命令优先使用原生批量 API(MGET/MSET/HMSET/UNLINK 等)
  • 高并发读场景已考虑读写分离或 Cluster 多节点 分散压力
  • 跨可用区访问已启用连接池 + Pipeline,减少 RTT 影响
  • 定期进行 redis-benchmark 基准测试,建立性能基线

9.4 监控告警指标

指标告警阈值意义
instantaneous_ops_per_sec< 基线 × 70%吞吐量突降
connected_clients> maxclients × 80%连接数接近上限
rejected_connections> 0连接被拒绝
client_longest_output_list> 10MB某个客户端输出缓冲区膨胀
used_memory_rss / used_memory> 1.5内存碎片严重
TCP retransmits/sec> 10网络拥塞或丢包
CPU iowait> 10%磁盘 I/O 阻塞(TLS 或持久化导致)

十、总结

Redis 的网络模型是一个从操作系统多路复用、协议层 RESP 到应用层事件循环深度协同的系统工程。理解它的核心在于把握几个关键点:

  1. 单线程事件循环 是 Redis 高性能的根基。没有上下文切换、没有锁竞争,主线程一心一意处理就绪事件。ae.c 的简洁设计(注册回调 -> epoll_wait -> 顺序执行)是千万级并发的起点。

  2. 多路复用选型 上,Linux 生产环境必须用 epoll,macOS 和 FreeBSD 用 kqueue。select 和 poll 只在老旧系统兜底用。理解 epoll 的红黑树 + 就绪链表设计,才能明白为什么它能支撑 C100K。

  3. RESP 协议 虽然是文本协议,但足够紧凑且解析简单。RESP3 的强类型支撑了现代 Redis 模块和客户端缓存。了解 wire-level 格式有助于自定义客户端和故障排查。

  4. Pipeline 是提升吞吐量的第一手段。它能将 RTT 从"每个命令一次"压缩到"一批命令一次"。生产环境中应养成批量写入的习惯,同时要控制批量大小避免缓冲区溢出。

  5. TCP Backlog 和内核参数 在高并发连接爆发时常常成为瓶颈。somaxconntcp_max_syn_backlog、fd 上限这三个参数必须与 Redis 的 maxclientstcp-backlog 联合调优。

  6. Redis 6.0+ IO 多线程 不改变命令执行的单线程本质,但将网络 I/O 的解析和序列化分发到多个线程。在 4-8 核服务器上能获得 1.5-3 倍的吞吐量提升,但超过 8 个 IO 线程收益递减。

  7. TLS 加密 让 Redis 满足安全合规要求,但代价是 10-30% 的 CPU 开销和额外的 RTT。长连接 + 会话缓存 + TLS 1.3 能将损失控制在可接受范围。安全要求与性能不可兼得时,优先用私有子网 + 防火墙降低加密需求。

  8. redis-benchmark 是验证一切调优效果的最终裁判。建立性能基线、对比不同配置下的吞吐量与延迟、关注长尾指标(p99/p99.9),才是数据驱动的 Redis 运维。

网络是 Redis 性能的放大器,也是瓶颈的放大器。把本文的检查清单融入到日常运维中,定期使用 redis-benchmark 验证假设,你的 Redis 将在高并发网络环境中始终保持稳定与高效。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南