使用GDB调试Redis
Contents
环境准备
以 Redis 4.0.14 为例子. Linux 环境
下载 redis 源码 redis-4.0.14.tar.gz
编译时使用 make noopt
然后启动 redis-server
安装 GDB : sudo apt-get install gdb
禁用 ptrace : sudo echo 0 > /proc/sys/kernel/yama/ptrace_scope
调试
gdb ./src/redis-server
# 在 main 函数中打断点
(gdb) b main
# 执行. 这时可以发现 gdb 停留在了 main 函数入口处.
(gdb) r
Starting program: /home/company/redis/redis-4.0.14/src/redis-server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main (argc=1, argv=0x7fffffffe5d8) at server.c:3714
3714 int main(int argc, char **argv) {
(gdb)
# 显示源码窗口
(gdb) layout src
GDB
运行
run 或 r : 运行程序, 遇到断点处停止运行
continue 或 c : 继续执行, 到下一个断点处.
next 或 n : 执行到代码下一行(有函数也不会跳进去)
step 或 s : 执行下一步(有函数则会跳进去)
until : 跳出循环体
call 函数(参数) : 调用函数
quit 或 q : 退出 gdb
分割窗口
layout:用于分割窗口,可以一边查看代码,一边测试:
layout src:显示源代码窗口
显示变量/函数
info locals
info function
whatis 变量或函数名
print 变量名
Redis 运行逻辑
spt_init(argc, argv);
: 初始化进程名(set proc title, spt)配置. 参考 https://www.cnblogs.com/imlgc/p/3823990.htmlsetlocale(LC_COLLATE,"");
设置 COLLATE. 它会影响排序和正则表达式zmalloc_set_oom_handler
: OOM 时的处理srand(time(NULL)^getpid());
: 初始化随机函数的种子gettimeofday(&tv,NULL);
获取日期时间getRandomHexChars(hashseed,sizeof(hashseed));
: 生成一个 redis 的run id
. 十六进制dictSetHashFunctionSeed((uint8_t*)hashseed);
: 根据run id
, 设置 Dict, Set, Hash 函数的 seedserver.sentinel_mode = checkForSentinelMode(argc,argv);
: 检测是否是 sentinel modeinitServerConfig();
初始化服务器配置moduleInitModulesSystem();
初始化模块系统server.executable=xxx; server.exec_argv=xxxx;
: 用来保存可执行文件的路径和相应的参数if (strstr(argv[0],"redis-check-rdb") != NULL) 或 if (strstr(argv[0],"redis-check-aof") != NULL)
: 判断是否只是 check rdb 或 rof 文件.- 然后是判断命令行选项参数. 比如
-v
或-c
之类的处理逻辑. 或命令行指定选项参数, 以覆盖默认或文件的参数.(将文件的 + 命令行参数的一起, 然后判断赋值. 这样子, 命令行是最后处理的, 就会覆盖文件指定的了) - 然后打印启动日志
server.supervised
: 是否启用supervised.daemonize
并且非server.supervised
才是后台模式. (background).int background = server.daemonize && !server.supervised;
supervised
的值可以为- no
- upstart
- systemd
- auto
initServer()
: 初始化服务器- 初始化相应的
server.xx
的值 - 初始化
epoll
初始化 db 的配置 - 以及相应的
ae
. 和注册相应的 ae 事件处理器. 比如acceptTcpHandler
.acceptUnixHandler
- Slow log 初始化
- 初始后台系统(比如处理关闭文件, AOF 的 fsync, Object 的释放)
- 初始化相应的
if (background || server.pidfile) createPidFile();
根据需要创建 PID 文件redisSetProcTitle(argv[0]);
: 正式设置进程标题redisAsciiArt();
: 输出 redis 的 ASCII 艺术字符checkTcpBacklogSettings();
检查 tcp 的 backlog 配置./proc/sys/net/core/somaxconn
的大小跟 redis 设置的 backlog 大小.- 如果 redis backlog > somaxconn 则警告. 因为系统取二者最小值.
linuxMemoryWarnings();
linux 相关的内存警告. 比如 overcommit, huge page 等moduleLoadFromQueue()
加载模块loadDataFromDisk()
加载 DB 文件- 最后就是
ae
事件循环处理的代码了.aeMain(server.el);
类似 Netty 的 EventLoop
Redis 的 AE
aeEventLoop 结构 :
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
最大 clients 数
默认值为 10000 .
#define CONFIG_DEFAULT_MAX_CLIENTS 10000
保留值为 32
#define CONFIG_MIN_RESERVED_FDS 32
根据上面, 最大文件数限制为 10000 + 32
实际大小, 会通过
adjustOpenFilesLimit()
根据系统限制的设置再调整.limit 的结构体如下:
struct rlimit { rlim_t rlim_cur; /* Soft limit */ rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */ };
获取当前系统的限制
getrlimit(RLIMIT_NOFILE,&limit)
- 用命令查看当前系统对每个进程的软/硬打开文件数限制:
ulimit -Hn
: 硬ulimit -Sn
: 软- 失败的话, 则最大的
maxclients = 1024 - CONFIG_MIN_RESERVED_FDS
- 成功的话
- 如果系统当前软限制 小于
CONFIG_DEFAULT_MAX_CLIENTS + CONFIG_MIN_RESERVED_FDS
则调整适当的大小- 尝试调用 api
setrlimit
来设置为bestlimit = CONFIG_DEFAULT_MAX_CLIENTS + CONFIG_MIN_RESERVED_FDS
的大小(软/硬都是该值) - 设置失败的话, 则每次递减 16. 即
bestlimit -= 16
- 直到成功或 bestlimit < 16 时退出这种尝试设置
- 如果 bestlimit 小于最开始时获取的软限制, 则 bestlimit = 最开始时的软限制大小
- 最后的 bestlimit 大小, 小于 `
CONFIG_DEFAULT_MAX_CLIENTS + CONFIG_MIN_RESERVED_FDS
则打印相应的警告信息, 提示当前的 maxclients 跟配置的不一致的信息, 以及尝试使用 api 来设置时出错的话也会提示为什么设置失败. 最后提示最终的 maxclients 数以及提示通过ulimit -n
来增加相应的最大打开文件数以匹配配置的大小.
- 尝试调用 api
ae
最大的events/fired
数为实际的 maxclients + CONFIG_FDSET_INCR(默认为 128)
创建
aeEventLoop *aeCreateEventLoop(int setsize)
:最终调用
aeApiCreate(aeEventLoop *eventLoop)
来调用系统 api 创建.int aeProcessEvents(aeEventLoop *eventLoop, int flags)
: 事件处理代码Flags 相关的在
ae.h
文件中定义了.
处理 TCP 连接过程
- 首先, 在
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
里进行 eventLoop 循环 - 然后拉取相应的事件(在文件
ae_epoll.c
)static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
- 通过系统 API 来获取有多少个事件:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
. 返回值为有多少个文件描述符准备好了请求的I/O
- 然后依次在结构体
eventLoop->fired[N].fd
上设置相应的文件描述符. 以及相应的操作事件 mask.eventLoop->fired[N].mask
- 然后循环处理每个fired 触发的文件文件描述符的读写事件.
- 读事件 (接收到 client 的请求):
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
- 由于是文件描述符的事件. 所以调用相应的事件处理器(这个在
initServer() 里初始化了, 并且注册的是 acceptTcpHandler
) .- 最多循环
MAX_ACCEPTS_PER_CALL
次调用acceptCommonHandler
来处理请求 acceptCommonHandler
会创建一个 client 并注册相应的处理器client *createClient(int fd)
aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c)
- 判断当前 client 列表是否超过
server.maxclients
- 是否开启了保护模式及相关判断. (默认情况下,开启保护模式, 没有设置绑定地址, 并且没有要求密码 时, 非通过本地登录
127.0.0.1
时, 会拒绝执行命令, 并输出一些警告write(c->fd,err,strlen(err))
. - 处理客户端的输入
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)
- 判断是否为 master/slave
- 处理 inputBuffer:
processInputBuffer(c);
- 判断请求的协议类型(比如是否为
PROTO_REQ_MULTIBULK
还是普通的PROTO_REQ_INLINE
) - 输入 queryBuffer 中, 第一个字符为
*
的表示是PROTO_REQ_MULTIBULK
- 否则为
PROTO_REQ_INLINE
. (管道类型命令) - 根据不同的请求类型, 调用不同的处理方法来解析命令. 并将解析好的命令, 保存在
c->argc
(参数个数),c->argv(参数值)
. 例如命令get hello
. 这时,c->argc = 2
,c->argv[0] = get; c->argv[1]=hello
processInlineBuffer(c)
processMultibulkBuffer(c)
- 处理完 queryBuffer (即解析命令)后, 就执行命令
processCommand(c)
- 根据
c->argv[0]
来查找命令. 如果没有该命令, 则提示unknown command xxx with args beginning with: yyy
- 然后判断命令的参数跟是否正确. 否则提示
wrong number of argumetns for xxx command
- 判断是否授权(要求密码时, client 是否已经授权)
- 最后调用
void call(client *c, int flags)
来执行命令 - 判断慢查询的统计就是
long start; proc(c); long end-start
. 根据这个时间差是否 > slow log 的判断值slowlog-log-slower-than
的值, 然后添加相应的 slow log .
- 判断请求的协议类型(比如是否为
- 最多循环
关于 Redis 的阻塞
看代码, 所有的操作都是在
aeMain
单线程循环处理的. 所以就是说整个流程```bash
aeMain -> acceptTcpHandler -> createClient -> readQueryFromClient -> processInputBuffer -> processInlineBuffer/processMultibulkBuffer -> processCommand -> call -> addReply -> aeMain
都是一条线程在处理的.
即无论是 `redis-server` 的事件, 还是 client 执行命令的事件, 都是在同一个 eventLoop 中串行执行的.
> Redis 有部分命令是异步的, 这个请参考具体的 Redis 说明 . 这里只了解下源码的大概流程.
>
> 特别是 Redis 6.0 , 添加多线程处理请求, 但执行命令是串行的.
也可以模拟验证.
启动 `redis-server`
在一个命令行窗口中执行命令 `debug sleep 100` (表示休眠 100 秒).
然后在另一个新的窗口中连接 `redis-cli` , 这时会发现它一直在阻塞中, 直到 sleep 命令执行完毕.
# Monitor 命令实现原理
- client 输入 monitor 命令
- 这时, redis 会将该 client 添加到 `server.monitors` 列表中
- 返回 `+OK` 表示执行成功
- 这时其他 client 执行命令时, redis 在调用前判断 `server.monitors` 的长度是否 > 0 , 然后将该命令字符及参数输出到这些 client 的输出缓存区中. 源码在 `server.c` 源文件中的 `void call(client *c, int flags)` 方法第一个 `if` 语句中.
c /* Sent the command to clients in MONITOR mode, only if the commands are * not generated from reading an AOF. */ if (listLength(server.monitors) && !server.loading && !(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN))) { replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc); } ```
关于 pipeline
Lettuce 库中的 piepline 抓包如下
redis-clie 的 pipeline 抓包如下
redis-cli –pipe < /tmp/test.cmd
## 差别
- Lettuce 的
pipeline
不是严格按照 Redis 的 pipeline 协议格式的, 只是在 TCP 层, 一次写入然后传输. - Redis-cli 的才是真正意义上的 pipeline
关于 RDB
触发条件
- 手动
save
命令 : 一直阻塞, 直到 save 完成bgsave
命令 : 后台任务执行 save, 只阻塞一段时间(主要是fork()
子进程来处理, 之后继续处理其他请求)
- 自动 (save x y)
- 根据配置文件
save x y
表示 X 秒内有 Y 次改变数据就触发bgsave
- 根据配置文件
注意
- Client 的 queryBuffer 一次性不能发送太多数据. 超过
PROTO_INLINE_MAX_SIZE
(64KB) 的话, redis server 会报Protocol error: too big inline request. too big inline request