2008年3月28日
#
先说说TCP收包的context(不定长包)。一般情况,发送方发送一个包,然后接收方收到一个包,这是最好处理的。第二种情况,当每次发生的包比较小时,发送数据时,TCP会启用优化算法,将多个小包集中起来发送,以提高传输效率。此时接收方的recv buffer中,可能出现不止一个包。第三种情况,recv buffer中每次只一个包,但接收方没及时取包,这时recv buffer中会积累多个包。
理所当然,TCP收包要考虑所有这些情况。一般来说有三种方法。第一种,定义好通讯协议,先收包头,然后根据包头中的消息真实大小,接收消息剩余部分。第二种方法,通讯协议规定好每个消息的开始和结束标识符。然后每次recv得到的数据先放到一个大(比如你的最大packet的2倍)buffer中,最后再来分析这个buffer分包。第三种方法,先用recv+MSG_PEEK接收某个固定长度,然后对接收到的"包"进行分析,然后做真正的recv操作。
2008年3月24日
#
能不能接受爆发连接(并发度如何),主要是取决于accept的速度。一个TCP连接的建立,要在client和server之间,完成三次握手,然后连接会被放到完成队列中,accept从完成队列中取出连接并返回。任何影响accept取连接的因素,都会影响并发度。一般策略是,1 独立处理accept,2 使用epoll处理accept,这两种情况,并发度都是不错的。
并发地接受了这么多连接,并不代表能完全处理。假如有很多连接同时在线,server accept成功并收到了数据,这时消息被放到消息队列中,等待逻辑线程来处理。因为生产者(收数据)的速度总是大于消费者(处理数据)的速度,因此消息队列会有考虑流控,以免系统资源被耗光。这样,有些消息就可能丢失。这就是同时在线连接数的问题。
前者是高并发,后者是高负载。设计时会权衡偏向。
并发服务器有三种常见的架构:
1. 单线程epoll(ET非阻塞I/O) +线程池,也叫半同步半异步模式。这种模型比较常见,而且因为异步层和同步层使用消息队列传递消息,更容易实现对消息的FIFO处理。缺点就是线程的同步和上下文切换开销比较大。
2. ConnectionPerThread+阻塞型I/O。这是最古老的服务器并发模型,不太适合现今的这些高并发高负载服务器,当连接数巨大的时候,创建销毁线程的开销会无法承受,并且内核创建线程的速度也会成为瓶颈。这种模型的一种改进型就是领导者/跟随者模式,它吸取第一种模型中,线程数量不会膨胀的优点,使用线程池来处理连接。每当有连接到达时,都使用一个线程阻塞处理,处理完成后,线程再回到线程池中,这样有限的线程模拟出了ConnectionPerThread。一般来说,领导者/跟随者模型比第一种模型更加高效,因为它减少了线程同步和切换的开销,它的缺点就是FIFO很难保证。
3. 流水线模型。前面两种模式都有个缺点,它们不能花太长时间处理逻辑,因为在多CPU系统上,某些耗时的长请求可能会不断占住CPU,而导致短请求得不到处理,这会使某些CPU闲置。于是这种模型提出,将请求处理的过程划分步骤,不同的步骤考虑不同的资源处理(比如CPU, DISK I/O等),每个步骤使用单独的线程或线程池。这样比较耗时的操作可能集中在流水线的下级,而短请求也可以在上级得到快速处理。因为各级线程之间使用消息队列传递请求,也很容易实现FIFO。
如何高效处理多个socket I/O的读写,是提高服务器性能的重点问题。unix-like下面,现有机制有select,poll, epoll,kqueue,/dev/poll两大类。
Select有个缺点,它用fd_set管理所有要监视的I/O句柄,但是fd_set是一个位数组,只能接受句柄号小于FD_SETSIZE(默认1024)的句柄,虽然进程默认句柄号都是小于1024的,但是可以通过ulimit –n来修改,尤其是连接数超过1024时必需这么做(实际可能更少),如果要将大于1024的句柄放入fd_set,就可能发生数组越界程序崩溃的场面。
Poll虽然解决了FD_SETSIZE问题,但是它和select一样,都有性能上的瓶颈。它们都会随着连接数的增加性能直线下降。这主要有两个原因,其一是每次select/poll操作,kernel都会建立一个当前线程关心的事件列表,并让线程阻塞在这个列表上,这是很耗时的操作。其二是每次select/poll返回后,线程都要扫描所有句柄来dispatch已发生的事件,这也是很耗时的。当连接数巨大时,这种消耗积累起来,就很受不了。
为了解决select/poll的性能问题,unix-like系统上开发出了三套新的利器epoll,kqueue,/dev/poll,其中epoll是linux的,kqueue是freebsd的,/dev/poll是Solaris上的,它们是select/poll的替代品。它们的设计就是针对select/poll的性能问题,主要避免 1。每次调用都建立事件等待列表,取而代之建立长期的事件关注列表,这个列表可通过句柄(比如epfd)来增加和删除事件。2。调用返回之后,不再需要遍历所有句柄进行分发,内核会直接返回当前已发生的事件。不用说,性能在select, poll基础上有了大幅提升。
要注意的是,凡是使用readiness notification(LT)或者readiness change notification(ET)机制,都应该配合非阻塞I/O,因为这种事件通知,并不一定表示文件描述符真正就绪,如果收到通知之后去read,很有可能进入阻塞状态,这会严重影响服务器的并发性能,同时对ET模式,不能漏掉任何事件的处理,并且每次都应该读到socket返回EWOULDBLOCK为止,不然这个socket之后会永远保持沉默。