背景
在今天,基于SOA的架构已经大行其道。伴随着架构的SOA化,相关联的服务熔断、降级、限流等思想,也在各种技术讲座中频繁出现。本文将结合Netflix开源的Hystrix框架,对这些思想做一个梳理。
伴随着业务复杂性的提高,系统的不断拆分,一个面向用户端的API,其内部的RPC调用层层嵌套,调用链条可能会非常长。这会造成以下几个问题:
API接口可用性降低
引用Hystrix官方的一个例子,假设tomcat对外提供的一个application,其内部依赖了30个服务,每个服务的可用性都很高,为99.99%。那整个applicatiion的可用性就是:99.99%的30次方 = 99.7%,即0.3%的失败率。
这也就意味着,每1亿个请求,有30万个失败;按时间来算,就是每个月的故障时间超过2小时。

服务熔断
为了解决上述问题,服务熔断的思想被提出来。类似现实世界中的“保险丝“,当某个异常条件被触发,直接熔断整个服务,而不是一直等到此服务超时。
熔断的触发条件可以依据不同的场景有所不同,比如统计一个时间窗口内失败的调用次数。
服务降级
有了熔断,就得有降级。所谓降级,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。
这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景。
关于Hystrix中fallback的使用,此处不详述,参见官网。
项目搭建
需求:搭建一套分布式rpc远程通讯案例:比如订单服务调用会员服务实现服务隔离,防止雪崩效应案例

订单工程

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
	<dependency>
		<groupId>org.apache.httpcomponents</groupId>
		<artifactId>httpclient</artifactId>
	</dependency>
	<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.47</version>
	</dependency>
	<dependency>
		<groupId>com.netflix.hystrix</groupId>
		<artifactId>hystrix-metrics-event-stream</artifactId>
		<version>1.5.12</version>
	</dependency>
	<dependency>
		<groupId>com.netflix.hystrix</groupId>
		<artifactId>hystrix-javanica</artifactId>
		<version>1.5.12</version>
	</dependency>
</dependencies>
1234567891011121314151617181920212223242526272829303132

@RestController
@RequestMapping(“/order”)
public class OrderController {
@Autowired
private MemberService memberService;

@RequestMapping("/orderIndex")
public Object orderIndex() throws InterruptedException {
	JSONObject member = memberService.getMember();
	System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
	return member;
}

@RequestMapping("/orderIndexHystrix")
public Object orderIndexHystrix() throws InterruptedException {
	return new OrderHystrixCommand(memberService).execute();
}

@RequestMapping("/orderIndexHystrix2")
public Object orderIndexHystrix2() throws InterruptedException {
	return new OrderHystrixCommand2(memberService).execute();
}

@RequestMapping("/findOrderIndex")
public Object findIndex() {
	System.out.println("当前线程:" + Thread.currentThread().getName() + ",findOrderIndex");
	return "findOrderIndex";
}
12345678910111213141516171819202122

}

@RestController
@RequestMapping(“/order”)
public class OrderController {
@Autowired
private MemberService memberService;

@RequestMapping("/orderIndex")
public Object orderIndex() throws InterruptedException {
	JSONObject member = memberService.getMember();
	System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",订单服务调用会员服务:member:" + member);
	return member;
}

@RequestMapping("/orderIndexHystrix")
public Object orderIndexHystrix() throws InterruptedException {
	return new OrderHystrixCommand(memberService).execute();
}

@RequestMapping("/orderIndexHystrix2")
public Object orderIndexHystrix2() throws InterruptedException {
	return new OrderHystrixCommand2(memberService).execute();
}

@RequestMapping("/findOrderIndex")
public Object findIndex() {
	System.out.println("当前线程:" + Thread.currentThread().getName() + ",findOrderIndex");
	return "findOrderIndex";
}
12345678910111213141516171819202122

}

@Service
public class MemberService {

public JSONObject getMember() {

	JSONObject result = HttpClientUtils.httpGet("http://127.0.0.1:8081/member/memberIndex");
	return result;
}
12345

}

public class HttpClientUtils {
private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); // 日志记录

private static RequestConfig requestConfig = null;

static {
	// 设置请求和传输超时时间
	requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build();
}

/**
 * post请求传输json参数
 * 
 * @param url
 *            url地址
 * @param json
 *            参数
 * @return
 */
public static JSONObject httpPost(String url, JSONObject jsonParam) {
	// post请求返回结果
	CloseableHttpClient httpClient = HttpClients.createDefault();
	JSONObject jsonResult = null;
	HttpPost httpPost = new HttpPost(url);
	// 设置请求和传输超时时间
	httpPost.setConfig(requestConfig);
	try {
		if (null != jsonParam) {
			// 解决中文乱码问题
			StringEntity entity = new StringEntity(jsonParam.toString(), "utf-8");
			entity.setContentEncoding("UTF-8");
			entity.setContentType("application/json");
			httpPost.setEntity(entity);
		}
		CloseableHttpResponse result = httpClient.execute(httpPost);
		// 请求发送成功,并得到响应
		if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
			String str = "";
			try {
				// 读取服务器返回过来的json字符串数据
				str = EntityUtils.toString(result.getEntity(), "utf-8");
				// 把json字符串转换成json对象
				jsonResult = JSONObject.parseObject(str);
			} catch (Exception e) {
				logger.error("post请求提交失败:" + url, e);
			}
		}
	} catch (IOException e) {
		logger.error("post请求提交失败:" + url, e);
	} finally {
		httpPost.releaseConnection();
	}
	return jsonResult;
}

/**
 * post请求传输String参数 例如:name=Jack&sex=1&type=2
 * Content-type:application/x-www-form-urlencoded
 * 
 * @param url
 *            url地址
 * @param strParam
 *            参数
 * @return
 */
public static JSONObject httpPost(String url, String strParam) {
	// post请求返回结果
	CloseableHttpClient httpClient = HttpClients.createDefault();
	JSONObject jsonResult = null;
	HttpPost httpPost = new HttpPost(url);
	httpPost.setConfig(requestConfig);
	try {
		if (null != strParam) {
			// 解决中文乱码问题
			StringEntity entity = new StringEntity(strParam, "utf-8");
			entity.setContentEncoding("UTF-8");
			entity.setContentType("application/x-www-form-urlencoded");
			httpPost.setEntity(entity);
		}
		CloseableHttpResponse result = httpClient.execute(httpPost);
		// 请求发送成功,并得到响应
		if (result.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
			String str = "";
			try {
				// 读取服务器返回过来的json字符串数据
				str = EntityUtils.toString(result.getEntity(), "utf-8");
				// 把json字符串转换成json对象
				jsonResult = JSONObject.parseObject(str);
			} catch (Exception e) {
				logger.error("post请求提交失败:" + url, e);
			}
		}
	} catch (IOException e) {
		logger.error("post请求提交失败:" + url, e);
	} finally {
		httpPost.releaseConnection();
	}
	return jsonResult;
}

/**
 * 发送get请求
 * 
 * @param url
 *            路径
 * @return
 */
public static JSONObject httpGet(String url) {
	// get请求返回结果
	JSONObject jsonResult = null;
	CloseableHttpClient client = HttpClients.createDefault();
	// 发送get请求
	HttpGet request = new HttpGet(url);
	request.setConfig(requestConfig);
	try {
		CloseableHttpResponse response = client.execute(request);

		// 请求发送成功,并得到响应
		if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
			// 读取服务器返回过来的json字符串数据
			HttpEntity entity = response.getEntity();
			String strResult = EntityUtils.toString(entity, "utf-8");
			// 把json字符串转换成json对象
			jsonResult = JSONObject.parseObject(strResult);
		} else {
			logger.error("get请求提交失败:" + url);
		}
	} catch (IOException e) {
		logger.error("get请求提交失败:" + url, e);
	} finally {
		request.releaseConnection();
	}
	return jsonResult;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131

}

会员工程
@RestController
@RequestMapping(“/member”)
public class MemberController {

@RequestMapping("/memberIndex")
public Object memberIndex() throws InterruptedException {
	Map<String, Object> hashMap = new HashMap<String, Object>();
	hashMap.put("code", 200);
	hashMap.put("msg", "memberIndex");
	Thread.sleep(1500);
	return hashMap;
}
12345678

}

Hystrix简介
使用Hystrix实现服务隔离
Hystrix 是一个微服务关于服务保护的框架,是Netflix开源的一款针对分布式系统的延迟和容错解决框架,目的是用来隔离分布式服务故障。它提供线程和信号量隔离,以减少不同服务之间资源竞争带来的相互影响;提供优雅降级机制;提供熔断机制使得服务可以快速失败,而不是一直阻塞等待服务响应,并能从中快速恢复。Hystrix通过这些机制来阻止级联失败并保证系统弹性、可用。

什么是服务隔离
当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。
使用线程隔离或信号隔离的目的是为不同的服务分配一定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。

Hystrix实现服务隔离两种方案
Hystrix的资源隔离策略有两种,分别为:线程池和信号量。

线程池方式
1、 使用线程池隔离可以完全隔离第三方应用,请求线程可以快速放回。 2、 请求线程可以继续接受新的请求,如果出现问题线程池隔离是独立的不会影响其他应用。
3、 当失败的应用再次变得可用时,线程池将清理并可立即恢复,而不需要一个长时间的恢复。
4、 独立的线程池提高了并发性
缺点:
线程池隔离的主要缺点是它们增加计算开销(CPU)。每个命令的执行涉及到排队、调度和上 下文切换都是在一个单独的线程上运行的。

public class OrderHystrixCommand extends HystrixCommand {
@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;
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

}

信号量
使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数 器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返 回成功后计数器-1。
与线程池隔离最大不同在于执行依赖代码的线程依然是请求线程
tips:信号量的大小可以动态调整, 线程池大小不可以

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;
}
12345678910111213141516171819202122232425262728293031323334353637383940

}

应用场景
线程池隔离:
1、 第三方应用或者接口
2、 并发量大

信号量隔离:
1、 内部应用或者中间件(redis)
2、 并发需求不大

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。

为什么要互联网项目要限流
互联网雪崩效应解决方案
服务降级: 在高并发的情况, 防止用户一直等待,直接返回一个友好的错误提示给客户端。
服务熔断:在高并发的情况,一旦达到服务最大的承受极限,直接拒绝访问,使用服务降级。
服务隔离: 使用服务隔离方式解决服务雪崩效应
服务限流: 在高并发的情况,一旦服务承受不了使用服务限流机制(计时器(滑动窗口计数)、漏桶算法、令牌桶(Restlimite))

高并发限流解决方案
高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx)
限流算法
常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。
计数器
它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器

20200307094404524

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);
				}
			}
		});
	}
}
123456789101112131415161718192021222324252627282930313233343536

}

滑动窗口计数

滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。

20200307094419694

令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
20200307094713150

使用RateLimiter实现令牌桶限流
RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。
通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
下面来看一些简单的实践,需要先引入guava的maven依赖。org.springframework.boot spring-boot-starter-parent 2.0.0.RELEASE org.springframework.boot spring-boot-starter-web com.google.guava guava 25.1-jre

/**

  • 功能说明:使用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 "抢购失败!";
}
12345678910111213141516171819

}

漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:
一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶和漏桶对比:
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
另外有时候我们还使用计数器来进行限流,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;只要全局总请求数或者一定时间段的总请求数设定的阀值则进行限流,是简单粗暴的总数量限流,而不是平均速率限流。
20200307094613348

应用级限流
限流总并发/连接/请求数
对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。
如果你使用过Tomcat,其Connector其中一种配置有如下几个参数:
acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
maxConnections:瞬时最大连接数,超出的会排队等待;
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。
详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。
限流总资源数
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常。

限流某个接口的总并发/请求数
如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发/请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong进行限流:

适合对业务无损的服务或者需要过载保护的服务进行限流,如抢购业务,超出了大小要么让用户排队,要么告诉用户没货了,对用户来说是可以接受的。而一些开放平台也会限制用户调用某个接口的试用请求量,也可以用这种计数器方式实现。这种方式也是简单粗暴的限流,没有平滑处理,需要根据实际情况选择使用;

限流某个接口的时间窗请求数
即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速;一种实现方式如下所示:

平滑限流某个接口的请求数
之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如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处理的且已经读取了整个请求头的请求连接才会被计数器统计。