-
Notifications
You must be signed in to change notification settings - Fork 5.9k
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
infoschema,planner,execuor: support hot history region #27224
Changes from 31 commits
dd87813
fe67ab4
07b0cdd
2df8855
da5baab
88d132f
b1d877b
84fd7a4
bc5e3eb
8324c3e
33f4f51
28d2400
ac05ea6
6a4a248
25b6a5d
f8a45b3
351bd5a
9728584
b5ecdb9
7f07ae1
b8967a8
e27bbf8
bc8cba1
ca58a5a
9357d24
19c0037
3c7b4e1
3c238ba
f9b0b1c
47ecb36
b258d65
a49182d
5947504
37484c3
d3917b1
9ed1f79
a81eb94
6f2d228
13c044b
f7f7810
5092089
826693b
d81ccf8
c881c52
361e891
f5e8925
6781bad
160e34b
039220c
6debfe2
c41c11c
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 |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
package executor | ||
|
||
import ( | ||
"bytes" | ||
"container/heap" | ||
"context" | ||
"encoding/json" | ||
|
@@ -39,18 +40,22 @@ import ( | |
plannercore "github.com/pingcap/tidb/planner/core" | ||
"github.com/pingcap/tidb/sessionctx" | ||
"github.com/pingcap/tidb/sessionctx/variable" | ||
"github.com/pingcap/tidb/store/helper" | ||
"github.com/pingcap/tidb/types" | ||
"github.com/pingcap/tidb/util" | ||
"github.com/pingcap/tidb/util/chunk" | ||
"github.com/pingcap/tidb/util/codec" | ||
"github.com/pingcap/tidb/util/execdetails" | ||
"github.com/pingcap/tidb/util/pdapi" | ||
"github.com/pingcap/tidb/util/set" | ||
"github.com/tikv/client-go/v2/tikv" | ||
"go.uber.org/zap" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
) | ||
|
||
const clusterLogBatchSize = 256 | ||
const hotRegionsHistoryBatchSize = 256 | ||
|
||
type dummyCloser struct{} | ||
|
||
|
@@ -699,3 +704,318 @@ func (e *clusterLogRetriever) close() error { | |
func (e *clusterLogRetriever) getRuntimeStats() execdetails.RuntimeStats { | ||
return nil | ||
} | ||
|
||
type hotRegionsStreamResult struct { | ||
addr string | ||
messages *HistoryHotRegions | ||
err error | ||
} | ||
|
||
type hotRegionsResponseHeap []hotRegionsStreamResult | ||
|
||
func (h hotRegionsResponseHeap) Len() int { | ||
return len(h) | ||
} | ||
|
||
func (h hotRegionsResponseHeap) Less(i, j int) bool { | ||
lhs, rhs := h[i].messages.HistoryHotRegion[0], h[j].messages.HistoryHotRegion[0] | ||
if lhs.UpdateTime != rhs.UpdateTime { | ||
return lhs.UpdateTime < rhs.UpdateTime | ||
} | ||
return lhs.HotDegree < rhs.HotDegree | ||
} | ||
|
||
func (h hotRegionsResponseHeap) Swap(i, j int) { | ||
h[i], h[j] = h[j], h[i] | ||
} | ||
|
||
func (h *hotRegionsResponseHeap) Push(x interface{}) { | ||
*h = append(*h, x.(hotRegionsStreamResult)) | ||
} | ||
|
||
func (h *hotRegionsResponseHeap) Pop() interface{} { | ||
old := *h | ||
n := len(old) | ||
x := old[n-1] | ||
*h = old[0 : n-1] | ||
return x | ||
} | ||
|
||
type hotRegionsHistoryRetriver struct { | ||
isDrained bool | ||
retrieving bool | ||
heap *hotRegionsResponseHeap | ||
extractor *plannercore.HotRegionsHistoryTableExtractor | ||
cancel context.CancelFunc | ||
} | ||
|
||
// HistoryHotRegionsRequest wrap conditions push down to PD. | ||
type HistoryHotRegionsRequest struct { | ||
StartTime int64 `json:"start_time,omitempty"` | ||
EndTime int64 `json:"end_time,omitempty"` | ||
RegionIDs []uint64 `json:"region_ids,omitempty"` | ||
StoreIDs []uint64 `json:"store_ids,omitempty"` | ||
PeerIDs []uint64 `json:"peer_ids,omitempty"` | ||
Roles []uint64 `json:"roles,omitempty"` | ||
HotRegionTypes []string `json:"hot_region_types,omitempty"` | ||
} | ||
|
||
// HistoryHotRegions records filtered hot regions stored in each PD. | ||
// it's the response of PD. | ||
type HistoryHotRegions struct { | ||
HistoryHotRegion []*HistoryHotRegion `json:"history_hot_region"` | ||
} | ||
|
||
// HistoryHotRegion records each hot region's statistics. | ||
// it's the response of PD. | ||
type HistoryHotRegion struct { | ||
UpdateTime int64 `json:"update_time,omitempty"` | ||
RegionID uint64 `json:"region_id,omitempty"` | ||
StoreID uint64 `json:"store_id,omitempty"` | ||
PeerID uint64 `json:"peer_id,omitempty"` | ||
IsLeader bool `json:"is_leader,omitempty"` | ||
HotRegionType string `json:"hot_region_type,omitempty"` | ||
HotDegree int64 `json:"hot_degree,omitempty"` | ||
FlowBytes float64 `json:"flow_bytes,omitempty"` | ||
KeyRate float64 `json:"key_rate,omitempty"` | ||
QueryRate float64 `json:"query_rate,omitempty"` | ||
StartKey []byte `json:"start_key,omitempty"` | ||
EndKey []byte `json:"end_key,omitempty"` | ||
} | ||
|
||
const ( | ||
// HotRegionTypeREAD hot read region. | ||
HotRegionTypeREAD = "READ" | ||
// HotRegionTypeWRITE hot write region. | ||
HotRegionTypeWRITE = "WRITE" | ||
) | ||
|
||
func (e *hotRegionsHistoryRetriver) initialize(ctx context.Context, sctx sessionctx.Context) ([]chan hotRegionsStreamResult, error) { | ||
if !hasPriv(sctx, mysql.ProcessPriv) { | ||
return nil, plannercore.ErrSpecificAccessDenied.GenWithStackByArgs("PROCESS") | ||
} | ||
pdServers, err := infoschema.GetPDServerInfo(sctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// To avoid search hot regions interface overload, the user should specify the time range in normally SQL. | ||
if e.extractor.StartTime == 0 { | ||
return nil, errors.New("denied to scan hot regions, please specified the start time, such as `update_time > '2020-01-01 00:00:00'`") | ||
} | ||
if e.extractor.EndTime == 0 { | ||
return nil, errors.New("denied to scan hot regions, please specified the end time, such as `update_time < '2020-01-01 00:00:00'`") | ||
} | ||
|
||
// Divide read write into two request because of time range ovelap, | ||
// because PD use [type,time] as key of hot regions. | ||
if e.extractor.HotRegionTypes.Count() == 0 { | ||
e.extractor.HotRegionTypes.Insert(HotRegionTypeREAD) | ||
e.extractor.HotRegionTypes.Insert(HotRegionTypeWRITE) | ||
} | ||
hotRegionTypes := make([]string, 0, e.extractor.HotRegionTypes.Count()) | ||
for typ := range e.extractor.HotRegionTypes { | ||
hotRegionTypes = append(hotRegionTypes, typ) | ||
} | ||
// set hotType before request | ||
historyHotRegionsRequest := &HistoryHotRegionsRequest{ | ||
StartTime: e.extractor.StartTime, | ||
EndTime: e.extractor.EndTime, | ||
RegionIDs: e.extractor.RegionIDs, | ||
StoreIDs: e.extractor.StoreIDs, | ||
PeerIDs: e.extractor.PeerIDs, | ||
Roles: e.extractor.Roles, | ||
} | ||
|
||
return e.startRetrieving(ctx, sctx, pdServers, historyHotRegionsRequest) | ||
} | ||
|
||
func (e *hotRegionsHistoryRetriver) startRetrieving( | ||
ctx context.Context, | ||
sctx sessionctx.Context, | ||
serversInfo []infoschema.ServerInfo, | ||
req *HistoryHotRegionsRequest, | ||
) ([]chan hotRegionsStreamResult, error) { | ||
var results []chan hotRegionsStreamResult | ||
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. seem we don't use stream here 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. change to |
||
for _, srv := range serversInfo { | ||
for typ := range e.extractor.HotRegionTypes { | ||
req.HotRegionTypes = []string{typ} | ||
jsonBody, err := json.Marshal(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
body := bytes.NewBuffer(jsonBody) | ||
ch := make(chan hotRegionsStreamResult) | ||
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. Does the channel need to be closed? 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. add defer to close it. ch := make(chan hotRegionsResult)
results = append(results, ch)
go func(ch chan hotRegionsResult, address string, body *bytes.Buffer) {
util.WithRecovery(func() {
defer close(ch) |
||
results = append(results, ch) | ||
go func(address string, body *bytes.Buffer) { | ||
util.WithRecovery(func() { | ||
url := fmt.Sprintf("%s://%s%s", util.InternalHTTPSchema(), address, pdapi.HotHistory) | ||
req, err := http.NewRequest(http.MethodGet, url, body) | ||
if err != nil { | ||
ch <- hotRegionsStreamResult{err: errors.Trace(err)} | ||
return | ||
} | ||
req.Header.Add("PD-Allow-follower-handle", "true") | ||
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. It seems that we don't sync the hot region history among PDs. 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. yes 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. Then it should be false here? 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. Some hot regions history may lost if PD down, we have discussed this in design stage. The dafult reserve interval is 7 day, and we think the importance of hot region history is not worth synchronizing right now. 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. Consider such a case that we have 3 PDs and it never changes the PD leader. If the request is sent to followers, the results here will be empty. 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. Below code is used to get all PD servers, the results are merge in pdServers, err := infoschema.GetPDServerInfo(sctx) 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. Oh, sorry, I missed the context of this line of code, it should be false~ |
||
resp, err := util.InternalHTTPClient().Do(req) | ||
if err != nil { | ||
ch <- hotRegionsStreamResult{err: errors.Trace(err)} | ||
return | ||
} | ||
defer func() { | ||
terror.Log(resp.Body.Close()) | ||
}() | ||
if resp.StatusCode != http.StatusOK { | ||
ch <- hotRegionsStreamResult{err: errors.Errorf("request %s failed: %s", url, resp.Status)} | ||
return | ||
} | ||
var historyHotRegions HistoryHotRegions | ||
if err = json.NewDecoder(resp.Body).Decode(&historyHotRegions); err != nil { | ||
ch <- hotRegionsStreamResult{err: errors.Trace(err)} | ||
return | ||
} | ||
ch <- hotRegionsStreamResult{addr: address, messages: &historyHotRegions} | ||
}, nil) | ||
}(srv.StatusAddr, body) | ||
} | ||
} | ||
return results, nil | ||
} | ||
|
||
func (e *hotRegionsHistoryRetriver) retrieve(ctx context.Context, sctx sessionctx.Context) ([][]types.Datum, error) { | ||
if e.extractor.SkipRequest || e.isDrained { | ||
return nil, nil | ||
} | ||
|
||
if !e.retrieving { | ||
e.retrieving = true | ||
results, err := e.initialize(ctx, sctx) | ||
if err != nil { | ||
e.isDrained = true | ||
return nil, err | ||
} | ||
// Initialize the heap | ||
e.heap = &hotRegionsResponseHeap{} | ||
for _, ch := range results { | ||
result := <-ch | ||
if result.err != nil || len(result.messages.HistoryHotRegion) == 0 { | ||
if result.err != nil { | ||
sctx.GetSessionVars().StmtCtx.AppendWarning(result.err) | ||
} | ||
continue | ||
} | ||
*e.heap = append(*e.heap, result) | ||
} | ||
heap.Init(e.heap) | ||
} | ||
// Merge the results | ||
var finalRows [][]types.Datum | ||
allSchemas := sctx.GetInfoSchema().(infoschema.InfoSchema).AllSchemas() | ||
tikvStore, ok := sctx.GetStore().(helper.Storage) | ||
tz := sctx.GetSessionVars().Location() | ||
if !ok { | ||
return nil, errors.New("Information about hot region can be gotten only when the storage is TiKV") | ||
} | ||
tikvHelper := &helper.Helper{ | ||
Store: tikvStore, | ||
RegionCache: tikvStore.GetRegionCache(), | ||
} | ||
for e.heap.Len() > 0 && len(finalRows) < hotRegionsHistoryBatchSize { | ||
minTimeItem := heap.Pop(e.heap).(hotRegionsStreamResult) | ||
row, err := e.getHotRegionRowWithSchemaInfo(minTimeItem.messages.HistoryHotRegion[0], tikvHelper, allSchemas, tz) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if row != nil { | ||
finalRows = append(finalRows, row) | ||
} | ||
minTimeItem.messages.HistoryHotRegion = minTimeItem.messages.HistoryHotRegion[1:] | ||
// Fetch next message item | ||
if len(minTimeItem.messages.HistoryHotRegion) != 0 { | ||
heap.Push(e.heap, minTimeItem) | ||
} | ||
} | ||
// All streams are drained | ||
e.isDrained = e.heap.Len() == 0 | ||
return finalRows, nil | ||
} | ||
|
||
func (e *hotRegionsHistoryRetriver) getHotRegionRowWithSchemaInfo( | ||
hisHotRegion *HistoryHotRegion, | ||
tikvHelper *helper.Helper, | ||
allSchemas []*model.DBInfo, | ||
tz *time.Location, | ||
) ([]types.Datum, error) { | ||
_, startKey, _ := codec.DecodeBytes(hisHotRegion.StartKey, []byte{}) | ||
_, endKey, _ := codec.DecodeBytes(hisHotRegion.EndKey, []byte{}) | ||
region := &tikv.KeyLocation{StartKey: startKey, EndKey: endKey} | ||
hotRange, err := helper.NewRegionFrameRange(region) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
f := tikvHelper.FindTableIndexOfRegion(allSchemas, hotRange) | ||
// Ignore row without coresponding schema f. | ||
IcePigZDB marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if f == nil { | ||
return nil, nil | ||
} | ||
row := make([]types.Datum, len(infoschema.TableTiDBHotRegionsHistoryCols)) | ||
updateTimestamp := time.Unix(hisHotRegion.UpdateTime/1000, (hisHotRegion.UpdateTime%1000)*int64(time.Millisecond)) | ||
|
||
IcePigZDB marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if updateTimestamp.Location() != tz { | ||
updateTimestamp.In(tz) | ||
} | ||
updateTime := types.NewTime(types.FromGoTime(updateTimestamp), mysql.TypeTimestamp, types.MinFsp) | ||
row[0].SetMysqlTime(updateTime) | ||
row[1].SetString(strings.ToUpper(f.DBName), mysql.DefaultCollationName) | ||
row[2].SetString(strings.ToUpper(f.TableName), mysql.DefaultCollationName) | ||
row[3].SetInt64(f.TableID) | ||
if f.IndexName != "" { | ||
row[4].SetString(strings.ToUpper(f.IndexName), mysql.DefaultCollationName) | ||
row[5].SetInt64(f.IndexID) | ||
} else { | ||
row[4].SetNull() | ||
row[5].SetNull() | ||
} | ||
row[6].SetInt64(int64(hisHotRegion.RegionID)) | ||
row[7].SetInt64(int64(hisHotRegion.StoreID)) | ||
row[8].SetInt64(int64(hisHotRegion.PeerID)) | ||
if hisHotRegion.IsLeader { | ||
row[9].SetInt64(1) | ||
} else { | ||
row[9].SetInt64(0) | ||
} | ||
|
||
row[10].SetString(strings.ToUpper(hisHotRegion.HotRegionType), mysql.DefaultCollationName) | ||
if hisHotRegion.HotDegree != 0 { | ||
row[11].SetInt64(hisHotRegion.HotDegree) | ||
} else { | ||
row[11].SetNull() | ||
} | ||
if hisHotRegion.FlowBytes != 0 { | ||
row[12].SetFloat64(float64(hisHotRegion.FlowBytes)) | ||
} else { | ||
row[12].SetNull() | ||
} | ||
if hisHotRegion.KeyRate != 0 { | ||
row[13].SetFloat64(float64(hisHotRegion.KeyRate)) | ||
} else { | ||
row[13].SetNull() | ||
} | ||
if hisHotRegion.QueryRate != 0 { | ||
row[14].SetFloat64(float64(hisHotRegion.QueryRate)) | ||
} else { | ||
row[14].SetNull() | ||
} | ||
return row, nil | ||
} | ||
|
||
func (e *hotRegionsHistoryRetriver) close() error { | ||
if e.cancel != nil { | ||
e.cancel() | ||
} | ||
return nil | ||
} | ||
|
||
func (e *hotRegionsHistoryRetriver) getRuntimeStats() execdetails.RuntimeStats { | ||
return 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.
The way of naming a variable doesn't meet the go style.
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.
change to
HotRegionTypeRead
andHotRegionTypeWrite
.