什么是C10K问题
C10K 就是 Client 10000 问题,即“在同时连接到服务器的客户端数量超过 10000 个的环境中,即便硬件性能足够, 依然无法正常提供服务。
其实说白了就是并发请求1W个请求 同时进行连接服务端,服务端可以支撑服务。带宽和内存如果够用的话,那么最后的瓶颈其实就在IO模型上。如果说为每个连接都创建一个线程进行处理,那么线程的切换,内存占用都会是瓶颈。
一要么就是一个线程同时处理多个任务,二使用更少的资源进行处理并发请求。
操作系统层面
文件句柄:每创建一个连接,都会使用一个文件描述符,当不够用的时候,就会出现 Socket/File:Can’t open so many files
而默认一个进程可以创建1024个,
我们可以进行设置
fs.file-max = 10000
net.ipv4.ip_conntrack_max = 10000
net.ipv4.netfilter.ip_conntrack_max = 10000
系统内存
每个TCP连接也会使用发送缓冲区和接收缓冲区,分别是最小,默认,最大值。如果1W个连接就是 156MB的发送缓冲区。 1.2G 接收缓冲区。以目前的硬件内存资源 也可以支撑。
root@qxlxi:/data# cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
root@qxlxi:/data# cat /proc/sys/net/ipv4/tcp_rmem
4096 131072 6291456
网络带宽
一个连接传输1KB ,大约1W连接,需要80Mbps 也可以
因为当创建多个线程的时候,线程之间数据的切换,用户态到内核态,是不足以支撑,所以问题的瓶颈就在于IO网络模型上
IO模型优化
IO事件通知方式 是用在套接字接口的文件描述符上的
-
水平触发:文件描述符可以非阻塞的执行IO,就触发通知,应用程序会随时查询文件描述符的状态,然后根据状态进行执行IO操作。
-
边缘触发:只有文件描述符的状态发生改变,才发送一次通知,应用程序多执行IO,只有无法读写的时候,才停止。
阻塞IO+进程
每个连接通过fork派生一个子进程进行处理,单独的子进程负责所有连接所有IO。效率不高,拓展性差。资源占用率高。
do {
accept connections
fork for conneced connection fd
process_run(fd)
}
缺点是进程模型占用资源太大。
阻塞IO+线程
可以使用轻量级的线程进行处理,每个连接就创建一个线程处理。为了提升效率 可以使用线程池。
do{
accept connections pthread_create
for conneced connection fd
thread_run(fd)
} while(true)
但是因为这样存在多个线程之间在发生IO时,在用户态和内核态之间进行频繁切换,以及数据拷贝,比较耗费资源。并且IO时线程需要进行阻塞等待,当IO执行完毕后,需要切换回执行线程。
非阻塞 I/O + readiness notification + 单线程
应用程序可以采用轮询的方式进行对创建的套接字进行询问,找出需要进行IO操作的套接字。但是当创建的连接过多的时候,其实需要O(N)的时间进行才能遍历到。CPU消耗比较大。
for fd in fdset{
if(is_readable(fd) == true) {
handle_read(fd)
}else if(is_writeable(fd)==true) {
handle_write(fd)
}
}
上述的方式比较耗费CPU,并且需要O(N)的遍历。并且需要应用程序主动去判断那些IO可以操作。应该由操作系统来通知我们套接字可以读。这种是select、poll这样IO分发技术。并且需要把文件描述符集合从用户空间传入内核空间,内核修改后,在传输用户空间,也增加了成本。
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)
虽然上述的方式可以,但是每次需要对注册的套接字进行排查,如果只对有IO操作的套接字进行处理,效率可以大大提升。
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 + 多线程
这种方式就是IO多路复用,其实就是一个主线程进行分发任务,然后子线程进行任务的处理。
小结
本篇简要介绍了C10K问题,本质是操作系统处理大并发请求的问题,对于客户端创建的连接到达1W个连接时,服务进程、线程就会导致数据拷贝频繁,缓存IO、内核将数据拷贝到用户进程空间、阻塞,进程/线程上下文切换开销大,从而导致资源耗尽,而导致程序崩溃。而最终其实是通过多路复用技术来解决的。