You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
338 lines
8.8 KiB
338 lines
8.8 KiB
// 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"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"runtime/pprof"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pingcap/errors"
|
|
"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/store/tikv"
|
|
"github.com/pingcap/tidb/util/sqlexec"
|
|
)
|
|
|
|
type sqlInfoFetcher struct {
|
|
store tikv.Storage
|
|
do *domain.Domain
|
|
s session.Session
|
|
}
|
|
|
|
type tableNamePair struct {
|
|
DBName string
|
|
TableName string
|
|
}
|
|
|
|
type tableNameExtractor struct {
|
|
curDB string
|
|
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 tp.DBName == "" {
|
|
tp.DBName = tne.curDB
|
|
}
|
|
if _, ok := tne.names[tp]; !ok {
|
|
tne.names[tp] = struct{}{}
|
|
}
|
|
}
|
|
return in, true
|
|
}
|
|
|
|
func (sh *sqlInfoFetcher) zipInfoForSQL(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
sh.s, err = session.CreateSession(sh.store)
|
|
if err != nil {
|
|
serveError(w, http.StatusInternalServerError, fmt.Sprintf("create session failed, err: %v", err))
|
|
return
|
|
}
|
|
defer sh.s.Close()
|
|
sh.do = domain.GetDomain(sh.s)
|
|
reqCtx := r.Context()
|
|
sql := r.FormValue("sql")
|
|
pprofTimeString := r.FormValue("pprof_time")
|
|
timeoutString := r.FormValue("timeout")
|
|
curDB := strings.ToLower(r.FormValue("current_db"))
|
|
if curDB != "" {
|
|
_, err = sh.s.Execute(reqCtx, fmt.Sprintf("use %v", curDB))
|
|
if err != nil {
|
|
serveError(w, http.StatusInternalServerError, fmt.Sprintf("use database %v failed, err: %v", curDB, err))
|
|
return
|
|
}
|
|
}
|
|
var (
|
|
pprofTime int
|
|
timeout int
|
|
)
|
|
if pprofTimeString != "" {
|
|
pprofTime, err = strconv.Atoi(pprofTimeString)
|
|
if err != nil {
|
|
serveError(w, http.StatusBadRequest, "invalid value for pprof_time, please input a int value larger than 5")
|
|
return
|
|
}
|
|
}
|
|
if pprofTimeString != "" && pprofTime < 5 {
|
|
serveError(w, http.StatusBadRequest, "pprof time is too short, please input a int value larger than 5")
|
|
}
|
|
if timeoutString != "" {
|
|
timeout, err = strconv.Atoi(timeoutString)
|
|
if err != nil {
|
|
serveError(w, http.StatusBadRequest, "invalid value for timeout")
|
|
return
|
|
}
|
|
}
|
|
if timeout < pprofTime {
|
|
timeout = pprofTime
|
|
}
|
|
pairs, err := sh.extractTableNames(sql, curDB)
|
|
if err != nil {
|
|
serveError(w, http.StatusBadRequest, fmt.Sprintf("invalid SQL text, err: %v", err))
|
|
return
|
|
}
|
|
zw := zip.NewWriter(w)
|
|
defer func() {
|
|
terror.Log(zw.Close())
|
|
}()
|
|
for pair := range pairs {
|
|
jsonTbl, err := sh.getStatsForTable(pair)
|
|
if err != nil {
|
|
err = sh.writeErrFile(zw, fmt.Sprintf("%v.%v.stats.err.txt", pair.DBName, pair.TableName), err)
|
|
terror.Log(err)
|
|
continue
|
|
}
|
|
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 {
|
|
err = sh.writeErrFile(zw, fmt.Sprintf("%v.%v.stats.err.txt", pair.DBName, pair.TableName), err)
|
|
terror.Log(err)
|
|
continue
|
|
}
|
|
_, err = statsFw.Write(data)
|
|
if err != nil {
|
|
err = sh.writeErrFile(zw, fmt.Sprintf("%v.%v.stats.err.txt", pair.DBName, pair.TableName), err)
|
|
terror.Log(err)
|
|
continue
|
|
}
|
|
}
|
|
for pair := range pairs {
|
|
err = sh.getShowCreateTable(pair, zw)
|
|
if err != nil {
|
|
err = sh.writeErrFile(zw, fmt.Sprintf("%v.%v.schema.err.txt", pair.DBName, pair.TableName), err)
|
|
terror.Log(err)
|
|
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 {
|
|
err = sh.writeErrFile(zw, "explain.err.txt", err)
|
|
terror.Log(err)
|
|
return
|
|
}
|
|
sRows, err := session.ResultSetToStringSlice(reqCtx, sh.s, recordSets[0])
|
|
if err != nil {
|
|
err = sh.writeErrFile(zw, "explain.err.txt", err)
|
|
terror.Log(err)
|
|
return
|
|
}
|
|
fw, err := zw.Create("explain.txt")
|
|
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)
|
|
var buf bytes.Buffer
|
|
go sh.catchCPUProfile(reqCtx, pprofTime, &buf, errChan)
|
|
select {
|
|
case result := <-resultChan:
|
|
timer.Stop()
|
|
cancelFunc()
|
|
if result.err != nil {
|
|
err = sh.writeErrFile(zw, "explain_analyze.err.txt", result.err)
|
|
terror.Log(err)
|
|
return
|
|
}
|
|
if len(result.rows) == 0 {
|
|
break
|
|
}
|
|
fw, err := zw.Create("explain_analyze.txt")
|
|
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 = dumpCPUProfile(errChan, &buf, zw)
|
|
if err != nil {
|
|
err = sh.writeErrFile(zw, "profile.err.txt", err)
|
|
terror.Log(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func dumpCPUProfile(errChan chan error, buf *bytes.Buffer, zw *zip.Writer) error {
|
|
err := <-errChan
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fw, err := zw.Create("profile")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fw.Write(buf.Bytes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sh *sqlInfoFetcher) writeErrFile(zw *zip.Writer, name string, err error) error {
|
|
fw, err1 := zw.Create(name)
|
|
if err1 != nil {
|
|
return err1
|
|
}
|
|
fmt.Fprintf(fw, "error: %v", err)
|
|
return nil
|
|
}
|
|
|
|
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 err != nil {
|
|
for _, rs := range recordSets {
|
|
err1 := rs.Close()
|
|
terror.Log(err1)
|
|
}
|
|
resultChan <- &explainAnalyzeResult{err: err}
|
|
return
|
|
}
|
|
rows, err := session.ResultSetToStringSlice(ctx, sh.s, recordSets[0])
|
|
if err != nil {
|
|
terror.Log(err)
|
|
return
|
|
}
|
|
resultChan <- &explainAnalyzeResult{rows: rows}
|
|
}
|
|
|
|
func (sh *sqlInfoFetcher) catchCPUProfile(ctx context.Context, sec int, buf *bytes.Buffer, errChan chan<- error) {
|
|
if err := pprof.StartCPUProfile(buf); 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 := session.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, curDB 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
|
|
}
|
|
if len(stmts) > 1 {
|
|
return nil, errors.Errorf("Only 1 statement is allowed")
|
|
}
|
|
extractor := &tableNameExtractor{
|
|
curDB: curDB,
|
|
names: make(map[tableNamePair]struct{}),
|
|
}
|
|
stmts[0].Accept(extractor)
|
|
return extractor.names, nil
|
|
}
|
|
|