服务降级:在高并发的情况,防止用户一直等待,直接返回一个友好的错误提示给客户端。
服务熔断:在高并发的情况,一旦达到服务最大的承受极限,直接拒绝访问,使用服务降级(计数器)。
服务隔离:使用服务隔离方式解决服务雪崩效应(线程池和信号量)
服务限流:在高并发的情况,一旦服务承受不了使用服务限流机制(计时器(滑动窗口计数)、漏桶算法、令牌桶(Ratelimite))
当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。
使用线程隔离或信号隔离的目的是为不同的服务分配一定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。
使用Hystrix实现服务隔离—线程池/信号量
Hystrix 是一个微服务关于服务保护的框架,是Netflix开源的一款针对分布式系统的延迟和容错解决框架,目的是用来隔离分布式服务故障。它提供线程和信号量隔离,以减少不同服务之间资源竞争带来的相互影响;提供优雅降级机制;提供熔断机制使得服务可以快速失败,而不是一直阻塞等待服务响应,并能从中快速恢复。Hystrix通过这些机制来阻止级联失败并保证系统弹性、可用。
Hystrix的资源隔离策略有两种,分别为:线程池和信号量。
1.1应用场景
线程池隔离:
- 第三方应用或者接口
- 并发量大
信号量隔离:
- 内部应用或者中间件(redis)
- 并发需求不大
线程池方式
- 使用线程池隔离可以完全隔离第三方应用,请求线程可以快速放回。
- 请求线程可以继续接受新的请求,如果出现问题线程池隔离是独立的不会影响其他应用。
- 当失败的应用再次变得可用时,线程池将清理并可立即恢复,而不需要一个长时间的恢复。
- 独立的线程池提高了并发性。
缺点:线程池隔离的主要缺点是它们增加计算开销(CPU)。每个命令的执行涉及到排队、调度和上 下文切换都是在一个单独的线程上运行的。
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
public class OrderHystrixCommand extends HystrixCommand<JSONObject> {
@Autowired
private MemberService memberService;
/**
* @param group
*/
public OrderHystrixCommand(MemberService memberService) {
super(setter());
this.memberService = memberService;
}
protected JSONObject run() throws Exception {
JSONObject member = memberService.getMember();
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
return member;
}
private static Setter setter() {
// 服务分组
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members");
// 服务标识
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("member");
// 线程池名称
HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("member-pool");
// #####################################################
// 线程池配置 线程池大小为10,线程存活时间15秒 队列等待的阈值为100,超过100执行拒绝策略
HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter().withCoreSize(10)
.withKeepAliveTimeMinutes(15).withQueueSizeRejectionThreshold(100);
// ########################################################
// 命令属性配置Hystrix 开启超时
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
// 采用线程池方式实现服务隔离
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
// 禁止
.withExecutionTimeoutEnabled(false);
return HystrixCommand.Setter.withGroupKey(groupKey).andCommandKey(commandKey).andThreadPoolKey(threadPoolKey)
.andThreadPoolPropertiesDefaults(threadPoolProperties).andCommandPropertiesDefaults(commandProperties);
}
@Override
protected JSONObject getFallback() {
// 如果Hystrix发生熔断,当前服务不可用,直接执行Fallback方法
System.out.println("系统错误!");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误!");
return jsonObject;
}
}
信号量
使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。
与线程池隔离最大不同在于执行依赖代码的线程依然是请求线程
tips:信号量的大小可以动态调整, 线程池大小不可以
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
public class OrderHystrixCommand2 extends HystrixCommand {
@Autowired
private MemberService memberService;
/**
* @param group
*/
public OrderHystrixCommand2(MemberService memberService) {
super(setter());
this.memberService = memberService;
}
protected JSONObject run() throws Exception {
// Thread.sleep(500);
// System.out.println("orderIndex线程名称" +
// Thread.currentThread().getName());
// System.out.println("success");
JSONObject member = memberService.getMember();
System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
return member;
}
private static Setter setter() {
// 服务分组
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("members");
// 命令属性配置 采用信号量模式
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
// 使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数
// 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。
.withExecutionIsolationSemaphoreMaxConcurrentRequests(50);
return HystrixCommand.Setter.withGroupKey(groupKey).andCommandPropertiesDefaults(commandProperties);
}
@Override
protected JSONObject getFallback() {
// 如果Hystrix发生熔断,当前服务不可用,直接执行Fallback方法
System.out.println("系统错误!");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误!");
return jsonObject;
}
}
服务限流—令牌桶/漏斗/滑动计数器
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx)
限流算法
常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。
计数器
它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果
该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class LimitService {
private int limtCount = 60;// 限制最大访问的容量
AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒钟 实际请求的数量
private long start = System.currentTimeMillis();// 获取当前系统时间
private int interval = 60;// 间隔时间60秒
public boolean acquire() {
long newTime = System.currentTimeMillis();
if (newTime > (start + interval)) {
// 判断是否是一个周期
start = newTime;
atomicInteger.set(0); // 清理为0
return true;
}
atomicInteger.incrementAndGet();// i++;
return atomicInteger.get() <= limtCount;
}
static LimitService limitService = new LimitService();
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i < 100; i++) {
final int tempI = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
if (limitService.acquire()) {
System.out.println("你没有被限流,可以正常访问逻辑 i:" + tempI);
} else {
System.out.println("你已经被限流呢 i:" + tempI);
}
}
});
}
}
}
滑动窗口计数
滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。
令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
使用RateLimiter实现令牌桶限流
RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。
通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
/**
* 功能说明:使用RateLimiter 实现令牌桶算法
*
*/
@RestController
public class IndexController {
@Autowired
private OrderService orderService;
// 解释:1.0 表示 每秒中生成1个令牌存放在桶中
RateLimiter rateLimiter = RateLimiter.create(1.0);
// 下单请求
@RequestMapping("/order")
public String order() {
// 1.限流判断
// 如果在500秒内 没有获取不到令牌的话,则会一直等待
System.out.println("生成令牌等待时间:" + rateLimiter.acquire());
boolean acquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!acquire) {
System.out.println("你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!");
return "你在怎么抢,也抢不到,因为会一直等待的,你先放弃吧!";
}
// 2.如果没有达到限流的要求,直接调用订单接口
boolean isOrderAdd = orderService.addOrder();
if (isOrderAdd) {
return "恭喜您,抢购成功!";
}
return "抢购失败!";
}
}
漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:
- 一个固定容量的漏桶,按照常量固定速率流出水滴;
- 如果桶是空的,则不需流出水滴;
- 可以以任意速率流入水滴到漏桶;
- 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶和漏桶对比:
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
- 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
- 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
另外有时候我们还使用计数器来进行限流,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;只要全局总请求数或者一定时间段的总请求数设定的阀值则进行限流,是简单粗暴的总数量限流,而不是平均速率限流。
一个固定的漏桶,以常量固定的速率流出水滴。
如果桶中没有水滴的话,则不会流出水滴。
如果流入的水滴超过桶中的流量,则流入的水滴可能会发生溢出,溢出的水滴请求是无法访问的,直接调用服务降级方法,桶中的容量是不会发生变化。
漏桶算法与令牌桶算法区别
主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。
封装RateLimiter
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtRateLimiter {
double value();
long timeOut();
}
@Aspect
@Component
public class RateLimiterAop {
// 存放接口是否已经存在
private static ConcurrentHashMap<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<String, RateLimiter>();
@Pointcut("execution(public * com.itmayeidu.api.*.*(..))")
public void rlAop() {
}
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
// 使用Java反射技术获取方法上是否有@ExtRateLimiter注解类
ExtRateLimiter extRateLimiter = signature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class);
if (extRateLimiter == null) {
// 正常执行方法
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
// ############获取注解上的参数 配置固定速率 ###############
// 获取配置的速率
double value = extRateLimiter.value();
// 获取等待令牌等待时间
long timeOut = extRateLimiter.timeOut();
RateLimiter rateLimiter = getRateLimiter(value, timeOut);
// 判断令牌桶获取token 是否超时
boolean tryAcquire = rateLimiter.tryAcquire(timeOut, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
serviceDowng();
return null;
}
// 获取到令牌,直接执行..
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
// 获取RateLimiter对象
private RateLimiter getRateLimiter(double value, long timeOut) {
// 获取当前URL
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
RateLimiter rateLimiter = null;
if (!rateLimiterMap.containsKey(requestURI)) {
// 开启令牌通限流
rateLimiter = RateLimiter.create(value); // 独立线程
rateLimiterMap.put(requestURI, rateLimiter);
} else {
rateLimiter = rateLimiterMap.get(requestURI);
}
return rateLimiter;
}
// 服务降级
private void serviceDowng() throws IOException {
// 执行服务降级处理
System.out.println("执行降级方法,亲,服务器忙!请稍后重试!");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
writer.println("执行降级方法,亲,服务器忙!请稍后重试!");
} catch (Exception e) {
} finally {
writer.close();
}
}
public static void main(String[] args) {
// 使用Java反射技术获取方法上是否有@ExtRateLimiter注解类
ExtRateLimiter extRateLimiter = IndexController.class.getClass().getAnnotation(ExtRateLimiter.class);
System.out.println(extRateLimiter);
}
}
测试
@RequestMapping("/myOrder")
@ExtRateLimiter(value = 10.0, timeOut = 500)
public String myOrder() throws InterruptedException {
System.out.println("myOrder");
return "SUCCESS";
}
应用级限流
1.1.1. 限流总并发/连接/请求数
对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。
如果你使用过Tomcat,其Connector其中一种配置有如下几个参数:
acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
maxConnections:瞬时最大连接数,超出的会排队等待;
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。
详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。
1.1.2. 限流总资源数
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常。
1.1.3. 限流某个接口的总并发/请求数
如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发/请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong进行限流:
适合对业务无损的服务或者需要过载保护的服务进行限流,如抢购业务,超出了大小要么让用户排队,要么告诉用户没货了,对用户来说是可以接受的。而一些开放平台也会限制用户调用某个接口的试用请求量,也可以用这种计数器方式实现。这种方式也是简单粗暴的限流,没有平滑处理,需要根据实际情况选择使用;
1.1.4. 限流某个接口的时间窗请求数
即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速;一种实现方式如下所示:
1.1.5. 平滑限流某个接口的请求数
之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如5r/s,则每隔200毫秒处理一个请求,平滑了速率)。这个时候有两种算法满足我们的场景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法实现,可直接拿来使用。
Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
接入层限流
接入层通常指请求流量的入口,该层的主要目的有:负载均衡、非法请求过滤、请求聚合、缓存、降级、限流、A/B测试、服务质量监控等等,可以参考笔者写的《使用Nginx+Lua(OpenResty)开发高性能Web应用》。
对于Nginx接入层限流可以使用Nginx自带了两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。还可以使用OpenResty提供的Lua限流模块lua-resty-limit-traffic进行更复杂的限流场景。
limit_conn用来对某个KEY对应的总的网络连接数进行限流,可以按照如IP、域名维度进行限流。limit_req用来对某个KEY对应的请求的平均速率进行限流,并有两种用法:平滑模式(delay)和允许突发模式(nodelay)。
ngx_http_limit_conn_module
limit_conn是对某个KEY对应的总的网络连接数进行限流。可以按照IP来限制IP维度的总连接数,或者按照服务域名来限制某个域名的总连接数。但是记住不是每一个请求连接都会被计数器统计,只有那些被Nginx处理的且已经读取了整个请求头的请求连接才会被计数器统计。