NewLife/NewLife.Remoting

优化服务端RPC处理性能:预编译委托、减少反射、减少对象分配

Co-authored-by: nnhy <506367+nnhy@users.noreply.github.com>
copilot-swe-agent[bot] authored at 2026-02-26 00:52:30 Stone committed at 2026-02-26 03:02:00
ccc89fa
Tree
1 Parent(s) ddeada0
Summary: 5 changed files with 466 additions and 8 deletions.
Modified +19 -0
Added +338 -0
Modified +61 -1
Modified +10 -1
Modified +38 -6
Modified +19 -0
diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs
index b986d99..d256b70 100644
--- a/Benchmark/Program.cs
+++ b/Benchmark/Program.cs
@@ -1,3 +1,22 @@
 using BenchmarkDotNet.Running;
+using NewLife.Remoting.Benchmarks;
+
+// 支持服务端吞吐量压力测试模式
+if (args.Length > 0 && args[0].Equals("throughput", StringComparison.OrdinalIgnoreCase))
+{
+    var clientCount = args.Length > 1 ? Int32.Parse(args[1]) : 100;
+    var duration = args.Length > 2 ? Int32.Parse(args[2]) : 10;
+    ServerThroughputTest.RunNetworkTest(clientCount, duration);
+    return;
+}
+
+// 服务端纯处理能力测试(绕过TCP网络栈)
+if (args.Length > 0 && args[0].Equals("direct", StringComparison.OrdinalIgnoreCase))
+{
+    var threadCount = args.Length > 1 ? Int32.Parse(args[1]) : 0;
+    var duration = args.Length > 2 ? Int32.Parse(args[2]) : 10;
+    ServerThroughputTest.RunDirectTest(threadCount, duration);
+    return;
+}
 
 BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
Added +338 -0
diff --git a/Benchmark/ServerThroughputTest.cs b/Benchmark/ServerThroughputTest.cs
new file mode 100644
index 0000000..55383a2
--- /dev/null
+++ b/Benchmark/ServerThroughputTest.cs
@@ -0,0 +1,338 @@
+using System.Diagnostics;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Messaging;
+using NewLife.Remoting;
+
+#pragma warning disable CS0618 // Packet obsolete
+
+namespace NewLife.Remoting.Benchmarks;
+
+/// <summary>服务端吞吐量压力测试。模拟多客户端并发对ApiServer施加压力,测量服务端处理能力</summary>
+public class ServerThroughputTest
+{
+    /// <summary>运行服务端吞吐量测试(通过TCP网络)</summary>
+    /// <param name="clientCount">客户端连接数</param>
+    /// <param name="durationSeconds">测试持续时间(秒)</param>
+    /// <param name="warmupSeconds">预热时间(秒)</param>
+    public static void RunNetworkTest(Int32 clientCount = 100, Int32 durationSeconds = 10, Int32 warmupSeconds = 3)
+    {
+        Console.WriteLine("========================================");
+        Console.WriteLine("  网络吞吐量测试(TCP端到端)");
+        Console.WriteLine("========================================");
+        Console.WriteLine();
+        Console.WriteLine($"客户端连接数:{clientCount}");
+        Console.WriteLine($"测试持续时间:{durationSeconds} 秒");
+        Console.WriteLine($"预热时间:{warmupSeconds} 秒");
+        Console.WriteLine();
+
+        // 创建服务端
+        var server = new ApiServer(0)
+        {
+            Log = Logger.Null,
+            EncoderLog = Logger.Null,
+            StatPeriod = 0,
+        };
+        server.Register<BenchController>();
+        server.Start();
+
+        var port = server.Port;
+        Console.WriteLine($"服务端启动完成,端口:{port}");
+
+        // 创建客户端连接
+        Console.Write($"正在创建 {clientCount} 个客户端连接...");
+        var clients = new ApiClient[clientCount];
+        for (var i = 0; i < clientCount; i++)
+        {
+            clients[i] = new ApiClient($"tcp://127.0.0.1:{port}") { Log = Logger.Null };
+            clients[i].Invoke<String[]>("Api/All");
+        }
+        Console.WriteLine(" 完成");
+        Console.WriteLine();
+
+        RunScenario("NoArg_ReturnInt32", clients, clientCount, durationSeconds, warmupSeconds,
+            (client) => client.InvokeAsync<Int32>("Bench/NoArg"));
+
+        RunScenario("EchoPacket_16B", clients, clientCount, durationSeconds, warmupSeconds,
+            (client) => client.InvokeAsync<Packet>("Bench/EchoPacket", new Byte[16]));
+
+        foreach (var client in clients) client?.TryDispose();
+        server.TryDispose();
+    }
+
+    /// <summary>运行服务端纯处理能力测试(绕过TCP网络栈)</summary>
+    /// <param name="threadCount">并发线程数</param>
+    /// <param name="durationSeconds">测试持续时间(秒)</param>
+    /// <param name="warmupSeconds">预热时间(秒)</param>
+    public static void RunDirectTest(Int32 threadCount = 0, Int32 durationSeconds = 10, Int32 warmupSeconds = 3)
+    {
+        if (threadCount <= 0) threadCount = Environment.ProcessorCount;
+
+        Console.WriteLine("========================================");
+        Console.WriteLine("  服务端纯处理能力测试(绕过TCP)");
+        Console.WriteLine("========================================");
+        Console.WriteLine();
+        Console.WriteLine($"并发线程数:{threadCount}");
+        Console.WriteLine($"CPU逻辑核心数:{Environment.ProcessorCount}");
+        Console.WriteLine($"测试持续时间:{durationSeconds} 秒");
+        Console.WriteLine($"预热时间:{warmupSeconds} 秒");
+        Console.WriteLine();
+
+        // 创建服务端(不需要监听端口)
+        var server = new ApiServer(0)
+        {
+            Log = Logger.Null,
+            EncoderLog = Logger.Null,
+            StatPeriod = 0,
+        };
+        server.Register<BenchController>();
+        server.Start();
+
+        var encoder = server.Encoder;
+
+        // 预创建请求消息模板(NoArg场景)
+        Console.Write("预创建请求消息模板...");
+        var noArgTemplate = CreateRequestPayload(encoder, "Bench/NoArg", null);
+        var echoPacketTemplate = CreateRequestPayload(encoder, "Bench/EchoPacket", new Byte[16]);
+        Console.WriteLine(" 完成");
+        Console.WriteLine();
+
+        // 创建模拟会话
+        var sessions = new MockApiSession[threadCount];
+        for (var i = 0; i < threadCount; i++)
+            sessions[i] = new MockApiSession(server);
+
+        // 测试NoArg场景
+        RunDirectScenario("NoArg_ReturnInt32(纯处理)", server, sessions, noArgTemplate, threadCount, durationSeconds, warmupSeconds);
+
+        // 测试EchoPacket场景
+        RunDirectScenario("EchoPacket_16B(纯处理)", server, sessions, echoPacketTemplate, threadCount, durationSeconds, warmupSeconds);
+
+        server.TryDispose();
+
+        Console.WriteLine();
+        Console.WriteLine("========================================");
+        Console.WriteLine("  所有测试完成");
+        Console.WriteLine("========================================");
+    }
+
+    /// <summary>创建请求消息的Payload模板</summary>
+    private static Byte[] CreateRequestPayload(IEncoder encoder, String action, Object? args)
+    {
+        using var msg = encoder.CreateRequest(action, args);
+        return msg.Payload!.ToArray();
+    }
+
+    /// <summary>运行直接处理场景测试</summary>
+    private static void RunDirectScenario(String name, ApiServer server, MockApiSession[] sessions, Byte[] requestTemplate, Int32 threadCount, Int32 durationSeconds, Int32 warmupSeconds)
+    {
+        Console.WriteLine($"--- 场景:{name} ---");
+
+        var totalRequests = 0L;
+        var errors = 0L;
+        var running = true;
+
+        // 预热
+        Console.Write($"  预热 {warmupSeconds} 秒...");
+        var warmupCts = new CancellationTokenSource();
+        var warmupTasks = new Thread[threadCount];
+        for (var i = 0; i < threadCount; i++)
+        {
+            var session = sessions[i];
+            warmupTasks[i] = new Thread(() =>
+            {
+                while (!warmupCts.Token.IsCancellationRequested)
+                {
+                    try
+                    {
+                        var msg = CreateMessage(requestTemplate);
+                        using var rs = server.Process(session, msg, session);
+                        rs?.Payload?.TryDispose();
+                    }
+                    catch { }
+                }
+            });
+            warmupTasks[i].IsBackground = true;
+            warmupTasks[i].Start();
+        }
+        Thread.Sleep(warmupSeconds * 1000);
+        warmupCts.Cancel();
+        foreach (var t in warmupTasks) t.Join(3000);
+        Console.WriteLine(" 完成");
+
+        // GC 基线
+        GC.Collect();
+        GC.WaitForPendingFinalizers();
+        GC.Collect();
+        var gen0Before = GC.CollectionCount(0);
+        var gen1Before = GC.CollectionCount(1);
+        var gen2Before = GC.CollectionCount(2);
+        var memBefore = GC.GetTotalMemory(false);
+
+        // 正式测试
+        var sw = Stopwatch.StartNew();
+        var threads = new Thread[threadCount];
+        for (var i = 0; i < threadCount; i++)
+        {
+            var session = sessions[i];
+            threads[i] = new Thread(() =>
+            {
+                var localCount = 0L;
+                var localErrors = 0L;
+                while (running)
+                {
+                    try
+                    {
+                        var msg = CreateMessage(requestTemplate);
+                        using var rs = server.Process(session, msg, session);
+                        rs?.Payload?.TryDispose();
+                        localCount++;
+                    }
+                    catch
+                    {
+                        localErrors++;
+                    }
+                }
+                Interlocked.Add(ref totalRequests, localCount);
+                Interlocked.Add(ref errors, localErrors);
+            });
+            threads[i].IsBackground = true;
+            threads[i].Start();
+        }
+
+        Thread.Sleep(durationSeconds * 1000);
+        running = false;
+        foreach (var t in threads) t.Join(3000);
+        sw.Stop();
+
+        var gen0After = GC.CollectionCount(0);
+        var gen1After = GC.CollectionCount(1);
+        var gen2After = GC.CollectionCount(2);
+        var memAfter = GC.GetTotalMemory(false);
+
+        var elapsed = sw.Elapsed.TotalSeconds;
+        var rps = totalRequests / elapsed;
+
+        Console.WriteLine($"  总请求数:{totalRequests:N0}");
+        Console.WriteLine($"  错误数:{errors:N0}");
+        Console.WriteLine($"  耗时:{elapsed:F2} 秒");
+        Console.WriteLine($"  吞吐量:{rps:N0} RPC/s");
+        Console.WriteLine($"  每请求分配:{(memAfter - memBefore) * 1.0 / totalRequests:F0} B/req(估算)");
+        Console.WriteLine($"  GC: Gen0={gen0After - gen0Before}, Gen1={gen1After - gen1Before}, Gen2={gen2After - gen2Before}");
+        Console.WriteLine();
+    }
+
+    /// <summary>从模板创建IMessage</summary>
+    private static DefaultMessage CreateMessage(Byte[] template)
+    {
+        var payload = new ArrayPacket(template);
+        return new DefaultMessage { Payload = payload };
+    }
+
+    private static void RunScenario(String name, ApiClient[] clients, Int32 clientCount, Int32 durationSeconds, Int32 warmupSeconds, Func<ApiClient, Task> action)
+    {
+        Console.WriteLine($"--- 场景:{name} ---");
+
+        var totalRequests = 0L;
+        var errors = 0L;
+        var running = true;
+
+        // 预热
+        Console.Write($"  预热 {warmupSeconds} 秒...");
+        var warmupCts = new CancellationTokenSource();
+        var warmupTasks = new Task[clientCount];
+        for (var i = 0; i < clientCount; i++)
+        {
+            var client = clients[i];
+            warmupTasks[i] = Task.Run(async () =>
+            {
+                while (!warmupCts.Token.IsCancellationRequested)
+                {
+                    try { await action(client); } catch { }
+                }
+            });
+        }
+        Thread.Sleep(warmupSeconds * 1000);
+        warmupCts.Cancel();
+        try { Task.WaitAll(warmupTasks, 5000); } catch { }
+        Console.WriteLine(" 完成");
+
+        // GC 基线
+        GC.Collect();
+        GC.WaitForPendingFinalizers();
+        GC.Collect();
+        var gen0Before = GC.CollectionCount(0);
+        var gen1Before = GC.CollectionCount(1);
+        var gen2Before = GC.CollectionCount(2);
+        var memBefore = GC.GetTotalMemory(false);
+
+        var sw = Stopwatch.StartNew();
+        var tasks = new Task[clientCount];
+        for (var i = 0; i < clientCount; i++)
+        {
+            var client = clients[i];
+            tasks[i] = Task.Run(async () =>
+            {
+                while (running)
+                {
+                    try
+                    {
+                        await action(client);
+                        Interlocked.Increment(ref totalRequests);
+                    }
+                    catch
+                    {
+                        Interlocked.Increment(ref errors);
+                    }
+                }
+            });
+        }
+
+        Thread.Sleep(durationSeconds * 1000);
+        running = false;
+        try { Task.WaitAll(tasks, 5000); } catch { }
+        sw.Stop();
+
+        var gen0After = GC.CollectionCount(0);
+        var gen1After = GC.CollectionCount(1);
+        var gen2After = GC.CollectionCount(2);
+        var memAfter = GC.GetTotalMemory(false);
+
+        var elapsed = sw.Elapsed.TotalSeconds;
+        var rps = totalRequests / elapsed;
+        var avgLatencyUs = elapsed * 1_000_000 * clientCount / totalRequests;
+
+        Console.WriteLine($"  总请求数:{totalRequests:N0}");
+        Console.WriteLine($"  错误数:{errors:N0}");
+        Console.WriteLine($"  耗时:{elapsed:F2} 秒");
+        Console.WriteLine($"  吞吐量:{rps:N0} RPC/s");
+        Console.WriteLine($"  平均延迟:{avgLatencyUs:F1} μs/请求");
+        Console.WriteLine($"  GC: Gen0={gen0After - gen0Before}, Gen1={gen1After - gen1Before}, Gen2={gen2After - gen2Before}");
+        Console.WriteLine();
+    }
+}
+
+/// <summary>模拟Api会话,用于绕过TCP直接测试服务端处理能力</summary>
+class MockApiSession : IApiSession, IServiceProvider
+{
+    private readonly ApiServer _host;
+    private IDictionary<String, Object?>? _items;
+
+    public MockApiSession(ApiServer host) => _host = host;
+
+    public IApiHost Host => _host;
+    public DateTime LastActive => DateTime.Now;
+    public IApiSession[] AllSessions => [this];
+    public String? Token { get; set; }
+    public IDictionary<String, Object?> Items => _items ??= new Dictionary<String, Object?>();
+
+    public Object? this[String key]
+    {
+        get => _items != null && _items.TryGetValue(key, out var v) ? v : null;
+        set => Items[key] = value;
+    }
+
+    public Int32 InvokeOneWay(String action, Object? args = null, Byte flag = 0) => 0;
+
+    public Object? GetService(Type serviceType) => (_host as IServiceProvider).GetService(serviceType);
+}
Modified +61 -1
diff --git a/NewLife.Remoting/ApiAction.cs b/NewLife.Remoting/ApiAction.cs
index 61da6e0..3e3f753 100644
--- a/NewLife.Remoting/ApiAction.cs
+++ b/NewLife.Remoting/ApiAction.cs
@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Linq.Expressions;
+using System.Reflection;
 using System.Threading.Tasks;
 using NewLife.Data;
 using NewLife.Log;
@@ -35,6 +36,12 @@ public class ApiAction : IExtend
     /// <summary>是否Accessor返回</summary>
     public Boolean IsAccessorReturn { get; }
 
+    /// <summary>是否无参数方法</summary>
+    public Boolean IsNoParameter { get; }
+
+    /// <summary>预编译的快速调用委托</summary>
+    public Func<Object, Object?[], Object?>? FastInvoker { get; private set; }
+
     /// <summary>处理统计</summary>
     public ICounter StatProcess { get; set; } = new PerfCounter();
 
@@ -67,12 +74,65 @@ public class ApiAction : IExtend
             if (ps[0].ParameterType.As<IAccessor>()) IsAccessorParameter = true;
         }
 
+        IsNoParameter = ps == null || ps.Length == 0;
+
         var returnType = method.ReturnType;
         if (returnType.As(typeof(Task<>)))
             returnType = returnType.GetGenericArguments()[0];
 
         if (returnType.As<IPacket>()) IsPacketReturn = true;
         if (returnType.As<IAccessor>()) IsAccessorReturn = true;
+
+        // 预编译快速调用委托
+        FastInvoker = CompileInvoker(method);
+    }
+
+    /// <summary>使用表达式树编译快速调用委托,避免每次调用走反射</summary>
+    /// <param name="method">方法信息</param>
+    /// <returns>编译后的委托,参数为(instance, args[]),返回Object</returns>
+    private static Func<Object, Object?[], Object?>? CompileInvoker(MethodInfo method)
+    {
+        try
+        {
+            var instanceParam = Expression.Parameter(typeof(Object), "instance");
+            var argsParam = Expression.Parameter(typeof(Object?[]), "args");
+
+            // 转换实例对象类型
+            var instance = method.IsStatic ? null : Expression.Convert(instanceParam, method.DeclaringType!);
+
+            // 构造参数列表
+            var parameters = method.GetParameters();
+            var argExpressions = new Expression[parameters.Length];
+            for (var i = 0; i < parameters.Length; i++)
+            {
+                var index = Expression.ArrayIndex(argsParam, Expression.Constant(i));
+                argExpressions[i] = Expression.Convert(index, parameters[i].ParameterType);
+            }
+
+            // 调用方法
+            var call = method.IsStatic
+                ? Expression.Call(method, argExpressions)
+                : Expression.Call(instance!, method, argExpressions);
+
+            // 处理返回值
+            Expression body;
+            if (method.ReturnType == typeof(void))
+            {
+                body = Expression.Block(call, Expression.Constant(null, typeof(Object)));
+            }
+            else
+            {
+                body = Expression.Convert(call, typeof(Object));
+            }
+
+            var lambda = Expression.Lambda<Func<Object, Object?[], Object?>>(body, instanceParam, argsParam);
+            return lambda.Compile();
+        }
+        catch
+        {
+            // 编译失败时回退到反射调用
+            return null;
+        }
     }
 
     /// <summary>获取名称</summary>
Modified +10 -1
diff --git a/NewLife.Remoting/ApiServer.cs b/NewLife.Remoting/ApiServer.cs
index 64489ef..c2c51d9 100644
--- a/NewLife.Remoting/ApiServer.cs
+++ b/NewLife.Remoting/ApiServer.cs
@@ -263,7 +263,7 @@ public class ApiServer : ApiHost, IServer, IServiceProvider
         if (request == null || request.Action.IsNullOrEmpty()) return null;
 
         // Action动作名必须是Ascii字符,跳过扫描乱码
-        if (!request.Action.All(e => e < 127u)) return null;
+        if (!IsAscii(request.Action)) return null;
 
         // 根据动作名,开始跟踪
         using var span = Tracer?.NewSpan("rps:" + request.Action, request.Data);
@@ -403,5 +403,14 @@ public class ApiServer : ApiHost, IServer, IServiceProvider
 
         return ServiceProvider?.GetService(serviceType)!;
     }
+
+    private static Boolean IsAscii(String str)
+    {
+        for (var i = 0; i < str.Length; i++)
+        {
+            if (str[i] >= 127) return false;
+        }
+        return true;
+    }
     #endregion
 }
\ No newline at end of file
Modified +38 -6
diff --git a/NewLife.Remoting/IApiHandler.cs b/NewLife.Remoting/IApiHandler.cs
index 4417785..76c19b5 100644
--- a/NewLife.Remoting/IApiHandler.cs
+++ b/NewLife.Remoting/IApiHandler.cs
@@ -59,9 +59,9 @@ public class ApiHandler : IApiHandler
             ?? throw new ApiException(ApiCode.Forbidden, $"无法创建名为[{api.Name}]的服务!");
         if (controller is IApi capi) capi.Session = session;
         if (session is INetSession ss)
-            api.LastSession = ss.Remote + "";
+            api.LastSession = ss.Remote?.ToString();
         else
-            api.LastSession = session + "";
+            api.LastSession = session?.ToString();
 
         var st = api.StatProcess;
         var sw = st.StartCount();
@@ -108,6 +108,29 @@ public class ApiHandler : IApiHandler
                 {
                     rs = controller.Invoke(api.Method, args);
                 }
+                else if (api.FastInvoker != null)
+                {
+                    // 使用预编译委托快速调用,避免反射开销
+                    var ps = ctx.ActionParameters;
+                    if (api.IsNoParameter || ps == null || ps.Count == 0)
+                    {
+                        rs = api.FastInvoker(controller, _emptyArgs);
+                    }
+                    else
+                    {
+                        var pis = api.Method.GetParameters();
+                        var pv = new Object?[pis.Length];
+                        for (var i = 0; i < pis.Length; i++)
+                        {
+                            var pn = pis[i].Name;
+                            if (pn != null) ps.TryGetValue(pn, out pv[i]);
+                            // 值类型参数不能为null,否则表达式树Unbox会抛NullReferenceException
+                            if (pv[i] == null && pis[i].ParameterType.IsValueType)
+                                pv[i] = Activator.CreateInstance(pis[i].ParameterType);
+                        }
+                        rs = api.FastInvoker(controller, pv);
+                    }
+                }
                 else
                 {
                     rs = controller.InvokeWithParams(api.Method, ctx.ActionParameters as IDictionary);
@@ -152,6 +175,8 @@ public class ApiHandler : IApiHandler
         return rs;
     }
 
+    private static readonly System.Collections.Concurrent.ConcurrentDictionary<Type, PropertyInfo?> _taskResultCache = new();
+
     private static Object? GetTaskResult(Task task)
     {
         task.GetAwaiter().GetResult();
@@ -159,7 +184,8 @@ public class ApiHandler : IApiHandler
         var taskType = task.GetType();
         if (!taskType.IsGenericType) return null;
 
-        var resultProperty = taskType.GetProperty("Result", BindingFlags.Public | BindingFlags.Instance);
+        var resultProperty = _taskResultCache.GetOrAdd(taskType,
+            t => t.GetProperty("Result", BindingFlags.Public | BindingFlags.Instance));
         return resultProperty?.GetValue(task);
     }
 
@@ -190,6 +216,9 @@ public class ApiHandler : IApiHandler
         // 如果服务只有一个二进制参数,则走快速通道
         if (api.IsPacketParameter) return ctx;
 
+        // 无参数方法,跳过参数解码和绑定
+        if (api.IsNoParameter) return ctx;
+
         // IAccessor参数,直接进行二进制反序列化
         if (api.IsAccessorParameter)
         {
@@ -247,6 +276,9 @@ public class ApiHandler : IApiHandler
         return ctx;
     }
 
+    private static readonly IDictionary<String, Object?> _emptyParameters = new Dictionary<String, Object?>();
+    private static readonly Object?[] _emptyArgs = new Object?[0];
+
     /// <summary>获取接口方法对应的参数值集合</summary>
     /// <param name="method">接口方法</param>
     /// <param name="dic">请求参数</param>
@@ -256,11 +288,11 @@ public class ApiHandler : IApiHandler
     /// <returns></returns>
     protected virtual IDictionary<String, Object?> GetParameterValues(MethodInfo method, IDictionary<String, Object?> dic, Object? raw, Object? args, IEncoder encoder)
     {
-        var ps = new Dictionary<String, Object?>();
-
         // 该方法没有参数,无视外部传入参数
         var pis = method.GetParameters();
-        if (pis == null || pis.Length <= 0) return ps;
+        if (pis == null || pis.Length <= 0) return _emptyParameters;
+
+        var ps = new Dictionary<String, Object?>();
 
         if (pis.Length == 1 && dic.Count == 0)
         {