网络编程:C10K问题
C10K问题
C10K问题就是如何一台物理机上同时服务10000个用户?C代表并发,10K就是10000
C10K 问题是由一个叫 Dan Kegel 的工程师提出并总结归纳的,你可以通过访问http://www.kegel.com/c10k.html获取最新相关信息
操作系统层面
C10K问题本质是一个操作系统给的问题,要在一台主机上同时支持1万个连接,意味着什么?需要考虑哪些方面?
文件句柄
每个客户连接就代表着一个文件描述符,一旦文件描述符不够用,新的连接就会被放弃。
在Linux中,单个进程打开的文件句柄数是有限制的,默认都是1024,可通过ulimit -n
查看
但这个连接上线可通过修改/etc/sysctl.conf文件,使得系统可以支持10000个描述符上限。
fs.file-max = 10000
net.ipv4.ip_conntrack_max = 10000
net.ipv4.netfilter.ip_conntrack_max = 10000
系统内存
每个TCP连接都需要占用一定的发送缓冲区和接受缓冲器等内存资源。
在Linux 4.4.0下发送缓冲区和接受缓冲区的值。
$cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
$ cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 6291456
分别代表着最小分配值、默认分配值和最大分配值,按照默认分配值计算,一万个连接需要的内存消耗就是:
发送缓冲区: 16384*10000 = 160M bytes
接收缓冲区: 87380*10000 = 880M bytes
应用程序也需要一定的缓冲区来进行数据的收发,为方便,假设每个连接需要128K的缓冲区,那么1万个连接就需要1.2G的应用层缓冲。
网络宽带
假设 1 万个连接,每个连接每秒传输大约 1KB 的数据,那么带宽需要 10000 x 1KB/s x8 = 80Mbps。这在今天的动辄万兆网卡的时代简直小菜一碟
C10K问题解决之道
要想解决 C10K 问题,就需要从两个层面上来统筹考虑。
- 第一个层面,应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作?
- 第二个层面,应用程序如何分配进程、线程资源来服务上万个连接?
基于上面两个层次的组合形成了解决C10K问题的几种解决方案。
阻塞I/O + 进程
每个连接通过 fork 派生一个子进程进行处理,因为一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响。
这个方法虽然简单,但是效率不高,扩展性差,资源占用率高。
伪代码描述了使用阻塞 I/O,为每个连接 fork 一个进程的做法:
do{
accept connections
fork for conneced connection fd
process_run(fd)
}
阻塞I/O + 线程
通过为每个连接调用 pthread_create 创建一个单独的线程,也可以达到上面使用进程的效果。
do{
accept connections
pthread_create for conneced connection fd
thread_run(fd)
}while(true)
因为线程的创建是比较消耗资源的,况且不是每个连接在每个时刻都需要服务,因此可预先通过创建一个线程池,并在多个连接中复用线程池来获得某种效率的提升。
create thread pool
do{
accept connections
get connection fd
push_queue(fd)
}while(true)
非阻塞I/O + readiness notification + 单线程
应用程序其实可以采取轮询的方式来对保存的套接字集合进行挨个询问,从而找出需要进行 I/O 处理的套接字,像给出的伪码一样,其中 is_readble 和 is_writeable 可以通过对套接字调用 read 或 write 操作来判断。
for fd in fdset{
if(is_readable(fd) == true){
handle_read(fd)
}else if(is_writeable(fd)==true){
handle_write(fd)
}
}
操作系统来告诉我们哪个套接字可以读,哪个套接字可以写,采用select、poll这样的I/O分发技术。
do {
poller.dispatch()
for fd in registered_fdset{
if(is_readable(fd) == true){
handle_read(fd)
}else if(is_writeable(fd)==true){
handle_write(fd)
}
}while(ture)
epoll设计如下:
do {
poller.dispatch()
for fd_event in active_event_set{
if(is_readable_event(fd_event) == true){
handle_read(fd_event)
}else if(is_writeable_event(fd_event)==true){
handle_write(fd_event)
}
}while(ture)
非阻塞I/O + readiness notification + 多线程
把线程引入进来,可以利用现代 CPU 多核的能力,让每个核都可以作为一个 I/O 分发器进行 I/O 事件的分发。这就是所谓的主从 reactor 模式
。
基于 epoll/poll/select 的 I/O 事件分发器可以叫做 reactor,也可以叫做事件驱动,或者事件轮询(eventloop)。
异步I/O + 多线程
异步非阻塞 I/O 模型是一种更为高效的方式,当调用结束之后,请求立即返回,由操作系统后台完成对应的操作,当最终操作完成,就会产生一个信号,或者执行一个回调函数来完成 I/O 处理。
涉及到了Linux下的aio机制。