一. 前言
我们经常会遇到这种情况,服务器流量异常,负载过大等等。对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个ip的连接数,并发数进行限制。
下面说说ngx_http_limit_conn_module 模块来实现该需求。该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数。并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。
二. ngx_http_limit_conn_module指令解释
limit_conn_zone
语法: limit_conn_zone
v
a
r
i
a
b
l
e
z
o
n
e
=
n
a
m
e
:
s
i
z
e
;
默
认
值
:
n
o
n
e
配
置
段
:
h
t
t
p
该
指
令
描
述
会
话
状
态
存
储
区
域
。
键
的
状
态
中
保
存
了
当
前
连
接
数
,
键
的
值
可
以
是
特
定
变
量
的
任
何
非
空
值
(
空
值
将
不
会
被
考
虑
)
。
variable zone=name:size; 默认值: none 配置段: http 该指令描述会话状态存储区域。键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值(空值将不会被考虑)。
variablezone=name:size;默认值:none配置段:http该指令描述会话状态存储区域。键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值(空值将不会被考虑)。variable定义键,zone=name定义区域名称,后面的limit_conn指令会用到的。size定义各个键共享内存空间大小。如:
limit_conn_zone $binary_remote_addr zone=addr:10m;
注释:客户端的IP地址作为键。注意,这里使用的是
b
i
n
a
r
y
r
e
m
o
t
e
a
d
d
r
变
量
,
而
不
是
binary_remote_addr变量,而不是
binaryremoteaddr变量,而不是remote_addr变量。
$remote_addr变量的长度为7字节到15字节,而存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
$binary_remote_addr变量的长度是固定的4字节,存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
1M共享空间可以保存3.2万个32位的状态,1.6万个64位的状态。
如果共享内存空间被耗尽,服务器将会对后续所有的请求返回 503 (Service Temporarily Unavailable) 错误。
limit_zone 指令和limit_conn_zone指令同等意思,已经被弃用,就不再做说明了。
limit_conn_log_level
语法:limit_conn_log_level info | notice | warn | error
默认值:error
配置段:http, server, location
当达到最大限制连接数后,记录日志的等级。
limit_conn
语法:limit_conn zone_name number
默认值:none
配置段:http, server, location
指定每个给定键值的最大同时连接数,当超过这个数字时被返回503 (Service Temporarily Unavailable)错误。如:
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
location /www.ttlsa.com/ {
limit_conn addr 1;
}
}
同一IP同一时间只允许有一个连接。
当多个 limit_conn 指令被配置时,所有的连接数限制都会生效。比如,下面配置不仅会限制单一IP来源的连接数,同时也会限制单一虚拟服务器的总连接数:
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;
}
[warning]limit_conn指令可以从上级继承下来。[/warning]
limit_conn_status
语法: limit_conn_status code;
默认值: limit_conn_status 503;
配置段: http, server, location
该指定在1.3.15版本引入的。指定当超过限制时,返回的状态码。默认是503。
limit_rate
语法:limit_rate rate
默认值:0
配置段:http, server, location, if in location
对每个连接的速率限制。参数rate的单位是字节/秒,设置为0将关闭限速。 按连接限速而不是按IP限制,因此如果某个客户端同时开启了两个连接,那么客户端的整体速率是这条指令设置值的2倍。
三. 完整实例配置
http {
limit_conn_zone $binary_remote_addr zone=limit:10m;
limit_conn_log_level info;
server {
location ^~ /download/ {
limit_conn limit 4;
limit_rate 200k;
alias /data/www.ttlsa.com/download/;
}
}
}
四. 使用注意事项
事务都具有两面性的。ngx_http_limit_conn_module 模块虽说可以解决当前面临的并发问题,但是会引入另外一些问题的。如前端如果有做LVS或反代,而我们后端启用了该模块功能,那不是非常多503错误了?这样的话,可以在前端启用该模块,要么就是设置白名单
五.进程内部限流Guava
Guava提供的RateLimiter可以限制物理或逻辑资源的被访问速率。RateLimit二的原理类似与令牌桶,它主要由许可发出的速率来定义,如果没有额外的配置,许可证将按每秒许可证规定的固定速度分配,许可将被平滑地分发,若请求超过permitsPerSecond则RateLimiter按照每秒1/permitsPerSecond的速率释放许可。
使用RateLimiter需要引入的jar包:
<!-- Guava是一种基于开源的Java库,谷歌很多项目使用它的很多核心库。这个库是为了方便编码,并减少编码错误 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
带参注解一种实现方式
1.定义注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 可控的限流注解
* @Date: 2019-04-10 10:35
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String value() default "";
/**
* 每秒放入桶中的令牌数,默认最大即不限流
* @return
*/
double perSecond() default Double.MAX_VALUE;
/**
* 获取令牌的等待时间 默认1
* @return
*/
int timeOut() default 1;
/**
* 超时时间单位 默认:秒
* @return
*/
TimeUnit timeOutUnit() default TimeUnit.SECONDS;
}
2.切面
import com.alibaba.fastjson.JSON;
import com.github.annotation.RateLimit;
import com.github.aspect.util.ResponseUtil;
import com.github.enums.http.HttpStatusEnum;
import com.github.vo.util.ResultUtil;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private HttpServletResponse response;
private RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
/**
* 定义切入点
* 两种方式可用:
* 1.通过指定包或者指定类切入
* @Pointcut("execution(public * com.github.controller.*.*(..))")
* 2.通过指定注解切入
* @Pointcut("@annotation(com.github.annotation.LxRateLimit)")
*/
@Pointcut("@annotation(com.github.annotation.RateLimit)")
public void pointcut() { }
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("拦截到了{}方法...", joinPoint.getSignature().getName());
Object obj = null;
// 获取目标方法
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
if (targetMethod.isAnnotationPresent(RateLimit.class)) {
// 获取目标方法的@LxRateLimit注解
RateLimit rateLimit = targetMethod.getAnnotation(RateLimit.class);
rateLimiter.setRate(rateLimit.perSecond());
// rateLimit.timeOut() = 1000
// rateLimit.timeOutUnit() = TimeUnit.MILLISECONDS 毫秒
// 判断能否在1秒内得到令牌,如果不能则立即返回false,不会阻塞程序
if (!rateLimiter.tryAcquire(rateLimit.timeOut(), rateLimit.timeOutUnit())) {
log.info("===== 接口并发量过大 =====");
ResponseUtil.output(response, JSON.toJSONString(ResultUtil.fail(HttpStatusEnum.BUSY_SERVER)));
}else{
obj = joinPoint.proceed();
}
}
return obj;
}
}
3.使用
import com.github.annotation.Limiting;
import com.github.annotation.RateLimit;
import com.github.vo.Result;
import com.github.vo.util.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 测试RateLimit限流(自定义注解+切面)
*
* @Date: 2019-04-04 14:17
*/
@Slf4j
@Controller
public class AppController {
/**
* AtomicInteger是一个线程安全的具有原子性的能返回int类型的对象
*/
private static AtomicInteger num = new AtomicInteger(0);
/**
* perSecond = 0.5:每秒创建0.5个
* timeOut = 1:在一秒的时间内如果有许可则成功,没有则失败
* @return
*/
@RateLimit(perSecond = 0.5,timeOut = 1)
@GetMapping("apps")
@ResponseBody
public Result apps(){
log.info("app() num = {}",num.incrementAndGet());
return ResultUtil.success();
}
}