经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
ASP.NET Core Web API 接口限流
来源:cnblogs  作者:0611163  时间:2023/3/14 8:49:05  对本文有异议

一. 前言

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

二. 需求

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

三. 说明

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

四. 效果截图

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

五. 代码

1. RateLimitInterface

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

  1. using JsonA = Newtonsoft.Json;
  2. using JsonB = System.Text.Json.Serialization;
  3. namespace Utils
  4. {
  5. /// <summary>
  6. /// 限速接口
  7. /// </summary>
  8. public interface RateLimitInterface
  9. {
  10. /// <summary>
  11. /// 是否限速
  12. /// </summary>
  13. [JsonA.JsonIgnore]
  14. [JsonB.JsonIgnore]
  15. bool IsLimit { get; }
  16. }
  17. }

2. 接口参数实体类

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

  1. public class XxxPostData : RateLimitInterface
  2. {
  3. ...省略
  4. /// <summary>
  5. /// 是否限速
  6. /// </summary>
  7. [JsonA.JsonIgnore]
  8. [JsonB.JsonIgnore]
  9. public bool IsLimit
  10. {
  11. get
  12. {
  13. if (peoples.Count > 2) //限速条件,自己定义
  14. {
  15. return true;
  16. }
  17. return false;
  18. }
  19. }
  20. }

3. RateLimitAttribute

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

  1. namespace Utils
  2. {
  3. /// <summary>
  4. /// 接口限速
  5. /// </summary>
  6. public class RateLimitAttribute : Attribute
  7. {
  8. private Semaphore _sem;
  9. public Semaphore Sem
  10. {
  11. get
  12. {
  13. return _sem;
  14. }
  15. }
  16. public RateLimitAttribute(int limitCount = 1)
  17. {
  18. _sem = new Semaphore(limitCount, limitCount);
  19. }
  20. }
  21. }

4. 使用RateLimitAttribute

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

  1. [HttpPost]
  2. [Route("[action]")]
  3. [RateLimit(8)]
  4. public async Task<List<XxxInfo>> Query([FromBody] XxxPostData data)
  5. {
  6. ...省略
  7. }

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

  1. /// <summary>
  2. /// 接口限速
  3. /// </summary>
  4. public class RateLimitFilter : ActionFilterAttribute
  5. {
  6. public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
  7. {
  8. Type controllerType = context.Controller.GetType();
  9. object arg = context.ActionArguments.Values.ToList()[0];
  10. var rateLimit = context.ActionDescriptor.EndpointMetadata.OfType<RateLimitAttribute>().FirstOrDefault();
  11. bool isLimit = false; //是否限速
  12. if (rateLimit != null && arg is RateLimitInterface) //接口方法打了RateLimitAttribute标签并且参数实体类实现了RateLimitInterface接口时才限速,否则不限速
  13. {
  14. RateLimitInterface model = arg as RateLimitInterface;
  15. if (model.IsLimit) //满足限速条件
  16. {
  17. isLimit = true;
  18. Semaphore sem = rateLimit.Sem;
  19. if (sem.WaitOne(0)) //注意:超时时间为0,表示不等待
  20. {
  21. try
  22. {
  23. await next.Invoke();
  24. }
  25. catch
  26. {
  27. throw;
  28. }
  29. finally
  30. {
  31. sem.Release();
  32. }
  33. }
  34. else
  35. {
  36. var routeList = context.RouteData.Values.Values.ToList();
  37. routeList.Reverse();
  38. var route = string.Join('/', routeList.ConvertAll(a => a.ToString()));
  39. var msg = $"当前访问{route}接口的用户数太多,请稍后再试";
  40. LogUtil.Info(msg);
  41. context.Result = new ObjectResult(new ApiResult
  42. {
  43. code = (int)HttpStatusCode.ServiceUnavailable,
  44. message = "当前查询的用户太多,请稍后再试。"
  45. });
  46. }
  47. }
  48. }
  49. if (!isLimit)
  50. {
  51. await next.Invoke();
  52. }
  53. }
  54. }

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

6. 注册拦截器

  1. //拦截器
  2. builder.Services.AddMvc(options =>
  3. {
  4. ...省略
  5. options.Filters.Add<RateLimitFilter>();
  6. });

六. 使用jMeter进行压力测试

测试结果:

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

七. 同时测试三个接口

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

jMeter测试设置

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

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

测试日志截图


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


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

八. 实际情况

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

九. 后续

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

代码如下:

1. RateLimitAttribute

  1. /// <summary>
  2. /// 接口限速
  3. /// </summary>
  4. public class RateLimitAttribute : Attribute
  5. {
  6. private SemaphoreSlim _sem;
  7. public SemaphoreSlim Sem
  8. {
  9. get
  10. {
  11. return _sem;
  12. }
  13. }
  14. /// <summary>
  15. /// 超时时间(单位:毫秒)
  16. /// </summary>
  17. private int _timeout;
  18. /// <summary>
  19. /// 超时时间(单位:毫秒)
  20. /// </summary>
  21. public int Timeout
  22. {
  23. get
  24. {
  25. return _timeout;
  26. }
  27. }
  28. /// <summary>
  29. /// 接口限速
  30. /// </summary>
  31. /// <param name="limitCount">限制并发数量</param>
  32. /// <param name="timeout">超时时间(单位:秒)</param>
  33. public RateLimitAttribute(int limitCount = 1, int timeout = 0)
  34. {
  35. _sem = new SemaphoreSlim(limitCount, limitCount);
  36. _timeout = timeout * 1000;
  37. }
  38. }

2. RateLimitFilter

  1. /// <summary>
  2. /// 接口限速
  3. /// </summary>
  4. public class RateLimitFilter : ActionFilterAttribute
  5. {
  6. public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
  7. {
  8. Type controllerType = context.Controller.GetType();
  9. object arg = context.ActionArguments.Values.ToList()[0];
  10. var rateLimit = context.ActionDescriptor.EndpointMetadata.OfType<RateLimitAttribute>().FirstOrDefault();
  11. bool isLimit = false;
  12. if (rateLimit != null && arg is RateLimitInterface)
  13. {
  14. RateLimitInterface model = arg as RateLimitInterface;
  15. if (model.IsLimit) //满足限速条件
  16. {
  17. isLimit = true;
  18. SemaphoreSlim sem = rateLimit.Sem;
  19. if (await sem.WaitAsync(rateLimit.Timeout))
  20. {
  21. try
  22. {
  23. await next.Invoke();
  24. }
  25. catch
  26. {
  27. throw;
  28. }
  29. finally
  30. {
  31. sem.Release();
  32. }
  33. }
  34. else
  35. {
  36. var routeList = context.RouteData.Values.Values.ToList();
  37. routeList.Reverse();
  38. var route = string.Join('/', routeList.ConvertAll(a => a.ToString()));
  39. var msg = $"当前访问{route}接口的用户数太多,请稍后再试";
  40. LogUtil.Info(msg);
  41. context.Result = new ObjectResult(new ApiResult
  42. {
  43. code = (int)HttpStatusCode.ServiceUnavailable,
  44. message = "当前查询的用户太多,请稍后再试。"
  45. });
  46. }
  47. }
  48. }
  49. if (!isLimit)
  50. {
  51. await next.Invoke();
  52. }
  53. }
  54. }

效果

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

原文链接:https://www.cnblogs.com/s0611163/p/17199379.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号