0%

五、流量性能提升

[toc]

透明多级分流系统#

对系统流量进行规划, 要注意以下2个原则

  1. 尽可能减少单点部件, 或者减少到达单点部件的流量或者作用
  2. 奥卡姆剃刀原则,确定有再有必要的时候才去使用,避免过度设计

1 客户端缓存#

即对于某些资源, 在客户端就做缓存,客户端不去重复请求。

1.1 强制缓存#

类似HTTP协议里在header里用到的两种标签,且都是服务端强行控制的,基于时间的

  1. Expires
    服务端直接返回数据不会变动的截止时间。
    缺点:受限于客户端本地时间、无法表示不缓存除非强制改时间戳、无法表示是否是私有资源(避免私有资源被其他节点缓存)
  2. Cache-Control
    这个请求头使用max-age、private、no-cache等标签解决了Expires里的3个缺点。

2.2 协商缓存#

协商缓存需要考虑是否真的发生变化。 协商和强制可以共同存在,即强制失效的时候就可以用上协商。
协商缓存不仅存在于地址输入、跳转,也存在F5中(但如果Ctrl+F5强制刷新则会让缓存失效)

  1. Last-Modified
    告诉客户端资源的最后修改时间, 客户端再次请求时也会对这个时间做修改
    如果服务端发现在那个时间之后资源未变动,返回304 Not Modified
    如果有变动,就返回OK,并携带完整的资源
  2. ETag
    需要对资源计算哈希值,客户端发请求也会带上自己存的ETag,每次会比对资源的哈希值是否一致,不一致则返回新资源。
    Etag是一致性最强的本地缓存机制,但也是性能最差的。

3 传输通道优化#

本章节大部分以熟知的HTTP协议作为主要传输通道协议,讲解如何进行优化

3.1 连接数优化#

HTTP是基于TCP的,每次都是重新建立一个TCP连接。 因此前端开发人员开发了很多小优化,来减少请求次数,例如雪碧图、分段文档、合并Ajax请求之类的。

HTTP1.0里的长连接(keep-alive连接复用)为什么不能解决这个问题?
因为存在队首阻塞问题,本质上是基于FIFO复用连接, 1个请求卡住了,后面9个请求都阻塞住了,但如果同时支持返回,在顺序混乱的情况下无法正常处理

HTTP2.0的多路复用解决了这个问题

  • 以帧作为最小粒度单位,每个帧都携带流ID识别是哪个流
  • 客户端可以很容易在不同流中重组HTTP请求和响应报文

3.2 传输压缩#

HTTP很早就支持GZip压缩来减少大资源的传输量

HTTP1.0中, 持久连接和传输压缩无法一起使用, 因为压缩后无法识别资源是否传输完毕。

HTTP1.1中引入了“分块传输编码”,来进行资源结束的判断。

3.3 用UDP来加快网络传输#

HTTP/3中,希望能替换掉HTTP on TCP的依赖、
谷歌推出了快速UDP网络连接, 即QUIC

  • QUIC以UDP为基础, 可靠传输能力由自己实现
  • QUIC专门面向移动设备支持, 移动设备的ip地址经常会切换,使用ip作为定位不合适, 因此提出了连接标识符来保持连接。
  • 对于不支持QUIC的情况,支持回退为TCP连接,实现兼容

4 内容分发网络CDN#

CND可以解决 互联网系统跨运营商、跨地域物理距离所导致的时延问题,为网站流量带宽起到分流、减负的作用。
主要包含以下4个工作部分

4.1 路由解析#

用户的静态资源请求访问CDN是通过DNS解析来完成的,甚至可能一个网站会有各种不同地域的CDN域名解析地址返回, 通过你的路由配置会自动选择符合地域的ip地址

4.2 内容分发#

如何分发内容有两种方式:

  1. 主动分发, 通过CND服务商提供的接口主动推送自己的资源,这样你需要额外编写资源推动的代码。大型活动例如双11会优先考虑主动分发预先准备资源。
  2. 被动回源, 由用户访问触发,当发现没有资源时,CDN会去源站请求并返回,则用你不需要新写相关代码,只要在CDN那边支持回源你的源站即可。小型站点基本都是用这个方法。

如何更新资源有两种方式:

  1. 超时被动失效,CDN的资源都有有效期,超时了就回源获取
  2. 手工主动失效, CDN服务商提供缓存失效接口,主动触发失效并进行被动回源更新。
    现在一般是1和2结合使用,二者不冲突

5 负载均衡#

负载均衡有两种大类

  • 四层负载均衡
    指的是计算机七层模型中四层及以下的均衡策略结合
    即 数据链路层 + 网络层 均可做均衡
  • 七层负载均衡
    指的是在应用层通过实际代码做均衡

5.1 数据链路层负载均衡(四层负载均衡)#

  • 通过链路层上的均衡器替换MAC地址,进行链路层的均衡
  • 各负载节点的IP是一样的(相同的虚拟IP)
  • 返回时无需经过均衡器,直接返回即可(因为目标ip、源ip基本没变)

缺点:
必须是同一个子网内,无法跨VLAN,只能作为最接近数据中心的均衡器

5.2 网络层负载均衡(四层负载均衡)#

有两种方式:

IP隧道模式#

均衡器在IP报文外面包了一层新的header,header里指定了目标机器的实际ip或者小网ip。 接收机器要支持解header,且同样要求作为返回的虚拟ip是一致的,也是直接返回无需经过均衡器。
缺点:

  1. 用到的服务器都要支持隧道解包能力(linux系统现在都支持)
  2. 虚拟ip仍然有较大限制,需要人工介入管理众多机器

NAT模式#

NAT模式中,就是进行真正的ip转换, 且返回时也要返回给NAT进行ip转换,这样只需要针对NAT进行人工管理即可。
缺点在于NAT容易成功性能瓶颈

SNAT会修改源IP改为NAT的ip, 可以做到对业务真正透明, 但是代价是如果需要对源IP做限制时容易有问题, 因为所有的来源ip都是一样的了。

5.3 应用层负载均衡(七层负载均衡)#

也叫做七层代理(应用层代理),因为这个负载均衡属于反向代理(即部署在服务端的代理,对客户端不感知)

不适合做下载站、视频站等流量应用
如果瓶颈在服务计算能力,则可以考虑做应用层均衡

七层代理除负载均衡外的其他功能:

  • 支持做CDN类似的缓存能力
  • 施行智能化路由,根据URL或者特定用户做特殊服务
  • 抵御安全工具,提前过滤攻击报文
  • 链路治理

5.4 负载均衡策略#

  • 轮询均衡
    轮流分配,从1到N再到1
    适用于所有服务器硬件配置完全相同,服务请求需要相对均衡
  • 权重轮询
    根据服务器权重分配周期内的轮询次数
  • 随机均衡
    适用于数据量足够大的相对均衡分布
  • 权重随机均衡
    提升权重高的随机率
  • 一致性哈希均衡
    适用于服务器经常可能掉线或者加入,可以避免哈希键全部更新的情况
  • 响应速度均衡
    定期探测各个服务器的响应速度,根据速度分配权重
  • 最少连接数均衡
    根据连接数分配权重, 适用于长时处理服务例如FTP等

  • 软件均衡器包括基于操作系统内核的LVS、 基于应用程序的Nginx、KeepAlive、HAProxy
  • 硬件均衡器包括F5、A10等公司提供的硬件负载均衡产品

6 服务端缓存#

引入缓存的理由:

  • 减缓CPU计算压力
  • 缓存IO压力
    这2个缓解只是能峰值时的压力缓解,如果普通的响应都很慢,那就算用了缓存也意义不大。

6.1 缓存的几个属性#

缓存需要选型,选型时需要根据实际场景选择你匹配的缓存熟悉

吞吐量#

JDK8改进后的ConcurrentHashMap是并发场景下吞吐量最高的缓存容器,但除了吞吐量其他的能力就很弱了。

缓存状态更新思路:

  • GuavaCache: 同步处理机制,在访问数据时一并更新,分段加锁减少竞争
  • Caffeine:异步日志提交机制,参考数据库日志,并且还有环形缓冲区容忍有损失的状态变更,读性能非常快, 使用多读少写的情况。

命中率和淘汰策略#

基础的三种淘汰方案:

  • FIFO:先进先出,简单实现,但对于高频访问的缓存命中率低,越常用到越可能先进入队列
  • LRU:优先淘汰最久未被访问,基于时间, 用HashMap+链表List实现,但每个缓存都要记录时间,且可能淘汰短期内正好没访问且价值高的数据
  • LFU:优先淘汰最不频繁使用,基于使用次数,可以解决LRU的缺点。
    自身缺点:
  1. 每个缓存专门维护要更新次数的计数器,维护开销大还有加锁问题(LRU的更新时间不需要考虑加锁,直接覆盖最新即可)
  2. 如果某个缓存某时期访问很高,比其他缓存高了一个数量级,后面不再使用,想淘汰很困难

为了解决上面2个缺点,有2个新的策略:

  • TinyLFU: 解决修改计数器的开销问题, 采用Sketch分析访问数据,用少量数据估计全体数据特征,采用滑动时间窗、热度衰减等处理
  • W-Tinfy-LFU: 结合了LRU+LFU的特点, 考虑热度和时间。

分布式能力#

分布式缓存介绍了复制式缓存JbossCache以及集中式缓存Memcached。

jbosscache的缺点在于写入性能太差,容易因为网络同步速度跟不上写入速度,导致内存中积累过多待发对象引发omm

memcached是C语言实现的,好处在于读写性能高,缺点在于数据结构太过紧密,非常依赖序列化做跨语言传输,如果100个字段中的1个字段发生更新,要把100个字段都发出去更新

redis基本打败了各种分布式缓存,成为首选。

对于redis等分布式缓存, 是不会追求一致性C的
如果一定要一致性C, 那应该选用zk或者etcd等分布式协调框架(但他们一般就不会拿来做缓存,因为高并发下吞吐量太低,没有可用性)

进程内缓存和分布式缓存通常结合使用,但容易出现二者数据不一致,写维护策略导致缓存对开发者而言不透明。
一种设置原则是 变更以分布式缓存中的数据为主,访问以进程内缓存的数据优先。
大致做法是数据发生变动时, 在分布式缓存内推送通知, 让一级缓存失效。
访问缓存时,提供封装好的一二级联合查询接口, 让开发者对一二级缓存不感知。

6.2 缓存风险#

缓存穿透#

大量不存在的缓存打进来
要么是支持对不存在的数据缓存空值
要么是引入布隆过滤器

缓存击穿#

同一时间瞬间涌现很多请求,访问数据库有但是缓存里没有的数据,此时可能直接打穿数据库(缓存生效是有延迟的)
可以是用锁、队列来完成同步
对于热点缓存,提前预处理或者配置策略