Files
justme c9beeee8e3 docs: describe the unified Agent topology and link controls
Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
2026-10-21 11:55:55 +08:00

15 KiB
Raw Permalink Blame History

任务与采集引擎

1. 原则

  • 只采集原始数据:逐包、逐消息、逐事件,TCP 另加周期性的内核采样;不计算任何统计,解释全部放在 analysis.md。
  • 一处声明,处处生效:采集方法(Kind)由积木组合声明而成。Agent 共用同一套积木(backend/shared/MoShiro.Measurement)。
  • 密度即参数:频率、包大小、采样间隔全部是任务参数,由 API 下发,不需要改代码。

2. 任务模型

同一个 Agent 节点 的 IPv4、IPv6 路径并行执行相同的持续线路任务模板,各自记录、上传、分析。IPv6 只在管理端配置了地址且 Agent 上报支持时建立;两端的控制和数据通道固定使用同一地址族。按需测试通过所选线路 id 指定地址族,重负载并发限制仍按 Agent 的本机策略执行。两个地址族分别使用有界发件箱(拨号方 每路径 8 MB,Agent 的共同上传队列 64 MB)。

Task = { id, scope, kind, target, params, schedule, enabled }

scope     line   : 拨号方 ↔ Agent 线路任务      target 为空(agent_id 为空时表示对所有 Agent 生效的模板)
          target : Agent → 目标 IP 的单边任务   target = ip[:port](受 Agent 本地 allowed_target_cidrs 限制)
kind      Kind 目录中的 id,例如 udp_echo / tcp_echo / udp_train …
params    按 Kind 的 ParamSpec 校验(默认值、上下限);未指定的参数取默认值
schedule  continuous   持续运行,按时间切块
          every(N)     每 N 分钟运行一次(N ≥ 60,适用于重负载 Kind)
          once         按需测试,由 API 触发,也就是一次 Test
  • 线路任务模板:agent_id 为空的 line 任务会自动应用到所有 active 的 Agent。新 Agent 注册后自动获得这些任务;也可以针对单个 Agent 覆盖参数或停用。
  • 测试方案(Plan):多个阶段的 once 任务组合,例如 hy2_ladder 是 udp_train 在 10、25、50、100 Mbps 下各运行 10 秒。Plan 定义在 backend/src/MoShiro.Backend.Application/Tests/Plans.cs。
  • 期望任务集:Backend 为每个 Agent 计算“有效任务集”(模板 + 覆盖 + 目标任务),带版本号,作为 Fleet 的期望状态(fleet.state.sync,文档类型 TaskSet)下发。Agent 的 TaskManager 与当前运行的任务对比后,启动、停止或修改参数;其中线路任务再经过控制通道同步给 拨号方。新增任务类型时协议不变。

3. Kind 目录:积木组合

3.1 积木(MoShiro.Measurement,只有新增积木才需要写代码)

类别 积木 参数示例
Transport udp、tcp、tls、icmp port、sni、nodelay、sndbuf
Pacer(发送节奏) fixed、poisson、burst、ladder rate_hz、mbps、burst_len、steps[]
Payload(负载形态) fixed、uniform、choice size、min/max、sizes[]
Behavior(对端行为) echo、sink、source、connect、hold direction(fwd/rev/bidir)、streams
Sampler(采样器,可挂到任意连接上) tcp_info、socket_counters every_ms
Recorder(记录器) 根据 Kind 声明的列自动生成的列式记录器 —

两种角色互相独立:连接角色(拨号方 永远是拨号方,适配 NAT)与数据角色(发送端、接收端、回显端,由 direction 决定)无关。每种积木只实现一次,Agent 共用。

3.2 Kind 声明(MoShiro.Protocol/Catalog/Kinds)

public static readonly KindSpec TcpEcho = new(
    Id: "tcp_echo",
    Scopes: Scope.Line,
    Transport: Blocks.Tcp(nodelay: true),
    Pacer: Blocks.Fixed(rateHz: P.Int("rate_hz", @default: 20, min: 1, max: 200)),
    Payload: Blocks.Uniform(P.Int("min", 32, 8, 1400), P.Int("max", 64, 8, 1400)),
    Behavior: Blocks.Echo(),
    Samplers: [Blocks.TcpInfo(everyMs: P.Int("tcp_info_ms", @default: 100, min: 10, max: 1000))],
    Records:
    [
        Rec.Stream("msg", Col.U32("seq"), Col.U16("size"),
                   Col.Ts拨号方("t1"), Col.TsAgent("t2"), Col.TsAgent("t3"), Col.Ts拨号方("t4")),
        Rec.Stream("tcp_info", Col.TsLocal("ts"), Col.U8("side"), Col.U32("srtt_us"),
                   Col.U32("rttvar_us"), Col.U32("cwnd"), Col.U32("retrans_total"), Col.U64("bytes_acked")),
    ],
    Budget: p => Bandwidth.Echo(p.RateHz, p.Max));   // 用于校验上限和估算带宽

这一条声明会被四个地方使用:

  • Agent:组装积木执行任务;
  • Recorder:根据列定义记录数据;
  • Backend:校验参数和预算,并在 /catalog/kinds 中展示;
  • Analysis:按列名读取数据。

3.3 内置 Kind

Kind 组合 默认参数 主要服务于
udp_echo udp + fixed + fixed + echo 10 Hz × 64 B 基础指标、mc、hy2
udp_train udp + fixed/ladder + fixed + sink 1250 B,rev hy2
tcp_echo tcp/tls + fixed + uniform + echo + tcp_info 20 Hz × 32–64 B,tcp_info 每 100 ms mc、reality
tcp_bulk tcp/tls + source/sink + tcp_info streams 条连接并行传输(direction = fwd 拨号方 发 / rev Agent 发),拨号方 每 sample_ms 记录每条连接的累计字节数(bytes 流);两端各自只采样第一条连接的内核 tcp_info;发送端受本地 MaxBulkMbps 约束;只用于按需测试,默认 4 秒(够量到峰值,省流量) reality
connect tcp/tls + fixed + connect 每分钟 6 次 reality,以及目标任务
hold tls + hold 每 30 s 一次心跳 reality
icmp_echo icmp + fixed + fixed + echo 1 Hz × 64 B 目标任务

默认的线路任务模板:udp_echo、tcp_echo、connect(持续运行;connect 每分钟 6 次,记录 TCP 建连和 TLS 握手耗时,约 0.4 kbps)。其余 Kind 可以按需开启,或者以 Plan 形式运行。每一批默认任务只种入一次,在设置里用标记记住,所以删掉的默认模板不会被重新建出来;后来新增的默认任务(connect)在已有部署升级后会补建一次。

3.4 回显的四个时间戳

拨号方 单调时钟         Agent 单调时钟
  t1 发送 ─────────────> t2 收到
  t4 收到 <───────────── t3 回复

RTT = (t4 − t1) − (t3 − t2)。缺少 t2 表示上行丢失;有 t2/t3 但缺少 t4 表示下行丢失。t2 − t1 和 t4 − t3 的变化量在去除偏移和漂移后,就是每个方向各自的时延抖动。

4. 原始块(自描述)

RawChunk
├─ header(JSON,自描述)
│   chunk_id(UUIDv7)  container_version  kind  task_id  test_id?  phase?
│   agent_id  dialer_id?  target?  params(完整快照)  port  direction
│   start_utc_ms  duration_ms
│   clock:agent 单调时钟 ↔ UTC 锚点,拨号方时钟偏移估计
│   streams: [ { name: "msg", columns: [ {name:"seq", type:"u32", enc:"delta"}, … ], count } … ]
├─ 各列数据(delta / zigzag / varint,按列定义编码)
├─ Brotli 压缩(BCL 自带)
└─ crc32c
  • 列定义写在块头中:新增列或新增 Kind 时不需要升级 container_version。只有容器格式本身变化时才升级。Backend 可以通用地解码任何块,不认识的列会被保留,但会被分析层忽略。
  • 切块规则:持续任务每 10 秒切一块,或者记录数达到上限(默认 50 000)时提前切块。这样采集密度提高后,单块大小仍然有界,实时显示和持久化也共用同一条链路。
  • 按需测试按阶段切块,阶段内同样受记录数上限约束。
  • 编解码器位于 MoShiro.Protocol/Raw,Agent、Backend、离线工具使用同一份代码。

5. 列的归属与合并

两侧各自记录完整行的流(StreamSpec.Union,例如 tcp_info):Agent 各自采样自己的套接字(Linux TCP_INFO,通过 Socket.GetRawSocketOption,不需要 P/Invoke),每一行带 side 标明属于哪一侧;合并时直接拼接,不连接,Agent 一侧的行每行只交出一次。拨号方 的重传反映上行方向,Agent 的重传反映下行方向(代理场景里大流量走这个方向)。非 Linux 平台没有这类采样,对应的指标在窗口里就是没有数据。

tcp_info 的可空 conn 列标识当前记录器内的一条 TCP 连接,不包含地址。Backend 按 (side, conn) 分别求累计计数差,避免把重连前后或同时存活的套接字混为一个序列;时间不递增的样本被忽略,任一计数器回退时重新建立基线。旧数据没有 conn,仍按 side 分组,无法事后恢复丢失的连接归属。升级时先更新 Backend 和 Agent,再更新 拨号方;旧 Agent 合并采样时会丢弃尚不认识的列。

TCP 回显的长度头和正文合为一次写入,避免在启用 TCP_NODELAY 时为长度头单独发送一个小段。tcp.retrans 仍按内核的重传段数与发送段数求比,不由应用消息数估算。

采集层只产生原始列(seq、size、t1…t4、t_send/t_recv、outcome……),不做任何统计,也不认识上层的方案或协议;丢包率、RTT、抖动由 Backend 的分析层从这些列推算。

每个记录流(StreamSpec)声明:

  • Driver:行的主导方——行由它发出的包或事件决定;
  • Key:连接键(通常是 seq);
  • 每一列属于哪一侧由它的时钟决定:Ts拨号方 属于 拨号方,TsAgent 属于 Agent,其余列属于主导方。

两端各自把本侧的列记在一张按键索引的环形行表里(RowStore),拨号方 每 10 秒把已完成的行切成列块经控制通道交给对应的 Agent,Agent 按声明合并成完整的 RawChunk 后上传:

  • Driver = 拨号方(回显):行取自列块,Agent 的列按键补上,缺失就是缺失(上行丢失);
  • Driver = Agent(单向数据流):行取自 Agent 的记录(按列块头里的序号范围),拨号方 的列按键补上,缺失就是缺失(丢失);
  • 没有 Key,或只有主导方一侧有列:原样透传。

新增一种记录流只需要写声明,不需要写合并代码。拨号方 的列在规定时间内没有送达时,Agent 带着已有的数据继续,不上传只有 Agent 一侧的块。

6. 代码结构(backend/shared/MoShiro.Measurement)

同一种采集方法的两端是一对角色写在同一个文件里:拨号方(拨号方 永远是拨号方,适配 NAT)和监听方(Agent)。两端共用包的构造和校验、时钟、行表和合并;Agent 只负责组装自己那一侧。

MoShiro.Measurement/                    依赖:MoShiro.Protocol(仅 BCL,零 NuGet)
├─ Core/
│  ├─ Contracts.cs         IBlock、BlockSpec、BlockEnv、DataSession、IAdmission、IFragmentSink……
│  ├─ RowStore.cs          本侧记录的行:只含本侧拥有的列,按键索引的环形缓冲,热路径不分配
│  ├─ MonoClock.cs         单调时钟(微秒)与 UTC 锚点
│  └─ RunningBlock.cs      一个运行中的积木(取消令牌 + 后台任务)
├─ Net/
│  ├─ MsQuic*.cs、QuicDatagramTransport.cs、MeasurementDatagramRouter.cs   MsQuic 数据面与逐流认证分发
│  ├─ LinuxIcmpEcho.cs、TcpInfo.cs   ICMP 回显;Linux `TCP_INFO` 采样
│  └─ Tcp.cs               TcpLink(拨号:TLS 按指纹固定 + 认证首帧)、TcpAcceptor(监听:TLS 自动识别 + 认证首帧)
├─ Blocks/                 一个文件 = 一种采集方法的两个角色
│  ├─ BlockBase.cs         积木的公共基类
│  ├─ Echo.cs              DatagramEchoDialer/Listener、TcpEchoDialer/Listener
│  ├─ Session.cs           ConnectDialer、HoldDialer、IcmpEchoSolo(没有对端)、IdleListener
│  ├─ Train.cs             TrainDialer(接收,记 t_recv)、TrainListener(按码率发包,记发送行)
│  ├─ Bulk.cs              `tcp_bulk` 的两个角色
│  └─ BlockCatalog.cs      Kind → (拨号方, 监听方);capabilities、本地预算的支持列表都从这里推出来
├─ Merging/                FragmentMerger(声明驱动的按键合并)、ChunkFinalizer(盖章:块头、时钟锚点、起止时间)
├─ Execution/              BudgetGuard(参数校验 + 本地硬上限,超出即拒绝)
├─ Platform/               主机汇总采样、IPv6 诊断、本机地址、平台识别
└─ Update/                 RestartUpdater(收到更新请求后有序退出)

边界(由架构测试强制):采集层的任何代码里不能出现 REALITY、Hysteria2、Minecraft 这些业务概念,也没有测试方案——方案(例如 hy2_ladder、reality_session)只是若干采集原语阶段的编排,只存在于 Backend。监听方的准入策略(重放窗口、令牌桶、源地址学习)是安全边界,通过 IAdmission 由 Agent 实现,积木本身不含策略。

7. 资源与安全上限

项目 默认值 由谁约束
同时运行的重负载测试 每个 Agent 1 个;拨号方 全局 1 个 Backend 编排 + 节点本地
单次测试时长 / 码率 ≤ 120 s;≤ 本地 max_total_mbps 节点本地硬上限
单块记录数 ≤ 50 000 Recorder
拨号方 上未交付的列数据 每个 Agent ≤ 8 MB,超出则丢弃最旧的数据并计入缺口 拨号方
持续任务的总带宽 每条线路 ≤ 100 kbps(默认模板约 40 kbps) Backend 校验
接收端速率校验 实际速率 > 声明速率 × 1.2 时中止 节点

8. 端口

用途 默认端口 说明
拨号方 控制通道(TLS,Fleet 点对点会话) 7443/tcp
QUIC Datagram 数据(udp_echo、udp_train) 7443/udp Agent 固定监听;拨号方 每次新连接轮换本地源端口
TCP/TLS 数据(tcp_echo、tcp_bulk、connect、hold) 7444/tcp 按首帧中的 stream_id 分发
  • 默认不使用 443,避免与 VPS 上已有的 REALITY 或 Hysteria2 服务冲突,也不需要特权端口权限。
  • 任务参数中的 port 只能取 Agent 本地配置 listen 列表中的端口。要用 443 测量,需要节点所有者在本机把 443 加入 listen(并授予 CAP_NET_BIND_SERVICE),然后在任务中指定 port: 443。
  • 每个原始块的头部都记录实际使用的端口。当测量端口与被推算服务的常用端口(443)不同时,分析结果附带 port_differs_from_service 置信说明,因为运营商的 QoS 可能按端口区别对待。

9. 边界

采集端只模拟流量特征(端口、包大小、速率、握手、方向),不实现 REALITY 或 Hysteria2 协议本身,分析结果附带置信说明。用户态看不到 TCP 的逐段收发,TCP 的原始数据是逐消息时间戳加内核采样;pcap / eBPF 见路线图。