diff --git a/client/go.mod b/client/go.mod index d5857dc01d7..1e250789938 100644 --- a/client/go.mod +++ b/client/go.mod @@ -36,9 +36,9 @@ require ( github.com/stretchr/objx v0.5.2 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/client/go.sum b/client/go.sum index 5b0ec9b2343..0ddcb8a7a84 100644 --- a/client/go.sum +++ b/client/go.sum @@ -126,8 +126,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -138,13 +138,13 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -153,8 +153,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/client/http/types.go b/client/http/types.go index 0ef8133310e..6acef7bcea2 100644 --- a/client/http/types.go +++ b/client/http/types.go @@ -171,9 +171,11 @@ type HotPeersStat struct { StoreByteRate float64 `json:"store_bytes"` StoreKeyRate float64 `json:"store_keys"` StoreQueryRate float64 `json:"store_query"` + StoreCPURate float64 `json:"store_cpu"` TotalBytesRate float64 `json:"total_flow_bytes"` TotalKeysRate float64 `json:"total_flow_keys"` TotalQueryRate float64 `json:"total_flow_query"` + TotalCPURate float64 `json:"total_flow_cpu"` Count int `json:"regions_count"` Stats []HotPeerStatShow `json:"statistics"` } @@ -189,6 +191,7 @@ type HotPeerStatShow struct { ByteRate float64 `json:"flow_bytes"` KeyRate float64 `json:"flow_keys"` QueryRate float64 `json:"flow_query"` + CPURate float64 `json:"flow_cpu"` AntiCount int `json:"anti_count"` LastUpdateTime time.Time `json:"last_update_time,omitempty"` } @@ -250,6 +253,7 @@ type HistoryHotRegion struct { HotRegionType string `json:"hot_region_type"` HotDegree int64 `json:"hot_degree"` FlowBytes float64 `json:"flow_bytes"` + FlowCPU float64 `json:"flow_cpu"` KeyRate float64 `json:"key_rate"` QueryRate float64 `json:"query_rate"` StartKey string `json:"start_key"` diff --git a/client/http/types_test.go b/client/http/types_test.go index 21aa612798b..1f80783b570 100644 --- a/client/http/types_test.go +++ b/client/http/types_test.go @@ -318,6 +318,78 @@ func TestRuleOpStartEndKey(t *testing.T) { re.Equal(ruleOpToMarshal.DeleteByIDPrefix, ruleOp.DeleteByIDPrefix) } +func TestHotRegionStatsCPUFields(t *testing.T) { + re := require.New(t) + raw := []byte(`{ + "as_peer": { + "1": { + "store_bytes": 10, + "store_keys": 11, + "store_query": 12, + "store_cpu": 13, + "total_flow_bytes": 20, + "total_flow_keys": 21, + "total_flow_query": 22, + "total_flow_cpu": 23, + "regions_count": 1, + "statistics": [{ + "store_id": 1, + "stores": [1, 2], + "is_leader": true, + "is_learner": false, + "region_id": 100, + "hot_degree": 6, + "flow_bytes": 30, + "flow_keys": 31, + "flow_query": 32, + "flow_cpu": 33, + "anti_count": 4 + }] + } + }, + "as_leader": { + "2": { + "store_bytes": 40, + "store_keys": 41, + "store_query": 42, + "store_cpu": 43, + "total_flow_bytes": 50, + "total_flow_keys": 51, + "total_flow_query": 52, + "total_flow_cpu": 53, + "regions_count": 1, + "statistics": [{ + "store_id": 2, + "stores": [2, 3], + "is_leader": true, + "is_learner": false, + "region_id": 200, + "hot_degree": 7, + "flow_bytes": 60, + "flow_keys": 61, + "flow_query": 62, + "flow_cpu": 63, + "anti_count": 5 + }] + } + } + }`) + + var stats StoreHotPeersInfos + err := json.Unmarshal(raw, &stats) + re.NoError(err) + + re.Equal(13.0, stats.AsPeer[1].StoreCPURate) + re.Equal(23.0, stats.AsPeer[1].TotalCPURate) + re.Len(stats.AsPeer[1].Stats, 1) + re.Equal(33.0, stats.AsPeer[1].Stats[0].CPURate) + + re.Equal(43.0, stats.AsLeader[2].StoreCPURate) + re.Equal(53.0, stats.AsLeader[2].TotalCPURate) + re.Len(stats.AsLeader[2].Stats, 1) + re.Equal(63.0, stats.AsLeader[2].Stats[0].CPURate) +} + func mustMarshalAndUnmarshalRuleOp(re *require.Assertions, ruleOp *RuleOp) *RuleOp { ruleOpJSON, err := json.Marshal(ruleOp) re.NoError(err) diff --git a/go.mod b/go.mod index a2e38a76365..3a4b3848bdb 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/time v0.5.0 - golang.org/x/tools v0.38.0 + golang.org/x/tools v0.41.0 google.golang.org/grpc v1.75.1 gotest.tools/gotestsum v1.7.0 ) @@ -227,17 +227,17 @@ require ( go.uber.org/fx v1.12.0 // indirect go.uber.org/multierr v1.11.0 golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect diff --git a/go.sum b/go.sum index f7a3198b6fe..e89fe28e84e 100644 --- a/go.sum +++ b/go.sum @@ -711,8 +711,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 h1:QLureRX3moex6NVu/Lr4MGakp9FdA7sBHGBmvRW7NaM= golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= @@ -734,8 +734,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -769,8 +769,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -788,8 +788,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -834,11 +834,11 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -850,8 +850,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -863,8 +863,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -895,8 +895,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/metrics/grafana/pd.json b/metrics/grafana/pd.json index 00cd5cdb040..be6295a5dc8 100644 --- a/metrics/grafana/pd.json +++ b/metrics/grafana/pd.json @@ -6641,6 +6641,118 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_TEST-CLUSTER}", + "decimals": 0, + "fill": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 608, + "legend": { + "alignAsTable": true, + "avg": false, + "current": true, + "hideEmpty": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "pd_scheduler_store_status{k8s_cluster=\"$k8s_cluster\", tidb_cluster=\"$tidb_cluster\", store=~\"$store\", type=\"store_read_cpu_usage\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{address}}-store-{{store}}", + "refId": "A", + "step": 4 + }, + { + "exemplar": true, + "expr": "pd_scheduler_hot_peers_summary{type=\"exp-cpu-rate-read-leader\"}", + "hide": true, + "interval": "", + "legendFormat": "exp-cpu-rate-read-leader-{{store}}", + "refId": "B" + }, + { + "exemplar": true, + "expr": "pd_scheduler_hot_peers_summary{type=\"exp-cpu-rate-read-region\"}", + "hide": true, + "interval": "", + "legendFormat": "exp-cpu-rate-read-region-{{store}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Store read cpu", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, diff --git a/pkg/core/factory.go b/pkg/core/factory.go index 0537cae5b93..8849f6354e0 100644 --- a/pkg/core/factory.go +++ b/pkg/core/factory.go @@ -45,4 +45,6 @@ var ( TimeIntervalFactory = func() *pdpb.TimeInterval { return &pdpb.TimeInterval{} } // QueryStatsFactory returns new query stats. QueryStatsFactory = func() *pdpb.QueryStats { return &pdpb.QueryStats{} } + // CPUStatsFactory returns new cpu stats. + CPUStatsFactory = func() *pdpb.CPUStats { return &pdpb.CPUStats{} } ) diff --git a/pkg/core/region.go b/pkg/core/region.go index 7d0e6418f74..7bd098146b0 100644 --- a/pkg/core/region.go +++ b/pkg/core/region.go @@ -65,15 +65,18 @@ func errRegionIsStale(region *metapb.Region, origin *metapb.Region) error { // the properties are Read-Only once created except buckets. // the `buckets` could be modified by the request `report buckets` with greater version. type RegionInfo struct { - meta *metapb.Region - learners []*metapb.Peer - witnesses []*metapb.Peer - voters []*metapb.Peer - leader *metapb.Peer - downPeers []*pdpb.PeerStats - pendingPeers []*metapb.Peer - term uint64 + meta *metapb.Region + learners []*metapb.Peer + witnesses []*metapb.Peer + voters []*metapb.Peer + leader *metapb.Peer + downPeers []*pdpb.PeerStats + pendingPeers []*metapb.Peer + term uint64 + // cpuUsage is deprecated and will be removed in the future. + // We should use `cpuStats` instead. cpuUsage uint64 + cpuStats *pdpb.CPUStats writtenBytes uint64 writtenKeys uint64 readBytes uint64 @@ -257,11 +260,8 @@ func RegionFromHeartbeat(heartbeat RegionHeartbeatRequest, flowRoundDivisor uint region.approximateKvSize = int64(h.GetApproximateKvSize() / units.MiB) region.approximateColumnarKvSize = int64(h.GetApproximateColumnarKvSize() / units.MiB) region.replicationStatus = h.GetReplicationStatus() - if cpuStats := h.GetCpuStats(); cpuStats != nil { - region.cpuUsage = cpuStats.GetUnifiedRead() - } else { - region.cpuUsage = h.CpuUsage - } + region.cpuStats = h.GetCpuStats() + region.cpuUsage = h.CpuUsage } if region.writtenKeys >= ImpossibleFlowSize || region.writtenBytes >= ImpossibleFlowSize { @@ -328,6 +328,7 @@ func (r *RegionInfo) Clone(opts ...RegionCreateOption) *RegionInfo { downPeers: downPeers, pendingPeers: pendingPeers, cpuUsage: r.cpuUsage, + cpuStats: typeutil.DeepClone(r.cpuStats, CPUStatsFactory), writtenBytes: r.writtenBytes, writtenKeys: r.writtenKeys, readBytes: r.readBytes, @@ -743,13 +744,19 @@ func (r *RegionInfo) GetPendingPeers() []*metapb.Peer { // GetCPUUsage returns the CPU usage of the region since the last heartbeat. // The number range is [0, N * 100], where N is the number of CPU cores. -// However, since the TiKV basically only meters the CPU usage inside the -// Unified Read Pool, it should be considered as an indicator of Region read -// CPU overhead for now. +// This is kept for compatibility with the legacy region heartbeat cpu_usage field. func (r *RegionInfo) GetCPUUsage() uint64 { return r.cpuUsage } +// GetReadCPUUsage returns the region-level unified-read CPU usage reported in cpu_stats. +func (r *RegionInfo) GetReadCPUUsage() uint64 { + if r.cpuStats == nil { + return 0 + } + return r.cpuStats.GetUnifiedRead() +} + // GetBytesRead returns the read bytes of the region. func (r *RegionInfo) GetBytesRead() uint64 { return r.readBytes @@ -2035,6 +2042,8 @@ func (r *RegionInfo) GetLoads() []float64 { float64(r.GetBytesWritten()), float64(r.GetKeysWritten()), float64(r.GetWriteQueryNum()), + float64(r.GetReadCPUUsage()), + 0, // RegionWriteCPU: reserved, not yet reported by TiKV } } @@ -2047,6 +2056,8 @@ func (r *RegionInfo) GetWriteLoads() []float64 { float64(r.GetBytesWritten()), float64(r.GetKeysWritten()), float64(r.GetWriteQueryNum()), + 0, + 0, // RegionWriteCPU: reserved, not yet reported by TiKV } } diff --git a/pkg/mcs/scheduling/server/cluster.go b/pkg/mcs/scheduling/server/cluster.go index 84a6812c0d6..ba96ec496bf 100644 --- a/pkg/mcs/scheduling/server/cluster.go +++ b/pkg/mcs/scheduling/server/cluster.go @@ -605,6 +605,7 @@ func (c *Cluster) HandleStoreHeartbeat(heartbeat *schedulingpb.StoreHeartbeatReq continue } readQueryNum := core.GetReadQueryNum(peerStat.GetQueryStats()) + regionReadCPU := statistics.RegionReadCPUUsage(peerStat) loads := []float64{ utils.RegionReadBytes: float64(peerStat.GetReadBytes()), utils.RegionReadKeys: float64(peerStat.GetReadKeys()), @@ -612,6 +613,8 @@ func (c *Cluster) HandleStoreHeartbeat(heartbeat *schedulingpb.StoreHeartbeatReq utils.RegionWriteBytes: 0, utils.RegionWriteKeys: 0, utils.RegionWriteQueryNum: 0, + utils.RegionReadCPU: regionReadCPU * float64(interval), + utils.RegionWriteCPU: 0, } checkReadPeerTask := func(cache *statistics.HotPeerCache) { stats := cache.CheckPeerFlow(region, []*metapb.Peer{peer}, loads, interval) diff --git a/pkg/schedule/coordinator.go b/pkg/schedule/coordinator.go index 06f98e5cb51..bc2b3761790 100644 --- a/pkg/schedule/coordinator.go +++ b/pkg/schedule/coordinator.go @@ -528,7 +528,7 @@ func (c *Coordinator) CollectHotSpotMetrics() { func collectHotMetrics(cluster sche.ClusterInformer, stores []*core.StoreInfo, typ utils.RWType) { kind := typ.String() hotPeerStats := cluster.GetHotPeerStats(typ) - status := statistics.CollectHotPeerInfos(stores, hotPeerStats) // only returns TotalBytesRate,TotalKeysRate,TotalQueryRate,Count + status := statistics.CollectHotPeerInfos(stores, hotPeerStats) // only returns TotalBytesRate,TotalKeysRate,TotalQueryRate,TotalCPURate,Count for _, s := range stores { // TODO: pre-allocate gauge metrics @@ -540,11 +540,13 @@ func collectHotMetrics(cluster sche.ClusterInformer, stores []*core.StoreInfo, t hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_bytes_as_leader").Set(stat.TotalBytesRate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_keys_as_leader").Set(stat.TotalKeysRate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_query_as_leader").Set(stat.TotalQueryRate) + hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_cpu_as_leader").Set(stat.TotalCPURate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "hot_"+kind+"_region_as_leader").Set(float64(stat.Count)) } else { hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_bytes_as_leader") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_keys_as_leader") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_query_as_leader") + hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_cpu_as_leader") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "hot_"+kind+"_region_as_leader") } @@ -553,11 +555,13 @@ func collectHotMetrics(cluster sche.ClusterInformer, stores []*core.StoreInfo, t hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_bytes_as_peer").Set(stat.TotalBytesRate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_keys_as_peer").Set(stat.TotalKeysRate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_query_as_peer").Set(stat.TotalQueryRate) + hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "total_"+kind+"_cpu_as_peer").Set(stat.TotalCPURate) hotSpotStatusGauge.WithLabelValues(storeAddress, storeLabel, "hot_"+kind+"_region_as_peer").Set(float64(stat.Count)) } else { hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_bytes_as_peer") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_keys_as_peer") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_query_as_peer") + hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "total_"+kind+"_cpu_as_peer") hotSpotStatusGauge.DeleteLabelValues(storeAddress, storeLabel, "hot_"+kind+"_region_as_peer") } diff --git a/pkg/schedule/handler/handler.go b/pkg/schedule/handler/handler.go index 2cdabc0b403..a42c773ed2c 100644 --- a/pkg/schedule/handler/handler.go +++ b/pkg/schedule/handler/handler.go @@ -994,6 +994,7 @@ type HotStoreStats struct { KeysReadStats map[uint64]float64 `json:"keys-read-rate,omitempty"` QueryWriteStats map[uint64]float64 `json:"query-write-rate,omitempty"` QueryReadStats map[uint64]float64 `json:"query-read-rate,omitempty"` + CPUReadStats map[uint64]float64 `json:"cpu-read-rate,omitempty"` } // GetHotStores gets all hot stores stats. @@ -1005,6 +1006,7 @@ func (h *Handler) GetHotStores() (*HotStoreStats, error) { KeysReadStats: make(map[uint64]float64), QueryWriteStats: make(map[uint64]float64), QueryReadStats: make(map[uint64]float64), + CPUReadStats: make(map[uint64]float64), } stores, error := h.GetStores() if error != nil { @@ -1031,6 +1033,7 @@ func (h *Handler) GetHotStores() (*HotStoreStats, error) { stats.KeysReadStats[id] = loads[utils.StoreReadKeys] stats.QueryWriteStats[id] = loads[utils.StoreWriteQuery] stats.QueryReadStats[id] = loads[utils.StoreReadQuery] + stats.CPUReadStats[id] = loads[utils.StoreReadCPU] } } return stats, nil diff --git a/pkg/schedule/schedulers/hot_region.go b/pkg/schedule/schedulers/hot_region.go index 4efd3d0d8d1..4ee36834614 100644 --- a/pkg/schedule/schedulers/hot_region.go +++ b/pkg/schedule/schedulers/hot_region.go @@ -41,6 +41,7 @@ const ( splitProgressiveRank = 5 minHotScheduleInterval = time.Second maxHotScheduleInterval = 20 * time.Second + defaultPendingWeight = 1.0 defaultPendingAmpFactor = 2.0 defaultStddevThreshold = 0.1 defaultTopnPosition = 10 @@ -142,10 +143,25 @@ func (s *baseHotScheduler) updateHistoryLoadConfig(sampleDuration, sampleInterva s.stHistoryLoads = s.stHistoryLoads.UpdateConfig(sampleDuration, sampleInterval) } +func (s *baseHotScheduler) getEffectivePendingWeight() float64 { + if conf, ok := s.conf.(*hotRegionSchedulerConfig); ok { + pendingWeight := conf.getPendingWeight() + if pendingWeight < defaultPendingWeight { + log.Warn("pending-weight is less than default, fallback to default", + zap.Float64("pendingWeight", pendingWeight), + zap.Float64("defaultPendingWeight", defaultPendingWeight)) + return defaultPendingWeight + } + return pendingWeight + } + return defaultPendingWeight +} + // summaryPendingInfluence calculate the summary of pending Influence for each store // and clean the region from regionInfluence if they have ended operator. // It makes each dim rate or count become `weight` times to the origin value. func (s *baseHotScheduler) summaryPendingInfluence(storeInfos map[uint64]*statistics.StoreSummaryInfo) { + pendingWeight := s.getEffectivePendingWeight() for id, p := range s.regionPendings { for _, from := range p.froms { from := storeInfos[from] @@ -158,6 +174,7 @@ func (s *baseHotScheduler) summaryPendingInfluence(storeInfos map[uint64]*statis continue } + weight *= pendingWeight if from != nil && weight > 0 { from.AddInfluence(&p.origin, -weight) } @@ -212,18 +229,29 @@ func (s *hotScheduler) ReloadConfig() error { s.conf.Lock() defer s.conf.Unlock() - newCfg := &hotRegionSchedulerConfig{} + newCfg := &hotRegionSchedulerConfig{ + hotRegionSchedulerParam: s.conf.hotRegionSchedulerParam, + } + newCfg.ReadPriorities = append([]string(nil), s.conf.ReadPriorities...) + newCfg.WriteLeaderPriorities = append([]string(nil), s.conf.WriteLeaderPriorities...) + newCfg.WritePeerPriorities = append([]string(nil), s.conf.WritePeerPriorities...) if err := s.conf.load(newCfg); err != nil { return err } + if err := newCfg.validateLocked(); err != nil { + return err + } s.conf.MinHotByteRate = newCfg.MinHotByteRate s.conf.MinHotKeyRate = newCfg.MinHotKeyRate s.conf.MinHotQueryRate = newCfg.MinHotQueryRate + s.conf.MinHotCPURate = newCfg.MinHotCPURate s.conf.MaxZombieRounds = newCfg.MaxZombieRounds + s.conf.PendingWeight = newCfg.PendingWeight s.conf.MaxPeerNum = newCfg.MaxPeerNum s.conf.ByteRateRankStepRatio = newCfg.ByteRateRankStepRatio s.conf.KeyRateRankStepRatio = newCfg.KeyRateRankStepRatio s.conf.QueryRateRankStepRatio = newCfg.QueryRateRankStepRatio + s.conf.CPURateRankStepRatio = newCfg.CPURateRankStepRatio s.conf.CountRankStepRatio = newCfg.CountRankStepRatio s.conf.GreatDecRatio = newCfg.GreatDecRatio s.conf.MinorDecRatio = newCfg.MinorDecRatio diff --git a/pkg/schedule/schedulers/hot_region_config.go b/pkg/schedule/schedulers/hot_region_config.go index 0ea9df4740a..659e6830d75 100644 --- a/pkg/schedule/schedulers/hot_region_config.go +++ b/pkg/schedule/schedulers/hot_region_config.go @@ -44,6 +44,14 @@ const ( ) var defaultPrioritiesConfig = prioritiesConfig{ + // Keep CPU-based hot-read scheduling opt-in until it has been validated in canary clusters. + read: []string{utils.QueryPriority, utils.BytePriority}, + writeLeader: []string{utils.QueryPriority, utils.BytePriority}, + writePeer: []string{utils.BytePriority, utils.KeyPriority}, +} + +// because tikv <= 8.5.6 and < 9.0.0-beta.1 does not report cpu information, we will use query and byte as the read priorities +var queryPrioritiesConfig = prioritiesConfig{ read: []string{utils.QueryPriority, utils.BytePriority}, writeLeader: []string{utils.QueryPriority, utils.BytePriority}, writePeer: []string{utils.BytePriority, utils.KeyPriority}, @@ -64,11 +72,14 @@ func initHotRegionScheduleConfig() *hotRegionSchedulerConfig { MinHotByteRate: 100, MinHotKeyRate: 10, MinHotQueryRate: 10, + MinHotCPURate: 10, MaxZombieRounds: 3, + PendingWeight: 1, MaxPeerNum: 1000, ByteRateRankStepRatio: 0.05, KeyRateRankStepRatio: 0.05, QueryRateRankStepRatio: 0.05, + CPURateRankStepRatio: 0.05, CountRankStepRatio: 0.01, GreatDecRatio: 0.95, MinorDecRatio: 0.99, @@ -92,19 +103,22 @@ func (conf *hotRegionSchedulerConfig) getValidConf() *hotRegionSchedulerParam { MinHotByteRate: conf.MinHotByteRate, MinHotKeyRate: conf.MinHotKeyRate, MinHotQueryRate: conf.MinHotQueryRate, + MinHotCPURate: conf.MinHotCPURate, MaxZombieRounds: conf.MaxZombieRounds, + PendingWeight: conf.PendingWeight, MaxPeerNum: conf.MaxPeerNum, ByteRateRankStepRatio: conf.ByteRateRankStepRatio, KeyRateRankStepRatio: conf.KeyRateRankStepRatio, QueryRateRankStepRatio: conf.QueryRateRankStepRatio, + CPURateRankStepRatio: conf.CPURateRankStepRatio, CountRankStepRatio: conf.CountRankStepRatio, GreatDecRatio: conf.GreatDecRatio, MinorDecRatio: conf.MinorDecRatio, SrcToleranceRatio: conf.SrcToleranceRatio, DstToleranceRatio: conf.DstToleranceRatio, - ReadPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.ReadPriorities, getReadPriorities), - WriteLeaderPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.WriteLeaderPriorities, getWriteLeaderPriorities), - WritePeerPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.WritePeerPriorities, getWritePeerPriorities), + ReadPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.lastCPUSupported, conf.ReadPriorities, getReadPriorities), + WriteLeaderPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.lastCPUSupported, conf.WriteLeaderPriorities, getWriteLeaderPriorities), + WritePeerPriorities: adjustPrioritiesConfig(conf.lastQuerySupported, conf.lastCPUSupported, conf.WritePeerPriorities, getWritePeerPriorities), StrictPickingStore: conf.StrictPickingStore, EnableForTiFlash: conf.EnableForTiFlash, RankFormulaVersion: conf.getRankFormulaVersionLocked(), @@ -119,7 +133,9 @@ type hotRegionSchedulerParam struct { MinHotByteRate float64 `json:"min-hot-byte-rate"` MinHotKeyRate float64 `json:"min-hot-key-rate"` MinHotQueryRate float64 `json:"min-hot-query-rate"` + MinHotCPURate float64 `json:"min-hot-cpu-rate"` MaxZombieRounds int `json:"max-zombie-rounds"` + PendingWeight float64 `json:"pending-weight"` MaxPeerNum int `json:"max-peer-number"` // rank step ratio decide the step when calculate rank @@ -127,6 +143,7 @@ type hotRegionSchedulerParam struct { ByteRateRankStepRatio float64 `json:"byte-rate-rank-step-ratio"` KeyRateRankStepRatio float64 `json:"key-rate-rank-step-ratio"` QueryRateRankStepRatio float64 `json:"query-rate-rank-step-ratio"` + CPURateRankStepRatio float64 `json:"cpu-rate-rank-step-ratio"` CountRankStepRatio float64 `json:"count-rank-step-ratio"` GreatDecRatio float64 `json:"great-dec-ratio"` MinorDecRatio float64 `json:"minor-dec-ratio"` // only for v1 @@ -161,6 +178,7 @@ type hotRegionSchedulerConfig struct { hotRegionSchedulerParam lastQuerySupported bool + lastCPUSupported bool } func (conf *hotRegionSchedulerConfig) encodeConfig() ([]byte, error) { @@ -175,6 +193,12 @@ func (conf *hotRegionSchedulerConfig) getStoreStatZombieDuration() time.Duration return time.Duration(conf.MaxZombieRounds*utils.StoreHeartBeatReportInterval) * time.Second } +func (conf *hotRegionSchedulerConfig) getPendingWeight() float64 { + conf.RLock() + defer conf.RUnlock() + return conf.PendingWeight +} + func (conf *hotRegionSchedulerConfig) getRegionsStatZombieDuration() time.Duration { conf.RLock() defer conf.RUnlock() @@ -229,6 +253,12 @@ func (conf *hotRegionSchedulerConfig) getQueryRateRankStepRatio() float64 { return conf.QueryRateRankStepRatio } +func (conf *hotRegionSchedulerConfig) getCPURateRankStepRatio() float64 { + conf.RLock() + defer conf.RUnlock() + return conf.CPURateRankStepRatio +} + func (conf *hotRegionSchedulerConfig) getCountRankStepRatio() float64 { conf.RLock() defer conf.RUnlock() @@ -283,6 +313,12 @@ func (conf *hotRegionSchedulerConfig) getMinHotQueryRate() float64 { return conf.MinHotQueryRate } +func (conf *hotRegionSchedulerConfig) getMinHotCPURate() float64 { + conf.RLock() + defer conf.RUnlock() + return conf.MinHotCPURate +} + func (conf *hotRegionSchedulerConfig) getReadPriorities() []string { conf.RLock() defer conf.RUnlock() @@ -393,7 +429,7 @@ func (conf *hotRegionSchedulerConfig) handleGetConfig(w http.ResponseWriter, _ * func isPriorityValid(priorities []string) (map[string]bool, error) { priorityMap := map[string]bool{} for _, p := range priorities { - if p != utils.BytePriority && p != utils.KeyPriority && p != utils.QueryPriority { + if p != utils.BytePriority && p != utils.KeyPriority && p != utils.QueryPriority && p != utils.CPUPriority { return nil, errs.ErrSchedulerConfig.FastGenByArgs("invalid scheduling dimensions") } priorityMap[p] = true @@ -411,11 +447,15 @@ func (conf *hotRegionSchedulerParam) validateLocked() error { if _, err := isPriorityValid(conf.ReadPriorities); err != nil { return err } - if _, err := isPriorityValid(conf.WriteLeaderPriorities); err != nil { + if pm, err := isPriorityValid(conf.WriteLeaderPriorities); err != nil { return err + } else if pm[utils.CPUPriority] { + return errs.ErrSchedulerConfig.FastGenByArgs("cpu is not allowed to be set in priorities for write-leader-priorities") } if pm, err := isPriorityValid(conf.WritePeerPriorities); err != nil { return err + } else if pm[utils.CPUPriority] { + return errs.ErrSchedulerConfig.FastGenByArgs("cpu is not allowed to be set in priorities for write-peer-priorities") } else if pm[utils.QueryPriority] { return errs.ErrSchedulerConfig.FastGenByArgs("query is not allowed to be set in priorities for write-peer-priorities") } @@ -428,6 +468,9 @@ func (conf *hotRegionSchedulerParam) validateLocked() error { conf.ForbidRWType != "none" && conf.ForbidRWType != "" { return errs.ErrSchedulerConfig.FastGenByArgs("invalid forbid-rw-type") } + if conf.PendingWeight < defaultPendingWeight { + return errs.ErrSchedulerConfig.FastGenByArgs("invalid pending-weight, should be at least 1") + } if conf.SplitThresholds < 0.01 || conf.SplitThresholds > 1.0 { return errs.ErrSchedulerConfig.FastGenByArgs("invalid split-thresholds, should be in range [0.01, 1.0]") } @@ -506,6 +549,22 @@ func (conf *hotRegionSchedulerConfig) checkQuerySupport(cluster sche.SchedulerCl return querySupport } +func (conf *hotRegionSchedulerConfig) checkCPUSupport(cluster sche.SchedulerCluster) bool { + version := cluster.GetSchedulerConfig().GetClusterVersion() + cpuSupport := versioninfo.IsHotScheduleWithCPUSupported(version) + conf.Lock() + defer conf.Unlock() + if cpuSupport != conf.lastCPUSupported { + log.Info("cpu supported changed", + zap.Bool("last-cpu-support", conf.lastCPUSupported), + zap.String("cluster-version", version.String()), + zap.Reflect("config", conf), + zap.Reflect("valid-config", conf.getValidConf())) + conf.lastCPUSupported = cpuSupport + } + return cpuSupport +} + type prioritiesConfig struct { read []string writeLeader []string @@ -530,20 +589,34 @@ func getWritePeerPriorities(c *prioritiesConfig) []string { return c.writePeer } -// adjustPrioritiesConfig will adjust config for cluster with low version tikv -// because tikv below 5.2.0 does not report query information, we will use byte and key as the scheduling dimensions -func adjustPrioritiesConfig(querySupport bool, origins []string, getPriorities func(*prioritiesConfig) []string) []string { +// adjustPrioritiesConfig will adjust config for cluster with low version tikv. +// because tikv below 5.2.0 does not report query information, we will use byte and key as the scheduling dimensions. +// because tikv <= 8.5.6 and < 9.0.0-beta.1 does not report cpu information, we will use query and byte as the read priorities. +func adjustPrioritiesConfig(querySupport, cpuSupport bool, origins []string, getPriorities func(*prioritiesConfig) []string) []string { withQuery := slice.AnyOf(origins, func(i int) bool { return origins[i] == utils.QueryPriority }) + withCPU := slice.AnyOf(origins, func(i int) bool { + return origins[i] == utils.CPUPriority + }) compatibles := getPriorities(&compatiblePrioritiesConfig) - if !querySupport && withQuery { + if !querySupport && (withQuery || withCPU) { return compatibles } + cpuFallback := getPriorities(&queryPrioritiesConfig) + if !cpuSupport && withCPU { + return cpuFallback + } + defaults := getPriorities(&defaultPrioritiesConfig) + // When CPU is unavailable, malformed or legacy configs should fall back to the + // query-based read defaults instead of silently selecting CPU-first priorities. + if !cpuSupport { + defaults = cpuFallback + } isLegal := slice.AllOf(origins, func(i int) bool { - return origins[i] == utils.BytePriority || origins[i] == utils.KeyPriority || origins[i] == utils.QueryPriority + return origins[i] == utils.BytePriority || origins[i] == utils.KeyPriority || origins[i] == utils.QueryPriority || origins[i] == utils.CPUPriority }) if len(defaults) == len(origins) && isLegal && origins[0] != origins[1] { return origins diff --git a/pkg/schedule/schedulers/hot_region_config_test.go b/pkg/schedule/schedulers/hot_region_config_test.go new file mode 100644 index 00000000000..e22a20cabe3 --- /dev/null +++ b/pkg/schedule/schedulers/hot_region_config_test.go @@ -0,0 +1,82 @@ +// Copyright 2026 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package schedulers + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tikv/pd/pkg/statistics/utils" +) + +func TestAdjustPrioritiesConfigCPUFallback(t *testing.T) { + re := require.New(t) + tests := []struct { + name string + querySupport bool + cpuSupport bool + origins []string + expect []string + }{ + { + name: "cpu-supported-keeps-manual-opt-in", + querySupport: true, + cpuSupport: true, + origins: []string{utils.CPUPriority, utils.BytePriority}, + expect: []string{utils.CPUPriority, utils.BytePriority}, + }, + { + name: "cpu-unsupported-fallback-to-query", + querySupport: true, + cpuSupport: false, + origins: []string{utils.CPUPriority, utils.BytePriority}, + expect: getReadPriorities(&queryPrioritiesConfig), + }, + { + name: "cpu-unsupported-keep-query-origins", + querySupport: true, + cpuSupport: false, + origins: []string{utils.QueryPriority, utils.BytePriority}, + expect: []string{utils.QueryPriority, utils.BytePriority}, + }, + { + name: "cpu-supported-invalid-config-uses-safe-default", + querySupport: true, + cpuSupport: true, + origins: []string{"invalid"}, + expect: []string{utils.QueryPriority, utils.BytePriority}, + }, + { + name: "query-unsupported-fallback-to-compatible", + querySupport: false, + cpuSupport: true, + origins: []string{utils.CPUPriority, utils.BytePriority}, + expect: getReadPriorities(&compatiblePrioritiesConfig), + }, + { + name: "query-unsupported-with-query-origins", + querySupport: false, + cpuSupport: false, + origins: []string{utils.QueryPriority, utils.BytePriority}, + expect: getReadPriorities(&compatiblePrioritiesConfig), + }, + } + + for _, test := range tests { + got := adjustPrioritiesConfig(test.querySupport, test.cpuSupport, test.origins, getReadPriorities) + re.Equal(test.expect, got, test.name) + } +} diff --git a/pkg/schedule/schedulers/hot_region_solver.go b/pkg/schedule/schedulers/hot_region_solver.go index e62fe7da578..5750cf2a31f 100644 --- a/pkg/schedule/schedulers/hot_region_solver.go +++ b/pkg/schedule/schedulers/hot_region_solver.go @@ -110,7 +110,9 @@ func (bs *balanceSolver) init() { rankStepRatios := []float64{ utils.ByteDim: bs.sche.conf.getByteRankStepRatio(), utils.KeyDim: bs.sche.conf.getKeyRankStepRatio(), - utils.QueryDim: bs.sche.conf.getQueryRateRankStepRatio()} + utils.QueryDim: bs.sche.conf.getQueryRateRankStepRatio(), + utils.CPUDim: bs.sche.conf.getCPURateRankStepRatio(), + } var stepLoads statistics.Loads for i := range stepLoads { stepLoads[i] = maxCur.Loads[i] * rankStepRatios[i] @@ -127,15 +129,16 @@ func (bs *balanceSolver) isSelectedDim(dim int) bool { func (bs *balanceSolver) getPriorities() []string { querySupport := bs.sche.conf.checkQuerySupport(bs.SchedulerCluster) + cpuSupport := bs.sche.conf.checkCPUSupport(bs.SchedulerCluster) // For read, transfer-leader and move-peer have the same priority config // For write, they are different switch bs.resourceTy { case readLeader, readPeer: - return adjustPrioritiesConfig(querySupport, bs.sche.conf.getReadPriorities(), getReadPriorities) + return adjustPrioritiesConfig(querySupport, cpuSupport, bs.sche.conf.getReadPriorities(), getReadPriorities) case writeLeader: - return adjustPrioritiesConfig(querySupport, bs.sche.conf.getWriteLeaderPriorities(), getWriteLeaderPriorities) + return adjustPrioritiesConfig(querySupport, cpuSupport, bs.sche.conf.getWriteLeaderPriorities(), getWriteLeaderPriorities) case writePeer: - return adjustPrioritiesConfig(querySupport, bs.sche.conf.getWritePeerPriorities(), getWritePeerPriorities) + return adjustPrioritiesConfig(querySupport, cpuSupport, bs.sche.conf.getWritePeerPriorities(), getWritePeerPriorities) } log.Error("illegal type or illegal operator while getting the priority", zap.String("type", bs.rwTy.String()), zap.String("operator", bs.opTy.String())) return []string{} @@ -262,6 +265,8 @@ func (bs *balanceSolver) skipCounter(label string) prometheus.Counter { return readSkipKeyDimUniformStoreCounter case "query": return readSkipQueryDimUniformStoreCounter + case "cpu": + return readSkipCPUDimUniformStoreCounter default: return readSkipAllDimUniformStoreCounter } @@ -746,6 +751,8 @@ func (bs *balanceSolver) getMinRate(dim int) float64 { return bs.sche.conf.getMinHotByteRate() case utils.QueryDim: return bs.sche.conf.getMinHotQueryRate() + case utils.CPUDim: + return bs.sche.conf.getMinHotCPURate() } return -1 } @@ -754,6 +761,7 @@ var dimToStep = [utils.DimLen]float64{ utils.ByteDim: 100, utils.KeyDim: 10, utils.QueryDim: 10, + utils.CPUDim: 10, } // compareSrcStore compares the source store of detail1, detail2, the result is: @@ -908,18 +916,21 @@ func (bs *balanceSolver) buildOperators() (ops []*operator.Operator) { } // bucketFirstStat returns the first priority statistics of the bucket. -// if the first priority is query rate, it will return the second priority . +// If the first priority is a dimension that buckets do not report, it falls +// back to another bucket-supported priority. func (bs *balanceSolver) bucketFirstStat() utils.RegionStatKind { - base := utils.RegionReadBytes - if bs.rwTy == utils.Write { - base = utils.RegionWriteBytes + dim := bs.firstPriority + if !isBucketLoadDimSupported(dim) { + dim = bs.secondPriority } - offset := bs.firstPriority - // todo: remove it if bucket's qps has been supported. - if bs.firstPriority == utils.QueryDim { - offset = bs.secondPriority + if !isBucketLoadDimSupported(dim) { + dim = utils.ByteDim } - return base + utils.RegionStatKind(offset) + return bs.rwTy.RegionStats()[dim] +} + +func isBucketLoadDimSupported(dim int) bool { + return dim == utils.ByteDim || dim == utils.KeyDim || dim == utils.QueryDim } func (bs *balanceSolver) splitBucketsOperator(region *core.RegionInfo, keys [][]byte) *operator.Operator { diff --git a/pkg/schedule/schedulers/hot_region_solver_test.go b/pkg/schedule/schedulers/hot_region_solver_test.go index eaf07376435..9a284c625b8 100644 --- a/pkg/schedule/schedulers/hot_region_solver_test.go +++ b/pkg/schedule/schedulers/hot_region_solver_test.go @@ -31,6 +31,7 @@ import ( "github.com/tikv/pd/pkg/statistics/buckets" "github.com/tikv/pd/pkg/statistics/utils" "github.com/tikv/pd/pkg/storage" + "github.com/tikv/pd/pkg/versioninfo" ) func TestSplitBucketsBySize(t *testing.T) { @@ -104,6 +105,9 @@ func TestSplitBucketsByLoad(t *testing.T) { defer cancel() hb, err := CreateScheduler(readType, oc, storage.NewStorageWithMemoryBackend(), nil) re.NoError(err) + // Explicitly use byte/key here so this test only verifies split-key selection + // instead of depending on the cluster-version-specific default priorities. + hb.(*hotScheduler).conf.ReadPriorities = []string{utils.BytePriority, utils.KeyPriority} solve := newBalanceSolver(hb.(*hotScheduler), tc, utils.Read, transferLeader) solve.cur = &solution{} region := core.NewTestRegionInfo(1, 1, []byte("a"), []byte("f")) @@ -156,6 +160,46 @@ func TestSplitBucketsByLoad(t *testing.T) { } } +func TestSplitBucketsByLoadWithCPUFallback(t *testing.T) { + re := require.New(t) + cancel, _, tc, oc := prepareSchedulersTest() + tc.SetRegionBucketEnabled(true) + tc.SetClusterVersion(versioninfo.MustParseVersion("8.5.7")) + defer cancel() + hb, err := CreateScheduler(readType, oc, storage.NewStorageWithMemoryBackend(), nil) + re.NoError(err) + hb.(*hotScheduler).conf.ReadPriorities = []string{utils.CPUPriority, utils.BytePriority} + solve := newBalanceSolver(hb.(*hotScheduler), tc, utils.Read, transferLeader) + solve.cur = &solution{} + region := core.NewTestRegionInfo(1, 1, []byte("a"), []byte("f")) + + b := &metapb.Buckets{ + RegionId: 1, + PeriodInMs: 1000, + Keys: [][]byte{[]byte(""), []byte("b"), []byte("d"), []byte("")}, + Stats: &metapb.BucketStats{ + ReadBytes: []uint64{2 * units.MiB, 2 * units.MiB, 20 * units.MiB}, + ReadKeys: []uint64{256, 256, 256}, + ReadQps: []uint64{1000, 1, 1}, + WriteBytes: []uint64{0, 0, 0}, + WriteQps: []uint64{0, 0, 0}, + WriteKeys: []uint64{0, 0, 0}, + }, + } + task := buckets.NewCheckPeerTask(b) + re.True(tc.CheckAsync(task)) + time.Sleep(time.Millisecond * 10) + + ops := solve.createSplitOperator([]*core.RegionInfo{region}, byLoad) + re.Len(ops, 1) + op := ops[0] + re.Equal(splitHotReadBuckets, op.Desc()) + + expectOp, err := operator.CreateSplitRegionOperator(splitHotReadBuckets, region, operator.OpSplit, pdpb.CheckPolicy_USEKEY, [][]byte{[]byte("d")}) + re.NoError(err) + re.Equal(expectOp.Brief(), op.Brief()) +} + func TestHotCacheSortHotPeer(t *testing.T) { re := require.New(t) cancel, _, tc, oc := prepareSchedulersTest() @@ -163,24 +207,28 @@ func TestHotCacheSortHotPeer(t *testing.T) { sche, err := CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), ConfigJSONDecoder([]byte("null"))) re.NoError(err) hb := sche.(*hotScheduler) + hb.conf.ReadPriorities = []string{utils.QueryPriority, utils.BytePriority} leaderSolver := newBalanceSolver(hb, tc, utils.Read, transferLeader) hotPeers := []*statistics.HotPeerStat{{ RegionID: 1, Loads: []float64{ utils.QueryDim: 10, utils.ByteDim: 1, + utils.CPUDim: 0, }, }, { RegionID: 2, Loads: []float64{ utils.QueryDim: 1, utils.ByteDim: 10, + utils.CPUDim: 0, }, }, { RegionID: 3, Loads: []float64{ utils.QueryDim: 5, utils.ByteDim: 6, + utils.CPUDim: 0, }, }} @@ -194,6 +242,30 @@ func TestHotCacheSortHotPeer(t *testing.T) { leaderSolver.maxPeerNum = 2 u = leaderSolver.filterHotPeers(st) checkSortResult(re, []uint64{1, 2}, u) + + // Verify the CPU-first priority path can pick by CPU dim. + tc.SetClusterVersion(versioninfo.MustParseVersion("8.5.7")) + hb.conf.ReadPriorities = []string{utils.CPUPriority, utils.BytePriority} + cpuLeaderSolver := newBalanceSolver(hb, tc, utils.Read, transferLeader) + cpuHotPeers := []*statistics.HotPeerStat{{ + RegionID: 1, + Loads: []float64{ + utils.QueryDim: 1, + utils.ByteDim: 1, + utils.CPUDim: 30, + }, + }, { + RegionID: 2, + Loads: []float64{ + utils.QueryDim: 100, + utils.ByteDim: 1, + utils.CPUDim: 5, + }, + }} + st.HotPeers = cpuHotPeers + cpuLeaderSolver.maxPeerNum = 1 + u = cpuLeaderSolver.filterHotPeers(st) + checkSortResult(re, []uint64{1}, u) } func checkSortResult(re *require.Assertions, regions []uint64, hotPeers []*statistics.HotPeerStat) { @@ -602,7 +674,7 @@ func TestBucketFirstStat(t *testing.T) { firstPriority: utils.QueryDim, secondPriority: utils.ByteDim, rwTy: utils.Write, - expect: utils.RegionWriteBytes, + expect: utils.RegionWriteQueryNum, }, { firstPriority: utils.KeyDim, @@ -614,6 +686,18 @@ func TestBucketFirstStat(t *testing.T) { firstPriority: utils.QueryDim, secondPriority: utils.ByteDim, rwTy: utils.Read, + expect: utils.RegionReadQueryNum, + }, + { + firstPriority: utils.CPUDim, + secondPriority: utils.QueryDim, + rwTy: utils.Read, + expect: utils.RegionReadQueryNum, + }, + { + firstPriority: utils.CPUDim, + secondPriority: utils.CPUDim, + rwTy: utils.Read, expect: utils.RegionReadBytes, }, } diff --git a/pkg/schedule/schedulers/hot_region_test.go b/pkg/schedule/schedulers/hot_region_test.go index 2c6f4da510f..204cf391420 100644 --- a/pkg/schedule/schedulers/hot_region_test.go +++ b/pkg/schedule/schedulers/hot_region_test.go @@ -41,6 +41,7 @@ import ( "github.com/tikv/pd/pkg/utils/testutil" "github.com/tikv/pd/pkg/utils/typeutil" "github.com/tikv/pd/pkg/versioninfo" + "github.com/tikv/pd/pkg/versioninfo/kerneltype" ) var ( @@ -93,6 +94,7 @@ func TestUpgrade(t *testing.T) { re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getReadPriorities()) re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getWriteLeaderPriorities()) re.Equal([]string{utils.BytePriority, utils.KeyPriority}, hb.conf.getWritePeerPriorities()) + re.Equal(defaultPendingWeight, hb.conf.getPendingWeight()) re.Equal("v2", hb.conf.getRankFormulaVersion()) // upgrade from json(null) sche, err = CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), ConfigJSONDecoder([]byte("null"))) @@ -101,6 +103,7 @@ func TestUpgrade(t *testing.T) { re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getReadPriorities()) re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getWriteLeaderPriorities()) re.Equal([]string{utils.BytePriority, utils.KeyPriority}, hb.conf.getWritePeerPriorities()) + re.Equal(defaultPendingWeight, hb.conf.getPendingWeight()) re.Equal("v2", hb.conf.getRankFormulaVersion()) // upgrade from < 5.2 config51 := `{"min-hot-byte-rate":100,"min-hot-key-rate":10,"min-hot-query-rate":10,"max-zombie-rounds":5,"max-peer-number":1000,"byte-rate-rank-step-ratio":0.05,"key-rate-rank-step-ratio":0.05,"query-rate-rank-step-ratio":0.05,"count-rank-step-ratio":0.01,"great-dec-ratio":0.95,"minor-dec-ratio":0.99,"src-tolerance-ratio":1.05,"dst-tolerance-ratio":1.05,"strict-picking-store":"true","enable-for-tiflash":"true"}` @@ -110,6 +113,7 @@ func TestUpgrade(t *testing.T) { re.Equal([]string{utils.BytePriority, utils.KeyPriority}, hb.conf.getReadPriorities()) re.Equal([]string{utils.KeyPriority, utils.BytePriority}, hb.conf.getWriteLeaderPriorities()) re.Equal([]string{utils.BytePriority, utils.KeyPriority}, hb.conf.getWritePeerPriorities()) + re.Equal(defaultPendingWeight, hb.conf.getPendingWeight()) re.Equal("v1", hb.conf.getRankFormulaVersion()) // upgrade from < 6.4 config54 := `{"min-hot-byte-rate":100,"min-hot-key-rate":10,"min-hot-query-rate":10,"max-zombie-rounds":5,"max-peer-number":1000,"byte-rate-rank-step-ratio":0.05,"key-rate-rank-step-ratio":0.05,"query-rate-rank-step-ratio":0.05,"count-rank-step-ratio":0.01,"great-dec-ratio":0.95,"minor-dec-ratio":0.99,"src-tolerance-ratio":1.05,"dst-tolerance-ratio":1.05,"read-priorities":["query","byte"],"write-leader-priorities":["query","byte"],"write-peer-priorities":["byte","key"],"strict-picking-store":"true","enable-for-tiflash":"true","forbid-rw-type":"none"}` @@ -119,9 +123,66 @@ func TestUpgrade(t *testing.T) { re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getReadPriorities()) re.Equal([]string{utils.QueryPriority, utils.BytePriority}, hb.conf.getWriteLeaderPriorities()) re.Equal([]string{utils.BytePriority, utils.KeyPriority}, hb.conf.getWritePeerPriorities()) + re.Equal(defaultPendingWeight, hb.conf.getPendingWeight()) re.Equal("v1", hb.conf.getRankFormulaVersion()) } +func TestReloadConfigKeepsPendingWeightDefault(t *testing.T) { + re := require.New(t) + cancel, _, _, oc := prepareSchedulersTest() + defer cancel() + + store := storage.NewStorageWithMemoryBackend() + data, err := EncodeConfig(map[string]any{ + "min-hot-byte-rate": 100, + "min-hot-key-rate": 10, + "min-hot-query-rate": 10, + "max-zombie-rounds": 5, + "max-peer-number": 1000, + "byte-rate-rank-step-ratio": 0.05, + "key-rate-rank-step-ratio": 0.05, + "query-rate-rank-step-ratio": 0.05, + "count-rank-step-ratio": 0.01, + "great-dec-ratio": 0.95, + "minor-dec-ratio": 0.99, + "src-tolerance-ratio": 1.05, + "dst-tolerance-ratio": 1.05, + }) + re.NoError(err) + re.NoError(store.SaveSchedulerConfig(types.BalanceHotRegionScheduler.String(), data)) + + sche, err := CreateScheduler(types.BalanceHotRegionScheduler, oc, store, ConfigJSONDecoder(data)) + re.NoError(err) + hb := sche.(*hotScheduler) + hb.conf.MaxZombieRounds = 9 + + re.NoError(hb.ReloadConfig()) + re.Equal(5, hb.conf.MaxZombieRounds) + re.Equal(defaultPendingWeight, hb.conf.getPendingWeight()) +} + +func TestCreateSchedulerRejectsPendingWeightBelowOne(t *testing.T) { + re := require.New(t) + cancel, _, _, oc := prepareSchedulersTest() + defer cancel() + + _, err := CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), + ConfigJSONDecoder([]byte(`{"pending-weight":0}`))) + re.Error(err) + + _, err = CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), + ConfigJSONDecoder([]byte(`{"pending-weight":0.5}`))) + re.Error(err) + + _, err = CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), + ConfigJSONDecoder([]byte(`{"pending-weight":-1}`))) + re.Error(err) + + _, err = CreateScheduler(types.BalanceHotRegionScheduler, oc, storage.NewStorageWithMemoryBackend(), + ConfigJSONDecoder([]byte(`{"pending-weight":1}`))) + re.NoError(err) +} + func TestGCPendingOpInfos(t *testing.T) { re := require.New(t) checkGCPendingOpInfos(re, false /* disable placement rules */) @@ -211,6 +272,7 @@ func TestSplitIfRegionTooHot(t *testing.T) { defer cancel() hb, err := CreateScheduler(readType, oc, storage.NewStorageWithMemoryBackend(), nil) re.NoError(err) + hb.(*hotScheduler).conf.ReadPriorities = []string{utils.BytePriority, utils.KeyPriority} b := &metapb.Buckets{ RegionId: 1, PeriodInMs: 1000, @@ -1280,6 +1342,7 @@ func TestHotReadRegionScheduleWithQuery(t *testing.T) { hb.(*hotScheduler).conf.setDstToleranceRatio(1) hb.(*hotScheduler).conf.RankFormulaVersion = "v1" hb.(*hotScheduler).conf.setHistorySampleDuration(0) + hb.(*hotScheduler).conf.ReadPriorities = []string{utils.QueryPriority, utils.BytePriority} tc.AddRegionStore(1, 20) tc.AddRegionStore(2, 20) @@ -2286,6 +2349,21 @@ func TestCompatibility(t *testing.T) { {utils.ByteDim, utils.KeyDim}, }) re.True(hb.(*hotScheduler).conf.lastQuerySupported) + re.Equal(kerneltype.IsNextGen(), hb.(*hotScheduler).conf.lastCPUSupported) + tc.SetClusterVersion(versioninfo.MustParseVersion("8.5.7")) + checkPriority(re, hb.(*hotScheduler), tc, [3][2]int{ + {utils.QueryDim, utils.ByteDim}, + {utils.QueryDim, utils.ByteDim}, + {utils.ByteDim, utils.KeyDim}, + }) + // CPU support alone does not change the default. Operators must explicitly opt in. + hb.(*hotScheduler).conf.ReadPriorities = []string{utils.CPUPriority, utils.BytePriority} + checkPriority(re, hb.(*hotScheduler), tc, [3][2]int{ + {utils.CPUDim, utils.ByteDim}, + {utils.QueryDim, utils.ByteDim}, + {utils.ByteDim, utils.KeyDim}, + }) + re.True(hb.(*hotScheduler).conf.lastCPUSupported) } func TestCompatibilityConfig(t *testing.T) { @@ -2391,6 +2469,16 @@ func TestConfigValidation(t *testing.T) { hc.WritePeerPriorities = []string{"query", "byte"} err = hc.validateLocked() re.Error(err) + // cpu is not allowed to be set in priorities for write-leader-priorities + hc = initHotRegionScheduleConfig() + hc.WriteLeaderPriorities = []string{"cpu", "byte"} + err = hc.validateLocked() + re.Error(err) + // cpu is not allowed to be set in priorities for write-peer-priorities + hc = initHotRegionScheduleConfig() + hc.WritePeerPriorities = []string{"cpu", "byte"} + err = hc.validateLocked() + re.Error(err) // priorities shouldn't be repeated hc.WritePeerPriorities = []string{"byte", "byte"} err = hc.validateLocked() @@ -2441,6 +2529,23 @@ func TestConfigValidation(t *testing.T) { err = hc.validateLocked() re.Error(err) + hc = initHotRegionScheduleConfig() + hc.PendingWeight = 0 + err = hc.validateLocked() + re.Error(err) + + hc.PendingWeight = -1 + err = hc.validateLocked() + re.Error(err) + + hc.PendingWeight = 0.5 + err = hc.validateLocked() + re.Error(err) + + hc.PendingWeight = defaultPendingWeight + err = hc.validateLocked() + re.NoError(err) + hc.SplitThresholds = 0 err = hc.validateLocked() re.Error(err) diff --git a/pkg/schedule/schedulers/init.go b/pkg/schedule/schedulers/init.go index 1d4b564db71..62b8b2aaa39 100644 --- a/pkg/schedule/schedulers/init.go +++ b/pkg/schedule/schedulers/init.go @@ -291,6 +291,9 @@ func schedulersRegister() { return nil, err } } + if err := conf.validateLocked(); err != nil { + return nil, err + } sche := newHotScheduler(opController, conf) conf.init(sche.GetName(), storage, conf) return sche, nil diff --git a/pkg/schedule/schedulers/metrics.go b/pkg/schedule/schedulers/metrics.go index 39b2077affa..9f5f7f1afc6 100644 --- a/pkg/schedule/schedulers/metrics.go +++ b/pkg/schedule/schedulers/metrics.go @@ -327,6 +327,7 @@ var ( writeSkipKeyDimUniformStoreCounter = hotRegionCounterWithEvent("write-skip-key-uniform-store") readSkipQueryDimUniformStoreCounter = hotRegionCounterWithEvent("read-skip-query-uniform-store") writeSkipQueryDimUniformStoreCounter = hotRegionCounterWithEvent("write-skip-query-uniform-store") + readSkipCPUDimUniformStoreCounter = hotRegionCounterWithEvent("read-skip-cpu-uniform-store") pendingOpFailsStoreCounter = hotRegionCounterWithEvent("pending-op-fails") labelCounter = labelCounterWithEvent("schedule") diff --git a/pkg/statistics/collector.go b/pkg/statistics/collector.go index ef6227b6393..ed316881192 100644 --- a/pkg/statistics/collector.go +++ b/pkg/statistics/collector.go @@ -59,6 +59,7 @@ func (tikvCollector) getLoads(storeLoads StoreKindLoads, peerLoadSum Loads, rwTy loads[utils.ByteDim] = storeLoads[utils.StoreReadBytes] loads[utils.KeyDim] = storeLoads[utils.StoreReadKeys] loads[utils.QueryDim] = storeLoads[utils.StoreReadQuery] + loads[utils.CPUDim] = storeLoads[utils.StoreReadCPU] case utils.Write: switch kind { case constant.LeaderKind: diff --git a/pkg/statistics/cpu.go b/pkg/statistics/cpu.go new file mode 100644 index 00000000000..f2da3e86059 --- /dev/null +++ b/pkg/statistics/cpu.go @@ -0,0 +1,60 @@ +// Copyright 2026 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package statistics + +import ( + "strings" + + "github.com/pingcap/kvproto/pkg/pdpb" +) + +const ( + // unifiedReadPoolThreadPrefix matches TiKV's unified-read thread prefix: + // https://github.com/tikv/tikv/blob/master/components/tikv_util/src/thread_name_prefix.rs#L60 + unifiedReadPoolThreadPrefix = "unified-read" +) + +func sumCPUUsageByPrefix(cpuUsages []*pdpb.RecordPair, prefix string) uint64 { + var total uint64 + for _, usage := range cpuUsages { + if strings.HasPrefix(usage.GetKey(), prefix) { + total += usage.GetValue() + } + } + return total +} + +// storeUnifiedReadCPUUsage returns the store-level unified-read CPU usage. +func storeUnifiedReadCPUUsage(cpuUsages []*pdpb.RecordPair) uint64 { + return sumCPUUsageByPrefix(cpuUsages, unifiedReadPoolThreadPrefix) +} + +// StoreReadCPUUsage returns the store-level read CPU usage derived from unified-read threads. +func StoreReadCPUUsage(cpuUsages []*pdpb.RecordPair) float64 { + return float64(storeUnifiedReadCPUUsage(cpuUsages)) +} + +// RegionReadCPUUsage returns the region-level read CPU usage based on unified-read CPU. +// If cpu_stats is missing, return 0. +func RegionReadCPUUsage(peerStat *pdpb.PeerStat) float64 { + if peerStat == nil { + return 0 + } + cpuStats := peerStat.GetCpuStats() + if cpuStats == nil { + return 0 + } + return float64(cpuStats.GetUnifiedRead()) +} diff --git a/pkg/statistics/cpu_test.go b/pkg/statistics/cpu_test.go new file mode 100644 index 00000000000..dfb72e04370 --- /dev/null +++ b/pkg/statistics/cpu_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package statistics + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pingcap/kvproto/pkg/pdpb" +) + +func TestStoreReadCPUUsage(t *testing.T) { + re := require.New(t) + cpuUsages := []*pdpb.RecordPair{ + {Key: "unified-read-0", Value: 80}, + {Key: "grpc-server-0", Value: 20}, + {Key: "other", Value: 30}, + } + re.Equal(80.0, StoreReadCPUUsage(cpuUsages)) +} + +func TestRegionReadCPUUsage(t *testing.T) { + re := require.New(t) + peerStat := &pdpb.PeerStat{} + re.Equal(0.0, RegionReadCPUUsage(peerStat)) + + cpuStats := &pdpb.CPUStats{} + cpuStats.UnifiedRead = 80 + peerStat.CpuStats = cpuStats + re.Equal(80.0, RegionReadCPUUsage(peerStat)) +} diff --git a/pkg/statistics/hot_cache_test.go b/pkg/statistics/hot_cache_test.go index 0f6029e8689..91ec40fba67 100644 --- a/pkg/statistics/hot_cache_test.go +++ b/pkg/statistics/hot_cache_test.go @@ -39,7 +39,11 @@ func TestIsHot(t *testing.T) { cache := NewHotCache(ctx, cluster) region, err := buildRegion(cluster, i, 3, 60) re.NoError(err) - stats := cache.CheckReadPeerSync(region, region.GetPeers(), []float64{100000000, 1000, 1000}, 60) + loads := make([]float64, utils.RegionStatCount) + loads[utils.RegionReadBytes] = 100000000 + loads[utils.RegionReadKeys] = 1000 + loads[utils.RegionReadQueryNum] = 1000 + stats := cache.CheckReadPeerSync(region, region.GetPeers(), loads, 60) cache.Update(stats[0], i) for range 100 { re.True(cache.IsRegionHot(region, 1)) diff --git a/pkg/statistics/hot_peer.go b/pkg/statistics/hot_peer.go index 55dcd03b4ee..c74f8690d9e 100644 --- a/pkg/statistics/hot_peer.go +++ b/pkg/statistics/hot_peer.go @@ -163,18 +163,24 @@ func (stat *HotPeerStat) GetActionType() utils.ActionType { // GetLoad returns denoising load if possible. func (stat *HotPeerStat) GetLoad(dim int) float64 { - if stat.rollingLoads != nil { + if dim < 0 || dim >= utils.DimLen { + return 0 + } + if stat.rollingLoads != nil && dim < len(stat.rollingLoads) && stat.rollingLoads[dim] != nil { return math.Round(stat.rollingLoads[dim].get()) } - return math.Round(stat.Loads[dim]) + if dim < len(stat.Loads) { + return math.Round(stat.Loads[dim]) + } + return 0 } // GetLoads returns denoising loads if possible. func (stat *HotPeerStat) GetLoads() []float64 { if stat.rollingLoads != nil { - ret := make([]float64, len(stat.rollingLoads)) + ret := make([]float64, utils.DimLen) for dim := range ret { - ret[dim] = math.Round(stat.rollingLoads[dim].get()) + ret[dim] = stat.GetLoad(dim) } return ret } @@ -194,13 +200,18 @@ func (stat *HotPeerStat) Clone() *HotPeerStat { func (stat *HotPeerStat) isHot(thresholds []float64) bool { return slice.AnyOf(stat.rollingLoads, func(i int) bool { + if stat.rollingLoads[i] == nil { + return false + } return stat.rollingLoads[i].isLastAverageHot(thresholds[i]) }) } func (stat *HotPeerStat) clearLastAverage() { for _, l := range stat.rollingLoads { - l.clearLastAverage() + if l != nil { + l.clearLastAverage() + } } } diff --git a/pkg/statistics/hot_peer_cache.go b/pkg/statistics/hot_peer_cache.go index 6e6783709e5..5edf8f505a3 100644 --- a/pkg/statistics/hot_peer_cache.go +++ b/pkg/statistics/hot_peer_cache.go @@ -66,7 +66,7 @@ type thresholds struct { updatedTime time.Time rates []float64 topNLen int - metrics [utils.DimLen + 1]prometheus.Gauge // 0 is for byte, 1 is for key, 2 is for query, 3 is for total length. + metrics [utils.DimLen + 1]prometheus.Gauge // 0 is for byte, 1 is for key, 2 is for query, 3 is for cpu, 4 is for total length. } // HotPeerCache saves the hot peer's statistics. @@ -274,6 +274,7 @@ func (f *HotPeerCache) collectMetrics() { thresholds.metrics[utils.ByteDim].Set(thresholds.rates[utils.ByteDim]) thresholds.metrics[utils.KeyDim].Set(thresholds.rates[utils.KeyDim]) thresholds.metrics[utils.QueryDim].Set(thresholds.rates[utils.QueryDim]) + thresholds.metrics[utils.CPUDim].Set(thresholds.rates[utils.CPUDim]) thresholds.metrics[utils.DimLen].Set(float64(thresholds.topNLen)) } } @@ -303,6 +304,7 @@ func (f *HotPeerCache) calcHotThresholds(storeID uint64) []float64 { utils.ByteDim: hotCacheStatusGauge.WithLabelValues("byte-rate-threshold", store, kind), utils.KeyDim: hotCacheStatusGauge.WithLabelValues("key-rate-threshold", store, kind), utils.QueryDim: hotCacheStatusGauge.WithLabelValues("query-rate-threshold", store, kind), + utils.CPUDim: hotCacheStatusGauge.WithLabelValues("cpu-rate-threshold", store, kind), utils.DimLen: hotCacheStatusGauge.WithLabelValues("total_length", store, kind), }, } @@ -430,7 +432,11 @@ func (f *HotPeerCache) updateHotPeerStat(region *core.RegionInfo, newItem, oldIt if source == utils.Inherit { for _, dim := range oldItem.rollingLoads { - newItem.rollingLoads = append(newItem.rollingLoads, dim.clone()) + if dim != nil { + newItem.rollingLoads = append(newItem.rollingLoads, dim.clone()) + } else { + newItem.rollingLoads = append(newItem.rollingLoads, nil) + } } newItem.allowInherited = false } else { @@ -493,7 +499,7 @@ func (f *HotPeerCache) updateNewHotPeerStat(newItem *HotPeerStat, deltaLoads []f initItem(newItem, f.kind.DefaultAntiCount()) } newItem.actionType = utils.Add - newItem.rollingLoads = make([]*dimStat, len(regionStats)) + newItem.rollingLoads = make([]*dimStat, utils.DimLen) for i, k := range regionStats { ds := newDimStat(f.interval()) ds.add(deltaLoads[k], interval) diff --git a/pkg/statistics/hot_peer_cache_test.go b/pkg/statistics/hot_peer_cache_test.go index 95d6089c7de..6aacae3aea6 100644 --- a/pkg/statistics/hot_peer_cache_test.go +++ b/pkg/statistics/hot_peer_cache_test.go @@ -330,10 +330,17 @@ func TestUpdateHotPeerStat(t *testing.T) { defer func() { ThresholdsUpdateInterval = 8 * time.Second }() + makeReadDeltaLoads := func(bytes, keys, query float64) []float64 { + loads := make([]float64, utils.RegionStatCount) + loads[utils.RegionReadBytes] = bytes + loads[utils.RegionReadKeys] = keys + loads[utils.RegionReadQueryNum] = query + return loads + } // skip interval=0 interval := uint64(0) - deltaLoads := []float64{0.0, 0.0, 0.0} + deltaLoads := makeReadDeltaLoads(0.0, 0.0, 0.0) utils.MinHotThresholds[utils.RegionReadBytes] = 0.0 utils.MinHotThresholds[utils.RegionReadKeys] = 0.0 utils.MinHotThresholds[utils.RegionReadQueryNum] = 0.0 @@ -343,7 +350,7 @@ func TestUpdateHotPeerStat(t *testing.T) { // new peer, interval is larger than report interval, but no hot interval = 10 - deltaLoads = []float64{0.0, 0.0, 0.0} + deltaLoads = makeReadDeltaLoads(0.0, 0.0, 0.0) utils.MinHotThresholds[utils.RegionReadBytes] = 1.0 utils.MinHotThresholds[utils.RegionReadKeys] = 1.0 utils.MinHotThresholds[utils.RegionReadQueryNum] = 1.0 @@ -352,7 +359,7 @@ func TestUpdateHotPeerStat(t *testing.T) { // new peer, interval is less than report interval interval = 4 - deltaLoads = []float64{60.0, 60.0, 60.0} + deltaLoads = makeReadDeltaLoads(60.0, 60.0, 60.0) utils.MinHotThresholds[utils.RegionReadBytes] = 0.0 utils.MinHotThresholds[utils.RegionReadKeys] = 0.0 utils.MinHotThresholds[utils.RegionReadQueryNum] = 0.0 @@ -361,7 +368,7 @@ func TestUpdateHotPeerStat(t *testing.T) { re.Equal(0, newItem[0].HotDegree) re.Equal(0, newItem[0].AntiCount) // sum of interval is less than report interval - deltaLoads = []float64{60.0, 60.0, 60.0} + deltaLoads = makeReadDeltaLoads(60.0, 60.0, 60.0) cache.UpdateStat(newItem[0]) newItem = cache.CheckPeerFlow(region, []*metapb.Peer{peer}, deltaLoads, interval) re.Equal(0, newItem[0].HotDegree) @@ -414,6 +421,45 @@ func TestThresholdWithUpdateHotPeerStat(t *testing.T) { testMetrics(ctx, re, 1., byteRate, expectThreshold) } +func TestHotPeerStatIsHotUsesCPULastAverage(t *testing.T) { + re := require.New(t) + interval := time.Duration(utils.StoreHeartBeatReportInterval) * time.Second + threshold := 10.0 + highDelta := threshold * interval.Seconds() + + // CPU dim uses isLastAverageHot, same as other dims. + cpuStat := newDimStat(interval) + cpuStat.add(highDelta, interval) + re.True(cpuStat.isLastAverageHot(threshold)) + + lowStat := newDimStat(interval) + lowStat.add(0, interval) + + stat := &HotPeerStat{ + rollingLoads: []*dimStat{ + lowStat, + lowStat.clone(), + lowStat.clone(), + cpuStat, + }, + } + thresholds := []float64{threshold, threshold, threshold, threshold} + re.True(stat.isHot(thresholds)) + + // When CPU last average is below threshold, it should not be hot. + coldCPU := newDimStat(interval) + coldCPU.add(0, interval) + stat2 := &HotPeerStat{ + rollingLoads: []*dimStat{ + lowStat.clone(), + lowStat.clone(), + lowStat.clone(), + coldCPU, + }, + } + re.False(stat2.isHot(thresholds)) +} + func testMetrics(ctx context.Context, re *require.Assertions, interval, byteRate, expectThreshold float64) { cluster := core.NewBasicCluster() cache := NewHotPeerCache(ctx, cluster, utils.Read) @@ -441,7 +487,8 @@ func testMetrics(ctx context.Context, re *require.Assertions, interval, byteRate if oldItem != nil && oldItem.rollingLoads[utils.ByteDim].isHot(thresholds[utils.ByteDim]) == true { break } - loads := []float64{byteRate * interval, 0.0, 0.0} + loads := make([]float64, utils.RegionStatCount) + loads[utils.RegionReadBytes] = byteRate * interval if oldItem == nil { item = cache.updateNewHotPeerStat(newItem, loads, time.Duration(interval)*time.Second) } else { diff --git a/pkg/statistics/hot_peer_test.go b/pkg/statistics/hot_peer_test.go new file mode 100644 index 00000000000..2a640ceddae --- /dev/null +++ b/pkg/statistics/hot_peer_test.go @@ -0,0 +1,65 @@ +// Copyright 2026 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package statistics + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/tikv/pd/pkg/statistics/utils" +) + +func TestHotPeerStatGetLoadBounds(t *testing.T) { + re := require.New(t) + + stat := &HotPeerStat{ + Loads: []float64{1, 2}, + } + re.Equal(1.0, stat.GetLoad(0)) + re.Equal(2.0, stat.GetLoad(1)) + re.Equal(0.0, stat.GetLoad(2)) + re.Equal(0.0, stat.GetLoad(-1)) + + // When rollingLoads has nil entries, fall back to Loads. + stat = &HotPeerStat{ + Loads: []float64{3, 4}, + rollingLoads: make([]*dimStat, utils.DimLen), // all nil + } + re.Equal(3.0, stat.GetLoad(0)) + re.Equal(4.0, stat.GetLoad(1)) + re.Equal(0.0, stat.GetLoad(2)) + re.Equal(0.0, stat.GetLoad(utils.CPUDim)) +} + +func TestHotPeerStatIsHotSkipsNilRollingLoads(t *testing.T) { + re := require.New(t) + interval := time.Second + + hotStat := newDimStat(interval) + hotStat.add(10, interval) + + rollingLoads := make([]*dimStat, utils.DimLen) + rollingLoads[utils.KeyDim] = hotStat + stat := &HotPeerStat{ + rollingLoads: rollingLoads, + } + thresholds := make([]float64, utils.DimLen) + for i := range thresholds { + thresholds[i] = 5 + } + re.True(stat.isHot(thresholds)) +} diff --git a/pkg/statistics/hot_regions_stat.go b/pkg/statistics/hot_regions_stat.go index d30a153492b..835385dd099 100644 --- a/pkg/statistics/hot_regions_stat.go +++ b/pkg/statistics/hot_regions_stat.go @@ -21,9 +21,11 @@ type HotPeersStat struct { StoreByteRate float64 `json:"store_bytes"` StoreKeyRate float64 `json:"store_keys"` StoreQueryRate float64 `json:"store_query"` + StoreCPURate float64 `json:"store_cpu"` TotalBytesRate float64 `json:"total_flow_bytes"` TotalKeysRate float64 `json:"total_flow_keys"` TotalQueryRate float64 `json:"total_flow_query"` + TotalCPURate float64 `json:"total_flow_cpu"` Count int `json:"regions_count"` Stats []HotPeerStatShow `json:"statistics"` } @@ -39,6 +41,7 @@ type HotPeerStatShow struct { ByteRate float64 `json:"flow_bytes"` KeyRate float64 `json:"flow_keys"` QueryRate float64 `json:"flow_query"` + CPURate float64 `json:"flow_cpu"` AntiCount int `json:"anti_count"` LastUpdateTime time.Time `json:"last_update_time,omitempty"` } diff --git a/pkg/statistics/store.go b/pkg/statistics/store.go index d323d2ace84..d54afdc8f39 100644 --- a/pkg/statistics/store.go +++ b/pkg/statistics/store.go @@ -30,6 +30,9 @@ import ( const ( storeStatsRollingWindowsSize = 3 + // It is used to moving average CPU usage, + // and the window size is larger than other dimensions to make the CPU usage more stable. + storeCPUStatsRollingWindowsSize = 5 // RegionsStatsObserveInterval is the interval for obtaining statistics from RegionTree RegionsStatsObserveInterval = 30 * time.Second @@ -148,10 +151,11 @@ func newRollingStoreStats() *RollingStoreStats { timeMedians[utils.StoreReadBytes] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultReadMfSize, interval) timeMedians[utils.StoreReadKeys] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultReadMfSize, interval) timeMedians[utils.StoreReadQuery] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultReadMfSize, interval) + timeMedians[utils.StoreReadCPU] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultReadMfSize, interval) timeMedians[utils.StoreWriteBytes] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultWriteMfSize, interval) timeMedians[utils.StoreWriteKeys] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultWriteMfSize, interval) timeMedians[utils.StoreWriteQuery] = movingaverage.NewTimeMedian(utils.DefaultAotSize, utils.DefaultWriteMfSize, interval) - movingAvgs[utils.StoreCPUUsage] = movingaverage.NewMedianFilter(storeStatsRollingWindowsSize) + movingAvgs[utils.StoreCPUUsage] = movingaverage.NewMedianFilter(storeCPUStatsRollingWindowsSize) movingAvgs[utils.StoreDiskReadRate] = movingaverage.NewMedianFilter(storeStatsRollingWindowsSize) movingAvgs[utils.StoreDiskWriteRate] = movingaverage.NewMedianFilter(storeStatsRollingWindowsSize) @@ -196,6 +200,8 @@ func (r *RollingStoreStats) Observe(stats *pdpb.StoreStats) { r.timeMedians[utils.StoreReadBytes].Add(float64(stats.BytesRead), interval) r.timeMedians[utils.StoreReadKeys].Add(float64(stats.KeysRead), interval) r.timeMedians[utils.StoreReadQuery].Add(float64(readQueryNum), interval) + readCPUUsage := StoreReadCPUUsage(stats.GetCpuUsages()) + r.timeMedians[utils.StoreReadCPU].Add(readCPUUsage*interval.Seconds(), interval) // Updates the cpu usages and disk rw rates of store. r.movingAvgs[utils.StoreCPUUsage].Add(collect(stats.GetCpuUsages())) @@ -227,6 +233,7 @@ func (r *RollingStoreStats) Set(stats *pdpb.StoreStats) { r.timeMedians[utils.StoreReadKeys].Set(float64(stats.KeysRead) / interval) r.timeMedians[utils.StoreReadQuery].Set(float64(readQueryNum) / interval) r.timeMedians[utils.StoreWriteQuery].Set(float64(writeQueryNum) / interval) + r.timeMedians[utils.StoreReadCPU].Set(StoreReadCPUUsage(stats.GetCpuUsages())) r.movingAvgs[utils.StoreCPUUsage].Set(collect(stats.GetCpuUsages())) r.movingAvgs[utils.StoreDiskReadRate].Set(collect(stats.GetReadIoRates())) r.movingAvgs[utils.StoreDiskWriteRate].Set(collect(stats.GetWriteIoRates())) @@ -245,7 +252,7 @@ func (r *RollingStoreStats) GetLoad(k utils.StoreLoadKind) float64 { r.RLock() defer r.RUnlock() switch k { - case utils.StoreReadBytes, utils.StoreReadKeys, utils.StoreReadQuery, utils.StoreWriteBytes, utils.StoreWriteKeys, utils.StoreWriteQuery: + case utils.StoreReadBytes, utils.StoreReadKeys, utils.StoreReadQuery, utils.StoreReadCPU, utils.StoreWriteBytes, utils.StoreWriteKeys, utils.StoreWriteQuery: return r.timeMedians[k].Get() case utils.StoreCPUUsage, utils.StoreDiskReadRate, utils.StoreDiskWriteRate, utils.StoreRegionsWriteBytes, utils.StoreRegionsWriteKeys: return r.movingAvgs[k].Get() @@ -258,7 +265,7 @@ func (r *RollingStoreStats) GetInstantLoad(k utils.StoreLoadKind) float64 { r.RLock() defer r.RUnlock() switch k { - case utils.StoreReadBytes, utils.StoreReadKeys, utils.StoreReadQuery, utils.StoreWriteBytes, utils.StoreWriteKeys, utils.StoreWriteQuery: + case utils.StoreReadBytes, utils.StoreReadKeys, utils.StoreReadQuery, utils.StoreReadCPU, utils.StoreWriteBytes, utils.StoreWriteKeys, utils.StoreWriteQuery: return r.timeMedians[k].GetInstantaneous() case utils.StoreCPUUsage, utils.StoreDiskReadRate, utils.StoreDiskWriteRate, utils.StoreRegionsWriteBytes, utils.StoreRegionsWriteKeys: return r.movingAvgs[k].GetInstantaneous() diff --git a/pkg/statistics/store_collection.go b/pkg/statistics/store_collection.go index 57eb496cd5a..25a39e2480a 100644 --- a/pkg/statistics/store_collection.go +++ b/pkg/statistics/store_collection.go @@ -208,6 +208,7 @@ func ObserveHotStat(store *core.StoreInfo, stats *StoresStats) { storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_rate_keys").Set(storeFlowStats.GetLoad(utils.StoreReadKeys)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_write_query_rate").Set(storeFlowStats.GetLoad(utils.StoreWriteQuery)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_query_rate").Set(storeFlowStats.GetLoad(utils.StoreReadQuery)) + storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_cpu_usage").Set(storeFlowStats.GetLoad(utils.StoreReadCPU)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_cpu_usage").Set(storeFlowStats.GetLoad(utils.StoreCPUUsage)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_disk_read_rate").Set(storeFlowStats.GetLoad(utils.StoreDiskReadRate)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_disk_write_rate").Set(storeFlowStats.GetLoad(utils.StoreDiskWriteRate)) @@ -220,6 +221,7 @@ func ObserveHotStat(store *core.StoreInfo, stats *StoresStats) { storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_rate_keys_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreReadKeys)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_write_query_rate_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreWriteQuery)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_query_rate_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreReadQuery)) + storeStatusGauge.WithLabelValues(storeAddress, id, "store_read_cpu_usage_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreReadCPU)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_regions_write_rate_bytes_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreRegionsWriteBytes)) storeStatusGauge.WithLabelValues(storeAddress, id, "store_regions_write_rate_keys_instant").Set(storeFlowStats.GetInstantLoad(utils.StoreRegionsWriteKeys)) } @@ -306,6 +308,8 @@ func ResetStoreStatistics(storeAddress string, id string) { "store_read_rate_keys", "store_write_query_rate", "store_read_query_rate", + "store_read_cpu_usage", + "store_read_cpu_usage_instant", "store_regions_write_rate_bytes", "store_regions_write_rate_keys", "store_slow_trend_cause_value", diff --git a/pkg/statistics/store_collection_test.go b/pkg/statistics/store_collection_test.go index b4a320fc2a8..b47227b0d41 100644 --- a/pkg/statistics/store_collection_test.go +++ b/pkg/statistics/store_collection_test.go @@ -16,6 +16,7 @@ package statistics import ( "fmt" + "math" "testing" "time" @@ -117,7 +118,7 @@ func TestSummaryStoreInfos(t *testing.T) { re.Len(details, 2) re.Empty(details[0].LoadPred.Current.HistoryLoads) re.Empty(details[1].LoadPred.Current.HistoryLoads) - expectHistoryLoads := []float64{1, 2, 5} + expectHistoryLoads := []float64{1, 2, 5, 0} for _, storeID := range []uint64{1, 3} { loads := storeHistoryLoad.Get(storeID, rw, kind) for i := range loads { @@ -133,7 +134,7 @@ func TestSummaryStoreInfos(t *testing.T) { storeHistoryLoad.sampleDuration = 0 for i := 1; i < 10; i++ { details = summaryStoresLoadByEngine(storeInfos, storeLoads, storeHistoryLoad, nil, rw, kind, collector) - expect := []float64{2, 4, 10} + expect := []float64{2, 4, 10, 0} for _, detail := range details { loads := detail.LoadPred.Current.HistoryLoads re.Len(loads, len(expectHistoryLoads)) @@ -158,6 +159,46 @@ func TestSummaryStoreInfos(t *testing.T) { } } +func TestSummaryStoresLoadByEngineZeroCPUStddev(t *testing.T) { + re := require.New(t) + rw := utils.Read + kind := constant.LeaderKind + collector := newTikvCollector() + + storeInfos := map[uint64]*StoreSummaryInfo{} + storeLoads := map[uint64]StoreKindLoads{} + storeHotPeers := map[uint64][]*HotPeerStat{} + for _, storeID := range []uint64{1, 2} { + storeInfos[storeID] = &StoreSummaryInfo{ + StoreInfo: core.NewStoreInfo( + &metapb.Store{Id: storeID, Address: fmt.Sprintf("mock://tikv-%d", storeID)}, + core.SetLastHeartbeatTS(time.Now()), + ), + } + loads := StoreKindLoads{} + loads[utils.StoreReadBytes] = 100 * float64(storeID) + loads[utils.StoreReadKeys] = 10 * float64(storeID) + loads[utils.StoreReadQuery] = 5 * float64(storeID) + storeLoads[storeID] = loads + storeHotPeers[storeID] = []*HotPeerStat{{ + StoreID: storeID, + RegionID: storeID, + Loads: []float64{1, 1, 1, 0}, + isLeader: true, + }} + } + + details := summaryStoresLoadByEngine(storeInfos, storeLoads, nil, storeHotPeers, rw, kind, collector) + re.Len(details, 2) + for _, detail := range details { + cpuStddev := detail.LoadPred.Stddev.Loads[utils.CPUDim] + re.False(math.IsNaN(cpuStddev)) + re.False(math.IsInf(cpuStddev, 0)) + re.Zero(cpuStddev) + re.True(detail.IsUniform(utils.CPUDim, 0.1)) + } +} + func TestTiFlashComputeExcludedFromExpectation(t *testing.T) { re := require.New(t) rw := utils.Write diff --git a/pkg/statistics/store_hot_peers_infos.go b/pkg/statistics/store_hot_peers_infos.go index 2a6c693c405..5d58663c0e0 100644 --- a/pkg/statistics/store_hot_peers_infos.go +++ b/pkg/statistics/store_hot_peers_infos.go @@ -33,7 +33,7 @@ type StoreHotPeersInfos struct { // NOTE: This type is exported by HTTP API. Please pay more attention when modifying it. type StoreHotPeersStat map[uint64]*HotPeersStat -// CollectHotPeerInfos only returns TotalBytesRate,TotalKeysRate,TotalQueryRate,Count +// CollectHotPeerInfos only returns TotalBytesRate,TotalKeysRate,TotalQueryRate,TotalCPURate,Count func CollectHotPeerInfos(stores []*core.StoreInfo, regionStats map[uint64][]*HotPeerStat) *StoreHotPeersInfos { peerLoadSum := make([]float64, utils.DimLen) collect := func(kind constant.ResourceKind) StoreHotPeersStat { @@ -57,6 +57,7 @@ func CollectHotPeerInfos(stores []*core.StoreInfo, regionStats map[uint64][]*Hot TotalBytesRate: peerLoadSum[utils.ByteDim], TotalKeysRate: peerLoadSum[utils.KeyDim], TotalQueryRate: peerLoadSum[utils.QueryDim], + TotalCPURate: peerLoadSum[utils.CPUDim], Count: len(peers), } } @@ -246,7 +247,12 @@ func summaryStoresLoadByEngine( } } for i := range stddevLoads { - stddevLoads[i] = math.Sqrt(stddevLoads[i]/float64(allStoreCount)) / expectLoads[i] + stddevLoads[i] = math.Sqrt(stddevLoads[i] / float64(allStoreCount)) + if expectLoads[i] == 0 { + stddevLoads[i] = 0 + continue + } + stddevLoads[i] /= expectLoads[i] } } @@ -259,6 +265,8 @@ func summaryStoresLoadByEngine( hotPeerSummary.WithLabelValues(ty, engine).Set(expectLoads[utils.KeyDim]) ty = "exp-query-rate-" + rwTy.String() + "-" + kind.String() hotPeerSummary.WithLabelValues(ty, engine).Set(expectLoads[utils.QueryDim]) + ty = "exp-cpu-rate-" + rwTy.String() + "-" + kind.String() + hotPeerSummary.WithLabelValues(ty, engine).Set(expectLoads[utils.CPUDim]) ty = "exp-count-rate-" + rwTy.String() + "-" + kind.String() hotPeerSummary.WithLabelValues(ty, engine).Set(expectCount) ty = "stddev-byte-rate-" + rwTy.String() + "-" + kind.String() @@ -267,6 +275,8 @@ func summaryStoresLoadByEngine( hotPeerSummary.WithLabelValues(ty, engine).Set(stddevLoads[utils.KeyDim]) ty = "stddev-query-rate-" + rwTy.String() + "-" + kind.String() hotPeerSummary.WithLabelValues(ty, engine).Set(stddevLoads[utils.QueryDim]) + ty = "stddev-cpu-rate-" + rwTy.String() + "-" + kind.String() + hotPeerSummary.WithLabelValues(ty, engine).Set(stddevLoads[utils.CPUDim]) } expect := StoreLoad{ Loads: expectLoads, diff --git a/pkg/statistics/store_load.go b/pkg/statistics/store_load.go index 79d7aa9a5f6..037b89e26dc 100644 --- a/pkg/statistics/store_load.go +++ b/pkg/statistics/store_load.go @@ -34,19 +34,22 @@ type StoreLoadDetail struct { func (li *StoreLoadDetail) ToHotPeersStat() *HotPeersStat { storeByteRate, storeKeyRate, storeQueryRate := li.LoadPred.Current.Loads[utils.ByteDim], li.LoadPred.Current.Loads[utils.KeyDim], li.LoadPred.Current.Loads[utils.QueryDim] + storeCPURate := li.LoadPred.Current.Loads[utils.CPUDim] if len(li.HotPeers) == 0 { return &HotPeersStat{ StoreByteRate: storeByteRate, StoreKeyRate: storeKeyRate, StoreQueryRate: storeQueryRate, + StoreCPURate: storeCPURate, TotalBytesRate: 0.0, TotalKeysRate: 0.0, TotalQueryRate: 0.0, + TotalCPURate: 0.0, Count: 0, Stats: make([]HotPeerStatShow, 0), } } - var byteRate, keyRate, queryRate float64 + var byteRate, keyRate, queryRate, cpuRate float64 peers := make([]HotPeerStatShow, 0, len(li.HotPeers)) for _, peer := range li.HotPeers { if peer.HotDegree > 0 { @@ -54,6 +57,7 @@ func (li *StoreLoadDetail) ToHotPeersStat() *HotPeersStat { byteRate += peer.GetLoad(utils.ByteDim) keyRate += peer.GetLoad(utils.KeyDim) queryRate += peer.GetLoad(utils.QueryDim) + cpuRate += peer.GetLoad(utils.CPUDim) } } @@ -61,9 +65,11 @@ func (li *StoreLoadDetail) ToHotPeersStat() *HotPeersStat { TotalBytesRate: byteRate, TotalKeysRate: keyRate, TotalQueryRate: queryRate, + TotalCPURate: cpuRate, StoreByteRate: storeByteRate, StoreKeyRate: storeKeyRate, StoreQueryRate: storeQueryRate, + StoreCPURate: storeCPURate, Count: len(peers), Stats: peers, } @@ -78,6 +84,7 @@ func toHotPeerStatShow(p *HotPeerStat) HotPeerStatShow { byteRate := p.GetLoad(utils.ByteDim) keyRate := p.GetLoad(utils.KeyDim) queryRate := p.GetLoad(utils.QueryDim) + cpuRate := p.GetLoad(utils.CPUDim) return HotPeerStatShow{ StoreID: p.StoreID, Stores: p.GetStores(), @@ -87,6 +94,7 @@ func toHotPeerStatShow(p *HotPeerStat) HotPeerStatShow { ByteRate: byteRate, KeyRate: keyRate, QueryRate: queryRate, + CPURate: cpuRate, AntiCount: p.AntiCount, } } @@ -166,10 +174,12 @@ func (load StoreLoad) ToLoadPred(rwTy utils.RWType, infl *Influence) *StoreLoadP future.Loads[utils.ByteDim] += infl.Loads[utils.RegionReadBytes] future.Loads[utils.KeyDim] += infl.Loads[utils.RegionReadKeys] future.Loads[utils.QueryDim] += infl.Loads[utils.RegionReadQueryNum] + future.Loads[utils.CPUDim] += infl.Loads[utils.RegionReadCPU] case utils.Write: future.Loads[utils.ByteDim] += infl.Loads[utils.RegionWriteBytes] future.Loads[utils.KeyDim] += infl.Loads[utils.RegionWriteKeys] future.Loads[utils.QueryDim] += infl.Loads[utils.RegionWriteQueryNum] + future.Loads[utils.CPUDim] += infl.Loads[utils.RegionWriteCPU] } future.HotPeerCount += infl.HotPeerCount } diff --git a/pkg/statistics/store_test.go b/pkg/statistics/store_test.go index 741fdc54fd3..7576d64072c 100644 --- a/pkg/statistics/store_test.go +++ b/pkg/statistics/store_test.go @@ -24,6 +24,7 @@ import ( "github.com/pingcap/kvproto/pkg/pdpb" "github.com/tikv/pd/pkg/core" + "github.com/tikv/pd/pkg/statistics/utils" ) func TestFilterUnhealthyStore(t *testing.T) { @@ -46,3 +47,27 @@ func TestFilterUnhealthyStore(t *testing.T) { re.NotNil(loads[4]) re.NotNil(loads[5]) } + +func TestStoreReadCPUUsesTimeMedian(t *testing.T) { + re := require.New(t) + stats := newRollingStoreStats() + + intervals := []uint64{5, 10, 20, 10, 5} + for _, secs := range intervals { + stats.Observe(&pdpb.StoreStats{ + Interval: &pdpb.TimeInterval{ + StartTimestamp: 0, + EndTimestamp: secs, + }, + CpuUsages: []*pdpb.RecordPair{ + { + Key: "unified-read-pool-0", + Value: 100, + }, + }, + }) + } + + re.Equal(100.0, stats.GetLoad(utils.StoreReadCPU)) + re.Equal(100.0, stats.GetInstantLoad(utils.StoreReadCPU)) +} diff --git a/pkg/statistics/utils/constant.go b/pkg/statistics/utils/constant.go index 14bee23778a..f2e7012a062 100644 --- a/pkg/statistics/utils/constant.go +++ b/pkg/statistics/utils/constant.go @@ -43,4 +43,6 @@ var MinHotThresholds = [RegionStatCount]float64{ RegionWriteBytes: 1 * units.KiB, RegionWriteKeys: 32, RegionWriteQueryNum: 32, + RegionReadCPU: 1, + RegionWriteCPU: 1, } diff --git a/pkg/statistics/utils/kind.go b/pkg/statistics/utils/kind.go index 618422880da..c213060417c 100644 --- a/pkg/statistics/utils/kind.go +++ b/pkg/statistics/utils/kind.go @@ -21,6 +21,8 @@ const ( KeyPriority = "key" // QueryPriority indicates hot-region-scheduler prefer query dim QueryPriority = "query" + // CPUPriority indicates hot-region-scheduler prefer cpu dim + CPUPriority = "cpu" ) // Indicator dims. @@ -28,6 +30,7 @@ const ( ByteDim int = iota KeyDim QueryDim + CPUDim DimLen ) @@ -40,6 +43,8 @@ func StringToDim(name string) int { return KeyDim case QueryPriority: return QueryDim + case CPUPriority: + return CPUDim } return ByteDim } @@ -53,6 +58,8 @@ func DimToString(dim int) string { return KeyPriority case QueryDim: return QueryPriority + case CPUDim: + return CPUPriority default: return "" } @@ -69,6 +76,9 @@ const ( RegionWriteBytes RegionWriteKeys RegionWriteQueryNum + // Keep RegionReadCPU and RegionWriteCPU appended to preserve the existing RegionStatKind values. + RegionReadCPU + RegionWriteCPU RegionStatCount ) @@ -87,6 +97,10 @@ func (k RegionStatKind) String() string { return "read_query" case RegionWriteQueryNum: return "write_query" + case RegionReadCPU: + return "read_cpu" + case RegionWriteCPU: + return "write_cpu" } return "unknown RegionStatKind" } @@ -108,6 +122,8 @@ const ( StoreRegionsWriteBytes // Same as StoreWriteBytes, but it is counted by RegionHeartbeat. StoreRegionsWriteKeys // Same as StoreWriteKeys, but it is counted by RegionHeartbeat. + // Keep StoreReadCPU appended to preserve the existing StoreLoadKind values. + StoreReadCPU StoreLoadCount ) @@ -122,6 +138,8 @@ func (k StoreLoadKind) String() string { return "store_write_bytes" case StoreReadQuery: return "store_read_query" + case StoreReadCPU: + return "store_read_cpu" case StoreWriteQuery: return "store_write_query" case StoreWriteKeys: @@ -181,8 +199,8 @@ func (rw RWType) String() string { } var ( - writeRegionStats = []RegionStatKind{RegionWriteBytes, RegionWriteKeys, RegionWriteQueryNum} - readRegionStats = []RegionStatKind{RegionReadBytes, RegionReadKeys, RegionReadQueryNum} + writeRegionStats = []RegionStatKind{RegionWriteBytes, RegionWriteKeys, RegionWriteQueryNum, RegionWriteCPU} + readRegionStats = []RegionStatKind{RegionReadBytes, RegionReadKeys, RegionReadQueryNum, RegionReadCPU} ) // RegionStats returns hot items according to kind diff --git a/pkg/statistics/utils/kind_test.go b/pkg/statistics/utils/kind_test.go index 083260167b0..08192aeba57 100644 --- a/pkg/statistics/utils/kind_test.go +++ b/pkg/statistics/utils/kind_test.go @@ -45,12 +45,20 @@ func TestGetLoads(t *testing.T) { Prewrite: 13, Commit: 14, } - regionA := core.NewRegionInfo(&metapb.Region{Id: 100, Peers: []*metapb.Peer{}}, nil, - core.SetReadBytes(1), - core.SetReadKeys(2), - core.SetWrittenBytes(3), - core.SetWrittenKeys(4), - core.SetQueryStats(queryStats)) + peer := &metapb.Peer{Id: 1, StoreId: 1} + regionA := core.RegionFromHeartbeat(&pdpb.RegionHeartbeatRequest{ + Region: &metapb.Region{Id: 100, Peers: []*metapb.Peer{peer}}, + Leader: peer, + BytesRead: 1, + KeysRead: 2, + BytesWritten: 3, + KeysWritten: 4, + QueryStats: queryStats, + CpuUsage: 12, + CpuStats: &pdpb.CPUStats{ + UnifiedRead: 7, + }, + }, 0) loads := regionA.GetLoads() re.Len(loads, int(RegionStatCount)) re.Equal(float64(regionA.GetBytesRead()), loads[RegionReadBytes]) @@ -61,6 +69,9 @@ func TestGetLoads(t *testing.T) { re.Equal(float64(regionA.GetBytesWritten()), loads[RegionWriteBytes]) re.Equal(float64(regionA.GetKeysWritten()), loads[RegionWriteKeys]) re.Equal(float64(regionA.GetWriteQueryNum()), loads[RegionWriteQueryNum]) + re.Equal(uint64(12), regionA.GetCPUUsage()) + re.Equal(uint64(7), regionA.GetReadCPUUsage()) + re.Equal(float64(regionA.GetReadCPUUsage()), loads[RegionReadCPU]) writeQuery := float64(queryStats.Put + queryStats.Delete + queryStats.DeleteRange + queryStats.AcquirePessimisticLock + queryStats.Rollback + queryStats.Prewrite + queryStats.Commit) re.Equal(float64(regionA.GetWriteQueryNum()), writeQuery) @@ -72,4 +83,5 @@ func TestGetLoads(t *testing.T) { re.Equal(float64(regionA.GetBytesWritten()), loads[RegionWriteBytes]) re.Equal(float64(regionA.GetKeysWritten()), loads[RegionWriteKeys]) re.Equal(float64(regionA.GetWriteQueryNum()), loads[RegionWriteQueryNum]) + re.Equal(0.0, loads[RegionReadCPU]) } diff --git a/pkg/storage/hot_region_storage.go b/pkg/storage/hot_region_storage.go index b230f24243f..b190c5a4798 100644 --- a/pkg/storage/hot_region_storage.go +++ b/pkg/storage/hot_region_storage.go @@ -80,6 +80,7 @@ type HistoryHotRegion struct { HotRegionType string `json:"hot_region_type"` HotDegree int64 `json:"hot_degree"` FlowBytes float64 `json:"flow_bytes"` + FlowCPU float64 `json:"flow_cpu"` KeyRate float64 `json:"key_rate"` QueryRate float64 `json:"query_rate"` StartKey string `json:"start_key"` diff --git a/pkg/versioninfo/versioninfo.go b/pkg/versioninfo/versioninfo.go index 56e44a4c087..ac186d640ef 100644 --- a/pkg/versioninfo/versioninfo.go +++ b/pkg/versioninfo/versioninfo.go @@ -97,6 +97,26 @@ func IsFeatureSupported(clusterVersion *semver.Version, f Feature) bool { return IsCompatible(minSupportVersion, *clusterVersion) } +var ( + hotScheduleWithCPUMinVersion = MustParseVersion("8.5.7") + hotScheduleWithCPUv9MinVersion = MustParseVersion("9.0.0-beta.1") +) + +// IsHotScheduleWithCPUSupported returns whether TiKV reports CPU info for hot scheduling. +func IsHotScheduleWithCPUSupported(clusterVersion *semver.Version) bool { + if clusterVersion == nil { + return false + } + if kerneltype.IsNextGen() { + return true + } + // TiKV <= 8.5.6 and < 9.0.0-beta.1 do not report CPU usage. + if clusterVersion.Major >= 9 { + return !clusterVersion.LessThan(*hotScheduleWithCPUv9MinVersion) + } + return !clusterVersion.LessThan(*hotScheduleWithCPUMinVersion) +} + // Log prints the version information of the PD with the specific service mode. func Log(serviceMode string) { mode := strings.ToUpper(serviceMode) diff --git a/pkg/versioninfo/versioninfo_test.go b/pkg/versioninfo/versioninfo_test.go new file mode 100644 index 00000000000..7d2d5c1951c --- /dev/null +++ b/pkg/versioninfo/versioninfo_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 TiKV Project Authors. +// +// 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, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package versioninfo + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/tikv/pd/pkg/utils/testutil" + "github.com/tikv/pd/pkg/versioninfo/kerneltype" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, testutil.LeakOptions...) +} + +func TestIsHotScheduleWithCPUSupported(t *testing.T) { + re := require.New(t) + re.False(IsHotScheduleWithCPUSupported(nil)) + + tests := []struct { + version string + expect bool + }{ + {"8.5.5", false}, + {"8.5.6", false}, + {"8.5.7", true}, + {"9.0.0-beta.1", true}, + {"9.0.0", true}, + {"9.1.0", true}, + } + for _, test := range tests { + if !kerneltype.IsNextGen() { + re.Equal(test.expect, IsHotScheduleWithCPUSupported(MustParseVersion(test.version)), test.version) + } + } +} diff --git a/server/api/region_test.go b/server/api/region_test.go index e462044b1ed..1f6c2ffb108 100644 --- a/server/api/region_test.go +++ b/server/api/region_test.go @@ -28,6 +28,7 @@ import ( "github.com/tikv/pd/pkg/core" "github.com/tikv/pd/pkg/response" + statutils "github.com/tikv/pd/pkg/statistics/utils" "github.com/tikv/pd/pkg/utils/testutil" ) @@ -57,6 +58,42 @@ func TestTopN(t *testing.T) { } } +func TestRegionCPUUsageCompatibility(t *testing.T) { + re := require.New(t) + + newHeartbeatRegion := func(id, cpuUsage, readCPU uint64) *core.RegionInfo { + peer := &metapb.Peer{Id: id*10 + 1, StoreId: 1} + return core.RegionFromHeartbeat(&pdpb.RegionHeartbeatRequest{ + Region: &metapb.Region{ + Id: id, + Peers: []*metapb.Peer{peer}, + }, + Leader: peer, + CpuUsage: cpuUsage, + CpuStats: &pdpb.CPUStats{ + UnifiedRead: readCPU, + }, + }, 0) + } + + r1 := newHeartbeatRegion(10001, 2000, 4000) + r2 := newHeartbeatRegion(10002, 3000, 1000) + + topRegions := TopNRegions([]*core.RegionInfo{r1, r2}, func(a, b *core.RegionInfo) bool { + return a.GetCPUUsage() < b.GetCPUUsage() + }, 2) + re.Len(topRegions, 2) + re.Equal(uint64(10002), topRegions[0].GetID()) + re.Equal(uint64(10001), topRegions[1].GetID()) + + re.Equal(uint64(2000), r1.GetCPUUsage()) + re.Equal(uint64(4000), r1.GetReadCPUUsage()) + re.Equal(float64(4000), r1.GetLoads()[statutils.RegionReadCPU]) + + got := response.NewAPIRegionInfo(r1) + re.Equal(uint64(2000), got.CPUUsage) +} + func TestRegionsInfoMarshal(t *testing.T) { re := require.New(t) regionWithNilPeer := core.NewRegionInfo(&metapb.Region{Id: 1}, &metapb.Peer{Id: 1}) diff --git a/server/cluster/cluster.go b/server/cluster/cluster.go index 6178f2fb39f..9803411600d 100644 --- a/server/cluster/cluster.go +++ b/server/cluster/cluster.go @@ -1124,6 +1124,7 @@ func (c *RaftCluster) HandleStoreHeartbeat(heartbeat *pdpb.StoreHeartbeatRequest continue } readQueryNum := core.GetReadQueryNum(peerStat.GetQueryStats()) + regionReadCPU := statistics.RegionReadCPUUsage(peerStat) loads := []float64{ utils.RegionReadBytes: float64(peerStat.GetReadBytes()), utils.RegionReadKeys: float64(peerStat.GetReadKeys()), @@ -1131,6 +1132,8 @@ func (c *RaftCluster) HandleStoreHeartbeat(heartbeat *pdpb.StoreHeartbeatRequest utils.RegionWriteBytes: 0, utils.RegionWriteKeys: 0, utils.RegionWriteQueryNum: 0, + utils.RegionReadCPU: regionReadCPU * float64(interval), + utils.RegionWriteCPU: 0, } checkReadPeerTask := func(cache *statistics.HotPeerCache) { stats := cache.CheckPeerFlow(region, []*metapb.Peer{peer}, loads, interval) diff --git a/server/cluster/cluster_test.go b/server/cluster/cluster_test.go index da326b61440..6268791519d 100644 --- a/server/cluster/cluster_test.go +++ b/server/cluster/cluster_test.go @@ -2772,7 +2772,7 @@ func TestCollectMetrics(t *testing.T) { item := &statistics.HotPeerStat{ StoreID: uint64(i % 5), RegionID: uint64(i*1000 + k), - Loads: []float64{10, 20, 30}, + Loads: []float64{10, 20, 30, 0}, HotDegree: 10, AntiCount: utils.HotRegionAntiCount, // for write } diff --git a/server/handler.go b/server/handler.go index 62ae86458ef..57e09eb943c 100644 --- a/server/handler.go +++ b/server/handler.go @@ -433,6 +433,7 @@ func (h *Handler) packHotRegions(hotPeersStat statistics.StoreHotPeersStat, hotR IsLearner: core.IsLearner(region.GetPeer(hotPeerStat.StoreID)), HotDegree: int64(hotPeerStat.HotDegree), FlowBytes: hotPeerStat.ByteRate, + FlowCPU: hotPeerStat.CPURate, KeyRate: hotPeerStat.KeyRate, QueryRate: hotPeerStat.QueryRate, StartKey: string(region.GetStartKey()), diff --git a/tests/integrations/go.mod b/tests/integrations/go.mod index 630239b8057..29678ab3a96 100644 --- a/tests/integrations/go.mod +++ b/tests/integrations/go.mod @@ -219,17 +219,17 @@ require ( go.uber.org/fx v1.12.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.41.0 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect diff --git a/tests/integrations/go.sum b/tests/integrations/go.sum index 00f2648346d..a3d55f0cf8e 100644 --- a/tests/integrations/go.sum +++ b/tests/integrations/go.sum @@ -697,8 +697,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 h1:QLureRX3moex6NVu/Lr4MGakp9FdA7sBHGBmvRW7NaM= golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= @@ -720,8 +720,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -755,8 +755,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -774,8 +774,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -816,8 +816,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -841,8 +841,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -870,8 +870,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tests/server/api/scheduler_test.go b/tests/server/api/scheduler_test.go index c01b0537273..d950bbd76d3 100644 --- a/tests/server/api/scheduler_test.go +++ b/tests/server/api/scheduler_test.go @@ -249,11 +249,14 @@ func (suite *scheduleTestSuite) checkAPI(cluster *tests.TestCluster) { "min-hot-byte-rate": 100.0, "min-hot-key-rate": 10.0, "min-hot-query-rate": 10.0, + "min-hot-cpu-rate": 10.0, "max-zombie-rounds": 3.0, + "pending-weight": 1.0, "max-peer-number": 1000.0, "byte-rate-rank-step-ratio": 0.05, "key-rate-rank-step-ratio": 0.05, "query-rate-rank-step-ratio": 0.05, + "cpu-rate-rank-step-ratio": 0.05, "count-rank-step-ratio": 0.01, "great-dec-ratio": 0.95, "minor-dec-ratio": 0.99, diff --git a/tools/go.mod b/tools/go.mod index e0df7817974..ca67c237862 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -40,8 +40,8 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 - golang.org/x/text v0.31.0 - golang.org/x/tools v0.38.0 + golang.org/x/text v0.34.0 + golang.org/x/tools v0.41.0 google.golang.org/grpc v1.75.1 ) @@ -220,14 +220,14 @@ require ( go.uber.org/fx v1.12.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect diff --git a/tools/go.sum b/tools/go.sum index eb1e742ec99..a6ea2e5d6d5 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -706,8 +706,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230711005742-c3f37128e5a4 h1:QLureRX3moex6NVu/Lr4MGakp9FdA7sBHGBmvRW7NaM= @@ -730,8 +730,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -765,8 +765,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -784,8 +784,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -826,8 +826,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -851,8 +851,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -881,8 +881,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tools/pd-ctl/pdctl/command/scheduler_command.go b/tools/pd-ctl/pdctl/command/scheduler_command.go index 6c37dfd76b5..e2bb0f12302 100644 --- a/tools/pd-ctl/pdctl/command/scheduler_command.go +++ b/tools/pd-ctl/pdctl/command/scheduler_command.go @@ -718,6 +718,7 @@ var hiddenHotConfig = []string{ "byte-rate-rank-step-ratio", "key-rate-rank-step-ratio", "query-rate-rank-step-ratio", + "cpu-rate-rank-step-ratio", "count-rank-step-ratio", "great-dec-ratio", "minor-dec-ratio", diff --git a/tools/pd-ctl/tests/hot/hot_test.go b/tools/pd-ctl/tests/hot/hot_test.go index a526572a7fd..5f6b53d6830 100644 --- a/tools/pd-ctl/tests/hot/hot_test.go +++ b/tools/pd-ctl/tests/hot/hot_test.go @@ -177,6 +177,7 @@ func (suite *hotTestSuite) checkHot(cluster *pdTests.TestCluster) { utils.RegionWriteBytes: 0, utils.RegionWriteKeys: 0, utils.RegionWriteQueryNum: 0, + utils.RegionReadCPU: 0, } leader := &metapb.Peer{ Id: 100 + regionIDCounter, diff --git a/tools/pd-ctl/tests/scheduler/scheduler_test.go b/tools/pd-ctl/tests/scheduler/scheduler_test.go index df8f813f486..2c9aa00f1f3 100644 --- a/tools/pd-ctl/tests/scheduler/scheduler_test.go +++ b/tools/pd-ctl/tests/scheduler/scheduler_test.go @@ -700,6 +700,8 @@ func (suite *schedulerTestSuite) checkHotRegionSchedulerConfig(cluster *pdTests. "min-hot-byte-rate": float64(100), "min-hot-key-rate": float64(10), "min-hot-query-rate": float64(10), + "min-hot-cpu-rate": float64(10), + "pending-weight": float64(1), "src-tolerance-ratio": 1.05, "dst-tolerance-ratio": 1.05, "read-priorities": []any{"byte", "key"},