Repository: pingcap/pd
Pull request #1684 (2019-08-15 13:42:17)

server: use a checker controller in coordinator
### What problem does this PR solve? <!--add the issue link with summary if it exists-->
Currently, we need to initialize all checkers in `coordinator`, which is untidy.

### What is changed and how it works?
This PR makes some changes:

1. use a checker controller to initialize all checkers. Especially, for merge checker, it is not supported, we won't initialize it.

2. Move `Cluster` interface to `opt.go` temporarily to reduce the duplication of the interface.
3. Move `checker` package into `schedule` directory.

### Check List <!--REMOVE the items that are not applicable-->

Tests <!-- At least one of them must be included. -->

 - Unit test

codecov-io: # [Codecov](https://codecov.io/gh/pingcap/pd/pull/1684?src=pr&el=h1) Report
> Merging [#1684](https://codecov.io/gh/pingcap/pd/pull/1684?src=pr&el=desc) into [master](https://codecov.io/gh/pingcap/pd/commit/c8b279aa7597e25ab8b0e0485afc0446d7e63ea2?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `84.05%`.

[![Impacted file tree graph](https://codecov.io/gh/pingcap/pd/pull/1684/graphs/tree.svg?width=650&token=J1yRL471OI&height=150&src=pr)](https://codecov.io/gh/pingcap/pd/pull/1684?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1684      +/-   ##
==========================================
+ Coverage   76.64%   76.68%   +0.03%     
==========================================
  Files         157      158       +1     
  Lines       15434    15451      +17     
==========================================
+ Hits        11829    11848      +19     
- Misses       2590     2594       +4     
+ Partials     1015     1009       -6
```


| [Impacted Files](https://codecov.io/gh/pingcap/pd/pull/1684?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [server/schedule/scheduler.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlL3NjaGVkdWxlci5nbw==) | `66.66% <ø> (ø)` | :arrow_up: |
| [server/version.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3ZlcnNpb24uZ28=) | `72.72% <ø> (ø)` | :arrow_up: |
| [server/schedule/checker/learner\_checker.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlL2NoZWNrZXIvbGVhcm5lcl9jaGVja2VyLmdv) | `100% <ø> (ø)` | |
| [server/schedule/checker/metrics.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlL2NoZWNrZXIvbWV0cmljcy5nbw==) | `100% <ø> (ø)` | |
| [server/cluster\_worker.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL2NsdXN0ZXJfd29ya2VyLmdv) | `48.73% <0%> (+0.4%)` | :arrow_up: |
| [server/cluster.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL2NsdXN0ZXIuZ28=) | `82.25% <0%> (-0.67%)` | :arrow_down: |
| [server/schedulers/balance\_leader.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvYmFsYW5jZV9sZWFkZXIuZ28=) | `96.52% <100%> (ø)` | :arrow_up: |
| [server/schedulers/balance\_region.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvYmFsYW5jZV9yZWdpb24uZ28=) | `86.74% <100%> (ø)` | :arrow_up: |
| [server/schedulers/label.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlcnMvbGFiZWwuZ28=) | `82.6% <100%> (ø)` | :arrow_up: |
| [server/schedule/range\_cluster.go](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree#diff-c2VydmVyL3NjaGVkdWxlL3JhbmdlX2NsdXN0ZXIuZ28=) | `90.47% <100%> (ø)` | :arrow_up: |
| ... and [32 more](https://codecov.io/gh/pingcap/pd/pull/1684/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pingcap/pd/pull/1684?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/pingcap/pd/pull/1684?src=pr&el=footer). Last update [c8b279a...1fd6986](https://codecov.io/gh/pingcap/pd/pull/1684?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Luffbee: @disksing has some different opinions on the `Cluster` interface problem, I think we need to discuss about it.
rleungx: @nolouch @disksing @lhy1024 PTAL.
sre-bot: /run-all-tests
sre-bot: @rleungx merge failed.
nolouch: /merge
sre-bot: /run-all-tests
sre-bot: @rleungx merge failed.
rleungx: /merge
sre-bot: /run-all-tests

Patch:
diff --git a/server/cluster.go b/server/cluster.go
index 07e3a79a4..e408575f5 100644
--- a/server/cluster.go
+++ b/server/cluster.go
@@ -34,6 +34,7 @@ import (
 	"github.com/pingcap/pd/server/namespace"
 	syncer "github.com/pingcap/pd/server/region_syncer"
 	"github.com/pingcap/pd/server/schedule"
+	"github.com/pingcap/pd/server/schedule/checker"
 	"github.com/pingcap/pd/server/statistics"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
@@ -1164,6 +1165,13 @@ func (c *RaftCluster) GetNamespaceClassifier() namespace.Classifier {
 	return c.s.classifier
 }
 
+// GetMergeChecker returns merge checker.
+func (c *RaftCluster) GetMergeChecker() *checker.MergeChecker {
+	c.RLock()
+	defer c.RUnlock()
+	return c.coordinator.checkers.GetMergeChecker()
+}
+
 // GetOpt returns the scheduling options.
 func (c *RaftCluster) GetOpt() namespace.ScheduleOptions {
 	return c.opt
diff --git a/server/cluster_worker.go b/server/cluster_worker.go
index da1b4cc8f..b4d439cbc 100644
--- a/server/cluster_worker.go
+++ b/server/cluster_worker.go
@@ -64,11 +64,10 @@ func (c *RaftCluster) handleAskSplit(request *pdpb.AskSplitRequest) (*pdpb.AskSp
 		}
 	}
 
-	c.RLock()
-	defer c.RUnlock()
-	// Disable merge for the 2 regions in a period of time.
-	c.coordinator.mergeChecker.RecordRegionSplit(reqRegion.GetId())
-	c.coordinator.mergeChecker.RecordRegionSplit(newRegionID)
+	if c.IsFeatureSupported(RegionMerge) {
+		// Disable merge for the 2 regions in a period of time.
+		c.GetMergeChecker().RecordRegionSplit([]uint64{reqRegion.GetId(), newRegionID})
+	}
 
 	split := &pdpb.AskSplitResponse{
 		NewRegionId: newRegionID,
@@ -102,11 +101,8 @@ func (c *RaftCluster) handleAskBatchSplit(request *pdpb.AskBatchSplitRequest) (*
 		return nil, err
 	}
 	splitIDs := make([]*pdpb.SplitID, 0, splitCount)
+	recordRegions := make([]uint64, splitCount+1)
 
-	c.RLock()
-	defer c.RUnlock()
-	// Disable merge the regions in a period of time.
-	c.coordinator.mergeChecker.RecordRegionSplit(reqRegion.GetId())
 	for i := 0; i < int(splitCount); i++ {
 		newRegionID, err := c.s.idAllocator.Alloc()
 		if err != nil {
@@ -120,13 +116,19 @@ func (c *RaftCluster) handleAskBatchSplit(request *pdpb.AskBatchSplitRequest) (*
 			}
 		}
 
-		c.coordinator.mergeChecker.RecordRegionSplit(newRegionID)
+		recordRegions = append(recordRegions, newRegionID)
 		splitIDs = append(splitIDs, &pdpb.SplitID{
 			NewRegionId: newRegionID,
 			NewPeerIds:  peerIDs,
 		})
 	}
 
+	recordRegions = append(recordRegions, reqRegion.GetId())
+	if c.IsFeatureSupported(RegionMerge) {
+		// Disable merge the regions in a period of time.
+		c.GetMergeChecker().RecordRegionSplit(recordRegions)
+	}
+
 	resp := &pdpb.AskBatchSplitResponse{Ids: splitIDs}
 
 	return resp, nil
diff --git a/server/coordinator.go b/server/coordinator.go
index 34c91afbf..2f65ec72d 100644
--- a/server/coordinator.go
+++ b/server/coordinator.go
@@ -21,8 +21,6 @@ import (
 
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/pkg/logutil"
-	"github.com/pingcap/pd/server/checker"
-	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
@@ -52,38 +50,32 @@ var (
 type coordinator struct {
 	sync.RWMutex
 
-	wg     sync.WaitGroup
-	ctx    context.Context
-	cancel context.CancelFunc
-
-	cluster          *RaftCluster
-	learnerChecker   *checker.LearnerChecker
-	replicaChecker   *checker.ReplicaChecker
-	namespaceChecker *checker.NamespaceChecker
-	mergeChecker     *checker.MergeChecker
-	regionScatterer  *schedule.RegionScatterer
-	schedulers       map[string]*scheduleController
-	opController     *schedule.OperatorController
-	classifier       namespace.Classifier
-	hbStreams        *heartbeatStreams
+	wg              sync.WaitGroup
+	ctx             context.Context
+	cancel          context.CancelFunc
+	cluster         *RaftCluster
+	checkers        *schedule.CheckerController
+	regionScatterer *schedule.RegionScatterer
+	schedulers      map[string]*scheduleController
+	opController    *schedule.OperatorController
+	classifier      namespace.Classifier
+	hbStreams       *heartbeatStreams
 }
 
 // newCoordinator creates a new coordinator.
 func newCoordinator(cluster *RaftCluster, hbStreams *heartbeatStreams, classifier namespace.Classifier) *coordinator {
 	ctx, cancel := context.WithCancel(context.Background())
+	opController := schedule.NewOperatorController(cluster, hbStreams)
 	return &coordinator{
-		ctx:              ctx,
-		cancel:           cancel,
-		cluster:          cluster,
-		learnerChecker:   checker.NewLearnerChecker(),
-		replicaChecker:   checker.NewReplicaChecker(cluster, classifier),
-		namespaceChecker: checker.NewNamespaceChecker(cluster, classifier),
-		mergeChecker:     checker.NewMergeChecker(cluster, classifier),
-		regionScatterer:  schedule.NewRegionScatterer(cluster, classifier),
-		schedulers:       make(map[string]*scheduleController),
-		opController:     schedule.NewOperatorController(cluster, hbStreams),
-		classifier:       classifier,
-		hbStreams:        hbStreams,
+		ctx:             ctx,
+		cancel:          cancel,
+		cluster:         cluster,
+		checkers:        schedule.NewCheckerController(cluster, classifier, opController),
+		regionScatterer: schedule.NewRegionScatterer(cluster, classifier),
+		schedulers:      make(map[string]*scheduleController),
+		opController:    opController,
+		classifier:      classifier,
+		hbStreams:       hbStreams,
 	}
 }
 
@@ -123,7 +115,7 @@ func (c *coordinator) patrolRegions() {
 
 			key = region.GetEndKey()
 
-			if c.checkRegion(region) {
+			if c.checkers.CheckRegion(region) {
 				break
 			}
 		}
@@ -155,43 +147,6 @@ func (c *coordinator) drivePushOperator() {
 	}
 }
 
-func (c *coordinator) checkRegion(region *core.RegionInfo) bool {
-	opController := c.opController
-
-	if op := c.learnerChecker.Check(region); op != nil {
-		if opController.AddOperator(op) {
-			return true
-		}
-	}
-
-	if opController.OperatorCount(operator.OpLeader) < c.cluster.GetLeaderScheduleLimit() &&
-		opController.OperatorCount(operator.OpRegion) < c.cluster.GetRegionScheduleLimit() &&
-		opController.OperatorCount(operator.OpReplica) < c.cluster.GetReplicaScheduleLimit() {
-		if op := c.namespaceChecker.Check(region); op != nil {
-			if opController.AddWaitingOperator(op) {
-				return true
-			}
-		}
-	}
-
-	if opController.OperatorCount(operator.OpReplica) < c.cluster.GetReplicaScheduleLimit() {
-		if op := c.replicaChecker.Check(region); op != nil {
-			if opController.AddWaitingOperator(op) {
-				return true
-			}
-		}
-	}
-	if c.cluster.IsFeatureSupported(RegionMerge) && opController.OperatorCount(operator.OpMerge) < c.cluster.GetMergeScheduleLimit() {
-		if ops := c.mergeChecker.Check(region); ops != nil {
-			// It makes sure that two operators can be added successfully altogether.
-			if opController.AddWaitingOperator(ops...) {
-				return true
-			}
-		}
-	}
-	return false
-}
-
 func (c *coordinator) run() {
 	ticker := time.NewTicker(runSchedulerCheckInterval)
 	defer ticker.Stop()
diff --git a/server/namespace_cluster.go b/server/namespace_cluster.go
index 66a2b2a37..ac2e0e538 100644
--- a/server/namespace_cluster.go
+++ b/server/namespace_cluster.go
@@ -20,19 +20,20 @@ import (
 	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/statistics"
 )
 
 // namespaceCluster is part of a global cluster that contains stores and regions
 // within a specific namespace.
 type namespaceCluster struct {
-	schedule.Cluster
+	opt.Cluster
 	classifier namespace.Classifier
 	namespace  string
 	stores     map[uint64]*core.StoreInfo
 }
 
-func newNamespaceCluster(c schedule.Cluster, classifier namespace.Classifier, namespace string) *namespaceCluster {
+func newNamespaceCluster(c opt.Cluster, classifier namespace.Classifier, namespace string) *namespaceCluster {
 	stores := make(map[uint64]*core.StoreInfo)
 	for _, s := range c.GetStores() {
 		if classifier.GetStoreNamespace(s) == namespace {
@@ -130,7 +131,7 @@ func (c *namespaceCluster) RegionWriteStats() map[uint64][]*statistics.HotPeerSt
 	return c.Cluster.RegionWriteStats()
 }
 
-func scheduleByNamespace(cluster schedule.Cluster, classifier namespace.Classifier, scheduler schedule.Scheduler) []*operator.Operator {
+func scheduleByNamespace(cluster opt.Cluster, classifier namespace.Classifier, scheduler schedule.Scheduler) []*operator.Operator {
 	namespaces := classifier.GetAllNamespaces()
 	for _, i := range rand.Perm(len(namespaces)) {
 		nc := newNamespaceCluster(cluster, classifier, namespaces[i])
diff --git a/server/checker/learner_checker.go b/server/schedule/checker/learner_checker.go
similarity index 100%
rename from server/checker/learner_checker.go
rename to server/schedule/checker/learner_checker.go
diff --git a/server/checker/merge_checker.go b/server/schedule/checker/merge_checker.go
similarity index 93%
rename from server/checker/merge_checker.go
rename to server/schedule/checker/merge_checker.go
index 18ee87507..7dc0a344d 100644
--- a/server/checker/merge_checker.go
+++ b/server/schedule/checker/merge_checker.go
@@ -20,8 +20,8 @@ import (
 	"github.com/pingcap/pd/pkg/cache"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
-	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"go.uber.org/zap"
 )
 
@@ -31,13 +31,13 @@ const mergeBlockMarker = 0
 
 // MergeChecker ensures region to merge with adjacent region when size is small
 type MergeChecker struct {
-	cluster    schedule.Cluster
+	cluster    opt.Cluster
 	classifier namespace.Classifier
 	splitCache *cache.TTLUint64
 }
 
 // NewMergeChecker creates a merge checker.
-func NewMergeChecker(cluster schedule.Cluster, classifier namespace.Classifier) *MergeChecker {
+func NewMergeChecker(cluster opt.Cluster, classifier namespace.Classifier) *MergeChecker {
 	splitCache := cache.NewIDTTL(time.Minute, cluster.GetSplitMergeInterval())
 	splitCache.Put(mergeBlockMarker)
 	return &MergeChecker{
@@ -49,8 +49,10 @@ func NewMergeChecker(cluster schedule.Cluster, classifier namespace.Classifier)
 
 // RecordRegionSplit put the recently splitted region into cache. MergeChecker
 // will skip check it for a while.
-func (m *MergeChecker) RecordRegionSplit(regionID uint64) {
-	m.splitCache.PutWithTTL(regionID, nil, m.cluster.GetSplitMergeInterval())
+func (m *MergeChecker) RecordRegionSplit(regionIDs []uint64) {
+	for _, regionID := range regionIDs {
+		m.splitCache.PutWithTTL(regionID, nil, m.cluster.GetSplitMergeInterval())
+	}
 }
 
 // Check verifies a region's replicas, creating an Operator if need.
diff --git a/server/checker/metrics.go b/server/schedule/checker/metrics.go
similarity index 100%
rename from server/checker/metrics.go
rename to server/schedule/checker/metrics.go
diff --git a/server/checker/namespace_checker.go b/server/schedule/checker/namespace_checker.go
similarity index 96%
rename from server/checker/namespace_checker.go
rename to server/schedule/checker/namespace_checker.go
index c72cb8935..cbd794679 100644
--- a/server/checker/namespace_checker.go
+++ b/server/schedule/checker/namespace_checker.go
@@ -18,9 +18,9 @@ import (
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
-	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"go.uber.org/zap"
 )
@@ -30,13 +30,13 @@ const namespaceCheckerName = "namespace-checker"
 // NamespaceChecker ensures region to go to the right place.
 type NamespaceChecker struct {
 	name       string
-	cluster    schedule.Cluster
+	cluster    opt.Cluster
 	filters    []filter.Filter
 	classifier namespace.Classifier
 }
 
 // NewNamespaceChecker creates a namespace checker.
-func NewNamespaceChecker(cluster schedule.Cluster, classifier namespace.Classifier) *NamespaceChecker {
+func NewNamespaceChecker(cluster opt.Cluster, classifier namespace.Classifier) *NamespaceChecker {
 	filters := []filter.Filter{
 		filter.StoreStateFilter{ActionScope: namespaceCheckerName, MoveRegion: true},
 	}
diff --git a/server/checker/replica_checker.go b/server/schedule/checker/replica_checker.go
similarity index 97%
rename from server/checker/replica_checker.go
rename to server/schedule/checker/replica_checker.go
index 4b5b70957..0225608c7 100644
--- a/server/checker/replica_checker.go
+++ b/server/schedule/checker/replica_checker.go
@@ -20,9 +20,9 @@ import (
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
-	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"go.uber.org/zap"
 )
@@ -32,17 +32,17 @@ const replicaCheckerName = "replica-checker"
 // ReplicaChecker ensures region has the best replicas.
 // Including the following:
 // Replica number management.
-// Unhealth replica management, mainly used for disaster recovery of TiKV.
+// Unhealthy replica management, mainly used for disaster recovery of TiKV.
 // Location management, mainly used for cross data center deployment.
 type ReplicaChecker struct {
 	name       string
-	cluster    schedule.Cluster
+	cluster    opt.Cluster
 	classifier namespace.Classifier
 	filters    []filter.Filter
 }
 
 // NewReplicaChecker creates a replica checker.
-func NewReplicaChecker(cluster schedule.Cluster, classifier namespace.Classifier, n ...string) *ReplicaChecker {
+func NewReplicaChecker(cluster opt.Cluster, classifier namespace.Classifier, n ...string) *ReplicaChecker {
 	name := replicaCheckerName
 	if len(n) != 0 {
 		name = n[0]
diff --git a/server/checker/replica_checker_test.go b/server/schedule/checker/replica_checker_test.go
similarity index 100%
rename from server/checker/replica_checker_test.go
rename to server/schedule/checker/replica_checker_test.go
diff --git a/server/schedule/checker_controller.go b/server/schedule/checker_controller.go
new file mode 100644
index 000000000..144ba39e7
--- /dev/null
+++ b/server/schedule/checker_controller.go
@@ -0,0 +1,90 @@
+// Copyright 2019 PingCAP, Inc.
+//
+// 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,
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package schedule
+
+import (
+	"github.com/pingcap/pd/server/core"
+	"github.com/pingcap/pd/server/namespace"
+	"github.com/pingcap/pd/server/schedule/checker"
+	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
+)
+
+// CheckerController is used to manage all checkers.
+type CheckerController struct {
+	cluster          opt.Cluster
+	opController     *OperatorController
+	learnerChecker   *checker.LearnerChecker
+	replicaChecker   *checker.ReplicaChecker
+	namespaceChecker *checker.NamespaceChecker
+	mergeChecker     *checker.MergeChecker
+}
+
+// NewCheckerController create a new CheckerController.
+// TODO: isSupportMerge should be removed.
+func NewCheckerController(cluster opt.Cluster, classifier namespace.Classifier, opController *OperatorController) *CheckerController {
+	return &CheckerController{
+		cluster:          cluster,
+		opController:     opController,
+		learnerChecker:   checker.NewLearnerChecker(),
+		replicaChecker:   checker.NewReplicaChecker(cluster, classifier),
+		namespaceChecker: checker.NewNamespaceChecker(cluster, classifier),
+		mergeChecker:     checker.NewMergeChecker(cluster, classifier),
+	}
+}
+
+// CheckRegion will check the region and add a new operator if needed.
+func (c *CheckerController) CheckRegion(region *core.RegionInfo) bool {
+	// If PD has restarted, it need to check learners added before and promote them.
+	// Don't check isRaftLearnerEnabled cause it maybe disable learner feature but there are still some learners to promote.
+	opController := c.opController
+
+	if op := c.learnerChecker.Check(region); op != nil {
+		if opController.AddOperator(op) {
+			return true
+		}
+	}
+
+	if opController.OperatorCount(operator.OpLeader) < c.cluster.GetLeaderScheduleLimit() &&
+		opController.OperatorCount(operator.OpRegion) < c.cluster.GetRegionScheduleLimit() &&
+		opController.OperatorCount(operator.OpReplica) < c.cluster.GetReplicaScheduleLimit() {
+		if op := c.namespaceChecker.Check(region); op != nil {
+			if opController.AddWaitingOperator(op) {
+				return true
+			}
+		}
+	}
+
+	if opController.OperatorCount(operator.OpReplica) < c.cluster.GetReplicaScheduleLimit() {
+		if op := c.replicaChecker.Check(region); op != nil {
+			if opController.AddWaitingOperator(op) {
+				return true
+			}
+		}
+	}
+	if c.mergeChecker != nil && opController.OperatorCount(operator.OpMerge) < c.cluster.GetMergeScheduleLimit() {
+		if ops := c.mergeChecker.Check(region); ops != nil {
+			// It makes sure that two operators can be added successfully altogether.
+			if opController.AddWaitingOperator(ops...) {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+// GetMergeChecker returns the merge checker.
+func (c *CheckerController) GetMergeChecker() *checker.MergeChecker {
+	return c.mergeChecker
+}
diff --git a/server/schedule/operator_controller.go b/server/schedule/operator_controller.go
index 820907f78..ced0c05bf 100644
--- a/server/schedule/operator_controller.go
+++ b/server/schedule/operator_controller.go
@@ -30,6 +30,7 @@ import (
 	"github.com/pingcap/pd/pkg/cache"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"go.uber.org/zap"
 )
 
@@ -58,7 +59,7 @@ type HeartbeatStreams interface {
 // OperatorController is used to limit the speed of scheduling.
 type OperatorController struct {
 	sync.RWMutex
-	cluster   Cluster
+	cluster   opt.Cluster
 	operators map[uint64]*operator.Operator
 	hbStreams HeartbeatStreams
 	histories *list.List
@@ -72,7 +73,7 @@ type OperatorController struct {
 }
 
 // NewOperatorController creates a OperatorController.
-func NewOperatorController(cluster Cluster, hbStreams HeartbeatStreams) *OperatorController {
+func NewOperatorController(cluster opt.Cluster, hbStreams HeartbeatStreams) *OperatorController {
 	return &OperatorController{
 		cluster:         cluster,
 		operators:       make(map[uint64]*operator.Operator),
@@ -584,7 +585,7 @@ func (oc *OperatorController) OperatorCount(mask operator.OpKind) uint64 {
 }
 
 // GetOpInfluence gets OpInfluence.
-func (oc *OperatorController) GetOpInfluence(cluster Cluster) operator.OpInfluence {
+func (oc *OperatorController) GetOpInfluence(cluster opt.Cluster) operator.OpInfluence {
 	oc.RLock()
 	defer oc.RUnlock()
 
@@ -601,7 +602,7 @@ func (oc *OperatorController) GetOpInfluence(cluster Cluster) operator.OpInfluen
 }
 
 // NewTotalOpInfluence creates a OpInfluence.
-func NewTotalOpInfluence(operators []*operator.Operator, cluster Cluster) operator.OpInfluence {
+func NewTotalOpInfluence(operators []*operator.Operator, cluster opt.Cluster) operator.OpInfluence {
 	influence := operator.OpInfluence{
 		StoresInfluence: make(map[uint64]*operator.StoreInfluence),
 	}
@@ -617,7 +618,7 @@ func NewTotalOpInfluence(operators []*operator.Operator, cluster Cluster) operat
 }
 
 // NewUnfinishedOpInfluence creates a OpInfluence.
-func NewUnfinishedOpInfluence(operators []*operator.Operator, cluster Cluster) operator.OpInfluence {
+func NewUnfinishedOpInfluence(operators []*operator.Operator, cluster opt.Cluster) operator.OpInfluence {
 	influence := operator.OpInfluence{
 		StoresInfluence: make(map[uint64]*operator.StoreInfluence),
 	}
diff --git a/server/schedule/opt/opts.go b/server/schedule/opt/opts.go
index 20978d892..7b39c0745 100644
--- a/server/schedule/opt/opts.go
+++ b/server/schedule/opt/opts.go
@@ -17,6 +17,9 @@ import (
 	"time"
 
 	"github.com/pingcap/kvproto/pkg/metapb"
+	"github.com/pingcap/pd/server/core"
+	"github.com/pingcap/pd/server/namespace"
+	"github.com/pingcap/pd/server/statistics"
 )
 
 // Options for schedulers.
@@ -63,3 +66,20 @@ const (
 	// have any region leaders.
 	RejectLeader = "reject-leader"
 )
+
+// Cluster provides an overview of a cluster's regions distribution.
+// TODO: This interface should be moved to a better place.
+type Cluster interface {
+	core.RegionSetInformer
+	core.StoreSetInformer
+	core.StoreSetController
+
+	statistics.RegionStatInformer
+	Options
+
+	// get config methods
+	GetOpt() namespace.ScheduleOptions
+	// TODO: it should be removed. Schedulers don't need to know anything
+	// about peers.
+	AllocPeer(storeID uint64) (*metapb.Peer, error)
+}
diff --git a/server/schedule/range_cluster.go b/server/schedule/range_cluster.go
index 30d225602..311e3b54b 100644
--- a/server/schedule/range_cluster.go
+++ b/server/schedule/range_cluster.go
@@ -17,11 +17,12 @@ import (
 	"github.com/gogo/protobuf/proto"
 	"github.com/pingcap/kvproto/pkg/pdpb"
 	"github.com/pingcap/pd/server/core"
+	"github.com/pingcap/pd/server/schedule/opt"
 )
 
 // RangeCluster isolates the cluster by range.
 type RangeCluster struct {
-	Cluster
+	opt.Cluster
 	regions           *core.RegionsInfo
 	tolerantSizeRatio float64
 }
@@ -30,7 +31,7 @@ const scanLimit = 128
 
 // GenRangeCluster gets a range cluster by specifying start key and end key.
 // The cluster can only know the regions within [startKey, endKey].
-func GenRangeCluster(cluster Cluster, startKey, endKey []byte) *RangeCluster {
+func GenRangeCluster(cluster opt.Cluster, startKey, endKey []byte) *RangeCluster {
 	regions := core.NewRegionsInfo()
 	for _, r := range cluster.ScanRegions(startKey, endKey, -1) {
 		regions.AddRegion(r)
diff --git a/server/schedule/region_scatterer.go b/server/schedule/region_scatterer.go
index e6cc3e4a1..4e641df18 100644
--- a/server/schedule/region_scatterer.go
+++ b/server/schedule/region_scatterer.go
@@ -23,6 +23,7 @@ import (
 	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
 )
@@ -69,7 +70,7 @@ func (s *selectedStores) newFilter(scope string) filter.Filter {
 // RegionScatterer scatters regions.
 type RegionScatterer struct {
 	name       string
-	cluster    Cluster
+	cluster    opt.Cluster
 	classifier namespace.Classifier
 	filters    []filter.Filter
 	selected   *selectedStores
@@ -77,7 +78,7 @@ type RegionScatterer struct {
 
 // NewRegionScatterer creates a region scatterer.
 // RegionScatter is used for the `Lightning`, it will scatter the specified regions before import data.
-func NewRegionScatterer(cluster Cluster, classifier namespace.Classifier) *RegionScatterer {
+func NewRegionScatterer(cluster opt.Cluster, classifier namespace.Classifier) *RegionScatterer {
 	return &RegionScatterer{
 		name:       regionScatterName,
 		cluster:    cluster,
diff --git a/server/schedule/scheduler.go b/server/schedule/scheduler.go
index d0e641e98..4901ad81a 100644
--- a/server/schedule/scheduler.go
+++ b/server/schedule/scheduler.go
@@ -16,13 +16,9 @@ package schedule
 import (
 	"time"
 
-	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/log"
-	"github.com/pingcap/pd/server/core"
-	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule/operator"
 	"github.com/pingcap/pd/server/schedule/opt"
-	"github.com/pingcap/pd/server/statistics"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
 )
@@ -34,10 +30,10 @@ type Scheduler interface {
 	GetType() string
 	GetMinInterval() time.Duration
 	GetNextInterval(interval time.Duration) time.Duration
-	Prepare(cluster Cluster) error
-	Cleanup(cluster Cluster)
-	Schedule(cluster Cluster) []*operator.Operator
-	IsScheduleAllowed(cluster Cluster) bool
+	Prepare(cluster opt.Cluster) error
+	Cleanup(cluster opt.Cluster)
+	Schedule(cluster opt.Cluster) []*operator.Operator
+	IsScheduleAllowed(cluster opt.Cluster) bool
 }
 
 // CreateSchedulerFunc is for creating scheduler.
@@ -68,19 +64,3 @@ func CreateScheduler(name string, opController *OperatorController, args ...stri
 	}
 	return fn(opController, args)
 }
-
-// Cluster provides an overview of a cluster's regions distribution.
-type Cluster interface {
-	core.RegionSetInformer
-	core.StoreSetInformer
-	core.StoreSetController
-
-	statistics.RegionStatInformer
-	opt.Options
-
-	// get config methods
-	GetOpt() namespace.ScheduleOptions
-	// TODO: it should be removed. Schedulers don't need to know anything
-	// about peers.
-	AllocPeer(storeID uint64) (*metapb.Peer, error)
-}
diff --git a/server/schedulers/adjacent_region.go b/server/schedulers/adjacent_region.go
index ccab92316..2a0367dcf 100644
--- a/server/schedulers/adjacent_region.go
+++ b/server/schedulers/adjacent_region.go
@@ -23,6 +23,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
@@ -134,7 +135,7 @@ func (l *balanceAdjacentRegionScheduler) GetNextInterval(interval time.Duration)
 	return intervalGrow(interval, maxAdjacentSchedulerInterval, linearGrowth)
 }
 
-func (l *balanceAdjacentRegionScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (l *balanceAdjacentRegionScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return l.allowBalanceLeader() || l.allowBalancePeer()
 }
 
@@ -146,7 +147,7 @@ func (l *balanceAdjacentRegionScheduler) allowBalancePeer() bool {
 	return l.opController.OperatorCount(operator.OpAdjacent|operator.OpRegion) < l.peerLimit
 }
 
-func (l *balanceAdjacentRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (l *balanceAdjacentRegionScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	if l.cacheRegions == nil {
 		l.cacheRegions = &adjacentState{
 			assignedStoreIds: make([]uint64, 0, len(cluster.GetStores())),
@@ -203,7 +204,7 @@ func (l *balanceAdjacentRegionScheduler) Schedule(cluster schedule.Cluster) []*o
 	return l.process(cluster)
 }
 
-func (l *balanceAdjacentRegionScheduler) process(cluster schedule.Cluster) []*operator.Operator {
+func (l *balanceAdjacentRegionScheduler) process(cluster opt.Cluster) []*operator.Operator {
 	if l.cacheRegions.len() < 2 {
 		return nil
 	}
@@ -237,7 +238,7 @@ func (l *balanceAdjacentRegionScheduler) process(cluster schedule.Cluster) []*op
 	return []*operator.Operator{op}
 }
 
-func (l *balanceAdjacentRegionScheduler) unsafeToBalance(cluster schedule.Cluster, region *core.RegionInfo) bool {
+func (l *balanceAdjacentRegionScheduler) unsafeToBalance(cluster opt.Cluster, region *core.RegionInfo) bool {
 	if len(region.GetPeers()) != cluster.GetMaxReplicas() {
 		return true
 	}
@@ -259,7 +260,7 @@ func (l *balanceAdjacentRegionScheduler) unsafeToBalance(cluster schedule.Cluste
 	return false
 }
 
-func (l *balanceAdjacentRegionScheduler) disperseLeader(cluster schedule.Cluster, before *core.RegionInfo, after *core.RegionInfo) *operator.Operator {
+func (l *balanceAdjacentRegionScheduler) disperseLeader(cluster opt.Cluster, before *core.RegionInfo, after *core.RegionInfo) *operator.Operator {
 	if !l.allowBalanceLeader() {
 		return nil
 	}
@@ -283,7 +284,7 @@ func (l *balanceAdjacentRegionScheduler) disperseLeader(cluster schedule.Cluster
 	return op
 }
 
-func (l *balanceAdjacentRegionScheduler) dispersePeer(cluster schedule.Cluster, region *core.RegionInfo) *operator.Operator {
+func (l *balanceAdjacentRegionScheduler) dispersePeer(cluster opt.Cluster, region *core.RegionInfo) *operator.Operator {
 	if !l.allowBalancePeer() {
 		return nil
 	}
diff --git a/server/schedulers/balance_leader.go b/server/schedulers/balance_leader.go
index 1aa8b9cae..60f624cd3 100644
--- a/server/schedulers/balance_leader.go
+++ b/server/schedulers/balance_leader.go
@@ -22,6 +22,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/prometheus/client_golang/prometheus"
 	"go.uber.org/zap"
@@ -96,11 +97,11 @@ func (l *balanceLeaderScheduler) GetType() string {
 	return "balance-leader"
 }
 
-func (l *balanceLeaderScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (l *balanceLeaderScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return l.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (l *balanceLeaderScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (l *balanceLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(l.GetName(), "schedule").Inc()
 
 	stores := cluster.GetStores()
@@ -158,7 +159,7 @@ func (l *balanceLeaderScheduler) Schedule(cluster schedule.Cluster) []*operator.
 // transferLeaderOut transfers leader from the source store.
 // It randomly selects a health region from the source store, then picks
 // the best follower peer and transfers the leader.
-func (l *balanceLeaderScheduler) transferLeaderOut(cluster schedule.Cluster, source *core.StoreInfo) []*operator.Operator {
+func (l *balanceLeaderScheduler) transferLeaderOut(cluster opt.Cluster, source *core.StoreInfo) []*operator.Operator {
 	sourceID := source.GetID()
 	region := cluster.RandLeaderRegion(sourceID, core.HealthRegion())
 	if region == nil {
@@ -178,7 +179,7 @@ func (l *balanceLeaderScheduler) transferLeaderOut(cluster schedule.Cluster, sou
 // transferLeaderIn transfers leader to the target store.
 // It randomly selects a health region from the target store, then picks
 // the worst follower peer and transfers the leader.
-func (l *balanceLeaderScheduler) transferLeaderIn(cluster schedule.Cluster, target *core.StoreInfo) []*operator.Operator {
+func (l *balanceLeaderScheduler) transferLeaderIn(cluster opt.Cluster, target *core.StoreInfo) []*operator.Operator {
 	targetID := target.GetID()
 	region := cluster.RandFollowerRegion(targetID, core.HealthRegion())
 	if region == nil {
@@ -204,7 +205,7 @@ func (l *balanceLeaderScheduler) transferLeaderIn(cluster schedule.Cluster, targ
 // If the region is hot or the difference between the two stores is tolerable, then
 // no new operator need to be created, otherwise create an operator that transfers
 // the leader from the source store to the target store for the region.
-func (l *balanceLeaderScheduler) createOperator(cluster schedule.Cluster, region *core.RegionInfo, source, target *core.StoreInfo) []*operator.Operator {
+func (l *balanceLeaderScheduler) createOperator(cluster opt.Cluster, region *core.RegionInfo, source, target *core.StoreInfo) []*operator.Operator {
 	if cluster.IsRegionHot(region) {
 		log.Debug("region is hot region, ignore it", zap.String("scheduler", l.GetName()), zap.Uint64("region-id", region.GetID()))
 		schedulerCounter.WithLabelValues(l.GetName(), "region-hot").Inc()
diff --git a/server/schedulers/balance_region.go b/server/schedulers/balance_region.go
index 4fcef5498..fd9ff65cb 100644
--- a/server/schedulers/balance_region.go
+++ b/server/schedulers/balance_region.go
@@ -20,11 +20,12 @@ import (
 
 	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/log"
-	"github.com/pingcap/pd/server/checker"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
+	"github.com/pingcap/pd/server/schedule/checker"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/prometheus/client_golang/prometheus"
 	"go.uber.org/zap"
@@ -107,11 +108,11 @@ func (s *balanceRegionScheduler) GetType() string {
 	return "balance-region"
 }
 
-func (s *balanceRegionScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *balanceRegionScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpRegion) < cluster.GetRegionScheduleLimit()
 }
 
-func (s *balanceRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *balanceRegionScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	stores := cluster.GetStores()
 
@@ -177,7 +178,7 @@ func (s *balanceRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.
 }
 
 // transferPeer selects the best store to create a new peer to replace the old peer.
-func (s *balanceRegionScheduler) transferPeer(cluster schedule.Cluster, region *core.RegionInfo, oldPeer *metapb.Peer) *operator.Operator {
+func (s *balanceRegionScheduler) transferPeer(cluster opt.Cluster, region *core.RegionInfo, oldPeer *metapb.Peer) *operator.Operator {
 	// scoreGuard guarantees that the distinct score will not decrease.
 	stores := cluster.GetRegionStores(region)
 	sourceStoreID := oldPeer.GetStoreId()
@@ -309,7 +310,7 @@ func (h *hitsStoreBuilder) put(source, target *core.StoreInfo) {
 	}
 }
 
-func (h *hitsStoreBuilder) buildSourceFilter(scope string, cluster schedule.Cluster) filter.Filter {
+func (h *hitsStoreBuilder) buildSourceFilter(scope string, cluster opt.Cluster) filter.Filter {
 	f := filter.NewBlacklistStoreFilter(scope, filter.BlacklistSource)
 	for _, source := range cluster.GetStores() {
 		if h.filter(source, nil) {
@@ -319,7 +320,7 @@ func (h *hitsStoreBuilder) buildSourceFilter(scope string, cluster schedule.Clus
 	return f
 }
 
-func (h *hitsStoreBuilder) buildTargetFilter(scope string, cluster schedule.Cluster, source *core.StoreInfo) filter.Filter {
+func (h *hitsStoreBuilder) buildTargetFilter(scope string, cluster opt.Cluster, source *core.StoreInfo) filter.Filter {
 	f := filter.NewBlacklistStoreFilter(scope, filter.BlacklistTarget)
 	for _, target := range cluster.GetStores() {
 		if h.filter(source, target) {
diff --git a/server/schedulers/base_scheduler.go b/server/schedulers/base_scheduler.go
index b9f738af2..04148bc42 100644
--- a/server/schedulers/base_scheduler.go
+++ b/server/schedulers/base_scheduler.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/pingcap/log"
 	"github.com/pingcap/pd/server/schedule"
+	"github.com/pingcap/pd/server/schedule/opt"
 )
 
 // options for interval of schedulers
@@ -68,6 +69,6 @@ func (s *baseScheduler) GetNextInterval(interval time.Duration) time.Duration {
 	return intervalGrow(interval, MaxScheduleInterval, exponentailGrowth)
 }
 
-func (s *baseScheduler) Prepare(cluster schedule.Cluster) error { return nil }
+func (s *baseScheduler) Prepare(cluster opt.Cluster) error { return nil }
 
-func (s *baseScheduler) Cleanup(cluster schedule.Cluster) {}
+func (s *baseScheduler) Cleanup(cluster opt.Cluster) {}
diff --git a/server/schedulers/evict_leader.go b/server/schedulers/evict_leader.go
index e77161614..5a944a063 100644
--- a/server/schedulers/evict_leader.go
+++ b/server/schedulers/evict_leader.go
@@ -21,6 +21,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"github.com/pkg/errors"
 )
@@ -69,19 +70,19 @@ func (s *evictLeaderScheduler) GetType() string {
 	return "evict-leader"
 }
 
-func (s *evictLeaderScheduler) Prepare(cluster schedule.Cluster) error {
+func (s *evictLeaderScheduler) Prepare(cluster opt.Cluster) error {
 	return cluster.BlockStore(s.storeID)
 }
 
-func (s *evictLeaderScheduler) Cleanup(cluster schedule.Cluster) {
+func (s *evictLeaderScheduler) Cleanup(cluster opt.Cluster) {
 	cluster.UnblockStore(s.storeID)
 }
 
-func (s *evictLeaderScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *evictLeaderScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (s *evictLeaderScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *evictLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	region := cluster.RandLeaderRegion(s.storeID, core.HealthRegion())
 	if region == nil {
diff --git a/server/schedulers/grant_leader.go b/server/schedulers/grant_leader.go
index 44fa55baa..6662cc745 100644
--- a/server/schedulers/grant_leader.go
+++ b/server/schedulers/grant_leader.go
@@ -20,6 +20,7 @@ import (
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pkg/errors"
 )
 
@@ -61,19 +62,19 @@ func (s *grantLeaderScheduler) GetName() string {
 func (s *grantLeaderScheduler) GetType() string {
 	return "grant-leader"
 }
-func (s *grantLeaderScheduler) Prepare(cluster schedule.Cluster) error {
+func (s *grantLeaderScheduler) Prepare(cluster opt.Cluster) error {
 	return cluster.BlockStore(s.storeID)
 }
 
-func (s *grantLeaderScheduler) Cleanup(cluster schedule.Cluster) {
+func (s *grantLeaderScheduler) Cleanup(cluster opt.Cluster) {
 	cluster.UnblockStore(s.storeID)
 }
 
-func (s *grantLeaderScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *grantLeaderScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (s *grantLeaderScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *grantLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	region := cluster.RandFollowerRegion(s.storeID, core.HealthRegion())
 	if region == nil {
diff --git a/server/schedulers/hot_region.go b/server/schedulers/hot_region.go
index a3531b676..296612bc1 100644
--- a/server/schedulers/hot_region.go
+++ b/server/schedulers/hot_region.go
@@ -25,6 +25,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/statistics"
 	"go.uber.org/zap"
 )
@@ -129,25 +130,25 @@ func (h *balanceHotRegionsScheduler) GetType() string {
 	return "hot-region"
 }
 
-func (h *balanceHotRegionsScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (h *balanceHotRegionsScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return h.allowBalanceLeader(cluster) || h.allowBalanceRegion(cluster)
 }
 
-func (h *balanceHotRegionsScheduler) allowBalanceLeader(cluster schedule.Cluster) bool {
+func (h *balanceHotRegionsScheduler) allowBalanceLeader(cluster opt.Cluster) bool {
 	return h.opController.OperatorCount(operator.OpHotRegion) < minUint64(h.leaderLimit, cluster.GetHotRegionScheduleLimit()) &&
 		h.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (h *balanceHotRegionsScheduler) allowBalanceRegion(cluster schedule.Cluster) bool {
+func (h *balanceHotRegionsScheduler) allowBalanceRegion(cluster opt.Cluster) bool {
 	return h.opController.OperatorCount(operator.OpHotRegion) < minUint64(h.peerLimit, cluster.GetHotRegionScheduleLimit())
 }
 
-func (h *balanceHotRegionsScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (h *balanceHotRegionsScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(h.GetName(), "schedule").Inc()
 	return h.dispatch(h.types[h.r.Int()%len(h.types)], cluster)
 }
 
-func (h *balanceHotRegionsScheduler) dispatch(typ BalanceType, cluster schedule.Cluster) []*operator.Operator {
+func (h *balanceHotRegionsScheduler) dispatch(typ BalanceType, cluster opt.Cluster) []*operator.Operator {
 	h.Lock()
 	defer h.Unlock()
 	switch typ {
@@ -162,7 +163,7 @@ func (h *balanceHotRegionsScheduler) dispatch(typ BalanceType, cluster schedule.
 	return nil
 }
 
-func (h *balanceHotRegionsScheduler) balanceHotReadRegions(cluster schedule.Cluster) []*operator.Operator {
+func (h *balanceHotRegionsScheduler) balanceHotReadRegions(cluster opt.Cluster) []*operator.Operator {
 	// balance by leader
 	srcRegion, newLeader := h.balanceByLeader(cluster, h.stats.readStatAsLeader)
 	if srcRegion != nil {
@@ -191,7 +192,7 @@ func (h *balanceHotRegionsScheduler) balanceHotReadRegions(cluster schedule.Clus
 // balanceHotRetryLimit is the limit to retry schedule for selected balance strategy.
 const balanceHotRetryLimit = 10
 
-func (h *balanceHotRegionsScheduler) balanceHotWriteRegions(cluster schedule.Cluster) []*operator.Operator {
+func (h *balanceHotRegionsScheduler) balanceHotWriteRegions(cluster opt.Cluster) []*operator.Operator {
 	for i := 0; i < balanceHotRetryLimit; i++ {
 		switch h.r.Int() % 2 {
 		case 0:
@@ -223,7 +224,7 @@ func (h *balanceHotRegionsScheduler) balanceHotWriteRegions(cluster schedule.Clu
 	return nil
 }
 
-func calcScore(storeItems map[uint64][]*statistics.HotPeerStat, cluster schedule.Cluster, kind core.ResourceKind) statistics.StoreHotRegionsStat {
+func calcScore(storeItems map[uint64][]*statistics.HotPeerStat, cluster opt.Cluster, kind core.ResourceKind) statistics.StoreHotRegionsStat {
 	stats := make(statistics.StoreHotRegionsStat)
 	for storeID, items := range storeItems {
 		// HotDegree is the update times on the hot cache. If the heartbeat report
@@ -269,7 +270,7 @@ func calcScore(storeItems map[uint64][]*statistics.HotPeerStat, cluster schedule
 }
 
 // balanceByPeer balances the peer distribution of hot regions.
-func (h *balanceHotRegionsScheduler) balanceByPeer(cluster schedule.Cluster, storesStat statistics.StoreHotRegionsStat) (*core.RegionInfo, *metapb.Peer, *metapb.Peer) {
+func (h *balanceHotRegionsScheduler) balanceByPeer(cluster opt.Cluster, storesStat statistics.StoreHotRegionsStat) (*core.RegionInfo, *metapb.Peer, *metapb.Peer) {
 	if !h.allowBalanceRegion(cluster) {
 		return nil, nil, nil
 	}
@@ -345,7 +346,7 @@ func (h *balanceHotRegionsScheduler) balanceByPeer(cluster schedule.Cluster, sto
 }
 
 // balanceByLeader balances the leader distribution of hot regions.
-func (h *balanceHotRegionsScheduler) balanceByLeader(cluster schedule.Cluster, storesStat statistics.StoreHotRegionsStat) (*core.RegionInfo, *metapb.Peer) {
+func (h *balanceHotRegionsScheduler) balanceByLeader(cluster opt.Cluster, storesStat statistics.StoreHotRegionsStat) (*core.RegionInfo, *metapb.Peer) {
 	if !h.allowBalanceLeader(cluster) {
 		return nil, nil
 	}
diff --git a/server/schedulers/label.go b/server/schedulers/label.go
index be1cc0d54..a2c7560ba 100644
--- a/server/schedulers/label.go
+++ b/server/schedulers/label.go
@@ -60,11 +60,11 @@ func (s *labelScheduler) GetType() string {
 	return "label"
 }
 
-func (s *labelScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *labelScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (s *labelScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *labelScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	stores := cluster.GetStores()
 	rejectLeaderStores := make(map[uint64]struct{})
diff --git a/server/schedulers/random_merge.go b/server/schedulers/random_merge.go
index 7c44fb723..a225ae5a4 100644
--- a/server/schedulers/random_merge.go
+++ b/server/schedulers/random_merge.go
@@ -20,6 +20,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 )
 
@@ -59,11 +60,11 @@ func (s *randomMergeScheduler) GetType() string {
 	return "random-merge"
 }
 
-func (s *randomMergeScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *randomMergeScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpMerge) < cluster.GetMergeScheduleLimit()
 }
 
-func (s *randomMergeScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *randomMergeScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 
 	stores := cluster.GetStores()
diff --git a/server/schedulers/scatter_range.go b/server/schedulers/scatter_range.go
index 559e42a05..0999de71c 100644
--- a/server/schedulers/scatter_range.go
+++ b/server/schedulers/scatter_range.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pkg/errors"
 )
 
@@ -78,11 +79,11 @@ func (l *scatterRangeScheduler) GetType() string {
 	return "scatter-range"
 }
 
-func (l *scatterRangeScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (l *scatterRangeScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return l.opController.OperatorCount(operator.OpRange) < cluster.GetRegionScheduleLimit()
 }
 
-func (l *scatterRangeScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (l *scatterRangeScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(l.GetName(), "schedule").Inc()
 	// isolate a new cluster according to the key range
 	c := schedule.GenRangeCluster(cluster, l.startKey, l.endKey)
diff --git a/server/schedulers/shuffle_hot_region.go b/server/schedulers/shuffle_hot_region.go
index 089bf9de1..1763f6a5d 100644
--- a/server/schedulers/shuffle_hot_region.go
+++ b/server/schedulers/shuffle_hot_region.go
@@ -23,6 +23,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/statistics"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
@@ -74,19 +75,19 @@ func (s *shuffleHotRegionScheduler) GetType() string {
 	return "shuffle-hot-region"
 }
 
-func (s *shuffleHotRegionScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *shuffleHotRegionScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpHotRegion) < s.limit &&
 		s.opController.OperatorCount(operator.OpRegion) < cluster.GetRegionScheduleLimit() &&
 		s.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (s *shuffleHotRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *shuffleHotRegionScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	i := s.r.Int() % len(s.types)
 	return s.dispatch(s.types[i], cluster)
 }
 
-func (s *shuffleHotRegionScheduler) dispatch(typ BalanceType, cluster schedule.Cluster) []*operator.Operator {
+func (s *shuffleHotRegionScheduler) dispatch(typ BalanceType, cluster opt.Cluster) []*operator.Operator {
 	switch typ {
 	case hotReadRegionBalance:
 		s.stats.readStatAsLeader = calcScore(cluster.RegionReadStats(), cluster, core.LeaderKind)
@@ -98,7 +99,7 @@ func (s *shuffleHotRegionScheduler) dispatch(typ BalanceType, cluster schedule.C
 	return nil
 }
 
-func (s *shuffleHotRegionScheduler) randomSchedule(cluster schedule.Cluster, storeStats statistics.StoreHotRegionsStat) []*operator.Operator {
+func (s *shuffleHotRegionScheduler) randomSchedule(cluster opt.Cluster, storeStats statistics.StoreHotRegionsStat) []*operator.Operator {
 	for _, stats := range storeStats {
 		i := s.r.Intn(len(stats.RegionsStat))
 		r := stats.RegionsStat[i]
diff --git a/server/schedulers/shuffle_leader.go b/server/schedulers/shuffle_leader.go
index 94e05bb6a..e4c231049 100644
--- a/server/schedulers/shuffle_leader.go
+++ b/server/schedulers/shuffle_leader.go
@@ -18,6 +18,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 )
 
@@ -57,11 +58,11 @@ func (s *shuffleLeaderScheduler) GetType() string {
 	return "shuffle-leader"
 }
 
-func (s *shuffleLeaderScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *shuffleLeaderScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpLeader) < cluster.GetLeaderScheduleLimit()
 }
 
-func (s *shuffleLeaderScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *shuffleLeaderScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	// We shuffle leaders between stores by:
 	// 1. random select a valid store.
 	// 2. transfer a leader to the store.
diff --git a/server/schedulers/shuffle_region.go b/server/schedulers/shuffle_region.go
index ca57a7edf..ae333beeb 100644
--- a/server/schedulers/shuffle_region.go
+++ b/server/schedulers/shuffle_region.go
@@ -20,6 +20,7 @@ import (
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/filter"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedule/selector"
 	"go.uber.org/zap"
 )
@@ -60,11 +61,11 @@ func (s *shuffleRegionScheduler) GetType() string {
 	return "shuffle-region"
 }
 
-func (s *shuffleRegionScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *shuffleRegionScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.opController.OperatorCount(operator.OpRegion) < cluster.GetRegionScheduleLimit()
 }
 
-func (s *shuffleRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.Operator {
+func (s *shuffleRegionScheduler) Schedule(cluster opt.Cluster) []*operator.Operator {
 	schedulerCounter.WithLabelValues(s.GetName(), "schedule").Inc()
 	region, oldPeer := s.scheduleRemovePeer(cluster)
 	if region == nil {
@@ -89,7 +90,7 @@ func (s *shuffleRegionScheduler) Schedule(cluster schedule.Cluster) []*operator.
 	return []*operator.Operator{op}
 }
 
-func (s *shuffleRegionScheduler) scheduleRemovePeer(cluster schedule.Cluster) (*core.RegionInfo, *metapb.Peer) {
+func (s *shuffleRegionScheduler) scheduleRemovePeer(cluster opt.Cluster) (*core.RegionInfo, *metapb.Peer) {
 	stores := cluster.GetStores()
 
 	source := s.selector.SelectSource(cluster, stores)
@@ -110,7 +111,7 @@ func (s *shuffleRegionScheduler) scheduleRemovePeer(cluster schedule.Cluster) (*
 	return region, region.GetStorePeer(source.GetID())
 }
 
-func (s *shuffleRegionScheduler) scheduleAddPeer(cluster schedule.Cluster, filter filter.Filter) *metapb.Peer {
+func (s *shuffleRegionScheduler) scheduleAddPeer(cluster opt.Cluster, filter filter.Filter) *metapb.Peer {
 	stores := cluster.GetStores()
 
 	target := s.selector.SelectTarget(cluster, stores, filter)
diff --git a/server/schedulers/utils.go b/server/schedulers/utils.go
index 0824d197c..b6c4e5f50 100644
--- a/server/schedulers/utils.go
+++ b/server/schedulers/utils.go
@@ -19,8 +19,8 @@ import (
 	"github.com/montanaflynn/stats"
 	"github.com/pingcap/pd/pkg/cache"
 	"github.com/pingcap/pd/server/core"
-	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 )
 
 const (
@@ -54,7 +54,7 @@ func isRegionUnhealthy(region *core.RegionInfo) bool {
 	return len(region.GetDownPeers()) != 0 || len(region.GetLearners()) != 0
 }
 
-func shouldBalance(cluster schedule.Cluster, source, target *core.StoreInfo, region *core.RegionInfo, kind core.ResourceKind, opInfluence operator.OpInfluence) bool {
+func shouldBalance(cluster opt.Cluster, source, target *core.StoreInfo, region *core.RegionInfo, kind core.ResourceKind, opInfluence operator.OpInfluence) bool {
 	// The reason we use max(regionSize, averageRegionSize) to check is:
 	// 1. prevent moving small regions between stores with close scores, leading to unnecessary balance.
 	// 2. prevent moving huge regions, leading to over balance.
@@ -72,7 +72,7 @@ func shouldBalance(cluster schedule.Cluster, source, target *core.StoreInfo, reg
 		target.ResourceScore(kind, cluster.GetHighSpaceRatio(), cluster.GetLowSpaceRatio(), targetDelta)
 }
 
-func adjustTolerantRatio(cluster schedule.Cluster) float64 {
+func adjustTolerantRatio(cluster opt.Cluster) float64 {
 	tolerantSizeRatio := cluster.GetTolerantSizeRatio()
 	if tolerantSizeRatio == 0 {
 		var maxRegionCount float64
@@ -91,7 +91,7 @@ func adjustTolerantRatio(cluster schedule.Cluster) float64 {
 	return tolerantSizeRatio
 }
 
-func adjustBalanceLimit(cluster schedule.Cluster, kind core.ResourceKind) uint64 {
+func adjustBalanceLimit(cluster opt.Cluster, kind core.ResourceKind) uint64 {
 	stores := cluster.GetStores()
 	counts := make([]float64, 0, len(stores))
 	for _, s := range stores {
diff --git a/server/version.go b/server/version.go
index 9d45ef23a..e0970e0b6 100644
--- a/server/version.go
+++ b/server/version.go
@@ -23,7 +23,7 @@ import (
 // Feature supported features.
 type Feature int
 
-// Fetures list.
+// Features list.
 // The cluster provides corresponding new features if the cluster version
 // greater than or equal to the required minimum version of the feature.
 const (


Test patch:
diff --git a/server/cluster_worker_test.go b/server/cluster_worker_test.go
index 3b7e7ae84..61b4b8bba 100644
--- a/server/cluster_worker_test.go
+++ b/server/cluster_worker_test.go
@@ -88,3 +88,41 @@ func (s *testClusterWorkerSuite) TestValidRequestRegion(c *C) {
 	c.Assert(cluster.validRequestRegion(r5), IsNil)
 	cluster.stop()
 }
+
+func (s *testClusterWorkerSuite) TestAskSplit(c *C) {
+	var err error
+	var cleanup func()
+	_, s.svr, cleanup, err = NewTestServer(c)
+	defer cleanup()
+	c.Assert(err, IsNil)
+	mustWaitLeader(c, []*Server{s.svr})
+	s.grpcPDClient = testutil.MustNewGrpcClient(c, s.svr.GetAddr())
+	_, err = s.svr.bootstrapCluster(s.newBootstrapRequest(c, s.svr.clusterID, "127.0.0.1:0"))
+	c.Assert(err, IsNil)
+	s.svr.SetClusterVersion("3.1.0")
+
+	cluster := s.svr.GetRaftCluster()
+	c.Assert(cluster, NotNil)
+	regions := cluster.GetRegions()
+
+	req := &pdpb.AskSplitRequest{
+		Header: &pdpb.RequestHeader{
+			ClusterId: s.svr.ClusterID(),
+		},
+		Region: regions[0].GetMeta(),
+	}
+
+	_, err = cluster.handleAskSplit(req)
+	c.Assert(err, IsNil)
+
+	req1 := &pdpb.AskBatchSplitRequest{
+		Header: &pdpb.RequestHeader{
+			ClusterId: s.svr.ClusterID(),
+		},
+		Region:     regions[0].GetMeta(),
+		SplitCount: 10,
+	}
+
+	_, err = cluster.handleAskBatchSplit(req1)
+	c.Assert(err, IsNil)
+}
diff --git a/server/coordinator_test.go b/server/coordinator_test.go
index 218815397..27b614b37 100644
--- a/server/coordinator_test.go
+++ b/server/coordinator_test.go
@@ -34,6 +34,7 @@ import (
 	syncer "github.com/pingcap/pd/server/region_syncer"
 	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
+	"github.com/pingcap/pd/server/schedule/opt"
 	"github.com/pingcap/pd/server/schedulers"
 )
 
@@ -293,10 +294,10 @@ func (s *testCoordinatorSuite) TestCheckRegion(c *C) {
 	c.Assert(tc.addRegionStore(2, 2), IsNil)
 	c.Assert(tc.addRegionStore(1, 1), IsNil)
 	c.Assert(tc.addLeaderRegion(1, 2, 3), IsNil)
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsTrue)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsTrue)
 	waitOperator(c, co, 1)
 	testutil.CheckAddPeer(c, co.opController.GetOperator(1), operator.OpReplica, 1)
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsFalse)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsFalse)
 
 	r := tc.GetRegion(1)
 	p := &metapb.Peer{Id: 1, StoreId: 1, IsLearner: true}
@@ -305,7 +306,7 @@ func (s *testCoordinatorSuite) TestCheckRegion(c *C) {
 		core.WithPendingPeers(append(r.GetPendingPeers(), p)),
 	)
 	c.Assert(tc.putRegion(r), IsNil)
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsFalse)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsFalse)
 	co.stop()
 	co.wg.Wait()
 
@@ -320,15 +321,15 @@ func (s *testCoordinatorSuite) TestCheckRegion(c *C) {
 	c.Assert(tc.addRegionStore(2, 2), IsNil)
 	c.Assert(tc.addRegionStore(1, 1), IsNil)
 	c.Assert(tc.putRegion(r), IsNil)
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsFalse)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsFalse)
 	r = r.Clone(core.WithPendingPeers(nil))
 	c.Assert(tc.putRegion(r), IsNil)
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsTrue)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsTrue)
 	waitOperator(c, co, 1)
 	op := co.opController.GetOperator(1)
 	c.Assert(op.Len(), Equals, 1)
 	c.Assert(op.Step(0).(operator.PromoteLearner).ToStore, Equals, uint64(1))
-	c.Assert(co.checkRegion(tc.GetRegion(1)), IsFalse)
+	c.Assert(co.checkers.CheckRegion(tc.GetRegion(1)), IsFalse)
 }
 
 func (s *testCoordinatorSuite) TestReplica(c *C) {
@@ -863,7 +864,7 @@ type mockLimitScheduler struct {
 	kind    operator.OpKind
 }
 
-func (s *mockLimitScheduler) IsScheduleAllowed(cluster schedule.Cluster) bool {
+func (s *mockLimitScheduler) IsScheduleAllowed(cluster opt.Cluster) bool {
 	return s.counter.OperatorCount(s.kind) < s.limit
 }
 
diff --git a/server/namespace_test.go b/server/namespace_test.go
index e300840e2..8cb912e95 100644
--- a/server/namespace_test.go
+++ b/server/namespace_test.go
@@ -16,11 +16,11 @@ package server
 import (
 	. "github.com/pingcap/check"
 	"github.com/pingcap/pd/pkg/testutil"
-	"github.com/pingcap/pd/server/checker"
 	"github.com/pingcap/pd/server/config"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule"
+	"github.com/pingcap/pd/server/schedule/checker"
 	"github.com/pingcap/pd/server/schedule/operator"
 )
 
diff --git a/server/checker/merge_checker_test.go b/server/schedule/checker/merge_checker_test.go
similarity index 88%
rename from server/checker/merge_checker_test.go
rename to server/schedule/checker/merge_checker_test.go
index c52a7566d..4d7658fd8 100644
--- a/server/checker/merge_checker_test.go
+++ b/server/schedule/checker/merge_checker_test.go
@@ -20,11 +20,9 @@ import (
 	. "github.com/pingcap/check"
 	"github.com/pingcap/kvproto/pkg/metapb"
 	"github.com/pingcap/pd/pkg/mock/mockcluster"
-	"github.com/pingcap/pd/pkg/mock/mockhbstream"
 	"github.com/pingcap/pd/pkg/mock/mockoption"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
-	"github.com/pingcap/pd/server/schedule"
 	"github.com/pingcap/pd/server/schedule/operator"
 	"github.com/pingcap/pd/server/schedule/opt"
 )
@@ -162,7 +160,7 @@ func (s *testMergeCheckerSuite) TestBasic(c *C) {
 	c.Assert(ops[1].RegionID(), Equals, s.regions[3].GetID())
 
 	// Skip recently split regions.
-	s.mc.RecordRegionSplit(s.regions[2].GetID())
+	s.mc.RecordRegionSplit([]uint64{s.regions[2].GetID()})
 	ops = s.mc.Check(s.regions[2])
 	c.Assert(ops, IsNil)
 	ops = s.mc.Check(s.regions[3])
@@ -387,42 +385,3 @@ func (s *testMergeCheckerSuite) TestMatchPeers(c *C) {
 		},
 	})
 }
-
-func (s *testMergeCheckerSuite) TestStorelimit(c *C) {
-	oc := schedule.NewOperatorController(s.cluster, mockhbstream.NewHeartbeatStream())
-	s.cluster.ScheduleOptions.SplitMergeInterval = time.Hour
-	s.cluster.ScheduleOptions.StoreBalanceRate = 60
-	s.regions[2] = s.regions[2].Clone(
-		core.SetPeers([]*metapb.Peer{
-			{Id: 109, StoreId: 2},
-			{Id: 110, StoreId: 3},
-			{Id: 111, StoreId: 6},
-		}),
-		core.WithLeader(&metapb.Peer{Id: 109, StoreId: 2}),
-	)
-	s.cluster.PutRegion(s.regions[2])
-	ops := s.mc.Check(s.regions[2])
-	c.Assert(ops, NotNil)
-	// The size of Region is less or equal than 1MB.
-	for i := 0; i < 50; i++ {
-		c.Assert(oc.AddOperator(ops...), IsTrue)
-		for _, op := range ops {
-			c.Assert(oc.RemoveOperator(op), IsTrue)
-		}
-	}
-	s.regions[2] = s.regions[2].Clone(
-		core.SetApproximateSize(2),
-		core.SetApproximateKeys(2),
-	)
-	s.cluster.PutRegion(s.regions[2])
-	ops = s.mc.Check(s.regions[2])
-	c.Assert(ops, NotNil)
-	// The size of Region is more than 1MB but no more than 20MB.
-	for i := 0; i < 5; i++ {
-		c.Assert(oc.AddOperator(ops...), IsTrue)
-		for _, op := range ops {
-			c.Assert(oc.RemoveOperator(op), IsTrue)
-		}
-	}
-	c.Assert(oc.AddOperator(ops...), IsFalse)
-}
diff --git a/server/schedule/operator_controller_test.go b/server/schedule/operator_controller_test.go
index 195cf0c25..f7a3cd43a 100644
--- a/server/schedule/operator_controller_test.go
+++ b/server/schedule/operator_controller_test.go
@@ -27,6 +27,8 @@ import (
 	"github.com/pingcap/pd/pkg/mock/mockhbstream"
 	"github.com/pingcap/pd/pkg/mock/mockoption"
 	"github.com/pingcap/pd/server/core"
+	"github.com/pingcap/pd/server/namespace"
+	"github.com/pingcap/pd/server/schedule/checker"
 	"github.com/pingcap/pd/server/schedule/operator"
 )
 
@@ -209,7 +211,7 @@ func (t *testOperatorControllerSuite) TestPollDispatchRegion(c *C) {
 	c.Assert(next, IsFalse)
 }
 
-func (t *testOperatorControllerSuite) TestStorelimit(c *C) {
+func (t *testOperatorControllerSuite) TestStoreLimit(c *C) {
 	opt := mockoption.NewScheduleOptions()
 	tc := mockcluster.NewCluster(opt)
 	oc := NewOperatorController(tc, mockhbstream.NewHeartbeatStream())
@@ -394,3 +396,81 @@ func (t *testOperatorControllerSuite) TestDispatchUnfinishedStep(c *C) {
 	c.Assert(len(stream.MsgCh()), Equals, 4)
 	c.Assert(controller.GetOperator(1), IsNil)
 }
+
+func (t *testOperatorControllerSuite) TestStoreLimitWithMerge(c *C) {
+	cfg := mockoption.NewScheduleOptions()
+	cfg.MaxMergeRegionSize = 2
+	cfg.MaxMergeRegionKeys = 2
+	tc := mockcluster.NewCluster(cfg)
+	regions := []*core.RegionInfo{
+		newRegionInfo(1, "", "a", 1, 1, []uint64{101, 1}, []uint64{101, 1}, []uint64{102, 2}),
+		newRegionInfo(2, "a", "t", 200, 200, []uint64{104, 4}, []uint64{103, 1}, []uint64{104, 4}, []uint64{105, 5}),
+		newRegionInfo(3, "t", "x", 1, 1, []uint64{108, 6}, []uint64{106, 2}, []uint64{107, 5}, []uint64{108, 6}),
+		newRegionInfo(4, "x", "", 10, 10, []uint64{109, 4}, []uint64{109, 4}),
+	}
+
+	tc.AddLeaderStore(1, 10)
+	tc.AddLeaderStore(4, 10)
+	tc.AddLeaderStore(5, 10)
+	for _, region := range regions {
+		tc.PutRegion(region)
+	}
+
+	mc := checker.NewMergeChecker(tc, namespace.DefaultClassifier)
+	oc := NewOperatorController(tc, mockhbstream.NewHeartbeatStream())
+
+	cfg.SplitMergeInterval = time.Hour
+	cfg.StoreBalanceRate = 60
+
+	regions[2] = regions[2].Clone(
+		core.SetPeers([]*metapb.Peer{
+			{Id: 109, StoreId: 2},
+			{Id: 110, StoreId: 3},
+			{Id: 111, StoreId: 6},
+		}),
+		core.WithLeader(&metapb.Peer{Id: 109, StoreId: 2}),
+	)
+	tc.PutRegion(regions[2])
+	ops := mc.Check(regions[2])
+	c.Assert(ops, NotNil)
+	// The size of Region is less or equal than 1MB.
+	for i := 0; i < 50; i++ {
+		c.Assert(oc.AddOperator(ops...), IsTrue)
+		for _, op := range ops {
+			oc.RemoveOperator(op)
+		}
+	}
+	regions[2] = regions[2].Clone(
+		core.SetApproximateSize(2),
+		core.SetApproximateKeys(2),
+	)
+	tc.PutRegion(regions[2])
+	ops = mc.Check(regions[2])
+	c.Assert(ops, NotNil)
+	// The size of Region is more than 1MB but no more than 20MB.
+	for i := 0; i < 5; i++ {
+		c.Assert(oc.AddOperator(ops...), IsTrue)
+		for _, op := range ops {
+			oc.RemoveOperator(op)
+		}
+	}
+	c.Assert(oc.AddOperator(ops...), IsFalse)
+}
+
+func newRegionInfo(id uint64, startKey, endKey string, size, keys int64, leader []uint64, peers ...[]uint64) *core.RegionInfo {
+	var prs []*metapb.Peer
+	for _, peer := range peers {
+		prs = append(prs, &metapb.Peer{Id: peer[0], StoreId: peer[1]})
+	}
+	return core.NewRegionInfo(
+		&metapb.Region{
+			Id:       id,
+			StartKey: []byte(startKey),
+			EndKey:   []byte(endKey),
+			Peers:    prs,
+		},
+		&metapb.Peer{Id: leader[0], StoreId: leader[1]},
+		core.SetApproximateSize(size),
+		core.SetApproximateKeys(keys),
+	)
+}
diff --git a/server/schedulers/balance_test.go b/server/schedulers/balance_test.go
index 9f43d314d..521340089 100644
--- a/server/schedulers/balance_test.go
+++ b/server/schedulers/balance_test.go
@@ -25,10 +25,10 @@ import (
 	"github.com/pingcap/pd/pkg/mock/mockhbstream"
 	"github.com/pingcap/pd/pkg/mock/mockoption"
 	"github.com/pingcap/pd/pkg/testutil"
-	"github.com/pingcap/pd/server/checker"
 	"github.com/pingcap/pd/server/core"
 	"github.com/pingcap/pd/server/namespace"
 	"github.com/pingcap/pd/server/schedule"
+	"github.com/pingcap/pd/server/schedule/checker"
 	"github.com/pingcap/pd/server/schedule/operator"
 	"github.com/pingcap/pd/server/statistics"
 )