mirror of
https://github.com/hanzoai/visor.git
synced 2026-10-30 14:46:33 +00:00
Several TCP measurements that are causally anchored to the arrival of an
inbound ACK were computed using the wall clock at the moment the segment was
*processed* (ep.stack.Clock().NowMonotonic()) rather than the time the segment
*arrived* at the stack (segment.rcvdTime, stamped in newIncomingSegment before
the segment is queued).
These differ when an inbound ACK is delayed inside the stack before being
processed. In particular, a busy sender holds the endpoint lock while Write
synchronously flushes a window of segments (each paying real per-packet cost
such as encryption and a tun write), so inbound ACKs sit unprocessed in the
endpoint's segment queue. Measuring against processing time then inflates the
result by that internal delay, corrupting:
- RACK.RTT / RACK.minRTT and the reorder window (rackControl.update), which
can trigger spurious loss detection and hold cwnd abnormally low on a path
with little or no real loss;
- the connection's SRTT/RTO via the timestamp and non-timestamp RTT samples
in sender.handleRcvdSegment;
- the initial RTO seeded at handshake completion
(handshake.transitionToStateEstablishedLocked);
- the receiver's RTT estimate used for receive-buffer autotuning
(receiver.updateRTT), and lastRcvdAckTime;
- CUBIC HyStart's ACK-train detection (now.Sub(LastAck)/now.Sub(RoundStart)),
where a burst of ACKs drained together after an internal delay can look
like a tight train and exit slow start prematurely.
This is the long-standing root cause behind reports of RACK performing poorly
under load (spurious retransmissions, reduced congestion window). detectLoss
already used the segment's rcvdTime; this change makes the rest of the
ACK-anchored time measurements consistent with it, and threads the ACK ingress
time through congestionControl.Update so CUBIC HyStart's ACK-train timing uses
it too. updateRTO also gains a defensive guard against a negative RTT sample.
Anchoring RTT to ingress time is more in line with RFC 6298 ("measure the
elapsed time between sending a data octet ... and receiving the
acknowledgment") and RFC 8985, and matches Linux, which samples RTT at receive
(softirq) time.
Note: CUBIC's congestion-event epoch timestamps (c.T in getCwnd and the loss/
recovery handlers) are intentionally left on processing time; they are event
markers, not arrival-anchored estimators, and Linux uses processing time there
as well.
Adds tcp_acktiming_test.go, which uses a manual clock and the StopWork hook to
hold the endpoint lock while an ACK ages in the queue, asserting the RACK RTT
and SRTT are not inflated by the internal delay; the timestamp and
non-timestamp RTT paths are each exercised. Adds a CUBIC HyStart regression
test that drives a "burst" of ACKs whose ingress times are spaced beyond
ackDelta while the processing clock is frozen, asserting HyStart does not fire.
Both fail without the corresponding fix and pass with it.
Fixes #9778
Updates tailscale/tailscale#9707
122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
// Copyright 2018 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package tcp
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
)
|
|
|
|
// renoState stores the variables related to TCP New Reno congestion
|
|
// control algorithm.
|
|
//
|
|
// +stateify savable
|
|
type renoState struct {
|
|
s *sender
|
|
}
|
|
|
|
// newRenoCC initializes the state for the NewReno congestion control algorithm.
|
|
func newRenoCC(s *sender) *renoState {
|
|
return &renoState{s: s}
|
|
}
|
|
|
|
// updateSlowStart will update the congestion window as per the slow-start
|
|
// algorithm used by NewReno. If after adjusting the congestion window
|
|
// we cross the SSthreshold then it will return the number of packets that
|
|
// must be consumed in congestion avoidance mode.
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) updateSlowStart(packetsAcked int) int {
|
|
// Don't let the congestion window cross into the congestion
|
|
// avoidance range.
|
|
newcwnd := r.s.SndCwnd + packetsAcked
|
|
if newcwnd >= r.s.Ssthresh {
|
|
newcwnd = r.s.Ssthresh
|
|
r.s.SndCAAckCount = 0
|
|
}
|
|
|
|
packetsAcked -= newcwnd - r.s.SndCwnd
|
|
r.s.SndCwnd = newcwnd
|
|
return packetsAcked
|
|
}
|
|
|
|
// updateCongestionAvoidance will update congestion window in congestion
|
|
// avoidance mode as described in RFC5681 section 3.1
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) updateCongestionAvoidance(packetsAcked int) {
|
|
// Consume the packets in congestion avoidance mode.
|
|
r.s.SndCAAckCount += packetsAcked
|
|
if r.s.SndCAAckCount >= r.s.SndCwnd {
|
|
r.s.SndCwnd += r.s.SndCAAckCount / r.s.SndCwnd
|
|
r.s.SndCAAckCount = r.s.SndCAAckCount % r.s.SndCwnd
|
|
}
|
|
}
|
|
|
|
// reduceSlowStartThreshold reduces the slow-start threshold per RFC 5681,
|
|
// page 6, eq. 4. It is called when we detect congestion in the network.
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) reduceSlowStartThreshold() {
|
|
r.s.Ssthresh = r.s.Outstanding / 2
|
|
if r.s.Ssthresh < 2 {
|
|
r.s.Ssthresh = 2
|
|
}
|
|
|
|
}
|
|
|
|
// Update updates the congestion state based on the number of packets that
|
|
// were acknowledged.
|
|
// Update implements congestionControl.Update.
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) Update(packetsAcked int, _ time.Duration, _ tcpip.MonotonicTime) {
|
|
if r.s.SndCwnd < r.s.Ssthresh {
|
|
packetsAcked = r.updateSlowStart(packetsAcked)
|
|
if packetsAcked == 0 {
|
|
return
|
|
}
|
|
}
|
|
r.updateCongestionAvoidance(packetsAcked)
|
|
}
|
|
|
|
// HandleLossDetected implements congestionControl.HandleLossDetected.
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) HandleLossDetected() {
|
|
// A retransmit was triggered due to nDupAckThreshold or when RACK
|
|
// detected loss. Reduce our slow start threshold.
|
|
r.reduceSlowStartThreshold()
|
|
}
|
|
|
|
// HandleRTOExpired implements congestionControl.HandleRTOExpired.
|
|
//
|
|
// +checklocks:r.s.ep.mu
|
|
func (r *renoState) HandleRTOExpired() {
|
|
// We lost a packet, so reduce ssthresh.
|
|
r.reduceSlowStartThreshold()
|
|
|
|
// Reduce the congestion window to 1, i.e., enter slow-start. Per
|
|
// RFC 5681, page 7, we must use 1 regardless of the value of the
|
|
// initial congestion window.
|
|
r.s.SndCwnd = 1
|
|
}
|
|
|
|
// PostRecovery implements congestionControl.PostRecovery.
|
|
func (r *renoState) PostRecovery() {
|
|
// noop.
|
|
}
|