measurement: adapt the echo probe rate to loss and RTT, and weight samples by the time they stand for
udp_echo and tcp_echo run at the lowest rate that still proves the loss bound and keeps the RTT p99 confidence interval within tolerance, go to the maximum when the line loses packets or its RTT rises, and back off exponentially when it is unreachable. Every echo row carries a weight (the time it stands for, in grid intervals) and the analysis ops accumulate loss and RTT by it, so p50/p95/p99 and loss stay time-averaged when the rate changes. State changes are logged for audit.
This commit is contained in:
1 parent
57acecdb86
commit
e0376b5e33
15 files changed
+858
-55
No files matched your search
@@ -23,6 +23,7 @@ internal sealed class EchoRows
|
||||
|
||||
private readonly DriverLog _log;
|
||||
private readonly int _size;
|
||||
private readonly int _w;
|
||||
private readonly int _t1;
|
||||
private readonly int _t4;
|
||||
private readonly long _graceUs;
|
||||
@@ -36,26 +37,36 @@ internal sealed class EchoRows
|
||||
StreamSpec spec = kind.FindStream("msg")!;
|
||||
_log = new DriverLog(spec, Side.Dialer, capacity);
|
||||
_size = _log.Store.ColumnIndex("size");
|
||||
_w = _log.Store.ColumnIndex("w");
|
||||
_t1 = _log.Store.ColumnIndex("t1");
|
||||
_t4 = _log.Store.ColumnIndex("t4");
|
||||
}
|
||||
|
||||
public long Count => _log.Count - Interlocked.Read(ref _discarded);
|
||||
|
||||
public uint Add(int size, long t1Us)
|
||||
/// <summary><paramref name="weight"/>:这个包代表多少个基础网格间隔(频率变化时分析按它累计,丢包率和分位数仍是按时间平均的)。</summary>
|
||||
public uint Add(int size, long t1Us, int weight = 1)
|
||||
{
|
||||
long key = _log.Store.Append();
|
||||
_log.Store.Set(key, _size, size);
|
||||
_log.Store.Set(key, _w, weight);
|
||||
_log.Store.Set(key, _t1, t1Us);
|
||||
return (uint)key;
|
||||
}
|
||||
|
||||
public void Reply(uint seq, long t4Us)
|
||||
public bool Reply(uint seq, long t4Us) => Reply(seq, t4Us, out _);
|
||||
|
||||
/// <summary>回填 t4;第一次回复才算,<paramref name="t1Us"/> 是这个包的发出时间。</summary>
|
||||
public bool Reply(uint seq, long t4Us, out long t1Us)
|
||||
{
|
||||
if (_log.Store.Contains(seq) && !_log.Store.IsSet(seq, _t4))
|
||||
t1Us = 0;
|
||||
if (_log.Store.Contains(seq) && !_log.Store.IsSet(seq, _t4) && _log.Store.TryGet(seq, _t1, out t1Us))
|
||||
{
|
||||
_log.Store.Set(seq, _t4, t4Us);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Discard(uint seq)
|
||||
@@ -83,6 +94,7 @@ public sealed class DatagramEchoDialer : BlockBase
|
||||
private readonly EchoRows _rows;
|
||||
private readonly object _gate = new();
|
||||
private long _nextSendFailureWarningUs;
|
||||
private AdaptiveRate? _rate;
|
||||
|
||||
public DatagramEchoDialer(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
@@ -100,29 +112,50 @@ public sealed class DatagramEchoDialer : BlockBase
|
||||
IPEndPoint target = PeerEndpoint(Spec.Port);
|
||||
int size = (int)Spec.Int("size");
|
||||
byte[] packet = new byte[size];
|
||||
_rate = CreateAdaptiveRate();
|
||||
|
||||
void SendOne()
|
||||
{
|
||||
long t1 = Env.Clock.NowUs();
|
||||
lock (_gate)
|
||||
{
|
||||
uint seq = _rows.Add(size, t1, _rate?.Weight ?? 1);
|
||||
MeasurementDatagram.Write(packet, new MeasurementDatagramHeader(MeasurementDatagramType.EchoRequest, session.SessionId, Spec.StreamId, seq, t1), session.Key);
|
||||
if (datagrams.Send(session, target, packet))
|
||||
{
|
||||
_rate?.OnSend(t1);
|
||||
return;
|
||||
}
|
||||
|
||||
_rows.Discard(seq);
|
||||
if (t1 >= _nextSendFailureWarningUs)
|
||||
{
|
||||
Env.Log.Warn("QUIC echo datagram was not queued; omitted from loss samples",
|
||||
("stream", Spec.StreamId), ("seq", seq), ("bytes", size));
|
||||
_nextSendFailureWarningUs = t1 + 5_000_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
datagrams.Register(Spec.StreamId, OnPacket);
|
||||
try
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / Spec.Int("rate_hz")), Env.Time);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
if (_rate is null)
|
||||
{
|
||||
long t1 = Env.Clock.NowUs();
|
||||
lock (_gate)
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / Spec.Int("rate_hz")), Env.Time);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
{
|
||||
uint seq = _rows.Add(size, t1);
|
||||
MeasurementDatagram.Write(packet, new MeasurementDatagramHeader(MeasurementDatagramType.EchoRequest, session.SessionId, Spec.StreamId, seq, t1), session.Key);
|
||||
if (!datagrams.Send(session, target, packet))
|
||||
{
|
||||
_rows.Discard(seq);
|
||||
if (t1 >= _nextSendFailureWarningUs)
|
||||
{
|
||||
Env.Log.Warn("QUIC echo datagram was not queued; omitted from loss samples",
|
||||
("stream", Spec.StreamId), ("seq", seq), ("bytes", size));
|
||||
_nextSendFailureWarningUs = t1 + 5_000_000;
|
||||
}
|
||||
}
|
||||
SendOne();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await PaceAsync(_rate, () =>
|
||||
{
|
||||
SendOne();
|
||||
return ValueTask.CompletedTask;
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -157,7 +190,10 @@ public sealed class DatagramEchoDialer : BlockBase
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_rows.Reply(delivery.Header.Seq, delivery.ReceiveUs);
|
||||
if (_rows.Reply(delivery.Header.Seq, delivery.ReceiveUs, out long t1))
|
||||
{
|
||||
_rate?.OnReply(t1, delivery.ReceiveUs, (delivery.ReceiveUs - t1) / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +247,7 @@ public sealed class TcpEchoDialer : BlockBase
|
||||
{
|
||||
private readonly EchoRows _rows;
|
||||
private readonly TcpInfoRecorder _info;
|
||||
private AdaptiveRate? _rate;
|
||||
|
||||
public TcpEchoDialer(BlockEnv env, BlockSpec spec)
|
||||
: base(env, spec)
|
||||
@@ -224,12 +261,14 @@ public sealed class TcpEchoDialer : BlockBase
|
||||
public override async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
var backoff = new Fleet.Core.Backoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30));
|
||||
_rate = CreateAdaptiveRate();
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using TcpLink link = await TcpLink.OpenAsync(Env, Spec.Port, Spec.Bool("tls"), Spec.StreamId, sendAuth: true, ct);
|
||||
backoff.Reset();
|
||||
_rate?.Reconnected(Env.Clock.NowUs());
|
||||
await ExchangeAsync(link, ct);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
@@ -300,16 +339,29 @@ public sealed class TcpEchoDialer : BlockBase
|
||||
int min = (int)Spec.Int("min");
|
||||
int max = (int)Spec.Int("max");
|
||||
byte[] body = new byte[Math.Max(max, min)];
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / Spec.Int("rate_hz")), Env.Time);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
|
||||
async ValueTask SendOneAsync()
|
||||
{
|
||||
int size = Math.Max(TcpWire.MinEchoBody, max > min ? Random.Shared.Next(min, max + 1) : min);
|
||||
long t1 = Env.Clock.NowUs();
|
||||
uint seq = _rows.Add(size, t1);
|
||||
uint seq = _rows.Add(size, t1, _rate?.Weight ?? 1);
|
||||
body.AsSpan(0, size).Clear();
|
||||
TcpWire.WriteEchoBody(body, seq, t1);
|
||||
await TcpWire.WriteMessageAsync(stream, body.AsMemory(0, size), ct);
|
||||
await stream.FlushAsync(ct);
|
||||
_rate?.OnSend(t1);
|
||||
}
|
||||
|
||||
if (_rate is not null)
|
||||
{
|
||||
await PaceAsync(_rate, SendOneAsync, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / Spec.Int("rate_hz")), Env.Time);
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
{
|
||||
await SendOneAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,9 +376,9 @@ public sealed class TcpEchoDialer : BlockBase
|
||||
}
|
||||
|
||||
long now = Env.Clock.NowUs();
|
||||
if (TcpWire.TryReadEchoBody(reply, out uint seq, out _))
|
||||
if (TcpWire.TryReadEchoBody(reply, out uint seq, out _) && _rows.Reply(seq, now, out long t1))
|
||||
{
|
||||
_rows.Reply(seq, now);
|
||||
_rate?.OnReply(t1, now, (now - t1) / 1000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user