如何选择TCP还是UDP?
什么时候选择TCP,什么时候选择UDP,什么时候采用多连接,什么时候采用混合连接?无论是经典的Unix网络编程,还是TCP/IP协议卷1-3,都只是笼统的描述了一下大概如何选择,且不说对与错,但是今天从我的角度看,这几个经典的著作里,其实都是从协议本身特性来指导大家如何选择,却没有考虑到TCP协议栈本身实现存在的各种局限,网络上有很多文章,往往是从CPU,缓冲,多队列网卡等硬件层,加上 Linux下的epoll,windows下的完成端口等系统调用,来侃侃而谈如何优化web服务之类,什么从1万连接到100万,说句老实话,真想做优化,别的不说,光是选择的层级我就觉得有问题,因为从1万到100万,造成性能瓶颈的主要方面除了上下文切换导致的开销,其实最大的问题出在TCP/IP协议栈的实现上,不去研究优化TCP/IP协议栈的影响,却抓着最底层的硬件和操作系统的系统调用,无论如何我是不敢苟同,你把原来的p4 2.0g CPU换成新一点的如xeon e1230 平台, 内存从ddr 400 2g升级到16g ddr3-1600,肯定能对网络的并发连接能力提升一个等级,因为大把的钱投进去了,总能听到个响。下面是我个人的一点基本总结。
#
TCP和UDP的区别?简单来说,TCP是有序可靠传输,而UDP是不可靠乱序传输,但是实际上你把UDP看做IP可能更准确一点,因为可以在UDP基础上开发出可靠UDP等各种传输。
#
TCP的优缺点是什么?优点:这是一个国际通行了很多年的标准实现,调用简单,省心,很多应用都基于TCP进行实现的,最关键的是,它的兼容性是跨平台的,也就是,只要你选择的是TCP,那么不管是windows还是linux,unix,只要支持TCP/IP,那么就可以保证实现可靠连接和传输。作为软件的开发者只需要考虑应用层。至于可靠传输的性能,由于是国际标准,在内核层实现和运行,很多都已经做了硬件级的优化。
缺点:
第一是每个TCP连接都是1对1连接,这意味着每个连接都需要一个套接字socket,并且需要随时监听是否有数据传输。当连接数量达到一定的程度,性能会直线下降。刚开始接触的朋友可能很难想像,就一个基本没有数据的空连接怎么会消耗系统的性能呢?其实这个问题,不可以单纯从硬件或者API接口等进行解释,而需要从TCP/IP协议栈的实现中去发现的,从可以查看到的代码,无一列外,在操作系统里使用的是指针链表进行管理。
在执行插入和写操作的时候,需要执行rcu_writelock,这和readlock不同,能且只有一个锁定,而无法多重锁定,性能在这里遇到了瓶颈,这个瓶颈出现在TCP/IP协议栈的实现中,由于TCP包的构成问题,至少到目前,或者可以遇见的将来,TCP这个严重影响并发性能的问题会一直存在在实现中,这是为什么TCP在面对海量并发的时候,在超过一定数字后性能出现直线下降的其中一个关键原因。
在执行遍历时使用独占的指针遍历模式,一万空连接在目前的CPU前,可能没什么,但是如果是并发模式n万小包呢,那开销n/2。。。,你再多的CPU核心也没用,一个链表只有一个独占模式的遍历操作,其他CPU只能干着急。所以在面对小数据量海量长时间连接的应用时,选择TCP必须要慎重再慎重。
第二个大问题是TCP是双向流传输,而keepalive这个功能并非普遍支持,双向流传输意味着,数据是采用流模式过来的,如何切割取决于协议的制定者,例如普遍的采用\r\n作为分割字符,而keepalive的非普遍支持,则导致另外一个问题,那就是每个连接,必须每过一定时间在应用协议层检测连接是否还存活,最典型的就是FTP,如果没有指令操作,那么客户端每过一定时间必须发送一条指令,通常是noop指令给服务器端,告诉服务器,我还保持着连接,不要中断。FTP等协议中引入noop等类似机制的原因是,当TCP建立连接后,如果双方没有应用层的数据传输,那么当物理中断发生的时候,等待的一方是接收不到发生故障的一方的任何消息的,直到没有发生故障的一方,主动发送数据给另一方,出现发送超时的时候,才能给出中断判断,否则就是个死等待。流传输的另一个问题是,无法实现数据的并发传输,而只等排队发送,这在很多应用,特别是游戏类应用是严重的缺陷,无论你有多着急,一个连接就是一个流,你要排队先发送到缓冲,然后由系统负责发送缓冲数据。
#
UDP的优缺点是什么?优点:
UDP是比TCP更接近IP的协议,通常UDP是不可靠传输,但是我们可以在应用层对UDP加上校验和序列号,做成可靠传输。
- 为什么UDP的性能比TCP好, 其实原因很简单,UDP是发射后不管,不需要对方发送ack包进行确认,TCP由于需要对每一个包进行ack确认,一来一回,就会影响到传输效率,但事实上,这个影响是很小的,如果不考虑丢包和线路不稳定等,这个差距一般只有百分只几,除非你做极限测试。 但实际上,真正用到UDP高效传输的场合是非常少的,一个关键的原因在于它的不可靠性,特别是在互联网上,遇到网关路由高负载的时候,优先扔掉UDP包。我个人认为,UDP最大的优点在于它的可塑性非常强,我们可以通过各种机制来改造UDP,例如实现可靠传输,实现1对多传输,实现包和流模式同时传输,优先发送,多路双向传输等等。同样,通过扩展UDP来实现可靠传输,我们可以避开TCP/IP协议栈实现中指针链表查询导致的性能急剧下降,在应对海量连接方面,UDP可靠传输能支持的用户数量远超TCP,因为UDP不需要那种大规模的链表查询,是个队列操作。
缺点:最要命的就是不可靠传输,虽然我们可以通过加入各种机制和扩展,把UDP改造成可靠传输,但是由于这个实现是在应用层,因此在面对少量用户大流量传输的时候,极限输出不如TCP。
#
如何选择TCP还是UDP?先看下人家怎么选
- HTTP, HTTP协议现在已经深入影响到我们的方方面面,重要性不容置疑,它采用的是TCP协议,为什么使用TCP,因为它传输的内容是不可以出现丢失,乱序等各种错误的,其次它需要跨平台实现,而TCP满足了这个要求,发展到今天,HTTP享受了TCP带来的简洁高效和跨平台,但是也承受了TCP的各种缺点,例如缺少TCP keepalive机制[这个其实是后来添加的支持,并非普遍实现],TCP协议栈的实现问题引发的难以支持海量用户并发连接[只能通过dns等级别的集群或者cdn来实现],协议太复杂导致很难模块化处理[其实这个问题已经在nginx解决了,nginx通过模块化和对协议的分段处理机制,并引入消息机制,避免了多进程[线程]的频繁切换,相比apache等老牌web服务器软件,在应对大量用户上拥有极大的优势。即使站在今天的角度看,HTTP也确实应该选择TCP。
- FTP,这个协议比HTTP更加古老,它采用的也是TCP协议,因为它的每一个指令,或者文件传输的数据流,都需要保证可靠性,同时要求在各种平台上广泛支持,那么就只能选择TCP,和HTTP不同,它采用了noop指令机制来处理TCP缺少keepalive机制带来的问题,也就是客户端必须每过一段时间,如果没有发送其他指令,就必须发送一个noop指令给服务器,避免被服务器认为是死连接。Ftp的缺陷在哪里呢?,其次它的文件传输是采用新的数据连接来执行,等于1个用户需要2个连接,其次当一个文件正在传输的时候,你无法进行其他操作,例如列表,也许你可以把它当作是一一对应的典范,因为这样我们可以直接用命令行进行控制,但是很多用户其实是需要在下载的时候同时进行列表等操作的,为了解决这个问题,很多客户端只要开启多个指令连接[和数据连接],这样一来,无形中额外带给了Ftp服务器很多压力,而采用UDP可靠传输就不存在这个问题,但是UDP可靠传输是没有跨平台支持的,这样是鱼和熊掌不可兼得,对于这样一个简单的开放协议的实现,TCP是个好选择。
- POP3/SMTP,常见的邮件协议,没什么好说的,反应--应答模式,跨平台要求,因此TCP是个选择,这个协议的悲剧在于,当初没有考虑到邮件附件会越来越大的问题,因此它的实现中将附件文件采用了base64编码格式,用文本模式进行发送,导致产生了大量的额外流量。
- TFTP,这是一个非常古老的用于内部传输小文件的协议,没有FTP那么多功能,采用的是UDP协议,通过在包中加入包头信息,用尽可能简单的代码来实现小文件传输,注意是小文件,是一个值得参考的UDP改造应用范例。
- 通常的VOIP,实时视频流等,通常会采用UDP协议,这是以内这些应用可以允许丢包,很多人可能认为是UDP的高效率所以才在这方面有广泛应用,这我不敢苟同,我个人认为,之所以采用UDP,是因为这些传输允许丢包,这是一个最大的前提
归纳一下
1.如果数据要求完整,不允许任何错误发生
1.1应用层协议开放模式 [ 例如 HTTPftp ]
建议选择TCP,几乎是唯一选择。
1.2应用曾协议封闭模式[例如游戏]
1.2.1大量连接
1.2.1.1长连接
少量数据传输
优先考虑可靠UDP传输,TCP建议在20000连接以下使用。
大流量数据传输
只有在10000连接以下可以考虑TCP,其他情况优先使用UDP可靠传输
1.2.1.2短连接
少量数据传输
建议使用UDP标准模式,加入序列号,如果连接上限不超2万,可以考虑TCP
大流量数据传输
10000连接以下考虑TCP,其他情况使用UDP可靠传输
在遇到海量连接的情况下,建议优先考虑UDP可靠传输,使用TCP,由于TCP/IP栈的链表实现的影响,连接越多,性能下降越快,而UDP可以实现队列,是一条平滑的直线,几乎没有性能影响。
1.2.2有限连接[通常小于2000,一般每服务器为几百到1000左右]
- 1.2.2.1长连接
除非有数据的实时性要求,优先考虑TCP,否则使用UDP可靠传输。
1.2.2.2短连接
优先考虑TCP。
在有限连接的情况下,使用TCP可以减少代码的复杂性,增加广泛的移植性,并且不需要考虑性能问题。
2.允许丢包,甚至可以乱序
- 2.1对实时性要求比较高,例如VOIP,那么UDP是最优选择。
- 2.2部分数据允许丢包,部分数据要求完整,部分有实时性要求,通常这样的需求是出现在游戏里,这时候,基于UDP协议改造的UDP多路可靠传输[同时支持不可靠模式],基本是唯一能满足的,当然也可以使用TCP来传输要求完整的数据,但是通常不建议,因为同时使用TCP和UDP传输数据,会导致代码臃肿复杂度增加,UDP可靠传输完全可以替代TCP。
- 2.3部分数据优先传输,部分可丢弃数据在规定时效内传输,这通常是实时视频流,在有限连接模式下,可以考虑TCP+UDP,但是通常,可靠UDP传输是最好的选择。
目前的协议实现
目前TCP的keepalive在几乎所有的协议栈下实现,其中主要有三个主要的选项:
- 空闲时长(tcp_keepalive_time):默认7200秒,多长时间内没有数据流动触发keepalive数据包发送
- 探测间隔(tcp_keepalive_intvl):默认75秒,在未收到ack的情况下keepalive包的发送间隔
- 最大重试次数(tcp_keepalive_probes):默认9次,在连续9次未收到ack则关闭链接
在Linux内核设置查看当前的配置:
变更配置vim /etc/sysctl.conf
#
协议栈调优#
1. fs.file-max最大可以打开的文件描述符数量,注意是整个系统。
在服务器中,我们知道每创建一个连接,系统就会打开一个文件描述符,所以,文件描述符打开的最大数量也决定了我们的最大连接数
select在高并发情况下被取代的原因也是文件描述符打开的最大值,虽然它可以修改但一般不建议这么做,详情可见unp select部分。
#
2.net.ipv4.tcp_max_syn_backlogTcp syn队列的最大长度,在进行系统调用connect时会发生Tcp的三次握手,server内核会为Tcp维护两个队列,Syn队列和Accept队列,Syn队列是指存放完成第一次握手的连接,Accept队列是存放完成整个Tcp三次握手的连接,修改net.ipv4.tcp_max_syn_backlog使之增大可以接受更多的网络连接。
注意此参数过大可能遭遇到Syn flood攻击,即对方发送多个Syn报文端填充满Syn队列,使server无法继续接受其他连接
#
3.net.ipv4.tcp_syncookies修改此参数可以有效的防范上面所说的syn flood攻击
原理:在Tcp服务器收到Tcp Syn包并返回Tcp Syn+ack包时,不专门分配一个数据区,而是根据这个Syn包计算出一个cookie值。在收到Tcp ack包时,Tcp服务器在根据那个cookie值检查这个Tcp ack包的合法性。如果合法,再分配专门的数据区进行处理未来的TCP连接。
默认为0,1表示开启
#
4.net.ipv4.tcp_keepalive_timeTcp keepalive心跳包机制,用于检测连接是否已断开,我们可以修改默认时间来间断心跳包发送的频率。
keepalive一般是服务器对客户端进行发送查看客户端是否在线,因为服务器为客户端分配一定的资源,但是Tcp 的keepalive机制很有争议,因为它们可耗费一定的带宽。
Tcp keepalive详情见Tcp/ip详解卷1 第23章
#
5.net.ipv4.tcp_tw_reuse我的上一篇文章中写到了time_wait状态,大量处于time_wait状态是很浪费资源的,它们占用server的描述符等。
修改此参数,允许重用处于time_wait的socket。
默认为0,1表示开启
#
6.net.ipv4.tcp_tw_recycle也是针对time_wait状态的,该参数表示快速回收处于time_wait的socket。
默认为0,1表示开启
#
7.net.ipv4.tcp_fin_timeout修改time_wait状的存在时间,默认的2MSL
注意:time_wait存在且生存时间为2MSL是有原因的,见我上一篇博客为什么会有time_wait状态的存在,所以修改它有一定的风险,还是根据具体的情况来分析。
#
8.net.ipv4.tcp_max_tw_buckets所允许存在time_wait状态的最大数值,超过则立刻被清楚并且警告。
#
9.net.ipv4.ip_local_port_range表示对外连接的端口范围。
#
10.somaxconn前面说了Syn队列的最大长度限制,somaxconn参数决定Accept队列长度,在listen函数调用时backlog参数即决定Accept队列的长度,该参数太小也会限制最大并发连接数,因为同一时间完成3次握手的连接数量太小,server处理连接速度也就越慢。服务器端调用accept函数实际上就是从已连接Accept队列中取走完成三次握手的连接。
Accept队列和Syn队列是listen函数完成创建维护的。
/proc/sys/net/core/somaxconn修改
#
总结TCP/IP协议很伟大,在这些基础上诞生了很多划时代的应用,但是时代在发展,需求也在改变,几十年前诞生的基础协议,也遇到了各种问题,典型的是32位地址编码问题,虽然通过NAT等技术尽可能的支持更多的机器接入,由于TCP/IP协议的巨大影响以及现实上的垄断,导致后续的更新必须考虑到完全兼容,然后IPv6出现了,它继承了IPv4的几乎所有优点和缺点,只为了一个目的,兼容。我们可以拥有更快的CPU,内存,更强大的TCP/IP系统调用API,但是比较遗憾,TCP/IP协议栈的实现,我们始终无法绕开指针链表,而正是这,导致了TCP模式在面对海量连接的时候,超过一定数量,网络io性能直线下降,许许多多的工程师始终认为是CPU内存不够导致,却没有想到是TCP协议栈的实现上存在性能瓶颈。在目前的情况下,也只有UDP能避开这个协议栈的性能瓶颈,为什么?因为UDP采用的是1对多的虚拟连接,例如,当通过UDP构建的连接数量是1万个的时候,实际上在协议栈增加的单元只有1个,而同样1万个连接,TCP增加的单元是1万个,每个片的到达平均要查询5千次,而可靠UDP采用队列模式,查询次数是1,因此,在今天,如果你希望你的每台服务器能支持更多的连接,除非你的应用协议需要开放或者兼容其他应用,否则尽可能考虑采用UDP,而不是TCP。