diff --git a/go/cmd/vtexplain/vtexplain.go b/go/cmd/vtexplain/vtexplain.go index 3d1edb16519..88fd6bf15df 100644 --- a/go/cmd/vtexplain/vtexplain.go +++ b/go/cmd/vtexplain/vtexplain.go @@ -29,18 +29,20 @@ import ( ) var ( - sqlFlag = flag.String("sql", "", "A list of semicolon-delimited SQL commands to analyze") - sqlFileFlag = flag.String("sql-file", "", "Identifies the file that contains the SQL commands to analyze") - schemaFlag = flag.String("schema", "", "The SQL table schema") - schemaFileFlag = flag.String("schema-file", "", "Identifies the file that contains the SQL table schema") - vschemaFlag = flag.String("vschema", "", "Identifies the VTGate routing schema") - vschemaFileFlag = flag.String("vschema-file", "", "Identifies the VTGate routing schema file") - numShards = flag.Int("shards", 2, "Number of shards per keyspace") - executionMode = flag.String("execution-mode", "multi", "The execution mode to simulate -- must be set to multi, legacy-autocommit, or twopc") - replicationMode = flag.String("replication-mode", "ROW", "The replication mode to simulate -- must be set to either ROW or STATEMENT") - normalize = flag.Bool("normalize", false, "Whether to enable vtgate normalization") - outputMode = flag.String("output-mode", "text", "Output in human-friendly text or json") - dbName = flag.String("dbname", "", "Optional database target to override normal routing") + sqlFlag = flag.String("sql", "", "A list of semicolon-delimited SQL commands to analyze") + sqlFileFlag = flag.String("sql-file", "", "Identifies the file that contains the SQL commands to analyze") + schemaFlag = flag.String("schema", "", "The SQL table schema") + schemaFileFlag = flag.String("schema-file", "", "Identifies the file that contains the SQL table schema") + vschemaFlag = flag.String("vschema", "", "Identifies the VTGate routing schema") + vschemaFileFlag = flag.String("vschema-file", "", "Identifies the VTGate routing schema file") + ksShardMapFlag = flag.String("ks-shard-map", "", "JSON map of keyspace name -> shard name -> ShardReference object. The inner map is the same as the output of FindAllShardsInKeyspace") + ksShardMapFileFlag = flag.String("ks-shard-map-file", "", "File containing json blob of keyspace name -> shard name -> ShardReference object") + numShards = flag.Int("shards", 2, "Number of shards per keyspace. Passing -ks-shard-map/-ks-shard-map-file causes this flag to be ignored.") + executionMode = flag.String("execution-mode", "multi", "The execution mode to simulate -- must be set to multi, legacy-autocommit, or twopc") + replicationMode = flag.String("replication-mode", "ROW", "The replication mode to simulate -- must be set to either ROW or STATEMENT") + normalize = flag.Bool("normalize", false, "Whether to enable vtgate normalization") + outputMode = flag.String("output-mode", "text", "Output in human-friendly text or json") + dbName = flag.String("dbname", "", "Optional database target to override normal routing") // vtexplainFlags lists all the flags that should show in usage vtexplainFlags = []string{ @@ -54,6 +56,8 @@ var ( "sql-file", "vschema", "vschema-file", + "ks-shard-map", + "ks-shard-map-file", "dbname", "queryserver-config-passthrough-dmls", } @@ -104,7 +108,7 @@ func init() { // getFileParam returns a string containing either flag is not "", // or the content of the file named flagFile -func getFileParam(flag, flagFile, name string) (string, error) { +func getFileParam(flag, flagFile, name string, required bool) (string, error) { if flag != "" { if flagFile != "" { return "", fmt.Errorf("action requires only one of %v or %v-file", name, name) @@ -113,7 +117,11 @@ func getFileParam(flag, flagFile, name string) (string, error) { } if flagFile == "" { - return "", fmt.Errorf("action requires one of %v or %v-file", name, name) + if required { + return "", fmt.Errorf("action requires one of %v or %v-file", name, name) + } + + return "", nil } data, err := ioutil.ReadFile(flagFile) if err != nil { @@ -137,17 +145,22 @@ func main() { } func parseAndRun() error { - sql, err := getFileParam(*sqlFlag, *sqlFileFlag, "sql") + sql, err := getFileParam(*sqlFlag, *sqlFileFlag, "sql", true) + if err != nil { + return err + } + + schema, err := getFileParam(*schemaFlag, *schemaFileFlag, "schema", true) if err != nil { return err } - schema, err := getFileParam(*schemaFlag, *schemaFileFlag, "schema") + vschema, err := getFileParam(*vschemaFlag, *vschemaFileFlag, "vschema", true) if err != nil { return err } - vschema, err := getFileParam(*vschemaFlag, *vschemaFileFlag, "vschema") + ksShardMap, err := getFileParam(*ksShardMapFlag, *ksShardMapFileFlag, "ks-shard-map", false) if err != nil { return err } @@ -164,7 +177,7 @@ func parseAndRun() error { log.V(100).Infof("schema %s\n", schema) log.V(100).Infof("vschema %s\n", vschema) - err = vtexplain.Init(vschema, schema, opts) + err = vtexplain.Init(vschema, schema, ksShardMap, opts) if err != nil { return err } diff --git a/go/vt/vtexplain/testdata/multi-output/select-sharded-8-output.txt b/go/vt/vtexplain/testdata/multi-output/select-sharded-8-output.txt new file mode 100644 index 00000000000..a50c6fcb067 --- /dev/null +++ b/go/vt/vtexplain/testdata/multi-output/select-sharded-8-output.txt @@ -0,0 +1,18 @@ +---------------------------------------------------------------------- +select * from user + +1 ks_sharded/-20: select * from user limit 10001 +1 ks_sharded/20-40: select * from user limit 10001 +1 ks_sharded/40-60: select * from user limit 10001 +1 ks_sharded/60-80: select * from user limit 10001 +1 ks_sharded/80-a0: select * from user limit 10001 +1 ks_sharded/a0-c0: select * from user limit 10001 +1 ks_sharded/c0-e0: select * from user limit 10001 +1 ks_sharded/e0-: select * from user limit 10001 + +---------------------------------------------------------------------- +select * from user where id in (1, 2) + +1 ks_sharded/-20: select * from user where id in (1, 2) limit 10001 + +---------------------------------------------------------------------- diff --git a/go/vt/vtexplain/testdata/multi-output/uneven-keyspace-output.txt b/go/vt/vtexplain/testdata/multi-output/uneven-keyspace-output.txt new file mode 100644 index 00000000000..8ed6a239e98 --- /dev/null +++ b/go/vt/vtexplain/testdata/multi-output/uneven-keyspace-output.txt @@ -0,0 +1,16 @@ +---------------------------------------------------------------------- +select * from user + +1 ks_sharded/-80: select * from user limit 10001 +1 ks_sharded/80-90: select * from user limit 10001 +1 ks_sharded/90-a0: select * from user limit 10001 +1 ks_sharded/a0-e8: select * from user limit 10001 +1 ks_sharded/e8-: select * from user limit 10001 + +---------------------------------------------------------------------- +select * from user where id in (10, 17, 42, 100000) + +1 ks_sharded/-80: select * from user where id in (10, 17, 42) limit 10001 +1 ks_sharded/80-90: select * from user where id in (100000) limit 10001 + +---------------------------------------------------------------------- diff --git a/go/vt/vtexplain/testdata/select-sharded-8-queries.sql b/go/vt/vtexplain/testdata/select-sharded-8-queries.sql new file mode 100644 index 00000000000..16db011b7a8 --- /dev/null +++ b/go/vt/vtexplain/testdata/select-sharded-8-queries.sql @@ -0,0 +1,2 @@ +select * from user; +select * from user where id in (1, 2); diff --git a/go/vt/vtexplain/testdata/uneven-keyspace-queries.sql b/go/vt/vtexplain/testdata/uneven-keyspace-queries.sql new file mode 100644 index 00000000000..0e3ef389612 --- /dev/null +++ b/go/vt/vtexplain/testdata/uneven-keyspace-queries.sql @@ -0,0 +1,2 @@ +select * from user; +select * from user where id in (10, 17, 42, 100000); diff --git a/go/vt/vtexplain/vtexplain.go b/go/vt/vtexplain/vtexplain.go index d1db8cea0e7..c4e543a329c 100644 --- a/go/vt/vtexplain/vtexplain.go +++ b/go/vt/vtexplain/vtexplain.go @@ -144,7 +144,7 @@ type Explain struct { } // Init sets up the fake execution environment -func Init(vSchemaStr, sqlSchema string, opts *Options) error { +func Init(vSchemaStr, sqlSchema, ksShardMapStr string, opts *Options) error { // Verify options if opts.ReplicationMode != "ROW" && opts.ReplicationMode != "STATEMENT" { return fmt.Errorf("invalid replication mode \"%s\"", opts.ReplicationMode) @@ -160,7 +160,7 @@ func Init(vSchemaStr, sqlSchema string, opts *Options) error { return fmt.Errorf("initTabletEnvironment: %v", err) } - err = initVtgateExecutor(vSchemaStr, opts) + err = initVtgateExecutor(vSchemaStr, ksShardMapStr, opts) if err != nil { return fmt.Errorf("initVtgateExecutor: %v", err) } diff --git a/go/vt/vtexplain/vtexplain_flaky_test.go b/go/vt/vtexplain/vtexplain_flaky_test.go index 2df2d0ad445..53dd6bb4a10 100644 --- a/go/vt/vtexplain/vtexplain_flaky_test.go +++ b/go/vt/vtexplain/vtexplain_flaky_test.go @@ -26,6 +26,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "vitess.io/vitess/go/vt/key" + "vitess.io/vitess/go/vt/proto/topodata" + "vitess.io/vitess/go/vt/topo" ) var testOutputTempDir string @@ -39,15 +42,27 @@ func defaultTestOpts() *Options { } } -func initTest(mode string, opts *Options, t *testing.T) { +type testopts struct { + shardmap map[string]map[string]*topo.ShardInfo +} + +func initTest(mode string, opts *Options, topts *testopts, t *testing.T) { schema, err := ioutil.ReadFile("testdata/test-schema.sql") require.NoError(t, err) vSchema, err := ioutil.ReadFile("testdata/test-vschema.json") require.NoError(t, err) + shardmap := "" + if topts.shardmap != nil { + shardmapBytes, err := json.Marshal(topts.shardmap) + require.NoError(t, err) + + shardmap = string(shardmapBytes) + } + opts.ExecutionMode = mode - err = Init(string(vSchema), string(schema), opts) + err = Init(string(vSchema), string(schema), shardmap, opts) require.NoError(t, err, "vtexplain Init error\n%s", string(schema)) } @@ -61,13 +76,13 @@ func testExplain(testcase string, opts *Options, t *testing.T) { } for _, mode := range modes { - runTestCase(testcase, mode, opts, t) + runTestCase(testcase, mode, opts, &testopts{}, t) } } -func runTestCase(testcase, mode string, opts *Options, t *testing.T) { +func runTestCase(testcase, mode string, opts *Options, topts *testopts, t *testing.T) { t.Run(testcase, func(t *testing.T) { - initTest(mode, opts, t) + initTest(mode, opts, topts, t) sqlFile := fmt.Sprintf("testdata/%s-queries.sql", testcase) sql, err := ioutil.ReadFile(sqlFile) @@ -130,7 +145,7 @@ func TestExplain(t *testing.T) { } func TestErrors(t *testing.T) { - initTest(ModeMulti, defaultTestOpts(), t) + initTest(ModeMulti, defaultTestOpts(), &testopts{}, t) tests := []struct { SQL string @@ -229,3 +244,60 @@ func TestJSONOutput(t *testing.T) { t.Errorf(diff) } } + +func testShardInfo(ks, start, end string, t *testing.T) *topo.ShardInfo { + kr, err := key.ParseKeyRangeParts(start, end) + require.NoError(t, err) + + return topo.NewShardInfo( + ks, + fmt.Sprintf("%s-%s", start, end), + &topodata.Shard{KeyRange: kr}, + &vtexplainTestTopoVersion{}, + ) +} + +func TestUsingKeyspaceShardMap(t *testing.T) { + tests := []struct { + testcase string + ShardRangeMap map[string]map[string]*topo.ShardInfo + }{ + { + testcase: "select-sharded-8", + ShardRangeMap: map[string]map[string]*topo.ShardInfo{ + "ks_sharded": { + "-20": testShardInfo("ks_sharded", "", "20", t), + "20-40": testShardInfo("ks_sharded", "20", "40", t), + "40-60": testShardInfo("ks_sharded", "40", "60", t), + "60-80": testShardInfo("ks_sharded", "60", "80", t), + "80-a0": testShardInfo("ks_sharded", "80", "a0", t), + "a0-c0": testShardInfo("ks_sharded", "a0", "c0", t), + "c0-e0": testShardInfo("ks_sharded", "c0", "e0", t), + "e0-": testShardInfo("ks_sharded", "e0", "", t), + }, + }, + }, + { + testcase: "uneven-keyspace", + ShardRangeMap: map[string]map[string]*topo.ShardInfo{ + // Have mercy on the poor soul that has this keyspace sharding. + // But, hey, vtexplain still works so they have that going for them. + "ks_sharded": { + "-80": testShardInfo("ks_sharded", "", "80", t), + "80-90": testShardInfo("ks_sharded", "80", "90", t), + "90-a0": testShardInfo("ks_sharded", "90", "a0", t), + "a0-e8": testShardInfo("ks_sharded", "a0", "e8", t), + "e8-": testShardInfo("ks_sharded", "e8", "", t), + }, + }, + }, + } + + for _, test := range tests { + runTestCase(test.testcase, ModeMulti, defaultTestOpts(), &testopts{test.ShardRangeMap}, t) + } +} + +type vtexplainTestTopoVersion struct{} + +func (vtexplain *vtexplainTestTopoVersion) String() string { return "vtexplain-test-topo" } diff --git a/go/vt/vtexplain/vtexplain_topo.go b/go/vt/vtexplain/vtexplain_topo.go index 342b5582f16..a8f02b30ae8 100644 --- a/go/vt/vtexplain/vtexplain_topo.go +++ b/go/vt/vtexplain/vtexplain_topo.go @@ -22,7 +22,6 @@ import ( "golang.org/x/net/context" - "vitess.io/vitess/go/vt/key" "vitess.io/vitess/go/vt/topo" topodatapb "vitess.io/vitess/go/vt/proto/topodata" @@ -38,6 +37,8 @@ type ExplainTopo struct { // Map of ks/shard to test tablet connection TabletConns map[string]*explainTablet + KeyspaceShards map[string]map[string]*topodatapb.ShardReference + // Synchronization lock Lock sync.Mutex @@ -83,68 +84,31 @@ func (et *ExplainTopo) GetSrvKeyspace(ctx context.Context, cell, keyspace string return nil, fmt.Errorf("no vschema for keyspace %s", keyspace) } - var srvKeyspace *topodatapb.SrvKeyspace - if vschema.Sharded { - shards := make([]*topodatapb.ShardReference, 0, et.NumShards) - for i := 0; i < et.NumShards; i++ { - kr, err := key.EvenShardsKeyRange(i, et.NumShards) - if err != nil { - return nil, err - } - - shard := &topodatapb.ShardReference{ - Name: key.KeyRangeString(kr), - KeyRange: kr, - } - shards = append(shards, shard) - } - - srvKeyspace = &topodatapb.SrvKeyspace{ - ShardingColumnName: "", // exact value is ignored - ShardingColumnType: 0, - Partitions: []*topodatapb.SrvKeyspace_KeyspacePartition{ - { - ServedType: topodatapb.TabletType_MASTER, - ShardReferences: shards, - }, - { - ServedType: topodatapb.TabletType_REPLICA, - ShardReferences: shards, - }, - { - ServedType: topodatapb.TabletType_RDONLY, - ShardReferences: shards, - }, + shards := make([]*topodatapb.ShardReference, 0, len(et.KeyspaceShards[keyspace])) + for _, shard := range et.KeyspaceShards[keyspace] { + shards = append(shards, shard) + } + + srvKeyspace := &topodatapb.SrvKeyspace{ + Partitions: []*topodatapb.SrvKeyspace_KeyspacePartition{ + { + ServedType: topodatapb.TabletType_MASTER, + ShardReferences: shards, }, - } - - } else { - // unsharded - kr, err := key.EvenShardsKeyRange(0, 1) - if err != nil { - return nil, err - } - - shard := &topodatapb.ShardReference{ - Name: key.KeyRangeString(kr), - } - - srvKeyspace = &topodatapb.SrvKeyspace{ - Partitions: []*topodatapb.SrvKeyspace_KeyspacePartition{ - { - ServedType: topodatapb.TabletType_MASTER, - ShardReferences: []*topodatapb.ShardReference{shard}, - }, - { - ServedType: topodatapb.TabletType_REPLICA, - ShardReferences: []*topodatapb.ShardReference{shard}, - }, - { - ServedType: topodatapb.TabletType_RDONLY, - ShardReferences: []*topodatapb.ShardReference{shard}, - }, + { + ServedType: topodatapb.TabletType_REPLICA, + ShardReferences: shards, }, - } + { + ServedType: topodatapb.TabletType_RDONLY, + ShardReferences: shards, + }, + }, + } + + if vschema.Sharded { + srvKeyspace.ShardingColumnName = "" // exact value is ignored + srvKeyspace.ShardingColumnType = 0 } return srvKeyspace, nil diff --git a/go/vt/vtexplain/vtexplain_vtgate.go b/go/vt/vtexplain/vtexplain_vtgate.go index f219a9c05f9..1dedcd53138 100644 --- a/go/vt/vtexplain/vtexplain_vtgate.go +++ b/go/vt/vtexplain/vtexplain_vtgate.go @@ -22,6 +22,7 @@ package vtexplain import ( "fmt" + "vitess.io/vitess/go/vt/topo" "vitess.io/vitess/go/vt/topo/memorytopo" "golang.org/x/net/context" @@ -52,14 +53,14 @@ var ( } ) -func initVtgateExecutor(vSchemaStr string, opts *Options) error { +func initVtgateExecutor(vSchemaStr, ksShardMapStr string, opts *Options) error { explainTopo = &ExplainTopo{NumShards: opts.NumShards} explainTopo.TopoServer = memorytopo.NewServer(vtexplainCell) healthCheck = discovery.NewFakeHealthCheck() resolver := newFakeResolver(opts, explainTopo, vtexplainCell) - err := buildTopology(opts, vSchemaStr, opts.NumShards) + err := buildTopology(opts, vSchemaStr, ksShardMapStr, opts.NumShards) if err != nil { return err } @@ -88,7 +89,7 @@ func newFakeResolver(opts *Options, serv srvtopo.Server, cell string) *vtgate.Re return vtgate.NewResolver(srvResolver, serv, cell, sc) } -func buildTopology(opts *Options, vschemaStr string, numShardsPerKeyspace int) error { +func buildTopology(opts *Options, vschemaStr string, ksShardMapStr string, numShardsPerKeyspace int) error { explainTopo.Lock.Lock() defer explainTopo.Lock.Unlock() @@ -102,31 +103,86 @@ func buildTopology(opts *Options, vschemaStr string, numShardsPerKeyspace int) e } explainTopo.Keyspaces = srvVSchema.Keyspaces + ksShardMap, err := getKeyspaceShardMap(ksShardMapStr) + if err != nil { + return err + } + explainTopo.TabletConns = make(map[string]*explainTablet) + explainTopo.KeyspaceShards = make(map[string]map[string]*topodatapb.ShardReference) for ks, vschema := range explainTopo.Keyspaces { - numShards := 1 - if vschema.Sharded { - numShards = numShardsPerKeyspace + shards, err := getShardRanges(ks, vschema, ksShardMap, numShardsPerKeyspace) + if err != nil { + return err } - for i := 0; i < numShards; i++ { - kr, err := key.EvenShardsKeyRange(i, numShards) - if err != nil { - return err - } - shard := key.KeyRangeString(kr) - hostname := fmt.Sprintf("%s/%s", ks, shard) - log.Infof("registering test tablet %s for keyspace %s shard %s", hostname, ks, shard) - tablet := healthCheck.AddFakeTablet(vtexplainCell, hostname, 1, ks, shard, topodatapb.TabletType_MASTER, true, 1, nil, func(t *topodatapb.Tablet) queryservice.QueryService { + explainTopo.KeyspaceShards[ks] = make(map[string]*topodatapb.ShardReference) + + for _, shard := range shards { + hostname := fmt.Sprintf("%s/%s", ks, shard.Name) + log.Infof("registering test tablet %s for keyspace %s shard %s", hostname, ks, shard.Name) + + tablet := healthCheck.AddFakeTablet(vtexplainCell, hostname, 1, ks, shard.Name, topodatapb.TabletType_MASTER, true, 1, nil, func(t *topodatapb.Tablet) queryservice.QueryService { return newTablet(opts, t) }) explainTopo.TabletConns[hostname] = tablet.(*explainTablet) + explainTopo.KeyspaceShards[ks][shard.Name] = shard } } return err } +func getKeyspaceShardMap(ksShardMapStr string) (map[string]map[string]*topo.ShardInfo, error) { + if ksShardMapStr == "" { + return map[string]map[string]*topo.ShardInfo{}, nil + } + + // keyspace-name -> shard-name -> ShardInfo + var ksShardMap map[string]map[string]*topo.ShardInfo + err := json2.Unmarshal([]byte(ksShardMapStr), &ksShardMap) + + return ksShardMap, err +} + +func getShardRanges(ks string, vschema *vschemapb.Keyspace, ksShardMap map[string]map[string]*topo.ShardInfo, numShardsPerKeyspace int) ([]*topodatapb.ShardReference, error) { + shardMap, ok := ksShardMap[ks] + if ok { + shards := make([]*topodatapb.ShardReference, 0, len(shardMap)) + for shard, info := range shardMap { + ref := &topodatapb.ShardReference{ + Name: shard, + KeyRange: info.KeyRange, + } + + shards = append(shards, ref) + } + return shards, nil + + } + + numShards := 1 + if vschema.Sharded { + numShards = numShardsPerKeyspace + } + + shards := make([]*topodatapb.ShardReference, numShards) + + for i := 0; i < numShards; i++ { + kr, err := key.EvenShardsKeyRange(i, numShards) + if err != nil { + return nil, err + } + + shards[i] = &topodatapb.ShardReference{ + Name: key.KeyRangeString(kr), + KeyRange: kr, + } + } + + return shards, nil +} + func vtgateExecute(sql string) ([]*engine.Plan, map[string]*TabletActions, error) { // use the plan cache to get the set of plans used for this query, then // clear afterwards for the next run