• 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
    {
        /// 
        /// 限速接口
        /// 
        public interface RateLimitInterface
        {
            /// 
            /// 是否限速
            /// 
            [JsonA.JsonIgnore]
            [JsonB.JsonIgnore]
            bool IsLimit { get; }
        }
    }
    

    2. 接口参数实体类

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

    public class XxxPostData : RateLimitInterface
    {
        ...省略
    
        /// 
        /// 是否限速
        /// 
        [JsonA.JsonIgnore]
        [JsonB.JsonIgnore]
        public bool IsLimit
        {
            get
            {
                if (peoples.Count > 2) //限速条件,自己定义
                {
                    return true;
                }
                return false;
            }
        }
    }
    

    3. RateLimitAttribute

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

    namespace Utils
    {
        /// 
        /// 接口限速
        /// 
        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> Query([FromBody] XxxPostData data)
    {
        ...省略
    }
    

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

    /// 
    /// 接口限速
    /// 
    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().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();
    });
    

    六. 使用jMeter进行压力测试

    测试结果:

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

    七. 同时测试三个接口

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

    jMeter测试设置

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

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

    测试日志截图


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


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

    八. 实际情况

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

    九. 后续

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

    代码如下:

    1. RateLimitAttribute

    /// 
    /// 接口限速
    /// 
    public class RateLimitAttribute : Attribute
    {
        private SemaphoreSlim _sem;
    
        public SemaphoreSlim Sem
        {
            get
            {
                return _sem;
            }
        }
    
        /// 
        /// 超时时间(单位:毫秒)
        /// 
        private int _timeout;
    
        /// 
        /// 超时时间(单位:毫秒)
        /// 
        public int Timeout
        {
            get
            {
                return _timeout;
            }
        }
    
        /// 
        /// 接口限速
        /// 
        /// 限制并发数量
        /// 超时时间(单位:秒)
        public RateLimitAttribute(int limitCount = 1, int timeout = 0)
        {
            _sem = new SemaphoreSlim(limitCount, limitCount);
            _timeout = timeout * 1000;
        }
    }
    

    2. RateLimitFilter

    /// 
    /// 接口限速
    /// 
    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().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. 当设置了并发量和超时时间后,接口平均一秒钟能被请求多少次,取决于接口耗时,耗时短的接口平均每秒能被请求的次数多,耗时长的接口平均每秒能被请求的次数少。
  • 相关阅读:
    NodeJs实战-待办列表(6)-前端绘制表格显示待办事项详情
    从头开发一个RISC-V的操作系统(四)嵌入式开发介绍
    设计模式——3 开闭原则
    DYN_TABLE_ILL_COMP_VAL-CX_SY_DYN_TABLE_ILL_COMP_VAL-DUMP
    从零开始设计一个共识算法——一场没有硝烟的战争
    如何使用Python进行数据可视化:Matplotlib和Seaborn指南【第123篇—Matplotlib和Seaborn指南】
    Azure SQL DB/MI以及SQL SERVER中sys.databases视图介绍
    金融数学方法:有限差分法
    函数 RFC 函数 BAPI
    一文带你详细了解浏览器安全
  • 原文地址:https://www.cnblogs.com/s0611163/p/17199379.html