ASP.NET Core Web API 接口限流

Posted 0611163

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core Web API 接口限流相关的知识,希望对你有一定的参考价值。

一. 前言

ASP.NET Core Web API 接口限流、限制接口并发数量,我也不知道自己写的有没有问题,抛砖引玉。

二. 需求

  1. 写了一个接口,参数可以传多个人员,也可以传单个人员,时间范围限制最长一个月。简单来说,当传单个人员时,接口耗时很短,当传多个人员时,一般人员会较多,接口耗时较长,一般耗时几秒。
  2. 当传多个人员时,并发量高时,接口的耗时就很长了,比如100个用户并发请求,耗时可长达几十秒,甚至1分钟。
  3. 所以需求是,当传单个人员时,不限制。当传多个人员时,限制并发数量。如果并发用户数少于限制数,那么所有用户都能成功。如果并发用户数,超出限制数,那么超出的用户请求失败,并提示"当前进行XXX查询的用户太多,请稍后再试"。
  4. 这样也可以减轻被请求的ES集群的压力。

三. 说明

  1. 使用的是.NET6
  2. 我知道有人写好了RateLimit中间件,但我暂时还没有学会怎么使用,能否满足我的需求,所以先自己实现一下。

四. 效果截图

下面是使用jMeter并发测试时,打的接口日志:

五. 代码

1. RateLimitInterface

接口参数的实体类要继承该接口

using JsonA = Newtonsoft.Json;
using JsonB = System.Text.Json.Serialization;

namespace Utils

    /// <summary>
    /// 限速接口
    /// </summary>
    public interface RateLimitInterface
    
        /// <summary>
        /// 是否限速
        /// </summary>
        [JsonA.JsonIgnore]
        [JsonB.JsonIgnore]
        bool IsLimit  get; 
    

2. 接口参数实体类

继承RateLimitInterface接口,并实现IsLimit属性

public class XxxPostData : RateLimitInterface

    ...省略

    /// <summary>
    /// 是否限速
    /// </summary>
    [JsonA.JsonIgnore]
    [JsonB.JsonIgnore]
    public bool IsLimit
    
        get
        
            if (peoples.Count > 2) //限速条件,自己定义
            
                return true;
            
            return false;
        
    

3. RateLimitAttribute

作用:标签打在接口方法上,并设置并发数量

namespace Utils

    /// <summary>
    /// 接口限速
    /// </summary>
    public class RateLimitAttribute : Attribute
    
        private Semaphore _sem;

        public Semaphore Sem
        
            get
            
                return _sem;
            
        

        public RateLimitAttribute(int limitCount = 1)
        
            _sem = new Semaphore(limitCount, limitCount);
        
    

4. 使用RateLimitAttribute

标签打在接口方法上,并设置并发数量。
服务器好像是24核的,并发限制为8应该没问题。

[HttpPost]
[Route("[action]")]
[RateLimit(8)]
public async Task<List<XxxInfo>> Query([FromBody] XxxPostData data)

    ...省略

5. 限制接口并发量的拦截器RateLimitFilter

/// <summary>
/// 接口限速
/// </summary>
public class RateLimitFilter : ActionFilterAttribute

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    
        Type controllerType = context.Controller.GetType();
        object arg = context.ActionArguments.Values.ToList()[0];
        var rateLimit = context.ActionDescriptor.EndpointMetadata.OfType<RateLimitAttribute>().FirstOrDefault();

        bool isLimit = false; //是否限速
        if (rateLimit != null && arg is RateLimitInterface) //接口方法打了RateLimitAttribute标签并且参数实体类实现了RateLimitInterface接口时才限速,否则不限速
        
            RateLimitInterface model = arg as RateLimitInterface;
            if (model.IsLimit) //满足限速条件
            
                isLimit = true;
                Semaphore sem = rateLimit.Sem;

                if (sem.WaitOne(0)) //注意:超时时间为0,表示不等待
                
                    try
                    
                        await next.Invoke();
                    
                    catch
                    
                        throw;
                    
                    finally
                    
                        sem.Release();
                    
                
                else
                
                    var routeList = context.RouteData.Values.Values.ToList();
                    routeList.Reverse();
                    var route = string.Join(\'/\', routeList.ConvertAll(a => a.ToString()));
                    var msg = $"当前访问route接口的用户数太多,请稍后再试";
                    LogUtil.Info(msg);
                    context.Result = new ObjectResult(new ApiResult
                    
                        code = (int)HttpStatusCode.ServiceUnavailable,
                        message = "当前查询的用户太多,请稍后再试。"
                    );
                
            
        

        if (!isLimit)
        
            await next.Invoke();
        
    

上述代码说明:sem.WaitOne(0)这个超时时间,最好是0,即不等待,否则高并发下会有问题。SemaphoreSlim的异步wait没试过。如果超时时间大于0,意味着,高并发下,会有大量的等待,异步等待也是等待。
SemaphoreSlim短时间是自旋,想象一下一瞬间产生大量自旋会怎么样?所以最好不等待,如果要等待,那代码还得再研究研究,经过测试才能用。

6. 注册拦截器

//拦截器
builder.Services.AddMvc(options =>

    ...省略

    options.Filters.Add<RateLimitFilter>();
);

六. 使用jMeter进行压力测试

测试结果:

  1. 被限速的接口,满足限速条件的调用并发量大时,部分用户成功,部分用户提示当前查询的人多请稍后再试。但不影响未满足限速条件的传参调用,也不影响其它未限速接口的调用。
  2. 测试的所有接口、所有查询参数条件的调用,耗时稳定,大量并发时,不会出现接口耗时几十秒甚至1分钟的情况。

七. 同时测试三个接口

测试三个接口,一个是触发限流的A接口,一个是未触发限流的A接口,一个是未被限流的B接口。

jMeter测试设置

触发限流的A接口,并发量设置为200:

未触发限流的A接口以及未被限流的B接口,并发量设置为1:

测试日志截图


截图说明:可以看到被限流接口共1000次调用,只有大约40次调用是成功的,剩下的返回请稍后再试。


截图说明:实际上触发限流的接口,并发量为8,压力依然很大,会拖慢自身以及其它接口,当触发限流的接口请求结束时,其它接口访问速度才正常。

八. 实际情况

  1. 这种接口计算量大,是难以支持高并发的,需要限流。争取客户的理解,仅支持少量用户在同一时间查询。
  2. 实际上只要用户错开几秒访问,接口的耗时就很正常。问题是,如何错开几秒呢?当用户看到"请稍后再试"的提示,关闭提示,重新点击查询,就可以错开了。如果一次两次不行,就多点几次查询。

九. 后续

  1. 修改为使用SemaphoreSlim类,这样可以异步等待
  2. RateLimitAttribute类增加了超时时间属性

代码如下:

1. RateLimitAttribute

/// <summary>
/// 接口限速
/// </summary>
public class RateLimitAttribute : Attribute

    private SemaphoreSlim _sem;

    public SemaphoreSlim Sem
    
        get
        
            return _sem;
        
    

    /// <summary>
    /// 超时时间(单位:毫秒)
    /// </summary>
    private int _timeout;

    /// <summary>
    /// 超时时间(单位:毫秒)
    /// </summary>
    public int Timeout
    
        get
        
            return _timeout;
        
    

    /// <summary>
    /// 接口限速
    /// </summary>
    /// <param name="limitCount">限制并发数量</param>
    /// <param name="timeout">超时时间(单位:秒)</param>
    public RateLimitAttribute(int limitCount = 1, int timeout = 0)
    
        _sem = new SemaphoreSlim(limitCount, limitCount);
        _timeout = timeout * 1000;
    

2. RateLimitFilter

/// <summary>
/// 接口限速
/// </summary>
public class RateLimitFilter : ActionFilterAttribute

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    
        Type controllerType = context.Controller.GetType();
        object arg = context.ActionArguments.Values.ToList()[0];
        var rateLimit = context.ActionDescriptor.EndpointMetadata.OfType<RateLimitAttribute>().FirstOrDefault();

        bool isLimit = false;
        if (rateLimit != null && arg is RateLimitInterface)
        
            RateLimitInterface model = arg as RateLimitInterface;
            if (model.IsLimit) //满足限速条件
            
                isLimit = true;
                SemaphoreSlim sem = rateLimit.Sem;

                if (await sem.WaitAsync(rateLimit.Timeout))
                
                    try
                    
                        await next.Invoke();
                    
                    catch
                    
                        throw;
                    
                    finally
                    
                        sem.Release();
                    
                
                else
                
                    var routeList = context.RouteData.Values.Values.ToList();
                    routeList.Reverse();
                    var route = string.Join(\'/\', routeList.ConvertAll(a => a.ToString()));
                    var msg = $"当前访问route接口的用户数太多,请稍后再试";
                    LogUtil.Info(msg);
                    context.Result = new ObjectResult(new ApiResult
                    
                        code = (int)HttpStatusCode.ServiceUnavailable,
                        message = "当前查询的用户太多,请稍后再试。"
                    );
                
            
        

        if (!isLimit)
        
            await next.Invoke();
        
    

效果

  1. 假如设置RateLimit(1, 0),即并发1,超时时间0,那么当100个并发请求时,只有1个成功,99个失败。
  2. 假如接口耗时2秒,设置RateLimit(1, 10),即并发1,超时时间10秒,那么当100个并发请求时,会有大约5个成功,95个失败。第1个成功的接口请求耗时大约2秒,后续成功的4个,请求耗时依次增加。
  3. 当设置了并发量和超时时间后,接口平均一秒钟能被请求多少次,取决于接口耗时,耗时短的接口平均每秒能被请求的次数多,耗时长的接口平均每秒能被请求的次数少。

以上是关于ASP.NET Core Web API 接口限流的主要内容,如果未能解决你的问题,请参考以下文章

asp.net core web api 生成 swagger 文档

asp.net core web api 生成 swagger 文档

ASP.NET Core设置 Web API 响应数据的格式——FormatFilter特性篇

ASP.NET Core基于滑动窗口算法实现限流控制

uniapp跨域调用ASP.NET Core Web API

uniapp跨域调用ASP.NET Core Web API