SpringCloud组件之Zuul
项目地址: https://gitee.com/yaiys/spring-cloud,还在完善中。
限流的分类如下所示:
-
合法性验证限流:比如验证码、IP 黑名单等,这些手段可以有效的防止恶意攻击和爬虫采集;
-
容器限流:比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以设置最大线程数(maxThreads),当并发超过最大线程数会排队等待执行;而 Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数;
-
服务端限流:比如我们在服务器端通过限流算法实现限流,此项也是我们本文介绍的重点。
合法性验证限流为最常规的业务代码,就是普通的验证码和 IP 黑名单系统
容器限流
Tomcat 限流
Tomcat 8.5 版本的最大线程数在 conf/server.xml 配置中,如下所示:
-
<Connector port="8080" protocol="HTTP/1.1"
-
connectionTimeout="20000"
-
maxThreads="150"
-
redirectPort="8443" />
其中 maxThreads
就是 Tomcat 的最大线程数,当请求的并发大于此值(maxThreads)时,请求就会排队执行,这样就完成了限流的目的。
Nginx 限流
Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数。
控制速率
我们需要使用 limit_req_zone
用来限制单位时间内的请求数,即速率限制,示例配置如下:
-
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
-
server {
-
location / {
-
limit_req zone=mylimit;
-
}
-
}
以上配置表示,限制每个 IP 访问的速度为 2r/s,因为 Nginx 的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第 2 个请求。
控制并发数
利用 limit_conn_zone
和 limit_conn
两个指令即可控制并发数,示例配置如下:
-
limit_conn_zone $binary_remote_addr zone=perip:10m;
-
limit_conn_zone $server_name zone=perserver:10m;
-
server {
-
...
-
limit_conn perip 10;
-
limit_conn perserver 100;
-
}
其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个连接;limit_conn perserver 100 表示 server 同时能处理并发连接的总数为 100 个。
-
Zuul,顾名思义,服务网关。可以和Eureka、consul、Ribbon、Hystrix等组件配合使用,Zuul的主要功能是路由转发和过滤器。可以独立使用,也可以搭配consul,fegin等使用。
-
单节点单独使用
MVN引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
集成consul
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
集成feigin
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件application.yml
server:
port: 8080
spring:
application:
name: zuul
zuul:
routes:
# 自定义转发规则
domo:
# 匹配的路由规则,即所有demo/**的请求都会被转发到url: http://192.168.50.33为服务器上。可以是任何请求。
path: /demo/**
url: http://192.168.50.33
# 过滤路由,test开头的任意请求不转发
ignored-patterns: /test/**
-
单节点搭配hystrix单独使用,对于小网站,防止恶意攻击,以及限制刷注册等有一定作用,配置文件如下
server: port: 8080 spring: application: name: zuul redis: host: 192.168.50.129 password: # 配置eureka地址 zuul: routes: # 这里可以自定义 demo2: # 匹配的路由规则 path: /**/** # 路由的目标地址 url: demo # 过滤路由,test开头的任意请求 ignored-patterns: /test/** # 如果不使用eureka的话,需要自己定义路由的那个服务的其他负载服务 ribbon: eureka: enabled: false demo: ribbon: # 这里写你要路由的demo服务的所有负载服务请求地址, listOfServers: http://192.168.50.33:5210,http://192.168.50.13:5210 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000 # 配置限流 ratelimit: enabled: true # 对应存储类型(用来统计存储统计信息) repository: redis # 配置路由的策略 policy-list: demo: # 每秒允许多少个请求 - limit: 500 # 刷新时间(单位秒) refresh-interval: 1 # 根据URL统计 type: - URL #url类型的限流就是通过请求路径区分,origin是通过客户端IP地址区分,user是通过登录用户名进行区分,也包括匿名用户
-
RateLimit:限流
RateLimit:限流
what:什么是限流
顾名思义限制流量why:为什么我们的服务需要限流
用户量病毒增长
微博热搜/淘宝双十一
竞品爬虫
恶意攻击
how:如何限流
一般可以根据服务的某项核心指标,如QPS,来决定是否将后续的请求拦截。比如设定某系统1s的QPS阈值为100,当1s内的QPS达到了110,那么差值的10个请求则会被拦截,直接返回503状态码:服务器繁忙。
根据以上结果导向论,又衍生出了如下3套算法:(1)计数器
1.思想
假设服务设定的最高QPS为100,声明一个计数器counter,在接下来的1s内,每有一个请求则counter+1,如果在这1s内,counter>100,则限流,1s结束后,counter重置清零。
2.缺点
如果在0.59s时QPS达到了100,在1.00s时QPS也达到了100,那么其实在1s内,QPS达到了200,限流GG!(2)漏桶算法
1.基本思想
想象有一个木桶,以恒定的速度漏水(处理请求),有新的请求来,可以放进桶里,如果桶满了,则直接拒绝请求。
2.优点
可以平滑收到的请求,以恒定的速度处理。
缺点
请求的处理(漏水)有一定的延时性(3)令牌桶算法
1.基本思想
想象有一个木桶,按一定速率往桶里放令牌,满了令牌则溢出舍弃。每来一个请求则取一个令牌,桶内无令牌可取则拒绝请求
2.优点
完美解决了计数器存在的临界问题,同时突增的QPS只要桶内有令牌就可以访问。关键代码
1.注解 Limit.java@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {//资源的名字
String name() default "";//资源的key
String key() default "";//Key的prefix
String prefix() default "";//给定的时间段 单位秒
int period() default 1;//最多的访问限制次数
int count();//类型
LimitType limitType() default LimitType.CUSTOMER;
}
2.枚举LimitType.javapublic enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;
}
3.切面LimitAspect.java@Aspect
@Slf4j
public class LimitAspect {@Resource(name = "limitRedisTemplate")
private RedisTemplate<String, Serializable> limitRedisTemplate;@Pointcut("@annotation(com.workbei.ratelimit.spring.boot.limit.Limit)")
public void limitAnnotationPointcut() {
}@Around("limitAnnotationPointcut()")
public Object interceptor(ProceedingJoinPoint pjp) {
//获得方法上的注解
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType) {
case IP:
key = IPUtils.getIpAddr();
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
//加载原子性lua脚本
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
//执行脚本
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
log.info("Access try count is {} for name={} and key = {}", count, name, key);
try {
if (count != null && count.intValue() <= limitCount) {//无需限流:通过
return pjp.proceed();
} else {
throw new RateLimitException("rate limit ing");
}
} catch (Throwable e) {
throw new RateLimitException(e.getMessage());
}
}/**
* 限流 脚本
*
* @return lua脚本
*/
private String buildLuaScript() {
return ScriptUtils.loader("limit.lua");
}
}
4.limit.lua(redis命令脚本文件)local c
c = redis.call('get',KEYS[1])
-- 调用不超过最大值,则直接返回
if c and tonumber(c) > tonumber(ARGV[1]) then
return c;
end
-- 执行计算器自加
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
-- 从第一次调用开始限流,设置对应键值的过期
redis.call('expire',KEYS[1],ARGV[2])
end
return c;
————————————————