共计 7311 个字符,预计需要花费 19 分钟才能阅读完成。
停更了很久的《面试补习》,随着最近的校招降临,也要提上日程了,在梳理八股文的同时,也能加深本人的了解,心愿对各位童鞋有所帮忙~
概述
在最近一期的文章 给几位小朋友面试辅导后,我发现了一些问题!中,有提到面试中,真的童鞋们的我的项目教训提出了比拟多的问题,也不晓得有没有人看 orz
次要列了一下我的项目中的这些问题:
- 去了解为什么你要做秒杀零碎?
- 秒杀零碎适宜什么场景,不适宜什么场景
- 思考你的零碎还有哪些欠缺的中央
- 把握你零碎的每一个点,包含性能,性能,数据流和部署架构
- 技术选型,为什么你要用 redis,为什么要用 MQ?
- 技术危险,援用了这些中间件,对你的零碎带来的收益和危险
- 怎么去容灾,怎么监控
明天写的这片对于限流文章,也是属于秒杀零碎中的一个关键技术点. 会从:技术原理,技术选型,应用场景等多方面来介绍,让你在面试中,肆意施展。
什么是限流
讲一个大家都懂的例子:三峡大坝排水
- 三峡水库的存水:能够了解是咱们秒杀流动的用户
- 放闸:流动开始
- 排水:秒杀胜利的用户
如果没有 闸口 在,受到的影响是啥?上游的村庄禁受洪水劫难,而对应你的零碎也是一样的解体!
可能大家有疑难,如果我没有做这个蓄水的动作(三峡没有那么多水),我是不是就不须要做限流了呢?其实不然,咱们都晓得 三峡解决了多少历史上造成的洪灾问题,这里找了个科普链接。
那对应到咱们的秒杀零碎上,咱们怎么晓得咱们的零碎会在哪个工夫点来一波用户暴增呢?如果这时候你没做好筹备,是不是就造成了这批用户的散失?而且零碎瘫痪,对存量用户也有影响。双输
我要这铁棒有何用~
所以,限流就是咱们零碎的定海神针, 让咱们的零碎惊涛骇浪。
最初再以一批数据来阐明一下限流的理论场景:
1 个商品
1 秒内
100 个名额
5000 个用户
1000 个进入下单页面
4000 个超时页面
100 个下单
900 个库存有余
后果:100 个胜利下单
4900 个抢单失败
限流量:1000
思考题
求:我这个服务最大并发量多少?
怎么限流
简略画了个调用链路
H5/ 客户端 -> Nginx -> Tomcat -> 秒杀零碎 -> DB
简略梳理为
- 网关限流
- Nginx 限流
- Tomcat 限流
- 服务端限流
- 单机限流
- 分布式限流
网关限流
Nginx 限流
Nginx 自带了两个限流模块:
- 连接数限流模块 ngx_http_limit_conn_module
- 漏桶算法实现的申请限流模块 ngx_http_limit_req_module
1、ngx_http_limit_conn_module
次要用于限度脚本攻打,如果咱们的秒杀流动开始,一个黑客 (伪装有,毕竟咱们的零碎要做大做强!) 写了脚本来攻打,会造成咱们带宽被节约,大量有效申请产生,对于这类申请,咱们能够通过对 ip 的连接数进行限度。
咱们能够在 nginx_conf 的 http{}中加上如下配置实现限度:
# 限度每个用户的并发连接数,取名 one
limit_conn_zone $binary_remote_addr zone=one:10m;
#配置异样日志,和状态码
limit_conn_log_level error;
limit_conn_status 503;
# 在 server{} 限度用户并发连接数为 1
limit_conn one 1;
2、ngx_http_limit_req_module
下面说的 是 ip 的连接数,那么如果咱们要管制申请数呢?限度的办法是通过应用漏斗算法,每秒固定解决申请数,推延过多申请。如果申请的频率超过了限度域配置的值,申请解决会被提早或被抛弃,所以所有的申请都是以定义的频率被解决的。
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
#设置每个 IP 桶的数量为 5
limit_req zone=one burst=5;
3、怎么了解 连接数,申请数限流
- 连接数限流(ngx_http_limit_conn_module)每个 IP,咱们只会接待一个,只有当这个 IP 解决完结了,我才会接待下一位。(单位工夫内,只有一个连贯在解决)
有滋味的解读:厕所(IP)限度只有一个坑了,只有当我上完了,能力下一个人上。
- 申请数限流(ngx_http_limit_req_module)通过 漏桶算法,依照单位工夫放行申请,也不论你服务器能不能解决完,我就放,哎,就是放!
有滋味的解读:厕所有五个坑,我一分钟放 5 集体进去,下一分钟再放 5 集体进去。外面可能有 5 集体,也可能有 10 集体,我也不分明。
4、怎么抉择?
可能面试官在听到你对 nginx 的限流那么理解后,会问你在什么状况下应用哪种限流策略
- IP 限流:能够在流动开始前进行配置,也能够用于预防脚本攻打(IP 代理的状况另说)
- 申请数限流:日程能够配置,爱护咱们的服务器在突发流量造成的解体
漏桶算法
漏桶算法的次要概念如下:
- 一个固定容量的漏桶,依照常量固定速率流出水滴;
- 如果桶是空的,则不需流出水滴;
- 能够以任意速率流入水滴到漏桶;
- 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被抛弃),而漏桶容量是不变的。
Tomcat 限流
这个其实不太好用,然而也理解一下吧~
可能当初的童鞋,对 Tomcat 也不太理解了,毕竟 SpringBoot 外面封装了 Tomcat,让开发者越来越懈怠了, 然而人类进化,根本原因就是懒,所以也未尝不是一件坏事。
在 Tomcat 的配置文件中, 有一个 maxThreads
<Connector port="8080" connectionTimeout="30000" protocol="HTTP/1.1"
maxThreads="1000" redirectPort="8000" />
这个如同没啥好介绍的了,如果你碰到你压测的时候,并发上不去,能够检查一下这个配置。
之前面试的时候,面试官有问过我 Tomcat 的问题:
Tomcat 默认最大连接数是多少?你们服务器的线程数设置了多少?线程占用内存是多少?
总结
联合咱们的 秒杀零碎,那么在介绍咱们零碎的时候,咱们能够说,在限流这块,从网关角度,咱们能够应用了 Nginx 的 ngx_http_limit_conn_module 模块,针对 IP 在单位工夫内只容许一个申请,防止用户屡次申请,加重服务的压力。在进入到订单界面后,在单位工夫内,会产生屡次申请,能够应用 ngx_http_limit_req_module 模块,针对申请数做限流,防止因为 IP 限度,导致订单失落。
除此之外,在服务上线前,咱们针对服务器进行了最大并发的压测(如 200 并发),因而在 Tomcat 容许的最大申请中,设置为(300,略微上调,有其余申请)。
服务器限流
单机限流
如果咱们的零碎部署,是只有一台机器,那咱们能够间接应用 单机限流的计划(毕竟你一台机器还要用分布式限流,是不是有点过了~)
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
实例代码
public static void main(String[] args) throws InterruptedException {
// 每秒产生 1 个令牌
RateLimiter rt = RateLimiter.create(1, 1, TimeUnit.SECONDS);
System.out.println("try acquire token:" + rt.tryAcquire(1) + "time:" + System.currentTimeMillis());
System.out.println("try acquire token:" + rt.tryAcquire(1) + "time:" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("try acquire token:" + rt.tryAcquire(1) + "time:" + System.currentTimeMillis());
System.out.println("try acquire token:" + rt.tryAcquire(1) + "time:" + System.currentTimeMillis());
System.out.println("------------- 分隔符 -----------------");
}
RateLimiter.tryAcquire() 和 RateLimiter.acquire() 两个办法都通过限流器获取令牌,
1、tryAcquire
反对传入等待时间, 通过 canAcquire 判断最早一个生成令牌工夫,判断是否进行期待下一个令牌的获取。
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);
private boolean canAcquire(long nowMicros, long timeoutMicros) {return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
示例代码:
public static void main(String[] args) throws InterruptedException {
// 每秒产生 1 个令牌
RateLimiter rt = RateLimiter.create(1, 3, TimeUnit.SECONDS);
System.out.println("try acquire token:" + rt.tryAcquire(1,TimeUnit.SECONDS) + "time:" + System.currentTimeMillis());
System.out.println("try acquire token:" + rt.tryAcquire(5,TimeUnit.SECONDS) + "time:" + System.currentTimeMillis());
Thread.sleep(10000);
System.out.println("------------- 分隔符 -----------------");
System.out.println("try acquire token:" + rt.tryAcquire(1,TimeUnit.SECONDS) + "time:" + System.currentTimeMillis());
System.out.println("try acquire token:" + rt.tryAcquire(1,TimeUnit.SECONDS) + "time:" + System.currentTimeMillis());
}
输入后果:
2、acquire
acquire 为阻塞期待获取令牌,通过查看源码能够看出同步加锁操作:
示例代码:
RateLimiter rt = RateLimiter.create(1);
// 每秒产生 1 个令牌
for (int i = 0; i < 11; i++) {new Thread(() -> {
// 获取 1 个令牌
rt.acquire();
System.out.println("try acquire token success,time:" +System.currentTimeMillis() + "ThreaName:"+Thread.currentThread().getName());
}).start();}
输入后果:
令牌算法
下面说到了几个概念,在 nignx 咱们提到的是 漏斗算法, 在 RateLimiter 这里咱们提到的是令牌算法
咱们能够通过下面这个图来进行解释,有一个容量无限的桶,令牌以固定的速率增加到这个桶外面。因为桶的容量是无限的,所以不可能无限度的往里面增加令牌,如果令牌达到桶的时候,桶是满的,那么这个令牌就被抛弃了。每次申请,n 个数量的令牌从桶外面被移除,如果桶外面的令牌数少于 n,那么该申请就会被回绝或阻塞。
这里有几个要害的属性
/** The currently stored permits. */
double storedPermits; // 目前令牌数量
/** The maximum number of stored permits. */
double maxPermits; // 最大令牌数量
private long nextFreeTicketMicros = 0L; // 下一个令牌获取工夫
在获取令牌前,会有一个判断规定, 判断以后获取令牌工夫,是否满足上一次令牌工夫获取 – 生产令牌工夫,
比方:我这次获取令牌工夫为 100 秒,令牌生成工夫为 10 秒 一个,那么当我 105 秒过去拿的时候,不论令牌桶有没有令牌,我都没方法获取到令牌。
private boolean canAcquire(long nowMicros, long timeoutMicros) {return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
这里是重点!!!
那么令牌桶当中的令牌数量(存量)到底有什么用呢?针对不同的申请,咱们能够设定须要不同数量的令牌,优先级高的,只须要 1 个令牌即可;优先级低的,则须要多个令牌。那么当获取令牌工夫到了之后,进行下一层判断,令牌数是否足够,优先级高的申请(须要令牌数量比拟少的),能够马上放行!!!!!
在 RateLimit 中刷新令牌的算法:
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
集群限流
随着咱们秒杀零碎做大做强,一台机器必定不能满足咱们的诉求了,那么咱们的部署架构就会衍生成为上面这个架构图(简版)
在将集群限流前,提个思考问题:
集群部署咱们就不能用单机部署的计划了吗?
答案必定是能够的,咱们能够将单机限流 的计划拓展到集群每一台机器,那么每天机器都是复用了雷同的一套限流代码(RateLimit 实现)。
那么这个计划存在什么问题呢?
- 流量调配不均
- 误限,错限
- 更新不及时
次要讲一下 误限 , 咱们服务端接管到的申请,都是有 nginx 进行散发,如果某个时间段,因为申请的调配不均(60,30,10 比例调配,限流 50qps),会触发第一台机器的限流,而对于集群而言,我的整体限流阀值为 150 qps, 当初 100qps 就限流了,那必定不行哇!
Redis 实现
参考文档:https://juejin.cn/post/6844904161604009997
咱们能够借助 Redis 的有序汇合 ZSet 来实现工夫窗口算法限流,实现的过程是先应用 ZSet 的 key 存储限流的 ID,score 用来存储申请的工夫,每次有申请拜访来了之后,先清空之前工夫窗口的访问量,统计当初工夫窗口的个数和最大容许访问量比照,如果大于等于最大访问量则返回 false 执行限流操作,负责容许执行业务逻辑,并且在 ZSet 中增加一条无效的拜访记录。
此实现形式存在的毛病有两个:
- 应用 ZSet 存储有每次的拜访记录,如果数据量比拟大时会占用大量的空间,比方 60s 容许 100W 拜访时;
- 此代码的执行非原子操作,先判断后减少,两头空隙可交叉其余业务逻辑的执行,最终导致后果不精确。
限流中间件
Sentinel 是阿里中间件团队研发的面向分布式服务架构的轻量级高可用流量管制组件,次要以流量为切入点,从流量管制、熔断降级、零碎负载爱护等多个维度来帮忙用户爱护服务的稳定性。
限流中间件的原理是在太有货色了,我这里简略裂了一下他们之间的一些区别,后续会独自写一篇文章来分享 Sentinel 的实现原理!目前能够比拟容易了解的就是,底层是基于滑动窗口的形式实现
滑动窗口算法
在 Sentinel 和 Hystrix 的底层实现,都是采纳了滑动窗口, 这里接简略来形容一下什么是滑动窗口,在 1S 内,我容许通过 5 个申请,别离处于 0~200ms,200~400ms 以此类推, 当工夫点来到 1.2s 的时候,咱们的工夫区间变成了 200ms ~ 1200ms。那么第一个申请,就不在统计的区间范畴内了,咱们目前总的 申请数为 4, 因而可能再承受一个新的申请进来解决!
总结
想闲扯一下,在我画的那张图中,我列出了 Hystrix(豪猪),Sentinel(哨兵)和蚂蚁内源的 Guardian(守卫)。他们都有一个共性: 爱护。豪猪有坚挺的刺爱护柔软的身材,哨兵和守卫则爱护着身后的家人。
当面试官问你为什么要应用限流的时候,你应该第一反馈就是爱护零碎,爱护零碎不受伤害!这才是你为什么要用到限流的各种策略的根本原因。
在探讨到高可用的时候,咱们会想到,削峰,限流和熔断。他们的指标都是为了爱护咱们的零碎,晋升零碎的可用率,咱们常说的零碎可用率 几个 9, 这些数据都是由各种高可用的策略来爱护的。
后续的打算:– 熔断,联合 Sentinel 的原理来介绍一下,秒杀零碎应用熔断的场景 – 削峰,联合 RocketMQ 讲一下,削峰的优缺点,引入 MQ 带来的老本和危险
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,我前面会每周都更新几篇高质量的大厂面试和罕用技术栈相干的文章。感激大伙能看到这里,如果这个文章写得还不错,求三连!!!创作不易,感激各位的反对和认可,咱们下篇文章见!
我是 九灵 , 有须要交换的童鞋能够 加我 wx,Jayce-K, 关注公众号:Java 补习课,把握第一手材料!
如果本篇博客有任何谬误,请批评指教,不胜感激!