Repository: tikv/pd (go)

Issue:
Store clone costs too much CPU
![photo_2020-12-21_16-07-05](https://user-images.githubusercontent.com/12077877/102844121-b7157100-4445-11eb-93d5-01b9c1fbc1bc.jpg)

This is an example that a cluster enables a scatter-range-scheduler. We need to estimate store clone is these cases.

Pull request description:
server/core: reduce the cost of CPU for Store.Clone
Signed-off-by: shirly <AndreMouche@126.com>

<!--
Thank you for working on PD! Please read PD's [CONTRIBUTING](https://github.com/tikv/pd/blob/master/CONTRIBUTING.md) document **BEFORE** filing this PR.
PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed
2. *: what's changed
-->

### What problem does this PR solve?

<!--

Please create an issue first to describe the problem.
There MUST be one line starting with "Issue Number:  " and 
linking the relevant issues via the "close" or "ref".
For more info, check https://github.com/tikv/pd/blob/master/CONTRIBUTING.md#linking-issues.

-->
Issue Number: Close #3291



### Check List

<!-- Remove the items that are not applicable. -->

Tests

<!-- At least one of these tests must be included. -->

- Unit test
### benchmark test
#### current branch
```
> $ go test -benchmem -run=^$ -bench ^BenchmarkStoreClone -benchtime=100x                                                                                       [±proto_clone ●]
goos: darwin
goarch: amd64
pkg: github.com/tikv/pd/server/core
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkStoreClone-12               100               789.0 ns/op           552 B/op         12 allocs/op
PASS
ok      github.com/tikv/pd/server/core  0.066s
                                                                                                                                                                                 
wuxuelian@fun-2 ~/go/pd/server/core                                                                                                                                   [17:11:16] 
> $ go test -benchmem -run=^$ -bench ^BenchmarkStoreClone -benchtime=100x                                                                                       [±proto_clone ●]
goos: darwin
goarch: amd64
pkg: github.com/tikv/pd/server/core
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkStoreClone-12               100               880.3 ns/op           552 B/op         12 allocs/op
PASS
ok      github.com/tikv/pd/server/core  0.070s
```
#### master
```
> $ go test -benchmem -run=^$ -bench ^BenchmarkStoreClone -benchtime=100x                                                                                           [±master ●●]
goos: darwin
goarch: amd64
pkg: github.com/tikv/pd/server/core
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkStoreClone-12               100              5115 ns/op             784 B/op         39 allocs/op
PASS
ok      github.com/tikv/pd/server/core  0.069s
                                                                                                                                                                                 
wuxuelian@fun-2 ~/go/pd/server/core                                                                                                                                   [17:12:26] 
> $ go test -benchmem -run=^$ -bench ^BenchmarkStoreClone -benchtime=100x                                                                                           [±master ●●]
goos: darwin
goarch: amd64
pkg: github.com/tikv/pd/server/core
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkStoreClone-12               100              5445 ns/op             784 B/op         39 allocs/op
PASS
ok      github.com/tikv/pd/server/core  0.072s
```
### Release note

<!-- A bugfix or a new feature needs a release note. If there is no need release note, just uncomment the below line. -->

```release-note
None.
```


Patch:
diff --git a/server/core/store.go b/server/core/store.go
index 01d1ba0dd..f7cb7dd92 100644
--- a/server/core/store.go
+++ b/server/core/store.go
@@ -19,7 +19,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/gogo/protobuf/proto"
 	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/log"
 	"github.com/tikv/pd/pkg/errs"
@@ -79,11 +78,17 @@ func NewStoreInfo(store *metapb.Store, opts ...StoreCreateOption) *StoreInfo {
 	return storeInfo
 }
 
+func (s *StoreInfo) cloneMetaStore() *metapb.Store {
+	b, _ := s.meta.Marshal()
+	store := &metapb.Store{}
+	store.Unmarshal(b)
+	return store
+}
+
 // Clone creates a copy of current StoreInfo.
 func (s *StoreInfo) Clone(opts ...StoreCreateOption) *StoreInfo {
-	meta := proto.Clone(s.meta).(*metapb.Store)
 	store := &StoreInfo{
-		meta:                meta,
+		meta:                s.cloneMetaStore(),
 		storeStats:          s.storeStats,
 		pauseLeaderTransfer: s.pauseLeaderTransfer,
 		slowStoreEvicted:    s.slowStoreEvicted,
diff --git a/server/core/store_option.go b/server/core/store_option.go
index b445e9da7..e95374551 100644
--- a/server/core/store_option.go
+++ b/server/core/store_option.go
@@ -17,7 +17,6 @@ package core
 import (
 	"time"
 
-	"github.com/gogo/protobuf/proto"
 	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/kvproto/pkg/pdpb"
 	"github.com/tikv/pd/server/core/storelimit"
@@ -29,7 +28,7 @@ type StoreCreateOption func(region *StoreInfo)
 // SetStoreAddress sets the address for the store.
 func SetStoreAddress(address, statusAddress, peerAddress string) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.Address = address
 		meta.StatusAddress = statusAddress
 		meta.PeerAddress = peerAddress
@@ -40,7 +39,7 @@ func SetStoreAddress(address, statusAddress, peerAddress string) StoreCreateOpti
 // SetStoreLabels sets the labels for the store.
 func SetStoreLabels(labels []*metapb.StoreLabel) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.Labels = labels
 		store.meta = meta
 	}
@@ -49,7 +48,7 @@ func SetStoreLabels(labels []*metapb.StoreLabel) StoreCreateOption {
 // SetStoreStartTime sets the start timestamp for the store.
 func SetStoreStartTime(startTS int64) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.StartTimestamp = startTS
 		store.meta = meta
 	}
@@ -58,7 +57,7 @@ func SetStoreStartTime(startTS int64) StoreCreateOption {
 // SetStoreVersion sets the version for the store.
 func SetStoreVersion(githash, version string) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.Version = version
 		meta.GitHash = githash
 		store.meta = meta
@@ -68,7 +67,7 @@ func SetStoreVersion(githash, version string) StoreCreateOption {
 // SetStoreDeployPath sets the deploy path for the store.
 func SetStoreDeployPath(deployPath string) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.DeployPath = deployPath
 		store.meta = meta
 	}
@@ -77,7 +76,7 @@ func SetStoreDeployPath(deployPath string) StoreCreateOption {
 // OfflineStore offline a store
 func OfflineStore(physicallyDestroyed bool) StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.State = metapb.StoreState_Offline
 		meta.NodeState = metapb.NodeState_Removing
 		meta.PhysicallyDestroyed = physicallyDestroyed
@@ -88,7 +87,7 @@ func OfflineStore(physicallyDestroyed bool) StoreCreateOption {
 // UpStore up a store
 func UpStore() StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.State = metapb.StoreState_Up
 		meta.NodeState = metapb.NodeState_Serving
 		store.meta = meta
@@ -98,7 +97,7 @@ func UpStore() StoreCreateOption {
 // TombstoneStore set a store to tombstone.
 func TombstoneStore() StoreCreateOption {
 	return func(store *StoreInfo) {
-		meta := proto.Clone(store.meta).(*metapb.Store)
+		meta := store.cloneMetaStore()
 		meta.State = metapb.StoreState_Tombstone
 		meta.NodeState = metapb.NodeState_Removed
 		store.meta = meta
diff --git a/server/core/store_stats.go b/server/core/store_stats.go
index 90d524d02..2cc8099ce 100644
--- a/server/core/store_stats.go
+++ b/server/core/store_stats.go
@@ -67,6 +67,16 @@ func (ss *storeStats) GetStoreStats() *pdpb.StoreStats {
 	return ss.rawStats
 }
 
+// CloneStoreStats returns the statistics information cloned from the store.
+func (ss *storeStats) CloneStoreStats() *pdpb.StoreStats {
+	ss.mu.RLock()
+	b, _ := ss.rawStats.Marshal()
+	ss.mu.RUnlock()
+	stats := &pdpb.StoreStats{}
+	stats.Unmarshal(b)
+	return stats
+}
+
 // GetCapacity returns the capacity size of the store.
 func (ss *storeStats) GetCapacity() uint64 {
 	ss.mu.RLock()
diff --git a/server/schedule/range_cluster.go b/server/schedule/range_cluster.go
index e2c32bee5..d2073fb61 100644
--- a/server/schedule/range_cluster.go
+++ b/server/schedule/range_cluster.go
@@ -15,8 +15,6 @@
 package schedule
 
 import (
-	"github.com/gogo/protobuf/proto"
-	"github.com/pingcap/kvproto/pkg/pdpb"
 	"github.com/tikv/pd/server/core"
 )
 
@@ -53,7 +51,7 @@ func (r *RangeCluster) updateStoreInfo(s *core.StoreInfo) *core.StoreInfo {
 	regionCount := r.subCluster.GetStoreRegionCount(id)
 	regionSize := r.subCluster.GetStoreRegionSize(id)
 	pendingPeerCount := r.subCluster.GetStorePendingPeerCount(id)
-	newStats := proto.Clone(s.GetStoreStats()).(*pdpb.StoreStats)
+	newStats := s.CloneStoreStats()
 	newStats.UsedSize = uint64(float64(regionSize)/amplification) * (1 << 20)
 	newStats.Available = s.GetCapacity() - newStats.UsedSize
 	newStore := s.Clone(


Test patch:
diff --git a/server/core/store_test.go b/server/core/store_test.go
index b311315a2..e93d0e80e 100644
--- a/server/core/store_test.go
+++ b/server/core/store_test.go
@@ -90,6 +90,26 @@ func TestCloneStore(t *testing.T) {
 	wg.Wait()
 }
 
+func TestCloneMetaStore(t *testing.T) {
+	re := require.New(t)
+	store := &metapb.Store{Id: 1, Address: "mock://tikv-1", Labels: []*metapb.StoreLabel{{Key: "zone", Value: "z1"}, {Key: "host", Value: "h1"}}}
+	store2 := NewStoreInfo(store).cloneMetaStore()
+	re.Equal(store2.Labels, store.Labels)
+	store2.Labels[0].Value = "changed value"
+	re.NotEqual(store2.Labels, store.Labels)
+}
+
+func BenchmarkStoreClone(b *testing.B) {
+	meta := &metapb.Store{Id: 1,
+		Address: "mock://tikv-1",
+		Labels:  []*metapb.StoreLabel{{Key: "zone", Value: "z1"}, {Key: "host", Value: "h1"}}}
+	store := NewStoreInfo(meta)
+	b.ResetTimer()
+	for t := 0; t < b.N; t++ {
+		store.Clone(SetLeaderCount(t))
+	}
+}
+
 func TestRegionScore(t *testing.T) {
 	re := require.New(t)
 	stats := &pdpb.StoreStats{}