agent: unify listening and dialing with directed mutual measurements
Replace Probe with Agent across node runtime, Fleet contracts, backend state, API and storage. Report incoming sessions and outgoing link health through each Agent uplink, and retain directed link controls and retry. Update protocol fixtures and end-to-end coverage for the unified model; accept requested cancellation during the Fleet timeout test cleanup.
This commit is contained in:
1 parent
d5ebf29e57
commit
ad5efc62ff
206 files changed
+4202
-4964
No files matched your search
@@ -3,7 +3,7 @@ using MoShiro.Protocol;
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>
|
||||
/// 一种采集方法(Kind)的两个角色:拨号方(Probe)和监听方(Agent),以及它能不能作为目标任务(Agent 自己探测一个地址,没有对端)运行。
|
||||
/// 一种采集方法(Kind)的两个角色:拨号方和监听方,以及它能不能作为目标任务(节点自己探测一个地址,没有对端)运行。
|
||||
/// 积木只按机制命名、只产生原始列,不知道上层的方案或协议。
|
||||
/// </summary>
|
||||
public sealed record BlockPair(
|
||||
@@ -19,7 +19,7 @@ public sealed record BlockPair(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 积木目录:新增一种采集方法 = 在 <see cref="Kinds"/> 里加一条声明,在这里加一行,实现它的两个角色。Probe 和 Agent 同时获得。
|
||||
/// 积木目录:新增一种采集方法 = 在 <see cref="Kinds"/> 里加一条声明,在这里加一行,实现它的两个角色。所有节点同时获得。
|
||||
/// hello 里上报的 capabilities、本地预算检查的支持列表都从这里推出来,不再手写。
|
||||
/// </summary>
|
||||
public static class BlockCatalog
|
||||
@@ -37,9 +37,12 @@ public static class BlockCatalog
|
||||
|
||||
public static BlockPair? Find(string kind) => All.FirstOrDefault(p => p.Kind == kind);
|
||||
|
||||
/// <summary>这个构建的 Agent 能运行的 Kind(监听方,或目标任务)。</summary>
|
||||
/// <summary>不监听的节点能运行的 Kind:只有目标任务(节点自己探测一个地址)。</summary>
|
||||
public static IReadOnlyList<string> TargetKinds { get; } = [.. All.Where(p => p.RunsAsTarget).Select(p => p.Kind)];
|
||||
|
||||
/// <summary>监听的节点能运行的 Kind(监听方,或目标任务)。</summary>
|
||||
public static IReadOnlyList<string> AgentKinds { get; } = [.. All.Where(p => p.Listener is not null || p.RunsAsTarget).Select(p => p.Kind)];
|
||||
|
||||
/// <summary>这个构建的 Probe 能运行的 Kind(有监听方可以配对的拨号方)。</summary>
|
||||
public static IReadOnlyList<string> ProbeKinds { get; } = [.. All.Where(p => p.Listener is not null).Select(p => p.Kind)];
|
||||
/// <summary>这个构建能作为拨号方运行的 Kind(有监听方可以配对的拨号方)。</summary>
|
||||
public static IReadOnlyList<string> DialerKinds { get; } = [.. All.Where(p => p.Listener is not null).Select(p => p.Kind)];
|
||||
}
|
||||
@@ -56,8 +56,8 @@ internal static class BulkIo
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量传输,拨号方(Probe):开 <c>streams</c> 条 TCP 连接(可选 TLS),按 <c>direction</c> 发送(fwd)或接收(rev),持续 <c>duration_s</c>。
|
||||
/// 记录每个采样间隔每条连接的<b>累计字节数</b>(<c>bytes</c> 流,Probe 的时钟),Backend 据此求每个间隔的吞吐;
|
||||
/// 批量传输,拨号方:开 <c>streams</c> 条 TCP 连接(可选 TLS),按 <c>direction</c> 发送(fwd)或接收(rev),持续 <c>duration_s</c>。
|
||||
/// 记录每个采样间隔每条连接的<b>累计字节数</b>(<c>bytes</c> 流,拨号方 的时钟),Backend 据此求每个间隔的吞吐;
|
||||
/// 同时采样第一条连接自己这一侧的内核 <c>tcp_info</c>(多条连接的累计计数器不能混在同一个序列里求差)。
|
||||
/// 发送速率不超过本地上限 <see cref="BlockEnv.MaxBulkMbps"/>。
|
||||
/// </summary>
|
||||
@@ -76,8 +76,8 @@ public sealed class TcpBulkDialer : BlockBase
|
||||
: base(env, spec)
|
||||
{
|
||||
long expectedRows = spec.Int("streams") * spec.Int("duration_s") * Math.Max(1, 1000 / Math.Max(1, spec.Int("sample_ms")));
|
||||
_log = new DriverLog(spec.Kind.FindStream("bytes")!, Side.Probe, (int)Math.Clamp(expectedRows + 256, 1024, 262_144));
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Probe, env.Clock);
|
||||
_log = new DriverLog(spec.Kind.FindStream("bytes")!, Side.Dialer, (int)Math.Clamp(expectedRows + 256, 1024, 262_144));
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Dialer, env.Clock);
|
||||
_ts = _log.Store.ColumnIndex("ts");
|
||||
_conn = _log.Store.ColumnIndex("conn");
|
||||
_bytes = _log.Store.ColumnIndex("bytes");
|
||||
@@ -211,7 +211,7 @@ public sealed class TcpBulkDialer : BlockBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量传输,监听方(Agent):接受连接,<c>rev</c> 时向 Probe 发送(受本地码率上限约束),<c>fwd</c> 时接收并丢弃;
|
||||
/// 批量传输,监听方:接受连接,<c>rev</c> 时向 拨号方 发送(受本地码率上限约束),<c>fwd</c> 时接收并丢弃;
|
||||
/// 采样第一条连接自己这一侧的内核 <c>tcp_info</c>(Agent 的重传反映下行方向,代理场景里大流量走这个方向)。
|
||||
/// 连接在 <c>duration_s</c> 之后(加几秒余量)或阶段结束时关闭。
|
||||
/// </summary>
|
||||
@@ -224,7 +224,7 @@ public sealed class TcpBulkListener : BlockBase
|
||||
public TcpBulkListener(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
{
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Agent, env.Clock);
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Listener, env.Clock);
|
||||
}
|
||||
|
||||
public override RowStore? Records(string streamName) => streamName == "tcp_info" ? _info.Rows : null;
|
||||
|
||||
@@ -34,7 +34,7 @@ internal sealed class EchoRows
|
||||
_graceUs = graceUs;
|
||||
_settleOnReply = settleOnReply;
|
||||
StreamSpec spec = kind.FindStream("msg")!;
|
||||
_log = new DriverLog(spec, Side.Probe, capacity);
|
||||
_log = new DriverLog(spec, Side.Dialer, capacity);
|
||||
_size = _log.Store.ColumnIndex("size");
|
||||
_t1 = _log.Store.ColumnIndex("t1");
|
||||
_t4 = _log.Store.ColumnIndex("t4");
|
||||
@@ -76,7 +76,7 @@ internal sealed class EchoRows
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDP 回显,拨号方(Probe):按声明的速率发包,t1 在发出时记录,回复到达时回填 t4。回复丢失的行 t4 缺失(Agent 一侧再补 t2/t3)。
|
||||
/// UDP 回显,拨号方:按声明的速率发包,t1 在发出时记录,回复到达时回填 t4。回复丢失的行 t4 缺失(Agent 一侧再补 t2/t3)。
|
||||
/// </summary>
|
||||
public sealed class DatagramEchoDialer : BlockBase
|
||||
{
|
||||
@@ -163,7 +163,7 @@ public sealed class DatagramEchoDialer : BlockBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>UDP 回显,监听方(Agent):校验后立即回复一个等长的包,记录 t2(到达)和 t3(回复发出),按序号留给合并。</summary>
|
||||
/// <summary>UDP 回显,监听方:校验后立即回复一个等长的包,记录 t2(到达)和 t3(回复发出),按序号留给合并。</summary>
|
||||
public sealed class DatagramEchoListener : BlockBase
|
||||
{
|
||||
private readonly RowStore _records;
|
||||
@@ -171,7 +171,7 @@ public sealed class DatagramEchoListener : BlockBase
|
||||
public DatagramEchoListener(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
{
|
||||
_records = new RowStore(spec.Kind.FindStream("msg")!, Side.Agent, 32768);
|
||||
_records = new RowStore(spec.Kind.FindStream("msg")!, Side.Listener, 32768);
|
||||
}
|
||||
|
||||
public override RowStore? Records(string streamName) => streamName == "msg" ? _records : null;
|
||||
@@ -216,7 +216,7 @@ public sealed class TcpEchoDialer : BlockBase
|
||||
: base(env, spec)
|
||||
{
|
||||
_rows = new EchoRows(spec.Kind, 16384, EchoRows.TcpGraceUs, settleOnReply: true);
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Probe, env.Clock);
|
||||
_info = new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Dialer, env.Clock);
|
||||
}
|
||||
|
||||
public override long Produced => _rows.Count;
|
||||
@@ -341,8 +341,8 @@ public sealed class TcpEchoListener : BlockBase
|
||||
public TcpEchoListener(BlockEnv env, BlockSpec spec, bool record = true)
|
||||
: base(env, spec)
|
||||
{
|
||||
_records = record ? new RowStore(spec.Kind.FindStream("msg")!, Side.Agent, 32768) : null;
|
||||
_info = record ? new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Agent, env.Clock) : null;
|
||||
_records = record ? new RowStore(spec.Kind.FindStream("msg")!, Side.Listener, 32768) : null;
|
||||
_info = record ? new TcpInfoRecorder(spec.Kind.FindStream("tcp_info")!, Side.Listener, env.Clock) : null;
|
||||
}
|
||||
|
||||
public override RowStore? Records(string streamName) => streamName switch
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed class IdleListener : BlockBase
|
||||
|
||||
/// <summary>
|
||||
/// 反复建立连接(TCP,可选 TLS)并记录每个阶段的耗时和结果。结果编码与 Backend 的 <c>ConnectOutcome</c> 一致:
|
||||
/// 0 成功、1 超时、2 被重置或拒绝、3 TLS 告警、4 其他。拨号方是 Probe 时对端是 Agent(按指纹固定证书,建好连接即关闭);
|
||||
/// 0 成功、1 超时、2 被重置或拒绝、3 TLS 告警、4 其他。拨号方连接监听方时(按指纹固定证书,建好连接即关闭);
|
||||
/// 作为目标任务在 Agent 上运行时对端是任意地址(不校验证书,只测握手耗时)。
|
||||
/// </summary>
|
||||
public sealed class ConnectDialer : BlockBase
|
||||
@@ -34,7 +34,7 @@ public sealed class ConnectDialer : BlockBase
|
||||
public ConnectDialer(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
{
|
||||
_log = new DriverLog(spec.Kind.FindStream("conn")!, Side.Probe, 1024);
|
||||
_log = new DriverLog(spec.Kind.FindStream("conn")!, Side.Dialer, 1024);
|
||||
_attempt = _log.Store.ColumnIndex("attempt");
|
||||
_ts = _log.Store.ColumnIndex("ts");
|
||||
_tcp = _log.Store.ColumnIndex("tcp_us");
|
||||
@@ -147,7 +147,7 @@ public sealed class HoldDialer : BlockBase
|
||||
public HoldDialer(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
{
|
||||
_log = new DriverLog(spec.Kind.FindStream("hold")!, Side.Probe, 1024);
|
||||
_log = new DriverLog(spec.Kind.FindStream("hold")!, Side.Dialer, 1024);
|
||||
_ts = _log.Store.ColumnIndex("ts");
|
||||
_event = _log.Store.ColumnIndex("event");
|
||||
_lifetime = _log.Store.ColumnIndex("lifetime_ms");
|
||||
@@ -293,7 +293,7 @@ public sealed class IcmpEchoSolo : BlockBase
|
||||
public IcmpEchoSolo(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
{
|
||||
_log = new DriverLog(spec.Kind.FindStream("msg")!, Side.Probe, 1024);
|
||||
_log = new DriverLog(spec.Kind.FindStream("msg")!, Side.Dialer, 1024);
|
||||
_seq = _log.Store.ColumnIndex("seq");
|
||||
_size = _log.Store.ColumnIndex("size");
|
||||
_t1 = _log.Store.ColumnIndex("t1");
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>
|
||||
/// 单向数据流(UDP train),监听方(Agent,行的主导方):按声明的码率均匀发包,发出的每个包 (seq, size, t_send) 记在按序号的记录里,
|
||||
/// Probe 的接收时刻之后按序号合并(没有到达的就是丢失)。发送在专用线程上完成,不占用线程池。
|
||||
/// 拨号方 的接收时刻之后按序号合并(没有到达的就是丢失)。发送在专用线程上完成,不占用线程池。
|
||||
/// 等不到对端的地址(5 秒内没有任何认证过的包)就以 <see cref="BlockFaultException"/> 结束。
|
||||
/// </summary>
|
||||
public sealed class TrainListener : BlockBase
|
||||
@@ -21,7 +21,7 @@ public sealed class TrainListener : BlockBase
|
||||
: base(env, spec)
|
||||
{
|
||||
double pps = spec.Params.GetNumber("mbps") * 1_000_000 / 8 / Math.Max(1, spec.Int("size"));
|
||||
_records = new RowStore(spec.Kind.FindStream("pkt")!, Side.Agent, (int)Math.Clamp(pps * spec.Int("duration_s") * 1.25, 1024, 4_000_000));
|
||||
_records = new RowStore(spec.Kind.FindStream("pkt")!, Side.Listener, (int)Math.Clamp(pps * spec.Int("duration_s") * 1.25, 1024, 4_000_000));
|
||||
}
|
||||
|
||||
public override long Produced => Interlocked.Read(ref _sent);
|
||||
@@ -30,7 +30,7 @@ public sealed class TrainListener : BlockBase
|
||||
|
||||
public override async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
// Probe 开始前发来的探活包(Data)要被接受,准入策略才能据此学到它当前的源地址(NAT 映射);它们本身没有内容
|
||||
// 拨号方 开始前发来的探活包(Data)要被接受,准入策略才能据此学到它当前的源地址(NAT 映射);它们本身没有内容
|
||||
Env.Datagrams!.Register(Spec.StreamId, static (in QuicDatagramDelivery _) => { });
|
||||
try
|
||||
{
|
||||
@@ -138,7 +138,7 @@ public sealed class TrainListener : BlockBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单向数据流,拨号方(Probe,行的跟随方):接收,记录每个序号第一次到达的时刻(按序号索引,热路径不分配);
|
||||
/// 单向数据流,拨号方(行的跟随方):接收,记录每个序号第一次到达的时刻(按序号索引,热路径不分配);
|
||||
/// 序号超过本地包速率上限推算出的范围时忽略。阶段结束时按序号范围切成多个列块交给 Agent,每块不超过 <see cref="ProtocolLimits.MaxRecordsPerChunk"/> 的记录。
|
||||
/// 开始时先发几个探活包,让 Agent 学到我们当前的源地址(NAT 映射)再开始发送。
|
||||
/// </summary>
|
||||
@@ -159,9 +159,9 @@ public sealed class TrainDialer : BlockBase
|
||||
double pps = spec.Params.GetNumber("mbps") * 1_000_000 / 8 / Math.Max(1, spec.Int("size"));
|
||||
long seconds = Math.Max(1, spec.Int("duration_s"));
|
||||
_maxSeq = Math.Max(1024, Math.Min((long)(pps * seconds * 1.25), env.MaxPacketsPerSecond * seconds * 3 / 2));
|
||||
_received = new RowStore(spec.Kind.FindStream("pkt")!, Side.Probe, (int)_maxSeq);
|
||||
_received = new RowStore(spec.Kind.FindStream("pkt")!, Side.Dialer, (int)_maxSeq);
|
||||
_tRecv = _received.ColumnIndex("t_recv");
|
||||
_step = new RowStore(spec.Kind.FindStream("step")!, Side.Probe, 16);
|
||||
_step = new RowStore(spec.Kind.FindStream("step")!, Side.Dialer, 16);
|
||||
}
|
||||
|
||||
public override async Task RunAsync(CancellationToken ct)
|
||||
|
||||
@@ -51,7 +51,7 @@ public interface IDataSessions
|
||||
DataSession? Find(long sessionId);
|
||||
}
|
||||
|
||||
/// <summary>只有一个会话(拨号方:Probe 与某个 Agent 的会话)。</summary>
|
||||
/// <summary>只有一个会话(拨号方:与某个监听方的会话)。</summary>
|
||||
public sealed class SingleSession : IDataSessions
|
||||
{
|
||||
private readonly DataSession _session;
|
||||
@@ -113,7 +113,7 @@ public sealed class DataPlaneCounters
|
||||
public sealed record FragmentScope(TaskId? Task, TestId? Test, int? Phase);
|
||||
|
||||
/// <summary>
|
||||
/// 积木交出一侧的行的地方。Probe:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。
|
||||
/// 积木交出一侧的行的地方。拨号方:放进发给 Agent 的发件箱(列块);Agent 自己运行的积木(目标任务):直接盖章上传。
|
||||
/// </summary>
|
||||
public interface IFragmentSink
|
||||
{
|
||||
@@ -139,7 +139,7 @@ public sealed class BlockFaultException : Exception
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方 = Probe,监听方 = Agent),写在同一个文件里,
|
||||
/// 一个运行中的积木角色。同一种采集方法的两端是一对角色(拨号方和监听方),写在同一个文件里,
|
||||
/// 共用包的构造、校验、行的记录;它们只产生<b>原始列</b>,不做任何统计,也不知道上层的方案或协议。
|
||||
/// </summary>
|
||||
public interface IBlock
|
||||
@@ -192,7 +192,7 @@ public sealed class BlockSpec
|
||||
public string ConfigSignature => KindId + "|" + Port + "|" +
|
||||
string.Join(',', Params.Values.OrderBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + "=" + p.Value.GetRawText()));
|
||||
|
||||
/// <summary>参数、端口或流编号变化时重启:Probe 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。</summary>
|
||||
/// <summary>参数、端口或流编号变化时重启:拨号方 据此判断 Agent 下发的任务有没有变(Agent 重启后流编号会变)。</summary>
|
||||
public string Signature => StreamId + "|" + ConfigSignature;
|
||||
|
||||
/// <summary>校验参数(补全默认值)后构造;Kind 不存在或参数非法返回 null。</summary>
|
||||
@@ -216,7 +216,7 @@ public sealed class BlockEnv
|
||||
|
||||
public required INodeLog Log { get; init; }
|
||||
|
||||
/// <summary>当前的数据面会话(拨号方固定;监听方是 Agent 当前的 Probe 会话,可能没有)。</summary>
|
||||
/// <summary>当前的数据面会话(拨号方固定;监听方是 Agent 当前的 拨号方 会话,可能没有)。</summary>
|
||||
public required Func<DataSession?> Session { get; init; }
|
||||
|
||||
public QuicDatagramTransport? Datagrams { get; init; }
|
||||
@@ -249,7 +249,7 @@ public sealed class BlockEnv
|
||||
|
||||
public TimeProvider Time => Clock.Time;
|
||||
|
||||
/// <summary>同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 Probe 会话各起一组监听方积木)。</summary>
|
||||
/// <summary>同一套环境,但绑定到一个固定的数据面会话(Agent 为每个 拨号方 会话各起一组监听方积木)。</summary>
|
||||
public BlockEnv WithSession(DataSession session) =>
|
||||
new()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>节点的单调时钟(微秒):t1…t4、t_send、t_recv 都用它;Probe 和 Agent 各自一个,块头里的时钟锚点把它对应到 UTC。</summary>
|
||||
/// <summary>节点的单调时钟(微秒):t1…t4、t_send、t_recv 都用它;每个节点各一个,块头里的时钟锚点把它对应到 UTC。</summary>
|
||||
public sealed class MonoClock
|
||||
{
|
||||
private readonly TimeProvider _time;
|
||||
|
||||
@@ -4,8 +4,8 @@ namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>
|
||||
/// 一侧记录的行:只含这一侧拥有的列(见 <see cref="StreamSpec.ColumnsOf"/>),按连接键(通常是 <c>seq</c>)索引的环形缓冲,
|
||||
/// 每列一个 long 数组,热路径不分配。Agent 用它记录 t2/t3 或发送行,等 Probe 的列块到达后按键合并;
|
||||
/// Probe 用它记录 t1/t4 或 t_recv,到点切成列块交出去。行被后来的键覆盖(环绕)就丢弃,这只会发生在一侧长时间没有交出行的时候。
|
||||
/// 每列一个 long 数组,热路径不分配。Agent 用它记录 t2/t3 或发送行,等 拨号方 的列块到达后按键合并;
|
||||
/// 拨号方 用它记录 t1/t4 或 t_recv,到点切成列块交出去。行被后来的键覆盖(环绕)就丢弃,这只会发生在一侧长时间没有交出行的时候。
|
||||
/// </summary>
|
||||
public sealed class RowStore
|
||||
{
|
||||
@@ -65,7 +65,7 @@ public sealed class RowStore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有行。一个新的数据面会话开始时,监听方必须清掉上一个会话留下的记录:Probe 重启之后序号从 0 重新开始,
|
||||
/// 清空所有行。一个新的数据面会话开始时,监听方必须清掉上一个会话留下的记录:拨号方 重启之后序号从 0 重新开始,
|
||||
/// 旧会话里同一个序号的 t2/t3 还留着的话,会被当成新会话里这个包"上行已经到达",真正的上行丢失就被算成了下行丢失。
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>一个运行中的积木:自己的取消令牌和后台任务。Probe 和 Agent 共用。</summary>
|
||||
/// <summary>一个运行中的积木:自己的取消令牌和后台任务。拨号方和监听方共用。</summary>
|
||||
public sealed class RunningBlock : IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
@@ -5,11 +5,11 @@ using MoShiro.Protocol;
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>Agent 单调时钟(微秒)与 UTC(毫秒)的对应关系:块头里的时钟锚点。</summary>
|
||||
public readonly record struct ClockAnchor(long AgentMonoUs, long UtcMs);
|
||||
public readonly record struct ClockAnchor(long ListenerMonoUs, long UtcMs);
|
||||
|
||||
/// <summary>
|
||||
/// 给合并好的流盖章:生成完整的块头(任务/测试、参数快照、端口、时钟锚点、起止时间)。
|
||||
/// 起止时间由数据本身的时间戳换算得出,而不是取收到列块的时刻,所以不受 Probe 交付延迟的影响。
|
||||
/// 起止时间由数据本身的时间戳换算得出,而不是取收到列块的时刻,所以不受拨号方交付延迟的影响。
|
||||
/// </summary>
|
||||
public static class ChunkFinalizer
|
||||
{
|
||||
@@ -26,7 +26,7 @@ public static class ChunkFinalizer
|
||||
ChunkId id)
|
||||
{
|
||||
RawChunkHeader source = fragment.Header;
|
||||
long offsetUs = source.Clock.ProbeOffsetUs;
|
||||
long offsetUs = source.Clock.DialerOffsetUs;
|
||||
long min = long.MaxValue;
|
||||
long max = long.MinValue;
|
||||
foreach (RawStream stream in streams)
|
||||
@@ -68,7 +68,7 @@ public static class ChunkFinalizer
|
||||
TestId = source.TestId,
|
||||
Phase = source.Phase,
|
||||
AgentId = agent,
|
||||
ProbeId = source.ProbeId,
|
||||
DialerId = source.DialerId,
|
||||
IpVersion = source.IpVersion,
|
||||
Target = null,
|
||||
Params = new Dictionary<string, JsonElement>(resolvedParams),
|
||||
@@ -77,15 +77,15 @@ public static class ChunkFinalizer
|
||||
StartUtcMs = start,
|
||||
DurationMs = duration,
|
||||
Partial = false,
|
||||
Clock = new ChunkClock { AgentMonoUs = anchor.AgentMonoUs, UtcMs = anchor.UtcMs, ProbeOffsetUs = offsetUs },
|
||||
Clock = new ChunkClock { ListenerMonoUs = anchor.ListenerMonoUs, UtcMs = anchor.UtcMs, DialerOffsetUs = offsetUs },
|
||||
};
|
||||
return new RawChunk(header, streams);
|
||||
}
|
||||
|
||||
public static long ToUtcMs(long value, ColumnRole role, ClockAnchor anchor, long probeOffsetUs)
|
||||
public static long ToUtcMs(long value, ColumnRole role, ClockAnchor anchor, long dialerOffsetUs)
|
||||
{
|
||||
long agentUs = role == ColumnRole.TsProbe ? value + probeOffsetUs : value;
|
||||
long delta = agentUs - anchor.AgentMonoUs;
|
||||
long listenerUs = role == ColumnRole.TsDialer ? value + dialerOffsetUs : value;
|
||||
long delta = listenerUs - anchor.ListenerMonoUs;
|
||||
return anchor.UtcMs + (delta >= 0 ? delta / 1000 : -((-delta + 999) / 1000));
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ public interface IRecordSource
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 Probe 一侧的列块(一个只带 Probe 列的 RawChunk)与 Agent 一侧的记录合并成完整的行。合并完全由 Kind 的 <see cref="StreamSpec"/> 声明驱动:
|
||||
/// 把 拨号方 一侧的列块(一个只带 拨号方 列的 RawChunk)与 Agent 一侧的记录合并成完整的行。合并完全由 Kind 的 <see cref="StreamSpec"/> 声明驱动:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>声明了连接键、两侧都有列:按键连接。主导方是 Probe(回显)时行取自列块,Agent 的列按键补上,缺失就是缺失(上行丢失);
|
||||
/// 主导方是 Agent(单向数据流)时行取自 Agent 的记录(按列块头里的序号范围),Probe 的列按键补上,缺失就是缺失(丢失)。</description></item>
|
||||
/// <item><description>声明了连接键、两侧都有列:按键连接。主导方是 拨号方(回显)时行取自列块,Agent 的列按键补上,缺失就是缺失(上行丢失);
|
||||
/// 主导方是 Agent(单向数据流)时行取自 Agent 的记录(按列块头里的序号范围),拨号方 的列按键补上,缺失就是缺失(丢失)。</description></item>
|
||||
/// <item><description>没有连接键,或者只有主导方一侧有列:原样透传,只按声明的列顺序重排。</description></item>
|
||||
/// </list>
|
||||
/// 合并之后的流按声明的列顺序排列,所以 Backend 的分析层不需要知道这些列是谁记录的;新增一种记录流不需要写合并代码。
|
||||
@@ -41,7 +41,7 @@ public static class FragmentMerger
|
||||
streams.Add(value);
|
||||
}
|
||||
|
||||
// 两侧各自记录完整行的流:Probe 的列块里没有它(例如 Probe 在不支持内核采样的平台上)时,Agent 自己的行也要交出去
|
||||
// 两侧各自记录完整行的流:拨号方 的列块里没有它(例如 拨号方 在不支持内核采样的平台上)时,Agent 自己的行也要交出去
|
||||
foreach (StreamSpec union in spec.Streams.Where(s => s.Union && fragment.Find(s.Name) is null))
|
||||
{
|
||||
RawStream? own = records.Records(streamId, union.Name)?.Drain(includeKey: false);
|
||||
@@ -54,32 +54,32 @@ public static class FragmentMerger
|
||||
return Result<IReadOnlyList<RawStream>>.Ok(streams);
|
||||
}
|
||||
|
||||
private static Result<RawStream> Join(StreamSpec spec, RawStream probe, RawChunkHeader header, RowStore? agent)
|
||||
private static Result<RawStream> Join(StreamSpec spec, RawStream fragment, RawChunkHeader header, RowStore? local)
|
||||
{
|
||||
if (spec.Union)
|
||||
{
|
||||
return Result<RawStream>.Ok(Concatenate(spec, probe, agent));
|
||||
return Result<RawStream>.Ok(Concatenate(spec, fragment, local));
|
||||
}
|
||||
|
||||
IReadOnlyList<ColumnSpec> probeColumns = spec.ColumnsOf(Side.Probe);
|
||||
IReadOnlyList<ColumnSpec> agentColumns = spec.ColumnsOf(Side.Agent);
|
||||
if (spec.Key is null || (spec.Driver == Side.Probe && agentColumns.Count == 0))
|
||||
IReadOnlyList<ColumnSpec> dialerColumns = spec.ColumnsOf(Side.Dialer);
|
||||
IReadOnlyList<ColumnSpec> listenerColumns = spec.ColumnsOf(Side.Listener);
|
||||
if (spec.Key is null || (spec.Driver == Side.Dialer && listenerColumns.Count == 0))
|
||||
{
|
||||
return Result<RawStream>.Ok(Conform(spec, probe));
|
||||
return Result<RawStream>.Ok(Conform(spec, fragment));
|
||||
}
|
||||
|
||||
RawColumn? key = probe.Find(spec.Key);
|
||||
if (key is null || probeColumns.Any(c => probe.Find(c.Name) is null))
|
||||
RawColumn? key = fragment.Find(spec.Key);
|
||||
if (key is null || dialerColumns.Any(c => fragment.Find(c.Name) is null))
|
||||
{
|
||||
return Result<RawStream>.Fail(MoShiroErrors.ValidationFailed, $"a fragment of stream {spec.Name} needs the columns {string.Join(", ", new[] { spec.Key }.Concat(probeColumns.Select(c => c.Name)))}");
|
||||
return Result<RawStream>.Fail(MoShiroErrors.ValidationFailed, $"a fragment of stream {spec.Name} needs the columns {string.Join(", ", new[] { spec.Key }.Concat(dialerColumns.Select(c => c.Name)))}");
|
||||
}
|
||||
|
||||
return spec.Driver == Side.Probe ? JoinOnProbeRows(spec, probe, key, agent) : JoinOnAgentRows(spec, probe, key, header, agent);
|
||||
return spec.Driver == Side.Dialer ? JoinOnDialerRows(spec, fragment, key, local) : JoinOnListenerRows(spec, fragment, key, header, local);
|
||||
}
|
||||
|
||||
private static Result<RawStream> JoinOnProbeRows(StreamSpec spec, RawStream probe, RawColumn key, RowStore? agent)
|
||||
private static Result<RawStream> JoinOnDialerRows(StreamSpec spec, RawStream fragment, RawColumn key, RowStore? local)
|
||||
{
|
||||
int n = probe.Count;
|
||||
int n = fragment.Count;
|
||||
var columns = new List<RawColumn>(spec.Columns.Count);
|
||||
foreach (ColumnSpec column in spec.Columns)
|
||||
{
|
||||
@@ -90,21 +90,21 @@ public static class FragmentMerger
|
||||
key.Values.AsSpan(0, n).CopyTo(values);
|
||||
Array.Fill(valid, true);
|
||||
}
|
||||
else if (spec.OwnerOf(column) == Side.Probe)
|
||||
else if (spec.OwnerOf(column) == Side.Dialer)
|
||||
{
|
||||
RawColumn source = probe.Find(column.Name)!;
|
||||
RawColumn source = fragment.Find(column.Name)!;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
values[i] = source.Values[i];
|
||||
valid[i] = source.IsValid(i);
|
||||
}
|
||||
}
|
||||
else if (agent is not null)
|
||||
else if (local is not null)
|
||||
{
|
||||
int index = agent.ColumnIndex(column.Name);
|
||||
int index = local.ColumnIndex(column.Name);
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
valid[i] = agent.TryGet(key.Values[i], index, out values[i]);
|
||||
valid[i] = local.TryGet(key.Values[i], index, out values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,17 +114,17 @@ public static class FragmentMerger
|
||||
return Result<RawStream>.Ok(new RawStream(spec.Name, n, columns));
|
||||
}
|
||||
|
||||
private static Result<RawStream> JoinOnAgentRows(StreamSpec spec, RawStream probe, RawColumn key, RawChunkHeader header, RowStore? agent)
|
||||
private static Result<RawStream> JoinOnListenerRows(StreamSpec spec, RawStream fragment, RawColumn key, RawChunkHeader header, RowStore? local)
|
||||
{
|
||||
if (!TryGetRange(header, out uint from, out uint to))
|
||||
{
|
||||
return Result<RawStream>.Fail(MoShiroErrors.ValidationFailed, $"a fragment of stream {spec.Name} must declare its sequence range");
|
||||
}
|
||||
|
||||
// 行取自 Agent 的记录:范围内发出过的每一个包都有一行,Probe 没有报告到达的就是丢失
|
||||
RawStream rows = agent?.Cut(from, to, includeKey: true) ?? new RawStream(spec.Name, 0, []);
|
||||
// 行取自 Agent 的记录:范围内发出过的每一个包都有一行,拨号方 没有报告到达的就是丢失
|
||||
RawStream rows = local?.Cut(from, to, includeKey: true) ?? new RawStream(spec.Name, 0, []);
|
||||
var arrived = new Dictionary<long, int>();
|
||||
for (int i = 0; i < probe.Count; i++)
|
||||
for (int i = 0; i < fragment.Count; i++)
|
||||
{
|
||||
arrived.TryAdd(key.Values[i], i); // 重复的包只算第一次
|
||||
}
|
||||
@@ -144,7 +144,7 @@ public static class FragmentMerger
|
||||
valid[i] = own.IsValid(i);
|
||||
}
|
||||
}
|
||||
else if (probe.Find(column.Name) is { } source)
|
||||
else if (fragment.Find(column.Name) is { } source)
|
||||
{
|
||||
RawColumn keys = rows.Find(spec.Key!)!;
|
||||
for (int i = 0; i < n; i++)
|
||||
@@ -164,13 +164,13 @@ public static class FragmentMerger
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 两侧各自记录完整行的流(例如各自套接字的内核采样):Probe 的行(列块里)加上 Agent 自己新记录的行,按声明的列顺序拼接。
|
||||
/// 两侧各自记录完整行的流(例如各自套接字的内核采样):拨号方 的行(列块里)加上 Agent 自己新记录的行,按声明的列顺序拼接。
|
||||
/// 每一行里有一列标明它属于哪一侧,所以不需要连接。
|
||||
/// </summary>
|
||||
private static RawStream Concatenate(StreamSpec spec, RawStream probe, RowStore? agent)
|
||||
private static RawStream Concatenate(StreamSpec spec, RawStream fragment, RowStore? local)
|
||||
{
|
||||
RawStream own = Conform(spec, probe);
|
||||
RawStream? extra = agent?.Drain(includeKey: false);
|
||||
RawStream own = Conform(spec, fragment);
|
||||
RawStream? extra = local?.Drain(includeKey: false);
|
||||
if (extra is null || extra.Count == 0)
|
||||
{
|
||||
return own;
|
||||
@@ -202,7 +202,7 @@ public static class FragmentMerger
|
||||
return new RawStream(spec.Name, n, columns);
|
||||
}
|
||||
|
||||
/// <summary>按声明的列顺序重排;Probe 一侧已经是完整的行时不改变内容。</summary>
|
||||
/// <summary>按声明的列顺序重排;拨号方 一侧已经是完整的行时不改变内容。</summary>
|
||||
private static RawStream Conform(StreamSpec spec, RawStream stream)
|
||||
{
|
||||
var columns = new List<RawColumn>(spec.Columns.Count);
|
||||
|
||||
@@ -75,7 +75,7 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection
|
||||
_configuration = configuration;
|
||||
_expectedPeerSpki = expectedPeerSpki;
|
||||
DatagramHandler = datagramHandler;
|
||||
_log = new ContextNodeLog(log, ("quic_id", DiagnosticId), ("side", expectedPeerSpki is null ? "agent" : "probe"));
|
||||
_log = new ContextNodeLog(log, ("quic_id", DiagnosticId), ("side", expectedPeerSpki is null ? "listener" : "dialer"));
|
||||
_closed = closed;
|
||||
_peerAddressChanged = peerAddressChanged;
|
||||
_self = GCHandle.Alloc(this);
|
||||
@@ -164,7 +164,7 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection
|
||||
long handshakeStarted = 0;
|
||||
try
|
||||
{
|
||||
registration = MsQuicApi.RegistrationOpen("moshiro-probe");
|
||||
registration = MsQuicApi.RegistrationOpen("moshiro-dialer");
|
||||
configuration = MsQuicApi.ConfigurationOpen(registration, QuicAlpn.Value);
|
||||
LoadClientCredentials(configuration);
|
||||
var endpoint = new IPEndPoint(address.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, localPort);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>The application protocol negotiated by the Agent and Probe QUIC endpoints.</summary>
|
||||
/// <summary>The application protocol negotiated by the Agent and 拨号方 QUIC endpoints.</summary>
|
||||
internal static class QuicAlpn
|
||||
{
|
||||
internal static readonly byte[] Value = "h3"u8.ToArray();
|
||||
|
||||
@@ -6,7 +6,7 @@ using Fleet.Core;
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>
|
||||
/// Measurement data transport. Agent instances listen on configured QUIC UDP ports; Probe instances
|
||||
/// Measurement data transport. Agent instances listen on configured QUIC UDP ports; 拨号方 instances
|
||||
/// open one QUIC connection from a newly selected ephemeral source port. Measurement frames are sent
|
||||
/// only with MsQuic DATAGRAM calls; the existing TLS peer connection continues to carry session control.
|
||||
/// </summary>
|
||||
@@ -21,7 +21,7 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
private readonly DataSession? _clientSession;
|
||||
private readonly IPEndPoint? _remote;
|
||||
private readonly byte[]? _peerSpki;
|
||||
private readonly int _previousSourcePort;
|
||||
private int _previousSourcePort;
|
||||
private readonly TaskCompletionSource _clientReady = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private MsQuicListener? _listener;
|
||||
private MsQuicConnection? _client;
|
||||
@@ -38,8 +38,8 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
_serverCertificate = serverCertificate;
|
||||
}
|
||||
|
||||
/// <summary>Constructs a Probe-side QUIC dialer for one Agent session.</summary>
|
||||
public QuicDatagramTransport(DataSession session, MonoClock clock, DataPlaneCounters counters, INodeLog log, IPEndPoint remote, byte[] peerSpki, int previousSourcePort)
|
||||
/// <summary>Constructs a 拨号方-side QUIC dialer for one Agent session.</summary>
|
||||
public QuicDatagramTransport(DataSession session, MonoClock clock, DataPlaneCounters counters, INodeLog log, IPEndPoint remote, byte[] peerSpki)
|
||||
{
|
||||
_router = new MeasurementDatagramRouter(new SingleSession(session), null, clock, counters, log);
|
||||
_log = log;
|
||||
@@ -49,7 +49,6 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
_clientSession = session;
|
||||
_remote = remote;
|
||||
_peerSpki = peerSpki;
|
||||
_previousSourcePort = previousSourcePort;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> BoundPorts { get; private set; } = [];
|
||||
@@ -79,33 +78,39 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
BoundPorts = _listener.BoundPorts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a QUIC connection from a new ephemeral source port. May be called again after the previous connection closed;
|
||||
/// the session, handler registrations and counters carry over. A failed attempt leaves the transport reusable.
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
if (_remote is null || _clientSession is null || _peerSpki is null)
|
||||
{
|
||||
throw new InvalidOperationException("Only Probe-side QUIC transports can connect.");
|
||||
throw new InvalidOperationException("Only 拨号方-side QUIC transports can connect.");
|
||||
}
|
||||
|
||||
try
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this);
|
||||
await CloseClientAsync().ConfigureAwait(false);
|
||||
int sourcePort = SelectSourcePort(_remote.AddressFamily, _previousSourcePort);
|
||||
MsQuicConnection client = await MsQuicConnection.ConnectAsync(_remote.Address, _remote.Port, sourcePort, _peerSpki, _onDatagram, _log, ct).ConfigureAwait(false);
|
||||
_client = client;
|
||||
_clientSession.BindDatagramConnection(client);
|
||||
BoundPorts = client.LocalEndPoint is { } local ? [local.Port] : [sourcePort];
|
||||
_previousSourcePort = BoundPorts[0];
|
||||
_clientReady.TrySetResult();
|
||||
}
|
||||
|
||||
/// <summary>Completes when the current 拨号方-side connection has shut down (peer close, idle timeout, local close).</summary>
|
||||
public Task WaitForDisconnectAsync(CancellationToken ct) =>
|
||||
_client is { } client ? client.WaitForShutdownAsync().WaitAsync(ct) : Task.CompletedTask;
|
||||
|
||||
/// <summary>Closes the current 拨号方-side connection; the next <see cref="ConnectAsync"/> opens a new one.</summary>
|
||||
public async Task CloseClientAsync()
|
||||
{
|
||||
MsQuicConnection? client = Interlocked.Exchange(ref _client, null);
|
||||
if (client is not null)
|
||||
{
|
||||
int sourcePort = SelectSourcePort(_remote.AddressFamily, _previousSourcePort);
|
||||
_client = await MsQuicConnection.ConnectAsync(
|
||||
_remote.Address,
|
||||
_remote.Port,
|
||||
sourcePort,
|
||||
_peerSpki,
|
||||
_onDatagram,
|
||||
_log,
|
||||
ct);
|
||||
_clientSession.BindDatagramConnection(_client);
|
||||
BoundPorts = _client.LocalEndPoint is { } local ? [local.Port] : [sourcePort];
|
||||
_clientReady.TrySetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error("QUIC client connection failed", ("err", ex.Message));
|
||||
_clientReady.TrySetException(ex);
|
||||
throw;
|
||||
await client.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,15 +133,9 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Agent side: holds the listener open until cancelled. 拨号方-side connections are driven by <see cref="ConnectAsync"/> and <see cref="WaitForDisconnectAsync"/>.</summary>
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
if (_remote is not null)
|
||||
{
|
||||
await _clientReady.Task.WaitAsync(ct);
|
||||
await _client!.WaitForShutdownAsync().WaitAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, ct);
|
||||
@@ -171,10 +170,7 @@ public sealed class QuicDatagramTransport : IDisposable, IAsyncDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_client is not null)
|
||||
{
|
||||
await _client.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
await CloseClientAsync().ConfigureAwait(false);
|
||||
|
||||
if (_listener is not null)
|
||||
{
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class TcpAcceptor
|
||||
using var authCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
authCts.CancelAfter(s_authTimeout);
|
||||
|
||||
// 预握手敲门:名册里的 Probe 才算得出敲门包;不通过就不写任何字节、直接断开
|
||||
// 预握手敲门:名册里的 拨号方 才算得出敲门包;不通过就不写任何字节、直接断开
|
||||
if (!await _knock.AdmitAsync(stream, (client.Client.RemoteEndPoint as IPEndPoint)?.Address, authCts.Token))
|
||||
{
|
||||
_counters.DropInvalid();
|
||||
|
||||
@@ -71,7 +71,7 @@ public static class TcpInfo
|
||||
|
||||
/// <summary>
|
||||
/// 一条连接上周期性的内核采样(<c>tcp_info</c> 流):每个采样一行 (ts, side, srtt, rttvar, cwnd, retrans_total, segs_out, bytes_acked, conn)。
|
||||
/// 两端各采样自己的套接字:Probe 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。
|
||||
/// 两端各采样自己的套接字:拨号方 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。
|
||||
/// 累计计数器在 Backend 按 (side, conn) 分别求差;conn 是当前记录器内分配的连接编号,不包含网络地址。
|
||||
/// </summary>
|
||||
public sealed class TcpInfoRecorder
|
||||
@@ -84,7 +84,7 @@ public sealed class TcpInfoRecorder
|
||||
{
|
||||
Rows = new RowStore(spec, side, 4096);
|
||||
_clock = clock;
|
||||
_side = side == Side.Probe ? 0 : 1;
|
||||
_side = side == Side.Dialer ? 0 : 1;
|
||||
}
|
||||
|
||||
public RowStore Rows { get; }
|
||||
|
||||
@@ -4,7 +4,7 @@ using MoShiro.Protocol;
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>
|
||||
/// Agent 和 Probe 共用。主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。
|
||||
/// 拨号方和监听方共用。主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。
|
||||
/// 只有汇总的数字:<b>不采集 CPU 型号、主机名、网卡名和任何 IP 地址</b>,所以面板和展示页想泄漏也泄漏不出去。
|
||||
/// Linux 读 <c>/proc</c> 和 <c>/sys</c>;容器通过 HOST_PROC、HOST_SYS、HOST_ROOT 指定宿主路径。
|
||||
/// Windows 用 GetSystemTimes、GlobalMemoryStatusEx 和 NetworkInterface(见 WindowsHost);macOS 等其他系统只有磁盘和运行时长。读不到的项是 null。不执行任何命令,不写文件。
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace MoShiro.Measurement;
|
||||
|
||||
/// <summary>节点本地配置里的 <c>reporting</c> 段(面板无法修改):Agent 和 Probe 共用。</summary>
|
||||
/// <summary>节点本地配置里的 <c>reporting</c> 段(面板无法修改):拨号方和监听方共用。</summary>
|
||||
public sealed class ReportingSection
|
||||
{
|
||||
/// <summary>上报主机资源占用(CPU、内存、磁盘、流量、运行时长,只有汇总数字,没有型号和 IP)。设为 false 就一个字也不上报。</summary>
|
||||
|
||||
Reference in new issue
Block a user