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 }