github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/benchmark/internal/loadgen/loadgen.go (about)

     1  package loadgen
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"math/rand"
     8  	"net/http"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/davecgh/go-spew/spew"
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"github.com/prometheus/client_golang/prometheus/promauto"
    15  	"github.com/prometheus/client_golang/prometheus/push"
    16  	"github.com/pyroscope-io/pyroscope/benchmark/internal/config"
    17  	"github.com/pyroscope-io/pyroscope/benchmark/internal/server"
    18  	"github.com/pyroscope-io/pyroscope/pkg/agent/upstream"
    19  	"github.com/pyroscope-io/pyroscope/pkg/agent/upstream/remote"
    20  	"github.com/pyroscope-io/pyroscope/pkg/storage/metadata"
    21  	"github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie"
    22  	"github.com/sirupsen/logrus"
    23  )
    24  
    25  // how many retries to check the pyroscope server is up
    26  const MaxReadinessRetries = 10
    27  
    28  type Fixtures [][]*transporttrie.Trie
    29  
    30  var defaultDurationBetweenUploads = 10 * time.Second
    31  
    32  type LoadGen struct {
    33  	Config    *config.LoadGen
    34  	Rand      *rand.Rand
    35  	SymbolBuf []byte
    36  	Tags      []string
    37  
    38  	runProgressMetric prometheus.Gauge
    39  	uploadErrors      prometheus.Counter
    40  	successfulUploads prometheus.Counter
    41  	pusher            GatewayPusher
    42  
    43  	// RWMutex allows one writer or multiple readers.
    44  	//  this fits great for the case where we want to pause writes before calling render endpoint
    45  	//  render thread becomes a "writer", and client upload threads become "readers"
    46  	pauseMutex sync.RWMutex
    47  
    48  	renderAppName string
    49  	duration      time.Duration
    50  }
    51  
    52  type GatewayPusher interface {
    53  	Push() error
    54  }
    55  type NoopGatewayPusher struct{}
    56  
    57  func (NoopGatewayPusher) Push() error {
    58  	return nil
    59  }
    60  
    61  func Cli(cfg *config.LoadGen) error {
    62  	r := rand.New(rand.NewSource(int64(cfg.RandSeed)))
    63  
    64  	var pusher GatewayPusher
    65  	if cfg.PushgatewayAddress == "" {
    66  		logrus.Debug("no pushgateway configured")
    67  		pusher = NoopGatewayPusher{}
    68  	} else {
    69  		logrus.Debug("will push metrics to ", cfg.PushgatewayAddress)
    70  		pusher = push.New(cfg.PushgatewayAddress, cfg.ServerAddress).Gatherer(prometheus.DefaultGatherer)
    71  	}
    72  
    73  	l := &LoadGen{
    74  		Config:    cfg,
    75  		Rand:      r,
    76  		SymbolBuf: make([]byte, cfg.ProfileSymbolLength),
    77  
    78  		runProgressMetric: promauto.NewGauge(prometheus.GaugeOpts{
    79  			Namespace: "pyroscope",
    80  			Subsystem: "benchmark",
    81  			Name:      "progress",
    82  			Help:      "",
    83  		}),
    84  		uploadErrors: promauto.NewCounter(prometheus.CounterOpts{
    85  			Namespace: "pyroscope",
    86  			Subsystem: "benchmark",
    87  			Name:      "upload_errors",
    88  			Help:      "",
    89  		}),
    90  		successfulUploads: promauto.NewCounter(prometheus.CounterOpts{
    91  			Namespace: "pyroscope",
    92  			Subsystem: "benchmark",
    93  			Name:      "successful_uploads",
    94  			Help:      "",
    95  		}),
    96  		pusher: pusher,
    97  	}
    98  
    99  	promauto.NewGaugeFunc(
   100  		prometheus.GaugeOpts{
   101  			Namespace: "pyroscope",
   102  			Subsystem: "benchmark",
   103  			Name:      "requests_total",
   104  			Help:      "",
   105  		},
   106  		func() float64 { return float64(cfg.Apps * cfg.Requests * cfg.Clients) },
   107  	)
   108  
   109  	if cfg.Duration != 0 {
   110  		cfg.Requests = int(cfg.Duration / defaultDurationBetweenUploads)
   111  	}
   112  
   113  	l.duration = time.Duration(l.Config.Requests) * (defaultDurationBetweenUploads)
   114  
   115  	return l.Run(cfg)
   116  }
   117  
   118  func (l *LoadGen) Run(cfg *config.LoadGen) error {
   119  	time.Sleep(5 * time.Second)
   120  	logrus.Info("running loadgenerator with config:")
   121  	spew.Dump(cfg)
   122  
   123  	logrus.Info("starting pull target server")
   124  	go server.StartServer()
   125  
   126  	logrus.Info("checking if server is available...")
   127  	err := waitUntilEndpointReady(cfg.ServerAddress)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	logrus.Info("generating fixtures")
   133  	fixtures := l.generateFixtures()
   134  
   135  	l.Tags = []string{}
   136  	for i := 0; i < l.Config.TagKeys; i++ {
   137  		tagName := fmt.Sprintf("key%d", i)
   138  		for j := 0; j < l.Config.TagValues; j++ {
   139  			tagValue := fmt.Sprintf("val%d", j)
   140  			l.Tags = append(l.Tags, fmt.Sprintf("{%s=%s}", tagName, tagValue))
   141  		}
   142  	}
   143  	logrus.Debug("done generating fixtures.")
   144  
   145  	logrus.Info("starting sending requests")
   146  	st := time.Now()
   147  	appNameBuf := make([]byte, 25)
   148  
   149  	servers := []string{
   150  		l.Config.ServerAddress,
   151  	}
   152  
   153  	if l.Config.ServerAddress2 != "" {
   154  		servers = append(servers, l.Config.ServerAddress2)
   155  	}
   156  
   157  	ctx, cancelFn := context.WithCancel(context.Background())
   158  
   159  	wg := sync.WaitGroup{}
   160  	wg.Add(l.Config.Apps * l.Config.Clients * len(servers))
   161  	for _, s := range servers {
   162  		for i := 0; i < l.Config.Apps; i++ {
   163  			// generate a random app name
   164  			l.Rand.Read(appNameBuf)
   165  			appName := hex.EncodeToString(appNameBuf)
   166  			if i == 0 {
   167  				l.renderAppName = appName
   168  			}
   169  			for j := 0; j < l.Config.Clients; j++ {
   170  				go l.startClientThread(ctx, s, j, appName, &wg, fixtures[i])
   171  			}
   172  		}
   173  	}
   174  	doneCh := make(chan struct{})
   175  	go l.startRenderThread(doneCh)
   176  	if cfg.TimeLimit != 0 {
   177  		go func() {
   178  			time.Sleep(cfg.TimeLimit)
   179  			cancelFn()
   180  		}()
   181  	}
   182  	wg.Wait()
   183  	logrus.Info("done sending requests, benchmark took ", time.Since(st))
   184  	doneCh <- struct{}{}
   185  
   186  	if l.Config.NoExitWhenDone {
   187  		logrus.Info("waiting forever")
   188  		time.Sleep(time.Hour * 24 * 365)
   189  	}
   190  
   191  	return nil
   192  }
   193  
   194  func (l *LoadGen) generateFixtures() Fixtures {
   195  	var f Fixtures
   196  
   197  	for i := 0; i < l.Config.Apps; i++ {
   198  		f = append(f, []*transporttrie.Trie{})
   199  
   200  		randomGen := rand.New(rand.NewSource(int64(l.Config.RandSeed + i)))
   201  		p := l.generateProfile(randomGen)
   202  		for j := 0; j < l.Config.Fixtures; j++ {
   203  			f[i] = append(f[i], p)
   204  		}
   205  	}
   206  
   207  	return f
   208  }
   209  
   210  //revive:disable:argument-limit it's benchmarking code, so lower standards are fine
   211  func (l *LoadGen) startClientThread(ctx context.Context, serverAddress string, threadID int, appName string, wg *sync.WaitGroup, appFixtures []*transporttrie.Trie) {
   212  	rc := remote.RemoteConfig{
   213  		UpstreamThreads:        1,
   214  		UpstreamAddress:        serverAddress,
   215  		UpstreamRequestTimeout: 10 * time.Second,
   216  	}
   217  
   218  	r, err := remote.New(rc, logrus.New())
   219  	if err != nil {
   220  		panic(err)
   221  	}
   222  	r.Start()
   223  
   224  	requestsCount := l.Config.Requests
   225  
   226  	threadStartTime := time.Now().Truncate(defaultDurationBetweenUploads)
   227  	threadStartTime = threadStartTime.Add(-1 * l.duration)
   228  
   229  	st := threadStartTime
   230  
   231  	var timerChan <-chan time.Time
   232  	if l.Config.RealTime {
   233  		timerChan = time.NewTicker(defaultDurationBetweenUploads / time.Duration(l.Config.TimeMultiplier)).C
   234  	} else {
   235  		ch := make(chan time.Time)
   236  		go func() {
   237  			for i := 0; i < requestsCount; i++ {
   238  				ch <- time.Now()
   239  			}
   240  			close(ch)
   241  		}()
   242  		timerChan = ch
   243  	}
   244  
   245  Outside:
   246  	for i := 0; i < requestsCount; i++ {
   247  		select {
   248  		case <-ctx.Done():
   249  			break Outside
   250  		case <-timerChan:
   251  		}
   252  
   253  		t := appFixtures[i%len(appFixtures)]
   254  
   255  		st = st.Add(10 * time.Second)
   256  		et := st.Add(10 * time.Second)
   257  
   258  		l.pauseMutex.RLock()
   259  		fullName := fmt.Sprintf("%s{pod=pod-%d}", appName, threadID)
   260  		err := r.UploadSync(&upstream.UploadJob{
   261  			Name:            fullName,
   262  			StartTime:       st,
   263  			EndTime:         et,
   264  			SpyName:         "gospy",
   265  			SampleRate:      100,
   266  			Units:           metadata.SamplesUnits,
   267  			AggregationType: metadata.SumAggregationType,
   268  			Trie:            t,
   269  		})
   270  		l.pauseMutex.RUnlock()
   271  		if err != nil {
   272  			l.uploadErrors.Add(1)
   273  			time.Sleep(time.Second)
   274  		} else {
   275  			l.successfulUploads.Add(1)
   276  		}
   277  
   278  		err = l.pusher.Push()
   279  		if err != nil {
   280  			logrus.Error(err)
   281  		}
   282  	}
   283  
   284  	wg.Done()
   285  }
   286  
   287  func (l *LoadGen) generateProfile(randomGen *rand.Rand) *transporttrie.Trie {
   288  	t := transporttrie.New()
   289  
   290  	for w := 0; w < l.Config.ProfileWidth; w++ {
   291  		symbol := []byte("root")
   292  		for d := 0; d < 2+l.Rand.Intn(l.Config.ProfileDepth); d++ {
   293  			randomGen.Read(l.SymbolBuf)
   294  			symbol = append(symbol, byte(';'))
   295  			symbol = append(symbol, []byte(hex.EncodeToString(l.SymbolBuf))...)
   296  			if l.Rand.Intn(100) <= 20 {
   297  				t.Insert(symbol, uint64(l.Rand.Intn(100)), true)
   298  			}
   299  		}
   300  
   301  		t.Insert(symbol, uint64(l.Rand.Intn(100)), true)
   302  	}
   303  	return t
   304  }
   305  
   306  // TODO(eh-am) exponential backoff and whatnot
   307  func waitUntilEndpointReady(url string) error {
   308  	client := http.Client{Timeout: 10 * time.Second}
   309  	retries := 0
   310  
   311  	for {
   312  		_, err := client.Get(url)
   313  
   314  		// all good?
   315  		if err == nil {
   316  			return nil
   317  		}
   318  		if retries >= MaxReadinessRetries {
   319  			break
   320  		}
   321  
   322  		time.Sleep(time.Second)
   323  		retries++
   324  	}
   325  
   326  	return fmt.Errorf("maximum retries exceeded ('%d') waiting for server ('%s') to respond", retries, url)
   327  }