-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
server: add http api to get some info of sub-optimal query #10717
Changes from 2 commits
a763b8a
0b32e29
d2d94a5
68e99ef
85a4bd8
11b99a2
6cb8777
6238feb
e0aef2f
c152e7e
65f5c09
258ec5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
// Copyright 2019 PingCAP, Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package server | ||
|
||
import ( | ||
"archive/zip" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"runtime/pprof" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/pingcap/parser" | ||
"github.com/pingcap/parser/ast" | ||
"github.com/pingcap/parser/model" | ||
"github.com/pingcap/parser/terror" | ||
"github.com/pingcap/tidb/domain" | ||
"github.com/pingcap/tidb/session" | ||
"github.com/pingcap/tidb/statistics/handle" | ||
"github.com/pingcap/tidb/util/sqlexec" | ||
"github.com/pingcap/tidb/util/testkit" | ||
) | ||
|
||
type sqlInfoFetcher struct { | ||
do *domain.Domain | ||
s session.Session | ||
} | ||
|
||
type tableNamePair struct { | ||
DBName string | ||
TableName string | ||
} | ||
|
||
type tableNameExtractor struct { | ||
names map[tableNamePair]struct{} | ||
} | ||
|
||
func (tne *tableNameExtractor) Enter(in ast.Node) (ast.Node, bool) { | ||
if _, ok := in.(*ast.TableName); ok { | ||
return in, true | ||
} | ||
return in, false | ||
} | ||
|
||
func (tne *tableNameExtractor) Leave(in ast.Node) (ast.Node, bool) { | ||
if t, ok := in.(*ast.TableName); ok { | ||
tp := tableNamePair{DBName: t.Schema.L, TableName: t.Name.L} | ||
if _, ok := tne.names[tp]; !ok { | ||
tne.names[tp] = struct{}{} | ||
} | ||
} | ||
return in, true | ||
} | ||
|
||
func (sh *sqlInfoFetcher) zipInfoForSQL(w http.ResponseWriter, r *http.Request) { | ||
reqCtx := r.Context() | ||
sql := r.FormValue("sql") | ||
alivxxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pprofTimeString := r.FormValue("pprof_time") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how about |
||
timeoutString := r.FormValue("timeout") | ||
var ( | ||
pprofTime int | ||
timeout int | ||
err error | ||
) | ||
if pprofTimeString != "" { | ||
pprofTime, err = strconv.Atoi(pprofTimeString) | ||
qw4990 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check |
||
serveError(w, http.StatusBadRequest, "invalid value for pprof_time") | ||
return | ||
} | ||
if pprofTime < 5 { | ||
winoros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
serveError(w, http.StatusBadRequest, "pprof time is too short") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a best practice for this kind of error message is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we return here if it's an invalid value? |
||
} | ||
if timeoutString != "" { | ||
timeout, err = strconv.Atoi(timeoutString) | ||
} | ||
if err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
serveError(w, http.StatusBadRequest, "invalid value for timeout") | ||
return | ||
} | ||
if timeout < pprofTime { | ||
timeout = pprofTime | ||
} | ||
pairs, err := sh.extractTableNames(sql) | ||
if err != nil { | ||
serveError(w, http.StatusBadRequest, fmt.Sprintf("invalid SQL text, err: %v", err)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how about "failed to extract table names, sql: %v" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But the sql is something given by the user. |
||
return | ||
} | ||
zw := zip.NewWriter(w) | ||
defer func() { | ||
terror.Log(zw.Close()) | ||
}() | ||
for pair := range pairs { | ||
jsonTbl, err := sh.getStatsForTable(pair) | ||
if err != nil { | ||
terror.Log(err) | ||
continue | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we continue or just throw error out? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we return a message to indicate the failure of dumping stats for this table? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. write a file like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks OK to me |
||
} | ||
statsFw, err := zw.Create(fmt.Sprintf("%v.%v.json", pair.DBName, pair.TableName)) | ||
if err != nil { | ||
terror.Log(err) | ||
continue | ||
} | ||
data, err := json.Marshal(jsonTbl) | ||
if err != nil { | ||
terror.Log(err) | ||
continue | ||
} | ||
_, err = statsFw.Write(data) | ||
if err != nil { | ||
terror.Log(err) | ||
continue | ||
} | ||
} | ||
for pair := range pairs { | ||
err = sh.getShowCreateTable(pair, zw) | ||
if err != nil { | ||
serveError(w, http.StatusInternalServerError, fmt.Sprintf("get schema for %v.%v failed", pair.DBName, pair.TableName)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it ok that we write error messages and zip formatted data to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, it's not very ok. The following is the curl result when error arisen.
But the api There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK... for simplifying, we can leave it as a further optimization and do it in another PR and please add a TODO here. |
||
return | ||
} | ||
} | ||
// If we don't catch profile. We just get a explain result. | ||
if pprofTime == 0 { | ||
recordSets, err := sh.s.(sqlexec.SQLExecutor).Execute(reqCtx, fmt.Sprintf("explain %s", sql)) | ||
if len(recordSets) > 0 { | ||
defer terror.Call(recordSets[0].Close) | ||
} | ||
if err != nil { | ||
serveError(w, http.StatusInternalServerError, fmt.Sprintf("execute SQL failed, err: %v", err)) | ||
return | ||
} | ||
sRows, err := testkit.ResultSetToStringSlice(reqCtx, sh.s, recordSets[0]) | ||
if err != nil { | ||
terror.Log(err) | ||
return | ||
} | ||
fw, err := zw.Create("explain.result") | ||
if err != nil { | ||
terror.Log(err) | ||
return | ||
} | ||
for _, row := range sRows { | ||
fmt.Fprintf(fw, "%s\n", strings.Join(row, "\t")) | ||
} | ||
} else { | ||
// Otherwise we catch a profile and run `EXPLAIN ANALYZE` result. | ||
ctx, cancelFunc := context.WithCancel(reqCtx) | ||
timer := time.NewTimer(time.Second * time.Duration(timeout)) | ||
resultChan := make(chan *explainAnalyzeResult) | ||
go sh.getExplainAnalyze(ctx, sql, resultChan) | ||
errChan := make(chan error) | ||
go sh.catchCPUProfile(reqCtx, pprofTime, zw, errChan) | ||
select { | ||
case result := <-resultChan: | ||
timer.Stop() | ||
cancelFunc() | ||
if result.err != nil { | ||
serveError(w, http.StatusInternalServerError, fmt.Sprintf("explain analyze SQL failed, err: %v", err)) | ||
return | ||
} | ||
if len(result.rows) == 0 { | ||
break | ||
} | ||
fw, err := zw.Create("explain_analyze.result") | ||
if err != nil { | ||
terror.Log(err) | ||
break | ||
} | ||
for _, row := range result.rows { | ||
fmt.Fprintf(fw, "%s\n", strings.Join(row, "\t")) | ||
} | ||
case <-timer.C: | ||
cancelFunc() | ||
} | ||
err = <-errChan | ||
if err != nil { | ||
serveError(w, http.StatusInternalServerError, fmt.Sprintf("catch cpu profile failed, error: %v", err)) | ||
return | ||
} | ||
} | ||
} | ||
|
||
type explainAnalyzeResult struct { | ||
rows [][]string | ||
err error | ||
} | ||
|
||
func (sh *sqlInfoFetcher) getExplainAnalyze(ctx context.Context, sql string, resultChan chan<- *explainAnalyzeResult) { | ||
recordSets, err := sh.s.(sqlexec.SQLExecutor).Execute(ctx, fmt.Sprintf("explain analyze %s", sql)) | ||
if len(recordSets) > 0 { | ||
defer terror.Call(recordSets[0].Close) | ||
} | ||
if err != nil { | ||
resultChan <- &explainAnalyzeResult{err: err} | ||
return | ||
} | ||
rows, err := testkit.ResultSetToStringSlice(ctx, sh.s, recordSets[0]) | ||
if err != nil { | ||
terror.Log(err) | ||
rows = nil | ||
return | ||
} | ||
resultChan <- &explainAnalyzeResult{rows: rows} | ||
} | ||
|
||
func (sh *sqlInfoFetcher) catchCPUProfile(ctx context.Context, sec int, zw *zip.Writer, errChan chan<- error) { | ||
// dump profile | ||
fw, err := zw.Create("profile") | ||
if err != nil { | ||
errChan <- err | ||
return | ||
} | ||
if err := pprof.StartCPUProfile(fw); err != nil { | ||
errChan <- err | ||
return | ||
} | ||
sleepWithCtx(ctx, time.Duration(sec)*time.Second) | ||
pprof.StopCPUProfile() | ||
errChan <- nil | ||
} | ||
|
||
func (sh *sqlInfoFetcher) getStatsForTable(pair tableNamePair) (*handle.JSONTable, error) { | ||
is := sh.do.InfoSchema() | ||
h := sh.do.StatsHandle() | ||
tbl, err := is.TableByName(model.NewCIStr(pair.DBName), model.NewCIStr(pair.TableName)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
js, err := h.DumpStatsToJSON(pair.DBName, tbl.Meta(), nil) | ||
return js, err | ||
} | ||
|
||
func (sh *sqlInfoFetcher) getShowCreateTable(pair tableNamePair, zw *zip.Writer) error { | ||
recordSets, err := sh.s.(sqlexec.SQLExecutor).Execute(context.TODO(), fmt.Sprintf("show create table `%v`.`%v`", pair.DBName, pair.TableName)) | ||
if len(recordSets) > 0 { | ||
defer terror.Call(recordSets[0].Close) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
sRows, err := testkit.ResultSetToStringSlice(context.Background(), sh.s, recordSets[0]) | ||
if err != nil { | ||
terror.Log(err) | ||
return nil | ||
} | ||
fw, err := zw.Create(fmt.Sprintf("%v.%v.schema.txt", pair.DBName, pair.TableName)) | ||
if err != nil { | ||
terror.Log(err) | ||
return nil | ||
} | ||
for _, row := range sRows { | ||
fmt.Fprintf(fw, "%s\n", strings.Join(row, "\t")) | ||
} | ||
return nil | ||
} | ||
|
||
func (sh *sqlInfoFetcher) extractTableNames(sql string) (map[tableNamePair]struct{}, error) { | ||
p := parser.New() | ||
charset, collation := sh.s.GetSessionVars().GetCharsetInfo() | ||
stmts, _, err := p.Parse(sql, charset, collation) | ||
if err != nil { | ||
return nil, err | ||
} | ||
extractor := &tableNameExtractor{ | ||
names: make(map[tableNamePair]struct{}), | ||
} | ||
for _, stmt := range stmts { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we allow multiple statements? Will it cause problems if we use |
||
stmt.Accept(extractor) | ||
} | ||
return extractor.names, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.