get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/server/core_benchmarks_test.go (about)

     1  // Copyright 2023 The NATS Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package server
    15  
    16  import (
    17  	"crypto/tls"
    18  	"errors"
    19  	"fmt"
    20  	"math"
    21  	"math/rand"
    22  	"os"
    23  	"strconv"
    24  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"get.pme.sh/pnats/internal/fastrand"
    29  	"github.com/nats-io/nats.go"
    30  )
    31  
    32  func BenchmarkCoreRequestReply(b *testing.B) {
    33  	const (
    34  		subject = "test-subject"
    35  	)
    36  
    37  	messageSizes := []int64{
    38  		1024,   // 1kb
    39  		4096,   // 4kb
    40  		40960,  // 40kb
    41  		409600, // 400kb
    42  	}
    43  
    44  	for _, messageSize := range messageSizes {
    45  		b.Run(fmt.Sprintf("msgSz=%db", messageSize), func(b *testing.B) {
    46  
    47  			// Start server
    48  			serverOpts := DefaultOptions()
    49  			server := RunServer(serverOpts)
    50  			defer server.Shutdown()
    51  
    52  			clientUrl := server.ClientURL()
    53  
    54  			// Create "echo" subscriber
    55  			ncSub, err := nats.Connect(clientUrl)
    56  			if err != nil {
    57  				b.Fatal(err)
    58  			}
    59  			defer ncSub.Close()
    60  			sub, err := ncSub.Subscribe(subject, func(msg *nats.Msg) {
    61  				// Responder echoes the request payload as-is
    62  				msg.Respond(msg.Data)
    63  			})
    64  			defer sub.Unsubscribe()
    65  			if err != nil {
    66  				b.Fatal(err)
    67  			}
    68  
    69  			// Create publisher
    70  			ncPub, err := nats.Connect(clientUrl)
    71  			if err != nil {
    72  				b.Fatal(err)
    73  			}
    74  			defer ncPub.Close()
    75  
    76  			var errors = 0
    77  
    78  			// Create message
    79  			messageData := make([]byte, messageSize)
    80  			rand.New(rand.NewSource(12345)).Read(messageData)
    81  
    82  			b.SetBytes(messageSize)
    83  
    84  			// Benchmark
    85  			b.ResetTimer()
    86  			for i := 0; i < b.N; i++ {
    87  				fastRandomMutation(messageData, 10)
    88  
    89  				_, err := ncPub.Request(subject, messageData, time.Second)
    90  				if err != nil {
    91  					errors++
    92  				}
    93  			}
    94  			b.StopTimer()
    95  
    96  			b.ReportMetric(float64(errors), "errors")
    97  		})
    98  	}
    99  }
   100  
   101  func BenchmarkCoreTLSFanOut(b *testing.B) {
   102  	const (
   103  		subject            = "test-subject"
   104  		configsBasePath    = "./configs/tls"
   105  		maxPendingMessages = 25
   106  		maxPendingBytes    = 15 * 1024 * 1024 // 15MiB
   107  	)
   108  
   109  	keyTypeCases := []string{
   110  		"none",
   111  		"ed25519",
   112  		"rsa-1024",
   113  		"rsa-2048",
   114  		"rsa-4096",
   115  	}
   116  	messageSizeCases := []int64{
   117  		512 * 1024, // 512Kib
   118  	}
   119  	numSubsCases := []int{
   120  		5,
   121  	}
   122  
   123  	// Custom error handler that ignores ErrSlowConsumer.
   124  	// Lots of them are expected in this benchmark which indiscriminately publishes at a rate higher
   125  	// than what the server can relay to subscribers.
   126  	ignoreSlowConsumerErrorHandler := func(conn *nats.Conn, s *nats.Subscription, err error) {
   127  		if errors.Is(err, nats.ErrSlowConsumer) {
   128  			// Swallow this error
   129  		} else {
   130  			_, _ = fmt.Fprintf(os.Stderr, "Warning: %s\n", err)
   131  		}
   132  	}
   133  
   134  	for _, keyType := range keyTypeCases {
   135  
   136  		b.Run(
   137  			fmt.Sprintf("keyType=%s", keyType),
   138  			func(b *testing.B) {
   139  
   140  				for _, messageSize := range messageSizeCases {
   141  					b.Run(
   142  						fmt.Sprintf("msgSz=%db", messageSize),
   143  						func(b *testing.B) {
   144  
   145  							for _, numSubs := range numSubsCases {
   146  								b.Run(
   147  									fmt.Sprintf("subs=%d", numSubs),
   148  									func(b *testing.B) {
   149  										// Start server
   150  										configPath := fmt.Sprintf("%s/tls-%s.conf", configsBasePath, keyType)
   151  										server, _ := RunServerWithConfig(configPath)
   152  										defer server.Shutdown()
   153  
   154  										opts := []nats.Option{
   155  											nats.MaxReconnects(-1),
   156  											nats.ReconnectWait(0),
   157  											nats.ErrorHandler(ignoreSlowConsumerErrorHandler),
   158  										}
   159  
   160  										if keyType != "none" {
   161  											opts = append(opts, nats.Secure(&tls.Config{
   162  												InsecureSkipVerify: true,
   163  											}))
   164  										}
   165  
   166  										clientUrl := server.ClientURL()
   167  
   168  										// Count of messages received for by each subscriber
   169  										counters := make([]int, numSubs)
   170  
   171  										// Wait group for subscribers to signal they received b.N messages
   172  										var wg sync.WaitGroup
   173  										wg.Add(numSubs)
   174  
   175  										// Create subscribers
   176  										for i := 0; i < numSubs; i++ {
   177  											subIndex := i
   178  											ncSub, err := nats.Connect(clientUrl, opts...)
   179  											if err != nil {
   180  												b.Fatal(err)
   181  											}
   182  											defer ncSub.Close()
   183  											sub, err := ncSub.Subscribe(subject, func(msg *nats.Msg) {
   184  												counters[subIndex] += 1
   185  												if counters[subIndex] == b.N {
   186  													wg.Done()
   187  												}
   188  											})
   189  											if err != nil {
   190  												b.Fatalf("failed to subscribe: %s", err)
   191  											}
   192  											err = sub.SetPendingLimits(maxPendingMessages, maxPendingBytes)
   193  											if err != nil {
   194  												b.Fatalf("failed to set pending limits: %s", err)
   195  											}
   196  											defer sub.Unsubscribe()
   197  											if err != nil {
   198  												b.Fatal(err)
   199  											}
   200  										}
   201  
   202  										// publisher
   203  										ncPub, err := nats.Connect(clientUrl, opts...)
   204  										if err != nil {
   205  											b.Fatal(err)
   206  										}
   207  										defer ncPub.Close()
   208  
   209  										var errorCount = 0
   210  
   211  										// random bytes as payload
   212  										messageData := make([]byte, messageSize)
   213  										rand.New(rand.NewSource(12345)).Read(messageData)
   214  
   215  										quitCh := make(chan bool, 1)
   216  
   217  										publish := func() {
   218  											for {
   219  												select {
   220  												case <-quitCh:
   221  													return
   222  												default:
   223  													// continue publishing
   224  												}
   225  
   226  												fastRandomMutation(messageData, 10)
   227  												err := ncPub.Publish(subject, messageData)
   228  												if err != nil {
   229  													errorCount += 1
   230  												}
   231  											}
   232  										}
   233  
   234  										// Set bytes per operation
   235  										b.SetBytes(messageSize)
   236  										// Start the clock
   237  										b.ResetTimer()
   238  										// Start publishing as fast as the server allows
   239  										go publish()
   240  										// Wait for all subscribers to have delivered b.N messages
   241  										wg.Wait()
   242  										// Stop the clock
   243  										b.StopTimer()
   244  
   245  										// Stop publisher
   246  										quitCh <- true
   247  
   248  										b.ReportMetric(float64(errorCount), "errors")
   249  									},
   250  								)
   251  							}
   252  						},
   253  					)
   254  				}
   255  			},
   256  		)
   257  	}
   258  }
   259  
   260  func BenchmarkCoreFanOut(b *testing.B) {
   261  	const (
   262  		subject            = "test-subject"
   263  		maxPendingMessages = 25
   264  		maxPendingBytes    = 15 * 1024 * 1024 // 15MiB
   265  	)
   266  
   267  	messageSizeCases := []int64{
   268  		100,        // 100B
   269  		1024,       // 1KiB
   270  		10240,      // 10KiB
   271  		512 * 1024, // 512KiB
   272  	}
   273  	numSubsCases := []int{
   274  		3,
   275  		5,
   276  		10,
   277  	}
   278  
   279  	// Custom error handler that ignores ErrSlowConsumer.
   280  	// Lots of them are expected in this benchmark which indiscriminately publishes at a rate higher
   281  	// than what the server can relay to subscribers.
   282  	ignoreSlowConsumerErrorHandler := func(_ *nats.Conn, _ *nats.Subscription, err error) {
   283  		if errors.Is(err, nats.ErrSlowConsumer) {
   284  			// Swallow this error
   285  		} else {
   286  			_, _ = fmt.Fprintf(os.Stderr, "Warning: %s\n", err)
   287  		}
   288  	}
   289  
   290  	for _, messageSize := range messageSizeCases {
   291  		b.Run(
   292  			fmt.Sprintf("msgSz=%db", messageSize),
   293  			func(b *testing.B) {
   294  				for _, numSubs := range numSubsCases {
   295  					b.Run(
   296  						fmt.Sprintf("subs=%d", numSubs),
   297  						func(b *testing.B) {
   298  							// Start server
   299  							defaultOpts := DefaultOptions()
   300  							server := RunServer(defaultOpts)
   301  							defer server.Shutdown()
   302  
   303  							opts := []nats.Option{
   304  								nats.MaxReconnects(-1),
   305  								nats.ReconnectWait(0),
   306  								nats.ErrorHandler(ignoreSlowConsumerErrorHandler),
   307  							}
   308  
   309  							clientUrl := server.ClientURL()
   310  
   311  							// Count of messages received for by each subscriber
   312  							counters := make([]int, numSubs)
   313  
   314  							// Wait group for subscribers to signal they received b.N messages
   315  							var wg sync.WaitGroup
   316  							wg.Add(numSubs)
   317  
   318  							// Create subscribers
   319  							for i := 0; i < numSubs; i++ {
   320  								subIndex := i
   321  								ncSub, err := nats.Connect(clientUrl, opts...)
   322  								if err != nil {
   323  									b.Fatal(err)
   324  								}
   325  								defer ncSub.Close()
   326  								sub, err := ncSub.Subscribe(subject, func(_ *nats.Msg) {
   327  									counters[subIndex] += 1
   328  									if counters[subIndex] == b.N {
   329  										wg.Done()
   330  									}
   331  								})
   332  								if err != nil {
   333  									b.Fatalf("failed to subscribe: %s", err)
   334  								}
   335  								err = sub.SetPendingLimits(maxPendingMessages, maxPendingBytes)
   336  								if err != nil {
   337  									b.Fatalf("failed to set pending limits: %s", err)
   338  								}
   339  								defer sub.Unsubscribe()
   340  							}
   341  
   342  							// publisher
   343  							ncPub, err := nats.Connect(clientUrl, opts...)
   344  							if err != nil {
   345  								b.Fatal(err)
   346  							}
   347  							defer ncPub.Close()
   348  
   349  							var errorCount = 0
   350  
   351  							// random bytes as payload
   352  							messageData := make([]byte, messageSize)
   353  							rand.New(rand.NewSource(123456)).Read(messageData)
   354  
   355  							quitCh := make(chan bool, 1)
   356  
   357  							publish := func() {
   358  								for {
   359  									select {
   360  									case <-quitCh:
   361  										return
   362  									default:
   363  										// continue publishing
   364  									}
   365  
   366  									fastRandomMutation(messageData, 10)
   367  									err := ncPub.Publish(subject, messageData)
   368  									if err != nil {
   369  										errorCount += 1
   370  									}
   371  								}
   372  							}
   373  
   374  							// Set bytes per operation
   375  							b.SetBytes(messageSize)
   376  							// Start the clock
   377  							b.ResetTimer()
   378  							// Start publishing as fast as the server allows
   379  							go publish()
   380  							// Wait for all subscribers to have delivered b.N messages
   381  							wg.Wait()
   382  							// Stop the clock
   383  							b.StopTimer()
   384  
   385  							// Stop publisher
   386  							quitCh <- true
   387  
   388  							b.ReportMetric(100*float64(errorCount)/float64(b.N), "%error")
   389  						},
   390  					)
   391  				}
   392  			},
   393  		)
   394  	}
   395  }
   396  
   397  func BenchmarkCoreFanIn(b *testing.B) {
   398  
   399  	type BenchPublisher struct {
   400  		// nats connection for this publisher
   401  		conn *nats.Conn
   402  		// number of publishing errors encountered
   403  		publishErrors int
   404  		// number of messages published
   405  		publishCounter int
   406  		// quit channel which will terminate publishing
   407  		quitCh chan bool
   408  		// message data buffer
   409  		messageData []byte
   410  	}
   411  
   412  	const subjectBaseName = "test-subject"
   413  
   414  	messageSizeCases := []int64{
   415  		100,        // 100B
   416  		1024,       // 1KiB
   417  		10240,      // 10KiB
   418  		512 * 1024, // 512KiB
   419  	}
   420  	numPubsCases := []int{
   421  		3,
   422  		5,
   423  		10,
   424  	}
   425  
   426  	// Custom error handler that ignores ErrSlowConsumer.
   427  	// Lots of them are expected in this benchmark which indiscriminately publishes at a rate higher
   428  	// than what the server can relay to subscribers.
   429  	ignoreSlowConsumerErrorHandler := func(_ *nats.Conn, _ *nats.Subscription, err error) {
   430  		if errors.Is(err, nats.ErrSlowConsumer) {
   431  			// Swallow this error
   432  		} else {
   433  			_, _ = fmt.Fprintf(os.Stderr, "Warning: %s\n", err)
   434  		}
   435  	}
   436  
   437  	workload := func(b *testing.B, clientUrl string, numPubs int, messageSize int64) {
   438  
   439  		// connection options
   440  		opts := []nats.Option{
   441  			nats.MaxReconnects(-1),
   442  			nats.ReconnectWait(0),
   443  			nats.ErrorHandler(ignoreSlowConsumerErrorHandler),
   444  		}
   445  
   446  		// waits for all publishers sub-routines and for main thread to be ready
   447  		var publishersReadyWg sync.WaitGroup
   448  		publishersReadyWg.Add(numPubs + 1)
   449  
   450  		// wait group to ensure all publishers have been torn down
   451  		var finishedPublishersWg sync.WaitGroup
   452  		finishedPublishersWg.Add(numPubs)
   453  
   454  		publishers := make([]BenchPublisher, numPubs)
   455  		// create N publishers
   456  		for i := range publishers {
   457  			// create publisher connection
   458  			ncPub, err := nats.Connect(clientUrl, opts...)
   459  			if err != nil {
   460  				b.Fatal(err)
   461  			}
   462  			defer ncPub.Close()
   463  
   464  			// create bench publisher object
   465  			publisher := BenchPublisher{
   466  				conn:           ncPub,
   467  				publishErrors:  0,
   468  				publishCounter: 0,
   469  				quitCh:         make(chan bool, 1),
   470  				messageData:    make([]byte, messageSize),
   471  			}
   472  			rand.New(rand.NewSource(int64(i))).Read(publisher.messageData)
   473  			publishers[i] = publisher
   474  		}
   475  
   476  		// total number of publishers that have published b.N to the subscriber successfully
   477  		completedPublishersCount := 0
   478  
   479  		// wait group blocks main thread until publish workload is completed, it is decremented after subscriber receives b.N messages from all publishers
   480  		var benchCompleteWg sync.WaitGroup
   481  		benchCompleteWg.Add(1)
   482  
   483  		// start subscriber
   484  		ncSub, err := nats.Connect(clientUrl, opts...)
   485  		if err != nil {
   486  			b.Fatal(err)
   487  		}
   488  		defer ncSub.Close()
   489  
   490  		// subscriber
   491  		ncSub.Subscribe(fmt.Sprintf("%s.*", subjectBaseName), func(msg *nats.Msg) {
   492  			// get the publisher id from subject
   493  			pubIdx, err := strconv.Atoi(msg.Subject[len(subjectBaseName)+1:])
   494  			if err != nil {
   495  				b.Fatal(err)
   496  			}
   497  
   498  			// message successfully received from publisher
   499  			publishers[pubIdx].publishCounter += 1
   500  
   501  			// subscriber has received a total of b.N messages from this publisher
   502  			if publishers[pubIdx].publishCounter == b.N {
   503  				completedPublishersCount++
   504  				// every publisher has successfully sent b.N messages to subscriber
   505  				if completedPublishersCount == numPubs {
   506  					benchCompleteWg.Done()
   507  				}
   508  			}
   509  		})
   510  
   511  		// start publisher sub-routines
   512  		for i := range publishers {
   513  			go func(pubId int) {
   514  
   515  				// publisher sub-routine initialized
   516  				publishersReadyWg.Done()
   517  
   518  				publisher := publishers[pubId]
   519  				subject := fmt.Sprintf("%s.%d", subjectBaseName, pubId)
   520  
   521  				// signal that this publisher has been torn down
   522  				defer finishedPublishersWg.Done()
   523  
   524  				// wait till all other publishers are ready to start workload
   525  				publishersReadyWg.Wait()
   526  
   527  				// publish until quitCh is closed
   528  				for {
   529  					select {
   530  					case <-publisher.quitCh:
   531  						return
   532  					default:
   533  						// continue publishing
   534  					}
   535  					fastRandomMutation(publisher.messageData, 10)
   536  					err := publisher.conn.Publish(subject, publisher.messageData)
   537  					if err != nil {
   538  						publisher.publishErrors += 1
   539  					}
   540  				}
   541  			}(i)
   542  		}
   543  
   544  		// set bytes per operation
   545  		b.SetBytes(messageSize)
   546  		// main thread is ready
   547  		publishersReadyWg.Done()
   548  		// wait till publishers are ready
   549  		publishersReadyWg.Wait()
   550  
   551  		// start the clock
   552  		b.ResetTimer()
   553  		// wait till termination cond reached
   554  		benchCompleteWg.Wait()
   555  		// stop the clock
   556  		b.StopTimer()
   557  
   558  		// send quit signal to all publishers
   559  		for i := range publishers {
   560  			publishers[i].quitCh <- true
   561  		}
   562  		// wait for all publishers to shutdown
   563  		finishedPublishersWg.Wait()
   564  
   565  		// sum errors from all publishers
   566  		totalErrors := 0
   567  		for _, publisher := range publishers {
   568  			totalErrors += publisher.publishErrors
   569  		}
   570  		// sum total messages sent from all publishers
   571  		totalMessages := 0
   572  		for _, publisher := range publishers {
   573  			totalMessages += publisher.publishCounter
   574  		}
   575  		errorRate := 100 * float64(totalErrors) / float64(totalMessages)
   576  
   577  		// report error rate
   578  		b.ReportMetric(errorRate, "%error")
   579  
   580  	}
   581  
   582  	// benchmark case matrix
   583  	for _, messageSize := range messageSizeCases {
   584  		b.Run(
   585  			fmt.Sprintf("msgSz=%db", messageSize),
   586  			func(b *testing.B) {
   587  				for _, numPubs := range numPubsCases {
   588  					b.Run(
   589  						fmt.Sprintf("pubs=%d", numPubs),
   590  						func(b *testing.B) {
   591  							// start server
   592  							defaultOpts := DefaultOptions()
   593  							server := RunServer(defaultOpts)
   594  							defer server.Shutdown()
   595  
   596  							// get connection string
   597  							clientUrl := server.ClientURL()
   598  
   599  							// run fan-in workload
   600  							workload(b, clientUrl, numPubs, messageSize)
   601  						})
   602  				}
   603  			})
   604  	}
   605  }
   606  
   607  // fastRandomMutation performs a minor in-place mutation to the given buffer.
   608  // This is useful in benchmark to avoid sending the same payload every time (which could result in some optimizations
   609  // we do not want to measure), while not slowing down the benchmark with a full payload generated for each operation.
   610  func fastRandomMutation(data []byte, mutations int) {
   611  	for i := 0; i < mutations; i++ {
   612  		data[fastrand.Uint32n(uint32(len(data)))] = byte(fastrand.Uint32() % math.MaxUint8)
   613  	}
   614  }