Vì sao tôi tự viết framework load testing riêng
Vì mọi tool tôi thử đều hoặc không test được SignalR cho đúng, hoặc tính toán sai.
Vấn đề
Tôi cần load test một SignalR hub trong điều kiện thật. Hàng nghìn kết nối persistent, nhắn tin hai chiều, gọi hub thật sự.
Mọi tool tôi thử đều rơi vào một trong ba trường hợp:
- Bỏ qua SignalR hoàn toàn - chỉ HTTP, không hỗ trợ WebSocket
- Làm giả - gửi raw WebSocket frame rồi giả vờ đó là SignalR
- Bắt dùng ngôn ngữ khác - viết scenario bằng JavaScript/Go/Python, rồi bằng cách nào đó đem test server .NET của mình
Không cái nào nói SignalR một cách native. Không cái nào dùng đúng Microsoft.AspNetCore.SignalR.Client.
Nên tôi viết một cái làm được việc đó.
AnPerf là gì
AnPerf là một framework performance testing phân tán viết bằng .NET 8.
Scenario load test của bạn chỉ là một class C#. Không DSL. Không ngôn ngữ scripting. Không phải dịch protocol qua YAML. Bạn viết code .NET để test app .NET.
1public class MySignalRScenario : IScenario
2{
3 private readonly HubConnection _hub;
4
5 public MySignalRScenario(string targetUrl)
6 {
7 _hub = new HubConnectionBuilder()
8 .WithUrl($"{targetUrl}/hub")
9 .Build();
10 }
11
12 public async Task ExecuteAsync(IStepContext context)
13 {
14 await context.Step("Connect", async () =>
15 await _hub.StartAsync());
16
17 await context.Step("InvokeMethod", async () =>
18 await _hub.InvokeAsync("SendMessage", "hello"));
19
20 await context.Step("Disconnect", async () =>
21 await _hub.StopAsync());
22 }
23}
Chỉ vậy. SignalR client thật. Hub connection thật. Metrics thật cho từng step.
Kiến trúc

AnPerf có ba tầng:
1. Framework
Engine performance cốt lõi. Lo phần scheduling, metrics, và protocol adapter.
- Scheduler - closed-model (số VU cố định) và open-model (nhắm tới RPS mục tiêu), cả hai đều dùng monotonic timing
- Metrics - histogram sharded lock-free với 256 bucket logarithmic, zero allocation trên hot path
- Protocol adapter - client native cho HTTP, TCP, WebSocket, và SignalR
2. Control Plane
Điều phối phân tán. Một orchestrator quản lý các agent ở xa qua WebSocket.
- Provisioning - SSH vào máy remote, deploy binary của agent, khởi động nó
- Job dispatch - gửi test job tới agent qua WebSocket hai chiều
- Result aggregation - gộp raw histogram bucket từ mọi agent, tính lại percentile
3. CLI
Một tool dòng lệnh duy nhất để điều khiển mọi thứ.
1# Add remote machines
2anperf-ctl add-host --ip 10.0.0.10 --user deploy --key ~/.ssh/id_rsa
3
4# Provision agents via SSH
5anperf-ctl provision --all
6
7# Push your scenario DLL (with content hash - only uploads once)
8anperf-ctl scenario push --name signalr-test --path ./publish/
9
10# Run the test
11anperf-ctl run start --file job.yaml --scenario signalr-test
12
13# Watch it live
14anperf-ctl dashboard
Các tuỳ chọn điều khiển:

Những quyết định thiết kế đáng nói
Hot path lock-free
Mỗi request mà virtual user của bạn gửi đi đều đi qua đường ghi metrics. Nếu đường đó phải lấy lock, bạn đang đo overhead của chính mình - không phải hiệu năng của server.
AnPerf dùng sharded histogram - mỗi thread ghi vào shard riêng, không tranh chấp. Các shard chỉ được gộp lại khi bạn đọc kết quả. Việc ghi là zero-allocation: không new, không boxing, không thao tác string.
Monotonic timing
Đừng bao giờ dùng DateTime.UtcNow để đo latency. Nó trôi, nó nhảy, nó nói dối.
AnPerf dùng Stopwatch.GetTimestamp() ở mọi chỗ. Monotonic. Độ phân giải nanosecond. Không có bất ngờ nào.
Gộp percentile cho đúng
Đây là chỗ hầu hết các tool làm sai.
Khi bạn có 3 agent lần lượt báo p99 = 12ms, 14ms, 11ms - thì p99 tổng hợp không phải trung bình của chúng (12.3ms). Con số đó vô nghĩa về mặt thống kê.
Agent của AnPerf gửi mảng raw histogram bucket (long[256]) về orchestrator. Orchestrator cộng số đếm của từng bucket theo từng phần tử, rồi tính lại percentile từ phân phối đã gộp. Đúng về mặt toán học.
Agent 1: [0, 0, 5, 12, 45, 102, ...] // 256 buckets
Agent 2: [0, 0, 3, 18, 52, 98, ...]
Agent 3: [0, 0, 7, 15, 41, 110, ...]
─────────────────────────────────────
Merged: [0, 0, 15, 45, 138, 310, ...] → recompute p50, p90, p99
Push scenario ≠ chạy test
Upload một DLL 50MB lên 10 agent thì mất thời gian. Còn chạy test thì phải tức thì.
AnPerf tách hai việc đó: scenario push upload qua SSH kèm dedup theo content hash (không đổi thì bỏ qua). run start chỉ gửi một lệnh WebSocket. Lần chạy thứ hai? Tức thì - không cần upload lại.
Report từ Orchestrator
Hiện tại report hỗ trợ định dạng JSON.
1 "summary": {
2 "total_requests": 34861,
3 "total_errors": 0,
4 "error_rate_pct": 0,
5 "throughput_rps": 1809.9,
6 "agent_count": 1,
7 "total_vus": 0
8 },

Một lần chạy thử bằng AnPerf (scenario E2E gồm SignalR + HTTP):
Ở khoảng ~1.8k RPS với một agent duy nhất, hệ thống giữ được latency dưới một millisecond trên toàn bộ luồng. Các step HTTP luôn ở quanh ~0.5 ms tại p50 và dưới 1 ms tại p99, cho thấy không có tranh chấp hay xếp hàng dưới tải. Latency push của SignalR gần như tức thì (p99 ~0.013 ms), thể hiện một pipeline real-time hiệu quả, không bị dồn ứ khi phát tin.
Có vài outlier (~40 ms) xuất hiện ở phần đuôi cực đại, nhưng chúng hiếm và không ảnh hưởng tới trải nghiệm người dùng nói chung.
Đây là một baseline tốt cho một hệ thống real-time low-latency.
Một lần chạy test trông như thế nào
Định nghĩa workload trong YAML:
1scenario:
2 name: "MySignalRScenario"
3 targetUrl: "http://target-server:5000"
4
5workload:
6 model: "closed"
7 stages:
8 - kind: "rampUp"
9 duration: "30s"
10 startTarget: 1
11 target: 200
12 - kind: "steady"
13 duration: "5m"
14 target: 200
15
16agents:
17 count: 3
18 labels:
19 region: "ap-southeast-2"
Dashboard trực tiếp hiện mọi thứ:
(Tôi sẽ cập nhật hình sau)
┌─ Active Run ────────────────────────────────────────────────────────┐
│ Run: run-01 Scenario: signalr-test State: ● Running │
│ │
│ Agent Status VUs RPS Errors p99 (ms) │
│ agent-1-abc123 Running 200 4,521 3 12.4 │
│ agent-2-def456 Running 200 4,388 1 13.1 │
│ agent-3-ghi789 Running 200 4,102 0 11.8 │
│ │
│ ─── Aggregate ─────────────────────────────────────────────────── │
│ Total VUs: 600 RPS: 13,011 Errors: 4 p50: 2.1 p99: 12.8 │
└───────────────────────────────────────────────────────────────────────┘
Chi tiết theo từng agent. Metrics tổng hợp được tính đúng. Refresh real-time.
Trạng thái hiện tại
Đây là v1. Nó chạy được. Nó chạy rất tốt.
Nhưng chưa open-source.
Phần bên trong thì chắc chắn - metrics lock-free, tổng hợp đúng, kiến trúc phân tán gọn gàng. Nhưng trải nghiệm cho người dùng thì chưa đạt mức tôi muốn. Set up vẫn còn quá nhiều bước thủ công. Tài liệu thì chưa có.
Kế hoạch: làm mượt phần setup, viết tài liệu đàng hoàng, rồi open-source khi nó đủ tốt để bất kỳ ai cũng dùng được - không chỉ riêng tôi.
Tôi học được gì
Làm cái này dạy tôi về hiệu năng nhiều hơn bất kỳ bài blog hay khoá học nào:
- Bạn không thể đo hiệu năng bằng một tool đang có vấn đề về hiệu năng. Hạ tầng đo phải nhanh hơn thứ bạn đem đo.
- Lập trình lock-free thì khó nhưng cần thiết. Một khi đã thấy tranh chấp làm gì với số đo của bạn ở quy mô lớn, bạn không thể coi như chưa thấy nữa.
- Tổng hợp phân tán có phần toán thật sự. Lấy trung bình percentile là sai. Lấy trung bình của trung bình cũng sai. Bạn cần dữ liệu thô.
- Tách bạch trách nhiệm giúp tiết kiệm thời gian. Tách upload khỏi execution, tách provisioning khỏi runtime - mỗi quyết định đều làm hệ thống đơn giản hơn và nhanh hơn.
Tiếp theo là gì
- Làm mượt trải nghiệm CLI
- Viết tài liệu và hướng dẫn bắt đầu
- Thêm protocol adapter
- Open-source
Nếu bạn quan tâm tới hiệu năng .NET, hệ thống phân tán, hay load testing - hãy theo dõi tiếp.
Xây bằng .NET 8, test với SignalR hub thật, và đo cho đúng.
