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

Cli Options

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: Cli Options


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  },

Report

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.