diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 6e1a0f912..96d64216e 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -312,6 +312,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/set from tailscale.com/health+ tailscale.com/util/singleflight from tailscale.com/control/controlclient+ tailscale.com/util/slicesx from tailscale.com/net/dnscache+ + tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock tailscale.com/util/systemd from tailscale.com/control/controlclient+ tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+ tailscale.com/util/vizerror from tailscale.com/tsweb diff --git a/util/sysresources/memory.go b/util/sysresources/memory.go new file mode 100644 index 000000000..7363155cd --- /dev/null +++ b/util/sysresources/memory.go @@ -0,0 +1,10 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package sysresources + +// TotalMemory returns the total accessible system memory, in bytes. If the +// value cannot be determined, then 0 will be returned. +func TotalMemory() uint64 { + return totalMemoryImpl() +} diff --git a/util/sysresources/memory_bsd.go b/util/sysresources/memory_bsd.go new file mode 100644 index 000000000..26850dce6 --- /dev/null +++ b/util/sysresources/memory_bsd.go @@ -0,0 +1,16 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build freebsd || openbsd || dragonfly || netbsd + +package sysresources + +import "golang.org/x/sys/unix" + +func totalMemoryImpl() uint64 { + val, err := unix.SysctlUint64("hw.physmem") + if err != nil { + return 0 + } + return val +} diff --git a/util/sysresources/memory_darwin.go b/util/sysresources/memory_darwin.go new file mode 100644 index 000000000..e07bac0cd --- /dev/null +++ b/util/sysresources/memory_darwin.go @@ -0,0 +1,16 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build darwin + +package sysresources + +import "golang.org/x/sys/unix" + +func totalMemoryImpl() uint64 { + val, err := unix.SysctlUint64("hw.memsize") + if err != nil { + return 0 + } + return val +} diff --git a/util/sysresources/memory_linux.go b/util/sysresources/memory_linux.go new file mode 100644 index 000000000..0239b0e80 --- /dev/null +++ b/util/sysresources/memory_linux.go @@ -0,0 +1,19 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build linux + +package sysresources + +import "golang.org/x/sys/unix" + +func totalMemoryImpl() uint64 { + var info unix.Sysinfo_t + + if err := unix.Sysinfo(&info); err != nil { + return 0 + } + + // uint64 casts are required since these might be uint32s + return uint64(info.Totalram) * uint64(info.Unit) +} diff --git a/util/sysresources/memory_unsupported.go b/util/sysresources/memory_unsupported.go new file mode 100644 index 000000000..0fde256e0 --- /dev/null +++ b/util/sysresources/memory_unsupported.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !(linux || darwin || freebsd || openbsd || dragonfly || netbsd) + +package sysresources + +func totalMemoryImpl() uint64 { return 0 } diff --git a/util/sysresources/sysresources.go b/util/sysresources/sysresources.go new file mode 100644 index 000000000..32d972ab1 --- /dev/null +++ b/util/sysresources/sysresources.go @@ -0,0 +1,6 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package sysresources provides OS-independent methods of determining the +// resources available to the current system. +package sysresources diff --git a/util/sysresources/sysresources_test.go b/util/sysresources/sysresources_test.go new file mode 100644 index 000000000..331ad913b --- /dev/null +++ b/util/sysresources/sysresources_test.go @@ -0,0 +1,25 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package sysresources + +import ( + "runtime" + "testing" +) + +func TestTotalMemory(t *testing.T) { + switch runtime.GOOS { + case "linux": + case "freebsd", "openbsd", "dragonfly", "netbsd": + case "darwin": + default: + t.Skipf("not supported on runtime.GOOS=%q yet", runtime.GOOS) + } + + mem := TotalMemory() + if mem == 0 { + t.Fatal("wanted TotalMemory > 0") + } + t.Logf("total memory: %v bytes", mem) +} diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index d391ea2c8..d33cbd244 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -63,6 +63,7 @@ import ( "tailscale.com/util/clientmetric" "tailscale.com/util/mak" "tailscale.com/util/ringbuffer" + "tailscale.com/util/sysresources" "tailscale.com/util/uniq" "tailscale.com/version" "tailscale.com/wgengine/capture" @@ -1418,12 +1419,59 @@ func (c *Conn) sendAddr(addr netip.AddrPort, pubKey key.NodePublic, b []byte) (s } } -// bufferedDerpWritesBeforeDrop is how many packets writes can be -// queued up the DERP client to write on the wire before we start -// dropping. -// -// TODO: this is currently arbitrary. Figure out something better? -const bufferedDerpWritesBeforeDrop = 32 +var ( + bufferedDerpWrites int + bufferedDerpWritesOnce sync.Once +) + +// bufferedDerpWritesBeforeDrop returns how many packets writes can be queued +// up the DERP client to write on the wire before we start dropping. +func bufferedDerpWritesBeforeDrop() int { + // For mobile devices, always return the previous minimum value of 32; + // we can do this outside the sync.Once to avoid that overhead. + if runtime.GOOS == "ios" || runtime.GOOS == "android" { + return 32 + } + + bufferedDerpWritesOnce.Do(func() { + // Some rough sizing: for the previous fixed value of 32, the + // total consumed memory can be: + // = numDerpRegions * messages/region * sizeof(message) + // + // For sake of this calculation, assume 100 DERP regions; at + // time of writing (2023-04-03), we have 24. + // + // A reasonable upper bound for the worst-case average size of + // a message is a *disco.CallMeMaybe message with 16 endpoints; + // since sizeof(netip.AddrPort) = 32, that's 512 bytes. Thus: + // = 100 * 32 * 512 + // = 1638400 (1.6MiB) + // + // On a reasonably-small node with 4GiB of memory that's + // connected to each region and handling a lot of load, 1.6MiB + // is about 0.04% of the total system memory. + // + // For sake of this calculation, then, let's double that memory + // usage to 0.08% and scale based on total system memory. + // + // For a 16GiB Linux box, this should buffer just over 256 + // messages. + systemMemory := sysresources.TotalMemory() + memoryUsable := float64(systemMemory) * 0.0008 + + const ( + theoreticalDERPRegions = 100 + messageMaximumSizeBytes = 512 + ) + bufferedDerpWrites = int(memoryUsable / (theoreticalDERPRegions * messageMaximumSizeBytes)) + + // Never drop below the previous minimum value. + if bufferedDerpWrites < 32 { + bufferedDerpWrites = 32 + } + }) + return bufferedDerpWrites +} // derpWriteChanOfAddr returns a DERP client for fake UDP addresses that // represent DERP servers, creating them as necessary. For real UDP @@ -1520,7 +1568,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha dc.DNSCache = dnscache.Get() ctx, cancel := context.WithCancel(c.connCtx) - ch := make(chan derpWriteRequest, bufferedDerpWritesBeforeDrop) + ch := make(chan derpWriteRequest, bufferedDerpWritesBeforeDrop()) ad.c = dc ad.writeCh = ch diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 53e8b60d9..18851d28d 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -1853,3 +1853,11 @@ func TestSetNetworkMapWithNoPeers(t *testing.T) { } } } + +func TestBufferedDerpWritesBeforeDrop(t *testing.T) { + vv := bufferedDerpWritesBeforeDrop() + if vv < 32 { + t.Fatalf("got bufferedDerpWritesBeforeDrop=%d, which is < 32", vv) + } + t.Logf("bufferedDerpWritesBeforeDrop = %d", vv) +}