feat: create profiling package + rewrite profiling tutorial
Cadoles/bouncer/pipeline/head This commit was not built
Details
Cadoles/bouncer/pipeline/head This commit was not built
Details
This commit is contained in:
parent
c73fe8cca5
commit
f35384c0f3
|
@ -10,4 +10,4 @@
|
||||||
/out
|
/out
|
||||||
.dockerconfigjson
|
.dockerconfigjson
|
||||||
*.prof
|
*.prof
|
||||||
proxy.test
|
*.test
|
2
Makefile
2
Makefile
|
@ -131,7 +131,7 @@ tools/grafterm/bin/grafterm:
|
||||||
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -bench=. -run '^$$' -count=10 ./...
|
go test -bench=. -run '^$$' ./internal/bench
|
||||||
|
|
||||||
tools/benchstat/bin/benchstat:
|
tools/benchstat/bin/benchstat:
|
||||||
mkdir -p tools/benchstat/bin
|
mkdir -p tools/benchstat/bin
|
||||||
|
|
|
@ -1,31 +1,54 @@
|
||||||
# Étudier les performances de Bouncer
|
# Étudier les performances de Bouncer
|
||||||
|
|
||||||
1. Lancer un benchmark du proxy
|
Le package `./internal` est dédié à l'étude des performances de Bouncer. Il contient une suite de benchmarks simulant de proxies avec différentes configurations de layers afin d'évaluer les points d'engorgement sur le traitement des requêtes.
|
||||||
|
|
||||||
```shell
|
Voir le répertoire `./internal/bench/testdata/proxies` pour voir les différentes configurations de cas.
|
||||||
go test -bench=. -run '^$' -count=5 -cpuprofile bench_proxy.prof ./internal/proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Visualiser les temps d'exécution
|
## Lancer les benchmarks
|
||||||
|
|
||||||
```shell
|
Le plus simple est d'utiliser la commande `make bench` qui exécutera séquentiellement tous les benchmarks. Il est également possible de lancer un benchmark spécifique via la commande suivante:
|
||||||
go tool pprof -web bench_proxy.prof
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Comparer les performances d'une exécution à l'autre
|
```bash
|
||||||
|
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
|
||||||
|
```
|
||||||
|
|
||||||
```shell
|
Par exemple:
|
||||||
# Lancer un premier benchmark
|
|
||||||
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_before.txt
|
|
||||||
|
|
||||||
# Faire des modifications sur les sources
|
```bash
|
||||||
|
# Pour exécuter ./internal/bench/testdata/proxies/basic-auth.yml
|
||||||
|
go test -bench='BenchmarkProxies/basic-auth' -run='^$' ./internal/bench
|
||||||
|
```
|
||||||
|
|
||||||
# Lancer un second benchmark
|
## Visualiser les profils d'exécution
|
||||||
go test -bench=. -run '^$' -count=10 ./internal/proxy > bench_after.txt
|
|
||||||
|
|
||||||
# Installer l'outil benchstat
|
Vous pouvez visualiser les profils d'exécution via la commande suivante:
|
||||||
make tools/benchstat/bin/benchstat
|
|
||||||
|
|
||||||
# Comparer les rapports
|
```shell
|
||||||
tools/benchstat/bin/benchstat bench_before.txt bench_after.txt
|
go tool pprof -web path/to/file.prof
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Par défaut l'exécution des benchmarks créera automatiquement des fichiers de profil dans le répertoire `./internal/bench/testdata/proxies`.
|
||||||
|
|
||||||
|
Par exemple:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go tool pprof -web ./internal/bench/testdata/proxies/basic-auth.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparer les évolutions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lancer un premier benchmark
|
||||||
|
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
|
||||||
|
|
||||||
|
# Faire une sauvegarde du fichier de profil
|
||||||
|
cp ./internal/bench/testdata/proxies/$BENCH_CASE.prof ./internal/bench/testdata/proxies/$BENCH_CASE-prev.prof
|
||||||
|
|
||||||
|
# Faire des modifications sur les sources
|
||||||
|
|
||||||
|
# Lancer un second benchmark
|
||||||
|
go test -bench="BenchmarkProxies/$BENCH_CASE" -run='^$' ./internal/bench
|
||||||
|
|
||||||
|
# Visualiser la différence entre les deux profils
|
||||||
|
go tool pprof -web -base=./internal/bench/testdata/proxies/$BENCH_CASE-prev.prof ./internal/bench/testdata/proxies/$BENCH_CASE.prof
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,300 @@
|
||||||
|
package proxy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/pprof"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/setup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkProxies(b *testing.B) {
|
||||||
|
proxyFiles, err := filepath.Glob("testdata/proxies/*.yml")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range proxyFiles {
|
||||||
|
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
|
||||||
|
|
||||||
|
b.Run(name, func(b *testing.B) {
|
||||||
|
conf, err := loadProxyBenchConfig(f)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%+v", errors.Wrapf(err, "could notre load bench config"))
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, backend, err := createProxy(name, conf, b.Logf)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%+v", errors.Wrapf(err, "could not create proxy"))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer proxy.Close()
|
||||||
|
|
||||||
|
if backend != nil {
|
||||||
|
defer backend.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
client := proxy.Client()
|
||||||
|
|
||||||
|
proxyURL, err := url.Parse(proxy.URL)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%+v", errors.Wrapf(err, "could not parse proxy url"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Fetch.URL.Path != "" {
|
||||||
|
proxyURL.Path = conf.Fetch.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Fetch.URL.RawQuery != "" {
|
||||||
|
proxyURL.RawQuery = conf.Fetch.URL.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Fetch.URL.User.Username != "" || conf.Fetch.URL.User.Password != "" {
|
||||||
|
proxyURL.User = url.UserPassword(conf.Fetch.URL.User.Username, conf.Fetch.URL.User.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawProxyURL := proxyURL.String()
|
||||||
|
|
||||||
|
b.Logf("fetching url '%s'", rawProxyURL)
|
||||||
|
|
||||||
|
profile, err := os.Create(filepath.Join("testdata", "proxies", name+".prof"))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("%+v", errors.Wrapf(err, "could not create cpu profile"))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer profile.Close()
|
||||||
|
|
||||||
|
if err := pprof.StartCPUProfile(profile); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer pprof.StopCPUProfile()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
res, err := client.Get(rawProxyURL)
|
||||||
|
if err != nil {
|
||||||
|
b.Errorf("could not fetch proxy url: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
b.Errorf("could not read response body: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Logf("%s \n %v", res.Status, string(body))
|
||||||
|
|
||||||
|
if err := res.Body.Close(); err != nil {
|
||||||
|
b.Errorf("could not close response body: %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxyBenchConfig struct {
|
||||||
|
Proxy config.BootstrapProxyConfig `yaml:"proxy"`
|
||||||
|
Fetch fetchBenchConfig `yaml:"fetch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fetchBenchConfig struct {
|
||||||
|
URL fetchURLBenchConfig `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fetchURLBenchConfig struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
RawQuery string `yaml:"rawQuery"`
|
||||||
|
User fetchURLUserBenchConfig `yaml:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fetchURLUserBenchConfig struct {
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProxyBenchConfig(filename string) (*proxyBenchConfig, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not read file '%s'", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := proxyBenchConfig{}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &conf); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not unmarshal config")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createProxy(name string, conf *proxyBenchConfig, logf func(format string, a ...any)) (*httptest.Server, *httptest.Server, error) {
|
||||||
|
redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR")
|
||||||
|
if redisEndpoint == "" {
|
||||||
|
redisEndpoint = "127.0.0.1:6379"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||||
|
Addrs: []string{redisEndpoint},
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyRepository := redisStore.NewProxyRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
|
||||||
|
layerRepository := redisStore.NewLayerRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
|
||||||
|
|
||||||
|
var backend *httptest.Server
|
||||||
|
|
||||||
|
if conf.Proxy.To == "" {
|
||||||
|
backend = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
if _, err := w.Write([]byte("Hello, world.")); err != nil {
|
||||||
|
logf("[ERROR] %+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
if err := waitFor(backend.URL, 5*time.Second); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logf("started backend '%s'", backend.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
proxyName := store.ProxyName("bench-" + name)
|
||||||
|
|
||||||
|
proxies, err := proxyRepository.QueryProxy(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup existing proxies
|
||||||
|
for _, p := range proxies {
|
||||||
|
if err := proxyRepository.DeleteProxy(ctx, p.Name); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logf("creating proxy '%s'", proxyName)
|
||||||
|
|
||||||
|
to := string(conf.Proxy.To)
|
||||||
|
if to == "" {
|
||||||
|
to = backend.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := proxyRepository.CreateProxy(ctx, proxyName, to, conf.Proxy.From...); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for layerName, layerConf := range conf.Proxy.Layers {
|
||||||
|
if err := layerRepository.DeleteLayer(ctx, proxyName, store.LayerName(layerName)); err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := layerRepository.CreateLayer(ctx, proxyName, store.LayerName(layerName), store.LayerType(layerConf.Type), layerConf.Options.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = layerRepository.UpdateLayer(ctx, proxyName, store.LayerName(layerName), store.WithLayerUpdateEnabled(bool(layerConf.Enabled)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
layers, err := setup.GetLayers(context.Background(), config.NewDefault())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
director := director.New(
|
||||||
|
proxyRepository, layerRepository,
|
||||||
|
director.WithLayerCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Layer](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
director.WithProxyCache(
|
||||||
|
ttl.NewCache(
|
||||||
|
memory.NewCache[string, []*store.Proxy](),
|
||||||
|
memory.NewCache[string, time.Time](),
|
||||||
|
30*time.Second,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
director.WithLayers(layers...),
|
||||||
|
)
|
||||||
|
|
||||||
|
directorMiddleware := director.Middleware()
|
||||||
|
|
||||||
|
handler := proxy.New(
|
||||||
|
proxy.WithRequestTransformers(
|
||||||
|
director.RequestTransformer(),
|
||||||
|
),
|
||||||
|
proxy.WithResponseTransformers(
|
||||||
|
director.ResponseTransformer(),
|
||||||
|
),
|
||||||
|
proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
|
||||||
|
reverse := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
logf("[ERROR] %s", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
return reverse
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
server := httptest.NewServer(directorMiddleware(handler))
|
||||||
|
|
||||||
|
return server, backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitFor(url string, ttl time.Duration) error {
|
||||||
|
var lastErr error
|
||||||
|
timeout := time.After(ttl)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
if lastErr != nil {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("wait timed out")
|
||||||
|
default:
|
||||||
|
res, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = errors.WithStack(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 200 && res.StatusCode < 400 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
proxy:
|
||||||
|
from: ["*"]
|
||||||
|
to: ""
|
||||||
|
layers:
|
||||||
|
basic-auth:
|
||||||
|
type: authn-basic
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
users:
|
||||||
|
- username: foo
|
||||||
|
passwordHash: "$2y$10$ShTc856wMB8PCxyr46qJRO8z06MpV4UejAVRDJ/bixhu0XTGn7Giy"
|
||||||
|
attributes:
|
||||||
|
email: foo@bar.com
|
||||||
|
rules:
|
||||||
|
- set_header("Remote-User-Attr-Email", user.attrs.email)
|
||||||
|
fetch:
|
||||||
|
url:
|
||||||
|
user:
|
||||||
|
username: foo
|
||||||
|
password: bar
|
|
@ -0,0 +1,3 @@
|
||||||
|
proxy:
|
||||||
|
from: ["*"]
|
||||||
|
to: ""
|
|
@ -0,0 +1,12 @@
|
||||||
|
proxy:
|
||||||
|
from: ["*"]
|
||||||
|
to: ""
|
||||||
|
layers:
|
||||||
|
host-rewriter:
|
||||||
|
type: rewriter
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
rules:
|
||||||
|
request:
|
||||||
|
- set_host(request.url.host)
|
||||||
|
- set_header("X-Proxied-With", "bouncer")
|
|
@ -80,9 +80,22 @@ func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, err
|
||||||
|
|
||||||
proxies := make(map[store.ProxyName]BootstrapProxyConfig)
|
proxies := make(map[store.ProxyName]BootstrapProxyConfig)
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
data, err := os.ReadFile(f)
|
proxy, err := loadBootstrappedProxyConfig(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "could not read file '%s'", f)
|
return nil, errors.Wrapf(err, "could not load proxy bootstrap file '%s'", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
|
||||||
|
proxies[name] = *proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBootstrappedProxyConfig(filename string) (*BootstrapProxyConfig, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not read file '%s'", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := BootstrapProxyConfig{}
|
proxy := BootstrapProxyConfig{}
|
||||||
|
@ -91,11 +104,7 @@ func loadBootstrapDir(dir string) (map[store.ProxyName]BootstrapProxyConfig, err
|
||||||
return nil, errors.Wrapf(err, "could not unmarshal proxy")
|
return nil, errors.Wrapf(err, "could not unmarshal proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
name := store.ProxyName(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
|
return &proxy, nil
|
||||||
proxies[name] = proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig {
|
func overrideProxies(base map[store.ProxyName]BootstrapProxyConfig, proxies map[store.ProxyName]BootstrapProxyConfig) map[store.ProxyName]BootstrapProxyConfig {
|
||||||
|
|
|
@ -127,7 +127,7 @@ func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (im *InterpolatedMap) interpolateRecursive(data any) (any, error) {
|
func (im InterpolatedMap) interpolateRecursive(data any) (any, error) {
|
||||||
switch typ := data.(type) {
|
switch typ := data.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
for key, value := range typ {
|
for key, value := range typ {
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
package proxy_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/Cadoles/go-proxy"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/cache/memory"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/cache/ttl"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
|
||||||
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
|
||||||
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
func BenchmarkProxy(b *testing.B) {
|
|
||||||
redisEndpoint := os.Getenv("BOUNCER_BENCH_REDIS_ADDR")
|
|
||||||
if redisEndpoint == "" {
|
|
||||||
redisEndpoint = "127.0.0.1:6379"
|
|
||||||
}
|
|
||||||
|
|
||||||
client := redis.NewUniversalClient(&redis.UniversalOptions{
|
|
||||||
Addrs: []string{redisEndpoint},
|
|
||||||
})
|
|
||||||
|
|
||||||
proxyRepository := redisStore.NewProxyRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
|
|
||||||
layerRepository := redisStore.NewLayerRepository(client, redisStore.DefaultTxMaxAttempts, redisStore.DefaultTxBaseDelay)
|
|
||||||
|
|
||||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
if _, err := w.Write([]byte("Hello, world.")); err != nil {
|
|
||||||
b.Logf("[ERROR] %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer backend.Close()
|
|
||||||
|
|
||||||
if err := waitFor(backend.URL, 5*time.Second); err != nil {
|
|
||||||
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Logf("started backend '%s'", backend.URL)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
proxyName := store.ProxyName(b.Name())
|
|
||||||
|
|
||||||
b.Logf("creating proxy '%s'", proxyName)
|
|
||||||
|
|
||||||
if err := proxyRepository.DeleteProxy(ctx, proxyName); err != nil {
|
|
||||||
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := proxyRepository.CreateProxy(ctx, proxyName, backend.URL, "*"); err != nil {
|
|
||||||
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := proxyRepository.UpdateProxy(ctx, proxyName, store.WithProxyUpdateEnabled(true)); err != nil {
|
|
||||||
b.Fatalf("[FATAL] %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
director := director.New(
|
|
||||||
proxyRepository, layerRepository,
|
|
||||||
director.WithLayerCache(
|
|
||||||
ttl.NewCache(
|
|
||||||
memory.NewCache[string, []*store.Layer](),
|
|
||||||
memory.NewCache[string, time.Time](),
|
|
||||||
30*time.Second,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
director.WithProxyCache(
|
|
||||||
ttl.NewCache(
|
|
||||||
memory.NewCache[string, []*store.Proxy](),
|
|
||||||
memory.NewCache[string, time.Time](),
|
|
||||||
30*time.Second,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
directorMiddleware := director.Middleware()
|
|
||||||
|
|
||||||
handler := proxy.New(
|
|
||||||
proxy.WithRequestTransformers(
|
|
||||||
director.RequestTransformer(),
|
|
||||||
),
|
|
||||||
proxy.WithResponseTransformers(
|
|
||||||
director.ResponseTransformer(),
|
|
||||||
),
|
|
||||||
proxy.WithReverseProxyFactory(func(ctx context.Context, target *url.URL) *httputil.ReverseProxy {
|
|
||||||
reverse := httputil.NewSingleHostReverseProxy(target)
|
|
||||||
reverse.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
b.Logf("[ERROR] %s", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
return reverse
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
server := httptest.NewServer(directorMiddleware(handler))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
b.Logf("started proxy '%s'", server.URL)
|
|
||||||
|
|
||||||
httpClient := server.Client()
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
res, err := httpClient.Get(server.URL)
|
|
||||||
if err != nil {
|
|
||||||
b.Errorf("could not fetch server url: %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
b.Errorf("could not read response body: %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Logf("%s - %v", res.Status, string(body))
|
|
||||||
|
|
||||||
if err := res.Body.Close(); err != nil {
|
|
||||||
b.Errorf("could not close response body: %+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitFor(url string, ttl time.Duration) error {
|
|
||||||
var lastErr error
|
|
||||||
timeout := time.After(ttl)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timeout:
|
|
||||||
if lastErr != nil {
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("wait timed out")
|
|
||||||
default:
|
|
||||||
res, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = errors.WithStack(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 400 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,7 +36,7 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
|
||||||
|
|
||||||
CreatedAt: wrap(now),
|
CreatedAt: wrap(now),
|
||||||
UpdatedAt: wrap(now),
|
UpdatedAt: wrap(now),
|
||||||
Options: wrap(store.LayerOptions{}),
|
Options: wrap(options),
|
||||||
}
|
}
|
||||||
|
|
||||||
txf := func(tx *redis.Tx) error {
|
txf := func(tx *redis.Tx) error {
|
||||||
|
@ -60,6 +60,11 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layerItem, err = r.txGetLayerItem(ctx, tx, proxyName, layerName)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,16 +75,16 @@ func (r *LayerRepository) CreateLayer(ctx context.Context, proxyName store.Proxy
|
||||||
|
|
||||||
return &store.Layer{
|
return &store.Layer{
|
||||||
LayerHeader: store.LayerHeader{
|
LayerHeader: store.LayerHeader{
|
||||||
Name: layerName,
|
Name: store.LayerName(layerItem.Name),
|
||||||
Proxy: proxyName,
|
Proxy: store.ProxyName(layerItem.Proxy),
|
||||||
Type: layerType,
|
Type: store.LayerType(layerItem.Type),
|
||||||
Weight: 0,
|
Weight: layerItem.Weight,
|
||||||
Enabled: false,
|
Enabled: layerItem.Enabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
CreatedAt: now,
|
CreatedAt: layerItem.CreatedAt.Value(),
|
||||||
UpdatedAt: now,
|
UpdatedAt: layerItem.UpdatedAt.Value(),
|
||||||
Options: store.LayerOptions{},
|
Options: layerItem.Options.Value(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue