Repository: alibaba/sentinel-golang
Pull request #376 (2021-01-10 07:22:54)

feat: hotspot rule add paramKey;
<!--  Thanks for submitting a pull request! Here are some tips for you:
1. Please make sure you have read and understood the contributing guidelines: https://github.com/alibaba/blob/master/CONTRIBUTING.md
2. Please make sure the PR has a corresponding issue.
-->
In the hotspot parameter rule, ParamKey is added to cooperate with the context attachments map to quickly specify the value that requires hotspot current limit from a large number of key-value pairs.
(alibaba#360)

### Describe what this PR does / why we need it
It is convenient to specify hot spot parameters in hot spot current limiting rules. For example, there are a lot of data for system parameters, calibration parameters in header, and business parameters, and the parameters required by different businesses are different. It is very convenient to add all the data into ctx.attachments and specify the key in the rules.

### Does this pull request fix one issue?
This time pr has added ParamKey, and modified the corresponding unit test and example.

<!--If that, add "Fixes #xxxx" below in the next line. For example, Fixes #15. Otherwise, add "NONE" -->

### Describe how you did it
ParamKey is added to the rule; trafficShaper also implements the extractArg interface correspondingly, which can be used with the existing paramIndex to easily specify hotspot parameters.

### Describe how to verify it
Verified in unit test and example

### Special notes for reviews
ParamKey can be considered as []string, so that users can specify hotspot rules flexibly in addition to specifying a single parameter. 
For example, ParamKey=[]string{"uid","merchant_id"} This allows uid and merchant_id to be used as joint parameters as hot spots.

codecov-io: # [Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=h1) Report
> Merging [#376](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=desc) (ffa0195) into [master](https://codecov.io/gh/alibaba/sentinel-golang/commit/aafce3f11390fbad986eaa21cc81463a1fb8bd40?el=desc) (aafce3f) will **decrease** coverage by `0.11%`.
> The diff coverage is `38.77%`.

[![Impacted file tree graph](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/graphs/tree.svg?width=650&height=150&src=pr&token=SrpzbhKA3E)](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #376      +/-   ##
==========================================
- Coverage   51.42%   51.30%   -0.12%     
==========================================
  Files          70       70              
  Lines        4331     4358      +27     
==========================================
+ Hits         2227     2236       +9     
- Misses       1806     1819      +13     
- Partials      298      303       +5     
```


| [Impacted Files](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [core/hotspot/concurrency\_stat\_slot.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L2NvbmN1cnJlbmN5X3N0YXRfc2xvdC5nbw==) | `6.66% <0.00%> (+0.41%)` | :arrow_up: |
| [core/hotspot/rule\_manager.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L3J1bGVfbWFuYWdlci5nbw==) | `57.93% <0.00%> (-0.51%)` | :arrow_down: |
| [core/hotspot/slot.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L3Nsb3QuZ28=) | `9.52% <0.00%> (-18.26%)` | :arrow_down: |
| [core/hotspot/traffic\_shaping.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L3RyYWZmaWNfc2hhcGluZy5nbw==) | `57.71% <40.47%> (-4.56%)` | :arrow_down: |
| [core/hotspot/rule.go](https://codecov.io/gh/alibaba/sentinel-golang/pull/376/diff?src=pr&el=tree#diff-Y29yZS9ob3RzcG90L3J1bGUuZ28=) | `66.66% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=footer). Last update [aafce3f...ffa0195](https://codecov.io/gh/alibaba/sentinel-golang/pull/376?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/core/hotspot/concurrency_stat_slot.go b/core/hotspot/concurrency_stat_slot.go
index ad64ae1..43d3c74 100644
--- a/core/hotspot/concurrency_stat_slot.go
+++ b/core/hotspot/concurrency_stat_slot.go
@@ -44,13 +44,12 @@ func (s *ConcurrencyStatSlot) Order() uint32 {
 
 func (c *ConcurrencyStatSlot) OnEntryPassed(ctx *base.EntryContext) {
 	res := ctx.Resource.Name()
-	args := ctx.Input.Args
 	tcs := getTrafficControllersFor(res)
 	for _, tc := range tcs {
 		if tc.BoundRule().MetricType != Concurrency {
 			continue
 		}
-		arg := matchArg(tc, args)
+		arg := tc.ExtractArgs(ctx)
 		if arg == nil {
 			continue
 		}
@@ -72,13 +71,12 @@ func (c *ConcurrencyStatSlot) OnEntryBlocked(ctx *base.EntryContext, blockError
 
 func (c *ConcurrencyStatSlot) OnCompleted(ctx *base.EntryContext) {
 	res := ctx.Resource.Name()
-	args := ctx.Input.Args
 	tcs := getTrafficControllersFor(res)
 	for _, tc := range tcs {
 		if tc.BoundRule().MetricType != Concurrency {
 			continue
 		}
-		arg := matchArg(tc, args)
+		arg := tc.ExtractArgs(ctx)
 		if arg == nil {
 			continue
 		}
diff --git a/core/hotspot/rule.go b/core/hotspot/rule.go
index 97917bb..f9dc6d1 100644
--- a/core/hotspot/rule.go
+++ b/core/hotspot/rule.go
@@ -80,6 +80,10 @@ type Rule struct {
 	// if ParamIndex is great than or equals to zero, ParamIndex means the <ParamIndex>-th parameter
 	// if ParamIndex is the negative, ParamIndex means the reversed <ParamIndex>-th parameter
 	ParamIndex int `json:"paramIndex"`
+	// ParamKey is the key in EntryContext.Input.Attachments map.
+	// ParamKey can be used as a supplement to ParamIndex to facilitate rules to quickly obtain parameter from a large number of parameters
+	// ParamKey is mutually exclusive with ParamIndex, ParamKey has the higher priority than ParamIndex
+	ParamKey string `json:"paramKey"`
 	// Threshold is the threshold to trigger rejection
 	Threshold int64 `json:"threshold"`
 	// MaxQueueingTimeMs only takes effect when ControlBehavior is Throttling and MetricType is QPS
@@ -100,8 +104,8 @@ func (r *Rule) String() string {
 	b, err := json.Marshal(r)
 	if err != nil {
 		// Return the fallback string
-		return fmt.Sprintf("{Id:%s, Resource:%s, MetricType:%+v, ControlBehavior:%+v, ParamIndex:%d, Threshold:%d, MaxQueueingTimeMs:%d, BurstCount:%d, DurationInSec:%d, ParamsMaxCapacity:%d, SpecificItems:%+v}",
-			r.ID, r.Resource, r.MetricType, r.ControlBehavior, r.ParamIndex, r.Threshold, r.MaxQueueingTimeMs, r.BurstCount, r.DurationInSec, r.ParamsMaxCapacity, r.SpecificItems)
+		return fmt.Sprintf("{Id:%s, Resource:%s, MetricType:%+v, ControlBehavior:%+v, ParamIndex:%d, ParamKey:%s, Threshold:%d, MaxQueueingTimeMs:%d, BurstCount:%d, DurationInSec:%d, ParamsMaxCapacity:%d, SpecificItems:%+v}",
+			r.ID, r.Resource, r.MetricType, r.ControlBehavior, r.ParamIndex, r.ParamKey, r.Threshold, r.MaxQueueingTimeMs, r.BurstCount, r.DurationInSec, r.ParamsMaxCapacity, r.SpecificItems)
 	}
 	return string(b)
 }
@@ -116,7 +120,7 @@ func (r *Rule) IsStatReusable(newRule *Rule) bool {
 
 // Equals checks whether current rule is consistent with the given rule.
 func (r *Rule) Equals(newRule *Rule) bool {
-	baseCheck := r.Resource == newRule.Resource && r.MetricType == newRule.MetricType && r.ControlBehavior == newRule.ControlBehavior && r.ParamsMaxCapacity == newRule.ParamsMaxCapacity && r.ParamIndex == newRule.ParamIndex && r.Threshold == newRule.Threshold && r.DurationInSec == newRule.DurationInSec && reflect.DeepEqual(r.SpecificItems, newRule.SpecificItems)
+	baseCheck := r.Resource == newRule.Resource && r.MetricType == newRule.MetricType && r.ControlBehavior == newRule.ControlBehavior && r.ParamsMaxCapacity == newRule.ParamsMaxCapacity && r.ParamIndex == newRule.ParamIndex && r.ParamKey == newRule.ParamKey && r.Threshold == newRule.Threshold && r.DurationInSec == newRule.DurationInSec && reflect.DeepEqual(r.SpecificItems, newRule.SpecificItems)
 	if !baseCheck {
 		return false
 	}
diff --git a/core/hotspot/rule_manager.go b/core/hotspot/rule_manager.go
index 3c89b3d..26f478f 100644
--- a/core/hotspot/rule_manager.go
+++ b/core/hotspot/rule_manager.go
@@ -425,12 +425,12 @@ func IsValidRule(rule *Rule) error {
 	if rule.ControlBehavior < 0 {
 		return errors.New("invalid control strategy")
 	}
-	if rule.ParamIndex < 0 {
-		return errors.New("invalid param index")
-	}
 	if rule.MetricType == QPS && rule.DurationInSec <= 0 {
 		return errors.New("invalid duration")
 	}
+	if rule.ParamIndex > 0 && rule.ParamKey != "" {
+		return errors.New("invalid param index and param key are mutually exclusive")
+	}
 	return checkControlBehaviorField(rule)
 }
 
diff --git a/core/hotspot/slot.go b/core/hotspot/slot.go
index 26fc4c8..f64e97c 100644
--- a/core/hotspot/slot.go
+++ b/core/hotspot/slot.go
@@ -16,7 +16,6 @@ package hotspot
 
 import (
 	"github.com/alibaba/sentinel-golang/core/base"
-	"github.com/alibaba/sentinel-golang/logging"
 	"github.com/alibaba/sentinel-golang/util"
 )
 
@@ -40,40 +39,14 @@ func (s *Slot) Order() uint32 {
 	return RuleCheckSlotOrder
 }
 
-// matchArg matches the arg from args based on TrafficShapingController
-// return nil if match failed.
-func matchArg(tc TrafficShapingController, args []interface{}) interface{} {
-	if tc == nil {
-		return nil
-	}
-	idx := tc.BoundParamIndex()
-	if idx < 0 {
-		idx = len(args) + idx
-	}
-	if idx < 0 {
-		if logging.DebugEnabled() {
-			logging.Debug("[Slot matchArg] The param index of hotspot traffic shaping controller is invalid", "args", args, "paramIndex", tc.BoundParamIndex())
-		}
-		return nil
-	}
-	if idx >= len(args) {
-		if logging.DebugEnabled() {
-			logging.Debug("[Slot matchArg] The argument in index doesn't exist", "args", args, "paramIndex", tc.BoundParamIndex())
-		}
-		return nil
-	}
-	return args[idx]
-}
-
 func (s *Slot) Check(ctx *base.EntryContext) *base.TokenResult {
 	res := ctx.Resource.Name()
-	args := ctx.Input.Args
 	batch := int64(ctx.Input.BatchCount)
 
 	result := ctx.RuleCheckResult
 	tcs := getTrafficControllersFor(res)
 	for _, tc := range tcs {
-		arg := matchArg(tc, args)
+		arg := tc.ExtractArgs(ctx)
 		if arg == nil {
 			continue
 		}
diff --git a/core/hotspot/traffic_shaping.go b/core/hotspot/traffic_shaping.go
index 7ba5865..dbe163e 100644
--- a/core/hotspot/traffic_shaping.go
+++ b/core/hotspot/traffic_shaping.go
@@ -33,6 +33,8 @@ type TrafficShapingController interface {
 
 	BoundParamIndex() int
 
+	ExtractArgs(ctx *base.EntryContext) interface{}
+
 	BoundMetric() *ParamsMetric
 
 	BoundRule() *Rule
@@ -44,6 +46,7 @@ type baseTrafficShapingController struct {
 	res           string
 	metricType    MetricType
 	paramIndex    int
+	paramKey      string
 	threshold     int64
 	specificItems map[interface{}]int64
 	durationInSec int64
@@ -60,6 +63,7 @@ func newBaseTrafficShapingControllerWithMetric(r *Rule, metric *ParamsMetric) *b
 		res:           r.Resource,
 		metricType:    r.MetricType,
 		paramIndex:    r.ParamIndex,
+		paramKey:      r.ParamKey,
 		threshold:     r.Threshold,
 		specificItems: r.SpecificItems,
 		durationInSec: r.DurationInSec,
@@ -155,6 +159,72 @@ func (c *baseTrafficShapingController) BoundParamIndex() int {
 	return c.paramIndex
 }
 
+// ExtractArgs matches the arg from ctx based on TrafficShapingController
+// return nil if match failed.
+func (c *baseTrafficShapingController) ExtractArgs(ctx *base.EntryContext) (value interface{}) {
+	if c == nil {
+		return nil
+	}
+	value = c.extractAttachmentArgs(ctx)
+	if value != nil {
+		return
+	}
+	value = c.extractArgs(ctx)
+	if value != nil {
+		return
+	}
+	return
+}
+func (c *baseTrafficShapingController) extractArgs(ctx *base.EntryContext) interface{} {
+	args := ctx.Input.Args
+	idx := c.BoundParamIndex()
+	if idx < 0 {
+		idx = len(args) + idx
+	}
+	if idx < 0 {
+		if logging.DebugEnabled() {
+			logging.Debug("[extractArgs] The param index of hotspot traffic shaping controller is invalid",
+				"args", args, "paramIndex", c.BoundParamIndex())
+		}
+		return nil
+	}
+	if idx >= len(args) {
+		if logging.DebugEnabled() {
+			logging.Debug("[extractArgs] The argument in index doesn't exist",
+				"args", args, "paramIndex", c.BoundParamIndex())
+		}
+		return nil
+	}
+	return args[idx]
+}
+func (c *baseTrafficShapingController) extractAttachmentArgs(ctx *base.EntryContext) interface{} {
+	attachments := ctx.Input.Attachments
+
+	if attachments == nil {
+		if logging.DebugEnabled() {
+			logging.Debug("[paramKey] The attachments of ctx is nil",
+				"args", attachments, "paramKey", c.paramKey)
+		}
+		return nil
+	}
+	if c.paramKey == "" {
+		if logging.DebugEnabled() {
+			logging.Debug("[paramKey] The param key is nil",
+				"args", attachments, "paramKey", c.paramKey)
+		}
+		return nil
+	}
+	arg, ok := attachments[c.paramKey]
+	if !ok {
+		if logging.DebugEnabled() {
+			logging.Debug("[paramKey] extracted data does not exist",
+				"args", attachments, "paramKey", c.paramKey)
+		}
+	}
+
+	return arg
+}
+
 func (c *rejectTrafficShapingController) PerformChecking(arg interface{}, batchCount int64) *base.TokenResult {
 	metric := c.metric
 	if metric == nil {
diff --git a/example/hotspot_param_flow/concurrency/hotspot_params_concurrency_example.go b/example/hotspot_param_flow/concurrency/hotspot_params_concurrency_example.go
index f9ea4e9..64e8d00 100644
--- a/example/hotspot_param_flow/concurrency/hotspot_params_concurrency_example.go
+++ b/example/hotspot_param_flow/concurrency/hotspot_params_concurrency_example.go
@@ -35,6 +35,7 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
+	testKey := "testKey"
 
 	// the max concurrency is 8
 	_, err = hotspot.LoadRules([]*hotspot.Rule{
@@ -42,6 +43,7 @@ func main() {
 			Resource:      "abc",
 			MetricType:    hotspot.Concurrency,
 			ParamIndex:    0,
+			ParamKey:      testKey,
 			Threshold:     8,
 			DurationInSec: 0,
 		},
@@ -79,7 +81,12 @@ func main() {
 	}
 
 	for {
-		e, b := sentinel.Entry("abc", sentinel.WithArgs(false, uint32(9)))
+		e, b := sentinel.Entry("abc",
+			sentinel.WithArgs(false, uint32(9),
+				sentinel.WithAttachments(map[interface{}]interface{}{
+					testKey: rand.Uint64() % 10,
+				}),
+			))
 		if b != nil {
 			// Blocked. We could get the block reason from the BlockError.
 			time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
diff --git a/example/hotspot_param_flow/qps_reject/hotspot_params_qps_reject_example.go b/example/hotspot_param_flow/qps_reject/hotspot_params_qps_reject_example.go
index 0e38e24..30e1d63 100644
--- a/example/hotspot_param_flow/qps_reject/hotspot_params_qps_reject_example.go
+++ b/example/hotspot_param_flow/qps_reject/hotspot_params_qps_reject_example.go
@@ -15,10 +15,14 @@
 package main
 
 import (
+	"fmt"
 	"log"
 	"math/rand"
 	"time"
 
+	"github.com/alibaba/sentinel-golang/core/base"
+	"github.com/alibaba/sentinel-golang/core/stat"
+
 	sentinel "github.com/alibaba/sentinel-golang/api"
 	"github.com/alibaba/sentinel-golang/core/config"
 	"github.com/alibaba/sentinel-golang/core/hotspot"
@@ -37,6 +41,8 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
+	rand.Seed(time.Now().UnixNano())
+	testKey := "testKey"
 
 	_, err = hotspot.LoadRules([]*hotspot.Rule{
 		{
@@ -57,11 +63,36 @@ func main() {
 			BurstCount:      0,
 			DurationInSec:   1,
 		},
+		{
+			Resource:        "efg",
+			MetricType:      hotspot.QPS,
+			ControlBehavior: hotspot.Reject,
+			ParamKey:        testKey,
+			Threshold:       50,
+			BurstCount:      0,
+			DurationInSec:   1,
+		},
 	})
 	if err != nil {
 		log.Fatalf("Unexpected error: %+v", err)
 		return
 	}
+	for _, resource := range []string{"abc", "def", "efg"} {
+		go func(name string) {
+			node := stat.GetOrCreateResourceNode(name, base.ResTypeCommon)
+			for {
+				logging.Info("[HotSpot QPS] "+name,
+					"pass", node.GetQPS(base.MetricEventPass),
+					"block", node.GetQPS(base.MetricEventBlock),
+					"complete", node.GetQPS(base.MetricEventComplete),
+					"error", node.GetQPS(base.MetricEventError),
+					"rt", node.GetQPS(base.MetricEventRt),
+					//"\n total", node.GetQPS(base.MetricEventTotal),
+				)
+				time.Sleep(time.Duration(1000) * time.Millisecond)
+			}
+		}(resource)
+	}
 
 	logging.Info("[HotSpot Reject] Sentinel Go hot-spot param flow control demo is running. You may see the pass/block metric in the metric log.")
 	for i := 0; i < 10; i++ {
@@ -81,15 +112,33 @@ func main() {
 			}
 		}()
 	}
+	go func() {
+		for {
+			e, b := sentinel.Entry("def", sentinel.WithArgs(false, 9, "ahas", fooStruct{rand.Int63() % 5}))
+			if b != nil {
+				// Blocked. We could get the block reason from the BlockError.
+				time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
+			} else {
+				// Passed, wrap the logic here.
+				time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
+				// Be sure the entry is exited finally.
+				e.Exit()
+			}
+		}
+	}()
 
 	for {
-		e, b := sentinel.Entry("def", sentinel.WithArgs(false, 9, "ahas", fooStruct{rand.Int63() % 5}))
+		val := fmt.Sprintf("test%v", rand.Int31()%10)
+		e, b := sentinel.Entry("efg",
+			sentinel.WithAttachments(map[interface{}]interface{}{
+				testKey: val,
+			}))
 		if b != nil {
 			// Blocked. We could get the block reason from the BlockError.
 			time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
 		} else {
 			// Passed, wrap the logic here.
-			time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)
+			time.Sleep(time.Duration(rand.Uint64()%2) * time.Millisecond)
 			// Be sure the entry is exited finally.
 			e.Exit()
 		}
@@ -97,4 +146,5 @@ func main() {
 
 	// The QPS of abc is about: 1500
 	// The QPS of def is about: 50
+	// The QPS of efg is about: 500
 }


Test patch:
diff --git a/core/hotspot/rule_test.go b/core/hotspot/rule_test.go
index 97e1288..a1af9f2 100644
--- a/core/hotspot/rule_test.go
+++ b/core/hotspot/rule_test.go
@@ -39,6 +39,7 @@ func Test_Rule_String(t *testing.T) {
 			MetricType:        Concurrency,
 			ControlBehavior:   Reject,
 			ParamIndex:        0,
+			ParamKey:          "key",
 			Threshold:         110.0,
 			MaxQueueingTimeMs: 5,
 			BurstCount:        10,
@@ -46,7 +47,7 @@ func Test_Rule_String(t *testing.T) {
 			ParamsMaxCapacity: 10000,
 			SpecificItems:     specific,
 		}
-		assert.True(t, fmt.Sprintf("%+v", []*Rule{r}) == "[{Id:abc, Resource:abc, MetricType:Concurrency, ControlBehavior:Reject, ParamIndex:0, Threshold:110, MaxQueueingTimeMs:5, BurstCount:10, DurationInSec:1, ParamsMaxCapacity:10000, SpecificItems:map[1123:3 sss:1]}]")
+		assert.True(t, fmt.Sprintf("%+v", []*Rule{r}) == "[{Id:abc, Resource:abc, MetricType:Concurrency, ControlBehavior:Reject, ParamIndex:0, ParamKey:key, Threshold:110, MaxQueueingTimeMs:5, BurstCount:10, DurationInSec:1, ParamsMaxCapacity:10000, SpecificItems:map[1123:3 sss:1]}]")
 	})
 }
 
@@ -61,6 +62,7 @@ func Test_Rule_Equals(t *testing.T) {
 			MetricType:        Concurrency,
 			ControlBehavior:   Reject,
 			ParamIndex:        0,
+			ParamKey:          "testKey",
 			Threshold:         110.0,
 			MaxQueueingTimeMs: 5,
 			BurstCount:        10,
@@ -78,6 +80,7 @@ func Test_Rule_Equals(t *testing.T) {
 			MetricType:        Concurrency,
 			ControlBehavior:   Reject,
 			ParamIndex:        0,
+			ParamKey:          "testKey",
 			Threshold:         110.0,
 			MaxQueueingTimeMs: 5,
 			BurstCount:        10,
diff --git a/core/hotspot/slot_test.go b/core/hotspot/slot_test.go
index 717d52a..2cfa9e7 100644
--- a/core/hotspot/slot_test.go
+++ b/core/hotspot/slot_test.go
@@ -15,11 +15,7 @@
 package hotspot
 
 import (
-	"reflect"
-	"testing"
-
 	"github.com/alibaba/sentinel-golang/core/base"
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
 
@@ -52,53 +48,8 @@ func (m *TrafficShapingControllerMock) Replace(r *Rule) {
 	return
 }
 
-func Test_matchArg(t *testing.T) {
-	t.Run("Test_matchArg", func(t *testing.T) {
-		args := make([]interface{}, 10)
-		args[0] = 1
-		args[1] = 2
-		args[2] = 3
-		args[3] = 4
-		args[4] = 5
-		args[5] = 6
-		args[6] = 7
-		args[7] = 8
-		args[8] = 9
-		args[9] = 10
-
-		tcMock := &TrafficShapingControllerMock{}
-		tcMock.On("BoundParamIndex").Return(0)
-		ret0 := matchArg(tcMock, args)
-		assert.True(t, reflect.DeepEqual(ret0, 1))
-
-		tcMock1 := &TrafficShapingControllerMock{}
-		tcMock1.On("BoundParamIndex").Return(5)
-		ret1 := matchArg(tcMock1, args)
-		assert.True(t, reflect.DeepEqual(ret1, 6))
-
-		tcMock2 := &TrafficShapingControllerMock{}
-		tcMock2.On("BoundParamIndex").Return(9)
-		ret2 := matchArg(tcMock2, args)
-		assert.True(t, reflect.DeepEqual(ret2, 10))
-
-		tcMock3 := &TrafficShapingControllerMock{}
-		tcMock3.On("BoundParamIndex").Return(-1)
-		ret3 := matchArg(tcMock3, args)
-		assert.True(t, reflect.DeepEqual(ret3, 10))
-
-		tcMock4 := &TrafficShapingControllerMock{}
-		tcMock4.On("BoundParamIndex").Return(-10)
-		ret4 := matchArg(tcMock4, args)
-		assert.True(t, reflect.DeepEqual(ret4, 1))
-
-		tcMock5 := &TrafficShapingControllerMock{}
-		tcMock5.On("BoundParamIndex").Return(10)
-		ret5 := matchArg(tcMock5, args)
-		assert.True(t, ret5 == nil)
-
-		tcMock6 := &TrafficShapingControllerMock{}
-		tcMock6.On("BoundParamIndex").Return(-11)
-		ret6 := matchArg(tcMock6, args)
-		assert.True(t, ret6 == nil)
-	})
+func (m *TrafficShapingControllerMock) ExtractArgs(ctx *base.EntryContext) []interface{} {
+	_ = m.Called()
+	ret := []interface{}{ctx.Input.Args[m.BoundParamIndex()]}
+	return ret
 }
diff --git a/core/hotspot/traffic_shaping_test.go b/core/hotspot/traffic_shaping_test.go
index 9f20c94..3369b01 100644
--- a/core/hotspot/traffic_shaping_test.go
+++ b/core/hotspot/traffic_shaping_test.go
@@ -15,10 +15,12 @@
 package hotspot
 
 import (
+	"reflect"
 	"sync/atomic"
 	"testing"
 	"time"
 
+	"github.com/alibaba/sentinel-golang/core/base"
 	"github.com/alibaba/sentinel-golang/util"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -321,3 +323,54 @@ func Test_newBaseTrafficShapingController(t *testing.T) {
 		assert.True(t, tc.metric.RuleTokenCounter.Len() == ParamsMaxCapacity)
 	})
 }
+
+func Test_baseTrafficShapingController_ExtractArgs(t *testing.T) {
+	t.Run("Test_baseTrafficShapingController_ExtractArgs", func(t *testing.T) {
+
+		c := &baseTrafficShapingController{}
+
+		args := make([]interface{}, 10)
+		attachments := make(map[interface{}]interface{})
+		ctx := base.NewEmptyEntryContext()
+		ctx.Input = &base.SentinelInput{
+			BatchCount:  1,
+			Flag:        0,
+			Args:        args,
+			Attachments: attachments,
+		}
+		// no data
+		ret := c.ExtractArgs(ctx)
+		assert.Nil(t, ret)
+
+		// set data
+		args[0] = 1
+		args[1] = 2
+		value1 := "v1"
+		attachments["test1"] = value1
+
+		// set index or key
+		// exist
+		c.paramIndex = 0
+		c.paramKey = "test1"
+		ret = c.ExtractArgs(ctx)
+		assert.True(t, reflect.DeepEqual(ret, value1), ret)
+
+		// part exist 1
+		c.paramIndex = 10
+		c.paramKey = "test1"
+		ret = c.ExtractArgs(ctx)
+		assert.True(t, reflect.DeepEqual(ret, value1), ret)
+
+		// part exist 2
+		c.paramIndex = 1
+		c.paramKey = "test2"
+		ret = c.ExtractArgs(ctx)
+		assert.True(t, reflect.DeepEqual(ret, 2), ret)
+
+		// not exist
+		c.paramIndex = 3
+		c.paramKey = "test2"
+		ret = c.ExtractArgs(ctx)
+		assert.Nil(t, ret)
+	})
+}
diff --git a/ext/datasource/helper_test.go b/ext/datasource/helper_test.go
index 3d55b26..becdd88 100644
--- a/ext/datasource/helper_test.go
+++ b/ext/datasource/helper_test.go
@@ -404,10 +404,10 @@ func TestHotSpotParamRuleJsonArrayParser(t *testing.T) {
 		for _, r := range rules {
 			fmt.Println(r)
 		}
-		assert.True(t, strings.Contains(rules[0].String(), "Resource:abc, MetricType:Concurrency, ControlBehavior:Reject, ParamIndex:0, Threshold:1000, MaxQueueingTimeMs:1, BurstCount:10, DurationInSec:1, ParamsMaxCapacity:10000, SpecificItems:map[true:10003 1000:10001 ximu:10002]"))
-		assert.True(t, strings.Contains(rules[1].String(), "Resource:abc, MetricType:Concurrency, ControlBehavior:Throttling, ParamIndex:1, Threshold:2000, MaxQueueingTimeMs:2, BurstCount:20, DurationInSec:2, ParamsMaxCapacity:20000, SpecificItems:map[true:20003 1000:20001 ximu:20002"))
-		assert.True(t, strings.Contains(rules[2].String(), "Resource:abc, MetricType:QPS, ControlBehavior:Reject, ParamIndex:2, Threshold:3000, MaxQueueingTimeMs:3, BurstCount:30, DurationInSec:3, ParamsMaxCapacity:30000, SpecificItems:map[true:30003 1000:30001 ximu:30002"))
-		assert.True(t, strings.Contains(rules[3].String(), "Resource:abc, MetricType:QPS, ControlBehavior:Throttling, ParamIndex:3, Threshold:4000, MaxQueueingTimeMs:4, BurstCount:40, DurationInSec:4, ParamsMaxCapacity:40000, SpecificItems:map[true:40003 1000:40001 ximu:40002"))
+		assert.True(t, strings.Contains(rules[0].String(), "Resource:abc, MetricType:Concurrency, ControlBehavior:Reject, ParamIndex:0, ParamKey:, Threshold:1000, MaxQueueingTimeMs:1, BurstCount:10, DurationInSec:1, ParamsMaxCapacity:10000, SpecificItems:map[true:10003 1000:10001 ximu:10002]"))
+		assert.True(t, strings.Contains(rules[1].String(), "Resource:abc, MetricType:Concurrency, ControlBehavior:Throttling, ParamIndex:1, ParamKey:, Threshold:2000, MaxQueueingTimeMs:2, BurstCount:20, DurationInSec:2, ParamsMaxCapacity:20000, SpecificItems:map[true:20003 1000:20001 ximu:20002"))
+		assert.True(t, strings.Contains(rules[2].String(), "Resource:abc, MetricType:QPS, ControlBehavior:Reject, ParamIndex:2, ParamKey:, Threshold:3000, MaxQueueingTimeMs:3, BurstCount:30, DurationInSec:3, ParamsMaxCapacity:30000, SpecificItems:map[true:30003 1000:30001 ximu:30002"))
+		assert.True(t, strings.Contains(rules[3].String(), "Resource:abc, MetricType:QPS, ControlBehavior:Throttling, ParamIndex:3, ParamKey:, Threshold:4000, MaxQueueingTimeMs:4, BurstCount:40, DurationInSec:4, ParamsMaxCapacity:40000, SpecificItems:map[true:40003 1000:40001 ximu:40002"))
 	})
 
 	t.Run("TestHotSpotParamRuleJsonArrayParser_Nil", func(t *testing.T) {
diff --git a/tests/benchmark/hotspot/hotspot_benchmark_test.go b/tests/benchmark/hotspot/hotspot_benchmark_test.go
index fca75bb..33eea7c 100644
--- a/tests/benchmark/hotspot/hotspot_benchmark_test.go
+++ b/tests/benchmark/hotspot/hotspot_benchmark_test.go
@@ -15,10 +15,15 @@
 package hotspot
 
 import (
+	"fmt"
 	"log"
 	"math"
+	"strconv"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/alibaba/sentinel-golang/api"
 	sentinel "github.com/alibaba/sentinel-golang/api"
 	"github.com/alibaba/sentinel-golang/core/hotspot"
 	"github.com/alibaba/sentinel-golang/tests/benchmark"
@@ -30,12 +35,18 @@ const (
 	resThrottling  = "resThrottling"
 )
 
+var (
+	hotspotParamKey   = "hotspotParamKey"
+	hotspotParamValue = "hotspotParamValue"
+)
+
 func init() {
 	benchmark.InitSentinel()
 	rule1 := &hotspot.Rule{
 		Resource:      resConcurrency,
 		MetricType:    hotspot.Concurrency,
 		ParamIndex:    0,
+		ParamKey:      hotspotParamKey,
 		Threshold:     math.MaxInt64,
 		DurationInSec: 0,
 	}
@@ -44,6 +55,7 @@ func init() {
 		MetricType:      hotspot.QPS,
 		ControlBehavior: hotspot.Reject,
 		ParamIndex:      0,
+		ParamKey:        hotspotParamKey,
 		Threshold:       math.MaxInt64,
 		BurstCount:      0,
 		DurationInSec:   1,
@@ -53,6 +65,7 @@ func init() {
 		MetricType:        hotspot.QPS,
 		ControlBehavior:   hotspot.Throttling,
 		ParamIndex:        0,
+		ParamKey:          hotspotParamKey,
 		Threshold:         math.MaxInt64,
 		BurstCount:        0,
 		DurationInSec:     1,
@@ -64,13 +77,44 @@ func init() {
 	}
 }
 
-func doCheck(res string) {
-	if se, err := sentinel.Entry(res); err == nil {
+func doCheck(res string, opts ...api.EntryOption) {
+	if se, err := sentinel.Entry(res, opts...); err == nil {
 		se.Exit()
 	} else {
 		log.Fatalf("Block err: %s", err.Error())
 	}
 }
+func setRules(n int) {
+	list := make([]*hotspot.Rule, 0, n)
+	for i := 0; i < n; i++ {
+		key := fmt.Sprint(hotspotParamKey, n)
+		list = append(list, &hotspot.Rule{
+			Resource:        resReject,
+			MetricType:      hotspot.QPS,
+			ControlBehavior: hotspot.Reject,
+			ParamIndex:      0,
+			ParamKey:        key,
+			Threshold:       math.MaxInt64,
+			BurstCount:      0,
+			DurationInSec:   1,
+		})
+	}
+	_, err := hotspot.LoadRules(list)
+	if err != nil {
+		panic(err)
+	}
+}
+func buildAttachments(n int) map[interface{}]interface{} {
+	data := make(map[interface{}]interface{})
+	buf := strings.Builder{}
+	for i := 0; i < n; i++ {
+		buf.Reset()
+		buf.WriteString(hotspotParamKey)
+		buf.WriteString(strconv.Itoa(i))
+		data[buf.String()] = time.Now().Nanosecond()
+	}
+	return data
+}
 
 func Benchmark_Concurrency_Concurrency4(b *testing.B) {
 	b.ReportAllocs()
@@ -78,7 +122,7 @@ func Benchmark_Concurrency_Concurrency4(b *testing.B) {
 	b.SetParallelism(4)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resConcurrency)
+			doCheck(resConcurrency, nil)
 		}
 	})
 }
@@ -89,7 +133,7 @@ func Benchmark_Concurrency_Concurrency8(b *testing.B) {
 	b.SetParallelism(8)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resConcurrency)
+			doCheck(resConcurrency, nil)
 		}
 	})
 }
@@ -100,7 +144,7 @@ func Benchmark_Concurrency_Concurrency16(b *testing.B) {
 	b.SetParallelism(16)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resConcurrency)
+			doCheck(resConcurrency, nil)
 		}
 	})
 }
@@ -111,7 +155,7 @@ func Benchmark_QPS_Reject_Concurrency4(b *testing.B) {
 	b.SetParallelism(4)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resConcurrency)
+			doCheck(resConcurrency, nil)
 		}
 	})
 }
@@ -122,7 +166,7 @@ func Benchmark_QPS_Reject_Concurrency8(b *testing.B) {
 	b.SetParallelism(8)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resReject)
+			doCheck(resReject, nil)
 		}
 	})
 }
@@ -133,18 +177,37 @@ func Benchmark_QPS_Reject_Concurrency16(b *testing.B) {
 	b.SetParallelism(16)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resReject)
+			doCheck(resReject, nil)
 		}
 	})
 }
 
+func Benchmark_ParamKeyCount_N_QPS_Reject_Concurrency_X(b *testing.B) {
+	maxArgCount := 6
+	setRules(maxArgCount)
+
+	for j := 0; j < maxArgCount; j++ {
+		name := fmt.Sprintf("mock hited key number=%v", j)
+		b.Run(name, func(b *testing.B) {
+			b.ReportAllocs()
+			b.ResetTimer()
+
+			b.RunParallel(func(pb *testing.PB) {
+				for pb.Next() {
+					doCheck(resReject, sentinel.WithAttachments(buildAttachments(j)))
+				}
+			})
+		})
+	}
+}
+
 func Benchmark_QPS_Throttling_Concurrency4(b *testing.B) {
 	b.ReportAllocs()
 	b.ResetTimer()
 	b.SetParallelism(4)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resThrottling)
+			doCheck(resThrottling, nil)
 		}
 	})
 }
@@ -155,7 +218,7 @@ func Benchmark_QPS_Throttling_Concurrency8(b *testing.B) {
 	b.SetParallelism(8)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resThrottling)
+			doCheck(resThrottling, nil)
 		}
 	})
 }
@@ -166,7 +229,7 @@ func Benchmark_QPS_Throttling_Concurrency16(b *testing.B) {
 	b.SetParallelism(16)
 	b.RunParallel(func(pb *testing.PB) {
 		for pb.Next() {
-			doCheck(resThrottling)
+			doCheck(resThrottling, nil)
 		}
 	})
 }