vitess.io/vitess@v0.16.2/go/vt/vtexplain/vtexplain.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 vtexplain analyzes a set of sql statements and returns the
    18  // corresponding vtgate and vttablet query plans that will be executed
    19  // on the given statements
    20  package vtexplain
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"sort"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/spf13/pflag"
    30  
    31  	"vitess.io/vitess/go/vt/discovery"
    32  	"vitess.io/vitess/go/vt/servenv"
    33  	"vitess.io/vitess/go/vt/vtgate"
    34  
    35  	"vitess.io/vitess/go/jsonutil"
    36  	"vitess.io/vitess/go/sync2"
    37  	"vitess.io/vitess/go/vt/log"
    38  	"vitess.io/vitess/go/vt/sqlparser"
    39  	"vitess.io/vitess/go/vt/vtgate/engine"
    40  
    41  	querypb "vitess.io/vitess/go/vt/proto/query"
    42  	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
    43  )
    44  
    45  var (
    46  	batchInterval = 10 * time.Millisecond
    47  )
    48  
    49  func init() {
    50  	servenv.OnParseFor("vtexplain", func(fs *pflag.FlagSet) {
    51  		fs.DurationVar(&batchInterval, "batch-interval", 10*time.Millisecond, "Interval between logical time slots.")
    52  	})
    53  }
    54  
    55  const (
    56  	vtexplainCell = "explainCell"
    57  
    58  	// ModeMulti is the default mode with autocommit implemented at vtgate
    59  	ModeMulti = "multi"
    60  
    61  	// ModeTwoPC enables the twopc feature
    62  	ModeTwoPC = "twopc"
    63  )
    64  
    65  type (
    66  	// ExecutorMode controls the mode of operation for the vtexplain simulator
    67  	ExecutorMode string
    68  
    69  	// Options to control the explain process
    70  	Options struct {
    71  		// NumShards indicates the number of shards in the topology
    72  		NumShards int
    73  
    74  		// PlannerVersion indicates whether or not we should use the Gen4 planner
    75  		PlannerVersion querypb.ExecuteOptions_PlannerVersion
    76  
    77  		// ReplicationMode must be set to either "ROW" or "STATEMENT" before
    78  		// initialization
    79  		ReplicationMode string
    80  
    81  		// Normalize controls whether or not vtgate does query normalization
    82  		Normalize bool
    83  
    84  		// ExecutionMode must be set to one of the modes above
    85  		ExecutionMode string
    86  
    87  		// StrictDDL is used in unit tests only to verify that the schema
    88  		// is parsed properly.
    89  		StrictDDL bool
    90  
    91  		// Target is used to override the "database" target in the
    92  		// vtgate session to simulate `USE <target>`
    93  		Target string
    94  	}
    95  
    96  	// TabletQuery defines a query that was sent to a given tablet and how it was
    97  	// processed in mysql
    98  	TabletQuery struct {
    99  		// Logical time of the query
   100  		Time int
   101  
   102  		// SQL command sent to the given tablet
   103  		SQL string
   104  
   105  		// BindVars sent with the command
   106  		BindVars map[string]*querypb.BindVariable
   107  	}
   108  
   109  	// MysqlQuery defines a query that was sent to a given tablet and how it was
   110  	// processed in mysql
   111  	MysqlQuery struct {
   112  		// Sequence number of the query
   113  		Time int
   114  
   115  		// SQL command sent to the given tablet
   116  		SQL string
   117  	}
   118  
   119  	// Explain defines how vitess will execute a given sql query, including the vtgate
   120  	// query plans and all queries run on each tablet.
   121  	Explain struct {
   122  		// original sql statement
   123  		SQL string
   124  
   125  		// the vtgate plan(s)
   126  		Plans []*engine.Plan
   127  
   128  		// list of queries / bind vars sent to each tablet
   129  		TabletActions map[string]*TabletActions
   130  	}
   131  
   132  	outputQuery struct {
   133  		tablet string
   134  		Time   int
   135  		sql    string
   136  	}
   137  
   138  	VTExplain struct {
   139  		explainTopo    *ExplainTopo
   140  		vtgateExecutor *vtgate.Executor
   141  		healthCheck    *discovery.FakeHealthCheck
   142  		vtgateSession  *vtgatepb.Session
   143  		spMap          map[string]string
   144  		spCount        int
   145  
   146  		// time simulator
   147  		batchTime       *sync2.Batcher
   148  		globalTabletEnv *tabletEnv
   149  	}
   150  )
   151  
   152  // MarshalJSON renders the json structure
   153  func (tq *TabletQuery) MarshalJSON() ([]byte, error) {
   154  	// Convert Bindvars to strings for nicer output
   155  	bindVars := make(map[string]string)
   156  	for k, v := range tq.BindVars {
   157  		var b strings.Builder
   158  		sqlparser.EncodeValue(&b, v)
   159  		bindVars[k] = b.String()
   160  	}
   161  
   162  	return jsonutil.MarshalNoEscape(&struct {
   163  		Time     int
   164  		SQL      string
   165  		BindVars map[string]string
   166  	}{
   167  		Time:     tq.Time,
   168  		SQL:      tq.SQL,
   169  		BindVars: bindVars,
   170  	})
   171  }
   172  
   173  // TabletActions contains the set of operations done by a given tablet
   174  type TabletActions struct {
   175  	// Queries sent from vtgate to the tablet
   176  	TabletQueries []*TabletQuery
   177  
   178  	// Queries that were run on mysql
   179  	MysqlQueries []*MysqlQuery
   180  }
   181  
   182  // Init sets up the fake execution environment
   183  func Init(vSchemaStr, sqlSchema, ksShardMapStr string, opts *Options) (*VTExplain, error) {
   184  	// Verify options
   185  	if opts.ReplicationMode != "ROW" && opts.ReplicationMode != "STATEMENT" {
   186  		return nil, fmt.Errorf("invalid replication mode \"%s\"", opts.ReplicationMode)
   187  	}
   188  
   189  	parsedDDLs, err := parseSchema(sqlSchema, opts)
   190  	if err != nil {
   191  		return nil, fmt.Errorf("parseSchema: %v", err)
   192  	}
   193  
   194  	tabletEnv, err := newTabletEnvironment(parsedDDLs, opts)
   195  	if err != nil {
   196  		return nil, fmt.Errorf("initTabletEnvironment: %v", err)
   197  	}
   198  	vte := &VTExplain{vtgateSession: &vtgatepb.Session{
   199  		TargetString: "",
   200  		Autocommit:   true,
   201  	}}
   202  	vte.setGlobalTabletEnv(tabletEnv)
   203  	err = vte.initVtgateExecutor(vSchemaStr, ksShardMapStr, opts)
   204  	if err != nil {
   205  		return nil, fmt.Errorf("initVtgateExecutor: %v", err.Error())
   206  	}
   207  
   208  	return vte, nil
   209  }
   210  
   211  // Stop and cleans up fake execution environment
   212  func (vte *VTExplain) Stop() {
   213  	// Cleanup all created fake dbs.
   214  	if vte.explainTopo != nil {
   215  		for _, conn := range vte.explainTopo.TabletConns {
   216  			conn.tsv.StopService()
   217  		}
   218  		for _, conn := range vte.explainTopo.TabletConns {
   219  			conn.db.Close()
   220  		}
   221  	}
   222  }
   223  
   224  func parseSchema(sqlSchema string, opts *Options) ([]sqlparser.DDLStatement, error) {
   225  	parsedDDLs := make([]sqlparser.DDLStatement, 0, 16)
   226  	for {
   227  		sql, rem, err := sqlparser.SplitStatement(sqlSchema)
   228  		sqlSchema = rem
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  		if sql == "" {
   233  			break
   234  		}
   235  		sql, _ = sqlparser.SplitMarginComments(sql)
   236  		if sql == "" {
   237  			continue
   238  		}
   239  
   240  		var stmt sqlparser.Statement
   241  		if opts.StrictDDL {
   242  			stmt, err = sqlparser.ParseStrictDDL(sql)
   243  			if err != nil {
   244  				return nil, err
   245  			}
   246  		} else {
   247  			stmt, err = sqlparser.Parse(sql)
   248  			if err != nil {
   249  				log.Errorf("ERROR: failed to parse sql: %s, got error: %v", sql, err)
   250  				continue
   251  			}
   252  		}
   253  		ddl, ok := stmt.(sqlparser.DDLStatement)
   254  		if !ok {
   255  			log.Infof("ignoring non-DDL statement: %s", sql)
   256  			continue
   257  		}
   258  		if ddl.GetAction() != sqlparser.CreateDDLAction {
   259  			log.Infof("ignoring %s table statement", ddl.GetAction().ToString())
   260  			continue
   261  		}
   262  		if ddl.GetTableSpec() == nil && ddl.GetOptLike() == nil {
   263  			log.Errorf("invalid create table statement: %s", sql)
   264  			continue
   265  		}
   266  		parsedDDLs = append(parsedDDLs, ddl)
   267  	}
   268  	return parsedDDLs, nil
   269  }
   270  
   271  // Run the explain analysis on the given queries
   272  func (vte *VTExplain) Run(sql string) ([]*Explain, error) {
   273  	explains := make([]*Explain, 0, 16)
   274  
   275  	var (
   276  		rem string
   277  		err error
   278  	)
   279  
   280  	for {
   281  		// Need to strip comments in a loop to handle multiple comments
   282  		// in a row.
   283  		for {
   284  			s := sqlparser.StripLeadingComments(sql)
   285  			if s == sql {
   286  				break
   287  			}
   288  			sql = s
   289  		}
   290  
   291  		sql, rem, err = sqlparser.SplitStatement(sql)
   292  		if err != nil {
   293  			return nil, err
   294  		}
   295  
   296  		if sql != "" {
   297  			// Reset the global time simulator unless there's an open transaction
   298  			// in the session from the previous statement.
   299  			if vte.vtgateSession == nil || !vte.vtgateSession.GetInTransaction() {
   300  				vte.batchTime = sync2.NewBatcher(batchInterval)
   301  			}
   302  			log.V(100).Infof("explain %s", sql)
   303  			e, err := vte.explain(sql)
   304  			if err != nil {
   305  				return nil, err
   306  			}
   307  			explains = append(explains, e)
   308  		}
   309  
   310  		sql = rem
   311  		if sql == "" {
   312  			break
   313  		}
   314  	}
   315  
   316  	return explains, nil
   317  }
   318  
   319  func (vte *VTExplain) explain(sql string) (*Explain, error) {
   320  	plans, tabletActions, err := vte.vtgateExecute(sql)
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	return &Explain{
   326  		SQL:           sql,
   327  		Plans:         plans,
   328  		TabletActions: tabletActions,
   329  	}, nil
   330  }
   331  
   332  // ExplainsAsText returns a text representation of the explains in logical time
   333  // order
   334  func (vte *VTExplain) ExplainsAsText(explains []*Explain) (string, error) {
   335  	var b bytes.Buffer
   336  	for _, explain := range explains {
   337  		fmt.Fprintf(&b, "----------------------------------------------------------------------\n")
   338  		fmt.Fprintf(&b, "%s\n\n", explain.SQL)
   339  
   340  		queries := make([]outputQuery, 0, 4)
   341  		for tablet, actions := range explain.TabletActions {
   342  			for _, q := range actions.MysqlQueries {
   343  				// change savepoint for printing out, that are internal to vitess.
   344  				err := vte.specialHandlingOfSavepoints(q)
   345  				if err != nil {
   346  					return "", err
   347  				}
   348  				queries = append(queries, outputQuery{
   349  					tablet: tablet,
   350  					Time:   q.Time,
   351  					sql:    q.SQL,
   352  				})
   353  			}
   354  		}
   355  
   356  		// Make sure to sort first by the batch time and then by the
   357  		// shard to avoid flakiness in the tests for parallel queries
   358  		sort.SliceStable(queries, func(i, j int) bool {
   359  			if queries[i].Time == queries[j].Time {
   360  				return queries[i].tablet < queries[j].tablet
   361  			}
   362  			return queries[i].Time < queries[j].Time
   363  		})
   364  
   365  		for _, q := range queries {
   366  			fmt.Fprintf(&b, "%d %s: %s\n", q.Time, q.tablet, q.sql)
   367  		}
   368  		fmt.Fprintf(&b, "\n")
   369  	}
   370  	fmt.Fprintf(&b, "----------------------------------------------------------------------\n")
   371  	return b.String(), nil
   372  }
   373  
   374  func (vte *VTExplain) specialHandlingOfSavepoints(q *MysqlQuery) error {
   375  	if !strings.HasPrefix(q.SQL, "savepoint") {
   376  		return nil
   377  	}
   378  
   379  	stmt, err := sqlparser.Parse(q.SQL)
   380  	if err != nil {
   381  		return err
   382  	}
   383  
   384  	sp, ok := stmt.(*sqlparser.Savepoint)
   385  	if !ok {
   386  		return fmt.Errorf("savepoint expected, got: %s", q.SQL)
   387  	}
   388  	if !strings.Contains(sp.Name.String(), "_vt") {
   389  		return nil
   390  	}
   391  
   392  	if vte.spMap == nil {
   393  		vte.spMap = map[string]string{}
   394  	}
   395  	spName := vte.spMap[sp.Name.String()]
   396  	if spName == "" {
   397  		spName = fmt.Sprintf("x%d", vte.spCount+1)
   398  		vte.spMap[sp.Name.String()] = spName
   399  		vte.spCount++
   400  	}
   401  	sp.Name = sqlparser.NewIdentifierCI(spName)
   402  	q.SQL = sqlparser.String(sp)
   403  
   404  	return nil
   405  }
   406  
   407  // ExplainsAsJSON returns a json representation of the explains
   408  func ExplainsAsJSON(explains []*Explain) string {
   409  	explainJSON, _ := jsonutil.MarshalIndentNoEscape(explains, "", "    ")
   410  	return string(explainJSON)
   411  }