环境准备

以 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.html
  • setlocale(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 函数的 seed
  • server.sentinel_mode = checkForSentinelMode(argc,argv); : 检测是否是 sentinel mode
  • initServerConfig(); 初始化服务器配置
  • 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 来增加相应的最大打开文件数以匹配配置的大小.
  • 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 单线程循环处理的. 所以就是说整个流程

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 语句中.
    /* 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 抓包如下

image-20190814175323662

redis-clie 的 pipeline 抓包如下

redis-cli –pipe < /tmp/test.cmd

image-20190814175730115

## 差别

  • 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

参考资料