多并发多级缓存项目限流策略

一. 前言

我们经常会遇到这种情况,服务器流量异常,负载过大等等。对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个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:httpvariable定义键,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变量,而不是 binaryremoteaddrremote_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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值