measurement: name the kinds by protocol and action, add the http_request kind and QUIC settings

This commit is contained in:
justme committed 2026-10-22 07:02:23 +08:00
1 parent bf2a598891
commit e77880d372
31 files changed
+938 -141

No files matched your search

@@ -10,7 +10,7 @@ public static class Kinds
/// <summary>这个任务可能达到的最高探测频率(Hz):自适应时是 <c>rate_max_hz</c>,否则是固定的 <c>rate_hz</c>。预算、限速和准入都按它算。</summary>
public static long PeakRateHz(string kindId, ParamValues values) =>
(kindId is "udp_echo" or "tcp_echo") && values.GetBool("adaptive") ? Math.Max(values.GetInt("rate_max_hz"), values.GetInt("rate_min_hz")) : values.GetInt("rate_hz");
(kindId is "quic_ping" or "tcp_ping") && values.GetBool("adaptive") ? Math.Max(values.GetInt("rate_max_hz"), values.GetInt("rate_min_hz")) : values.GetInt("rate_hz");
private static readonly StreamSpec s_echoMsg = new(
"msg",
@@ -30,8 +30,8 @@ public static class Kinds
],
Union: true);
public static readonly KindSpec UdpEcho = new(
"udp_echo",
public static readonly KindSpec QuicPing = new(
"quic_ping",
"QUIC Datagram echo",
"Fixed-rate QUIC Datagram echo with four timestamps per packet.",
Scope.Line,
@@ -47,10 +47,10 @@ public static class Kinds
P.Int("port", 7443, 1, 65535),
],
[s_echoMsg],
p => Bandwidth.UdpEcho(PeakRateHz("udp_echo", p), p.GetInt("size")));
p => Bandwidth.UdpEcho(PeakRateHz("quic_ping", p), p.GetInt("size")));
public static readonly KindSpec UdpTrain = new(
"udp_train",
public static readonly KindSpec QuicStream = new(
"quic_stream",
"QUIC Datagram train",
"Fixed-rate or laddered one-way QUIC Datagram stream (Agent to 拨号方) for loss and delay variation under load.",
Scope.Line,
@@ -68,8 +68,8 @@ public static class Kinds
],
p => (long)(p.GetNumber("mbps") * 1_000_000));
public static readonly KindSpec TcpEcho = new(
"tcp_echo",
public static readonly KindSpec TcpPing = new(
"tcp_ping",
"TCP echo",
"Small TCP messages echoed with four timestamps, plus periodic kernel tcp_info samples.",
Scope.Line,
@@ -88,10 +88,10 @@ public static class Kinds
P.Bool("tls", false),
],
[s_echoMsg, s_tcpInfo],
p => Bandwidth.TcpEcho(PeakRateHz("tcp_echo", p), p.GetInt("max")));
p => Bandwidth.TcpEcho(PeakRateHz("tcp_ping", p), p.GetInt("max")));
public static readonly KindSpec TcpBulk = new(
"tcp_bulk",
public static readonly KindSpec TcpTransfer = new(
"tcp_transfer",
"TCP bulk",
"Bulk TCP transfer on several streams with periodic byte counters and tcp_info.",
Scope.Line,
@@ -110,8 +110,8 @@ public static class Kinds
],
_ => 0);
public static readonly KindSpec Connect = new(
"connect",
public static readonly KindSpec TcpConnect = new(
"tcp_connect",
"Connect",
"Repeated TCP and TLS connection establishment with per-phase timings and outcome.",
Scope.Line | Scope.Target,
@@ -129,8 +129,8 @@ public static class Kinds
],
p => p.GetInt("per_minute") * 4000 / 60);
public static readonly KindSpec Hold = new(
"hold",
public static readonly KindSpec TlsHold = new(
"tls_hold",
"Hold",
"A long-lived TLS connection with periodic heartbeats; records resets and lifetime.",
Scope.Line,
@@ -144,8 +144,38 @@ public static class Kinds
],
p => 2000 / Math.Max(1, p.GetInt("heartbeat_s")) * 8);
public static readonly KindSpec IcmpEcho = new(
"icmp_echo",
public static readonly KindSpec HttpRequest = new(
"http_request",
"HTTP request",
"One Agent requests a URL over HTTP/1.1, HTTP/2 or HTTP/3 and records DNS, TCP, time to first byte, total time and status. Target is the URL.",
Scope.Target,
new BlockSet("http", "fixed", "fixed", "request", []),
[
P.Enum("version", "h2", ["h1", "h2", "h3"], "HTTP version; the request fails instead of falling back."),
P.Enum("method", "GET", ["GET", "HEAD"]),
P.Bool("adaptive", true, "Adapt the request rate to the failure rate: low while the target answers, up to rate_max_per_min when it fails, exponential back-off while it is unreachable."),
P.Int("per_minute", 6, 1, 120, "/min", "Fixed rate, used when adaptive is false."),
P.Int("rate_min_per_min", 1, 1, 120, "/min"),
P.Int("rate_max_per_min", 12, 1, 120, "/min", "Highest rate, and the reference grid for sample weights."),
P.Number("tau_pct", 5, 0.1, 50, "%", "Failure rate a quiet target has to be able to prove below, at 95% confidence."),
P.Int("timeout_ms", 10000, 500, 60000, "ms"),
P.Int("read_kb", 16, 0, 1024, "KiB", "Body bytes to read before the request counts as finished (0: headers only)."),
P.Int("ok_status", 0, 0, 599, null, "Status that counts as success; 0 means any status below 400."),
P.Bool("tls_verify", true),
],
[
new StreamSpec(
"req",
[
Col.Seq("attempt"), Col.TsDialer("ts"), Col.U16("w"),
Col.U32("dns_us", nullable: true), Col.U32("tcp_us", nullable: true), Col.U32("ttfb_us", nullable: true), Col.U32("total_us", nullable: true),
Col.U16("status", nullable: true), Col.U8("proto", nullable: true), Col.U8("outcome"),
]),
],
p => p.GetInt("rate_max_per_min") * (2000 + p.GetInt("read_kb") * 1024) * 8 / 60);
public static readonly KindSpec IcmpPing = new(
"icmp_ping",
"ICMP echo",
"拨号方 or Agent side ICMP echo to a target address.",
Scope.Target,
@@ -159,7 +189,7 @@ public static class Kinds
],
p => Bandwidth.UdpEcho(p.GetInt("rate_hz"), p.GetInt("size")));
public static readonly IReadOnlyList<KindSpec> All = [UdpEcho, UdpTrain, TcpEcho, TcpBulk, Connect, Hold, IcmpEcho];
public static readonly IReadOnlyList<KindSpec> All = [QuicPing, QuicStream, TcpPing, TcpTransfer, TcpConnect, TlsHold, HttpRequest, IcmpPing];
public static KindSpec? Find(string id)
{
@@ -488,7 +488,7 @@ public sealed class ControlHealth
/// <summary>任务层:测量自己使用的链路。TCP 类积木自带套接字和重拨,QUIC 数据通道由拨号方单独维护。</summary>
public sealed class TaskHealth
{
/// <summary>正在运行的 TCP 类线路积木数(tcp_echo、connect、hold、tcp_bulk)。</summary>
/// <summary>正在运行的 TCP 类线路积木数(tcp_ping、connect、hold、tcp_transfer)。</summary>
[JsonPropertyName("tcp_blocks")]
public int TcpBlocks { get; set; }
@@ -496,7 +496,7 @@ public sealed class TaskHealth
public QuicHealth Quic { get; set; } = new();
}
/// <summary>QUIC 数据通道(udp_echo、udp_train 使用)的状态、握手统计和传输层统计。</summary>
/// <summary>QUIC 数据通道(quic_ping、quic_stream 使用)的状态、握手统计和传输层统计。</summary>
public sealed class QuicHealth
{
[JsonPropertyName("state")]
@@ -0,0 +1,104 @@
using System.Globalization;
using System.Net;
namespace MoShiro.Protocol;
/// <summary>
/// 单个 Agent 任务的目标:<c>http</c> 是 <c>http(s)://主机[:端口][/路径]</c>,其余 Kind 是 <c>主机[:端口]</c>(IP 或域名,IPv6 带端口时用方括号)。
/// Backend 只检查格式,不做 DNS 解析;解析在 Agent 上进行,每个解析出的地址都要在 <c>allowed_target_cidrs</c> 里。
/// </summary>
public static class TargetHost
{
public const string HttpKind = "http_request";
/// <summary>解析目标;<paramref name="port"/> 为 0 表示没有写端口,<paramref name="url"/> 只在 http(s) URL 时有值。</summary>
public static bool TryParse(string? target, out string host, out int port, out Uri? url)
{
host = "";
port = 0;
url = null;
if (string.IsNullOrWhiteSpace(target) || target.Length > 2048 || target.Any(char.IsWhiteSpace))
{
return false;
}
if (target.Contains("://", StringComparison.Ordinal))
{
if (!Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) || uri.Scheme is not ("http" or "https") || uri.UserInfo.Length > 0 || !ValidHost(uri.IdnHost))
{
return false;
}
url = uri;
host = uri.IdnHost.Trim('[', ']');
port = uri.IsDefaultPort ? 0 : uri.Port;
return true;
}
// IPAddress.TryParse 会把「[v6]:端口」的端口丢掉,带方括号的先按端点解析
if (!target.StartsWith('[') && IPAddress.TryParse(target, out IPAddress? ip))
{
host = ip.ToString();
return true;
}
if (IPEndPoint.TryParse(target, out IPEndPoint? endpoint))
{
if (endpoint.Port is 0)
{
return false;
}
host = endpoint.Address.ToString();
port = endpoint.Port;
return true;
}
int colon = target.LastIndexOf(':');
string name = colon < 0 ? target : target[..colon];
if (colon >= 0 && (!int.TryParse(target.AsSpan(colon + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) || port is < 1 or > 65535))
{
return false;
}
if (!ValidHost(name))
{
return false;
}
host = name.ToLowerInvariant();
return true;
}
/// <summary>按 Kind 检查并规范化:<c>http</c> 必须是 http(s) URL,其余 Kind 必须是主机[:端口];不合格返回 null。</summary>
public static string? Normalize(string kindId, string? target)
{
if (!TryParse(target, out string host, out int port, out Uri? url) || (url is not null) != (kindId == HttpKind))
{
return null;
}
if (url is not null)
{
return url.AbsoluteUri;
}
string shown = IPAddress.TryParse(host, out IPAddress? ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 && port != 0 ? "[" + host + "]" : host;
return port == 0 ? host : shown + ":" + port.ToString(CultureInfo.InvariantCulture);
}
private static bool ValidHost(string name)
{
if (IPAddress.TryParse(name.Trim('[', ']'), out _))
{
return true;
}
if (name.Length is 0 or > 253 || name[^1] == '-' || name[0] is '-' or '.')
{
return false;
}
return name.TrimEnd('.').Split('.').All(label => label.Length is > 0 and <= 63 && label.All(c => char.IsAsciiLetterOrDigit(c) || c == '-'));
}
}
@@ -8,7 +8,7 @@ public enum MeasurementDatagramType : byte
EchoRequest = 1,
EchoReply = 2,
/// <summary>单向数据(udp_train),以及 拨号方 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
/// <summary>单向数据(quic_stream),以及 拨号方 为了保持 NAT 映射和让 Agent 学到源地址而发的探活包。</summary>
Data = 3,
}