-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[NPM-3586] Add RTT support to ebpf-less tracer (#31491)
- Loading branch information
Showing
6 changed files
with
303 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
pkg/network/tracer/connection/ebpfless/tcp_processor_rtt.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024-present Datadog, Inc. | ||
|
||
//go:build linux_bpf | ||
|
||
package ebpfless | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/DataDog/datadog-agent/pkg/util/log" | ||
) | ||
|
||
func absDiff(a uint64, b uint64) uint64 { | ||
if a < b { | ||
return b - a | ||
} | ||
return a - b | ||
} | ||
|
||
// nanosToMicros converts nanoseconds to microseconds, rounding and converting to | ||
// the uint32 type that ConnectionStats uses | ||
func nanosToMicros(nanos uint64) uint32 { | ||
micros := time.Duration(nanos).Round(time.Microsecond).Microseconds() | ||
return uint32(micros) | ||
} | ||
|
||
// rttTracker implements the RTT algorithm specified here: | ||
// https://datatracker.ietf.org/doc/html/rfc6298#section-2 | ||
type rttTracker struct { | ||
// sampleSentTimeNs is the timestamp our current round trip began. | ||
// If it is 0, there is nothing in flight or a retransmit cleared this | ||
sampleSentTimeNs uint64 | ||
// expectedAck is the ack needed to complete the round trip | ||
expectedAck uint32 | ||
// rttSmoothNs is the smoothed RTT in nanoseconds | ||
rttSmoothNs uint64 | ||
// rttVarNs is the variance of the RTT in nanoseconds | ||
rttVarNs uint64 | ||
} | ||
|
||
func (rt *rttTracker) isActive() bool { | ||
return rt.sampleSentTimeNs > 0 | ||
} | ||
|
||
// processOutgoing is called to (potentially) start a round trip. | ||
// Records the time of the packet for later | ||
func (rt *rttTracker) processOutgoing(timestampNs uint64, nextSeq uint32) { | ||
if !rt.isActive() { | ||
rt.sampleSentTimeNs = timestampNs | ||
rt.expectedAck = nextSeq | ||
} | ||
} | ||
|
||
// clearTrip is called by a retransmit or when a round-trip completes | ||
// Retransmits pollute RTT accuracy and cause a trip to be thrown out | ||
func (rt *rttTracker) clearTrip() { | ||
if rt.isActive() { | ||
rt.sampleSentTimeNs = 0 | ||
rt.expectedAck = 0 | ||
} | ||
} | ||
|
||
// processIncoming is called to (potentially) close out a round trip. | ||
// Based off this https://github.com/DataDog/datadog-windows-filter/blob/d7560d83eb627117521d631a4c05cd654a01987e/ddfilter/flow/flow_tcp.c#L269 | ||
// Returns whether the RTT stats were updated. | ||
func (rt *rttTracker) processIncoming(timestampNs uint64, ack uint32) bool { | ||
hasCompletedTrip := rt.isActive() && isSeqBeforeEq(rt.expectedAck, ack) | ||
if !hasCompletedTrip { | ||
return false | ||
} | ||
|
||
elapsedNs := timestampNs - rt.sampleSentTimeNs | ||
if timestampNs < rt.sampleSentTimeNs { | ||
log.Warn("rttTracker encountered non-monotonic clock") | ||
elapsedNs = 0 | ||
} | ||
rt.clearTrip() | ||
|
||
if rt.rttSmoothNs == 0 { | ||
rt.rttSmoothNs = elapsedNs | ||
rt.rttVarNs = elapsedNs / 2 | ||
return true | ||
} | ||
|
||
// update variables based on fixed point math. | ||
// RFC 6298 says alpha=1/8 and beta=1/4 | ||
const fixedBasis uint64 = 1000 | ||
// SRTT < -(1 - alpha) * SRTT + alpha * R' | ||
oneMinusAlpha := fixedBasis - (fixedBasis / 8) | ||
alphaRPrime := elapsedNs / 8 | ||
s := ((oneMinusAlpha * rt.rttSmoothNs) / fixedBasis) + alphaRPrime | ||
rt.rttSmoothNs = s | ||
|
||
// RTTVAR <- (1 - beta) * RTTVAR + beta * |SRTT - R'| | ||
oneMinusBeta := fixedBasis - fixedBasis/4 | ||
rt.rttVarNs = (oneMinusBeta*rt.rttVarNs)/fixedBasis + absDiff(rt.rttSmoothNs, elapsedNs)/4 | ||
|
||
return true | ||
} |
142 changes: 142 additions & 0 deletions
142
pkg/network/tracer/connection/ebpfless/tcp_processor_rtt_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2024-present Datadog, Inc. | ||
|
||
//go:build linux_bpf | ||
|
||
package ebpfless | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNanosToMicros(t *testing.T) { | ||
require.Equal(t, uint32(0), nanosToMicros(0)) | ||
require.Equal(t, uint32(0), nanosToMicros(200)) | ||
require.Equal(t, uint32(1), nanosToMicros(500)) | ||
require.Equal(t, uint32(1), nanosToMicros(1000)) | ||
require.Equal(t, uint32(1), nanosToMicros(1200)) | ||
require.Equal(t, uint32(123), nanosToMicros(123*1000)) | ||
} | ||
|
||
func TestSingleSampleRTT(t *testing.T) { | ||
var rt rttTracker | ||
|
||
require.False(t, rt.isActive()) | ||
|
||
rt.processOutgoing(1000, 123) | ||
require.True(t, rt.isActive()) | ||
|
||
hasUpdated := rt.processIncoming(2000, 42) | ||
// ack is too low, not a round trip | ||
require.False(t, hasUpdated) | ||
|
||
// ack is high enough to complete a round trip | ||
hasUpdated = rt.processIncoming(3000, 123) | ||
require.True(t, hasUpdated) | ||
|
||
require.Equal(t, uint64(2000), rt.rttSmoothNs) | ||
require.Equal(t, uint64(1000), rt.rttVarNs) | ||
} | ||
|
||
func TestLowVarianceRtt(t *testing.T) { | ||
var rt rttTracker | ||
|
||
for i := range 10 { | ||
ts := uint64(i + 1) | ||
seq := uint32(123 + i) | ||
|
||
startNs := (2 * ts) * 1000 | ||
endNs := startNs + 1000 | ||
// round trip time always the 1000, so variance goes to 0 | ||
rt.processOutgoing(startNs, seq) | ||
hasUpdated := rt.processIncoming(endNs, seq) | ||
require.True(t, hasUpdated) | ||
require.Equal(t, rt.rttSmoothNs, uint64(1000)) | ||
} | ||
|
||
// after 10 iterations, the variance should have mostly converged to zero | ||
require.Less(t, rt.rttVarNs, uint64(100)) | ||
} | ||
|
||
func TestConstantVarianceRtt(t *testing.T) { | ||
var rt rttTracker | ||
|
||
for i := range 10 { | ||
ts := uint64(i + 1) | ||
seq := uint32(123 + i) | ||
|
||
startNs := (2 * ts) * 1000 | ||
endNs := startNs + 500 | ||
if i%2 == 0 { | ||
endNs = startNs + 1000 | ||
} | ||
|
||
// round trip time alternates between 500 and 100 | ||
rt.processOutgoing(startNs, seq) | ||
hasUpdated := rt.processIncoming(endNs, seq) | ||
require.True(t, hasUpdated) | ||
|
||
require.LessOrEqual(t, uint64(500), rt.rttSmoothNs) | ||
require.LessOrEqual(t, rt.rttSmoothNs, uint64(1000)) | ||
} | ||
|
||
// This is not exact since it uses an exponential rolling sum | ||
// In this test, the time delta alternates between 500 and 1000, | ||
// so rttSmoothNs is 750, for an average difference of ~250. | ||
const epsilon = 20 | ||
require.Less(t, uint64(250-epsilon), rt.rttVarNs) | ||
require.Less(t, rt.rttVarNs, uint64(250+epsilon)) | ||
} | ||
|
||
func TestTcpProcessorRtt(t *testing.T) { | ||
pb := newPacketBuilder(lowerSeq, higherSeq) | ||
syn := pb.outgoing(0, 0, 0, SYN) | ||
// t=200 us | ||
syn.timestampNs = 200 * 1000 | ||
synack := pb.incoming(0, 0, 1, SYN|ACK) | ||
// t=300 us, for a round trip of 100us | ||
synack.timestampNs = 300 * 1000 | ||
|
||
f := newTcpTestFixture(t) | ||
|
||
f.runPkt(syn) | ||
// round trip has not completed yet | ||
require.Zero(t, f.conn.RTT) | ||
require.Zero(t, f.conn.RTTVar) | ||
|
||
f.runPkt(synack) | ||
// round trip has completed in 100us | ||
require.Equal(t, uint32(100), f.conn.RTT) | ||
require.Equal(t, uint32(50), f.conn.RTTVar) | ||
} | ||
|
||
func TestTcpProcessorRttRetransmit(t *testing.T) { | ||
pb := newPacketBuilder(lowerSeq, higherSeq) | ||
syn := pb.outgoing(0, 0, 0, SYN) | ||
// t=200 us | ||
syn.timestampNs = 200 * 1000 | ||
synack := pb.incoming(0, 0, 1, SYN|ACK) | ||
// t=300 us, for a round trip of 100us | ||
synack.timestampNs = 300 * 1000 | ||
|
||
f := newTcpTestFixture(t) | ||
|
||
f.runPkt(syn) | ||
// round trip has not completed yet | ||
require.Zero(t, f.conn.RTT) | ||
require.Zero(t, f.conn.RTTVar) | ||
|
||
f.runPkt(syn) | ||
// this is a retransmit, should reset the round trip | ||
require.Zero(t, f.conn.RTT) | ||
require.Zero(t, f.conn.RTTVar) | ||
|
||
f.runPkt(synack) | ||
// should STILL not have a round trip because the retransmit contaminated the results | ||
require.Zero(t, f.conn.RTT) | ||
require.Zero(t, f.conn.RTTVar) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.