C10K
的问题不知道现在还有多少人还记得?Dan Kegel
在01年左右在个人博客上面拿来探讨的话题,指的是在当前的机器设备情况下能不能单机扛得住10K的用户同时访问(现在单机300W并发都没得问题了,科技发展的快速吧!)。所以这里聊聊网络从阻塞一路走来...
1. 同步IO走过蛮荒时期
阻塞模式
可能是学习网络的绝佳方案。早些年异步IO
的基础技术还不完善的时候阻塞IO
在那个年代是主要的网络通信方式。
一个线程一个Connection的模式成为了程序员主要使用方式:每次Accept
到一个新的Connection
的时候就启动一个线程来用专门对这个链接做读取写入操作。这种方式极大的制约了系统能够处理链接的数量。(试想下如果有10w个终端同时向一台服务器发起请求,那么服务器一次性创建10w个线程,系统必然扛不住)
腾讯的解决方案:在这个时期使用TCP的阻塞多线程
模型想要构建超大规避的高并发系统几乎是不可能。同时期暴发的腾讯科技反其道行之,利用UDP构建了类似TCP的可靠传输(用户态模拟了TCP协议)。这样可以通过UDP模拟的并发系统并不需要起对应的线程,从而扛起了腾讯当时的流量(实现UDP的可靠传输并不简单,需要丢包重传流量拥塞控制等等)。
由于腾讯使用了UDP作为了QQ的通讯手段,导致很多教科书不明缘由。大肆忽悠鼓吹UDP适合IM通讯的场景,实际上当时是有历史原因的。而且腾讯现在已经换TCP/HTTP作为主要通讯方式了
2. 异步或者半异步带来真正的高并发
经历了阵痛的蛮荒期,终于需要支持异步IO系统的接口被提上日程。以Posix-Select
Windows IOCP
Linux EPoll
Mac KQueue
为代表的异步IO接口终于稳定的面向了大众开发者,也标志着C10K
甚至C100K
的问题(实际上现在单机C300k
都OK了)终于真正解决了。nginx
就是基于异步IO的方法,所以在性能上面完爆Tomcat
。
那么这些异步IO接口解决了哪些问题? 主要解决了通讯状态的的管理。举个例子:在同步IO里面之所以需要一个连接一个线程,是因为每次写入读取都是阻塞的。比如当前发起了对连接的读操作,如果没有数据达到,操作系统会阻塞到直到有可读数据或者对端主动断开为止。如果一个线程同时管理2个或者2个以上的连接,对其中一个读写阻塞会导致影响其他链接的读写操作。那么异步IO
在的读写过程中并不是阻塞的(比如当前数据读不到的情况下不会卡主线程,而是返回错误码),同样系统提供一组接口可以帮助程序管理大量(百万千万上亿级别)的链接(当这些链接中出现了一个或者多个链接可读可写主动通知程序去处理)。
以Linux的EPoll为例:首先明白2个关键知识点,系统的网络接口发送Send
和读取Write
都不是表示把数据发送出去了、或者接受到刚刚对方发来的数据。Send
函数成功只是表明用户态
需要发送的数据成功拷贝到了内核的发送缓冲中,Write
指的是从接受缓冲中读取到了数据。Linux在构建链接的时候会顺带构建2个缓冲区,一个是发送缓冲
一个是接受缓冲
。当发送缓冲
用空余的空间出来就表明了当前可以'发送'了,当接受缓冲
有数据的时候就表明当前链接的状态是可读的。EPoll底层使用红黑树
配合双向列表
来维护Socket
和数据状态,所以性能非常高。比如网卡收到对端发送来的数据后,系统内核就把数据从网卡中读到内核内存空间,并把当前的套接字
句柄添加到读就绪列表
中,整系统通过类似的方式高效的运行着。
系统异步的接口(由于Epoll的接口只是通知程序可读/可写
,具体的读写操作还是要程序主动完成,所以也有人说EPoll不是完全的异步操作
)让我们可以使用一个线程(或者少量的线程)就可以维护数以万计的Socket
状态管理。TCP终于可以单机可以和数百万的客户端同时通讯了。
到这里问个简单的问题(敲黑板@@@@!)? 历史上TCP和UDP哪个出现的比较早? 可能很多人觉得TCP是稳定可靠的一定比UDP这种不可靠通讯方式出现来的晚吧。实际上UDP是在TCP出现之后才出现的,UDP之初一定程度就是为了弥补TCP的不足而诞生的。
那么使用UDP的场景是不是很少了? 确实!大部分能用UDP的场景都可以使用TCP代替,除了部分历史原因。比如DNS就是选的UDP实现的,现在如果想改TCP基本不可能,而且也没有必要。但是TCP存在一些性能问题,比如:TCP每次链接都需要三次握手
,结束需要4次挥手。TCP在运行过程中会产生更多的SysCall
(系统调用,每次调用都要自陷入内核,这个耗时就大了)。如果能够在用户态实现可靠传输协议(包括流量控制
丢包重传
等等特性),能够优化上面的TCP协议存在问题,说白了TCP协议本身设计的还是有冗余的。尤其在局域网环境(服务器基本都是搭建在局域网里面)这种网络环境可控的情况下可以极大提升网络性能,我简单试过基于UDP的内网的传输方式比TCP的的通道建立时间要短很多,数据收发速度也有很大的提升(5倍以上)。基于这种方式的组件的分布式架构网络通讯带来的数据延迟可以降到更低,分布式系统的性能会更好。
3. 更加极端的DPDK
传统的网络从网卡走到用户态处理数据经过了太多的数据处理过程:
DPDK的处理流程:
DPDK直接把网卡数据传递给了用户态,减少了大量的内核的处理逻辑。所以DPDK的处理性能比传统的网络处理性能更高。可是用户态拿到数据是物理帧(没有经过协议处理的原始数据)需要用户态模拟出整个协议栈。其次由于放弃了系统原生的中断处理方式,所以需要独立一个线程不停的LOOP去收发数据。
所以DPDK天生就是为高并发而生的,比如反向代理
和负载均衡器
就可以采用DPDK处理。
4. 协程
基于异步IO已经可以解决高并发的IO的问题了,但是异步IO
需要配合Callback
:也就是每次收到数据回调对应处理函数,但是并不是每次回调都是完整的Package(TCP是流可能在任何位置被分包),所以我们需要在回调函数里面判别是不是收到一个完整的逻辑包。如果收到了就做对应的逻辑处理,如果数据还不够就暂时先不出处理。所以这样处理程序是碎片化的,碎片化最大的问题程序可读性被打断了。并没有同步IO的那种流畅的可读性。协程
就在这个情况下孕育而生了,每个Connection
被放置在一个协程
中,调度程序根据当前是否可读
还是可写
来调度不同的协程
执行不同的逻辑。例如:在一个协程
里面执行了读操作,但是当前Connection
是不可以读的。协程调度器就会把当前执行的协程
切换下来,一直到Connection
可读了才继续被调度。整个过程和多线程调度类似,只是协程
在用户态更加轻量。现在系统同时并行运行和调度百万个协程
没有压力。
所以协程只是优化了异步IO的交互体验,有保证了异步IO的高并发性。
THE - END
版权所有,如有转载请联系我本人http://www.breakerror.com/archives/101-i.html