Prechádzať zdrojové kódy

feat: 新增接口压测系统通用接口

喵你个旺呀 1 rok pred
rodič
commit
873a2dbe3e

+ 7 - 1
Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs

@@ -833,5 +833,11 @@ public enum ErrorCodeEnum
     /// 身份标识已存在
     /// </summary>
     [ErrorCodeItemMetadata("身份标识已存在")]
-    O1000
+    O1000,
+
+    /// <summary>
+    /// 禁止非超级管理员操作
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止非超级管理员操作")]
+    SA001
 }

+ 70 - 0
Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs

@@ -0,0 +1,70 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 接口压测输入参数
+/// </summary>
+public class StressTestInput
+{
+    /// <summary>
+    /// 接口请求地址
+    /// </summary>
+    /// <example>https://gitee.com/zuohuaijun/Admin.NET</example>
+    [Required(ErrorMessage = "接口请求地址不能为空")]
+    public string RequestUri { get; set; }
+
+    /// <summary>
+    /// 请求方式
+    /// </summary>
+    [Required(ErrorMessage = "请求方式不能为空")]
+    public HttpMethod? RequestMethod { get; set; } = HttpMethod.Get;
+
+    /// <summary>
+    /// 每轮请求量
+    /// </summary>
+    /// <example>100</example>
+    [Required(ErrorMessage = "每轮请求量不能为空")]
+    [Range(1, 100000, ErrorMessage = "每轮请求量必须为1-100000")]
+    public int? NumberOfRequests { get; set; }
+
+    /// <summary>
+    /// 压测轮数
+    /// </summary>
+    /// <example>5</example>
+    [Required(ErrorMessage = "压测轮数不能为空")]
+    [Range(1, 10000, ErrorMessage = "压测轮数必须为1-10000")]
+    public int? NumberOfRounds { get; set; }
+
+    /// <summary>
+    /// 最大并行量(默认为当前主机逻辑处理器的数量)
+    /// </summary>
+    /// <example>500</example>
+    [Range(0, 10000, ErrorMessage = "最大并行量必须为0-10000")]
+    public int? MaxDegreeOfParallelism { get; set; } = Environment.ProcessorCount;
+
+    /// <summary>
+    /// 请求参数
+    /// </summary>
+    public List<KeyValuePair<string, string>> RequestParameters { get; set; } = new();
+
+    /// <summary>
+    /// 请求头参数
+    /// </summary>
+    public Dictionary<string, string> Headers { get; set; } = new();
+
+    /// <summary>
+    /// 路径参数
+    /// </summary>
+    public Dictionary<string, string> PathParameters { get; set; } = new();
+
+    /// <summary>
+    /// Query参数
+    /// </summary>
+    public Dictionary<string, string> QueryParameters { get; set; } = new();
+}
+

+ 89 - 0
Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonOutput.cs

@@ -0,0 +1,89 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 接口压测输出参数
+/// </summary>
+public class StressTestOutput
+{
+    /// <summary>
+    /// 总请求次数
+    /// </summary>
+    public long TotalRequests { get; set; }
+
+    /// <summary>
+    /// 总用时(秒)
+    /// </summary>
+    public double TotalTimeInSeconds { get; set; }
+
+    /// <summary>
+    /// 成功请求次数
+    /// </summary>
+    public long SuccessfulRequests { get; set; }
+
+    /// <summary>
+    /// 失败请求次数
+    /// </summary>
+    public long FailedRequests { get; set; }
+
+    /// <summary>
+    /// 每秒查询率(QPS)
+    /// </summary>
+    public double QueriesPerSecond { get; set; }
+
+    /// <summary>
+    /// 最小响应时间(毫秒)
+    /// </summary>
+    public double MinResponseTime { get; set; }
+
+    /// <summary>
+    /// 最大响应时间(毫秒)
+    /// </summary>
+    public double MaxResponseTime { get; set; }
+
+    /// <summary>
+    /// 平均响应时间(毫秒)
+    /// </summary>
+    public double AverageResponseTime { get; set; }
+
+    /// <summary>
+    /// P10 响应时间(毫秒)
+    /// </summary>
+    public double Percentile10ResponseTime { get; set; }
+
+    /// <summary>
+    /// P25 响应时间(毫秒)
+    /// </summary>
+    public double Percentile25ResponseTime { get; set; }
+
+    /// <summary>
+    /// P50 响应时间(毫秒)
+    /// </summary>
+    public double Percentile50ResponseTime { get; set; }
+
+    /// <summary>
+    /// P75 响应时间(毫秒)
+    /// </summary>
+    public double Percentile75ResponseTime { get; set; }
+
+    /// <summary>
+    /// P90 响应时间(毫秒)
+    /// </summary>
+    public double Percentile90ResponseTime { get; set; }
+
+    /// <summary>
+    /// P99 响应时间(毫秒)
+    /// </summary>
+    public double Percentile99ResponseTime { get; set; }
+
+    /// <summary>
+    /// P999 响应时间(毫秒)
+    /// </summary>
+    public double Percentile999ResponseTime { get; set; }
+}
+

+ 124 - 1
Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs

@@ -21,14 +21,20 @@ public class SysCommonService : IDynamicApiController, ITransient
     private readonly IApiDescriptionGroupCollectionProvider _apiProvider;
     private readonly SqlSugarRepository<SysUser> _sysUserRep;
     private readonly CDConfigOptions _cdConfigOptions;
+    private readonly UserManager _userManager;
+    private readonly HttpClient _httpClient;
 
     public SysCommonService(IApiDescriptionGroupCollectionProvider apiProvider,
         SqlSugarRepository<SysUser> sysUserRep,
-        IOptions<CDConfigOptions> giteeOptions)
+        IOptions<CDConfigOptions> giteeOptions,
+        IHttpClientFactory httpClientFactory,
+        UserManager userManager)
     {
         _sysUserRep = sysUserRep;
         _apiProvider = apiProvider;
+        _userManager = userManager;
         _cdConfigOptions = giteeOptions.Value;
+        _httpClient = httpClientFactory.CreateClient();
     }
 
     /// <summary>
@@ -102,4 +108,121 @@ public class SysCommonService : IDynamicApiController, ITransient
     {
         return CryptogramUtil.Encrypt(plainText);
     }
+
+    /// <summary>
+    /// 接口压测 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("接口压测")]
+    public async Task<StressTestOutput> StressTest(StressTestInput input)
+    {
+        // 限制仅超管用户才能使用此功能
+        if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001);
+
+        var stopwatch = new Stopwatch();
+        var responseTimes = new List<double>();
+        long totalRequests = 0, successfulRequests = 0, failedRequests = 0;
+
+        stopwatch.Start();
+        var semaphore = new SemaphoreSlim(input.MaxDegreeOfParallelism!.Value > 0 ? input.MaxDegreeOfParallelism.Value : Environment.ProcessorCount);
+
+        var tasks = Enumerable.Range(0, input.NumberOfRounds!.Value * input.NumberOfRequests!.Value).Select(async _ =>
+        {
+            await semaphore.WaitAsync();
+            try
+            {
+                var requestStopwatch = new Stopwatch();
+                requestStopwatch.Start();
+
+                // 构建完整的请求URI,包括路径参数和查询参数
+                var uriBuilder = new UriBuilder(input.RequestUri);
+                var queryString = HttpUtility.ParseQueryString(uriBuilder.Query);
+
+                foreach (var param in input.PathParameters)
+                {
+                    uriBuilder.Path = uriBuilder.Path.Replace($"{{{param.Key}}}", param.Value, StringComparison.OrdinalIgnoreCase);
+                }
+
+                foreach (var param in input.QueryParameters)
+                {
+                    queryString[param.Key] = param.Value;
+                }
+
+                uriBuilder.Query = queryString.ToString() ?? string.Empty;
+                var fullUri = uriBuilder.Uri.ToString();
+
+                HttpRequestMessage request = new(input.RequestMethod!, fullUri);
+
+                // 设置请求头
+                foreach (var header in input.Headers)
+                {
+                    request.Headers.Add(header.Key, header.Value);
+                }
+
+                if (input.RequestMethod != HttpMethod.Get && input.RequestParameters.Any())
+                {
+                    var content = new FormUrlEncodedContent(input.RequestParameters);
+                    request.Content = content;
+                }
+
+                var response = await _httpClient.SendAsync(request);
+                response.EnsureSuccessStatusCode(); // 抛出错误状态码异常
+
+                requestStopwatch.Stop();
+                responseTimes.Add(requestStopwatch.Elapsed.TotalMilliseconds);
+                Interlocked.Increment(ref successfulRequests);
+            }
+            catch
+            {
+                Interlocked.Increment(ref failedRequests);
+            }
+            finally
+            {
+                Interlocked.Increment(ref totalRequests);
+                semaphore.Release();
+            }
+        });
+
+        await Task.WhenAll(tasks);
+        stopwatch.Stop();
+
+        var totalTimeInSeconds = stopwatch.Elapsed.TotalSeconds;
+        var qps = totalTimeInSeconds > 0 ? totalRequests / totalTimeInSeconds : 0;
+        var averageResponseTime = responseTimes.Any() ? responseTimes.Average() : 0;
+        var minResponseTime = responseTimes.Any() ? responseTimes.Min() : 0;
+        var maxResponseTime = responseTimes.Any() ? responseTimes.Max() : 0;
+
+        return new StressTestOutput
+        {
+            TotalRequests = totalRequests,
+            TotalTimeInSeconds = totalTimeInSeconds,
+            SuccessfulRequests = successfulRequests,
+            FailedRequests = failedRequests,
+            QueriesPerSecond = qps,
+            MinResponseTime = minResponseTime,
+            MaxResponseTime = maxResponseTime,
+            AverageResponseTime = averageResponseTime,
+            Percentile10ResponseTime = CalculatePercentile(responseTimes, 0.1),
+            Percentile25ResponseTime = CalculatePercentile(responseTimes, 0.25),
+            Percentile50ResponseTime = CalculatePercentile(responseTimes, 0.5),
+            Percentile75ResponseTime = CalculatePercentile(responseTimes, 0.75),
+            Percentile90ResponseTime = CalculatePercentile(responseTimes, 0.9),
+            Percentile99ResponseTime = CalculatePercentile(responseTimes, 0.99),
+            Percentile999ResponseTime = CalculatePercentile(responseTimes, 0.999)
+        };
+    }
+
+    /// <summary>
+    /// 计算百分位请求耗时
+    /// </summary>
+    /// <param name="times">请求耗时列表</param>
+    /// <param name="percentile">百分位</param>
+    /// <returns></returns>
+    private double CalculatePercentile(List<double> times, double percentile)
+    {
+        if (!times.Any()) return 0;
+        var sortedTimes = times.OrderBy(t => t).ToList();
+        var index = (int)Math.Ceiling(percentile * sortedTimes.Count) - 1;
+        return sortedTimes[index < sortedTimes.Count ? index : sortedTimes.Count - 1];
+    }
 }