高并发API限流策略

高并发系统常用策略

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容。
  • 降级:降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开。
  • 限流:限流的目的是通过对并发访问量限制,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
  • 分流:分流是为了将流量分发到多个服务进行处理,以便总体可以支撑巨大流量。

限流常用策略

常见限流模式有:控制并发和控制速率。控制并发是限制并发的总数量(比如数据库连接池、线程池等),控制速率是限制并发访问的速率(如nginx的limit_conn模块,用来限制瞬时并发连接数)。另外还可以限制单位时间窗口内的请求数量(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)。其他限流还可以限制远程接口调用速率、限制MQ的消费速率,根据网络连接数、网络流量、CPU或内存负载等来限流等。

控制并发量

在实际应用中可以通过信号量机制(Semaphore,线程安全:无名信号量,进程安全:有名信号量、V信号量)来实现。Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

例如:

public class SemaphoreService { 
    private final Semaphore permit = new Semaphore(10, true);
 
    public void process(){ 
        try{
            permit.acquire();
            //业务逻辑处理 
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            permit.release();
        }
    }
}

Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore可以用tryAcquire()方法尝试获取许可证。

控制访问速率

  • 漏桶算法

漏桶(Leaky Bucket)算法思路:水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。示意图如下:

高并发API限流策略_第1张图片

 

这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

  • 令牌桶算法

令牌桶算法原理:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。当需要提高速率,则按需提高放入桶中的令牌的速率即可。示意图如下:

高并发API限流策略_第2张图片

每过1/r秒桶中增加一个令牌;桶中最多存放b个令牌,如果桶满了,新放入的令牌会被丢弃;当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包;如果桶中可用令牌小于n,则该数据包将被缓存或丢弃。

Google开源工具包Guava提供的限流工具类RateLimiter来实现控制速率,该类基于令牌桶算法来完成限流,非常易于使用,而且非常高效。java 测试样例:

public void testRateLimiter() {
      RateLimiter limiter = RateLimiter.create(1);

      for(int i = 1; i < 10; i = i + 2 ) {
          double waitTime = limiter.acquire(i);
          System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
      }
  }

输出结果:

cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223

RateLimiter.create(1)用来创建一个限流器,参数代表每秒生成的令牌数,通过limiter.acquire(i);来以阻塞的方式获取令牌,当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,如果超timeout为0,则代表非阻塞,获取不到立即返回。RateLimiter通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费)。RateLimiter适用于单体应用,且RateLimiter不保证公平性访问。

分布式限流

考虑在分布式应用中限定所有应用的单位时间内的总请求量,可以基于Redis + 拦截器实现。假设单位时间为1分钟,设置Redis key 有效时间为60s, 如果key不存在则设置有效时间和初始化计数为1, 当key存在时则只新增1,当新增总数超过限制阈值则拒绝请求,注意这里需要保证设置有效期和初始化计数为1时的原子性。

注解+拦截器实现

自定义注解:

@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
 
    int limit() default 5;   
    int sec() default 5;
}

拦截器:

public class AccessLimitInterceptor implements HandlerInterceptor {
 
    @Autowired
    private RedisTemplate redisTemplate;
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if (!method.isAnnotationPresent(AccessLimit.class)) {
                return true;
            }
            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            int sec = accessLimit.sec();
            String key = IPUtil.getIpAddr(request) + request.getRequestURI();
            Integer maxLimit = redisTemplate.opsForValue().get(key);
            if (maxLimit == null) {
                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);  //set时一定要加过期时间
            } else if (maxLimit < limit) {
                redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);
            } else {
                output(response, "请求超过限制!");
                return false;
            }
        }
        return true;
    }
 
    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
 
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
 
    }
}

controller:


@Controller
@RequestMapping("/activity")
public class AopController {
    @ResponseBody
    @RequestMapping("/test")
    @AccessLimit(limit = 4,sec = 10)  //加上自定义注解即可
    public String test (HttpServletRequest request,@RequestParam(value = "username",required = false) String userName){
        //TODO somethings……
        return   "hello world !";
    }
}

配置文件:

/*springmvc的配置文件中加入自定义拦截器*/

   
      
      
   

接入层限流

主要介绍nginx 限流,采用漏桶算法。可以根据客户端请求IP,限制其访问频率。

用limit_req模块来限制基于IP请求的访问频率:

http://nginx.org/en/docs/http/ngx_http_limit_req_module.html

也可以用tengine中的增强版:

http://tengine.taobao.org/document_cn/http_limit_req_cn.html

 

1. 并发数和连接数控制的配置:

nginx http配置:
    #请求数量控制,每秒20个
    limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;
    #并发限制30个
    limit_conn_zone $binary_remote_addr zone=addr:10m;
 
    server块配置
    limit_req zone=one burst=5;
    limit_conn addr 30;

2. ngx_http_limit_conn_module  限制单个IP的连接数:

ngx_http_limit_conn_module模块可以按照定义的键限定每个键值的连接数。可以设定单一 IP 来源的连接数。并不是所有的连接都会被模块计数;只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    ...
    server {
        ...
        location /download/ {
            limit_conn addr 1;
        }

参考:https://blog.csdn.net/zrg523/article/details/82185088

你可能感兴趣的:(系统架构和性能优化)