mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
net/tunstats: more efficient stats gatherer
Signed-off-by: Joe Tsai <joetsai@digital-static.net> Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
parent
91794f6498
commit
b63618452c
367
net/tunstats/stats.go
Normal file
367
net/tunstats/stats.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package tunstats maintains statistics about connections
|
||||||
|
// flowing through a TUN device (which operate at the IP layer).
|
||||||
|
package tunstats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"hash/maphash"
|
||||||
|
"math/bits"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"tailscale.com/net/flowtrack"
|
||||||
|
"tailscale.com/types/ipproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Statistics maintains counters for every connection.
|
||||||
|
// All methods are safe for concurrent use.
|
||||||
|
// The zero value is ready for use.
|
||||||
|
type Statistics struct {
|
||||||
|
v4 hashTable[addrsPortsV4]
|
||||||
|
v6 hashTable[addrsPortsV6]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counts are statistics about a particular connection.
|
||||||
|
type Counts struct {
|
||||||
|
TxPackets uint64 `json:"txPkts,omitempty"`
|
||||||
|
TxBytes uint64 `json:"txBytes,omitempty"`
|
||||||
|
RxPackets uint64 `json:"rxPkts,omitempty"`
|
||||||
|
RxBytes uint64 `json:"rxBytes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
minTableLen = 8
|
||||||
|
maxProbeLen = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
// hashTable is a hash table that uses open addressing with probing.
|
||||||
|
// See https://en.wikipedia.org/wiki/Hash_table#Open_addressing.
|
||||||
|
// The primary table is in the active field and can be retrieved atomically.
|
||||||
|
// In the common case, this data structure is mostly lock free.
|
||||||
|
//
|
||||||
|
// If the current table is too small, a new table is allocated that
|
||||||
|
// replaces the current active table. The contents of the older table are
|
||||||
|
// NOT copied to the new table, but rather the older table is appended
|
||||||
|
// to a list of outgrown tables. Re-growth happens under a lock,
|
||||||
|
// but is expected to happen rarely as the table size grows exponentially.
|
||||||
|
//
|
||||||
|
// To reduce memory usage, the counters uses 32-bit unsigned integers,
|
||||||
|
// which carry the risk of overflowing. If an overflow is detected,
|
||||||
|
// we add the amount overflowed to the overflow map. This is a naive Go map
|
||||||
|
// protected by a sync.Mutex. Overflow is rare that contention is not a concern.
|
||||||
|
//
|
||||||
|
// To extract all counters, we replace the active table with a zeroed table,
|
||||||
|
// and clear out the outgrown and overflow tables.
|
||||||
|
// We take advantage of the fact that all the tables can be merged together
|
||||||
|
// by simply adding up all the counters for each connection.
|
||||||
|
type hashTable[AddrsPorts addrsPorts] struct {
|
||||||
|
// TODO: Get rid of this. It is just an atomic update in the common case,
|
||||||
|
// but contention updating the same word still incurs a 25% performance hit.
|
||||||
|
mu sync.RWMutex // RLock held while updating, Lock held while extracting
|
||||||
|
|
||||||
|
active atomic.Pointer[countsTable[AddrsPorts]]
|
||||||
|
inserts atomic.Uint32 // heuristic for next active table to allocate
|
||||||
|
|
||||||
|
muGrow sync.Mutex // muGrow.Lock implies that mu.RLock held
|
||||||
|
outgrown []countsTable[AddrsPorts]
|
||||||
|
|
||||||
|
muOverflow sync.Mutex // muOverflow.Lock implies that mu.RLock held
|
||||||
|
overflow map[flowtrack.Tuple]Counts
|
||||||
|
}
|
||||||
|
|
||||||
|
type countsTable[AddrsPorts addrsPorts] []counts[AddrsPorts]
|
||||||
|
|
||||||
|
func (t *countsTable[AddrsPorts]) len() int {
|
||||||
|
if t == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(*t)
|
||||||
|
}
|
||||||
|
|
||||||
|
type counts[AddrsPorts addrsPorts] struct {
|
||||||
|
// initProto is both an initialization flag and the IP protocol.
|
||||||
|
// It is 0 if uninitialized, 1 if initializing, and
|
||||||
|
// 2+ipproto.Proto if initialized.
|
||||||
|
initProto atomic.Uint32
|
||||||
|
|
||||||
|
addrsPorts AddrsPorts // only valid if initProto is initialized
|
||||||
|
|
||||||
|
txPackets atomic.Uint32
|
||||||
|
txBytes atomic.Uint32
|
||||||
|
rxPackets atomic.Uint32
|
||||||
|
rxBytes atomic.Uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: There is some degree of duplicated code.
|
||||||
|
// For example, the functionality to swap the addrsPorts and compute the hash
|
||||||
|
// should be performed by hashTable.update rather than Statistics.update.
|
||||||
|
// However, Go generics cannot invoke pointer methods on addressable values.
|
||||||
|
// See https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#no-way-to-require-pointer-methods
|
||||||
|
|
||||||
|
type addrsPorts interface {
|
||||||
|
comparable
|
||||||
|
asTuple(ipproto.Proto) flowtrack.Tuple
|
||||||
|
}
|
||||||
|
|
||||||
|
type addrsPortsV4 [4 + 4 + 2 + 2]byte
|
||||||
|
|
||||||
|
func (x *addrsPortsV4) addrs() *[8]byte { return (*[8]byte)(x[:]) }
|
||||||
|
func (x *addrsPortsV4) ports() *[4]byte { return (*[4]byte)(x[8:]) }
|
||||||
|
func (x *addrsPortsV4) swap() {
|
||||||
|
*(*[4]byte)(x[0:]), *(*[4]byte)(x[4:]) = *(*[4]byte)(x[4:]), *(*[4]byte)(x[0:])
|
||||||
|
*(*[2]byte)(x[8:]), *(*[2]byte)(x[10:]) = *(*[2]byte)(x[10:]), *(*[2]byte)(x[8:])
|
||||||
|
}
|
||||||
|
func (x addrsPortsV4) asTuple(proto ipproto.Proto) flowtrack.Tuple {
|
||||||
|
return flowtrack.Tuple{Proto: proto,
|
||||||
|
Src: netip.AddrPortFrom(netip.AddrFrom4(*(*[4]byte)(x[0:])), binary.BigEndian.Uint16(x[8:])),
|
||||||
|
Dst: netip.AddrPortFrom(netip.AddrFrom4(*(*[4]byte)(x[4:])), binary.BigEndian.Uint16(x[10:])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type addrsPortsV6 [16 + 16 + 2 + 2]byte
|
||||||
|
|
||||||
|
func (x *addrsPortsV6) addrs() *[32]byte { return (*[32]byte)(x[:]) }
|
||||||
|
func (x *addrsPortsV6) ports() *[4]byte { return (*[4]byte)(x[32:]) }
|
||||||
|
func (x *addrsPortsV6) swap() {
|
||||||
|
*(*[16]byte)(x[0:]), *(*[16]byte)(x[16:]) = *(*[16]byte)(x[16:]), *(*[16]byte)(x[0:])
|
||||||
|
*(*[2]byte)(x[32:]), *(*[2]byte)(x[34:]) = *(*[2]byte)(x[34:]), *(*[2]byte)(x[32:])
|
||||||
|
}
|
||||||
|
func (x addrsPortsV6) asTuple(proto ipproto.Proto) flowtrack.Tuple {
|
||||||
|
return flowtrack.Tuple{Proto: proto,
|
||||||
|
Src: netip.AddrPortFrom(netip.AddrFrom16(*(*[16]byte)(x[0:])), binary.BigEndian.Uint16(x[32:])),
|
||||||
|
Dst: netip.AddrPortFrom(netip.AddrFrom16(*(*[16]byte)(x[16:])), binary.BigEndian.Uint16(x[34:])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTx updates the statistics for a transmitted IP packet.
|
||||||
|
func (s *Statistics) UpdateTx(b []byte) {
|
||||||
|
s.update(b, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRx updates the statistics for a received IP packet.
|
||||||
|
func (s *Statistics) UpdateRx(b []byte) {
|
||||||
|
s.update(b, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seed = maphash.MakeSeed()
|
||||||
|
|
||||||
|
func (s *Statistics) update(b []byte, receive bool) {
|
||||||
|
switch {
|
||||||
|
case len(b) >= 20 && b[0]>>4 == 4: // IPv4
|
||||||
|
proto := ipproto.Proto(b[9])
|
||||||
|
hasPorts := proto == ipproto.TCP || proto == ipproto.UDP
|
||||||
|
var addrsPorts addrsPortsV4
|
||||||
|
if hdrLen := int(4 * (b[0] & 0xf)); hdrLen == 20 && len(b) >= 24 && hasPorts {
|
||||||
|
addrsPorts = *(*addrsPortsV4)(b[12:]) // addresses and ports are contiguous
|
||||||
|
} else {
|
||||||
|
*addrsPorts.addrs() = *(*[8]byte)(b[12:])
|
||||||
|
// May have IPv4 options in-between address and ports.
|
||||||
|
if len(b) >= hdrLen+4 && hasPorts {
|
||||||
|
*addrsPorts.ports() = *(*[4]byte)(b[hdrLen:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if receive {
|
||||||
|
addrsPorts.swap()
|
||||||
|
}
|
||||||
|
hash := maphash.Bytes(seed, addrsPorts[:]) ^ uint64(proto) // TODO: Hash proto better?
|
||||||
|
s.v4.update(receive, proto, &addrsPorts, hash, uint32(len(b)))
|
||||||
|
return
|
||||||
|
case len(b) >= 40 && b[0]>>4 == 6: // IPv6
|
||||||
|
proto := ipproto.Proto(b[6])
|
||||||
|
hasPorts := proto == ipproto.TCP || proto == ipproto.UDP
|
||||||
|
var addrsPorts addrsPortsV6
|
||||||
|
if len(b) >= 44 && hasPorts {
|
||||||
|
addrsPorts = *(*addrsPortsV6)(b[8:]) // addresses and ports are contiguous
|
||||||
|
} else {
|
||||||
|
*addrsPorts.addrs() = *(*[32]byte)(b[8:])
|
||||||
|
// TODO: Support IPv6 extension headers?
|
||||||
|
if hdrLen := 40; len(b) > hdrLen+4 && hasPorts {
|
||||||
|
*addrsPorts.ports() = *(*[4]byte)(b[hdrLen:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if receive {
|
||||||
|
addrsPorts.swap()
|
||||||
|
}
|
||||||
|
hash := maphash.Bytes(seed, addrsPorts[:]) ^ uint64(proto) // TODO: Hash proto better?
|
||||||
|
s.v6.update(receive, proto, &addrsPorts, hash, uint32(len(b)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Track malformed packets?
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashTable[AddrsPorts]) update(receive bool, proto ipproto.Proto, addrsPorts *AddrsPorts, hash uint64, size uint32) {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
|
||||||
|
table := h.active.Load()
|
||||||
|
for {
|
||||||
|
// Start with an initialized table.
|
||||||
|
if table.len() == 0 {
|
||||||
|
table = h.grow(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to update an entry in the currently active table.
|
||||||
|
for i := 0; i < len(*table) && i < maxProbeLen; i++ {
|
||||||
|
probe := uint64(i) // linear probing for small tables
|
||||||
|
if len(*table) > 2*maxProbeLen {
|
||||||
|
probe *= probe // quadratic probing for large tables
|
||||||
|
}
|
||||||
|
entry := &(*table)[(hash+probe)%uint64(len(*table))]
|
||||||
|
|
||||||
|
// Spin-lock waiting for the entry to be initialized,
|
||||||
|
// which should be quick as it only stores the AddrsPort.
|
||||||
|
retry:
|
||||||
|
switch initProto := entry.initProto.Load(); initProto {
|
||||||
|
case 0: // uninitialized
|
||||||
|
if !entry.initProto.CompareAndSwap(0, 1) {
|
||||||
|
goto retry // raced with another initialization attempt
|
||||||
|
}
|
||||||
|
entry.addrsPorts = *addrsPorts
|
||||||
|
entry.initProto.Store(uint32(proto) + 2) // initialization done
|
||||||
|
h.inserts.Add(1)
|
||||||
|
case 1: // initializing
|
||||||
|
goto retry
|
||||||
|
default: // initialized
|
||||||
|
if ipproto.Proto(initProto-2) != proto || entry.addrsPorts != *addrsPorts {
|
||||||
|
continue // this entry is for a different connection; try next entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically update the counters for the connection entry.
|
||||||
|
var overflowPackets, overflowBytes bool
|
||||||
|
if receive {
|
||||||
|
overflowPackets = entry.rxPackets.Add(1) < 1
|
||||||
|
overflowBytes = entry.rxBytes.Add(size) < size
|
||||||
|
} else {
|
||||||
|
overflowPackets = entry.txPackets.Add(1) < 1
|
||||||
|
overflowBytes = entry.txBytes.Add(size) < size
|
||||||
|
}
|
||||||
|
if overflowPackets || overflowBytes {
|
||||||
|
h.updateOverflow(receive, proto, addrsPorts, overflowPackets, overflowBytes)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unable to update, so grow the table and try again.
|
||||||
|
// TODO: Use overflow map instead if table utilization is too low.
|
||||||
|
table = h.grow(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// grow grows the table unless the active table is larger than oldTable.
|
||||||
|
func (h *hashTable[AddrsPorts]) grow(oldTable *countsTable[AddrsPorts]) (newTable *countsTable[AddrsPorts]) {
|
||||||
|
h.muGrow.Lock()
|
||||||
|
defer h.muGrow.Unlock()
|
||||||
|
|
||||||
|
if newTable = h.active.Load(); newTable.len() > oldTable.len() {
|
||||||
|
return newTable // raced with another grow
|
||||||
|
}
|
||||||
|
newTable = new(countsTable[AddrsPorts])
|
||||||
|
if oldTable.len() == 0 {
|
||||||
|
*newTable = make(countsTable[AddrsPorts], minTableLen)
|
||||||
|
} else {
|
||||||
|
*newTable = make(countsTable[AddrsPorts], 2*len(*oldTable))
|
||||||
|
h.outgrown = append(h.outgrown, *oldTable)
|
||||||
|
}
|
||||||
|
h.active.Store(newTable)
|
||||||
|
return newTable
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateOverflow updates the overflow map for counters that overflowed.
|
||||||
|
// Using 32-bit counters, this condition happens rarely as it only triggers
|
||||||
|
// after every 4 GiB of unidirectional network traffic on the same connection.
|
||||||
|
func (h *hashTable[AddrsPorts]) updateOverflow(receive bool, proto ipproto.Proto, addrsPorts *AddrsPorts, overflowPackets, overflowBytes bool) {
|
||||||
|
h.muOverflow.Lock()
|
||||||
|
defer h.muOverflow.Unlock()
|
||||||
|
if h.overflow == nil {
|
||||||
|
h.overflow = make(map[flowtrack.Tuple]Counts)
|
||||||
|
}
|
||||||
|
tuple := (*addrsPorts).asTuple(proto)
|
||||||
|
cnts := h.overflow[tuple]
|
||||||
|
if overflowPackets {
|
||||||
|
if receive {
|
||||||
|
cnts.RxPackets += 1 << 32
|
||||||
|
} else {
|
||||||
|
cnts.TxPackets += 1 << 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if overflowBytes {
|
||||||
|
if receive {
|
||||||
|
cnts.RxBytes += 1 << 32
|
||||||
|
} else {
|
||||||
|
cnts.TxBytes += 1 << 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.overflow[tuple] = cnts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hashTable[AddrsPorts]) extractInto(out map[flowtrack.Tuple]Counts) {
|
||||||
|
// Allocate a new table based on previous usage.
|
||||||
|
var newTable *countsTable[AddrsPorts]
|
||||||
|
if numInserts := h.inserts.Load(); numInserts > 0 {
|
||||||
|
newLen := 1 << bits.Len(uint(4*numInserts/3)|uint(minTableLen-1))
|
||||||
|
newTable = new(countsTable[AddrsPorts])
|
||||||
|
*newTable = make(countsTable[AddrsPorts], newLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap out the old tables for new tables.
|
||||||
|
// We do not need to lock h.muGrow or h.muOverflow since holding h.mu
|
||||||
|
// implies that nothing else could be holding those locks.
|
||||||
|
h.mu.Lock()
|
||||||
|
oldTable := h.active.Swap(newTable)
|
||||||
|
oldOutgrown := h.outgrown
|
||||||
|
oldOverflow := h.overflow
|
||||||
|
h.outgrown = nil
|
||||||
|
h.overflow = nil
|
||||||
|
h.inserts.Store(0)
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
// Merge tables into output.
|
||||||
|
if oldTable != nil {
|
||||||
|
mergeTable(out, *oldTable)
|
||||||
|
}
|
||||||
|
for _, table := range oldOutgrown {
|
||||||
|
mergeTable(out, table)
|
||||||
|
}
|
||||||
|
mergeMap(out, oldOverflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract extracts and resets the counters for all active connections.
|
||||||
|
// It must be called periodically otherwise the memory used is unbounded.
|
||||||
|
func (s *Statistics) Extract() map[flowtrack.Tuple]Counts {
|
||||||
|
out := make(map[flowtrack.Tuple]Counts)
|
||||||
|
s.v4.extractInto(out)
|
||||||
|
s.v6.extractInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeTable[AddrsPorts addrsPorts](dst map[flowtrack.Tuple]Counts, src countsTable[AddrsPorts]) {
|
||||||
|
for i := range src {
|
||||||
|
entry := &src[i]
|
||||||
|
if initProto := entry.initProto.Load(); initProto > 0 {
|
||||||
|
tuple := entry.addrsPorts.asTuple(ipproto.Proto(initProto - 2))
|
||||||
|
cnts := dst[tuple]
|
||||||
|
cnts.TxPackets += uint64(entry.txPackets.Load())
|
||||||
|
cnts.TxBytes += uint64(entry.txBytes.Load())
|
||||||
|
cnts.RxPackets += uint64(entry.rxPackets.Load())
|
||||||
|
cnts.RxBytes += uint64(entry.rxBytes.Load())
|
||||||
|
dst[tuple] = cnts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeMap(dst, src map[flowtrack.Tuple]Counts) {
|
||||||
|
for tuple, cntsSrc := range src {
|
||||||
|
cntsDst := dst[tuple]
|
||||||
|
cntsDst.TxPackets += cntsSrc.TxPackets
|
||||||
|
cntsDst.TxBytes += cntsSrc.TxBytes
|
||||||
|
cntsDst.RxPackets += cntsSrc.RxPackets
|
||||||
|
cntsDst.RxBytes += cntsSrc.RxBytes
|
||||||
|
dst[tuple] = cntsDst
|
||||||
|
}
|
||||||
|
}
|
325
net/tunstats/stats_test.go
Normal file
325
net/tunstats/stats_test.go
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package tunstats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash/maphash"
|
||||||
|
"math"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
|
"tailscale.com/net/flowtrack"
|
||||||
|
"tailscale.com/types/ipproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SimpleStatistics struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
m map[flowtrack.Tuple]Counts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SimpleStatistics) UpdateTx(b []byte) {
|
||||||
|
s.update(b, false)
|
||||||
|
}
|
||||||
|
func (s *SimpleStatistics) UpdateRx(b []byte) {
|
||||||
|
s.update(b, true)
|
||||||
|
}
|
||||||
|
func (s *SimpleStatistics) update(b []byte, receive bool) {
|
||||||
|
var tuple flowtrack.Tuple
|
||||||
|
var size uint64
|
||||||
|
if len(b) >= 1 {
|
||||||
|
// This logic is mostly copied from Statistics.update.
|
||||||
|
switch v := b[0] >> 4; {
|
||||||
|
case v == 4 && len(b) >= 20: // IPv4
|
||||||
|
proto := ipproto.Proto(b[9])
|
||||||
|
size = uint64(binary.BigEndian.Uint16(b[2:]))
|
||||||
|
var addrsPorts addrsPortsV4
|
||||||
|
*(*[8]byte)(addrsPorts[0:]) = *(*[8]byte)(b[12:])
|
||||||
|
if hdrLen := int(4 * (b[0] & 0xf)); len(b) >= hdrLen+4 && (proto == ipproto.TCP || proto == ipproto.UDP) {
|
||||||
|
*(*[4]byte)(addrsPorts[8:]) = *(*[4]byte)(b[hdrLen:])
|
||||||
|
}
|
||||||
|
if receive {
|
||||||
|
addrsPorts.swap()
|
||||||
|
}
|
||||||
|
tuple = addrsPorts.asTuple(proto)
|
||||||
|
case v == 6 && len(b) >= 40: // IPv6
|
||||||
|
proto := ipproto.Proto(b[6])
|
||||||
|
size = uint64(binary.BigEndian.Uint16(b[4:]))
|
||||||
|
var addrsPorts addrsPortsV6
|
||||||
|
*(*[32]byte)(addrsPorts[0:]) = *(*[32]byte)(b[8:])
|
||||||
|
if hdrLen := 40; len(b) > hdrLen+4 && (proto == ipproto.TCP || proto == ipproto.UDP) {
|
||||||
|
*(*[4]byte)(addrsPorts[32:]) = *(*[4]byte)(b[hdrLen:])
|
||||||
|
}
|
||||||
|
if receive {
|
||||||
|
addrsPorts.swap()
|
||||||
|
}
|
||||||
|
tuple = addrsPorts.asTuple(proto)
|
||||||
|
default:
|
||||||
|
return // non-IP packet
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return // invalid packet
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.m == nil {
|
||||||
|
s.m = make(map[flowtrack.Tuple]Counts)
|
||||||
|
}
|
||||||
|
cnts := s.m[tuple]
|
||||||
|
if receive {
|
||||||
|
cnts.RxPackets++
|
||||||
|
cnts.RxBytes += size
|
||||||
|
} else {
|
||||||
|
cnts.TxPackets++
|
||||||
|
cnts.TxBytes += size
|
||||||
|
}
|
||||||
|
s.m[tuple] = cnts
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmpty(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
var s Statistics
|
||||||
|
c.Assert(s.Extract(), qt.DeepEquals, map[flowtrack.Tuple]Counts{})
|
||||||
|
c.Assert(s.Extract(), qt.DeepEquals, map[flowtrack.Tuple]Counts{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverflow(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
var s Statistics
|
||||||
|
var cnts Counts
|
||||||
|
|
||||||
|
a := &addrsPortsV4{192, 168, 0, 1, 192, 168, 0, 2, 12, 34, 56, 78}
|
||||||
|
h := maphash.Bytes(seed, a[:])
|
||||||
|
|
||||||
|
cnts.TxPackets++
|
||||||
|
cnts.TxBytes += math.MaxUint32
|
||||||
|
s.v4.update(false, ipproto.UDP, a, h, math.MaxUint32)
|
||||||
|
for i := 0; i < 1e6; i++ {
|
||||||
|
cnts.TxPackets++
|
||||||
|
cnts.TxBytes += uint64(i)
|
||||||
|
s.v4.update(false, ipproto.UDP, a, h, uint32(i))
|
||||||
|
}
|
||||||
|
c.Assert(s.Extract(), qt.DeepEquals, map[flowtrack.Tuple]Counts{a.asTuple(ipproto.UDP): cnts})
|
||||||
|
c.Assert(s.Extract(), qt.DeepEquals, map[flowtrack.Tuple]Counts{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParse(f *testing.F) {
|
||||||
|
f.Fuzz(func(t *testing.T, b []byte) {
|
||||||
|
var s Statistics
|
||||||
|
s.UpdateRx(b) // must not panic
|
||||||
|
s.UpdateTx(b) // must not panic
|
||||||
|
s.Extract() // must not panic
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var testV4 = func() (b [24]byte) {
|
||||||
|
b[0] = 4<<4 | 5 // version and header length
|
||||||
|
binary.BigEndian.PutUint16(b[2:], 1234) // size
|
||||||
|
b[9] = byte(ipproto.UDP) // protocol
|
||||||
|
*(*[4]byte)(b[12:]) = [4]byte{192, 168, 0, 1} // src addr
|
||||||
|
*(*[4]byte)(b[16:]) = [4]byte{192, 168, 0, 2} // dst addr
|
||||||
|
binary.BigEndian.PutUint16(b[20:], 456) // src port
|
||||||
|
binary.BigEndian.PutUint16(b[22:], 789) // dst port
|
||||||
|
return b
|
||||||
|
}()
|
||||||
|
|
||||||
|
/*
|
||||||
|
func BenchmarkA(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
s.UpdateTx(testV4[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkB(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s SimpleStatistics
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
s.UpdateTx(testV4[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkC(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for k := 0; k < runtime.NumCPU(); k++ {
|
||||||
|
group.Add(1)
|
||||||
|
go func(k int) {
|
||||||
|
defer group.Done()
|
||||||
|
b := testV4
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
binary.LittleEndian.PutUint32(b[12:], uint32(k))
|
||||||
|
binary.LittleEndian.PutUint32(b[16:], uint32(j))
|
||||||
|
s.UpdateTx(b[:])
|
||||||
|
}
|
||||||
|
}(k)
|
||||||
|
}
|
||||||
|
group.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkD(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s SimpleStatistics
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for k := 0; k < runtime.NumCPU(); k++ {
|
||||||
|
group.Add(1)
|
||||||
|
go func(k int) {
|
||||||
|
defer group.Done()
|
||||||
|
b := testV4
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
binary.LittleEndian.PutUint32(b[12:], uint32(k))
|
||||||
|
binary.LittleEndian.PutUint32(b[16:], uint32(j))
|
||||||
|
s.UpdateTx(b[:])
|
||||||
|
}
|
||||||
|
}(k)
|
||||||
|
}
|
||||||
|
group.Wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// FUZZ
|
||||||
|
// Benchmark:
|
||||||
|
// IPv4 vs IPv6
|
||||||
|
// single vs all cores
|
||||||
|
// same vs unique addresses
|
||||||
|
|
||||||
|
/*
|
||||||
|
linear probing
|
||||||
|
|
||||||
|
1 => 115595714 ns/op 859003746 B/op
|
||||||
|
2 => 9355585 ns/op 46454947 B/op
|
||||||
|
4 => 3301663 ns/op 8706967 B/op
|
||||||
|
8 => 2775162 ns/op 4176433 B/op
|
||||||
|
16 => 2517899 ns/op 2099434 B/op
|
||||||
|
32 => 2397939 ns/op 2098986 B/op
|
||||||
|
64 => 2118390 ns/op 1197352 B/op
|
||||||
|
128 => 2029255 ns/op 1046729 B/op
|
||||||
|
256 => 2069939 ns/op 1042577 B/op
|
||||||
|
|
||||||
|
quadratic probing
|
||||||
|
|
||||||
|
1 => 111134367 ns/op 825962200 B/op
|
||||||
|
2 => 8061189 ns/op 45106117 B/op
|
||||||
|
4 => 3216728 ns/op 8079556 B/op
|
||||||
|
8 => 2576443 ns/op 2355890 B/op
|
||||||
|
16 => 2471713 ns/op 2097196 B/op
|
||||||
|
32 => 2108294 ns/op 1050225 B/op
|
||||||
|
64 => 1964441 ns/op 1048736 B/op
|
||||||
|
128 => 2118538 ns/op 1046663 B/op
|
||||||
|
256 => 1968353 ns/op 1042568 B/op
|
||||||
|
512 => 2049336 ns/op 1034306 B/op
|
||||||
|
1024 => 2001605 ns/op 1017786 B/op
|
||||||
|
2048 => 2046972 ns/op 984988 B/op
|
||||||
|
4096 => 2108753 ns/op 919105 B/op
|
||||||
|
*/
|
||||||
|
|
||||||
|
func testPacketV4(proto ipproto.Proto, srcAddr, dstAddr [4]byte, srcPort, dstPort, size uint16) (out []byte) {
|
||||||
|
var ipHdr [20]byte
|
||||||
|
ipHdr[0] = 4<<4 | 5
|
||||||
|
binary.BigEndian.PutUint16(ipHdr[2:], size)
|
||||||
|
ipHdr[9] = byte(proto)
|
||||||
|
*(*[4]byte)(ipHdr[12:]) = srcAddr
|
||||||
|
*(*[4]byte)(ipHdr[16:]) = dstAddr
|
||||||
|
out = append(out, ipHdr[:]...)
|
||||||
|
switch proto {
|
||||||
|
case ipproto.TCP:
|
||||||
|
var tcpHdr [20]byte
|
||||||
|
binary.BigEndian.PutUint16(tcpHdr[0:], srcPort)
|
||||||
|
binary.BigEndian.PutUint16(tcpHdr[2:], dstPort)
|
||||||
|
out = append(out, tcpHdr[:]...)
|
||||||
|
case ipproto.UDP:
|
||||||
|
var udpHdr [8]byte
|
||||||
|
binary.BigEndian.PutUint16(udpHdr[0:], srcPort)
|
||||||
|
binary.BigEndian.PutUint16(udpHdr[2:], dstPort)
|
||||||
|
out = append(out, udpHdr[:]...)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown proto: %d", proto))
|
||||||
|
}
|
||||||
|
return append(out, make([]byte, int(size)-len(out))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Benchmark(b *testing.B) {
|
||||||
|
b.Run("SingleRoutine/SameConn", func(b *testing.B) {
|
||||||
|
p := testPacketV4(ipproto.UDP, [4]byte{192, 168, 0, 1}, [4]byte{192, 168, 0, 2}, 123, 456, 789)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
s.UpdateTx(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("SingleRoutine/UniqueConns", func(b *testing.B) {
|
||||||
|
p := testPacketV4(ipproto.UDP, [4]byte{}, [4]byte{}, 0, 0, 789)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
for j := 0; j < 1e3; j++ {
|
||||||
|
binary.BigEndian.PutUint32(p[20:], uint32(j)) // unique port combination
|
||||||
|
s.UpdateTx(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("MultiRoutine/SameConn", func(b *testing.B) {
|
||||||
|
p := testPacketV4(ipproto.UDP, [4]byte{192, 168, 0, 1}, [4]byte{192, 168, 0, 2}, 123, 456, 789)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for j := 0; j < runtime.NumCPU(); j++ {
|
||||||
|
group.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer group.Done()
|
||||||
|
for k := 0; k < 1e3; k++ {
|
||||||
|
s.UpdateTx(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
group.Wait()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("MultiRoutine/UniqueConns", func(b *testing.B) {
|
||||||
|
ps := make([][]byte, runtime.NumCPU())
|
||||||
|
for i := range ps {
|
||||||
|
ps[i] = testPacketV4(ipproto.UDP, [4]byte{192, 168, 0, 1}, [4]byte{192, 168, 0, 2}, 0, 0, 789)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var s Statistics
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for j := 0; j < runtime.NumCPU(); j++ {
|
||||||
|
group.Add(1)
|
||||||
|
go func(j int) {
|
||||||
|
defer group.Done()
|
||||||
|
p := ps[j]
|
||||||
|
j *= 1e3
|
||||||
|
for k := 0; k < 1e3; k++ {
|
||||||
|
binary.BigEndian.PutUint32(p[20:], uint32(j+k)) // unique port combination
|
||||||
|
s.UpdateTx(p)
|
||||||
|
}
|
||||||
|
}(j)
|
||||||
|
}
|
||||||
|
group.Wait()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user