简单粗暴的了解redis、nginx中的epoll多路复用

2,339 阅读6分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

前面我们比较深入切直白的分析了io到nio的演变,其中提到了epoll多路复用,今天主要结合着redis、nginx来重点了解下epoll这个玩意儿

首先我们要搞清楚什么是多路复用,简单理解就是:一个服务端进程可以同时处理多个套接字描述符。

  • 多路:多个客户端连接(连接就是套接字描述符)
  • 复用:使用单进程就能够实现同时处理多个客户端的连接

废话不多说,直接干吧(我在机器上安装了redis和nginx,如果你想操练下可以按照下面的步骤走一遍)

nginx

启动并追踪

通过如下命令启动并且追踪nginx

strace -ff -o ./nginxout /usr/local/nginx/sbin/nginx

执行上面命令后发现生成了如下三个文件,对应的是三个进程

通过ps -ef|grep nginx可以发现Nginx 实际上是有两个进程master-14607和worker-14608(配置文件默认只有一个worker),那14606是啥玩意呢

过渡进程

看下nginxout.14606这个文件,如下:

mkdir("/usr/local/nginx/scgi_temp", 0700) = -1 EEXIST (File exists)
stat("/usr/local/nginx/scgi_temp", {st_mode=S_IFDIR|0700, st_size=4096, ...}) = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 4
fcntl(4, F_SETFD, FD_CLOEXEC)           = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 5
fcntl(5, F_SETFD, FD_CLOEXEC)           = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
ioctl(6, FIONBIO, [1])                  = 0
bind(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(6, 511)                          = 0
listen(6, 511)                          = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024*1024, rlim_max=1024*1024}) = 0
mmap(NULL, 384, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0x7fe7f8911000
rt_sigaction(SIGHUP, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGUSR1, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGWINCH, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGTERM, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGQUIT, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGUSR2, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGALRM, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGIO, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGCHLD, {sa_handler=0x41eed5, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGSYS, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7fe7f84d8b20}, NULL, 8) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14607
exit_group(0)                           = ?
+++ exited with 0 +++
~                     

可以看到这个文件的最后一行显示退出了,但是在退出之前它还在做了它该做的,截取如下:

// 创建socket
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
// 绑定端口
bind(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
//监听
listen(6, 511) 

是不是有点通了呢,不要急我们继续看

其实它是一个过渡的程序,为啥这么说呢,仔细看上面的图片你会发现有个clone操作,也就是说14606生出了14607

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14607

出现了socket..、bind..、listen.. 但是并没有发现epoll操作,我们再去matser(14607)进程中看下

master进程

set_robust_list(0x7fe7f8907a20, 24)     = 0
getpid()                                = 14607
setsid()                                = 14607
umask(000)                              = 022
openat(AT_FDCWD, "/dev/null", O_RDWR)   = 7
dup2(7, 0)                              = 0
dup2(7, 1)                              = 1
close(7)                                = 0
openat(AT_FDCWD, "/usr/local/nginx/logs/nginx.pid", O_RDWR|O_CREAT|O_TRUNC, 0644) = 7
pwrite64(7, "14607\n", 6, 0)            = 6
close(7)                                = 0
dup2(5, 2)                              = 2
close(3)                                = 0
rt_sigprocmask(SIG_BLOCK, [HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO], NULL, 8) = 0
socketpair(AF_UNIX, SOCK_STREAM, 0, [3, 7]) = 0
ioctl(3, FIONBIO, [1])                  = 0
ioctl(7, FIONBIO, [1])                  = 0
ioctl(3, FIOASYNC, [1])                 = 0
fcntl(3, F_SETOWN, 14607)               = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
fcntl(7, F_SETFD, FD_CLOEXEC)           = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14608
rt_sigsuspend([], 8

然后你会发现master进程中的内容很少,因为master其实是不干活的,master中直接就是一个clone,创建了一个worker进程

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe7f8907a10) = 14608 

worker进程

所以我们还是要接着往下找,看下worker进程干了哪些事(由于内容比较多,我这边copy了主要的内容并加了一些注释)

// 开辟了一块内核空间
epoll_create(512)                       = 8
// 将上面提到的6 放到这块内核空间中(6、7分别是ipv4、ipv6的)
epoll_ctl(8, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLRDHUP, {u32=4169998352, u64=140634284171280}}) = 0
close(3)                                = 0
epoll_ctl(8, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLRDHUP, {u32=4169998560, u64=140634284171488}}) = 0
// 等待就绪
epoll_wait(8,

对于上图的6、7可以直接到主进程的fd看下(cd /proc/14607/fd)

然后再看下关于nginx中关于80端口的监听情况

以上是关于nginx中epoll的情况,

redis

启动并追踪

strace -ff -o ./redisout  /software/redis-5.0.10/src/redis-server 

日志一直在变大

主进程

进入主进程看看,注意下面这个图包含的信息量较大

重要的内容提取下

epoll_create(1024)                      = 5
socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 6 
bind(6, {sa_family=AF_INET6, sin6_port=htons(6379), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::", &sin6_addr), sin6_scope_id=0}, 28) = 0
listen(6, 511)                          = 0
fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)
fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 7
setsockopt(7, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(7, { =AF_INET, sin_port=htons(6379), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(7, 511)                          = 0
fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)
fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
gettimeofday({tv_sec=1628522184, tv_usec=972588}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972618}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972647}, NULL) = 0
gettimeofday({tv_sec=1628522184, tv_usec=972677}, NULL) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=6}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=7, u64=7}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN, {u32=3, u64=3}}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0

至于它为什么一直变大,可以通过tail -f redisout.14719命令,可以看到它一直在轮询,是不是发现有点不对啊

同样是使用epoll,我们前面介绍nginx是阻塞,但是redis怎么是轮询呢?其实仔细思考下应该会有一些想法

  • redis是单线程,这一个线程除了读写还要去做其他事情(fork线程做LRU、RDB、AOF…),所以需要轮询,在循环中做io以及其他操作
  • nginx也是单线程的,和redis一样使用了epoll,不同点在于nginx多进程,提高了稳定性和并发能力

IO是个细活,最近几篇文章主要是聚焦于如何高效的处理io,epoll多路复用解决就是io消息事件的事情,它可以通过事件驱动的方式告诉工作线程那是连接准备就绪了(可读/可写);工作线程拿到这些连接再去做读/写。IO很难一下子搞清楚,需要从多个方面去思考,文章主要是从实际操作日志的角度分析了多路复用的一些逻辑,希望对你有点帮助,先到这里吧,希望大家越来越好!

待得秋来九月八,我花开时百花杀 - 黄巢