摘要:本文作者:白金,《CDN 之我见》是一个系列文章,共由三个篇章组成,分为“原理篇”、“详解篇”和“陨坑篇”。本篇章属于“详解篇”的第一部分:网络优化。详解篇适合那些接触过 CDN,对 CDN 多少有些了解,但很多知识点知其然而不知其所以然,想深入了解学习的同学。
本文作者:白金
上篇回顾:《CDN 之我见》系列二:原理篇(缓存、安全)https://yq.aliyun.com/articles/599253
《CDN 之我见》是一个系列文章,共由三个篇章组成,分为“原理篇”、“详解篇”和“陨坑篇”。本篇章属于“详解篇”的第一部分:网络优化。详解篇适合那些接触过 CDN,对 CDN 多少有些了解,但很多知识点知其然而不知其所以然,想深入了解学习的同学。
众所周知,当前是互联网时代,无论大大小小的公司,无论是互联网企业还是传统企业,统统离不开一个“网”字,而相比之下,硬件服务器、操作系统、应用组件等,在没有特殊必要需求的话(如果硬件不坏、软件或系统无急需修复的 BUG、无急需实现的客户需求),这些都是不用去主动改变。
相比之下网络则不同,最频繁变化的,最不想变化而又最无奈被动跟随变化的,就是网络质量,而 CDN 的缩写也是 Content Delivery Network,因此在详解篇我会把重点放在网络上。其它的部分相对都好把控,但网络问题存在一定的随机性和不可预测性。如果我们可以驾驭网络,相信在这个互联互通的互联网时代,我们将变得如虎添翼、叱咤风云!
为此,这里提出我对 CDN 的另一个解读:CDN - Can Do sth. on Network(我们可以在网络层面搞些事情)
提到优化,其实从 OSI 七层模型来讲(准确说是 TCP/IP 模型,二者是不同的,具体可以 google 一下),从物理层到应用层其实都是可以优化的,优化手段各异。
L1 物理层:硬件优化(升级硬件设备,承载更多业务)
L2 链路层:资源好坏(寻找更快的网络节点、确保 Lastmile 尽量短)
L3 路由层:路径优化(寻找 A 到 B 的最短路径)
L4 传输层:协议栈优化(TCP Optimization)
L7 应用层:能做的事情太多了(主要面向 HTTP 面向业务)
针对 CDN 而言,常用的是 L4 和 L7 的优化,例如上图所述。
分布式就近部署:确保网民访问到的 Cache Server 离他是最近的。
策略性缓存:针对明确已知的图片、CSS、JS 等,若源站更新并不快,但却没有明确高值缓存多长时间,CDN Cache Server 可以自定义一个缓存时间(例如 60s),这样可以确保在 60s 之内的同样请求会快速给出数据而不用穿越整个 Internet 从源站获取。
传输路径优化:寻找从边缘 CDN Cache Server 经 upper CDN Cache Server 到源站的最优传输路径,确保动态传输的数据可以走端到端的最优路径。
连接加速:通过修改协议栈的 Handshake Timer 来实现快速重试,以弥补由于丢包导致的重试超时。
传输层优化:这里主要是指 TCP 协议,针对 TCP 协议可以做很多优化策略,由于篇幅问题后面再讲。
内容预取:解析 WEB 内容,对于里面的 Object,在网民请求之前,优先由 CDN Cache Server 主动获取并缓存下来,以缩短数据交互时间,提升网民体验感。
合并回源:当有多个人先后下载同一个还未缓存住的内容时(例如一个 mp4 视频文件),CDN Cache Server 做到合并连接从 upstream 拿数据,而不是几个请求,几个 to uptream connection,既做到了带宽成本优化,又做到了给 upstream 降载,后请求的人还能享受之前 CDN Cache Server 已经下载过的部分文件,这部分内容直接 HIT,只有当追上第一个 downloader 的时候才一起等待 MISS 数据。
持久连接池:在 Middlemile 之间预先建立好 TCP Connection,并一直保持不断开,当网民有新请求过来时,边缘 CDN Cache Server 直接借助与 upper 建立好连接,直接发送 HTTP 的 GET/POST 报文到 upper CDN Cache Server,进行 TCP “去握手化”,减少由于 TCP 连接建立而造成的时间损耗(多适用于高并发小文件请求)。
主动压缩:很多源站由于规划设计问题或担心负载过高问题,页面中的 HTML、CSS、JS 文件(这种文件具有高度可压缩性)并未压缩传输,此时 CDN Cache Server 可以主动对其进行压缩后传输并缓存,以减少传输量、降低交互时间、提升用户体验感。
Offline:当源站挂了怎么办?网民访问时,会拿不到数据。那么 CDN 此时可以策略性发送最新缓存的一份旧数据给网民,而不是生硬的告知用户不可访问,以提升用户体验感。
总之,优化策略非常多,下面将抽取其中最具通用性的网络部分进行详细阐述。
如前文所述,所谓路径优化就是找到两点间的最优路径。
对于网络而言,A 到 B 最快 ≠ A 距离 B 最近,从广东联通访问福建联通,可能不如广东联通经北京联通再到福建联通更快,因此要对节点做实时探测,计算最优路径。
计算最优路径时,还要考虑带宽饱和度、成本、客户敏感度问题综合计算,因此不是看上去那么简单的。
带宽饱和度:作为中转节点(例如上例所说的北京),如果带宽本身已经没有多少剩余,那么穿越北京的路径优化可能会作为压死大象的最后一根稻草,使原本还 OK 的北京节点变得不堪重负。
成本:还以北京为例,北京资源的带宽成本肯定远高于其它省市,例如比河北联通、天津联通可能要贵很多,但可能只比河北天津慢几个毫秒(运动员起跑时最快的反应时间是 150 毫秒),那么为了这几毫秒要多支付很多带宽费用显然是不值当的,那么利用北京进行中转显然就是不值得的(当然,有的时候就是为了和对手 PK,那也没办法)。
客户敏感度:有了中转路径,提速效果当然是好的,但如前文所述也是有代价的,那么是所有业务流量都走最优路径呢?还是只让个别业务走最优路径呢?这个就要看客户敏感度了。例如重点大客户,例如对质量要求较高的高价优质客户,这些客户可能就是首选。
如前文所述,所谓传输层优化主要是指 TCP 优化,这是所有互联网行业的通用技术,是重中之重的领域,TCP 优化如果做的好,可弥补节点质量低下而造成的响应时间过大的损失。
赛马比赛时,有好马当然跑的快。如果马一般(不是太差),骑手的骑术精湛,或许同样也可以得第一,就是这个道理!
另外一点 TCP 优化重要的原因在于,TCP 是互联网尤其是 CDN 的基础协议,基本上所有业务都是 over TCP 来进行传输的,如果将 TCP 优化做好,受益面非常广,可以说全局收益(不仅是提升客户体验感,也能提升内部支撑系统的使用体验感,例如日志传输、配置下发等)。
谈到 TCP 优化,需要先将 TCP 协议基础知识。需要首先明确一些名词属于的概念。
CWND:Congestion Window,拥塞窗口,负责控制单位时间内,数据发送端的报文发送量。TCP 协议规定,一个 RTT(Round-Trip Time,往返时延,大家常说的 ping 值)时间内,数据发送端只能发送 CWND 个数据包(注意不是字节数)。TCP 协议利用 CWND/RTT 来控制速度。
SS:Slow Start,慢启动阶段。TCP 刚开始传输的时候,速度是慢慢涨起来的,除非遇到丢包,否则速度会一直指数性增长(标准 TCP 协议的拥塞控制算法,例如 cubic 就是如此。很多其它拥塞控制算法或其它厂商可能修改过慢启动增长特性,未必符合指数特性)。
CA:Congestion Avoid,拥塞避免阶段。当 TCP 数据发送方感知到有丢包后,会降低 CWND,此时速度会下降,CWND 再次增长时,不再像 SS 那样指数增,而是线性增(同理,标准 TCP 协议的拥塞控制算法,例如 cubic 是这样,很多其它拥塞控制算法或其它厂商可能修改过慢启动增长特性,未必符合这个特性)。
ssthresh:Slow Start Threshold,慢启动阈值。当数据发送方感知到丢包时,会记录此时的 CWND,并计算合理的 ssthresh 值(ssthresh <= 丢包时的 CWND),当 CWND 重新由小至大增长,直到 sshtresh 时,不再 SS 而是 CA。但因为数据确认超时(数据发送端始终收不到对端的接收确认报文),发送端会骤降 CWND 到最初始的状态。
了解了 TCP 的一些名词属于,其实大致也就了解了 TCP 的运作机制,但是有几点要注意。
1.不同的操作系统、内核版本,initcwnd(初始 CWND)不同
以 Linux 为例,kernel-2.6.18 的 initcwnd 是 2,而 kernel-2.6.32 变成了 10,通过 iproute 软件包中的 ip 命令可以修改 Linux 的 CWND 和 RWND。
具体可以参考一篇 Google 写的 Paper,是他们提出了原始 initcwnd = 2 太小了,需要扩大的理念。
原文请参考:
https://developers.google.com/speed/protocols/tcp_initcwnd_techreport.pdf
2.单位时间内(一个 RTT)发送量不是 CWND,而是 min(CWND, RWND)
除了数据发送端有个 CWND 以外,数据接收端还有个 RWND(Receive Window,接收窗口),决定还能接收多少数据,并通过接收确认报文显性告知数据发送端,发送端要参考 CWND 和 RWND 信息决定最终发送的数据量(packet 个数)。管道中究竟能放多少数据,取决于 CWND 和 RWND。
另一个问题:TCP 怎么了?TCP 有什么问题吗?
如果能问出这个问题,证明同学们的关注点是正确的。
TCP 是在上个世纪六七十年代设计的,当时面向的是短距离传输、窄带(可能还是半双工通信)的链路环境。链路本身不太可能丢包,丢包基本都是因为链路拥塞造成的。根据早起的 TCP 拥塞控制算法,丢包 -> 降速,再丢 -> 再降,算法本身的目的是希望通过降速来规避拥塞,进而规避丢包情况。
这个算法本身的理念是正确的,但是随着时代的发展,有了 4G,有了 WiFi,有了长距传输,有了策略性丢包逻辑,导致 TCP 传输过程中,丢包不一定就是拥塞造成的,如果按照传统的“丢包就降速”策略来考虑问题,可能不但不能缓解问题,甚至可能会导致问题更加恶化。
举个例子来说,在一个平均随机丢包 50% 的链路上(平均发出去的包,每 2 个就必然丢 1 个),在这种环境下,发现丢包了,降速发送有用吗?答案毋庸置疑,降速只会让对端收到的有效数据更少。这种环境如何优化呢?很简单,每个包发 2 遍不就可以了?这样对端就会 100% 收到数据了,但代价就是发送端的出口流量是之前的 2 倍。
当然,真正在做 TCP 优化时不是这么简单的,要考虑很多细节,例如如何区分丢包原因,例如该如何控制 CWND,例如如何更早的发现接收端没收到数据,例如当对端无响应时如何快速感知等等等等……
TCP 优化实际上是在用带宽换用户体验感,低成本低质量网络虽然可以通过 TCP 优化提升体验感,但综合成本甚至可能比直接采购优质高价节点更高,效果还未必比优质节点直接服务好。
TCP 之所以叫优化不叫加速,是因为它可以让那些原本应当传输很快,由于算法不合理导致的传输变慢的传输变得快起来,但却不能让那些链路质量原本没有问题的变得更快。
除此之外还有一个优化点,就是 TCP Pacing。
前文我们已经讲过,TCP 是通过 CWND/RTT 来控制传输速率的,在一个 RTT 中,最多只能发 min(CWND, RWND) 这么多数据。但是 TCP 协议栈在发送数据时,是在 RTT 周期之初一股脑将数据发送出去的,这样宏观看没有任何问题,但如果微观看,数据发送波形就像上图那样,一个又一个的凸起。虽然在 RTT 单位时间内发送量恒定,但某微观时间线上的发送速率确实超级猛烈的,这样有可能造成瞬间链路拥塞(尤其是窄带线路)。
原文请参考:https://homes.cs.washington.edu/~tom/pubs/pacing.pdf
通过 TCP 三次握手建连可以测得 RTT,在已知 CWND 的情况下,通过控制发包的时间间隔可以实现 Pacing 效果,使数据包在围观看是均衡发送充满整个 RTT 空间的效果,这样可以避免瞬时拥塞,对窄带链路后需要匀速恒定传输的业务非常有效。
除了 Pacing 以外,还有很多不同的优化算法或策略:
建连优化:TCP 在建立连接时,如果丢包,会进入重试,重试时间是 1s、2s、4s、8s 的指数递增间隔,缩短定时器可以让 TCP 在丢包环境建连时间更快,非常适用于高并发短连接的业务场景。
首包优化:此优化其实没什么实质意义,若要说一定会有意义的话,可能就是满足一些评测标准的需要吧,例如有些客户以首包时间作为性能评判的一个依据。所谓首包时间,简单解释就是从 HTTP Client 发出 GET 请求开始计时,到收到 HTTP 响应的时间。为此,Server 端可以通过 TCP_NODELAY 让服务器先吐出 HTTP 头,再吐出实际内容(分包发送,原本是粘到一起的),来进行提速和优化。据说更有甚者先让服务器无条件返回 "HTTP/" 这几个字符,然后再去 upstream 拿数据。这种做法在真实场景中没有任何帮助,只能欺骗一下探测者罢了,因此还没见过有直接发 "HTTP/" 的,其实是一种作弊行为。
平滑发包:如前文所述,在 RTT 内均匀发包,规避微分时间内的流量突发,尽量避免瞬间拥塞,此处不再赘述。
丢包预判:有些网络的丢包是有规律性的,例如每隔一段时间出现一次丢包,例如每次丢包都连续丢几个等,如果程序能自动发现这个规律(有些不明显),就可以针对性提前多发数据,减少重传时间、提高有效发包率。
RTO 探测:如前文讲 TCP 基础时说过的,若始终收不到 ACK 报文,则需要触发 RTO 定时器。RTO 定时器一般都时间非常长,会浪费很多等待时间,而且一旦 RTO,CWND 就会骤降(标准 TCP),因此利用 Probe 提前与 RTO 去试探,可以规避由于 ACK 报文丢失而导致的速度下降问题。
带宽评估:通过单位时间内收到的 ACK 或 SACK 信息可以得知客户端有效接收速率,通过这个速率可以更合理的控制发包速度。
带宽争抢:有些场景(例如合租)是大家互相挤占带宽的,假如你和室友各 1Mbps 的速度看电影,会把 2Mbps 出口占满,而如果一共有 3 个人看,则没人只能分到 1/3。若此时你的流量流量达到 2Mbps,而他俩还都是 1Mbps,则你至少仍可以分到 2/(2+1+1) * 2Mbps = 1Mbps 的 50% 的带宽,甚至更多,代价就是服务器侧的出口流量加大,增加成本。(TCP 优化的本质就是用带宽换用户体验感)
链路质量记忆:如果一个 Client IP 或一个 C 段 Network,若已经得知了网络质量规律(例如 CWND 多大合适,丢包规律是怎样的等),就可以在下次连接时,优先使用历史经验值,取消慢启动环节直接进入告诉发包状态,以提升客户端接收数据速率。
之前讲的 TCP 优化都是需要去修改代码的,这里有一个不用修改代码的方法,仅修改参数就可以。
内核协议栈参数 net.ipv4.tcp_slow_start_after_idle 默认是开启的,这个参数的用途,是为了规避 CWND 无休止增长,因此在连接不断开,但一段时间不传输数据的话,就将 CWND 收敛到 initcwnd,kernel-2.6.32 是 10,kernel-2.6.18 是 2。因此在 HTTP Connection: keep-alive 的环境下,若连续两个 GET 请求之间存在一定时间间隔,则此时服务器端会降低 CWND 到初始值,当 Client 再次发起 GET 后,服务器会重新进入慢启动流程。
这种友善的保护机制,对于 CDN 来说是帮倒忙,因此我们可以通过命令将此功能关闭,以提高 HTTP Connection: keep-alive 环境下的用户体验感。
# sysctl net.ipv4.tcp_slow_start_after_idle=0
原文链接