[toc]
透明多级分流系统#
对系统流量进行规划, 要注意以下2个原则
- 尽可能减少单点部件, 或者减少到达单点部件的流量或者作用
- 奥卡姆剃刀原则,确定有再有必要的时候才去使用,避免过度设计
1 客户端缓存#
即对于某些资源, 在客户端就做缓存,客户端不去重复请求。
1.1 强制缓存#
类似HTTP协议里在header里用到的两种标签,且都是服务端强行控制的,基于时间的
- Expires
服务端直接返回数据不会变动的截止时间。
缺点:受限于客户端本地时间、无法表示不缓存除非强制改时间戳、无法表示是否是私有资源(避免私有资源被其他节点缓存) - Cache-Control
这个请求头使用max-age、private、no-cache等标签解决了Expires里的3个缺点。
2.2 协商缓存#
协商缓存需要考虑是否真的发生变化。 协商和强制可以共同存在,即强制失效的时候就可以用上协商。
协商缓存不仅存在于地址输入、跳转,也存在F5中(但如果Ctrl+F5强制刷新则会让缓存失效)
- Last-Modified
告诉客户端资源的最后修改时间, 客户端再次请求时也会对这个时间做修改
如果服务端发现在那个时间之后资源未变动,返回304 Not Modified
如果有变动,就返回OK,并携带完整的资源 - 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 内容分发#
如何分发内容有两种方式:
- 主动分发, 通过CND服务商提供的接口主动推送自己的资源,这样你需要额外编写资源推动的代码。大型活动例如双11会优先考虑主动分发预先准备资源。
- 被动回源, 由用户访问触发,当发现没有资源时,CDN会去源站请求并返回,则用你不需要新写相关代码,只要在CDN那边支持回源你的源站即可。小型站点基本都是用这个方法。
如何更新资源有两种方式:
- 超时被动失效,CDN的资源都有有效期,超时了就回源获取
- 手工主动失效, CDN服务商提供缓存失效接口,主动触发失效并进行被动回源更新。
现在一般是1和2结合使用,二者不冲突
5 负载均衡#
负载均衡有两种大类
- 四层负载均衡
指的是计算机七层模型中四层及以下的均衡策略结合
即 数据链路层 + 网络层 均可做均衡 - 七层负载均衡
指的是在应用层通过实际代码做均衡
5.1 数据链路层负载均衡(四层负载均衡)#
- 通过链路层上的均衡器替换MAC地址,进行链路层的均衡
- 各负载节点的IP是一样的(相同的虚拟IP)
- 返回时无需经过均衡器,直接返回即可(因为目标ip、源ip基本没变)
缺点:
必须是同一个子网内,无法跨VLAN,只能作为最接近数据中心的均衡器
5.2 网络层负载均衡(四层负载均衡)#
有两种方式:
IP隧道模式#
均衡器在IP报文外面包了一层新的header,header里指定了目标机器的实际ip或者小网ip。 接收机器要支持解header,且同样要求作为返回的虚拟ip是一致的,也是直接返回无需经过均衡器。
缺点:
- 用到的服务器都要支持隧道解包能力(linux系统现在都支持)
- 虚拟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的缺点。
自身缺点:
- 每个缓存专门维护要更新次数的计数器,维护开销大还有加锁问题(LRU的更新时间不需要考虑加锁,直接覆盖最新即可)
- 如果某个缓存某时期访问很高,比其他缓存高了一个数量级,后面不再使用,想淘汰很困难
为了解决上面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 缓存风险#
缓存穿透#
大量不存在的缓存打进来
要么是支持对不存在的数据缓存空值
要么是引入布隆过滤器
缓存击穿#
同一时间瞬间涌现很多请求,访问数据库有但是缓存里没有的数据,此时可能直接打穿数据库(缓存生效是有延迟的)
可以是用锁、队列来完成同步
对于热点缓存,提前预处理或者配置策略