前言
面试的时候经常会问为什么redis单线程,性能那么高?于是很多培训机构的老师说是因为IO多路复用,于是就硬背回答,结果面试官也给你过了,可是到底什么是IO多路复用?仍然一知半解,下面我用实际抓包工具来谈谈IO多路复用。
redis IO多路复用
这个图应该是讲redis IO多路复用的老演员了。但是靠这个讲明白IO多路复用,我个人觉得是不合适的。因为他没体现出redis的单线程。
什么是socket
在谈IO多路复用时,我们需要做一下前期知识的铺垫。我们知道,进行网络通信时,需要三次握手建立TCP连接,建立连接后我们可以通过socket进行通信。那么socket是什么东西呢?
socket翻译就是插座的意思,如图所示,我客户端的插头插到服务端的插座上,就建立起了连接进行通信。那么这个插座插头对应到我们的操作系统是什么呢?其实就是一个文件,这个文件的文件类型是socket(后续会给大家看这个文件)。在我们的应用程序中,我们打开文件比如file.open(xxx),会返回给我们一个整数,这个整数叫做文件描述符(或者文件句柄)。我们可以用这个文件句柄来对文件进行操作。所以我们可以知道,当我们连接通信是通过TCP时,TCP对应到我们实际操作系统时,就会客户端 服务端各打开一个socket文件。所以比如我redis有五个客户端连接,那么我服务端就会有五个socket文件。
安装redis服务端
#下载二进制包
wget http://download.redis.io/releases/redis-4.0.10.tar.gz
#解压
tar xzf redis-4.0.10.tar.gz
cd redis-4.0.10/
#编译 可能会报错 记得先装gcc相关工具 具体我懒得写 网上安装redis一大堆
make
#启动 记得把redis.conf改为可以外部连接 同时关闭防火墙
cd src
./redis-server ../redis.conf
记得redis用二进制安装,不要用docker,docker有自身的网络协议栈,测试起来比较麻烦。当我们把redis启动后我们使用命令,查看下tcp连接数
netstat -na|grep ESTABLISHED
我们可以看到这个redis服务端已经有4个tcp连接了,客户端分别是192.168.110.105:55343,172.17.0.5:60554等等。
同样,我们看看这个对应的socket的文件数量。
#找到redis的pid
ps -ef |grep redis
#填上redis的pid 查看这个进程的文件描述符
cd /proc/{pid}/fd
#列出进程内所有文件
ll
我们可以看到很多的socket文件和管道文件。这个socket文件和客户端连接数不一致原因,是因为我比较懒,随便截的图。所以你们自己测试时,添加一个客户端连接就会多一个socket。
然后我们使用命令打开tcpdump抓包,查看连接过程
#监听服务端redis的网络数据包
tcpdump port 6379 and dst 192.168.140.185
可以看到 我们左侧 直接通过命令 ”nc 192.168.140.185 6379“连接到服务端,发送消息。你们自己去尝试时会发现,nc连接时,socket增加了,tcp连接也增加了。
什么是IO多路复用
好了,我们知道了客户端和服务端是怎么连接的,那么现在看看IO多路复用。IO多路复用有三个函数,select、poll、epoll我们看下这三个函数是什么?具体的可以在linux使用 ”man 2 epoll“查看。我们知道了很多客户端连接时,有很多的fd文件描述符。IO多路复用就是我把一堆的文件描述符给epoll函数,然后这个函数查找这些文件里有没有客户端写入数据,有的话,就把这些文件描述符返回。再循环的去读取数据。
回到我们redis IO多路复用,我很多的客户端连接redis服务端,有很多的文件描述符,我调用epoll函数,得到那些有数据的文件描述符。然后单线程循环这些文件描述符,根据文件描述符读出客户端的指令,单线程进行执行。因为redis操作内存而且数据结构设计很合理,所以每行指令执行速度很快,cpu对于性能影响不大,所以单线程也很高性能。
下面是一段这个过程的伪代码,当然实际的代码和这个差别很大,为了便于理解这个过程,整体大概流程如下,实际流程跟这个还是有区别的。
#得到有数据的文件描述符
var fds=epoll(socketfd1,socketfd2,socketfd3.......socketfd1024);
#循环文件描述符
for(int i=0;i<fds.count;i++)
{
var commond=fds[i].read(); #读出指令 set name zhangsan
#执行指令
command.excute();
}
总结
本篇文章写的很粗糙,这个确实最近太忙了,所以懒得写东西,但是整体思路是这样的,可以按照上述命令和方法进行尝试