add hotpath proxy and roundtripper benchmarks

This commit is contained in:
Alisdair MacLeod
2026-02-11 09:47:40 +00:00
parent b79adb706c
commit 5ae15b3af3
2 changed files with 237 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
package proxy_test
import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/netbirdio/netbird/proxy/internal/proxy"
"github.com/netbirdio/netbird/proxy/internal/types"
)
type nopTransport struct{}
func (nopTransport) RoundTrip(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: http.NoBody,
}, nil
}
func BenchmarkServeHTTP(b *testing.B) {
rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil)
rp.AddMapping(proxy.Mapping{
ID: rand.Text(),
AccountID: types.AccountID(rand.Text()),
Host: "app.example.com",
Paths: map[string]*url.URL{
"/": {
Scheme: "http",
Host: "10.0.0.1:8080",
},
},
})
req := httptest.NewRequest(http.MethodGet, "http://app.example.com", nil)
req.Host = "app.example.com"
req.RemoteAddr = "203.0.113.50:12345"
for b.Loop() {
rp.ServeHTTP(httptest.NewRecorder(), req)
}
}
func BenchmarkServeHTTPHostCount(b *testing.B) {
hostCounts := []int{1, 10, 100, 1_000, 10_000}
for _, hostCount := range hostCounts {
b.Run(fmt.Sprintf("hosts=%d", hostCount), func(b *testing.B) {
rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil)
var target string
targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(hostCount)))
if err != nil {
b.Fatal(err)
}
for i := range hostCount {
id := rand.Text()
host := fmt.Sprintf("%s.example.com", id)
if int64(i) == targetIndex.Int64() {
target = id
}
rp.AddMapping(proxy.Mapping{
ID: id,
AccountID: types.AccountID(rand.Text()),
Host: host,
Paths: map[string]*url.URL{
"/": {
Scheme: "http",
Host: "10.0.0.1:8080",
},
},
})
}
req := httptest.NewRequest(http.MethodGet, "http://"+target+"/", nil)
req.Host = target
req.RemoteAddr = "203.0.113.50:12345"
for b.Loop() {
rp.ServeHTTP(httptest.NewRecorder(), req)
}
})
}
}
func BenchmarkServeHTTPPathCount(b *testing.B) {
pathCounts := []int{1, 5, 10, 25, 50}
for _, pathCount := range pathCounts {
b.Run(fmt.Sprintf("paths=%d", pathCount), func(b *testing.B) {
rp := proxy.NewReverseProxy(nopTransport{}, "http", nil, nil)
var target string
targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(pathCount)))
if err != nil {
b.Fatal(err)
}
paths := make(map[string]*url.URL, pathCount)
for i := range pathCount {
path := "/" + rand.Text()
if int64(i) == targetIndex.Int64() {
target = path
}
paths[path] = &url.URL{
Scheme: "http",
Host: "10.0.0.1:" + fmt.Sprintf("%d", 8080+i),
}
}
rp.AddMapping(proxy.Mapping{
ID: rand.Text(),
AccountID: types.AccountID(rand.Text()),
Host: "app.example.com",
Paths: paths,
})
req := httptest.NewRequest(http.MethodGet, "http://app.example.com"+target, nil)
req.Host = "app.example.com"
req.RemoteAddr = "203.0.113.50:12345"
for b.Loop() {
rp.ServeHTTP(httptest.NewRecorder(), req)
}
})
}
}

View File

@@ -0,0 +1,107 @@
package roundtrip
import (
"crypto/rand"
"math/big"
"sync"
"testing"
"time"
"github.com/netbirdio/netbird/proxy/internal/types"
"github.com/netbirdio/netbird/shared/management/domain"
)
// Simple benchmark for comparison with AddPeer contention.
func BenchmarkHasClient(b *testing.B) {
// Knobs for dialling in:
initialClientCount := 100 // Size of initial peer map to generate.
nb := mockNetBird()
var target types.AccountID
targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(initialClientCount)))
if err != nil {
b.Fatal(err)
}
for i := range initialClientCount {
id := types.AccountID(rand.Text())
if int64(i) == targetIndex.Int64() {
target = id
}
nb.clients[id] = &clientEntry{
domains: map[domain.Domain]domainInfo{
domain.Domain(rand.Text()): {
reverseProxyID: rand.Text(),
},
},
createdAt: time.Now(),
started: true,
}
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
nb.HasClient(target)
}
})
b.StopTimer()
}
func BenchmarkHasClientDuringAddPeer(b *testing.B) {
// Knobs for dialling in:
initialClientCount := 100 // Size of initial peer map to generate.
addPeerWorkers := 5 // Number of workers to concurrently call AddPeer.
nb := mockNetBird()
// Add random client entries to the netbird instance.
// We're trying to test map lock contention, so starting with
// a populated map should help with this.
// Pick a random one to target for retrieval later.
var target types.AccountID
targetIndex, err := rand.Int(rand.Reader, big.NewInt(int64(initialClientCount)))
if err != nil {
b.Fatal(err)
}
for i := range initialClientCount {
id := types.AccountID(rand.Text())
if int64(i) == targetIndex.Int64() {
target = id
}
nb.clients[id] = &clientEntry{
domains: map[domain.Domain]domainInfo{
domain.Domain(rand.Text()): {
reverseProxyID: rand.Text(),
},
},
createdAt: time.Now(),
started: true,
}
}
// Launch workers that continuously call AddPeer with new random accountIDs.
var wg sync.WaitGroup
for range addPeerWorkers {
wg.Go(func() {
for {
if err := nb.AddPeer(b.Context(),
types.AccountID(rand.Text()),
domain.Domain(rand.Text()),
rand.Text(),
rand.Text()); err != nil {
b.Log(err)
}
}
})
}
// Benchmark calling HasClient during AddPeer contention.
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
nb.HasClient(target)
}
})
b.StopTimer()
}