•什么是Sentinel
Sentinel 是一款由阿里开发的面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助我们保护服务的稳定性。Sentinel的使用分为两部分:
1、sentinel-dashboard:与hystrix-dashboard类似,但是它更为强大一些。除了与hystrix-dashboard一样提供实时监控之外,还提供了流控规则、熔断规则的在线维护等功能。
2、客户端整合:每个微服务客户端都需要整合sentinel的客户端封装与配置,才能将监控信息上报给dashboard展示以及实时的更改限流或熔断规则等。
Sentinel是阿里开源的一款熔断器的实现,目前在Spring Cloud的孵化器项目Spring Cloud Alibaba中的一员Sentinel本身在阿里内部已经被大规模采用,非常稳定。因此,可以作为一个很好的替代品。(服务熔断Hystrix的替换方案)
•Sentinel的作用
1.丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
2.完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
3.完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
•Sentinel的图形化界面
下载jar包:
https://github.com/alibaba/Sentinel/releases/download/1.7.1/sentinel-dashboard-1.7.1.jar
启动命令:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080
-Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar
Sentinel的登录界面(访问地址默认是 http://localhost:8080/ ,用户名和密码为sentinel/sentinel):
先创建一个工程,让sentinel可以检测到
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.csp
sentinel-web-servlet
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
${spring-boot.version}
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
${lombok.version}
compile
org.springframework.boot
spring-boot-maven-plugin
${spring-boot.version}
application.yml:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动类:
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401
{
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
业务类controller:
@RestController
public class FlowLimitController
{
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
启动测试
测试工程
可以看到sentinel已经对8401的工程进行监控
•Sentinel的限流算法
流控规则
1.资源名: 唯一名称,默认请求路径,表示对该资源进行流控
2. 针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
3. 阈值类型/单击阈值:
QPS(每秒钟的请求数量):当调用该api的QPS达到阈值时,进行限流
线程数:当调用该线程数达到阈值的时候,进行限流
4.是否集群:不需要集群
5.流控模式:
直接: api达到限流条件时,直接限流
关联: 当关联的资源达到阈值时,就限流自己
链路: 只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
6.流控效果:
快速失败: 直接失败,抛异常
Warm Up: 根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFctor,经过预热时长,才达到设置的QPS阈值
排队等待: 匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
eg:
第二次访问被限流
•滑动窗口
滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求,滑动窗口在固定窗口的基础上,将一个窗口分为若干个等份的小窗口,每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阀值。
将原来的一个时间窗口划分成多个时间窗口,并且不断向右滑动该窗口。流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。对比固定时间窗口限流算法,滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。 在临界位置的突发请求都会被算到时间窗口内。
滑动窗口的作用
为了解决计数器限流方式中在窗口切换时产生2倍于阈值的缺点。
适用场景
例如每秒限制 100 个请求。希望请求每 10ms 来一个,这样我们的流量处理就很平滑,但是真实场景很难控制请求的频率。因此可能存在 5ms 内就打满了阈值的情况。因此时间窗口可以解决计数器算法的临界问题。
代码实现
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 滑动窗口可以理解为细分之后的计数器,
* 计数器粗暴的限定1分钟内的访问次数,
* 而滑动窗口限流将1分钟拆为多个段,
* 不但要求整个1分钟内请求数小于上限,
* 而且要求每个片段请求数也要小于上限。
* 相当于将原来的计数周期做了多个片段拆分。更为精细。
* 定时器每一秒滑动一步,滑动窗口LinkList,当前时间在Last节点,假如当前时间为T秒,
* 那么前面为T-1,T-2
* 用map记录每片的计数,有几片就有几个key值,T-2=1;T-2=2;T=2
*/
public class WindowLmt {
//整个窗口的流量上限,超出会被限流
final int totalMax = 5;
//每片的流量上限,超出同样会被拒绝,可以设置不同的值
final int sliceMax = 5;
//分多少片
final int slice = 3;
//窗口,分3段,每段1s,也就是总长度3s
final LinkedList linkedList = new LinkedList();
//计数器,每片一个key,可以使用HashMap,这里为了控制台保持有序性和可读性,采用TreeMap
Map map = new TreeMap();
//心跳,每1s跳动1次,滑动窗口向前滑动一步,实际业务中可能需要手动控制滑动窗口的时机。
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
//获取key值,这里即是时间戳(秒)
private Long getKey(){
return System.currentTimeMillis()/1000;
}
public WindowLmt(){
//初始化窗口,当前时间指向的是最末端,前两片其实是过去的2s
Long key = getKey();
for (int i = 0; i < slice; i++) {
linkedList.addFirst(key-i);
map.put(key-i,new AtomicInteger(0));
}
//启动心跳任务,窗口根据时间,自动向前滑动,每秒1步
service.scheduleAtFixedRate(new Runnable() {
public void run() {
Long key = getKey();
//队尾添加最新的片
linkedList.addLast(key);
map.put(key,new AtomicInteger());
//将最老的片移除
map.remove(linkedList.getFirst());
linkedList.removeFirst();
System.out.println("step:"+key+":"+map);;
}
},1000,1000,TimeUnit.MILLISECONDS);
}
//检查当前时间所在的片是否达到上限
public boolean checkCurrentSlice(){
long key = getKey();
AtomicInteger integer = map.get(key);
if (integer != null){
return integer.get() < sliceMax ;
}
//默认允许访问
return true;
}
//检查整个窗口所有片的计数之和是否达到上限
public boolean checkAllCount(){
return map.values().stream().mapToInt(value -> value.get()).sum() value.get()).sum() < totalMax;
}
//请求来临....
public void req(){
Long key = getKey();
//如果时间窗口未到达当前时间片,稍微等待一下
//其实是一个保护措施,放置心跳对滑动窗口的推动滞后于当前请求
while (linkedList.getLast()
•令牌桶
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限。
适用场景
如果要让自己的系统不被打垮,用令牌桶。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候令牌桶算法就很适合。
代码实现
import com.cvnavi.network.NetUsage;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
//令牌桶
class TokenLimiterUtils {
//饿汉单例,私有化构造,无法通过new实例创建
private TokenLimiterUtils() {
}
private static ConcurrentHashMap limiter = new ConcurrentHashMap<>();
//尝试获取令牌
public static boolean tryAcquire(String topic) {
synchronized (topic) {
if (limiter.get(topic) == null) {
limiter.put(topic, new TokenLimiter(topic,80,500));
}
return limiter.get(topic).tryAcquire();
}
}
//内部内定义
private static class TokenLimiter {
//线程安全数组阻塞队列
private static ArrayBlockingQueue blockingQueue;
//容量大小
private int limit;
//令牌的产生间隔
private int period;
private String topic;
public TokenLimiter(String topic,int limit, int period) {
this.limit = limit;
this.period = period;
this.topic = topic;
blockingQueue = new ArrayBlockingQueue<>(limit);
Object lock = new Object();
init(lock);
//让线程先产生2个令牌(溢出)
start(lock);
}
//默认初始化令牌
private void init(Object lock) {
for (int i = 0; i < limit; i++) {
if (blockingQueue.size() >= limit) {
break;
}
//不超过队列容量插入队列尾部
blockingQueue.add(topic);
}
}
//添加令牌
private void addToken(int amount) {
for (int i = 0; i < amount; i++) {
//溢出返回false
blockingQueue.offer(topic);
}
}
//获取令牌
public boolean tryAcquire() {
//队首元素出队, 获取并移除此队列的头,如果此队列为空,则返回 null,等待空间变有用
return blockingQueue.poll() != null ? true : false;
}
//生产令牌
private void start(Object lock) {
//定时任务 启动生产令牌
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
if (blockingQueue.size() < limit) {
synchronized (lock) {
//最大值填充令牌,size()包含的元素数,计算队列容量和队列大小之间的差异来查看队列包含多少元素以及可以向此队列添加多少元素。
addToken(limit - blockingQueue.size());
lock.notify();
}
}
}, 500, this.period, TimeUnit.MILLISECONDS);
}
}
public static void main(String[] args) throws InterruptedException {
//模拟洪峰5个请求,前3个迅速响应,后两个排队
new Thread(() -> {
for (int i = 0; i < 1200; i++) {
int finalI = i;
new Thread(() -> {
System.out.println("洪峰:" + finalI + "[获取令牌]" + TokenLimiterUtils.tryAcquire("topicname1"));
}).start();
if (i % 80 == 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
//模拟洪峰5个请求,前3个迅速响应,后两个排队
//模拟洪峰5个请求,前3个迅速响应,后两个排队
new Thread(() -> {
for (int i = 0; i < 1200; i++) {
int finalI = i;
new Thread(() -> {
System.err.println("洪峰测试2:" + finalI + "[获取令牌 topic2]" + TokenLimiterUtils.tryAcquire("topic2"));
}).start();
if (i % 80 == 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
•漏桶
漏桶(Leaky Bucket)可以看作是一个带有常量服务时间的单服务器队列 。算法思路简单,水(请求)先进入到漏桶中,漏桶以一定的速度出水(接口有响应速率),当水流速度过大桶满了,直接溢出(访问频率超过接口响应频率),然后就拒绝请求。
漏桶的作用
可以看出漏桶算法能强制限制数据的传输速率,很好的控制流量的访问速度,超过速度就拒绝服务;对流量进行整形。
漏桶算法优缺点
优点:
保证别人的系统不被打垮,将系统的处理能力维持在一个比较平稳的水平,平滑访问。
缺点:
因为当流出速度固定,大规模持续突发量,无法多余处理,浪费网络带宽
代码实现
import com.google.common.util.concurrent.Monitor;
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
import static java.lang.Thread.currentThread;
/**
* @create 2022-06-18 11:36
* @desc 漏桶算法的应用
**/
public class RateLimiterBucket{
//漏桶采用线程安全的容器
private final ConcurrentLinkedQueue bucket = new ConcurrentLinkedQueue<>();
//定义漏桶的上沿容量
private final static long BUCKET_CAPACITY = 100*1024*1024;
//定义漏桶的下沿水流速率,每秒匀速放行 5 个 Request
private final RateLimiter rateLimiter = RateLimiter.create(5.0D);
//提交请求时需要用到的 Monitor
private final Monitor requestMonitor = new Monitor();
//处理请求时需要用到的 Monitor
private final Monitor handleMonitor = new Monitor();
public void submitRequest(int data) {
this.submitRequest(new Request(data));
}
// 该方法主要用于接受来自客户端提交的请求数据
public void submitRequest(Request request) {
/**
* enterIf() 方法: 主要用于判断当前的 Guard 是否满足临界值的判断,
* 也是使用比较多的一个操作,调用该方法,当前线程
* 并不会进入阻塞之中
//
if (requestMonitor.enterIf(new Monitor.Guard(requestMonitor) {
@Override
public boolean isSatisfied() {
return bucket.size() < BUCKET_CAPACITY;
}
})) {
try {
// 向桶中加入新的 request
boolean result = bucket.offer(request);
if (result) {
System.out.println(currentThread() + " 提交请求 : " + request.getData() +
" 成功.");
} else {
// 此处可以将请求数据存入“高吞吐量的 MQ 中”,然后从 MQ 中消费请求,再尝试提交
System.out.println("生成到 MQ 中,稍后再试.");
}
} finally {
requestMonitor.leave();
}
} else {
// 当漏桶溢出的时候做“降权”处理
System.out.println("请求:" + request.getData() + "由于桶溢出将被降权处理");
// 此处可以将请求数据存入“高吞吐量的 MQ 中”,然后从 MQ 中消费请求,再尝试提交
//System.out.println("produce into MQ and will try again later.");
}
}
// 该方法主要从漏桶中匀速地处理相关请求
public void handleRequest(Consumer consumer) {
// 若漏桶中存在请求,则处理
if (handleMonitor.enterIf(new Monitor.Guard(handleMonitor) {
@Override
public boolean isSatisfied() {
return !bucket.isEmpty();
}
})) {
try {
// 匀速处理
rateLimiter.acquire();
// 处理数据
consumer.accept(bucket.poll());
} finally {
handleMonitor.leave();
}
}
}
}
public class Request {
private long data;
private String topic;
get/set/tostring
}
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @create 2022-06-18 15:15
* @desc 测试自定义的 RateLimiterBucket
**/
public class Test
{
private static final AtomicInteger data = new AtomicInteger(0);
private static final RateLimiterBucket bucket = new RateLimiterBucket();
public static void main(String[] args)
{
//启动 10 个线程模拟高并发的业务请求
for(int i = 0 ; i < 20 ; i++)
{
new Thread(
()->
{
while(true)
{
bucket.submitRequest(data.getAndIncrement());
try
{
TimeUnit.SECONDS.sleep(3);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
).start();
}
//启动 10 个线程模拟匀速地对漏桶中的请求进行处理
for(int i = 0 ; i < 10 ; i++)
{
new Thread(
()->
{
while(true)
{
bucket.handleRequest(System.out::println);
}
}
).start();
}
}
}
•Sentinel完成对接口的限流