vitess.io/vitess@v0.16.2/go/vtbench/vtbench.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vtbench
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	"vitess.io/vitess/go/stats"
    27  	"vitess.io/vitess/go/vt/log"
    28  
    29  	querypb "vitess.io/vitess/go/vt/proto/query"
    30  )
    31  
    32  // ClientProtocol indicates how to connect
    33  type ClientProtocol int
    34  
    35  const (
    36  	// MySQL uses the mysql wire protocol
    37  	MySQL ClientProtocol = iota
    38  
    39  	// GRPCVtgate uses the grpc wire protocol to vttablet
    40  	GRPCVtgate
    41  
    42  	// GRPCVttablet uses the grpc wire protocol to vttablet
    43  	GRPCVttablet
    44  )
    45  
    46  // ProtocolString returns a string representation of the protocol
    47  func (cp ClientProtocol) String() string {
    48  	switch cp {
    49  	case MySQL:
    50  		return "mysql"
    51  	case GRPCVtgate:
    52  		return "grpc-vtgate"
    53  	case GRPCVttablet:
    54  		return "grpc-vttablet"
    55  	default:
    56  		return fmt.Sprintf("unknown-protocol-%d", cp)
    57  	}
    58  }
    59  
    60  // ConnParams specifies how to connect to the vtgate(s)
    61  type ConnParams struct {
    62  	Hosts      []string
    63  	Port       int
    64  	DB         string
    65  	Username   string
    66  	Password   string
    67  	UnixSocket string
    68  	Protocol   ClientProtocol
    69  }
    70  
    71  // Bench controls the test
    72  type Bench struct {
    73  	ConnParams ConnParams
    74  	Threads    int
    75  	Count      int
    76  	Query      string
    77  
    78  	threads []benchThread
    79  
    80  	// synchronization waitgroup for startup / completion
    81  	wg sync.WaitGroup
    82  
    83  	// starting gate used to block all threads on startup
    84  	lock sync.RWMutex
    85  
    86  	Rows    *stats.Counter
    87  	Bytes   *stats.Counter
    88  	Timings *stats.Timings
    89  
    90  	TotalTime time.Duration
    91  }
    92  
    93  type benchThread struct {
    94  	b        *Bench
    95  	i        int
    96  	conn     clientConn
    97  	query    string
    98  	bindVars map[string]*querypb.BindVariable
    99  }
   100  
   101  // NewBench creates a new bench test
   102  func NewBench(threads, count int, cp ConnParams, query string) *Bench {
   103  	bench := Bench{
   104  		Threads:    threads,
   105  		Count:      count,
   106  		ConnParams: cp,
   107  		Query:      query,
   108  		Rows:       stats.NewCounter("", ""),
   109  		Timings:    stats.NewTimings("", "", ""),
   110  	}
   111  	return &bench
   112  }
   113  
   114  // Run executes the test
   115  func (b *Bench) Run(ctx context.Context) error {
   116  	err := b.createConns(ctx)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	b.createThreads(ctx)
   122  	if err := b.runTest(ctx); err != nil {
   123  		return err
   124  	}
   125  	return nil
   126  }
   127  
   128  func (b *Bench) createConns(ctx context.Context) error {
   129  	log.V(10).Infof("creating %d client connections...", b.Threads)
   130  	start := time.Now()
   131  	reportInterval := 2 * time.Second
   132  	report := start.Add(reportInterval)
   133  	for i := 0; i < b.Threads; i++ {
   134  		host := b.ConnParams.Hosts[i%len(b.ConnParams.Hosts)]
   135  		cp := b.ConnParams
   136  		cp.Hosts = []string{host}
   137  
   138  		var conn clientConn
   139  		var err error
   140  
   141  		switch b.ConnParams.Protocol {
   142  		case MySQL:
   143  			log.V(5).Infof("connecting to %s using mysql protocol...", host)
   144  			conn = &mysqlClientConn{}
   145  			err = conn.connect(ctx, cp)
   146  		case GRPCVtgate:
   147  			log.V(5).Infof("connecting to %s using grpc vtgate protocol...", host)
   148  			conn = &grpcVtgateConn{}
   149  			err = conn.connect(ctx, cp)
   150  		case GRPCVttablet:
   151  			log.V(5).Infof("connecting to %s using grpc vttablet protocol...", host)
   152  			conn = &grpcVttabletConn{}
   153  			err = conn.connect(ctx, cp)
   154  		default:
   155  			return fmt.Errorf("unimplemented connection protocol %s", b.ConnParams.Protocol.String())
   156  		}
   157  
   158  		if err != nil {
   159  			return fmt.Errorf("error connecting to %s using %v protocol: %v", host, cp.Protocol.String(), err)
   160  		}
   161  
   162  		// XXX handle normalization and per-thread query templating
   163  		query, bindVars := b.getQuery(i)
   164  		b.threads = append(b.threads, benchThread{
   165  			b:        b,
   166  			i:        i,
   167  			conn:     conn,
   168  			query:    query,
   169  			bindVars: bindVars,
   170  		})
   171  
   172  		if time.Now().After(report) {
   173  			fmt.Printf("Created %d/%d connections after %v\n", i, b.Threads, time.Since(start))
   174  			report = time.Now().Add(reportInterval)
   175  		}
   176  	}
   177  
   178  	return nil
   179  }
   180  
   181  func (b *Bench) getQuery(i int) (string, map[string]*querypb.BindVariable) {
   182  	query := strings.Replace(b.Query, ":thread", fmt.Sprintf("%d", i), -1)
   183  	bindVars := make(map[string]*querypb.BindVariable)
   184  	return query, bindVars
   185  }
   186  
   187  func (b *Bench) createThreads(ctx context.Context) {
   188  	// Create a barrier so all the threads start at the same time
   189  	b.lock.Lock()
   190  
   191  	log.V(10).Infof("starting %d threads", b.Threads)
   192  	for i := 0; i < b.Threads; i++ {
   193  		b.wg.Add(1)
   194  		go b.threads[i].clientLoop(ctx)
   195  	}
   196  
   197  	log.V(10).Infof("waiting for %d threads to start", b.Threads)
   198  	b.wg.Wait()
   199  
   200  	b.wg.Add(b.Threads)
   201  }
   202  
   203  func (b *Bench) runTest(ctx context.Context) error {
   204  	start := time.Now()
   205  	fmt.Printf("Starting test threads\n")
   206  	b.lock.Unlock()
   207  
   208  	// Then wait for them all to finish looping
   209  	log.V(10).Infof("waiting for %d threads to finish", b.Threads)
   210  	b.wg.Wait()
   211  	b.TotalTime = time.Since(start)
   212  
   213  	return nil
   214  }
   215  
   216  func (bt *benchThread) clientLoop(ctx context.Context) {
   217  	b := bt.b
   218  
   219  	// Declare that startup is finished and wait for
   220  	// the barrier
   221  	b.wg.Done()
   222  	log.V(10).Infof("thread %d waiting for startup barrier", bt.i)
   223  	b.lock.RLock()
   224  	log.V(10).Infof("thread %d starting loop", bt.i)
   225  
   226  	for i := 0; i < b.Count; i++ {
   227  		start := time.Now()
   228  		result, err := bt.conn.execute(ctx, bt.query, bt.bindVars)
   229  		b.Timings.Record("query", start)
   230  		if err != nil {
   231  			log.Errorf("query error: %v", err)
   232  			break
   233  		} else {
   234  			b.Rows.Add(int64(len(result.Rows)))
   235  		}
   236  
   237  	}
   238  
   239  	b.wg.Done()
   240  }