高并发之缓存、限流和降级。

架构之高并发:缓存

  • 缓存在高并发系统中有者极其广阔的应用,需要重点掌握。
    随着互联网的普及,内容信息越来越复杂,用户数和访问量越来越大,应用服务器和数据库服务器所做的计算也越来越多。
    但是应用服务器资源是有限的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的)。
    如何能够有效利用有限的资源来提供尽可能大的吞吐量? 一个有效的办法就是引入缓存。
    部分请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。
    
  • 关键词:命中率 = 命中数 / (命中数 + 没有命中数)
    1. 业务场景和业务需求
      缓存通常适合读多写少的业务场景,反之的使用意义并不大,命中率会很低。
      业务需求也决定了实时性的要求,直接影响到过期时间和更新策略,实时性要求越低越适合缓存。
      
    2. 缓存的设计(策略和粒度)
      通常情况下缓存的粒度越小,命中率越高。
      比如说缓存一个用户信息的对象,只有当这个用户的信息发生变化的时候才更新缓存,而如果是缓存一个集合的话,集合中任何一个对象发生变化都要重新更新缓存。 
      当数据发生变化时,直接更新缓存的值比移除缓存或者让缓存过期它的命中率更高,不过这个时候系统的复杂度过高。
      
    3. 缓存的容量和基础设施
      缓存的容量有限就会容易引起缓存的失效和被淘汰。
      采用缓存的技术选型也是至关重要的,比如采用本地内置的应用缓存,就比较容易出现单机瓶颈。而采用分布式缓存就更加容易扩展。
      所以需要做好系统容量规划,系统是否可扩展。
      
  • 缓存应用和实现
    • 在目前的应用服务框架中,比较常见的,时根据缓存雨应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存)。
      本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,
      没有过多的网络开销等,在单体应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;
      同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。 
      
      分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
      

架构之高并发:限流

  • 每个系统都有服务的上限,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。
  • 限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用
  • 方案一:令牌桶方式(Token Bucket)
    令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。
    先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加。
    当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务。
    
    • 举例:Guava RateLimiter - 平滑突发限流(SmoothBursty)
    • 举例:Guava RateLimiter - SmoothWarmingUp
  • 方案二:漏桶方式
    水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。
    
    • 令牌桶和漏桶对比
      令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求; 
      漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝; 
      令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量; 
      漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率; 
      令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率; 
      两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
      
  • 方案三:计数器方式
    计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。也是最简单粗暴的算法。
    
    • 采用AtomicInteger
      使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。
      弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。
      
    • 采用令牌Semaphore
      使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
      相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。
      
    • 采用ThreadPoolExecutor java线程池
      固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;
      

架构之高并发:降级和熔断

  • 在高并发环境下,服务之间的依赖关系导致调用失败,解决的方式通常是: 限流->熔断->隔离->降级, 其目的是防止雪崩效应
    当用户请求 A、P、H、I 四个服务获取数据时,在正常流量下系统稳定运行,
    如果某天系统进来大量流量,其中服务 I 出现 CPU、内存占用过高等问题,结果导致服务 I 出现延迟、响应过慢,
    随着请求的持续增加,服务 I 承受不住压力导致内部错误或资源耗尽,一直不响应,
    此时更糟糕的是其他服务对 I 有依赖,那么这些依赖 I 的服务一直等待 I 的响应,也会出现请求堆积、资源占用,慢慢扩散到所有微服务,引发雪崩效应。
    
  • 基本的容错模式
    • 主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积
    • 限流:限制最大并发数
    • 熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求去重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)
    • 隔离:把每个依赖或调用的服务都隔离开来,防止级联失败引起整体服务不可用
    • 降级:服务失败或异常后,返回指定的默认信息
      图片2.png

架构之高可用:负载均衡

  • 负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案
  • 比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。这就是典型的集群和负载均衡架构:如下图:
    图片1.png
  • 负载均衡的方式
    • 应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。
    • 负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备)
  • 负载均衡的作用(解决的问题):
    1. 解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);
    2. 提供故障转移,实现高可用;
    3. 通过添加或减少服务器数量,提供网站伸缩性(扩展性);
    4. 安全防护;(负载均衡设备上做一些过滤,黑白名单等处理)
  • 负载均衡分类
    • 根据实现技术不同,可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡等
posted @ 2022-05-11 09:04  zhαojh  阅读(383)  评论(0编辑  收藏  举报