程序的工作流程:高级语言 -> 编译器 -> 低级语言指令 -> 系统管理运行程序 <—> 硬件。
文章主要对 tcp 通信进行 epoll 源码走读。
Linux 源码:Linux 5.7 版本。epoll 核心源码:eventpoll.h / eventpoll.c。
搭建 epoll 内核调试环境视频:vscode + gdb 远程调试 linux (EPOLL) 内核源码
epoll 源码涉及到很多知识点:(socket)网络通信,进程调度,等待队列,socket 信号处理,VFS(虚拟文件系统),红黑树算法等等知识点。有些接口的实现,藏得很深,参考了不少网上的帖子,在此整理一下。
本文主要为 《[epoll 源码走读] epoll 实现原理》,提供预备知识。
本章主要说 c 语言。
源码工作流程:程序员编写代码 -> 编译 -> 产生二进制执行文件 -> 文件加载到系统运行。
编译这个环节,其实是一个高级语言翻译成低级语言过程:高级语言 -> 汇编 -> 机器语言。
从业务逻辑上,了解一下 epoll
多路复用 I/O 的工作流程。
有兴趣了解 epoll 源码实现,可以参考: [epoll 源码走读] epoll 实现原理
Redis 6.0 版本增加了多线程并发处理网络 IO 功能,主要是为了利用多核资源,减轻主线程负载,提高程序整体性能。
Redis 是 多进程 + 多线程
混合并发模型。
详细参考:《[Redis] 浅析 Redis 并发模型》
下图描述了 Redis 客户端与服务端主线程异步通信流程,有兴趣的朋友可以参考:《[redis 源码走读] 异步通信流程-单线程》,这里不详细展开了。
Redis 6.0 以前,主线程处理网络 IO;Redis 6.0 增加了多线程处理网络 IO 功能,详见下图。
io-threads 线程配置,redis.conf 配置文件默认是不开放的,默认只有一个线程在工作,这个线程就是主线程
。
io-threads 4
,如果开放多线程配置,那么 IO 处理线程默认共有 4 个,包括主线程。也就是说,新增的 IO 线程有 3 个。io-threads-do-reads no
,是否开启读操作多线程模式;因为 Redis 作为缓存服务,读入数据比较小,写出数据比较多,所以读操作非必要不需要开启多线程模式。1
2
3
4
5
6
7
# redis.conf
# 配置多线程处理线程个数,默认 4。
# io-threads 4
#
# 多线程是否处理读事件,默认关闭。
# io-threads-do-reads no
网络读写操作大同小异,下面根据源码剖析写操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
int handleClientsWithPendingWritesUsingThreads(void) {
int processed = listLength(server.clients_pending_write);
if (processed == 0) return 0;
// 如果 client 很少,关闭多线程模式,用主线程处理写操作。
if (stopThreadedIOIfNeeded()) {
// 主线程处理写操作。
return handleClientsWithPendingWrites();
}
if (!io_threads_active) startThreadedIO();
// 主线程分配任务,将 client 按取模的方式分配给各个线程。
listIter li;
listNode *ln;
listRewind(server.clients_pending_write,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
// 标识写操作。
io_threads_op = IO_THREADS_OP_WRITE;
// 设置 io_threads_pending 数据,
// 后面根据这个数据确定子线程是否已完成任务。
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
io_threads_pending[j] = count;
}
// 主线程处理第一个队列。
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 写数据,发送给回复给客户端。
writeToClient(c,0);
}
listEmpty(io_threads_list[0]);
// 等待所有子线程完成任务。
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;
}
listRewind(server.clients_pending_write,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 如果有的 client 数据还没发送完(异步),那么注册写事件,下次再触发发送。
if (clientHasPendingReplies(c)
&& connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR) {
freeClientAsync(c);
}
}
listEmpty(server.clients_pending_write);
return processed;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void *IOThreadMain(void *myid) {
long id = (unsigned long)myid;
while(1) {
...
// 根据操作类型,处理对应的读/写逻辑。
listIter li;
listNode *ln;
listRewind(io_threads_list[id],&li);
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);
}
...
}
// 已完成任务,清空数据。
listEmpty(io_threads_list[id]);
io_threads_pending[id] = 0;
}
}
压测设备:8 核心,16G 内存。
配置。多线程模式测试,开启读写两个选项;单线程模式测试则会关闭。
1
2
3
# redis.conf
io-threads 4
io-threads-do-reads yes
命令逻辑已整理成脚本,放到 github,顺手录制了测试视频:压力测试 redis 多线程处理网络 I/O。
1
2
3
4
5
# 压测工具会模拟多个终端,防止超出限制,被停止。
ulimit -n 16384
# 可以设置对应的链接数/包体大小进行测试。
./redis-benchmark -c xxxx -r 1000000 -n 100000 -t set,get -q --threads 2 -d yyyy
Linux 环境下,用 tcpdump
抓包分析 tcp 三次握手和四次挥手/三次挥手。
测试一下看看,Linux 环境下,这三个函数(strcpy, strncpy, snprintf)哪个比较安全。
redis 服务底层采用了异步事件
管理(aeEventLoop
):管理时间事件和文件事件。对大量网络文件描述符(fd)事件管理,redis 建立在安装系统对应的事件驱动基础上(例如 Linux 的 epoll
)。
关于事件驱动,本章主要讲述 Linux 系统的 epoll 事件驱动。
关于事件处理,本章主要讲述文件事件,时间事件可以参考帖子 《[redis 源码走读] 事件 - 定时器》。
定时器是 redis 异步处理事件的一个十分重要的功能。
redis 定时器功能由多个时间事件组成,事件由一个双向链表维护。
时间事件可以处理多个定时任务。
aof 和 rdb 是 redis 持久化的两种方式。我们看看它们的特点和具体应用场景区别。
aof (Append Only File) 是 redis 持久化的其中一种方式。
服务器接收的每个写入操作命令,都会追加记录到 aof 文件末尾,当服务器重新启动时,记录的命令会重新载入到服务器内存还原数据。这一章我们走读一下源码,看看 aof 持久化的数据结构和应用场景是怎样的。
主要源码逻辑在
aof.c
文件中。
文章重点讲述 aof 持久化的应用场景。aof 持久化,拆分上下为两章,可以先读上一章。
商品秒杀场景比较常见,例如 12306 抢票等等。是高并发和分布式系统处理的经典案例。个人没做过秒杀项目,仅仅对这个问题感兴趣,研究了一下。个人认为秒杀场景主要有两个核心问题:到点抢购负载和商品超量。
rdb 文件是一个经过压缩的二进制文件,上一章讲了 rdb 持久化 - 应用场景,本章主要讲述 rdb 文件的结构组成包含了哪些数据。