Skip to content

Commit 4e8f5a0

Browse files
authored
Support arbitrary statistic duration for flow control and refactor internal implementation (#200)
* Support arbitrary statistic duration for flow control. Add `StatIntervalInMs` attribute in flow.Rule. When StatIntervalInMs > globalInterval or < bucketLength, we create a new sliding window for it. * Add stat reuse mechanism for flow rules * Make threshold of flow rule "request amount per interval" rather than QPS. * Refine internal implementation
1 parent 7ddba92 commit 4e8f5a0

24 files changed

+834
-213
lines changed

adapter/echo/middleware_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ func initSentinel(t *testing.T) {
2424
Threshold: 1,
2525
TokenCalculateStrategy: flow.Direct,
2626
ControlBehavior: flow.Reject,
27+
StatIntervalInMs: 1000,
2728
},
2829
{
2930
Resource: "/api/:uid",
3031
Threshold: 0,
3132
TokenCalculateStrategy: flow.Direct,
3233
ControlBehavior: flow.Reject,
34+
StatIntervalInMs: 1000,
3335
},
3436
})
3537
if err != nil {

adapter/gin/middleware_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ func initSentinel(t *testing.T) {
2424
Threshold: 1,
2525
TokenCalculateStrategy: flow.Direct,
2626
ControlBehavior: flow.Reject,
27+
StatIntervalInMs: 1000,
2728
},
2829
{
2930
Resource: "/api/users/:id",
3031
Threshold: 0,
3132
TokenCalculateStrategy: flow.Direct,
3233
ControlBehavior: flow.Reject,
34+
StatIntervalInMs: 1000,
3335
},
3436
})
3537
if err != nil {

api/slot_chain.go

+1
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ func BuildDefaultSlotChain() *base.SlotChain {
3838
sc.AddStatSlotLast(&log.Slot{})
3939
sc.AddStatSlotLast(&circuitbreaker.MetricStatSlot{})
4040
sc.AddStatSlotLast(&hotspot.ConcurrencyStatSlot{})
41+
sc.AddStatSlotLast(&flow.StandaloneStatSlot{})
4142
return sc
4243
}

core/base/stat.go

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package base
22

3+
import (
4+
"github.com/pkg/errors"
5+
)
6+
37
type TimePredicate func(uint64) bool
48

59
type MetricEvent int8
@@ -31,7 +35,7 @@ type ReadStat interface {
3135
}
3236

3337
type WriteStat interface {
34-
AddMetric(event MetricEvent, count uint64)
38+
AddCount(event MetricEvent, count int64)
3539
}
3640

3741
// StatNode holds real-time statistics for resources.
@@ -46,4 +50,47 @@ type StatNode interface {
4650
DecreaseGoroutineNum()
4751

4852
Reset()
53+
54+
// GenerateReadStat generates the readonly metric statistic based on resource level global statistic
55+
// If parameters, sampleCount and intervalInMs, are not suitable for resource level global statistic, return (nil, error)
56+
GenerateReadStat(sampleCount uint32, intervalInMs uint32) (ReadStat, error)
57+
}
58+
59+
var (
60+
IllegalGlobalStatisticParamsError = errors.New("Invalid parameters, sampleCount or interval, for resource's global statistic")
61+
IllegalStatisticParamsError = errors.New("Invalid parameters, sampleCount or interval, for metric statistic")
62+
GlobalStatisticNonReusableError = errors.New("The parameters, sampleCount and interval, mismatch for reusing between resource's global statistic and readonly metric statistic.")
63+
)
64+
65+
func CheckValidityForStatistic(sampleCount, intervalInMs uint32) error {
66+
if intervalInMs == 0 || sampleCount == 0 || intervalInMs%sampleCount != 0 {
67+
return IllegalStatisticParamsError
68+
}
69+
return nil
70+
}
71+
72+
// CheckValidityForReuseStatistic check the compliance whether readonly metric statistic can be built based on resource's global statistic
73+
// The parameters, sampleCount and intervalInMs, are the parameters of the metric statistic you want to build
74+
// The parameters, parentSampleCount and parentIntervalInMs, are the parameters of the resource's global statistic
75+
// If compliance passes, return nil, if not returns specific error
76+
func CheckValidityForReuseStatistic(sampleCount, intervalInMs uint32, parentSampleCount, parentIntervalInMs uint32) error {
77+
if intervalInMs == 0 || sampleCount == 0 || intervalInMs%sampleCount != 0 {
78+
return IllegalStatisticParamsError
79+
}
80+
bucketLengthInMs := intervalInMs / sampleCount
81+
82+
if parentIntervalInMs == 0 || parentSampleCount == 0 || parentIntervalInMs%parentSampleCount != 0 {
83+
return IllegalGlobalStatisticParamsError
84+
}
85+
parentBucketLengthInMs := parentIntervalInMs / parentSampleCount
86+
87+
//SlidingWindowMetric's intervalInMs is not divisible by BucketLeapArray's intervalInMs
88+
if parentIntervalInMs%intervalInMs != 0 {
89+
return GlobalStatisticNonReusableError
90+
}
91+
// BucketLeapArray's BucketLengthInMs is not divisible by SlidingWindowMetric's BucketLengthInMs
92+
if bucketLengthInMs%parentBucketLengthInMs != 0 {
93+
return GlobalStatisticNonReusableError
94+
}
95+
return nil
4996
}

core/base/stat_test.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package base
22

3-
import "github.com/stretchr/testify/mock"
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/mock"
8+
)
49

510
type StatNodeMock struct {
611
mock.Mock
712
}
813

9-
func (m *StatNodeMock) AddMetric(event MetricEvent, count uint64) {
14+
func (m *StatNodeMock) AddCount(event MetricEvent, count int64) {
1015
m.Called(event, count)
1116
}
1217

@@ -64,3 +69,19 @@ func (m *StatNodeMock) Reset() {
6469
m.Called()
6570
return
6671
}
72+
73+
func (m *StatNodeMock) GenerateReadStat(sampleCount uint32, intervalInMs uint32) (ReadStat, error) {
74+
args := m.Called(sampleCount, intervalInMs)
75+
return args.Get(0).(ReadStat), args.Error(1)
76+
}
77+
78+
func TestCheckValidityForReuseStatistic(t *testing.T) {
79+
assert.Equal(t, CheckValidityForReuseStatistic(3, 1000, 20, 10000), IllegalStatisticParamsError)
80+
assert.Equal(t, CheckValidityForReuseStatistic(0, 1000, 20, 10000), IllegalStatisticParamsError)
81+
assert.Equal(t, CheckValidityForReuseStatistic(2, 1000, 21, 10000), IllegalGlobalStatisticParamsError)
82+
assert.Equal(t, CheckValidityForReuseStatistic(2, 1000, 0, 10000), IllegalGlobalStatisticParamsError)
83+
assert.Equal(t, CheckValidityForReuseStatistic(2, 8000, 20, 10000), GlobalStatisticNonReusableError)
84+
assert.Equal(t, CheckValidityForReuseStatistic(2, 1000, 10, 10000), GlobalStatisticNonReusableError)
85+
assert.Equal(t, CheckValidityForReuseStatistic(1, 1000, 100, 10000), nil)
86+
assert.Equal(t, CheckValidityForReuseStatistic(2, 1000, 20, 10000), nil)
87+
}

core/config/config.go

+19
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,22 @@ func SystemStatCollectIntervalMs() uint32 {
205205
func UseCacheTime() bool {
206206
return globalCfg.UseCacheTime()
207207
}
208+
209+
func GlobalStatisticIntervalMsTotal() uint32 {
210+
return globalCfg.GlobalStatisticIntervalMsTotal()
211+
}
212+
213+
func GlobalStatisticSampleCountTotal() uint32 {
214+
return globalCfg.GlobalStatisticSampleCountTotal()
215+
}
216+
217+
func GlobalStatisticBucketLengthInMs() uint32 {
218+
return globalCfg.GlobalStatisticIntervalMsTotal() / GlobalStatisticSampleCountTotal()
219+
}
220+
221+
func MetricStatisticIntervalMs() uint32 {
222+
return globalCfg.MetricStatisticIntervalMs()
223+
}
224+
func MetricStatisticSampleCount() uint32 {
225+
return globalCfg.MetricStatisticSampleCount()
226+
}

core/config/entity.go

+33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66

7+
"github.com/alibaba/sentinel-golang/core/base"
78
"github.com/alibaba/sentinel-golang/logging"
89
"github.com/pkg/errors"
910
)
@@ -52,6 +53,15 @@ type MetricLogConfig struct {
5253

5354
// StatConfig represents the configuration items of statistics.
5455
type StatConfig struct {
56+
// GlobalStatisticSampleCountTotal and GlobalStatisticIntervalMsTotal is the per resource's global default statistic sliding window config
57+
GlobalStatisticSampleCountTotal uint32 `yaml:"globalStatisticSampleCountTotal"`
58+
GlobalStatisticIntervalMsTotal uint32 `yaml:"globalStatisticIntervalMsTotal"`
59+
60+
// MetricStatisticSampleCount and MetricStatisticIntervalMs is the per resource's default readonly metric statistic
61+
// This default readonly metric statistic must be reusable based on global statistic.
62+
MetricStatisticSampleCount uint32 `yaml:"metricStatisticSampleCount"`
63+
MetricStatisticIntervalMs uint32 `yaml:"metricStatisticIntervalMs"`
64+
5565
System SystemStatConfig `yaml:"system"`
5666
}
5767

@@ -84,6 +94,10 @@ func NewDefaultConfig() *Entity {
8494
},
8595
},
8696
Stat: StatConfig{
97+
GlobalStatisticSampleCountTotal: base.DefaultSampleCountTotal,
98+
GlobalStatisticIntervalMsTotal: base.DefaultIntervalMsTotal,
99+
MetricStatisticSampleCount: base.DefaultSampleCount,
100+
MetricStatisticIntervalMs: base.DefaultIntervalMs,
87101
System: SystemStatConfig{
88102
CollectIntervalMs: DefaultSystemStatCollectIntervalMs,
89103
},
@@ -117,6 +131,10 @@ func checkConfValid(conf *SentinelConfig) error {
117131
if mc.SingleFileMaxSize <= 0 {
118132
return errors.New("Illegal metric log globalCfg: singleFileMaxSize <= 0")
119133
}
134+
if err := base.CheckValidityForReuseStatistic(conf.Stat.MetricStatisticSampleCount, conf.Stat.MetricStatisticIntervalMs,
135+
conf.Stat.GlobalStatisticSampleCountTotal, conf.Stat.GlobalStatisticIntervalMsTotal); err != nil {
136+
return err
137+
}
120138
return nil
121139
}
122140

@@ -168,3 +186,18 @@ func (entity *Entity) SystemStatCollectIntervalMs() uint32 {
168186
func (entity *Entity) UseCacheTime() bool {
169187
return entity.Sentinel.UseCacheTime
170188
}
189+
190+
func (entity *Entity) GlobalStatisticIntervalMsTotal() uint32 {
191+
return entity.Sentinel.Stat.GlobalStatisticIntervalMsTotal
192+
}
193+
194+
func (entity *Entity) GlobalStatisticSampleCountTotal() uint32 {
195+
return entity.Sentinel.Stat.GlobalStatisticSampleCountTotal
196+
}
197+
198+
func (entity *Entity) MetricStatisticIntervalMs() uint32 {
199+
return entity.Sentinel.Stat.MetricStatisticIntervalMs
200+
}
201+
func (entity *Entity) MetricStatisticSampleCount() uint32 {
202+
return entity.Sentinel.Stat.MetricStatisticSampleCount
203+
}

core/flow/rule.go

+41-9
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,54 @@ type Rule struct {
7070
Resource string `json:"resource"`
7171
TokenCalculateStrategy TokenCalculateStrategy `json:"tokenCalculateStrategy"`
7272
ControlBehavior ControlBehavior `json:"controlBehavior"`
73-
Threshold float64 `json:"threshold"`
74-
RelationStrategy RelationStrategy `json:"relationStrategy"`
75-
RefResource string `json:"refResource"`
76-
MaxQueueingTimeMs uint32 `json:"maxQueueingTimeMs"`
77-
WarmUpPeriodSec uint32 `json:"warmUpPeriodSec"`
78-
WarmUpColdFactor uint32 `json:"warmUpColdFactor"`
73+
// Threshold means the threshold during StatIntervalInMs
74+
// If StatIntervalInMs is 1000(1 second), Threshold means QPS
75+
Threshold float64 `json:"threshold"`
76+
RelationStrategy RelationStrategy `json:"relationStrategy"`
77+
RefResource string `json:"refResource"`
78+
MaxQueueingTimeMs uint32 `json:"maxQueueingTimeMs"`
79+
WarmUpPeriodSec uint32 `json:"warmUpPeriodSec"`
80+
WarmUpColdFactor uint32 `json:"warmUpColdFactor"`
81+
// StatIntervalInMs indicates the statistic interval and it's the optional setting for flow Rule.
82+
// If user doesn't set StatIntervalInMs, that means using default metric statistic of resource.
83+
// If the StatIntervalInMs user specifies can not reuse the global statistic of resource,
84+
// sentinel will generate independent statistic structure for this rule.
85+
StatIntervalInMs uint32 `json:"statIntervalInMs"`
86+
}
87+
88+
func (r *Rule) isEqualsTo(newRule *Rule) bool {
89+
if newRule == nil {
90+
return false
91+
}
92+
if !(r.Resource == newRule.Resource && r.RelationStrategy == newRule.RelationStrategy &&
93+
r.RefResource == newRule.RefResource && r.StatIntervalInMs == newRule.StatIntervalInMs &&
94+
r.TokenCalculateStrategy == newRule.TokenCalculateStrategy && r.ControlBehavior == newRule.ControlBehavior && r.Threshold == newRule.Threshold &&
95+
r.MaxQueueingTimeMs == newRule.MaxQueueingTimeMs && r.WarmUpPeriodSec == newRule.WarmUpPeriodSec && r.WarmUpColdFactor == newRule.WarmUpColdFactor) {
96+
return false
97+
}
98+
return true
99+
}
100+
101+
func (r *Rule) isStatReusable(newRule *Rule) bool {
102+
if newRule == nil {
103+
return false
104+
}
105+
return r.Resource == newRule.Resource && r.RelationStrategy == newRule.RelationStrategy &&
106+
r.RefResource == newRule.RefResource && r.StatIntervalInMs == newRule.StatIntervalInMs
107+
}
108+
109+
func (r *Rule) needStatistic() bool {
110+
return !(r.TokenCalculateStrategy == Direct && r.ControlBehavior == Throttling)
79111
}
80112

81113
func (r *Rule) String() string {
82114
b, err := json.Marshal(r)
83115
if err != nil {
84116
// Return the fallback string
85117
return fmt.Sprintf("Rule{Resource=%s, TokenCalculateStrategy=%s, ControlBehavior=%s, "+
86-
"Threshold=%.2f, RelationStrategy=%s, RefResource=%s, MaxQueueingTimeMs=%d, WarmUpPeriodSec=%d, WarmUpColdFactor=%d}",
87-
r.Resource, r.TokenCalculateStrategy, r.ControlBehavior, r.Threshold, r.RelationStrategy,
88-
r.RefResource, r.MaxQueueingTimeMs, r.WarmUpPeriodSec, r.WarmUpColdFactor)
118+
"Threshold=%.2f, RelationStrategy=%s, RefResource=%s, MaxQueueingTimeMs=%d, WarmUpPeriodSec=%d, WarmUpColdFactor=%d, StatIntervalInMs=%d}",
119+
r.Resource, r.TokenCalculateStrategy, r.ControlBehavior, r.Threshold, r.RelationStrategy, r.RefResource,
120+
r.MaxQueueingTimeMs, r.WarmUpPeriodSec, r.WarmUpColdFactor, r.StatIntervalInMs)
89121
}
90122
return string(b)
91123
}

0 commit comments

Comments
 (0)