今天来给大家分享下关于 CDN 的东西,以及我自己的一些发现、一些个人的拙见。总共分为 3 个部分:原理、详解、各种坑。
首先说一下 CDN 的基本原理部分,主要分 4 块来描述:CDN 的由来、调度是怎么做的、缓存是什么、关于安全。
最初刚有互联网的时候,带宽用量不多、用户少,并不存在什么问题,后来随着发展,逐渐出现了使用量大、访问缓慢的情况。最初 95 年的时候,有两个博士试图通过利用数学的办法来解决动态路由问题,且效果还不错,这便是 Akamai 的前身,也是全球第一个CDN 公司。98 年中国成立了国内第一家 CDN 公司,蓝汛,ChinaCache,很荣幸我曾在这个公司任职,群里也有好多前蓝汛的同事们,也有很多现还在蓝汛的同事。
什么是 CDN?
这是一个做过 CDN 之后的拓扑图,里面有几个概念需要明确一下:
Origin Server: 源站,也就是做 CDN 之前的客户真正的服务器;
User: 访问者,也就是要访问网站的网民;
Edge Server: CDN 的服务器,不单只“边缘服务器”,这个之后细说;
s/\(单\)只/\1指/;
Last Mile: 最后一公里,也就是网民到他所访问到的 CDN 服务器之间的路径。
我们平时所使用的DNS服务器,一般称之为LDNS,在解析一个域名的时候,一般有两个情况,一种是域名在DNS上有记录,另一种情况是没有记录,两种情况的处理流程不一样。
当你访问163这个域名时,如果LDNS上有缓存记录,那它会直接将IP地址直接给你。如果没有缓存记录,它将会一步步向后面的服务器做请求,然后将所有数据进行汇总交给最终的客户。
当你访问163这个地址时,实际上如果本身没有内容的话,它要去后面拿数据,这个过程术语叫递归,它首先会向全球13个根域服务器请求,问com域名在哪,然后根域服务器作出回答,一步步往下,这个过程较复杂,如果大家感兴趣可去查相关资料,在这就不一一赘述。
DNS调度
肯定很多人好奇是如何进行调度和进行定位的?
其实也是通过LDNS的具体地址来进行的,比如,看图,假设你是一个广东电信客户,那你所使用的DNS服务器去做递归的时会访问到某一个CDN厂商的GRB,全球的一个调度系统,他就能看到来自于哪个LDNS。假设如果用户和LDNS使用同一个区域的服务器,他就会间接认为用户也是广东电信的。
再举个例子,比如说北京联通的用户,它使用DNS地址,一般自动给它分配的是北京联通的服务器,这个服务器去做递归的时候,调度服务器就会看到这个请求是来自北京联通的LDNS服务器,就会给它分配一个北京联通的服务器地址,然后让来自北京联通的用户直接访问北京联通的服务器地址,这样来实现精准的区域性调度。
从这个调度理论上看,我们可以发现一个问题,就是假设用户所使用的LDNS地址和你是同一个区域,那么这个时候我们的调度才有可能是正确的。但是举个例子来说,如果你是北京联通的用户,可是使用的是广东电信的LDNS的话,就会让GRB系统误以为你是广东电信的客户,这样就会错误的调度过去。
之前有一次我在小区里上网,由于我的路由器有问题,我设了202.106.0.20的北京联通的DNS服务器地址,后来出差去深圳,访问比较大的网站发现比较慢,经过分析,才发现原来我设的DNS地址是北京联通的,而我在广东和深圳使用的网络都是电信接入的,但是分配给我的是北京联通的地址,那我用电信的线路访问北京联通的地址,势必就会很慢。
因为刚才讲到的DNS调度机制存在一定问题,所以在某些场合下我们会使用第二种调度机制,叫HTTP的调度。
了解http协议的人知道,在http协议中有一个叫302跳转的功能,它的实现并不是说你访问一个URL,然后马上吐给你想要的数据,而是吐给你一个302返回信令,这个信令头部会告诉你,有一个location目标,这个location就是告诉你下一步将要怎么做,而具体调度是通过location来实现的。
即便我所使用的DNS和我不在一个区域,但当我访问http server的时,这个server是由CDN公司提供的。客户访问server的时,虽说通过DNS方式无法拿到客户的真正IP地址,但是如果你访问的是http server,他一定能直接看到客户的真实IP,利用这种方法可以进行调度的纠偏,可以直接返回给你一个302,然后location里面携带一个真正离你最近的CDN server。
这种调度方式,优势是准确,但是也存在弊端,它需要有一次TCP的三次握手建连,他不像DNS那样直接请求一个数据包过去给一个反馈就OK了,他需要一次TCP的三次握手建连。
第二个是你如何访问到http的服务器?如果你之前是通过DNS调度过去的,实际上前边的那个DNS也是省不了,在国内是没有办法做anycast的,也就是没有办法来直接访问一个众所周知的大的IP来进行,所以,一般情况下都是通过DNS来进行第一次调度,然后用http来进行第二次纠偏。这种情况下大家可以想象,如果你下载一个大文件,比如说电影,但你访问的是一个页面小元素,比如说这个图片只有几k,那么,实际上你调度的时间就已占用了很大的成分。实际上,这种302调度是一种磨刀不误砍柴工的方案,如果你后面有很多工作要做,比如要下载一个电影时间会很长,那你调度准确,即使花一点时间调度也是值得的。但是如果你后续访问一下就完了,那么你这样调度就没有太大意义。
除了DNS调度和http的302调度以外,其实还有一种调度方式,叫http DNS调度,它的原理是通过一个正常的http请求,发一个get的请求,然后再请求里面以参数的形式携带一个我要解析的域名,然后服务器那边去通过数据库查询,查询之后又通过http的正常响应,把这个你要请求的IP通过http协议给你,这种协议有一个特点就是必须双端都支持,因为这种模式是非标准的。没有任何一个RFC文档说,你的客户端或者你的操作系统天生就支持这种机制。这有点类似是一种API的这种方式,那如果要实现的话就必须双端都支持。
一般,第三种调度的应用场景是在手机的APP端,在APP软件里面,你要访问某些东西很有可能被运营商劫持等问题,这个劫持问题后面还有很大的篇幅去讲。那为了避免这种劫持,可能会用到这种http DNS的调度方式。既然APP的程序都是你自己写的,所以说实现这么简单一个API的借口是很容易的。
CDN的接入
可能会有人问,你讲了这么多DNS和具体CDN的调度有什么关系呢?
因为在讲你获得一个具体的DNS域名地址的时,他给你的就是一个IP地址。那在没有CDN之前,他给你的IP地址就是在原来没做CDN时的原始服务器地址。但如果你做过CDN的话,你会发现最终拿到的这个IP地址是CDN的节点,而并不是真正的原始服务器。
我们通常说的拿到一个IP地址,这实际上是DNS的A记录。DNS里面有很多不同的记录,比如像A记录负责给你一个IP地址;比如像CNAME记录给你的是一个域名的别名。当然还有很多其他记录,比如TXT的记录、MX记录等等。这个跟CDN无关,这里就不细说了,有兴趣去查一下DNS相关的文档。
上图就是一个很明显的CDN介入后的效果图。linux里有一个命令叫dig,它可直接把要访问域名的具体的解析情况列出来。那么,通过这个图可看出,当你要访问www.163.com时,他最终虽给出的是一个IP地址,但实际上,它经过了两次CNAME记录。第一次CNAEM记录就是我们之前说得CDN的GRB,他拿到了这个数据,就可以间接知道你的这个LOCODNS是从哪里来的,然后间接给你进行一个定位。以这个图为例,他实际上第一跳是跳到网速地址,第二跳是分配了网速的一个平台,这个平台又分开其他的IP给最终的客户。
Cache 系统—缓存系统
除DNS调度以外,在CDN里还有一个非常大的重头戏就是Cache系统,也就是缓存系统。它用于把那些可以缓存住的东西,缓存到CDN的边缘节点,这样当第二个人去访问同一节点,同一具体电影或MP3时就不用再经过CDN链路回到真正的源站去拿数据,而是由边缘节点直接给数据。
在Cache系统里囊括了很多的技术,比如,用空间换时间的这种高效的数据结构和算法,多级缓存以热度来区分,前端是SSD后面是机械硬盘等等。很多的细节就不说了,如感兴趣的可之后交流。
对于Cache系统来说,有两种不同的工作状态。第一种工作状态就是所谓的命中(hit),第二种就是没有命中(miss)。如果命中了,直接通过检索找到磁盘或内存上的数据,把这个数据直接吐给客户,而不是从后面去拿数据。这样的话就起到一个很完美的加速效果。
第二种是在miss时,其实,miss的时候跟hit唯一的区别就是,当我发现我的本机上没有这个资源,我会去我的upstream(上游)去拿数据。拿完这个数据,除了第一时间给客户,同时还会在硬盘上缓存一份。如果这个硬盘空间满了,会通过一系列置换方法,把最老的数据、最冷的数据替换出去。
提到了upstream,不是原始服务器,原因是因为当客户访问到CDN节点的时,他发现上面没有数据,并不是直接从原始服务器上去拿,而是经过他的另一个CDN节点,然后通过middlemell的方式去进行一些数据传输。然后upstream这一层,从原始服务器拿数据,通过一系列的加速手段,快速的把数据投递给我们的边缘节点,再把这个数据给最终客户。在过程当中upstream和downstream这两层都会把数据缓存一份。通过这种树形结构,比如说多个边缘节点,然后汇总到一个或者几个副层结点,这样的话可以逐渐的实现流量的收敛。
提到Cache的具体技术,我相信这里的很多朋友都是同行业的,有人会说其实这没有什么难的,你只要有网络、有运维人员就可以了。其实我并不这样认为,因为你如果想把它做好的话其实很难,比如,我列出的很多技术你有没有在考虑?
举几个例子来说,你有没有做网卡的的多队列和CPU的亲和性绑定?你有没有做磁盘的调度算法改进?另外,你存储的时候还是用还是?等等都是有讲究的。包括内核的调优包括架构和CPU的绑定,CPU的多级缓存的使用,然后你的处理你使用,还是用标准的的这种机制。再比如说编译的程序时使用的去编译还是用英特尔的,然后你再做很多的调用。比如说一个很简单的字符串拷贝,那你是用,你还是用汇编去写,你还是用什么方式等等很多细节。
关于高性能这一块,还有很多的研究,如大家感兴趣的话,可以之后跟我进行进一步的沟通。我想表达的一个观点就是说,看上去做CDN很简单,入门确实也简单,但是要真正想做好很难。
安全问题
在没有做CDN之前你的网站很有可能会遭受到各种各样的攻击。那么攻击一般分成两种,第一种叫蛮力型攻击,量大的让你的带宽无法抗住最后导致拒绝服务,另外一种是技巧性攻击。
作为CDN来讲,就已经将你的原始服务器的IP进行了隐藏。这样当一个攻击者去访问你的域名的时,实际上访问的并不是你真正的服务器。当他访问的是CDN的节点,就没有办法把CDN的节点打倒,换句话说,即使有能力把CDN的比如10g的节点或者是40g的大节点全部打倒,但由于CDN天然的分布式的部署方式,他也很难在同一时间之内迅速的把全国所有CDN的边缘节点全都打瘫。
另外,还有一种攻击是针对你的DNS地址的。如果你的GRB瘫了的话,会导致整个调度系统失灵。如果调动系统失灵,即使你的CDN的Cache server还是能够正常接受请求,但由于流量调度不了。因此,你需要在DNS层做很多防护机制,比如说用高性能的DNS或用分布式的部署方式等等。
技巧型攻击不需要很大的流量,就可以把你的原针打倒或是让你的网页出现错误的情况。比如说,像注入、挂马甚至说更严重的会直接拖走你的数据库等等。那么作为CDN来说,有很多厂商实际上已经开始具备这样的技巧性的防护能力了,比如说WAF(Web Application Fierwall),就是应用层防火墙,他可以直接去解析你的请求内容,分析内容是否有恶意性,如有恶意性的话去进行过滤,报警等一系列措施来保证你的原始服务器的安全。
详解篇
第二部分主要是针对网络层的优化、架构的优化、Cache的选型还有性能分析等等几个方面,对整个CDN的基础原理作很深入地剖析。
原始的CDN其实是Content Delivery Network这三个词的缩写,也就是内容分发网络。但我认为应该是can do something on Network。CDN的理念是加速,所以,我们就尽一切可能去做各种优化,从一层到七层的优化来实现最终的优化效果。
为什么说一层是优化,实际上也是硬件,你的服务器选型就是一种优化。你是用ssd,还是用saker硬盘,你是该用pce卡,还是应该用ssd。你的CPU应该用至强还是应该用阿童木的等等,都是需要去斟酌。
至于二层,链路层的优化指的就是资源方面。比如机房如何去选择。
三层路由层是指你在middlemell这块真正选路的具体的细节,后面会有一个图来具体讲一下。
四层是指传输层的优化,我们一般的业务全都是TCP,所以说这里面就可以明确的说这里是指TCP的优化。还有一个就是七层也是可以优化的。比如说你强行对内容进行压缩,甚至你改变压缩级别去压缩。
作为CDN来说,基本上我罗列了一下可能会用到的一些技术,大概10个。比如说就近分布、策略性的缓存、传输的优化、链路层的优化、包括内容的预取、合并回源。然后持久连接池、主动压缩,还有当你原始服务器挂了的话你怎么样能够保证让客户看到数据等很多的细节。
路径的优化,实际上,我们可以把它抽象成是一个求最短路径最优解的思路去解决真实的问题。当你从a点到b点需要传输数据的时,往往会经过一个c点,比直接从a到b更快。在互联网里有个三角原理,和地理位置的原理有一定区别的。虽说有一定的相关性,但还是有区别的,有可能从a经过c到b会比a直接到b更快。
在数据传输的时,需要去考虑很多综合因素,目前为止,包括阿克麦也很难做到完全系统自动化去做链路选择和切换。在调度的时,很多公司都有专门的团队管流量调度的。很多的系统可能只起到支撑和参考的作用,而真正需要决策的还是人。因为你需要考虑的元素太多了,比如说要考虑你的带宽成本、带宽节点冗余量、服务器承载能力,要考虑你的客户敏感度哪些该切哪些不该切等很多细节。
传输层的优化刚才讲到了是TCP优化,在现今的互联网里,TCP优化是可以带来最直接客户体验感的一种实现方式。如果想讲TCP优化到底是怎么回事,我们就得先从头讲一下TCP具体的原理是怎样的。
上图,我画了四个不同的红圈,cwnd,英文是,就是拥塞控制窗口,用途是控制发送端的发送速度。ss是slow start的缩写,也就是慢启动,这是任何一个TCP协议在最开始的时候必经的一个阶段。
后两个词较有意思,ssthresh,是slow start threshold的缩写,也就是说慢启动阈值。它是出现在你传输到一定速度的时,认为应慢点去传的时,原来的指数传输方式,增长速度方式变成现行的速度增长,来尽量的规避和避免网络的拥塞。
那整个拥塞避免阶段其实就是图中右下角的CA,这是拥塞避免这个线性的过程,不是指数。指数的那个叫慢启动。
当TCP开始传输数据的时,最开始的时候并不是以一个很快的速度发出。你看到的wget或是下载某一个东西的速度猛的就非常快,实际上它是一个由微观慢慢把速度加起来的过程,只是这个时间很短你可能并没有察觉。但实际上从微观上来看最开始是有一个所谓的初始发包数量的这么一个概念。在早期的2.6.18内核,也就是3645相对应的这个版本之前,初始的发包数量是两个。
它发出第一轮数据,实际上只发两个数据包。等待这两个数据包的完全确认,如这两个数据包完全收到ACK确认数据之后,会认为第一轮数据你都已经收到了,这时它会把发包的数量调整成4个。如果第二个也收到了,就调成8个16个32个这样的一个增长过程,就是slow start的过程。那这个过程实际上就是一个最开始在TCP刚开始建立的时候的一个最初始的过程。
那这个过程什么时候会结束?其实就是在丢包的时候。如果没有丢包,我没有理由降速或者调整发送速度。当他遇到丢包的时候他就会把这个值记录下来,并且通过他的拥塞控制算法,算出一个合理的阈值。那么当下一次速度增长到这个阈值的时候,就会知道不能再指数增长了,而是应该线性的增长发包的数量,来避免再次丢包。
还有个概念就是RTO,实际上是在服务器发数据而客户端始终没有响应的时,它会等待一个超时定时器。这个超时定时器一旦出现,就会回到另一个协议栈里的一个状态机。当这个状态机处于丢包状态时,它就会把它的CWND降到最开始这么大,那么他的速度就会骤降,这是个非常严重的问题。且一旦骤降它会重新评估。有可能,你之前,比如说你的窗口长到24,但是你丢包了,然后他给你算出你应该到16就变成线性。如果你再出现严重问题,它可能会把阈值降到12,并启用线性传输模式了。
通过介绍这个原理,大家可能会看到,其实TCP是一个很聪明的做法。它在能尽量传的时候拼命的提高速度,然后在丢包的时候就尽量降低速度,尽量的规避拥堵。
现如今的网络产生了很大不同,因为,比如说WiFi的接入、3G、4G的移动信号的接入,甚至南电信北联通的一些资源的不充沛,更甚至是恶意的一些限速,会导致你丢包的原因,有时并不是真正的拥塞。而是你链路里面命中注定会有这么多的数据会丢掉。
大家想象一下,假如有一个恒定的丢包概率的网络。当我发一百个包的时候,丢掉百分之二十,只收到了八十个,这时如果去降速,就意味着降得速度越低,发送的数据量就越小,那么对端收到的就更少。因为丢包概率是恒定的,如果遇到这种情况的话,早期的TCP的拥塞控制算法就已经不能满足现有的这种环境的需求了,因此我们要考虑如何去优化。
这是用tcpdump把数据包抓下来后用Verashape软件打开并且进行图形化分析的一个微观展示图。
图里可以看到另外一个细节,就是能看到有很多不同的这种发包的周期。这个其实就是我刚才讲的每次发两个、四个、八个、十六个这样的不同的发送的时刻。但这里有个问题,他发送时会一股脑儿地把数据发出去。虽说在宏观上来讲,你单位时间之内只能发这么多,但是从微观上来讲,实际上你这么一次发送就相当于是burst,这种大的冲击有可能会导致中间网络链路不充沛,然后会造成丢包。
在早期研究GPRS网络或者是25G的网络的时候,尤其会遇到这种情况。他的特征是RTT很长,同时你的带宽很小。那大家可以想象一下,如果你的带宽很小,每一次突发这么大,从微观角度来讲,这个数据就已经有可能会造成微观上的丢包了。
另外一种优化的方法就是的平滑发包,充分的利用每一个发包周期之间的时间间隔,然后把数据包打散。这样的话,既没有让对方从宏观上感觉发送速度慢,从微观上我也让这个数据变得更平滑,而不会导致某一个具体的小时间的一个时刻,由于链路不充足而导致丢包。
除了刚才说的以外,还有很多优化的方法。比如说建连优化,当你去发信包三次握手的时,默认情况下,对方如果未反馈,你会以1为一个贝司值然后以2的主数递增这样去重试。比如,一秒钟重试一次,两秒钟一次,四秒钟一次,很多次之后,它会自动的放弃。那如果按照6.18内核,以3为一个贝司值,以3的主数递增,三、六、十二这样。所以,这个环节就可能会导致很严重的问题,那对服务器来说你可以做到什么?比如说你再发完这个CS第二次握手之后,如果他一段时间没响应,可快速给他再重发一遍。
这种数据包的优化实际上并不会占用什么网络,而且有可能会勾引出第三次握手,快速的解决由于你的服务器在出项上导致第二次握手丢包。
另外,还有很多的客户可能较关心具体的细节,比如,你的首包时间是多少?首包时间是当你发完http的get请求之后,你所拿到的第一个数据。那这第一个数据往往是你的响应头。这个响应头有可能是和你的内容一起发送过来的,也有可能是先发送一个响应头然后再发内容,这取决于你自己的server的时限。在TCP里面有一个Nagel算法,Nagel算法会把这个数据拼凑成一个大块儿后发出。如果你要是在engikers里配TCP nodelay,把这个配完后就可以。有什么发什么可以去提升这个首包的效果。
平滑发包刚才也讲过了,丢包预判就是你通过统计学的一些方法,把端到端的,比如,c到你的传输具体情况做一个记录。然后,如果你要是发现丢包率是比较符合规律的话,可以在没有丢包的时候你预判有可能丢包,那你就时不时的去把某些数据包重发一遍,发两遍,即使他能收到。这样的话也是可以达到加速的抗丢包效果的。
后面还有很多细节,这里就不再赘述。右边是一个linux下的TCP协议栈状态机的切换跃迁图。这个图里面的open状态指的是在没有任何丢包的正常状态,Recovery状态是开始有重传。Disorder这个状态是看到有数据包乱序。CWR是一种TCP头部携带的显性的拥塞控制,这个一般很少用到.但是苹果操作系统确实支持的。左边那个Loss状态就是之前我一直讲的一旦遇到RTO以后,就会骤降这种情况。所以,在TPC优化的时还需考虑你怎么样去优化你的协议的状态跃迁,让他尽量不跑到Loss这个状态。
很多做TCP优化的,只是改变TCP拥塞控制算法,直接编译出一个内核模块重新加载,然后改一下的拥塞控制模块,再重新加载一下就OK了。实际上,这种做法只能改变你计算CWND的数量,但并不能改变他的状态。如果你想做TCP优化,你必须要动TCP协议栈本身。
在linux协议栈里面有一个控制参数叫tcp slow start after idle。意思是,你在数据传输的时,如果等了一段时间超出一定的时间阈值之后他会把CWND给你降到初始值。
那么,这种情况是有问题的,假如你是一个http的业务,而且都是这种小文件。恰好你又用了keep life这种状态,每一个请求结束后并不马上断掉链接,而是期待下一次的数据请求。在第一个数据块object,比如下载第一个图片,他去请求时,这个CWND会逐渐通过慢系统长到一定的高度。由于你两次get请求之间可能间隔了一段时间,这个时间一旦超过阈值,就会在发送端自己把这个CWND降到初始值。一旦降到初始值,这个时候你第一个object在下载以后,CWND好不容易涨上去的就白长了。你在下载第二个的时候实际上还是从慢速开始。
在linux系统里默认这个值开启,它实际上是会给你降的。如果你想做优化的话,最简单的做法就是把它置成0。如果置成0,即使连接中间隔的时间很长,你在请求第二个object的时,他的初始的发送速度就继续按照刚才的大小继续发送。这样,就可以达到一个很好的加速效果。
通过第一部分的讲解,大家也知道CDN有一个非常重要的就是缓存系统,用来做数据的缓存。当你下载一个电影或mp3的时,数据会留在你的缓存系统里面。如果有第二个人还访问同一个文件的时,还恰好通过你去访问,那你就可以直接把这个内容给客户,而无需把这个内容递交到真正的原始服务器,然后再从原始服务器去拿。
但在真正的业务场景里,除了可缓存的内容以外,还有一种是完全不可缓存的。比如说你的登陆、再比如说你浏览论坛页面的时候有一些动态的一些元素,因为每个人看到的东西是不一样的,不同的URL、不同的cookie可能看到的东西玩是完全不同,那这个时候这个数据就没有办法缓存。有人会说这种情况下是不是CDN就没有价值了,如果是纯动态页面的话,其实不是的。通过架构优化我可以给你展示一下,通过CDN你能怎么样去加速,能加到一个什么样的速度?
这是个在没有做CDN之前,客户访问源站的时候的连接示意图。这里面提到一个概念RTT,之前也说到了它的真正术语是往返时延,实际上就是我们平时说的ping值。但是ping值只是RTT的一部分,并不是说RTT就是ping值。实际上,TCP也有往返时延的,比如,你发起一个信包,然后对方回复,这段时间间隔也是RTT。有一种ping叫TCPping,利用的就是这个特点。如图,假设客户到原站的RTT是80毫秒,假设你要下载的文件是30kb,算一下他大概需要多久时间可以下载完成。
图里共3种颜色,红色代表TCP的三次握手是一个建连的过程。第一次,第二次第三次然后建连完成。大家仔细看绿色的,绿色是发get请求的时候的一个数据包。蓝色的大量的数据传输表示服务器开始以不同周期开始吐数据。那么这里边我是假设他初始CWND是2,那就是2、4、8这样去长。
在TCP里边还有一个MSS的概念,TCP协议能一个数据包存储的最大真正的七层内容是有多长。在普通的网络当中,MTU如果是1500字节的话,IP头是20字节,TCP头是20字节,在一般情况下MSS是1460,也就是说一个数据包可以传递1460字节的内容。那我们算一下,30kb是30*1024这么多字节,那么它需要几轮才能传输完成呢?
第一轮发两个数据包,第二轮发四个数据包,第三轮发八个数据包,第四轮的时候其实剩余的数据量已经不足16个数据包了,但是仍然要发送一轮。
后面是四个来回我刚才讲了,再加上前面的TCP三次握手也占一个往返时延。也就是说一共有五个来回周期。那假设这个往返时延是80毫秒,可以算一下,5*80,这是400毫秒。也就是在这么一个网络的情况下,带宽足够充足,然后在没有抖动也没有丢包的情况下,要传一个30kb的数据在80毫秒延迟的情况下,他最快的理论速度也需400毫秒才能完成,这是在CDN之前。
上图是在做CDN之后的效果图。我们可以看到,在客户和园站之间,部署两个CDN的节点,一个叫下层一个叫上层。下层离用户很近,这一块儿的距离就lastmell。上层离源站近,这一块儿这个距离我们firstmell。然后上下层之间我们叫middlemell。为确保能够充分的体现即使是动态的数据我们也能起到很完美的加速效果,我在上下层之间我仍保留80毫秒。同时在Lastmell和firstmell分别引入20毫秒,那么总共延时就变成了120毫秒。也就是说现在网络环境总延时是原来的1.5倍。
首先来看一下firstmell,因为firstmell和加速之前的拓普相比,唯一不同的是往返时延变小,由80毫秒变成了20毫秒。计算一下20*(1+4)=100毫秒。
再来看lastmell,由于lastmell发送端是我们自己的服务器,所以完全可以利用TCP优化这种方式来让数据包发送更快。比如,最简单的加速方式就是去修改你的CWND,原来是2现在修改到10,目前在2.6.32内核以上,他默认的这个CWND应该都是10,早期是2。谷歌在很早之前就已提出一个观点,早期ARFC的标准里说初始值是2,,已不合时宜,因为现在的网络带宽都很大。所以我们应该把它提到10,后来就所有的东西都是以10为初始值。
假设初始值就是10,可以算一下需要多少个周期能把数据发完?共有30K的数据,第一轮发10个,每一个数据包记住这里边说的10个指的是10个数据包。每个数据包可存放的内容是1460个字节,那实际上第一轮发10个就已经变成14.6k。那第二轮20个数据包用不满就已经发完了,套用公式20*3,实际上只需要60毫秒就可完成。
最有意思的是middlemell这块。因为所有的东西全都是我们自己的,服务器也是我们自己的,然后网络也是相对来说可控的。所以可以在middlemell这块儿做很多有意思的事情。比如TCP,由于服务器是在机房,他跟原针不一样,原针有可能带宽很小,而在lastmell也不可能吐数据太快,如果太快你的最终客户端的网络又个瓶颈。但我们两个节点之间传输的时实际上是可以速度很快的,也可以直接把数据一次性的30个包传到下层,从上层传到下层。
除这个以外还有一个很重要的观点,我可以通过上下层的一种长连接的机制keeplive的一个机制,忽略TCP的3次握手。即使换不同用户去访问同一个下层,但因我上下层之间已经建立了一个所谓的通道,那么也可以让这个数据通过我这个通道直接把get请求发送到上层,上层把这个交给原针。这样减少一个往返。套用公式可以看一下80*(0+1),总共只需要80毫秒。
把三个部分一起加起来,可以算一下60+80+100=240。也就是说,这种环境下,在总的延时是原来1.5倍的情况下,完美的做到比原来提升40%的优化效果。在不能缓存的纯动态的情况下,我在中间的middlemell没有任何RTT减少的情况下,我的CDN架构给你带来的价值。
还有两个细节我没有说,第一个细节是,真正的我们找上下层的链路的时有可能会小于80毫秒。因为利用我们之前说的那个路由的最短路径的算法,有可能会找一个经过c点到达上层,总共的RTT可能会小于80毫秒或更小,实际上还能进一步的缩短时间。另外,这里讲的是上层拿到所有的数据之后才会给下层,下层拿到所有数据之后才会给用户,实际上他不会在所有数据收到之后才传输,他是随收随传的,所以这三个过程在时间的横轴上是有叠加的,就导致时间进一步缩短。
之前我有讲,在CDN里你玩的是什么?你玩的实际上就是网络,尤其是对CDN公司来说。坦白来讲,服务器有三大部分组成,第一部分是你的操作系统,第二部分是你的Cache缓存系统,第三部分就是你的网络。而对于OS来说,一般你的操作系统选型完毕优化之后你一般不会再动它了,除非遇到了重大的安全隐患或者是有重大的升级。而对你Cache系统来说也是,一般都求稳,在没有重大的bug的时,不会去轻易的改变。但最复杂的就是网络,你必须要掌握对网络的控制度,这样的话你才能驾驭它。
如果你的网络研究的很透彻,通过你的分析会发现很多问题。给各位讲个案例,我们在访问某一个资源的时,大概可能五年前或更早,我刚加入蓝讯的时去分析,看到我们不如竞争对手。我通过数据包的分析发现,不是我们资源不好,而是我们的TCP优化没有做,而对方做了TCP优化。
以这个例子来讲,可通过第一个信包和第二个包之间的时间差可看到他的RTT是23毫秒。通过这个RTT我们可看到它在第19个包和第21个包之间有一个时间跳变,这个跳变就意味着它属于第二轮发包机制。那第一轮可以数一下一共是10个包,也就是说初始initcwnd值是10。假设有一个50kb的数据包需要发送,算一下需要多长时间。50*1024算出字节再除以 1460换成包数,再加1,加1的原因是考虑到一定会有余数,余数的话就占一个包所以加1。等于36个包,要把36个包发出去才能完成这个发送。
假如要发36个包,初始发包数量是10。可算下36拆解等于10+20+6。他需要3个往返才能把这个数据发完。套公式算下它的发送时间需要多久?RTT我们之前算了是23,23*4。为什么是4呢,因为你还要加一个TCP三次握手的一个时间,一共需要92毫秒才能完成。
上图是竞争对手的情况,我们可以通过第一次和第二次握手,看到他的往返时延是35毫秒。和之前的23毫秒相比,可知这个资源的ping值比原来增加了52%。通过刚才的分析方法我们也可以找到第35和37号包的跳变点。那么35号包之前是第一个发送轮回。整个的发包数量是20,它的初始发包数量实际上并不是标准的10,而是20。那么,我们可以再算一下,如果你有50kb必须要发出,你最终需要也是36个包,但是你初始是20就需两轮,分别是20+16。
通过套公式,可知需要150毫秒完成。那150毫秒跟之前的92比只慢14%。在资源落后52%的情况下,最终效果才慢了14%,中间的这个差距实际上就是你的技术带来的价值。
这是个视频点播对比数据图,高低代表发送速率,横轴是时间。通过对比,明显可看到厂商a的发送速度高于厂商b。做完TCP后和做TCP优化前,差距还是很大。整个过程可看到第一个厂商的速度起来以后非常平稳,直到结束,而第二个厂商他最开始速度很快逐渐发现要丢包减速,速度就就涨不起来了。这就是提速优化价值。
上图是当时分析为什么服务器发送慢的一个结果,通过这个分析直接就给大家总结一下吧!结果可以看到这是由于TCP算法自身的问题而导致的,他把时间白白浪费了,他们有时间其实是可以继续发出去的但并没有继续发。
另外,还有一个有意思的,每个厂商无论你是做CDN,做电商、做IT企业,只要你有对外提供的server,而且server的负载比较高都会遇到的一个syncookie的坑。给大家讲一下。在TCP的标准里有两个选项一个叫WScale一个是SACK。他并不是在RFC793里边出现的,他是在RFC1323里补充而出现的。
现在讲一下这个WScale什么东西。默认的情况下在标准的TCP协议,在早期的时候是没有WScale概念的。他在TCP的头部有一个16byte的空间来表示你能发送的最大字节数,一个周期能发送的最大字节数。那根据TCP的吞吐量的计算公式,吞吐量一定是小于等于最大发送窗口除以RTT的,这个RTT是以秒为单位。
之所以说小于等于是因为一般的情况下他是有可能有乱序或者抖动的。假如你的TCP协议传输时,RTT是100毫秒,假设网络之间没有丢包也没有乱序也没有抖动,且带宽是无限大的。套公式可知,64k除以100毫秒,也就是0.1,吞吐量最大是640k。即使你的带宽无限大,没有丢包,没有抖动,最大640k,就是这么严格。
大家可能觉得这个有点儿不可思议,为什么我们传输速度是远大于这个呢?因为在新的标准里引用WScale这个概念。在TCP三次握手的时候,客户端如果要支持这个选项的话,服务端被通知支持这个选项。如果服务端也支持,会记录下来说客户端支持,同时回应也支持。在客户端拿到第二次握手时,服务端就也把自己置成支持状态了。在数据传输的时,它是以2为底数,然后以WScale的这个n值为指数的一个滑动窗口递增值。
利用这个WScale是可把发送窗口的数量涨到很大的,比如说64k、128k、256k甚至更大。如果要这样再套公式,他的传输效果就会变得非常好了。
关于参数SACK,选择性应答,全称是Selective ACK。在你数据传输的时,没有这个选项他会怎么样呢?比如,要传10个数据包,只有第6个数据包丢掉了,那么在服务端收到ACK的时候他会知道,只收到了5,然后就没有其他信息了。这个时候他需要怎么做呢?需要把6到10重新发一遍。那会导致两个问题,第一,你的数据从开始到传完,速度就会变慢。第二个就是占用额外带宽把7到10进行一个没必要的重传。
同样的在TCP三次握手的时候写标记,并且两边都确认同时开启,如果要是都支持的话,在客户端反馈数据的时,他就会告诉服务端,收到连续的序号完整的到5,但是我还收到了7到10。服务端就可通过这两个信息进行拼接找到中间的空隙,就会知道只有6号丢掉了,他就只需传6就可以。为什么要强调这两个,可能是为后面那个syncookie的坑做铺垫。
接触过linux的人都知道在里面有一个叫syncookie的机制。是一种帮助你去防护一定量攻击的一种方法。那么它的原理是什么呢?在每一次数据正常建立的时它优先消耗一个叫连接队列的一个概念。等连接队列满了,如果你syncookie未开启,有新的请求过来,他就会把那个新丢掉,如果你有大量的这种假的建连数据包已充斥满了整个建连队列的,那么他就会导致拒绝服务。
那syncookie开启的情况下会怎么样呢?他会在协议栈之前自己伪造一个应答机制,并不是真正的协议栈去代应答第二次握手。同时他的第二次握手会携带一个算好的一个cookie值作为第三次握手的校验。如果他收到了第三次握手的校验值的会被认为是一个合法的建连,那么,他会把这个通过注入的方式,直接告诉你这个链接可以直接用了。那在前期syncookie当满的时候开始启动这个状态,他是不占用队列的,所以说他是一个非常好的防攻击的一个手段,但是他的防攻击的量不会很大,微量是可以的。
但坑也恰恰就在这。由于syncookie他不是标准的TCP协议栈,所以说他的支持,并不是非常的完备。等一段syncookie发出,他代应答的第二次握手并不携带WScale和SACK这个选项。就会让客户端误认为是不支持的,所以,后续的沟通就变得非常的低效。我们之前做过一个实验,在有一定量丢包而且大延时的情况下,你的速度可能只有300多k。后来查了很多资料发现确实是这个样子,而且我们做了很多的模拟时间。比如,都为syncookie出发的时,他速度确实就很快。
后来我们做了一个改动,在syncookie上,如果要是代应答的时,我们携带SACK的这个数据给客户,那后来建连的时都可以把这个功能用起来。用起来时我们在线上是真正的无环境试验可以提升大概25%到35%的服务质量。
另外,讲一下关于Cache的选型。很多的公司都在自研,也有很多公司直接用的开源的软件,至于怎么去选可能就要看你自己的场景了。比如,裸盘技术不依赖文件系统,但他的缺点是在于他对业务的支撑比较差,且他是c++语言写的COVER住的人还是不多的,那世界上有很多的公司是跟那个SD相结合,利用的方式去拆解业务,后续来做Cache。
还有很多公司是走的自研路线,比如像网速蓝汛,阿里快忙这些公司都是曾经做过自研的,后续是什么发展目前不太清楚。
坦白来讲,并不推荐自研。能用钱解决的问题都不是问题。如果去自研,第一,你要耗费大量的人力和时间。第二,你需要去思考,你能不能cover住这些原本人家已经做好的东西。你要自研的话,前提是你在重复造车的过程当中一定有一个车轮,那个车轮能不能保证你的这个龙骨能够跟人家一样,甚至比人家更好。这里给大家分享一个我之前遇到过的自研软件的坑。
这个数据包它体现的是什么?体现的是你看最后的那两个数据包,倒数第二个是一个get请求。它发送完这个get请求之后马上收到了一个RST,这个连接被断掉了。当时造成了很多的投诉,那我们也是去考虑这个问题到底是怎么造成的?
通过抓包在服务器上抓包微观去分析,我们发现实际上这个问题是怎么造成的呢?看上面这个截图,在客户端发起请求的时候他有一个字段就可能是keep live。意思是说他期望去和服务器进行这种长连接。而服务器是怎么给的呢?服务器给出去的时,并没有明确的告诉客户端是否支持。导致这个问题的原因是由于我们的研发人员并没有真正地领会RTT协议的精髓,他没有完全cover住这个RTT协议导致最基本的这种车轮,这个轮骨做的是有问题的导致很严重的坑。
在HTTP1.0协议里边,如果你要是发一个可能是keeplive你希望和server进行keeplive这种沟通的话。Server端必须要告诉说我支持这样才能支持,有点像TCP的三次握手,那个WScale和那个SACK类似这样,但是在1.1协议里边儿恰恰不是这个样子的。如果你的身边没有任何反馈的话,客户端会默认为,我发了这个东西,你有没有告诉我说你不支持,那能不能支持?那这个时候问题就出现了。
这个就导致了第一个请求结束之后,服务端实际上是不支持,但没有给出明确的显性的信息来告诉客户端。研发人员把1.0和1.1协议弄混了,或是他还认为1.1协议是跟1.0一样的标准。这个时候,在发送的第一个数据之后就把这个连接关掉,此时客户端并不知道服务端是不支持的,他还尝试发起第二次请求结果就导致被塞。
欢迎关注技术公众号:架构师成长营